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:
Bill committed 2026-09-23 11:14:31 +08:00
1 parent 41b1cb1b25
commit e08e1cfec4
39 files changed
+3164 -89

No files matched your search

+4
View File
@@ -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 */
+147
View File
@@ -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]
}
+83
View File
@@ -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))
}
+77 -1
View File
@@ -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',
+2
View File
@@ -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',
+75 -1
View File
@@ -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 アドレス',
+2
View File
@@ -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': 'デフォルト',
+71 -1
View File
@@ -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 地址',
+2
View File
@@ -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': '默认',
+71 -1
View File
@@ -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 位址',
+2
View File
@@ -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
View File
@@ -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' }
}