feat(highlight): preset overhaul, categories, stats, theme colours, per-host profiles
Rules & engine: - 22 presets (was 11): split status into okstate/warnstate/badstate, add delop, createop, danger, secret, level, exitcode, percent, http; status words are case-insensitive and cover the ✓ ✔ ✅ ✗ ✘ ✖ ❌ ⚠ symbol set - value bands: the first number in a match picks the colour (percent: <20% red / 20-50% yellow / 50-80% light green / >=80% green) - optional per-rule `caseInsensitive`, `category`, `bands`; load-time `refreshBuiltinRules` upgrades untouched built-in patterns in place Settings page: - three-way master switch `highlightMode` (all / basic / off); `basic` runs only the five safety+status rules and `off` empties the rule set rather than bypassing HighlightStream (which would drop the held tail) - category column + grouping (`highlightGroupByCategory`), per-rule hit/duration stats (`highlightStats`, opt-in sink, snapshot once a second), theme-following colours (`highlightThemeColors`, hue-bucket mapping onto the ANSI palette), import/export JSON envelope, live preview through the real engine - named rule subsets bound per host (`highlightPerHost` + `highlightProfiles` + `SshConnection.highlightProfileId`); empty ruleIds = every rule Tests: new tests/hl-rules.mjs (word boundaries, case flag, negative words, bands, import/export, preview, basic mode, categories, stats, theme colours, profiles) + profile round-trip in tests/settings-store.mjs
This commit is contained in:
1 parent
41b1cb1b25
commit
e08e1cfec4
39 files changed
+3164
-89
No files matched your search
@@ -35,6 +35,8 @@ export interface SshConnection {
|
||||
createdAt: number
|
||||
lastConnectedAt?: number
|
||||
savedAuth: SshSavedAuthFlags
|
||||
/** highlight profile bound to this host; absent = run every rule */
|
||||
highlightProfileId?: string
|
||||
}
|
||||
|
||||
/** Renderer-supplied connection data on save; secrets here are plaintext on purpose (one-way, encrypted before persisting). */
|
||||
@@ -50,6 +52,8 @@ export interface SshConnectionInput {
|
||||
askPassphraseAtConnect: boolean
|
||||
keyPath?: string
|
||||
keepaliveIntervalSec: number
|
||||
/** highlight profile bound to this host; absent = run every rule */
|
||||
highlightProfileId?: string
|
||||
/** plain password to store; omit to keep the previously stored one */
|
||||
password?: string
|
||||
/** private key content to store inline; omit to keep the previously stored one */
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { isHighlightCategory, type HighlightRule } from './settings'
|
||||
|
||||
/**
|
||||
* Import / export of highlight rule sets — share a set with a colleague, or keep
|
||||
* a copy before experimenting.
|
||||
*
|
||||
* The envelope carries a kind + version so pasting the wrong JSON (a layout, a
|
||||
* connection list) fails loudly instead of importing nonsense. Validation here
|
||||
* stays deliberately light: it only rejects what cannot be a rule at all
|
||||
* (unparseable JSON, no `rules` array, an unusable regex). Everything else is
|
||||
* repaired by the settings store on save, which already owns that job.
|
||||
*/
|
||||
|
||||
export const HIGHLIGHT_FILE_KIND = 'openterminal.highlight-rules'
|
||||
export const HIGHLIGHT_FILE_VERSION = 1
|
||||
|
||||
/** Default colour for an imported rule that lost its own. */
|
||||
const FALLBACK_FG = '#3fb950'
|
||||
|
||||
const HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
|
||||
|
||||
export interface HighlightFile {
|
||||
kind: string
|
||||
version: number
|
||||
exportedAt: string
|
||||
rules: HighlightRule[]
|
||||
}
|
||||
|
||||
/** Reasons a paste can be refused, as codes the UI turns into a message. */
|
||||
export type ImportError = 'not-json' | 'no-rules' | 'wrong-kind'
|
||||
|
||||
export interface ParsedHighlightFile {
|
||||
rules: HighlightRule[]
|
||||
/** One line per rule that was dropped, for the "N skipped" note. */
|
||||
warnings: string[]
|
||||
error?: ImportError
|
||||
}
|
||||
|
||||
/** Serialize a rule set into the shareable envelope. */
|
||||
export function exportHighlightRules(rules: HighlightRule[]): string {
|
||||
const file: HighlightFile = {
|
||||
kind: HIGHLIGHT_FILE_KIND,
|
||||
version: HIGHLIGHT_FILE_VERSION,
|
||||
exportedAt: new Date().toISOString(),
|
||||
rules
|
||||
}
|
||||
return JSON.stringify(file, null, 2)
|
||||
}
|
||||
|
||||
/** Id for an imported rule, so it can never collide with an existing one. */
|
||||
function freshId(): string {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
return `imported-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
||||
}
|
||||
|
||||
/** Accepts the exported envelope, or a bare array of rules. */
|
||||
export function parseHighlightRules(text: string): ParsedHighlightFile {
|
||||
let raw: unknown
|
||||
try {
|
||||
raw = JSON.parse(text)
|
||||
} catch {
|
||||
return { rules: [], warnings: [], error: 'not-json' }
|
||||
}
|
||||
|
||||
let list: unknown[]
|
||||
if (Array.isArray(raw)) {
|
||||
list = raw
|
||||
} else if (raw !== null && typeof raw === 'object' && Array.isArray((raw as { rules?: unknown }).rules)) {
|
||||
const kind = (raw as { kind?: unknown }).kind
|
||||
if (typeof kind === 'string' && kind !== HIGHLIGHT_FILE_KIND) {
|
||||
return { rules: [], warnings: [], error: 'wrong-kind' }
|
||||
}
|
||||
list = (raw as { rules: unknown[] }).rules
|
||||
} else {
|
||||
return { rules: [], warnings: [], error: 'no-rules' }
|
||||
}
|
||||
|
||||
const rules: HighlightRule[] = []
|
||||
const warnings: string[] = []
|
||||
list.forEach((entry, index) => {
|
||||
const rule = coerceImported(entry, index, warnings)
|
||||
if (rule) rules.push(rule)
|
||||
})
|
||||
return { rules, warnings }
|
||||
}
|
||||
|
||||
/** Keep the fields we understand; the store repairs the rest on save. */
|
||||
function coerceImported(value: unknown, index: number, warnings: string[]): HighlightRule | null {
|
||||
if (value === null || typeof value !== 'object') {
|
||||
warnings.push(`#${index + 1}: not an object`)
|
||||
return null
|
||||
}
|
||||
const raw = value as Record<string, unknown>
|
||||
const pattern = typeof raw.pattern === 'string' ? raw.pattern.trim() : ''
|
||||
if (pattern === '') {
|
||||
warnings.push(`#${index + 1}: no pattern`)
|
||||
return null
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new RegExp(pattern)
|
||||
} catch {
|
||||
warnings.push(`#${index + 1}: invalid regex`)
|
||||
return null
|
||||
}
|
||||
|
||||
const color = raw.color !== null && typeof raw.color === 'object' ? (raw.color as Record<string, unknown>) : null
|
||||
const fg = typeof color?.fg === 'string' && HEX.test(color.fg) ? color.fg : FALLBACK_FG
|
||||
const bg = typeof color?.bg === 'string' && HEX.test(color.bg) ? color.bg : undefined
|
||||
const rawPriority = typeof raw.priority === 'number' ? raw.priority : Number(raw.priority)
|
||||
|
||||
return {
|
||||
id: freshId(),
|
||||
pattern,
|
||||
enabled: raw.enabled !== false,
|
||||
priority: Number.isFinite(rawPriority) ? Math.min(100, Math.max(1, Math.round(rawPriority))) : 10,
|
||||
color: { fg, ...(bg ? { bg } : {}) },
|
||||
...(Array.isArray(raw.bands) ? { bands: raw.bands as HighlightRule['bands'] } : {}),
|
||||
...(raw.caseInsensitive === true ? { caseInsensitive: true } : {}),
|
||||
...(raw.basic === true ? { basic: true } : {}),
|
||||
...(isHighlightCategory(raw.category) ? { category: raw.category } : {}),
|
||||
...(typeof raw.note === 'string' && raw.note.trim() !== '' ? { note: raw.note.trim() } : {}),
|
||||
builtin: false
|
||||
}
|
||||
}
|
||||
|
||||
/** Merge imported rules into the current set: replace everything, or append. */
|
||||
export function mergeRules(
|
||||
existing: HighlightRule[],
|
||||
incoming: HighlightRule[],
|
||||
mode: 'replace' | 'append'
|
||||
): HighlightRule[] {
|
||||
if (mode === 'replace') return incoming
|
||||
const used = new Set(existing.map((rule) => rule.id))
|
||||
const appended = incoming.map((rule) => {
|
||||
if (!used.has(rule.id)) {
|
||||
used.add(rule.id)
|
||||
return rule
|
||||
}
|
||||
const id = freshId()
|
||||
used.add(id)
|
||||
return { ...rule, id }
|
||||
})
|
||||
return [...existing, ...appended]
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { HighlightProfile, HighlightRule } from './settings'
|
||||
|
||||
/**
|
||||
* Per-host highlight profiles.
|
||||
*
|
||||
* A profile is a named subset of the rule list, and an SSH connection can be
|
||||
* bound to one so a production host runs a quieter set than a local shell. The
|
||||
* master switch (`terminal.highlightPerHost`) decides whether any of this is
|
||||
* consulted: with it off, `rulesForProfile` is never called and every session
|
||||
* runs the full set exactly as before.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validate a profile list coming from disk. Ids and names are required, rule
|
||||
* references that no longer exist are dropped (deleting a rule must not leave a
|
||||
* profile pointing at nothing), and duplicate ids are refused because the UI
|
||||
* keys rows by them.
|
||||
*/
|
||||
export function sanitizeProfiles(
|
||||
value: unknown,
|
||||
knownRuleIds: Set<string>,
|
||||
warn: (message: string) => void
|
||||
): HighlightProfile[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
const profiles: HighlightProfile[] = []
|
||||
const seen = new Set<string>()
|
||||
value.forEach((entry, index) => {
|
||||
if (entry === null || typeof entry !== 'object') {
|
||||
warn(`highlightProfiles[${index}]: not an object — skipped`)
|
||||
return
|
||||
}
|
||||
const raw = entry as Record<string, unknown>
|
||||
if (typeof raw.id !== 'string' || raw.id === '' || typeof raw.name !== 'string') {
|
||||
warn(`highlightProfiles[${index}]: missing id/name — skipped`)
|
||||
return
|
||||
}
|
||||
if (seen.has(raw.id)) {
|
||||
warn(`highlightProfiles[${index}]: duplicate id — skipped`)
|
||||
return
|
||||
}
|
||||
seen.add(raw.id)
|
||||
|
||||
const ruleIds: string[] = []
|
||||
if (Array.isArray(raw.ruleIds)) {
|
||||
for (const id of raw.ruleIds) {
|
||||
if (typeof id !== 'string') continue
|
||||
if (!knownRuleIds.has(id)) {
|
||||
warn(`highlightProfiles[${index}]: unknown rule "${id}" dropped`)
|
||||
continue
|
||||
}
|
||||
if (!ruleIds.includes(id)) ruleIds.push(id)
|
||||
}
|
||||
}
|
||||
profiles.push({ id: raw.id, name: raw.name, ruleIds })
|
||||
})
|
||||
return profiles
|
||||
}
|
||||
|
||||
/**
|
||||
* The rules a session runs: the bound profile's subset, or everything when no
|
||||
* profile is bound, the profile is gone, or it selects nothing.
|
||||
*/
|
||||
export function rulesForProfile(
|
||||
rules: HighlightRule[],
|
||||
profileId: string | undefined,
|
||||
profiles: HighlightProfile[]
|
||||
): HighlightRule[] {
|
||||
if (profileId === undefined) return rules
|
||||
const profile = profiles.find((candidate) => candidate.id === profileId)
|
||||
if (profile === undefined || profile.ruleIds.length === 0) return rules
|
||||
const wanted = new Set(profile.ruleIds)
|
||||
return rules.filter((rule) => wanted.has(rule.id))
|
||||
}
|
||||
|
||||
/** Rules a profile leaves out — shown as a hint in the editor. */
|
||||
export function excludedByProfile(
|
||||
rules: HighlightRule[],
|
||||
profile: HighlightProfile | undefined
|
||||
): HighlightRule[] {
|
||||
if (profile === undefined) return []
|
||||
const wanted = new Set(profile.ruleIds)
|
||||
return rules.filter((rule) => !wanted.has(rule.id))
|
||||
}
|
||||
@@ -117,6 +117,72 @@ const settings: Record<string, string> = {
|
||||
'settings.highlight.palette': 'Palette',
|
||||
'settings.highlight.note': 'Note',
|
||||
'settings.highlight.notePlaceholder': 'Rule description (optional)',
|
||||
'settings.highlight.caseInsensitive': 'Ignore case',
|
||||
'settings.highlight.caseInsensitiveHint': 'letters match in any case (OK also matches ok)',
|
||||
'settings.highlight.bands': 'Value bands',
|
||||
'settings.highlight.bandsHint':
|
||||
'Colour by the first number in the match; no number (or below the lowest band) keeps the foreground colour',
|
||||
'settings.highlight.bandMin': 'Min',
|
||||
'settings.highlight.addBand': '+ Add band',
|
||||
'settings.highlight.removeBand': 'Remove band',
|
||||
'settings.highlight.importExport': 'Import / export',
|
||||
'settings.highlight.importExportTitle': 'Import / export highlight rules',
|
||||
'settings.highlight.exportSection': 'Export — copy or save this JSON to share the set',
|
||||
'settings.highlight.copy': 'Copy',
|
||||
'settings.highlight.copied': 'Copied',
|
||||
'settings.highlight.download': 'Save as file',
|
||||
'settings.highlight.importSection': 'Import — paste JSON, or pick a .json file',
|
||||
'settings.highlight.chooseFile': 'Choose file',
|
||||
'settings.highlight.importMode': 'Mode',
|
||||
'settings.highlight.importReplace': 'Replace current rules',
|
||||
'settings.highlight.importAppend': 'Append to current rules',
|
||||
'settings.highlight.importOk': 'Import',
|
||||
'settings.highlight.importEmpty': 'No usable rules found',
|
||||
'settings.highlight.importBadJson': 'Not valid JSON',
|
||||
'settings.highlight.importWrongKind': 'This JSON is not a highlight-rule file',
|
||||
'settings.highlight.importSkipped': 'Skipped {n}: {list}',
|
||||
'settings.highlight.imported': 'Imported {n} rule(s)',
|
||||
'settings.highlight.testText': 'Test text',
|
||||
'settings.highlight.testHint': 'see the effect as you edit the pattern (first 2000 characters only)',
|
||||
'settings.highlight.mode': 'Highlighting',
|
||||
'settings.highlight.modeAll': 'All',
|
||||
'settings.highlight.modeBasic': 'Basic only',
|
||||
'settings.highlight.modeOff': 'Off',
|
||||
'settings.highlight.modeDesc':
|
||||
'Basic only: keeps just the key rules (errors, success, warnings, dangerous commands, secrets) — cheaper and quieter. Off: no colour injection at all (rules and colours are kept).',
|
||||
'settings.highlight.category': 'Category',
|
||||
'settings.highlight.groupByCategory': 'Group by category',
|
||||
'settings.highlight.category.safety': 'Safety',
|
||||
'settings.highlight.category.status': 'Status',
|
||||
'settings.highlight.category.file': 'Files',
|
||||
'settings.highlight.category.net': 'Network',
|
||||
'settings.highlight.category.text': 'Text',
|
||||
'settings.highlight.category.metric': 'Metrics',
|
||||
'settings.highlight.category.none': 'Uncategorised',
|
||||
'settings.highlight.stats': 'Count hits and time',
|
||||
'settings.highlight.statsHint':
|
||||
'Collect per-rule hit counts and durations for this page (zero cost while off)',
|
||||
'settings.highlight.statsHits': 'Hits',
|
||||
'settings.highlight.statsMs': 'Time',
|
||||
'settings.highlight.statsReset': 'Reset stats',
|
||||
'settings.highlight.statsEmpty': 'No stats yet — run some output in a terminal first',
|
||||
'settings.highlight.themeColors': 'Follow theme colours',
|
||||
'settings.highlight.themeColorsHint':
|
||||
'Map highlight colours onto the active terminal palette by hue (neutral greys are kept as-is)',
|
||||
'settings.highlight.perHost': 'Per-host rule sets',
|
||||
'settings.highlight.perHostHint':
|
||||
'When on, an SSH connection bound to a rule set runs only that subset; when off every session uses all rules',
|
||||
'settings.highlight.profiles': 'Host rule sets',
|
||||
'settings.highlight.profilesHint':
|
||||
'Save a subset of the rules under a name, then bind it to a host in the connection editor',
|
||||
'settings.highlight.profileNew': '+ New rule set',
|
||||
'settings.highlight.profileEdit': 'Edit rule set',
|
||||
'settings.highlight.profileName': 'Name',
|
||||
'settings.highlight.profileNamePlaceholder': 'e.g. production (errors only)',
|
||||
'settings.highlight.profileRules': 'Included rules',
|
||||
'settings.highlight.profileRulesHint': 'Unchecked rules stay off on that host',
|
||||
'settings.highlight.profileEmpty': 'No rule sets yet',
|
||||
'settings.highlight.profileDeleteTitle': 'Delete rule set',
|
||||
'settings.highlight.actions': 'Actions',
|
||||
'settings.highlight.addRule': '+ Add rule',
|
||||
'settings.highlight.empty': 'No highlight rules yet — click "+ Add rule" to create one',
|
||||
@@ -129,8 +195,18 @@ const settings: Record<string, string> = {
|
||||
'settings.highlight.builtin.perm': 'Linux permissions and user/group',
|
||||
'settings.highlight.builtin.path': 'Linux file paths',
|
||||
'settings.highlight.builtin.shellkw': 'Shell keywords and control flow',
|
||||
'settings.highlight.builtin.danger': 'Dangerous / destructive commands',
|
||||
'settings.highlight.builtin.secret': 'Possible secret / token',
|
||||
'settings.highlight.builtin.loglevel': 'Log levels',
|
||||
'settings.highlight.builtin.rootat': 'Root prompt (root@)',
|
||||
'settings.highlight.builtin.exitcode': 'Exit codes',
|
||||
'settings.highlight.builtin.http': 'HTTP status codes',
|
||||
'settings.highlight.builtin.latency': 'Durations (milliseconds)',
|
||||
'settings.highlight.builtin.delop': 'Delete / move / overwrite operations',
|
||||
'settings.highlight.builtin.createop': 'Create / add operations',
|
||||
'settings.highlight.builtin.okstate': 'Success states',
|
||||
'settings.highlight.builtin.badstate': 'Error/warning states',
|
||||
'settings.highlight.builtin.warnstate': 'Warnings and TODO markers',
|
||||
'settings.highlight.builtin.badstate': 'Error / failure states',
|
||||
'settings.highlight.builtin.quoted': 'Strings and quotes',
|
||||
'settings.highlight.builtin.envvar': 'Environment variables and arguments',
|
||||
'settings.highlight.builtin.ipv4': 'Network and IP addresses',
|
||||
|
||||
@@ -43,6 +43,8 @@ const ssh: Record<string, string> = {
|
||||
'ssh.dialog.keepalive': 'Keepalive interval (seconds)',
|
||||
'ssh.dialog.keepaliveTooltip': '0 disables keepalive',
|
||||
'ssh.dialog.keepaliveMin': 'Cannot be negative',
|
||||
'ssh.dialog.highlightProfile': 'Highlight rule set',
|
||||
'ssh.dialog.highlightProfileDefault': 'Default (all rules)',
|
||||
|
||||
// Connection sidebar
|
||||
'ssh.sidebar.defaultGroup': 'Default',
|
||||
|
||||
@@ -114,6 +114,70 @@ const settings: Record<string, string> = {
|
||||
'settings.highlight.palette': 'パレット',
|
||||
'settings.highlight.note': 'メモ',
|
||||
'settings.highlight.notePlaceholder': 'ルールの説明(任意)',
|
||||
'settings.highlight.caseInsensitive': '大文字小文字を無視',
|
||||
'settings.highlight.caseInsensitiveHint': '大文字小文字を区別しない(OK は ok にも一致)',
|
||||
'settings.highlight.bands': '数値の段階',
|
||||
'settings.highlight.bandsHint': 'マッチ内の最初の数値で色を決めます(数値なし・最小段階未満は前景色)',
|
||||
'settings.highlight.bandMin': '下限',
|
||||
'settings.highlight.addBand': '+ 段階を追加',
|
||||
'settings.highlight.removeBand': '段階を削除',
|
||||
'settings.highlight.importExport': 'インポート / エクスポート',
|
||||
'settings.highlight.importExportTitle': 'ハイライトルールのインポート / エクスポート',
|
||||
'settings.highlight.exportSection': 'エクスポート(下の JSON をコピー/保存して共有)',
|
||||
'settings.highlight.copy': 'コピー',
|
||||
'settings.highlight.copied': 'コピーしました',
|
||||
'settings.highlight.download': 'ファイルとして保存',
|
||||
'settings.highlight.importSection': 'インポート(JSON を貼り付け、または .json を選択)',
|
||||
'settings.highlight.chooseFile': 'ファイルを選択',
|
||||
'settings.highlight.importMode': '取り込み方法',
|
||||
'settings.highlight.importReplace': '現在のルールを置き換える',
|
||||
'settings.highlight.importAppend': '現在のルールに追加する',
|
||||
'settings.highlight.importOk': 'インポート',
|
||||
'settings.highlight.importEmpty': '有効なルールが見つかりません',
|
||||
'settings.highlight.importBadJson': '有効な JSON ではありません',
|
||||
'settings.highlight.importWrongKind': 'この JSON はハイライトルールのファイルではありません',
|
||||
'settings.highlight.importSkipped': '{n} 件をスキップ:{list}',
|
||||
'settings.highlight.imported': '{n} 件のルールを取り込みました',
|
||||
'settings.highlight.testText': 'テスト文字列',
|
||||
'settings.highlight.testHint': '正規表現を編集すると即座に反映されます(先頭 2000 文字のみ)',
|
||||
'settings.highlight.mode': 'ハイライト',
|
||||
'settings.highlight.modeAll': 'すべて',
|
||||
'settings.highlight.modeBasic': '基本のみ',
|
||||
'settings.highlight.modeOff': 'オフ',
|
||||
'settings.highlight.modeDesc':
|
||||
'基本のみ:エラー/成功/警告/危険なコマンド/シークレットなど重要なルールだけを適用(軽量・低ノイズ)。オフ:色をまったく注入しません(ルールと色は保持)。',
|
||||
'settings.highlight.category': '分類',
|
||||
'settings.highlight.groupByCategory': '分類でグループ化',
|
||||
'settings.highlight.category.safety': '安全',
|
||||
'settings.highlight.category.status': 'ステータス',
|
||||
'settings.highlight.category.file': 'ファイル',
|
||||
'settings.highlight.category.net': 'ネットワーク',
|
||||
'settings.highlight.category.text': 'テキスト',
|
||||
'settings.highlight.category.metric': '数値',
|
||||
'settings.highlight.category.none': '未分類',
|
||||
'settings.highlight.stats': 'ヒット数と所要時間を計測',
|
||||
'settings.highlight.statsHint': 'ルールごとのヒット数と所要時間を計測します(オフのときはコストゼロ)',
|
||||
'settings.highlight.statsHits': 'ヒット',
|
||||
'settings.highlight.statsMs': '所要時間',
|
||||
'settings.highlight.statsReset': '統計をリセット',
|
||||
'settings.highlight.statsEmpty': 'まだ統計がありません。端末で出力を流してください',
|
||||
'settings.highlight.themeColors': 'テーマの配色に合わせる',
|
||||
'settings.highlight.themeColorsHint':
|
||||
'色相でハイライト色を現在の端末パレットに割り当てます(無彩色はそのまま)',
|
||||
'settings.highlight.perHost': 'ホスト別ルールセット',
|
||||
'settings.highlight.perHostHint':
|
||||
'オンにすると、ルールセットを紐付けた SSH 接続はそのサブセットのみ適用されます。オフでは全セッションが全ルールを使います',
|
||||
'settings.highlight.profiles': 'ホスト用ルールセット',
|
||||
'settings.highlight.profilesHint':
|
||||
'ルールのサブセットに名前を付けて保存し、接続の編集でホストに紐付けます',
|
||||
'settings.highlight.profileNew': '+ ルールセットを作成',
|
||||
'settings.highlight.profileEdit': 'ルールセットを編集',
|
||||
'settings.highlight.profileName': '名前',
|
||||
'settings.highlight.profileNamePlaceholder': '例:本番機(エラーのみ)',
|
||||
'settings.highlight.profileRules': '含めるルール',
|
||||
'settings.highlight.profileRulesHint': 'チェックを外したルールはそのホストでは効きません',
|
||||
'settings.highlight.profileEmpty': 'ルールセットがまだありません',
|
||||
'settings.highlight.profileDeleteTitle': 'ルールセットを削除',
|
||||
'settings.highlight.actions': '操作',
|
||||
'settings.highlight.addRule': '+ ルールを追加',
|
||||
'settings.highlight.empty': 'ハイライトルールがありません。「+ ルールを追加」から作成できます',
|
||||
@@ -126,8 +190,18 @@ const settings: Record<string, string> = {
|
||||
'settings.highlight.builtin.perm': 'Linux のパーミッションとユーザー',
|
||||
'settings.highlight.builtin.path': 'Linux のファイルパス',
|
||||
'settings.highlight.builtin.shellkw': 'Shell のキーワードと制御構文',
|
||||
'settings.highlight.builtin.danger': '危険・破壊的なコマンド',
|
||||
'settings.highlight.builtin.secret': 'シークレット・トークンの疑い',
|
||||
'settings.highlight.builtin.loglevel': 'ログレベル',
|
||||
'settings.highlight.builtin.rootat': 'root プロンプト(root@)',
|
||||
'settings.highlight.builtin.exitcode': '終了コード',
|
||||
'settings.highlight.builtin.http': 'HTTP ステータスコード',
|
||||
'settings.highlight.builtin.latency': '所要時間(ミリ秒)',
|
||||
'settings.highlight.builtin.delop': '削除・移動・上書きの操作',
|
||||
'settings.highlight.builtin.createop': '作成・追加の操作',
|
||||
'settings.highlight.builtin.okstate': '成功ステータス',
|
||||
'settings.highlight.builtin.badstate': 'エラー・警告ステータス',
|
||||
'settings.highlight.builtin.warnstate': '警告と TODO マーカー',
|
||||
'settings.highlight.builtin.badstate': 'エラー・失敗ステータス',
|
||||
'settings.highlight.builtin.quoted': '文字列と引用符',
|
||||
'settings.highlight.builtin.envvar': '環境変数と引数',
|
||||
'settings.highlight.builtin.ipv4': 'ネットワークと IP アドレス',
|
||||
|
||||
@@ -45,6 +45,8 @@ const ssh: Record<string, string> = {
|
||||
'ssh.dialog.keepalive': 'Keepalive 間隔(秒)',
|
||||
'ssh.dialog.keepaliveTooltip': '0 で keepalive を無効にします',
|
||||
'ssh.dialog.keepaliveMin': '負の値は指定できません',
|
||||
'ssh.dialog.highlightProfile': 'ハイライトルールセット',
|
||||
'ssh.dialog.highlightProfileDefault': '既定(すべてのルール)',
|
||||
|
||||
// 接続サイドバー
|
||||
'ssh.sidebar.defaultGroup': 'デフォルト',
|
||||
|
||||
@@ -111,6 +111,66 @@ const settings: Record<string, string> = {
|
||||
'settings.highlight.palette': '色板',
|
||||
'settings.highlight.note': '备注',
|
||||
'settings.highlight.notePlaceholder': '规则说明(可选)',
|
||||
'settings.highlight.caseInsensitive': '忽略大小写',
|
||||
'settings.highlight.caseInsensitiveHint': '字母不区分大小写(OK 也能匹配 ok)',
|
||||
'settings.highlight.bands': '数值分级',
|
||||
'settings.highlight.bandsHint': '按匹配到的第一个数字取色;没有数字或低于最小分级时用前景色',
|
||||
'settings.highlight.bandMin': '下限',
|
||||
'settings.highlight.addBand': '+ 添加分级',
|
||||
'settings.highlight.removeBand': '删除分级',
|
||||
'settings.highlight.importExport': '导入 / 导出',
|
||||
'settings.highlight.importExportTitle': '导入 / 导出高亮规则',
|
||||
'settings.highlight.exportSection': '导出(复制或保存下面的 JSON 分享给别人)',
|
||||
'settings.highlight.copy': '复制',
|
||||
'settings.highlight.copied': '已复制',
|
||||
'settings.highlight.download': '保存为文件',
|
||||
'settings.highlight.importSection': '导入(粘贴 JSON,或选择 .json 文件)',
|
||||
'settings.highlight.chooseFile': '选择文件',
|
||||
'settings.highlight.importMode': '导入方式',
|
||||
'settings.highlight.importReplace': '替换现有规则',
|
||||
'settings.highlight.importAppend': '追加到现有规则',
|
||||
'settings.highlight.importOk': '导入',
|
||||
'settings.highlight.importEmpty': '没有解析到可用规则',
|
||||
'settings.highlight.importBadJson': '不是有效的 JSON',
|
||||
'settings.highlight.importWrongKind': '这份 JSON 不是高亮规则文件',
|
||||
'settings.highlight.importSkipped': '已跳过 {n} 条:{list}',
|
||||
'settings.highlight.imported': '已导入 {n} 条规则',
|
||||
'settings.highlight.testText': '测试文本',
|
||||
'settings.highlight.testHint': '改完正则立刻看到效果(仅预览前 2000 字符)',
|
||||
'settings.highlight.mode': '高亮模式',
|
||||
'settings.highlight.modeAll': '全部',
|
||||
'settings.highlight.modeBasic': '仅基础',
|
||||
'settings.highlight.modeOff': '关闭',
|
||||
'settings.highlight.modeDesc': '仅基础:只保留错误/成功/警告/危险命令/密钥等关键规则,开销更小;关闭:完全不注入颜色(规则与颜色都保留)',
|
||||
'settings.highlight.category': '分类',
|
||||
'settings.highlight.groupByCategory': '按分类分组',
|
||||
'settings.highlight.category.safety': '安全',
|
||||
'settings.highlight.category.status': '状态',
|
||||
'settings.highlight.category.file': '文件',
|
||||
'settings.highlight.category.net': '网络',
|
||||
'settings.highlight.category.text': '文本',
|
||||
'settings.highlight.category.metric': '数值',
|
||||
'settings.highlight.category.none': '未分类',
|
||||
'settings.highlight.stats': '统计命中与耗时',
|
||||
'settings.highlight.statsHint': '开启后统计每条规则的命中次数与耗时(设置页显示,关闭时零开销)',
|
||||
'settings.highlight.statsHits': '命中',
|
||||
'settings.highlight.statsMs': '耗时',
|
||||
'settings.highlight.statsReset': '重置统计',
|
||||
'settings.highlight.statsEmpty': '还没有统计数据,先去终端里跑点输出',
|
||||
'settings.highlight.themeColors': '跟随主题配色',
|
||||
'settings.highlight.themeColorsHint': '按色相把高亮颜色映射到当前终端主题的调色板(灰阶色保持原样)',
|
||||
'settings.highlight.perHost': '按主机规则集',
|
||||
'settings.highlight.perHostHint': '开启后,绑定了规则集的 SSH 连接只跑该子集;关闭时所有会话都用全部规则',
|
||||
'settings.highlight.profiles': '主机规则集',
|
||||
'settings.highlight.profilesHint': '把规则的子集存成命名规则集,在「连接编辑」里绑定给某台主机',
|
||||
'settings.highlight.profileNew': '+ 新建规则集',
|
||||
'settings.highlight.profileEdit': '编辑规则集',
|
||||
'settings.highlight.profileName': '名称',
|
||||
'settings.highlight.profileNamePlaceholder': '例如:生产机(只看错误)',
|
||||
'settings.highlight.profileRules': '包含的规则',
|
||||
'settings.highlight.profileRulesHint': '未勾选的规则在这台主机上不生效',
|
||||
'settings.highlight.profileEmpty': '尚未创建规则集',
|
||||
'settings.highlight.profileDeleteTitle': '删除规则集',
|
||||
'settings.highlight.actions': '操作',
|
||||
'settings.highlight.addRule': '+ 新增规则',
|
||||
'settings.highlight.empty': '暂无高亮规则,点击「+ 新增规则」创建',
|
||||
@@ -123,8 +183,18 @@ const settings: Record<string, string> = {
|
||||
'settings.highlight.builtin.perm': 'Linux 权限与用户',
|
||||
'settings.highlight.builtin.path': 'Linux 文件路径',
|
||||
'settings.highlight.builtin.shellkw': 'Shell 关键字与流程',
|
||||
'settings.highlight.builtin.danger': '危险/破坏性命令',
|
||||
'settings.highlight.builtin.secret': '疑似密钥/令牌',
|
||||
'settings.highlight.builtin.loglevel': '日志级别',
|
||||
'settings.highlight.builtin.rootat': '管理员提示符 root@',
|
||||
'settings.highlight.builtin.exitcode': '退出码',
|
||||
'settings.highlight.builtin.http': 'HTTP 状态码',
|
||||
'settings.highlight.builtin.latency': '耗时(毫秒)',
|
||||
'settings.highlight.builtin.delop': '删除/移动/覆盖操作',
|
||||
'settings.highlight.builtin.createop': '新建/创建操作',
|
||||
'settings.highlight.builtin.okstate': '成功状态',
|
||||
'settings.highlight.builtin.badstate': '错误/警告状态',
|
||||
'settings.highlight.builtin.warnstate': '警告与待办标记',
|
||||
'settings.highlight.builtin.badstate': '错误/失败状态',
|
||||
'settings.highlight.builtin.quoted': '字符串与引号',
|
||||
'settings.highlight.builtin.envvar': '环境变量与参数',
|
||||
'settings.highlight.builtin.ipv4': '网络与 IP 地址',
|
||||
|
||||
@@ -41,6 +41,8 @@ const ssh: Record<string, string> = {
|
||||
'ssh.dialog.keepalive': 'Keepalive 间隔(秒)',
|
||||
'ssh.dialog.keepaliveTooltip': '0 表示关闭 keepalive',
|
||||
'ssh.dialog.keepaliveMin': '不能为负数',
|
||||
'ssh.dialog.highlightProfile': '高亮规则集',
|
||||
'ssh.dialog.highlightProfileDefault': '默认(全部规则)',
|
||||
|
||||
// 连接侧栏
|
||||
'ssh.sidebar.defaultGroup': '默认',
|
||||
|
||||
@@ -111,6 +111,66 @@ const settings: Record<string, string> = {
|
||||
'settings.highlight.palette': '色盤',
|
||||
'settings.highlight.note': '備註',
|
||||
'settings.highlight.notePlaceholder': '規則說明(選填)',
|
||||
'settings.highlight.caseInsensitive': '忽略大小寫',
|
||||
'settings.highlight.caseInsensitiveHint': '字母不區分大小寫(OK 也能匹配 ok)',
|
||||
'settings.highlight.bands': '數值分級',
|
||||
'settings.highlight.bandsHint': '依匹配到的第一個數字取色;沒有數字或低於最小分級時用前景色',
|
||||
'settings.highlight.bandMin': '下限',
|
||||
'settings.highlight.addBand': '+ 新增分級',
|
||||
'settings.highlight.removeBand': '刪除分級',
|
||||
'settings.highlight.importExport': '匯入 / 匯出',
|
||||
'settings.highlight.importExportTitle': '匯入 / 匯出高亮規則',
|
||||
'settings.highlight.exportSection': '匯出(複製或儲存下面的 JSON 分享給別人)',
|
||||
'settings.highlight.copy': '複製',
|
||||
'settings.highlight.copied': '已複製',
|
||||
'settings.highlight.download': '儲存為檔案',
|
||||
'settings.highlight.importSection': '匯入(貼上 JSON,或選擇 .json 檔案)',
|
||||
'settings.highlight.chooseFile': '選擇檔案',
|
||||
'settings.highlight.importMode': '匯入方式',
|
||||
'settings.highlight.importReplace': '取代現有規則',
|
||||
'settings.highlight.importAppend': '附加到現有規則',
|
||||
'settings.highlight.importOk': '匯入',
|
||||
'settings.highlight.importEmpty': '沒有解析到可用規則',
|
||||
'settings.highlight.importBadJson': '不是有效的 JSON',
|
||||
'settings.highlight.importWrongKind': '這份 JSON 不是高亮規則檔案',
|
||||
'settings.highlight.importSkipped': '已略過 {n} 條:{list}',
|
||||
'settings.highlight.imported': '已匯入 {n} 條規則',
|
||||
'settings.highlight.testText': '測試文字',
|
||||
'settings.highlight.testHint': '改完正規表示式立刻看到效果(僅預覽前 2000 字元)',
|
||||
'settings.highlight.mode': '高亮模式',
|
||||
'settings.highlight.modeAll': '全部',
|
||||
'settings.highlight.modeBasic': '僅基礎',
|
||||
'settings.highlight.modeOff': '關閉',
|
||||
'settings.highlight.modeDesc': '僅基礎:只保留錯誤/成功/警告/危險命令/密鑰等關鍵規則,開銷更小;關閉:完全不注入顏色(規則與顏色都保留)',
|
||||
'settings.highlight.category': '分類',
|
||||
'settings.highlight.groupByCategory': '依分類分組',
|
||||
'settings.highlight.category.safety': '安全',
|
||||
'settings.highlight.category.status': '狀態',
|
||||
'settings.highlight.category.file': '檔案',
|
||||
'settings.highlight.category.net': '網路',
|
||||
'settings.highlight.category.text': '文字',
|
||||
'settings.highlight.category.metric': '數值',
|
||||
'settings.highlight.category.none': '未分類',
|
||||
'settings.highlight.stats': '統計命中與耗時',
|
||||
'settings.highlight.statsHint': '開啟後統計每條規則的命中次數與耗時(設定頁顯示,關閉時零開銷)',
|
||||
'settings.highlight.statsHits': '命中',
|
||||
'settings.highlight.statsMs': '耗時',
|
||||
'settings.highlight.statsReset': '重設統計',
|
||||
'settings.highlight.statsEmpty': '還沒有統計資料,先去終端機跑點輸出',
|
||||
'settings.highlight.themeColors': '跟隨主題配色',
|
||||
'settings.highlight.themeColorsHint': '依色相把高亮顏色對應到目前終端機主題的調色盤(灰階色保持原樣)',
|
||||
'settings.highlight.perHost': '依主機規則集',
|
||||
'settings.highlight.perHostHint': '開啟後,綁定了規則集的 SSH 連線只跑該子集;關閉時所有工作階段都用全部規則',
|
||||
'settings.highlight.profiles': '主機規則集',
|
||||
'settings.highlight.profilesHint': '把規則的子集存成命名規則集,在「編輯連線」裡綁定給某台主機',
|
||||
'settings.highlight.profileNew': '+ 新增規則集',
|
||||
'settings.highlight.profileEdit': '編輯規則集',
|
||||
'settings.highlight.profileName': '名稱',
|
||||
'settings.highlight.profileNamePlaceholder': '例如:正式機(只看錯誤)',
|
||||
'settings.highlight.profileRules': '包含的規則',
|
||||
'settings.highlight.profileRulesHint': '未勾選的規則在這台主機上不生效',
|
||||
'settings.highlight.profileEmpty': '尚未建立規則集',
|
||||
'settings.highlight.profileDeleteTitle': '刪除規則集',
|
||||
'settings.highlight.actions': '操作',
|
||||
'settings.highlight.addRule': '+ 新增規則',
|
||||
'settings.highlight.empty': '尚無高亮規則,點擊「+ 新增規則」建立',
|
||||
@@ -123,8 +183,18 @@ const settings: Record<string, string> = {
|
||||
'settings.highlight.builtin.perm': 'Linux 權限與使用者',
|
||||
'settings.highlight.builtin.path': 'Linux 檔案路徑',
|
||||
'settings.highlight.builtin.shellkw': 'Shell 關鍵字與流程',
|
||||
'settings.highlight.builtin.danger': '危險/破壞性命令',
|
||||
'settings.highlight.builtin.secret': '疑似密鑰/權杖',
|
||||
'settings.highlight.builtin.loglevel': '日誌等級',
|
||||
'settings.highlight.builtin.rootat': '管理員提示字元 root@',
|
||||
'settings.highlight.builtin.exitcode': '結束碼',
|
||||
'settings.highlight.builtin.http': 'HTTP 狀態碼',
|
||||
'settings.highlight.builtin.latency': '耗時(毫秒)',
|
||||
'settings.highlight.builtin.delop': '刪除/移動/覆蓋操作',
|
||||
'settings.highlight.builtin.createop': '新建/建立操作',
|
||||
'settings.highlight.builtin.okstate': '成功狀態',
|
||||
'settings.highlight.builtin.badstate': '錯誤/警告狀態',
|
||||
'settings.highlight.builtin.warnstate': '警告與待辦標記',
|
||||
'settings.highlight.builtin.badstate': '錯誤/失敗狀態',
|
||||
'settings.highlight.builtin.quoted': '字串與引號',
|
||||
'settings.highlight.builtin.envvar': '環境變數與參數',
|
||||
'settings.highlight.builtin.ipv4': '網路與 IP 位址',
|
||||
|
||||
@@ -41,6 +41,8 @@ const ssh: Record<string, string> = {
|
||||
'ssh.dialog.keepalive': 'Keepalive 間隔(秒)',
|
||||
'ssh.dialog.keepaliveTooltip': '0 表示關閉 keepalive',
|
||||
'ssh.dialog.keepaliveMin': '不能為負數',
|
||||
'ssh.dialog.highlightProfile': '高亮規則集',
|
||||
'ssh.dialog.highlightProfileDefault': '預設(全部規則)',
|
||||
|
||||
// 連線側欄
|
||||
'ssh.sidebar.defaultGroup': '預設',
|
||||
|
||||
+295
-8
@@ -1,6 +1,23 @@
|
||||
import type { TerminalTheme } from './theme'
|
||||
import type { Language } from './i18n'
|
||||
|
||||
/** Coarse grouping for the settings table's grouped view. */
|
||||
export type HighlightCategory = 'safety' | 'status' | 'file' | 'net' | 'text' | 'metric'
|
||||
|
||||
/** Stable order for the category column, the filter list and the editor select. */
|
||||
export const HIGHLIGHT_CATEGORIES: HighlightCategory[] = [
|
||||
'safety',
|
||||
'status',
|
||||
'file',
|
||||
'net',
|
||||
'text',
|
||||
'metric'
|
||||
]
|
||||
|
||||
export function isHighlightCategory(value: unknown): value is HighlightCategory {
|
||||
return typeof value === 'string' && (HIGHLIGHT_CATEGORIES as string[]).includes(value)
|
||||
}
|
||||
|
||||
/** One terminal keyword-highlight rule (regex → ANSI color injection). */
|
||||
export interface HighlightRule {
|
||||
id: string
|
||||
@@ -11,6 +28,21 @@ export interface HighlightRule {
|
||||
priority: number
|
||||
/** truecolor hex (#rrggbb); fg required, bg optional */
|
||||
color: { fg: string; bg?: string }
|
||||
/**
|
||||
* Value bands: the first number found in the match picks the colour, lowest
|
||||
* `min` first. `fg` above stays the fallback for matches without a number or
|
||||
* below the smallest `min`.
|
||||
*/
|
||||
bands?: { min: number; fg: string }[]
|
||||
/** match without regard to letter case (compiled with the 'i' flag) */
|
||||
caseInsensitive?: boolean
|
||||
/**
|
||||
* Part of the "basic" set: still applied when the highlight mode is
|
||||
* `basic` (the cheap, low-noise subset — status, danger, secrets).
|
||||
*/
|
||||
basic?: boolean
|
||||
/** Grouping shown by the settings table's category column / grouped view. */
|
||||
category?: HighlightCategory
|
||||
note?: string
|
||||
/** part of the factory preset set (restored by 恢复默认) */
|
||||
builtin?: boolean
|
||||
@@ -49,12 +81,50 @@ export interface TerminalSettings {
|
||||
showOpenCwdButton: boolean
|
||||
/** accent color for the active tab outline, rail indicator and SSH badges */
|
||||
tabAccentColor: string
|
||||
/**
|
||||
* How much keyword highlighting runs: `all` uses every enabled rule, `basic`
|
||||
* only the rules flagged `basic` (status, danger, secrets), `off` injects no
|
||||
* colour at all. The rule set itself is never touched by this.
|
||||
*/
|
||||
highlightMode: HighlightMode
|
||||
/**
|
||||
* Settings-table view: cluster rules by category (adds a category column and
|
||||
* sorts by it) instead of listing them in plain priority order.
|
||||
*/
|
||||
highlightGroupByCategory: boolean
|
||||
/**
|
||||
* Collect per-rule hit counts and timings for the settings page. Off by
|
||||
* default: the engine skips all counting unless a sink is attached.
|
||||
*/
|
||||
highlightStats: boolean
|
||||
/**
|
||||
* Draw highlight colours from the active terminal palette (by hue) instead of
|
||||
* the rule's own hex. Off by default: the shipped colours already read on
|
||||
* every theme, this is for users who want palette-consistent highlighting.
|
||||
*/
|
||||
highlightThemeColors: boolean
|
||||
/**
|
||||
* Consult per-host highlight profiles: an SSH connection bound to a profile
|
||||
* runs only that profile's rules. Off by default — with it off every session
|
||||
* runs the full rule set, exactly as before.
|
||||
*/
|
||||
highlightPerHost: boolean
|
||||
/** absolute path to an image rendered behind the terminal; '' = none */
|
||||
backgroundImage: string
|
||||
/** background image layer opacity, 10..100 (%) */
|
||||
backgroundImageOpacity: number
|
||||
}
|
||||
|
||||
export type HighlightMode = 'all' | 'basic' | 'off'
|
||||
|
||||
/**
|
||||
* Read a stored mode, tolerating anything a hand-edited settings.json holds: an
|
||||
* unknown value behaves like `all`, the default before modes existed.
|
||||
*/
|
||||
export function highlightModeOf(value: unknown): HighlightMode {
|
||||
return value === 'basic' || value === 'off' ? value : 'all'
|
||||
}
|
||||
|
||||
export interface SystemSettings {
|
||||
/** register the app to launch at OS login */
|
||||
launchAtLogin: boolean
|
||||
@@ -81,12 +151,25 @@ export interface SystemSettings {
|
||||
language?: Language
|
||||
}
|
||||
|
||||
/**
|
||||
* A named subset of the highlight rules, bound to an SSH connection so a
|
||||
* production host can run a quieter set than a local shell. An empty `ruleIds`
|
||||
* means "everything", the same as not binding a profile at all.
|
||||
*/
|
||||
export interface HighlightProfile {
|
||||
id: string
|
||||
name: string
|
||||
ruleIds: string[]
|
||||
}
|
||||
|
||||
export interface AppSettings {
|
||||
terminal: TerminalSettings
|
||||
/** user-created themes; builtin themes are immutable and live in shared/theme.ts */
|
||||
customThemes: TerminalTheme[]
|
||||
/** terminal keyword highlight rules, applied at write time in priority order */
|
||||
highlightRules: HighlightRule[]
|
||||
/** named rule subsets for per-host highlighting (see terminal.highlightPerHost) */
|
||||
highlightProfiles: HighlightProfile[]
|
||||
system: SystemSettings
|
||||
}
|
||||
|
||||
@@ -96,23 +179,221 @@ const RULE = (
|
||||
priority: number,
|
||||
fg: string,
|
||||
note: string,
|
||||
bg?: string
|
||||
): HighlightRule => ({ id, pattern, enabled: true, priority, color: { fg, bg }, note, builtin: true })
|
||||
opts?: {
|
||||
bg?: string
|
||||
caseInsensitive?: boolean
|
||||
bands?: HighlightRule['bands']
|
||||
basic?: boolean
|
||||
}
|
||||
): HighlightRule => ({
|
||||
id,
|
||||
pattern,
|
||||
enabled: true,
|
||||
priority,
|
||||
color: { fg, bg: opts?.bg },
|
||||
note,
|
||||
// omitted (not `false`) unless set, so a preset round trip stays compact
|
||||
...(opts?.caseInsensitive ? { caseInsensitive: true } : {}),
|
||||
...(opts?.bands ? { bands: opts.bands } : {}),
|
||||
...(opts?.basic ? { basic: true } : {}),
|
||||
builtin: true
|
||||
})
|
||||
|
||||
/**
|
||||
* Factory preset rules.
|
||||
*
|
||||
* Status words are matched case-insensitively (`\bOK\b` should fire on `ok` as
|
||||
* well as `OK`), but always behind word boundaries: `invalid` must not light up
|
||||
* `valid`, `disabled` must not light up `enabled`. The `okstate` pattern carries
|
||||
* a `not ` lookbehind so the `ok` in `not ok` (TAP, mocha, pytest summaries) is
|
||||
* left for `badstate` to claim instead of turning it green.
|
||||
*
|
||||
* Priorities put the destructive-command and file-operation rules above the
|
||||
* status words, and the status words above `shellkw`: `done` is a shell keyword
|
||||
* and a success word, `if` sits inside `dd if=`, and `drop table` belongs to
|
||||
* `danger` rather than the plain `drop` of `delop`. Whichever rule runs first
|
||||
* paints the span and the other one is skipped, so that order is the whole
|
||||
* contract between them.
|
||||
*
|
||||
* Keyword stems carry their own endings rather than a generic suffix group:
|
||||
* `DELET(?:E|ED|ES|ING|ION)` instead of `DELETE(?:D|S|ING)?`, which silently
|
||||
* misses `deleting`. Stems that are not words on their own (MOV, SAV, CLON,
|
||||
* PURG, ERAS, WIP, REVOK) always demand an ending, so a bare `MOV` in an
|
||||
* assembly listing is left alone.
|
||||
*
|
||||
* `percent` carries value bands instead of one colour: the number in the match
|
||||
* picks the band (below 20% red, 20–50% yellow, 50–80% light green, 80%+ green),
|
||||
* which is what makes a progress bar readable at a glance.
|
||||
*/
|
||||
export const DEFAULT_HIGHLIGHT_RULES: HighlightRule[] = [
|
||||
RULE('perm', '^[-dlbcps][rwxstST-]{9}', 1, '#3fb950', 'Linux 权限与用户'),
|
||||
RULE('path', '(?:^|[\\s:=])(?:/[A-Za-z0-9_.\\-/@+]+|~[A-Za-z0-9_.\\-/]*)', 2, '#58a6ff', 'Linux 文件路径'),
|
||||
RULE('shellkw', '\\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|break|continue|export|source)\\b', 3, '#d2a8ff', 'Shell 关键字与流程'),
|
||||
RULE('okstate', '\\b(SUCCESS|PASS|OK|DONE|PASSED)\\b', 5, '#3fb950', '成功状态'),
|
||||
RULE('badstate', '\\b(FAILED|ERROR|FAIL|FATAL|WARN|WARNING|DENIED)\\b', 6, '#f85149', '错误/警告状态'),
|
||||
RULE('quoted', '"[^"\\n]*"|\'[^\'\\n]*\'', 8, '#e3b341', '字符串与引号'),
|
||||
RULE('envvar', '\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?', 10, '#79c0ff', '环境变量与参数'),
|
||||
RULE(
|
||||
'danger',
|
||||
'\\brm\\s+-(?:[a-z]*[rf][a-z]*)\\b|\\bmkfs(?:\\.[a-z0-9]+)?\\b|\\bdd\\s+if(?==)|\\bchmod\\s+(?:-R\\s+)?0?777\\b|\\b(?:shutdown|reboot|poweroff|halt)\\b|\\bgit\\s+(?:push\\s+(?:-f|--force)|reset\\s+--hard)\\b|\\bdrop\\s+(?:database|table)\\b|\\|\\s*(?:sudo\\s+)?(?:sh|bash|zsh|dash|ksh|python[0-9.]*|perl|ruby|node)\\b|\\beval\\b|\\bbase64\\s+(?:-d|--decode)\\b|\\bchmod\\s+\\+x\\b',
|
||||
3,
|
||||
'#ff7b72',
|
||||
'危险/破坏性命令',
|
||||
{ caseInsensitive: true }
|
||||
),
|
||||
RULE(
|
||||
'delop',
|
||||
'\\b(?:DELET(?:E|ED|ES|ING|ION)|REMOV(?:E|ED|ES|ING|AL)|RM|RMDIR|UNLINK(?:ED|S|ING)?|ERAS(?:E|ED|ES|ING)|PURG(?:E|ED|ES|ING)|DESTROY(?:ED|S|ING)?|TRUNCAT(?:E|ED|ES|ING|ION)|WIP(?:E|ED|ES|ING)|DISCARD(?:ED|S|ING)?|REVOK(?:E|ED|ES|ING)|UNMOUNT(?:ED|S|ING)?|KILL(?:ED|S|ING)?|TERMINAT(?:E|ED|ES|ING|ION)|DROP(?:PED|S|PING)?|CLEANUP|SHRED(?:DED|DING)?|MOV(?:E|ED|ES|ING)|RENAM(?:E|ED|ES|ING)|OVERWRIT(?:E|TEN|ING|ES)?)\\b',
|
||||
4,
|
||||
'#ffa657',
|
||||
'删除/移动/覆盖操作',
|
||||
{ caseInsensitive: true }
|
||||
),
|
||||
RULE(
|
||||
'createop',
|
||||
'\\b(?:CREAT(?:E|ED|ES|ING|ION)|MKDIR|TOUCH(?:ED|ES|ING)?|ADD(?:ED|S|ING)?|INSTALL(?:ED|S|ING)?|CLON(?:E|ED|ES|ING)|GENERAT(?:E|ED|ES|ING|ION)|INIT(?:IALIZ(?:E|ED|ES|ING))?|IMPORT(?:ED|S|ING)?|UPLOAD(?:ED|S|ING)?|DOWNLOAD(?:ED|S|ING)?|SAV(?:E|ED|ES|ING)|WROTE|WRITTEN|MOUNT(?:ED|S|ING)?)\\b',
|
||||
5,
|
||||
'#56d364',
|
||||
'新建/创建操作',
|
||||
{ caseInsensitive: true }
|
||||
),
|
||||
RULE(
|
||||
'okstate',
|
||||
'(?<!not\\s)\\b(?:SUCCESS(?:FUL(?:LY)?)?|SUCCEED(?:S|ED|ING)?|PASS(?:ED|ES|ING)?|OK(?:AY)?|DONE|FINISH(?:ED|ES|ING)?|COMPLET(?:E|ED|ES|ING|ION)|GOOD|FINE|TRUE|ENABL(?:E|ED|ES|ING)|ACTIVE|ONLINE|READY|HEALTHY|MATCH(?:ED|ES|ING)?|FOUND|ACCEPT(?:ED|S|ING)?|APPROV(?:E|ED|ES|ING|AL)|VALID|ALLOW(?:ED|S|ING)?)\\b|[\\u2713\\u2714\\u2705]\\uFE0F?',
|
||||
6,
|
||||
'#3fb950',
|
||||
'成功状态',
|
||||
{ caseInsensitive: true }
|
||||
),
|
||||
RULE(
|
||||
'warnstate',
|
||||
'\\b(?:WARN(?:S|ED|ING|INGS)?|CAUTION|ATTENTION|NOTICE|DEPRECAT(?:E|ED|ES|ING|ION)|OBSOLETE|UNSTABLE|TODO|FIXME|XXX|HACK|TBD)\\b|\\u26A0\\uFE0F?',
|
||||
7,
|
||||
'#e3b341',
|
||||
'警告与待办标记',
|
||||
{ caseInsensitive: true }
|
||||
),
|
||||
RULE(
|
||||
'badstate',
|
||||
'\\b(?:ERROR|ERR|ERRNO|EXCEPTION|TRACEBACK|FAIL(?:ED|S|URE|URES|ING)?|FATAL|CRITICAL|PANIC|ABORT(?:ED|S|ING)?|CRASH(?:ED|ES|ING)?|BUG(?:S|GY)?|SEGFAULT|SEGV|OOM|DENIED|REFUS(?:E|ED|ES|ING|AL)|REJECT(?:ED|S|ING)?|INVALID|ILLEGAL|MISMATCH(?:ED|ES)?|TIMEOUT|TIMED\\s+OUT|UNREACHABLE|UNHEALTHY|CONFLICT(?:ED|S|ING)?|VIOLAT(?:ED|ES|ING|ION)|CORRUPT(?:ED|S|ING|ION)?|MISSING|NOT\\s+(?:OK|DONE|PASSED|FOUND|READY|ACTIVE|RUNNING)|NG|BAD|WRONG|OFFLINE)\\b|[\\u2717\\u2718\\u2716\\u274C]\\uFE0F?',
|
||||
8,
|
||||
'#f85149',
|
||||
'错误/失败状态',
|
||||
{ caseInsensitive: true }
|
||||
),
|
||||
RULE('shellkw', '\\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|break|continue|export|source)\\b', 9, '#d2a8ff', 'Shell 关键字与流程'),
|
||||
RULE('quoted', '"[^"\\n]*"|\'[^\'\\n]*\'', 10, '#e3b341', '字符串与引号'),
|
||||
RULE('loglevel', '\\b(?:INFO|DEBUG|TRACE|VERBOSE)\\b', 11, '#79c0ff', '日志级别'),
|
||||
RULE('envvar', '\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?', 12, '#79c0ff', '环境变量与参数'),
|
||||
RULE('rootat', '\\broot@[A-Za-z0-9_.-]+', 13, '#ffa657', '管理员提示符 root@'),
|
||||
RULE('ipv4', '\\b(?:\\d{1,3}\\.){3}\\d{1,3}(?::\\d{1,5})?\\b', 15, '#58a6ff', '网络与 IP 地址'),
|
||||
RULE('datetime', '\\b\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}(?:[ T]\\d{1,2}:\\d{2}(?::\\d{2})?)?\\b|\\b\\d{1,2}:\\d{2}(?::\\d{2})?\\b', 20, '#bc8cff', '时间与日期'),
|
||||
RULE(
|
||||
'exitcode',
|
||||
'\\bexit(?:ed)?\\s+(?:with\\s+)?(?:code|status)\\s*[:=]?\\s*\\d+|\\bexit\\s+\\d{1,3}\\b',
|
||||
21,
|
||||
'#3fb950',
|
||||
'退出码',
|
||||
{
|
||||
bands: [
|
||||
{ min: 0, fg: '#3fb950' },
|
||||
{ min: 1, fg: '#f85149' }
|
||||
]
|
||||
}
|
||||
),
|
||||
RULE(
|
||||
'percent',
|
||||
'\\b\\d{1,3}(?:\\.\\d+)?\\s?%',
|
||||
22,
|
||||
'#3fb950',
|
||||
'百分比与进度',
|
||||
{
|
||||
bands: [
|
||||
{ min: 0, fg: '#f85149' },
|
||||
{ min: 20, fg: '#e3b341' },
|
||||
{ min: 50, fg: '#7ee787' },
|
||||
{ min: 80, fg: '#3fb950' }
|
||||
]
|
||||
}
|
||||
),
|
||||
RULE(
|
||||
'http',
|
||||
'(?<=\\bHTTP/\\d(?:\\.\\d)?\\s)[1-5]\\d\\d\\b|(?<=\\b(?:status|code|http_code|status_code)\\s*[:=]?\\s)[1-5]\\d\\d\\b|\\b[1-5]\\d\\d(?=\\s+(?:OK|Created|Accepted|No Content|Moved Permanently|Found|Not Modified|Bad Request|Unauthorized|Forbidden|Not Found|Method Not Allowed|Conflict|Too Many Requests|Internal Server Error|Bad Gateway|Service Unavailable|Gateway Timeout)\\b)',
|
||||
23,
|
||||
'#58a6ff',
|
||||
'HTTP 状态码',
|
||||
{
|
||||
bands: [
|
||||
{ min: 100, fg: '#8b949e' },
|
||||
{ min: 200, fg: '#3fb950' },
|
||||
{ min: 300, fg: '#58a6ff' },
|
||||
{ min: 400, fg: '#e3b341' },
|
||||
{ min: 500, fg: '#f85149' }
|
||||
]
|
||||
}
|
||||
),
|
||||
RULE(
|
||||
'latency',
|
||||
'\\b\\d+(?:\\.\\d+)?\\s?(?:ms|msec|µs|us|ns)\\b',
|
||||
24,
|
||||
'#3fb950',
|
||||
'耗时(毫秒)',
|
||||
{
|
||||
bands: [
|
||||
{ min: 0, fg: '#3fb950' },
|
||||
{ min: 100, fg: '#e3b341' },
|
||||
{ min: 500, fg: '#f85149' }
|
||||
]
|
||||
}
|
||||
),
|
||||
RULE('numbers', '\\b\\d+(?:\\.\\d+)?(?:%)?\\b', 25, '#f2cc60', '数字与计数'),
|
||||
RULE('url', 'https?://[^\\s]+|ftp://[^\\s]+|www\\.[A-Za-z0-9-]+\\.[A-Za-z]{2,}(?::\\d+)?(?:/[^\\s]*)?', 30, '#58a6ff', '网址链接')
|
||||
RULE('url', 'https?://[^\\s]+|ftp://[^\\s]+|www\\.[A-Za-z0-9-]+\\.[A-Za-z]{2,}(?::\\d+)?(?:/[^\\s]*)?', 30, '#58a6ff', '网址链接'),
|
||||
RULE(
|
||||
'secret',
|
||||
'\\b(?:sk-[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{16,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{30,}|eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,})\\b|-----BEGIN [A-Z ]*PRIVATE KEY-----|\\bBearer\\s+[A-Za-z0-9._-]{16,}',
|
||||
31,
|
||||
'#ff7b72',
|
||||
'疑似密钥/令牌'
|
||||
)
|
||||
]
|
||||
|
||||
/**
|
||||
* Rules that keep working in the `basic` highlight mode: the cheap, high-signal
|
||||
* ones — failures, successes, warnings, destructive commands, leaked secrets.
|
||||
* Everything else (paths, numbers, URLs, network, timings) is dropped there,
|
||||
* which is what makes that mode both quieter and roughly 3x cheaper.
|
||||
*/
|
||||
const BASIC_RULE_IDS = new Set(['danger', 'secret', 'okstate', 'warnstate', 'badstate'])
|
||||
/**
|
||||
* Category per preset id, for the settings table's grouped view. One table
|
||||
* rather than a field on each rule, so the whole taxonomy reads at a glance —
|
||||
* and so `basic` stays a strict subset of the safety + status groups.
|
||||
*/
|
||||
const PRESET_CATEGORIES: Record<string, HighlightCategory> = {
|
||||
danger: 'safety',
|
||||
secret: 'safety',
|
||||
okstate: 'status',
|
||||
warnstate: 'status',
|
||||
badstate: 'status',
|
||||
loglevel: 'status',
|
||||
exitcode: 'status',
|
||||
rootat: 'status',
|
||||
percent: 'metric',
|
||||
latency: 'metric',
|
||||
numbers: 'metric',
|
||||
ipv4: 'net',
|
||||
url: 'net',
|
||||
http: 'net',
|
||||
perm: 'file',
|
||||
path: 'file',
|
||||
delop: 'file',
|
||||
createop: 'file',
|
||||
shellkw: 'text',
|
||||
quoted: 'text',
|
||||
envvar: 'text',
|
||||
datetime: 'text'
|
||||
}
|
||||
for (const rule of DEFAULT_HIGHLIGHT_RULES) {
|
||||
if (BASIC_RULE_IDS.has(rule.id)) rule.basic = true
|
||||
const category = PRESET_CATEGORIES[rule.id]
|
||||
if (category !== undefined) rule.category = category
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: AppSettings = {
|
||||
terminal: {
|
||||
fontFamily: "'Cascadia Mono', Consolas, 'JetBrains Mono', 'Courier New', monospace",
|
||||
@@ -137,10 +418,16 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||
showOpenLogsButton: false,
|
||||
showOpenCwdButton: true,
|
||||
tabAccentColor: '#3fb950',
|
||||
highlightMode: 'all',
|
||||
highlightGroupByCategory: false,
|
||||
highlightStats: false,
|
||||
highlightThemeColors: false,
|
||||
highlightPerHost: false,
|
||||
backgroundImage: '',
|
||||
backgroundImageOpacity: 60
|
||||
},
|
||||
customThemes: [],
|
||||
highlightRules: DEFAULT_HIGHLIGHT_RULES,
|
||||
highlightProfiles: [],
|
||||
system: { launchAtLogin: false, preventSleep: false, globalShowHide: '', closeAction: 'tray', autoCheckUpdate: true, restoreSession: true, shellIntegration: false, language: 'zh-CN' }
|
||||
}
|
||||
Reference in new issue
Block a user