[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { APP_ICON_VARIANT_ASSET_PATH } from './appIconVariants';
test('original icon preview uses the desktop-sized runtime asset', () => {
assert.equal(APP_ICON_VARIANT_ASSET_PATH.original, '/icons/variants/original.png');
});

View File

@@ -0,0 +1,86 @@
import {
APP_ICON_VARIANTS,
DEFAULT_APP_ICON_VARIANT,
type AppIconVariant,
} from '../../domain/appIconVariant';
export { APP_ICON_VARIANTS, DEFAULT_APP_ICON_VARIANT, type AppIconVariant };
// Elf SVG icon source — shared across all variants.
export const APP_ICON_ELF_SVG = '/senmesh-elf-logo.svg';
// Background colors for the icon preview tiles in Settings.
// Each variant is: background (can be a linear-gradient or solid color), optional border.
export const APP_ICON_VARIANT_TILE: Record<
AppIconVariant,
{ background: string; border?: string }
> = {
original: { background: 'linear-gradient(135deg, #5BAF7E 0%, #3D8B5E 100%)' },
bright: { background: 'linear-gradient(135deg, #A8E0B0 0%, #7BC48A 100%)' },
dark: { background: 'linear-gradient(135deg, #2E7D4F 0%, #1B5E3A 100%)' },
colorful: { background: 'linear-gradient(135deg, #5BAF7E 0%, #7BC4A5 50%, #A8D8B9 100%)' },
'high-contrast': { background: '#0F2418', border: '1px solid #5BAF7E' },
'white-navy': { background: 'linear-gradient(135deg, #1E3A5F 0%, #0F2440 100%)' },
'white-sky': { background: 'linear-gradient(135deg, #7EC8E3 0%, #4A9BC0 100%)' },
'white-rose': { background: 'linear-gradient(135deg, #F8BBD0 0%, #E8A0B8 100%)' },
'white-emerald': { background: 'linear-gradient(135deg, #5BAF7E 0%, #3D8B5E 100%)' },
'white-amber': { background: 'linear-gradient(135deg, #F5CBA7 0%, #E0A86A 100%)' },
'white-violet': { background: 'linear-gradient(135deg, #9B7FC4 0%, #6D4F99 100%)' },
rainbow: {
background: 'linear-gradient(135deg, #E74C3C 0%, #F39C12 20%, #F1C40F 40%, #27AE60 60%, #3498DB 80%, #9B59B6 100%)',
},
};
// Legacy PNG paths — kept for Electron main-process window icon usage
// (real PNG files are still needed for taskbar/dock icons).
export const APP_ICON_VARIANT_ASSET_PATH: Record<AppIconVariant, string> = {
original: '/icons/variants/original.png',
bright: '/icons/variants/bright.png',
dark: '/icons/variants/dark.png',
colorful: '/icons/variants/colorful.png',
'high-contrast': '/icons/variants/high-contrast.png',
'white-navy': '/icons/variants/white-navy.png',
'white-sky': '/icons/variants/white-sky.png',
'white-rose': '/icons/variants/white-rose.png',
'white-emerald': '/icons/variants/white-emerald.png',
'white-amber': '/icons/variants/white-amber.png',
'white-violet': '/icons/variants/white-violet.png',
rainbow: '/icons/variants/rainbow.png',
};
export const APP_ICON_VARIANT_I18N_KEY: Record<AppIconVariant, string> = {
original: 'settings.appearance.appIcon.original',
bright: 'settings.appearance.appIcon.bright',
dark: 'settings.appearance.appIcon.dark',
colorful: 'settings.appearance.appIcon.colorful',
'high-contrast': 'settings.appearance.appIcon.highContrast',
'white-navy': 'settings.appearance.appIcon.whiteNavy',
'white-sky': 'settings.appearance.appIcon.whiteSky',
'white-rose': 'settings.appearance.appIcon.whiteRose',
'white-emerald': 'settings.appearance.appIcon.whiteEmerald',
'white-amber': 'settings.appearance.appIcon.whiteAmber',
'white-violet': 'settings.appearance.appIcon.whiteViolet',
rainbow: 'settings.appearance.appIcon.rainbow',
};
export const APP_ICON_VARIANT_GROUPS: Array<{
id: 'classic' | 'white' | 'special';
labelKey: string;
variants: AppIconVariant[];
}> = [
{
id: 'classic',
labelKey: 'settings.appearance.appIcon.group.classic',
variants: ['original', 'bright', 'dark', 'colorful', 'high-contrast'],
},
{
id: 'white',
labelKey: 'settings.appearance.appIcon.group.white',
variants: ['white-navy', 'white-sky', 'white-rose', 'white-emerald', 'white-amber', 'white-violet'],
},
{
id: 'special',
labelKey: 'settings.appearance.appIcon.group.special',
variants: ['rainbow'],
},
];

View File

@@ -0,0 +1,315 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
composeFontFamilyStack,
getDefaultCjkFallback,
getRecommendedCjkFor,
splitFontFamilyList,
CJK_SYSTEM_FALLBACK_STACK,
} from './cjkFonts';
describe('composeFontFamilyStack', () => {
it('puts the primary font first', () => {
const stack = composeFontFamilyStack({
primaryFamily: 'Menlo, monospace',
userFallback: '',
latinFontId: 'menlo',
platform: 'darwin',
});
assert.match(stack, /^Menlo,\s*/);
});
it('inserts user fallback right after primary when provided', () => {
const stack = composeFontFamilyStack({
primaryFamily: '"Fira Code", monospace',
userFallback: 'Sarasa Mono SC',
latinFontId: 'fira-code',
platform: 'darwin',
});
const firaIdx = stack.indexOf('Fira Code');
const userIdx = stack.indexOf('Sarasa Mono SC');
assert.ok(firaIdx >= 0 && userIdx > firaIdx, 'user fallback after primary');
});
it('uses per-Latin-font recommended CJK when user fallback is empty', () => {
const stack = composeFontFamilyStack({
primaryFamily: '"Cascadia Code", monospace',
userFallback: '',
latinFontId: 'cascadia-code',
platform: 'win32',
});
// Cascadia Code now recommends Sarasa Mono SC (true monospace).
assert.match(stack, /Sarasa Mono SC/);
});
it('falls back to OS default when Latin font has no recommendation', () => {
const stack = composeFontFamilyStack({
primaryFamily: '"Unknown Font", monospace',
userFallback: '',
latinFontId: 'unknown',
platform: 'darwin',
});
// macOS no-recommendation default is now Sarasa Mono SC (bundled).
assert.match(stack, /Sarasa Mono SC/);
});
it('quotes multi-word user fallback names', () => {
const stack = composeFontFamilyStack({
primaryFamily: 'Menlo, monospace',
userFallback: 'Source Han Mono SC',
latinFontId: 'menlo',
platform: 'linux',
});
assert.match(stack, /"Source Han Mono SC"/);
});
it('keeps an unquoted user fallback containing a comma as one font family', () => {
const stack = composeFontFamilyStack({
primaryFamily: 'Menlo, monospace',
userFallback: 'Foo, Inc. Mono',
latinFontId: 'menlo',
platform: 'darwin',
});
assert.ok(stack.includes('"Foo, Inc. Mono"'));
assert.ok(splitFontFamilyList(stack).includes('"Foo, Inc. Mono"'));
});
it('escapes quotes inside a manually entered font family', () => {
const stack = composeFontFamilyStack({
primaryFamily: 'Menlo, monospace',
userFallback: 'Foo"Bar',
latinFontId: 'menlo',
platform: 'darwin',
});
assert.ok(stack.includes('"Foo\\"Bar"'));
assert.ok(splitFontFamilyList(stack).includes('"Foo\\"Bar"'));
});
it('quotes a manually entered font family containing an apostrophe', () => {
const stack = composeFontFamilyStack({
primaryFamily: 'Menlo, monospace',
userFallback: "Rock'nRoll",
latinFontId: 'menlo',
platform: 'darwin',
});
assert.ok(stack.includes('"Rock\'nRoll"'));
assert.ok(splitFontFamilyList(stack).includes('"Rock\'nRoll"'));
});
it('quotes a manually entered font family that starts with a digit', () => {
const stack = composeFontFamilyStack({
primaryFamily: 'Menlo, monospace',
userFallback: '3270font',
latinFontId: 'menlo',
platform: 'darwin',
});
assert.ok(stack.includes('"3270font"'));
assert.ok(splitFontFamilyList(stack).includes('"3270font"'));
});
it('keeps CSS generic font families unquoted', () => {
const stack = composeFontFamilyStack({
primaryFamily: 'Menlo, monospace',
userFallback: 'ui-monospace',
latinFontId: 'menlo',
platform: 'darwin',
});
assert.ok(splitFontFamilyList(stack).includes('ui-monospace'));
});
it('does not duplicate identical fallback entries', () => {
// User explicitly picks the same font the per-font pairing would,
// and that font also lives in the system stack — should appear once.
const stack = composeFontFamilyStack({
primaryFamily: '"Cascadia Code", monospace',
userFallback: 'Sarasa Mono SC',
latinFontId: 'cascadia-code',
platform: 'win32',
});
const matches = stack.match(/Sarasa Mono SC/g) || [];
assert.equal(matches.length, 1);
});
it('inserts JetBrains Mono as Latin-only fallback right after the primary family', () => {
const stack = composeFontFamilyStack({
primaryFamily: 'Menlo',
userFallback: '',
latinFontId: 'menlo',
platform: 'darwin',
});
const families = stack.split(',').map((s) => s.trim().replace(/^"|"$/g, ''));
assert.equal(families[0], 'Menlo');
assert.equal(families[1], 'JetBrains Mono');
});
it('Latin fallback (JetBrains Mono) precedes every CJK family', () => {
// Regression guard for codex P1 review on PR #940 (first round):
// when the primary font isn't installed, Latin glyphs must fall to
// a Latin-only monospace face — NOT a CJK font's full-width Latin
// variant — to keep xterm's fixed cell grid aligned. JetBrains Mono
// is bundled via @fontsource and contains no CJK glyphs, so it
// catches Latin while letting CJK glyphs flow past.
const stack = composeFontFamilyStack({
primaryFamily: '"Fira Code", monospace',
userFallback: 'LXGW WenKai Mono',
latinFontId: 'fira-code',
platform: 'darwin',
});
const jbIdx = stack.indexOf('JetBrains Mono');
const sarasaIdx = stack.indexOf('Sarasa Mono SC');
const userFallbackIdx = stack.indexOf('LXGW WenKai Mono');
const simSunIdx = stack.indexOf('SimSun');
assert.ok(jbIdx > 0, 'JetBrains Mono must appear in the stack');
assert.ok(jbIdx < userFallbackIdx, 'JetBrains Mono before user CJK');
assert.ok(jbIdx < sarasaIdx, 'JetBrains Mono before Sarasa system fallback');
assert.ok(jbIdx < simSunIdx, 'JetBrains Mono before SimSun system fallback');
});
it('preserves a quoted primary family name that contains a comma', () => {
// Regression guard for codex P2 review on PR #940: when the primary
// family is something like `"Foo, Inc. Mono"`, the composed stack
// must keep that token intact rather than splitting on the internal
// comma and emitting fragmented pieces.
const stack = composeFontFamilyStack({
primaryFamily: '"Foo, Inc. Mono", monospace',
userFallback: '',
latinFontId: 'foo-inc-mono',
platform: 'darwin',
});
assert.ok(
stack.includes('"Foo, Inc. Mono"'),
'quoted family with comma stays a single token',
);
assert.ok(
!stack.includes('"Foo,') || stack.includes('"Foo, Inc. Mono"'),
'must not produce a dangling `"Foo,` fragment',
);
});
it('user-chosen CJK fallback precedes generic monospace', () => {
// Regression guard for codex P1 review on PR #940 (second round):
// generic `monospace` on macOS Chrome resolves Chinese glyphs to
// PingFang via Chromium's CJK system fallback. If `monospace`
// appeared in the chain BEFORE the user's CJK pick, CSS per-glyph
// fallback would stop at monospace for CJK characters and never
// consult the user's choice, silently nullifying the CJK picker.
const stack = composeFontFamilyStack({
primaryFamily: '"Fira Code", monospace',
userFallback: 'LXGW WenKai Mono',
latinFontId: 'fira-code',
platform: 'darwin',
});
const userFallbackIdx = stack.indexOf('LXGW WenKai Mono');
// Match `monospace` as a standalone token (after the comma+space).
const monospaceIdx = stack.lastIndexOf(', monospace');
assert.ok(userFallbackIdx > 0, 'user CJK must appear');
assert.ok(monospaceIdx > userFallbackIdx, 'user CJK must come before generic monospace');
});
it('explicit user fallback overrides the per-font recommendation', () => {
const stack = composeFontFamilyStack({
primaryFamily: '"JetBrains Mono", monospace',
userFallback: 'LXGW WenKai Mono',
latinFontId: 'jetbrains-mono',
platform: 'darwin',
});
// User chose LXGW WenKai Mono; the JetBrains Mono recommendation
// (Sarasa Mono SC) should be suppressed, so Sarasa only shows up
// later in the system fallback stack, AFTER the user choice.
const userIdx = stack.indexOf('LXGW WenKai Mono');
const sarasaIdx = stack.indexOf('Sarasa Mono SC');
assert.ok(userIdx >= 0);
assert.ok(sarasaIdx > userIdx, 'system Sarasa appears after explicit user choice');
});
});
describe('getDefaultCjkFallback', () => {
it('returns SimSun on Windows (always installed, monospace)', () => {
assert.equal(getDefaultCjkFallback('win32'), 'SimSun');
});
it('returns Sarasa Mono SC on macOS (bundled by app)', () => {
assert.equal(getDefaultCjkFallback('darwin'), 'Sarasa Mono SC');
});
it('returns Noto Sans Mono CJK SC on Linux', () => {
assert.equal(getDefaultCjkFallback('linux'), 'Noto Sans Mono CJK SC');
});
it('never returns a known proportional font', () => {
const proportional = ['PingFang SC', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Hiragino Sans GB'];
for (const platform of ['darwin', 'win32', 'linux'] as const) {
const v = getDefaultCjkFallback(platform);
assert.ok(!proportional.includes(v), `${platform} default ${v} must not be proportional`);
}
});
});
describe('getRecommendedCjkFor', () => {
it('returns null for unknown fonts', () => {
assert.equal(getRecommendedCjkFor('unknown-font-id', 'darwin'), null);
});
it('returns a non-empty string for known fonts', () => {
const v = getRecommendedCjkFor('jetbrains-mono', 'darwin');
assert.ok(v && v.length > 0);
});
});
describe('splitFontFamilyList', () => {
it('splits a simple comma-separated list', () => {
assert.deepEqual(
splitFontFamilyList('Menlo, monospace'),
['Menlo', 'monospace'],
);
});
it('keeps quoted family names with commas intact', () => {
// Regression guard for codex P2 review on PR #940: a font family
// name like `"Foo, Inc. Mono"` is a single token in CSS, not two.
assert.deepEqual(
splitFontFamilyList('"Foo, Inc. Mono", monospace'),
['"Foo, Inc. Mono"', 'monospace'],
);
});
it('handles a single unquoted name', () => {
assert.deepEqual(splitFontFamilyList('Iosevka'), ['Iosevka']);
});
it('handles single quotes too', () => {
assert.deepEqual(
splitFontFamilyList("'Foo, Inc.', serif"),
["'Foo, Inc.'", 'serif'],
);
});
it('keeps escaped quotes inside a quoted family name', () => {
assert.deepEqual(
splitFontFamilyList('"Foo\\"Bar", monospace'),
['"Foo\\"Bar"', 'monospace'],
);
});
it('drops empty segments produced by double commas', () => {
assert.deepEqual(
splitFontFamilyList('Menlo,, monospace'),
['Menlo', 'monospace'],
);
});
});
describe('CJK_SYSTEM_FALLBACK_STACK', () => {
it('contains true-monospace CJK fonts only', () => {
assert.match(CJK_SYSTEM_FALLBACK_STACK, /Sarasa Mono SC/);
assert.match(CJK_SYSTEM_FALLBACK_STACK, /Noto Sans Mono CJK SC/);
assert.match(CJK_SYSTEM_FALLBACK_STACK, /SimSun/);
});
it('does not include known proportional CJK fonts', () => {
assert.doesNotMatch(CJK_SYSTEM_FALLBACK_STACK, /PingFang SC/);
assert.doesNotMatch(CJK_SYSTEM_FALLBACK_STACK, /Microsoft YaHei UI/);
assert.doesNotMatch(CJK_SYSTEM_FALLBACK_STACK, /Hiragino Sans GB/);
});
});

View File

@@ -0,0 +1,205 @@
export type SupportedPlatform = 'darwin' | 'win32' | 'linux' | (string & {});
// True monospace CJK fonts only. Proportional fonts (PingFang SC,
// Microsoft YaHei UI, Hiragino Sans GB) render at non-2x widths in a
// terminal grid — including them here visibly broke alignment for users
// whose primary font lacked CJK glyphs. They are intentionally absent.
const CJK_SYSTEM_FALLBACK_FONTS = [
'"Sarasa Mono SC"',
'"Sarasa Mono TC"',
'"Maple Mono CN"',
'"LXGW WenKai Mono"',
'"Noto Sans Mono CJK SC"',
'"Source Han Mono SC"',
'"NSimSun"',
'"SimSun"',
];
export const CJK_SYSTEM_FALLBACK_STACK = CJK_SYSTEM_FALLBACK_FONTS.join(', ');
const NERD_FONT_FALLBACK_FONTS = [
'"Symbols Nerd Font Mono"',
'"Symbols Nerd Font"',
];
// Per-OS default CJK font when user hasn't explicitly set fallbackFont
// AND the current Latin font has no recommended pairing.
// All choices are TRUE monospace fonts that keep the terminal grid
// aligned. macOS has no system-installed monospace CJK font, so we
// reference Sarasa Mono SC which netcatty bundles as a webfont.
export function getDefaultCjkFallback(platform: SupportedPlatform): string {
if (platform === 'win32') return 'SimSun';
if (platform === 'darwin') return 'Sarasa Mono SC';
return 'Noto Sans Mono CJK SC';
}
// Every entry must point at a TRUE monospace CJK font. Sarasa Mono SC
// is the safest universal choice because netcatty bundles it via
// @font-face, so it works even on machines without other CJK monospace
// fonts installed.
const PER_FONT_CJK_PAIRING: Record<string, string> = {
'fira-code': 'Sarasa Mono SC',
'fira-mono': 'Sarasa Mono SC',
'jetbrains-mono': 'Sarasa Mono SC',
'cascadia-code': 'Sarasa Mono SC',
'cascadia-mono': 'Sarasa Mono SC',
'source-code-pro': 'Source Han Mono SC',
'ibm-plex-mono': 'Sarasa Mono SC',
'iosevka': 'Sarasa Mono SC',
'ioskeley-mono': 'Sarasa Mono SC',
'mononoki': 'Sarasa Mono SC',
'menlo': 'Sarasa Mono SC',
'monaco': 'Sarasa Mono SC',
'consolas': 'Sarasa Mono SC',
'courier-new': 'Sarasa Mono SC',
'dejavu-sans-mono':'Noto Sans Mono CJK SC',
'liberation-mono': 'Noto Sans Mono CJK SC',
'inconsolata': 'Noto Sans Mono CJK SC',
'victor-mono': 'Sarasa Mono SC',
'roboto-mono': 'Noto Sans Mono CJK SC',
'space-mono': 'Sarasa Mono SC',
'hack': 'Sarasa Mono SC',
'ubuntu-mono': 'Noto Sans Mono CJK SC',
'go-mono': 'Sarasa Mono SC',
};
export function getRecommendedCjkFor(
latinFontId: string,
platform: SupportedPlatform,
): string | null {
void platform;
return PER_FONT_CJK_PAIRING[latinFontId] ?? null;
}
/**
* Split a CSS font-family list on commas that are OUTSIDE quoted family
* names. CSS permits commas inside quoted family names (e.g.
* `"Foo, Inc. Mono"`); a naive `string.split(',')` would tokenize that
* into broken pieces like `"Foo` and `Inc. Mono"`. Exported so other
* font-parsing call sites (extractPrimaryFamily, etc.) share the same
* rules.
*/
export function splitFontFamilyList(css: string): string[] {
const tokens: string[] = [];
let buf = '';
let quote: '"' | "'" | null = null;
for (let i = 0; i < css.length; i++) {
const c = css[i];
if (quote) {
buf += c;
if (c === '\\' && i + 1 < css.length) {
buf += css[++i];
continue;
}
if (c === quote) quote = null;
continue;
}
if (c === '"' || c === "'") {
buf += c;
quote = c;
continue;
}
if (c === ',') {
const trimmed = buf.trim();
if (trimmed) tokens.push(trimmed);
buf = '';
continue;
}
buf += c;
}
const tail = buf.trim();
if (tail) tokens.push(tail);
return tokens;
}
const CSS_GENERIC_FONT_FAMILIES = new Set([
'serif',
'sans-serif',
'monospace',
'cursive',
'fantasy',
'system-ui',
'ui-serif',
'ui-sans-serif',
'ui-monospace',
'ui-rounded',
'math',
'emoji',
'fangsong',
]);
function quoteIfNeeded(family: string): string {
const trimmed = family.trim();
if (!trimmed) return '';
if (CSS_GENERIC_FONT_FAMILIES.has(trimmed.toLowerCase())) return trimmed;
if (
(trimmed.startsWith('"') && trimmed.endsWith('"'))
|| (trimmed.startsWith("'") && trimmed.endsWith("'"))
) return trimmed;
const escaped = trimmed.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
return `"${escaped}"`;
}
interface ComposeArgs {
primaryFamily: string;
userFallback: string;
latinFontId: string;
platform: SupportedPlatform;
}
export function composeFontFamilyStack(args: ComposeArgs): string {
const { primaryFamily, userFallback, latinFontId, platform } = args;
const userFallbackQuoted = userFallback.trim() ? quoteIfNeeded(userFallback) : null;
const recommended = userFallbackQuoted
? null
: (getRecommendedCjkFor(latinFontId, platform) ?? getDefaultCjkFallback(platform));
const recommendedQuoted = recommended ? quoteIfNeeded(recommended) : null;
const seen = new Set<string>();
const pieces: string[] = [];
const push = (item: string | null | undefined) => {
if (!item) return;
const key = item.toLowerCase();
if (seen.has(key)) return;
seen.add(key);
pieces.push(item);
};
// Quote-aware split so a family name like `"Foo, Inc. Mono"` keeps
// its comma intact instead of being shredded into `"Foo` / `Inc. Mono"`.
for (const p of splitFontFamilyList(primaryFamily)) {
if (p.toLowerCase() === 'monospace') continue;
push(p);
}
// Latin-only fallback (bundled via @fontsource/jetbrains-mono in
// index.tsx). Catches Latin glyphs when the primary font isn't
// installed without intercepting CJK glyphs the way the bare
// `monospace` generic would on macOS Chrome (where the generic
// monospace pulls in PingFang via system CJK fallback, masking the
// user's CJK font choice).
//
// Per-glyph CSS fallback then behaves as intended:
// - Latin chars: primary (if installed) → JetBrains Mono. Cells
// stay aligned because JetBrains Mono is true monospace.
// - CJK chars: primary (no) → JetBrains Mono (no CJK glyphs) →
// user-chosen CJK font (or per-Latin-font recommendation) →
// system CJK stack.
// - Nerd PUA: all of the above (none have PUA) → Nerd Font stack.
push('"JetBrains Mono"');
push(userFallbackQuoted);
push(recommendedQuoted);
for (const sys of CJK_SYSTEM_FALLBACK_FONTS) push(sys);
for (const nerd of NERD_FONT_FALLBACK_FONTS) push(nerd);
// Final safety net only — should rarely be reached because JetBrains
// Mono covers Latin and the CJK stack covers Chinese glyphs. Kept
// for the edge case where bundled fonts fail to load.
push('monospace');
return pieces.join(', ');
}

View File

@@ -0,0 +1,9 @@
import { Host, Snippet } from '../../domain/models';
export const INITIAL_HOSTS: Host[] = [];
export const INITIAL_SNIPPETS: Snippet[] = [
{ id: '1', label: 'Check Disk Space', command: 'df -h', tags: [] },
{ id: '2', label: 'Tail System Log', command: 'tail -f /var/log/syslog', tags: [] },
{ id: '3', label: 'Update Ubuntu', command: 'sudo apt update && sudo apt upgrade -y', tags: [] },
];

View File

@@ -0,0 +1,159 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
TERMINAL_FONTS,
getDefaultTerminalFontIdForPlatform,
detectFontPlatform,
resolveTerminalFontFamilyId,
isSameResolvedTerminalFont,
TERMINAL_FONT_AUTO,
} from './fonts';
/**
* Proportional (non-monospace) fonts must never appear in the terminal
* primary font dropdown. They produce broken cell-grid alignment because
* xterm.js samples cell width from a single probe glyph, and a font with
* variable-width Latin glyphs renders other characters with inconsistent
* widths around (or beyond) that cell.
*/
const KNOWN_PROPORTIONAL_FONTS = [
// CJK system fonts — proportional sans-serif designed for body text.
'PingFang SC',
'PingFang TC',
'PingFang HK',
'Microsoft YaHei',
'Microsoft YaHei UI',
'Hiragino Sans GB',
'Hiragino Sans',
'Heiti SC',
'Heiti TC',
// Latin proportional fonts that get mistakenly listed as "terminal
// fonts". Comic Sans MS was historically in this dropdown labeled
// "non-traditional terminal font" — picking it produced bloated cell
// widths because Comic Sans is a handwriting-style proportional face.
'Comic Sans MS',
'Arial',
'Helvetica',
'Times New Roman',
'Times',
'Georgia',
'Verdana',
'Trebuchet MS',
'Tahoma',
];
describe('TERMINAL_FONTS dropdown contents', () => {
it('does not list any known proportional font as a primary choice', () => {
for (const banned of KNOWN_PROPORTIONAL_FONTS) {
const matches = TERMINAL_FONTS.filter((f) =>
f.name === banned ||
f.family.includes(`"${banned}"`) ||
f.family.split(',')[0].trim() === banned,
);
assert.deepEqual(
matches,
[],
`${banned} must not appear in TERMINAL_FONTS — it is proportional and breaks terminal grid alignment`,
);
}
});
it('every entry has a non-empty id, name, and family', () => {
for (const font of TERMINAL_FONTS) {
assert.ok(font.id.length > 0, `${JSON.stringify(font)} missing id`);
assert.ok(font.name.length > 0, `${font.id} missing name`);
assert.ok(font.family.length > 0, `${font.id} missing family`);
}
});
it('font ids are unique', () => {
const seen = new Set<string>();
for (const font of TERMINAL_FONTS) {
assert.equal(seen.has(font.id), false, `duplicate id: ${font.id}`);
seen.add(font.id);
}
});
});
describe('getDefaultTerminalFontIdForPlatform', () => {
it('uses a locally-installed Windows font (no webfont swap on cold start)', () => {
assert.equal(getDefaultTerminalFontIdForPlatform('win32'), 'consolas');
});
it('uses the most widely pre-installed Linux monospace font', () => {
assert.equal(getDefaultTerminalFontIdForPlatform('linux'), 'dejavu-sans-mono');
});
it('keeps the macOS system font default', () => {
assert.equal(getDefaultTerminalFontIdForPlatform('darwin'), 'menlo');
});
it('falls back to the macOS default for unknown platforms', () => {
assert.equal(getDefaultTerminalFontIdForPlatform('freebsd'), 'menlo');
});
it('only ever returns ids that exist in TERMINAL_FONTS', () => {
const ids = new Set(TERMINAL_FONTS.map((f) => f.id));
for (const platform of ['darwin', 'win32', 'linux', 'unknown']) {
assert.ok(
ids.has(getDefaultTerminalFontIdForPlatform(platform)),
`default for ${platform} not in TERMINAL_FONTS`,
);
}
});
});
describe('resolveTerminalFontFamilyId', () => {
it('resolves the auto sentinel to the per-platform default', () => {
assert.equal(resolveTerminalFontFamilyId(TERMINAL_FONT_AUTO, 'Win32'), 'consolas');
assert.equal(resolveTerminalFontFamilyId(TERMINAL_FONT_AUTO, 'MacIntel'), 'menlo');
assert.equal(resolveTerminalFontFamilyId(TERMINAL_FONT_AUTO, 'Linux x86_64'), 'dejavu-sans-mono');
});
it('treats empty/nullish ids as auto (resolves per platform)', () => {
assert.equal(resolveTerminalFontFamilyId('', 'Win32'), 'consolas');
assert.equal(resolveTerminalFontFamilyId(null, 'MacIntel'), 'menlo');
assert.equal(resolveTerminalFontFamilyId(undefined, 'Linux'), 'dejavu-sans-mono');
});
it('keeps an explicit font id regardless of platform', () => {
assert.equal(resolveTerminalFontFamilyId('jetbrains-mono', 'Win32'), 'jetbrains-mono');
assert.equal(resolveTerminalFontFamilyId('menlo', 'Win32'), 'menlo');
});
});
describe('isSameResolvedTerminalFont', () => {
it('treats clicking the displayed default while stored=auto as a no-op', () => {
// Prevents the side panel from pinning a concrete per-OS font (which would
// then sync across devices) on a no-op-looking click of the shown default.
assert.equal(isSameResolvedTerminalFont('consolas', TERMINAL_FONT_AUTO, 'Win32'), true);
assert.equal(isSameResolvedTerminalFont('menlo', TERMINAL_FONT_AUTO, 'MacIntel'), true);
assert.equal(isSameResolvedTerminalFont('dejavu-sans-mono', TERMINAL_FONT_AUTO, 'Linux'), true);
});
it('reports a real change when a different font is selected', () => {
assert.equal(isSameResolvedTerminalFont('jetbrains-mono', TERMINAL_FONT_AUTO, 'Win32'), false);
});
it('compares concrete ids directly', () => {
assert.equal(isSameResolvedTerminalFont('fira-code', 'fira-code', 'Win32'), true);
assert.equal(isSameResolvedTerminalFont('fira-code', 'menlo', 'Win32'), false);
});
});
describe('detectFontPlatform', () => {
it('maps navigator.platform Windows values to win32', () => {
assert.equal(detectFontPlatform('Win32'), 'win32');
assert.equal(detectFontPlatform('Windows'), 'win32');
});
it('maps macOS and iOS values to darwin', () => {
assert.equal(detectFontPlatform('MacIntel'), 'darwin');
assert.equal(detectFontPlatform('iPhone'), 'darwin');
});
it('treats everything else (incl. Linux) as linux', () => {
assert.equal(detectFontPlatform('Linux x86_64'), 'linux');
assert.equal(detectFontPlatform(''), 'linux');
});
});

View File

@@ -0,0 +1,178 @@
/**
* Terminal Fonts Configuration
*
* `family` is the raw CSS font-family string for the Latin glyphs only.
* CJK and icon fallbacks are composed at runtime by composeFontFamilyStack()
* in cjkFonts.ts, which lets users pick the CJK font independently or have
* one chosen automatically per Latin font.
*/
export interface TerminalFont {
id: string;
name: string;
family: string;
description: string;
category: 'monospace' | 'proportional';
}
const BASE_TERMINAL_FONTS: TerminalFont[] = [
// Existing Latin monospace fonts (ids unchanged for sync compatibility)
{ id: 'menlo', name: 'Menlo', family: 'Menlo, monospace', description: 'macOS system font, clean and professional', category: 'monospace' },
{ id: 'monaco', name: 'Monaco', family: 'Monaco, monospace', description: 'Classic monospace, excellent readability', category: 'monospace' },
{ id: 'consolas', name: 'Consolas', family: 'Consolas, monospace', description: 'Windows-style monospace, clear and compact', category: 'monospace' },
{ id: 'courier-new', name: 'Courier New', family: '"Courier New", monospace', description: 'Classic typewriter style, universal support', category: 'monospace' },
{ id: 'source-code-pro', name: 'Source Code Pro', family: '"Source Code Pro", monospace', description: "Adobe's professional programming font", category: 'monospace' },
{ id: 'fira-code', name: 'Fira Code', family: '"Fira Code", monospace', description: 'Monospace font with programming ligatures', category: 'monospace' },
{ id: 'fira-mono', name: 'Fira Mono', family: '"Fira Mono", monospace', description: 'Clean monospace without ligatures', category: 'monospace' },
{ id: 'inconsolata', name: 'Inconsolata', family: 'Inconsolata, monospace', description: 'Elegant and readable monospace font', category: 'monospace' },
{ id: 'dejavu-sans-mono', name: 'DejaVu Sans Mono', family: '"DejaVu Sans Mono", monospace', description: 'Wide character support, very readable', category: 'monospace' },
{ id: 'liberation-mono', name: 'Liberation Mono', family: '"Liberation Mono", monospace', description: 'Open source monospace font, Courier alternative', category: 'monospace' },
{ id: 'jetbrains-mono', name: 'JetBrains Mono', family: '"JetBrains Mono", monospace', description: 'Professional font designed for IDEs', category: 'monospace' },
{ id: 'victor-mono', name: 'Victor Mono', family: '"Victor Mono", monospace', description: 'Stylish monospace with italic support', category: 'monospace' },
{ id: 'cascadia-code', name: 'Cascadia Code', family: '"Cascadia Code", monospace', description: "Microsoft's modern monospace font", category: 'monospace' },
{ id: 'cascadia-mono', name: 'Cascadia Mono', family: '"Cascadia Mono", monospace', description: 'Cascadia without ligatures', category: 'monospace' },
{ id: 'droid-sans-mono', name: 'Droid Sans Mono', family: '"Droid Sans Mono", monospace', description: "Google's Droid monospace font", category: 'monospace' },
{ id: 'ubuntu-mono', name: 'Ubuntu Mono', family: '"Ubuntu Mono", monospace', description: "Ubuntu's official monospace font", category: 'monospace' },
{ id: 'roboto-mono', name: 'Roboto Mono', family: '"Roboto Mono", monospace', description: "Google's Roboto monospace variant", category: 'monospace' },
{ id: 'ibm-plex-mono', name: 'IBM Plex Mono', family: '"IBM Plex Mono", monospace', description: "IBM's professional monospace font", category: 'monospace' },
{ id: 'space-mono', name: 'Space Mono', family: '"Space Mono", monospace', description: 'Geometric monospace with strong personality', category: 'monospace' },
{ id: 'input-mono', name: 'Input Mono', family: '"Input Mono", monospace', description: 'Designed specifically for coding', category: 'monospace' },
{ id: 'hack', name: 'Hack', family: 'Hack, monospace', description: 'Designed for source code, excellent in terminals', category: 'monospace' },
{ id: 'anonymous-pro', name: 'Anonymous Pro', family: '"Anonymous Pro", monospace', description: 'Designed for coding and terminal use', category: 'monospace' },
{ id: 'programmer-fonts', name: 'Programmer Fonts', family: '"Programmer Fonts", monospace', description: 'Optimized for programming with clear glyphs', category: 'monospace' },
{ id: 'pt-mono', name: 'PT Mono', family: '"PT Mono", monospace', description: "ParaType's monospace font", category: 'monospace' },
{ id: 'iosevka', name: 'Iosevka', family: 'Iosevka, monospace', description: 'Highly customizable monospace font', category: 'monospace' },
{ id: 'ioskeley-mono', name: 'Ioskeley Mono', family: '"Ioskeley Mono", monospace', description: 'Iosevka variant mimicking Berkeley Mono style', category: 'monospace' },
{ id: 'mononoki', name: 'Mononoki', family: 'Mononoki, monospace', description: 'Crisp and clear monospace with ligatures', category: 'monospace' },
{ id: 'go-mono', name: 'Go Mono', family: '"Go Mono", monospace', description: "Google Go's monospace font", category: 'monospace' },
{ id: 'overpass-mono', name: 'Overpass Mono', family: '"Overpass Mono", monospace', description: 'Open source monospace with good coverage', category: 'monospace' },
// True monospace CJK-coverage fonts only. PingFang SC and Microsoft
// YaHei UI (the OS system fonts) are deliberately omitted — they are
// proportional sans-serif designs whose Latin glyphs render with
// variable widths and whose CJK glyphs don't fit a terminal's 2x cell
// grid. Picking one as the primary font produced visibly bloated
// spacing for ASCII characters in #931.
{ id: 'sarasa-mono-sc', name: 'Sarasa Mono SC', family: '"Sarasa Mono SC", monospace', description: 'Iosevka + Source Han Sans (Simplified Chinese), 2:1 monospace', category: 'monospace' },
{ id: 'sarasa-mono-tc', name: 'Sarasa Mono TC', family: '"Sarasa Mono TC", monospace', description: 'Iosevka + Source Han Sans (Traditional Chinese), 2:1 monospace', category: 'monospace' },
{ id: 'maple-mono-cn', name: 'Maple Mono CN', family: '"Maple Mono CN", monospace', description: 'Maple Mono with unified Latin + Simplified Chinese metrics', category: 'monospace' },
{ id: 'lxgw-wenkai-mono', name: 'LXGW WenKai Mono', family: '"LXGW WenKai Mono", monospace', description: 'Monospace Kaishu (regular-script) derived from Fontworks Klee One', category: 'monospace' },
];
export const TERMINAL_FONTS: TerminalFont[] = BASE_TERMINAL_FONTS;
export const DEFAULT_FONT_SIZE = 14;
export const MIN_FONT_SIZE = 10;
export const MAX_FONT_SIZE = 32;
export type FontPlatform = 'darwin' | 'win32' | 'linux' | (string & {});
/**
* The default terminal font id for a given OS. Chosen so the primary
* Latin glyphs render with a font that ships with the OS — avoiding the
* cold-start cell-misalignment in #1647, where a bundled webfont
* (JetBrains Mono, font-display: swap) swapped in *after* xterm measured
* the cell grid and left ASCII garbled until a manual window resize.
*
* - darwin: Menlo is a macOS system font (instant, no swap).
* - win32: Consolas ships with every Windows since Vista.
* - linux: DejaVu Sans Mono is the most widely pre-installed Linux
* monospace; if a given distro lacks it, the runtime font-ready
* remeasure (useTerminalEffects) still recovers any swap.
*
* Unknown platforms fall back to the macOS default.
*/
export function getDefaultTerminalFontIdForPlatform(platform: FontPlatform): string {
if (platform === 'win32') return 'consolas';
if (platform === 'linux') return 'dejavu-sans-mono';
return 'menlo';
}
/**
* Normalize a `navigator.platform` string to the coarse OS buckets used
* by getDefaultTerminalFontIdForPlatform. Mirrors the detection already
* used for the CJK fallback stack in Terminal.tsx (Mac/Win regex on
* navigator.platform); anything else — including Linux — is `linux`.
*/
export function detectFontPlatform(navigatorPlatform: string): FontPlatform {
if (/Win/i.test(navigatorPlatform)) return 'win32';
if (/Mac|iPhone|iPad|iPod/i.test(navigatorPlatform)) return 'darwin';
return 'linux';
}
/**
* The stored sentinel meaning "follow the OS default font". It is
* persisted and cloud-synced as-is (platform-neutral), so each device
* resolves it to its own locally-installed default at render time —
* unlike a concrete platform id, which would leak one OS's font to
* another via sync and reintroduce #1647 on the receiving machine.
*/
export const TERMINAL_FONT_AUTO = 'auto';
/**
* Resolve a stored terminal font id to a concrete font id. The `auto`
* sentinel (and any empty/nullish value) resolves to the per-platform
* default; an explicit user choice is returned unchanged.
*/
export function resolveTerminalFontFamilyId(
id: string | null | undefined,
navigatorPlatform: string,
): string {
if (id && id !== TERMINAL_FONT_AUTO) return id;
return getDefaultTerminalFontIdForPlatform(detectFontPlatform(navigatorPlatform));
}
/**
* Whether two terminal font ids resolve to the same concrete font on this
* platform. Font pickers display the resolved concrete font for an `auto`
* value, so a click on the shown default arrives as that concrete id while
* the stored value is still `auto`; comparing raw would treat it as a
* change and pin a per-OS font (which then syncs across devices). Comparing
* resolved ids makes that click a correct no-op.
*/
export function isSameResolvedTerminalFont(
a: string | null | undefined,
b: string | null | undefined,
navigatorPlatform: string,
): boolean {
return (
resolveTerminalFontFamilyId(a, navigatorPlatform) ===
resolveTerminalFontFamilyId(b, navigatorPlatform)
);
}
// Font ids that earlier versions of netcatty exposed in the primary font
// dropdown but that are proportional (non-monospace) and produce broken
// cell-grid alignment when used as a terminal font. Reads should migrate
// these to a sane default.
const DEPRECATED_PRIMARY_FONT_IDS = new Set<string>([
'pingfang-sc',
'microsoft-yahei',
'comic-sans-ms',
]);
export function isDeprecatedPrimaryFontId(fontId: string | null | undefined): boolean {
return !!fontId && DEPRECATED_PRIMARY_FONT_IDS.has(fontId);
}
/**
* In-place migration for any object carrying `fontFamily` /
* `fontFamilyOverride` (Host, GroupConfig). When the saved id is one
* we've since removed from TERMINAL_FONTS, drop the override so the
* record inherits the global default rather than silently rendering
* "fallback to fonts[0]" while still claiming an override is active.
*
* Returns the (possibly new) value to assign back. Caller decides
* whether to mutate or copy; both are safe with this shape.
*/
export function migrateDeprecatedFontOverride<
T extends { fontFamily?: string; fontFamilyOverride?: boolean },
>(record: T): T {
if (!isDeprecatedPrimaryFontId(record.fontFamily)) return record;
const next = { ...record };
delete next.fontFamily;
if (next.fontFamilyOverride === true) {
next.fontFamilyOverride = false;
}
return next;
}

View File

@@ -0,0 +1,30 @@
export type LocaleOption = {
id: string;
label: string;
};
export const DEFAULT_UI_LOCALE = 'en';
// Add new languages by appending to this list and providing message dictionaries.
export const SUPPORTED_UI_LOCALES: LocaleOption[] = [
{ id: 'en', label: 'English' },
{ id: 'es', label: 'Español' },
{ id: 'ru', label: 'Русский' },
{ id: 'zh-CN', label: '简体中文' },
{ id: 'zh-TW', label: '繁體中文' },
];
const isSupportedLocale = (locale: string): boolean => {
return SUPPORTED_UI_LOCALES.some((l) => l.id === locale);
};
export const resolveSupportedLocale = (locale: string): string => {
if (isSupportedLocale(locale)) return locale;
const base = locale.split('-')[0] || locale;
const baseExact = SUPPORTED_UI_LOCALES.find((l) => l.id === base)?.id;
if (baseExact) return baseExact;
const basePrefix = SUPPORTED_UI_LOCALES.find((l) => l.id.startsWith(`${base}-`))?.id;
if (basePrefix) return basePrefix;
return DEFAULT_UI_LOCALE;
};

View File

@@ -0,0 +1,18 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
DEFAULT_SSH_TRANSPORT_IDLE_TTL_MS,
resolveSshTransportIdleTtlMs,
} from "./sshTransportIdleTtl.ts";
test("ssh transport idle TTL defaults to 5 minutes", () => {
assert.equal(DEFAULT_SSH_TRANSPORT_IDLE_TTL_MS, 5 * 60_000);
assert.equal(resolveSshTransportIdleTtlMs(() => null), DEFAULT_SSH_TRANSPORT_IDLE_TTL_MS);
});
test("ssh transport idle TTL accepts presets including never-reclaim", () => {
assert.equal(resolveSshTransportIdleTtlMs(() => 60_000), 60_000);
assert.equal(resolveSshTransportIdleTtlMs(() => 0), 0);
assert.equal(resolveSshTransportIdleTtlMs(() => 123), DEFAULT_SSH_TRANSPORT_IDLE_TTL_MS);
});

View File

@@ -0,0 +1,33 @@
/**
* Idle park TTL for the main-process SSH transport registry
* (shell / SFTP / transfer / port-forward shared connections).
* 0 keeps transports warm until app quit or explicit discard (never auto-reclaim).
* Positive values park for that many ms after the last lease returns.
*/
export const DEFAULT_SSH_TRANSPORT_IDLE_TTL_MS = 5 * 60_000;
export const SSH_TRANSPORT_IDLE_TTL_PRESETS_MS = [
60_000,
5 * 60_000,
15 * 60_000,
30 * 60_000,
0,
] as const;
export type SshTransportIdleTtlMs = (typeof SSH_TRANSPORT_IDLE_TTL_PRESETS_MS)[number];
export function resolveSshTransportIdleTtlMs(
readStoredValue: () => number | null | undefined,
): number {
const stored = readStoredValue();
if (stored === 0) return 0;
if (
typeof stored === "number"
&& Number.isFinite(stored)
&& (SSH_TRANSPORT_IDLE_TTL_PRESETS_MS as readonly number[]).includes(stored)
) {
return stored;
}
return DEFAULT_SSH_TRANSPORT_IDLE_TTL_MS;
}

View File

@@ -0,0 +1,298 @@
export const STORAGE_KEY_HOSTS = 'netcatty_hosts_v1';
export const STORAGE_KEY_KEYS = 'netcatty_keys_v1';
export const STORAGE_KEY_GROUPS = 'netcatty_groups_v1';
export const STORAGE_KEY_CUSTOM_GROUPS = STORAGE_KEY_GROUPS;
export const STORAGE_KEY_SNIPPETS = 'netcatty_snippets_v1';
export const STORAGE_KEY_SNIPPET_PACKAGES = 'netcatty_snippet_packages_v1';
export const STORAGE_KEY_NOTES = 'netcatty_notes_v1';
export const STORAGE_KEY_NOTE_GROUPS = 'netcatty_note_groups_v1';
/** Last-filled values per snippet id for {{variable}} placeholders. */
export const STORAGE_KEY_SNIPPET_VAR_VALUES = 'netcatty_snippet_var_values_v1';
export const STORAGE_KEY_THEME = 'netcatty_theme_v1';
export const STORAGE_KEY_COLOR = 'netcatty_color_v1';
export const STORAGE_KEY_ACCENT_MODE = 'netcatty_accent_mode_v1';
export const STORAGE_KEY_UI_THEME_LIGHT = 'netcatty_ui_theme_light_v1';
export const STORAGE_KEY_UI_THEME_DARK = 'netcatty_ui_theme_dark_v1';
export const STORAGE_KEY_UI_FONT_FAMILY = 'netcatty_ui_font_family_v1';
export const STORAGE_KEY_SYNC = 'netcatty_sync_v1';
/** Device-local experimental convergent-sync toggle; never included in SyncPayload settings. */
export const STORAGE_KEY_CONVERGENT_SYNC_CONFIG = 'netcatty_convergent_sync_config_v2';
export const STORAGE_KEY_TERM_THEME = 'netcatty_term_theme_v1';
export const STORAGE_KEY_TERM_FOLLOW_APP_THEME = 'netcatty_term_follow_app_theme_v1';
export const STORAGE_KEY_TERM_THEME_DARK = 'netcatty_term_theme_dark_v1';
export const STORAGE_KEY_TERM_THEME_LIGHT = 'netcatty_term_theme_light_v1';
export const STORAGE_KEY_TERM_FONT_FAMILY = 'netcatty_term_font_family_v1';
export const STORAGE_KEY_TERM_FONT_SIZE = 'netcatty_term_font_size_v1';
export const STORAGE_KEY_TERM_SETTINGS = 'netcatty_term_settings_v1';
export const STORAGE_KEY_HOTKEY_SCHEME = 'netcatty_hotkey_scheme_v1';
export const STORAGE_KEY_CUSTOM_KEY_BINDINGS = 'netcatty_custom_key_bindings_v1';
export const STORAGE_KEY_HOTKEY_RECORDING = 'netcatty_hotkey_recording_v1';
export const STORAGE_KEY_CUSTOM_CSS = 'netcatty_custom_css_v1';
export const STORAGE_KEY_UI_LANGUAGE = 'netcatty_ui_language_v1';
export const STORAGE_KEY_PORT_FORWARDING = 'netcatty_port_forwarding_v1';
/** Width (px) shared by port forwarding edit, wizard, and host picker panels. */
export const STORAGE_KEY_PORT_FORWARDING_PANEL_WIDTH = 'netcatty_port_forwarding_panel_width_v1';
export const STORAGE_KEY_PF_PREFER_FORM_MODE = 'netcatty_pf_prefer_form_mode_v1';
export const STORAGE_KEY_PF_VIEW_MODE = 'netcatty_pf_view_mode_v1';
export const STORAGE_KEY_KNOWN_HOSTS = 'netcatty_known_hosts_v1';
export const STORAGE_KEY_SHELL_HISTORY = 'netcatty_shell_history_v1';
export const STORAGE_KEY_CONNECTION_LOGS = 'netcatty_connection_logs_v1';
/** Side store for unsaved connection-log terminal replay buffers (main blob omits them for perf). */
export const STORAGE_KEY_CONNECTION_LOG_TERMINAL_DATA = 'netcatty_connection_log_terminal_data_v1';
export const STORAGE_KEY_SESSION_RESTORE = 'netcatty_session_restore_v1';
export const STORAGE_KEY_RESTORE_PREVIOUS_SESSION = 'netcatty_restore_previous_session_v1';
export const STORAGE_KEY_RESTORE_TERMINAL_CWD = 'netcatty_restore_terminal_cwd_v1';
/** Cold-start landing: vault (home) or local terminal when nothing is restored. */
export const STORAGE_KEY_STARTUP_LANDING = 'netcatty_startup_landing_v1';
export const STORAGE_KEY_IDENTITIES = 'netcatty_identities_v1';
export const STORAGE_KEY_PROXY_PROFILES = 'netcatty_proxy_profiles_v1';
export const STORAGE_KEY_VAULT_HOSTS_VIEW_MODE = 'netcatty_vault_hosts_view_mode_v1';
export const STORAGE_KEY_VAULT_HOSTS_SORT_MODE = 'netcatty_vault_hosts_sort_mode_v1';
export const STORAGE_KEY_VAULT_HOSTS_TREE_EXPANDED = 'netcatty_vault_hosts_tree_expanded_v1';
export const STORAGE_KEY_VAULT_SIDEBAR_COLLAPSED = 'netcatty_vault_sidebar_collapsed_v1';
export const STORAGE_KEY_VAULT_SIDEBAR_WIDTH = 'netcatty_vault_sidebar_width_v1';
export const STORAGE_KEY_VAULT_KEYS_VIEW_MODE = 'netcatty_vault_keys_view_mode_v1';
export const STORAGE_KEY_VAULT_PROXY_PROFILES_VIEW_MODE = 'netcatty_vault_proxy_profiles_view_mode_v1';
export const STORAGE_KEY_VAULT_SNIPPETS_VIEW_MODE = 'netcatty_vault_snippets_view_mode_v1';
export const STORAGE_KEY_VAULT_NOTES_VIEW_MODE = 'netcatty_vault_notes_view_mode_v1';
export const STORAGE_KEY_VAULT_NOTES_EDITOR_MODE = 'netcatty_vault_notes_editor_mode_v1';
export const STORAGE_KEY_VAULT_NOTES_SELECTED_GROUP = 'netcatty_vault_notes_selected_group_v1';
export const STORAGE_KEY_VAULT_NOTES_TREE_WIDTH = 'netcatty_vault_notes_tree_width_v1';
export const STORAGE_KEY_VAULT_NOTES_FONT_FAMILY = 'netcatty_vault_notes_font_family_v1';
export const STORAGE_KEY_VAULT_NOTES_FONT_SIZE = 'netcatty_vault_notes_font_size_v1';
export const STORAGE_KEY_VAULT_NOTES_CODE_FONT_SIZE = 'netcatty_vault_notes_code_font_size_v1';
/** Inline snippet/script edit panel width (px). */
export const STORAGE_KEY_SNIPPETS_PANEL_WIDTH = 'netcatty_snippets_panel_width_v1';
/** Inline vault host/group details panel width (px). */
export const STORAGE_KEY_VAULT_HOST_PANEL_WIDTH = 'netcatty_vault_host_panel_width_v1';
/** Inline snippet script editor height (px) in vault edit panel. */
export const STORAGE_KEY_SNIPPET_SCRIPT_EDITOR_HEIGHT = 'netcatty_snippet_script_editor_height_v1';
/** Automation script Monaco editor height (px) in vault sidebar. */
export const STORAGE_KEY_SCRIPT_EDITOR_HEIGHT = 'netcatty_script_editor_height_v1';
/** Terminal compose bar total height (px). */
export const STORAGE_KEY_COMPOSE_BAR_HEIGHT = 'netcatty_compose_bar_height_v1';
/** Snippet IDs pinned to the terminal compose bar quick strip. */
export const STORAGE_KEY_COMPOSE_BAR_PINNED_SNIPPETS = 'netcatty_compose_bar_pinned_snippets_v1';
export const STORAGE_KEY_VAULT_KNOWN_HOSTS_VIEW_MODE = 'netcatty_vault_known_hosts_view_mode_v1';
/** Device-local: silently import system OpenSSH known_hosts on Vault load (default true). */
export const STORAGE_KEY_AUTO_IMPORT_SYSTEM_KNOWN_HOSTS = 'netcatty_auto_import_system_known_hosts_v1';
// Update check
export const STORAGE_KEY_UPDATE_LAST_CHECK = 'netcatty_update_last_check_v1';
export const STORAGE_KEY_UPDATE_DISMISSED_VERSION = 'netcatty_update_dismissed_version_v1';
export const STORAGE_KEY_UPDATE_LATEST_RELEASE = 'netcatty_update_latest_release_v1';
export const STORAGE_KEY_AUTO_UPDATE_ENABLED = 'netcatty_auto_update_enabled_v1';
export const STORAGE_KEY_LOCAL_VAULT_BACKUP_MAX_COUNT = 'netcatty_local_vault_backup_max_count_v1';
export const STORAGE_KEY_LOCAL_VAULT_BACKUP_LAST_APP_VERSION = 'netcatty_local_vault_backup_last_app_version_v1';
/**
* Cross-window barrier: set while a local vault restore is applying so
* auto-sync in another window doesn't upload a pre-restore snapshot
* concurrently. The value is an epoch-ms deadline — auto-sync treats any
* value in the future as "restore in progress" and any value in the past
* as a stale lock that can be ignored. See useAutoSync and
* CloudSyncSettings for readers/writers.
*/
export const STORAGE_KEY_VAULT_RESTORE_IN_PROGRESS_UNTIL = 'netcatty_vault_restore_in_progress_until_v1';
/**
* Apply-in-progress sentinel. Set before a destructive applySyncPayload
* starts writing and cleared after it completes successfully. If this
* value is present on a later startup, the previous apply was
* interrupted mid-way (renderer crash, power loss, IPC failure) and the
* local vault is a partial mix of pre-apply and post-apply state.
* Auto-sync must refuse to push in that window — otherwise the partial
* state would silently overwrite an intact cloud copy — until the user
* manually restores from a protective backup or completes a full merge.
* The value is a JSON-encoded record (startedAt, protectiveBackupId,
* source) so the UI can surface a specific recovery hint rather than a
* generic "something broke" warning.
*/
export const STORAGE_KEY_VAULT_APPLY_IN_PROGRESS = 'netcatty_vault_apply_in_progress_v1';
// SFTP File Opener Associations
export const STORAGE_KEY_SFTP_FILE_ASSOCIATIONS = 'netcatty_sftp_file_associations_v1';
export const STORAGE_KEY_SFTP_DEFAULT_OPENER = 'netcatty_sftp_default_opener_v1';
// SFTP Local Bookmarks
export const STORAGE_KEY_SFTP_LOCAL_BOOKMARKS = 'netcatty_sftp_local_bookmarks_v1';
// SFTP Global Bookmarks (shared across all hosts)
export const STORAGE_KEY_SFTP_GLOBAL_BOOKMARKS = 'netcatty_sftp_global_bookmarks_v1';
// SFTP Settings
export const STORAGE_KEY_SFTP_DOUBLE_CLICK_BEHAVIOR = 'netcatty_sftp_double_click_behavior_v1';
export const STORAGE_KEY_SFTP_AUTO_SYNC = 'netcatty_sftp_auto_sync_v1';
export const STORAGE_KEY_SFTP_SHOW_HIDDEN_FILES = 'netcatty_sftp_show_hidden_files_v1';
export const STORAGE_KEY_SFTP_USE_COMPRESSED_UPLOAD = 'netcatty_sftp_use_compressed_upload_v1';
export const STORAGE_KEY_SFTP_TRANSFER_CENTER = 'netcatty_sftp_transfer_center_v1';
export const STORAGE_KEY_SFTP_AUTO_OPEN_SIDEBAR = 'netcatty_sftp_auto_open_sidebar_v1';
export const STORAGE_KEY_SFTP_FOLLOW_TERMINAL_CWD = 'netcatty_sftp_follow_terminal_cwd_v1';
export const STORAGE_KEY_SFTP_DEFAULT_VIEW_MODE = 'netcatty_sftp_default_view_mode_v1';
/** Compact vs comfortable SFTP file-list row density. */
export const STORAGE_KEY_SFTP_LIST_DENSITY = 'netcatty_sftp_list_density_v1';
export const STORAGE_KEY_SFTP_HOST_VIEW_MODES = 'netcatty_sftp_host_view_modes_v1';
export const STORAGE_KEY_SFTP_VISIBLE_COLUMNS = 'netcatty_sftp_visible_columns_v1';
export const STORAGE_KEY_SFTP_DIRECTORIES_FIRST = 'netcatty_sftp_directories_first_v1';
/** Dense SFTP toolbar actions: show / collapse / hide + order. */
export const STORAGE_KEY_SFTP_TOOLBAR_LAYOUT = 'netcatty_sftp_toolbar_layout_v1';
/** Dense terminal session toolbar actions: show / collapse / hide + order. */
export const STORAGE_KEY_TERMINAL_TOOLBAR_LAYOUT = 'netcatty_terminal_toolbar_layout_v1';
/** Terminal host-tree sidebar toolbar: show / collapse / hide + order. */
export const STORAGE_KEY_TERMINAL_HOST_TREE_TOOLBAR_LAYOUT =
'netcatty_terminal_host_tree_toolbar_layout_v1';
/** Side-panel tab strip: show / collapse / hide + order (supersedes order-only key when present). */
export const STORAGE_KEY_TERMINAL_SIDE_PANEL_TAB_LAYOUT = 'netcatty_terminal_side_panel_tab_layout_v1';
/** System Manager sub-tabs (Overview / Processes / …): show / collapse / hide + order. */
export const STORAGE_KEY_SYSTEM_MANAGER_TAB_LAYOUT = 'netcatty_system_manager_tab_layout_v1';
export const STORAGE_KEY_SFTP_TRANSFER_PANEL_HEIGHT = 'netcatty_sftp_transfer_panel_height_v1';
export const STORAGE_KEY_SFTP_TRANSFER_CHILD_NAME_WIDTH = 'netcatty_sftp_transfer_child_name_width_v1';
// Editor Settings
export const STORAGE_KEY_EDITOR_WORD_WRAP = 'netcatty_editor_word_wrap_v1';
// Session Logs Settings
export const STORAGE_KEY_SESSION_LOGS_ENABLED = 'netcatty_session_logs_enabled_v1';
export const STORAGE_KEY_SESSION_LOGS_DIR = 'netcatty_session_logs_dir_v1';
export const STORAGE_KEY_SESSION_LOGS_FORMAT = 'netcatty_session_logs_format_v1';
export const STORAGE_KEY_SESSION_LOGS_TIMESTAMPS_ENABLED = 'netcatty_session_logs_timestamps_enabled_v1';
export const STORAGE_KEY_SSH_DEBUG_LOGS_ENABLED = 'netcatty_ssh_debug_logs_enabled_v1';
export const STORAGE_KEY_SSH_DEEP_LINK_ENABLED = 'netcatty_ssh_deep_link_enabled_v1';
export const STORAGE_KEY_JMS_DEEP_LINK_ENABLED = 'netcatty_jms_deep_link_enabled_v1';
/** Windows Explorer "Open in Netcatty" folder context menu (device-local). */
export const STORAGE_KEY_EXPLORER_CONTEXT_MENU_ENABLED = 'netcatty_explorer_context_menu_enabled_v1';
// Archived legacy key records that are no longer supported by the app (e.g. biometric/WebAuthn/FIDO2 experiments).
export const STORAGE_KEY_LEGACY_KEYS = 'netcatty_legacy_keys_v1';
// Managed Sources - external files that manage groups of hosts (e.g., ~/.ssh/config)
export const STORAGE_KEY_MANAGED_SOURCES = 'netcatty_managed_sources_v1';
// Global Toggle Window Settings (Quake Mode)
export const STORAGE_KEY_TOGGLE_WINDOW_HOTKEY = 'netcatty_toggle_window_hotkey_v1';
export const STORAGE_KEY_CLOSE_TO_TRAY = 'netcatty_close_to_tray_v1';
/** Optimistic cache only — the source of truth is app.getLoginItemSettings() in the main process. */
export const STORAGE_KEY_AUTO_LAUNCH_ENABLED = 'netcatty_auto_launch_enabled_v1';
/** App-level HTTP(S) proxy for cloud sync / AI (not SSH ProxyJump). */
export const STORAGE_KEY_HTTP_NETWORK_PROXY = 'netcatty_http_network_proxy_v1';
export const STORAGE_KEY_GLOBAL_HOTKEY_ENABLED = 'netcatty_global_hotkey_enabled_v1';
export const STORAGE_KEY_WINDOW_OPACITY = 'netcatty_window_opacity_v1';
export const STORAGE_KEY_APP_ICON_VARIANT = 'netcatty_app_icon_variant_v1';
// Custom Terminal Themes
export const STORAGE_KEY_CUSTOM_THEMES = 'netcatty_custom_themes_v1';
// AI Settings
export const STORAGE_KEY_AI_PROVIDERS = 'netcatty_ai_providers_v1';
export const STORAGE_KEY_AI_ACTIVE_PROVIDER = 'netcatty_ai_active_provider_v1';
export const STORAGE_KEY_AI_ACTIVE_MODEL = 'netcatty_ai_active_model_v1';
export const STORAGE_KEY_AI_PERMISSION_MODE = 'netcatty_ai_permission_mode_v1';
export const STORAGE_KEY_AI_TOOL_INTEGRATION_MODE = 'netcatty_ai_tool_integration_mode_v1';
export const STORAGE_KEY_AI_HOST_PERMISSIONS = 'netcatty_ai_host_permissions_v1';
export const STORAGE_KEY_AI_EXTERNAL_AGENTS = 'netcatty_ai_external_agents_v1';
export const STORAGE_KEY_AI_DEFAULT_AGENT = 'netcatty_ai_default_agent_v1';
export const STORAGE_KEY_AI_COMMAND_BLOCKLIST = 'netcatty_ai_command_blocklist_v1';
export const STORAGE_KEY_AI_COMMAND_TIMEOUT = 'netcatty_ai_command_timeout_v1';
export const STORAGE_KEY_AI_RESPONSE_IDLE_TIMEOUT = 'netcatty_ai_response_idle_timeout_v1';
export const STORAGE_KEY_AI_MAX_ITERATIONS = 'netcatty_ai_max_iterations_v1';
export const STORAGE_KEY_AI_SESSIONS = 'netcatty_ai_sessions_v1';
export const STORAGE_KEY_AI_ACTIVE_SESSION_MAP = 'netcatty_ai_active_session_map_v1';
export const STORAGE_KEY_AI_AGENT_MODEL_MAP = 'netcatty_ai_agent_model_map_v1';
export const STORAGE_KEY_AI_AGENT_PROVIDER_MAP = 'netcatty_ai_agent_provider_map_v1';
export const STORAGE_KEY_AI_AGENT_THINKING_MAP = 'netcatty_ai_agent_thinking_map_v1';
export const STORAGE_KEY_AI_COMPOSER_MODEL_PREFS = 'netcatty_ai_composer_model_prefs_v1';
export const STORAGE_KEY_AI_WEB_SEARCH = 'netcatty_ai_web_search_v1';
export const STORAGE_KEY_AI_QUICK_MESSAGES = 'netcatty_ai_quick_messages_v1';
/** Confirm-mode permission grant memory (capability + session/command patterns). */
export const STORAGE_KEY_AI_PERMISSION_GRANTS = 'netcatty_ai_permission_grants_v1';
export const STORAGE_KEY_AI_SHOW_TERMINAL_SELECTION_ACTION = 'netcatty_ai_show_terminal_selection_action_v1';
/** External MCP: whether the user last enabled the public catalog MCP endpoint. */
export const STORAGE_KEY_AI_EXTERNAL_MCP_ENABLED = 'netcatty_ai_external_mcp_enabled_v1';
/** External MCP lifecycle mode: temporary (idle timeout) or persistent (restore on launch). */
export const STORAGE_KEY_AI_EXTERNAL_MCP_MODE = 'netcatty_ai_external_mcp_mode_v1';
/** External MCP idle timeout in minutes (temporary mode only). */
export const STORAGE_KEY_AI_EXTERNAL_MCP_IDLE_TIMEOUT_MINUTES = 'netcatty_ai_external_mcp_idle_timeout_minutes_v1';
/** External MCP: whether host_open should surface/focus the main window (default true). */
export const STORAGE_KEY_AI_EXTERNAL_MCP_FOCUS_ON_HOST_OPEN = 'netcatty_ai_external_mcp_focus_on_host_open_v1';
/** Idle timeout for terminal sessions opened by an AI through host_open. */
export const STORAGE_KEY_AI_SESSION_IDLE_TIMEOUT_MINUTES = 'netcatty_ai_session_idle_timeout_minutes_v1';
/** External MCP: whether host_open sessions stay hidden from the tab bar (default false). */
export const STORAGE_KEY_AI_EXTERNAL_MCP_SILENT_SESSIONS = 'netcatty_ai_external_mcp_silent_sessions_v1';
/** AI panel diagnostic hide list (comma-separated part names). */
export const STORAGE_KEY_AI_PANEL_DIAGNOSTIC_HIDE = 'netcatty.aiDebug.hide';
/** AI panel React profiler toggle. */
export const STORAGE_KEY_AI_PANEL_DIAGNOSTIC_PROFILE = 'netcatty.aiDebug.profile';
// SFTP Transfer Concurrency
export const STORAGE_KEY_SFTP_TRANSFER_CONCURRENCY = 'netcatty_sftp_transfer_concurrency_v1';
/**
* Legacy key only. Folder full-tree pre-scan was removed; values are ignored
* so old localStorage / sync payloads do not resurrect a live setting.
*/
export const STORAGE_KEY_SFTP_FOLDER_PRESCAN = 'netcatty_sftp_folder_prescan_v1';
/** Skip files when target size + mtime already match the source (rsync-like). */
export const STORAGE_KEY_SFTP_SKIP_UNCHANGED = 'netcatty_sftp_skip_unchanged_v1';
/**
* @deprecated Legacy transfer-pool idle TTL. No longer read; SSH keep-alive uses
* STORAGE_KEY_SSH_TRANSPORT_IDLE_TTL_MS. Kept so old localStorage entries are ignored safely.
*/
export const STORAGE_KEY_SFTP_TRANSFER_POOL_IDLE_TTL_MS = 'netcatty_sftp_transfer_pool_idle_ttl_ms_v1';
/** Shared SSH transport idle park TTL in ms (0 = keep until app quit). */
export const STORAGE_KEY_SSH_TRANSPORT_IDLE_TTL_MS = 'netcatty_ssh_transport_idle_ttl_ms_v1';
// Workspace Focus Indicator Style
export const STORAGE_KEY_WORKSPACE_FOCUS_STYLE = 'netcatty_workspace_focus_style_v1';
// Vault: Show Recently Connected hosts section
export const STORAGE_KEY_SHOW_RECENT_HOSTS = 'netcatty_show_recent_hosts_v1';
export const STORAGE_KEY_HOST_CLICK_BEHAVIOR = 'netcatty_host_click_behavior_v1';
export const STORAGE_KEY_SHOW_ONLY_UNGROUPED_HOSTS_IN_ROOT = 'netcatty_show_only_ungrouped_hosts_in_root_v1';
// Top tabs: Show standalone SFTP view tab
export const STORAGE_KEY_SHOW_SFTP_TAB = 'netcatty_show_sftp_tab_v1';
export const STORAGE_KEY_SHOW_HOST_TREE_SIDEBAR = 'netcatty_show_host_tree_sidebar_v1';
// Shortcuts: Cmd/Ctrl+[1...9] and Ctrl+Tab skip pinned Vault/SFTP tabs
export const STORAGE_KEY_SHELL_ONLY_TAB_NUMBER_SHORTCUTS = 'netcatty_shell_only_tab_number_shortcuts_v1';
// Shortcuts: show 1...9 badge on tabs that match number switch shortcuts
export const STORAGE_KEY_SHOW_TAB_NUMBER_BADGES = 'netcatty_show_tab_number_badges_v1';
// Shortcuts: disable terminal font zoom shortcuts
export const STORAGE_KEY_DISABLE_TERMINAL_FONT_ZOOM = 'netcatty_disable_terminal_font_zoom_v1';
/** Host IDs for which the "enable Network Device Mode" suggestion has already been shown/handled (suggest once per host). */
export const STORAGE_KEY_NETWORK_DEVICE_SUGGEST_HANDLED = 'netcatty_network_device_suggest_handled_v1';
// Group Configurations (default settings inherited by hosts)
export const STORAGE_KEY_GROUP_CONFIGS = 'netcatty_group_configs_v1';
/** Crash-recovery journal for the plugin importer multi-key Vault commit. */
export const STORAGE_KEY_PLUGIN_IMPORT_TRANSACTION = 'netcatty_plugin_import_transaction_v1';
// Side Panel
export const STORAGE_KEY_SIDE_PANEL_WIDTH = 'netcatty_side_panel_width';
export const STORAGE_KEY_TERMINAL_SIDE_PANEL_TAB_ORDER = 'netcatty_terminal_side_panel_tab_order_v1';
export const STORAGE_KEY_TERMINAL_SIDE_PANEL_AUTO_OPEN = 'netcatty_terminal_side_panel_auto_open_v1';
export const STORAGE_KEY_TERMINAL_SIDE_PANEL_AUTO_OPEN_TAB = 'netcatty_terminal_side_panel_auto_open_tab_v1';
export const STORAGE_KEY_WORKSPACE_FOCUS_SIDEBAR_WIDTH = 'netcatty_workspace_focus_sidebar_width';
export const STORAGE_KEY_TERMINAL_HOST_TREE_WIDTH = 'netcatty_terminal_host_tree_width_v1';
export const STORAGE_KEY_TERMINAL_HOST_TREE_COLLAPSED = 'netcatty_terminal_host_tree_collapsed_v1';
export const STORAGE_KEY_TERMINAL_COMPOSE_BAR_OPEN = 'netcatty_terminal_compose_bar_open_v1';
export const STORAGE_KEY_TERMINAL_SEARCH_OPEN = 'netcatty_terminal_search_open_v1';
export const STORAGE_KEY_TERMINAL_ENCODING_BY_HOST_PREFIX = 'netcatty_terminal_encoding_by_host_v1:';
export const STORAGE_KEY_TERMINAL_YMODEM_SEND_DIR = 'netcatty_terminal_ymodem_send_dir_v1';
// Port Forwarding (transient cross-window broadcast key)
export const STORAGE_KEY_PF_RECONNECT_CANCEL = '__netcatty_pf_cancel_reconnect';
// Default SSH Key Passphrases (for ~/.ssh keys not managed in the vault)
export const STORAGE_KEY_DEFAULT_KEY_PASSPHRASES = 'netcatty_default_key_passphrases_v1';
// Plugin sync sidecars / availability. Literals MUST match domain/sync
// SYNC_STORAGE_KEYS (PLUGIN_SIDECARS_* / AVAILABLE_PLUGIN_SYNC_PROVIDERS).
export const STORAGE_KEY_PLUGIN_SIDECARS_LAST_KNOWN = 'netcatty_plugin_sidecars_last_known_v1';
export const STORAGE_KEY_PLUGIN_SIDECARS_PENDING_REMOTE = 'netcatty_plugin_sidecars_pending_remote_v1';
export const STORAGE_KEY_AVAILABLE_PLUGIN_SYNC_PROVIDERS = 'netcatty_available_plugin_sync_providers_v1';
// Debug Flags (no _v1 suffix — developer-only, not persisted data)
export const STORAGE_KEY_DEBUG_HOTKEYS = 'debug.hotkeys';
export const STORAGE_KEY_DEBUG_UPDATE_DEMO = 'debug.updateDemo';

View File

@@ -0,0 +1,7 @@
"use strict";
/**
* Terminal flow-control thresholds (single source: terminalFlowConstants.json).
* Aligned with VS Code FlowControlConstants (terminal.ts).
*/
module.exports = require("./terminalFlowConstants.json");

View File

@@ -0,0 +1,14 @@
{
"FLOW_HIGH_WATER_MARK": 1048576,
"FLOW_LOW_WATER_MARK": 8192,
"FLOW_CHAR_COUNT_ACK_SIZE": 4096,
"MAX_PENDING_WRITE_COALESCE_BYTES": 1048576,
"MAX_PENDING_WRITE_COALESCE_BYTES_FLOOD": 131072,
"MAX_TERMINAL_PLAIN_WRITE_CHUNK_BYTES": 131072,
"MAX_TERMINAL_UNBROKEN_WRITE_CHUNK_BYTES": 131072,
"MAX_TERMINAL_WRITE_QUEUE_DRAIN_BYTES": 524288,
"TERMINAL_LONG_LINE_PRESSURE_BYTES": 65536,
"TERMINAL_AUX_LONG_LINE_SCAN_LIMIT_CHARS": 65536,
"XTERM_WRITE_CALLBACK_FAST_PATH_MAX_BYTES": 1024,
"XTERM_WRITE_CALLBACK_BATCH_BYTES": 100000
}

View File

@@ -0,0 +1,48 @@
import type { TerminalTheme } from '../../domain/models';
import { classicTerminalThemes } from './terminalThemes/classic';
import { systemPresetTerminalThemes } from './terminalThemes/systemPresets';
import { coreTerminalThemes } from './terminalThemes/core';
import { extraTerminalThemes } from './terminalThemes/extra';
import { modernTerminalThemes } from './terminalThemes/modern';
import { uiMatchTerminalThemes } from './terminalThemes/uiMatch';
// Re-export for convenience
export type TerminalThemeConfig = TerminalTheme;
const UI_MATCH_TERMINAL_THEME_IDS = new Set([
'ui-snow',
'ui-pure-white',
'ui-ivory',
'ui-mist',
'ui-mint',
'ui-sand',
'ui-lavender',
'ui-pure-black',
'ui-midnight',
'ui-deep-blue',
'ui-vscode',
'ui-graphite',
'ui-obsidian',
'ui-forest',
]);
export const TERMINAL_THEMES: TerminalTheme[] = [
...coreTerminalThemes,
...uiMatchTerminalThemes,
...modernTerminalThemes,
...classicTerminalThemes,
...systemPresetTerminalThemes,
...extraTerminalThemes,
];
const TERMINAL_THEME_BY_ID = new Map(TERMINAL_THEMES.map((theme) => [theme.id, theme]));
export const getBuiltinTerminalThemeById = (themeId: string): TerminalTheme | undefined =>
TERMINAL_THEME_BY_ID.get(themeId);
export const isUiMatchTerminalThemeId = (themeId: string): boolean =>
UI_MATCH_TERMINAL_THEME_IDS.has(themeId);
export const USER_VISIBLE_TERMINAL_THEMES: TerminalTheme[] = TERMINAL_THEMES.filter(
(theme) => !isUiMatchTerminalThemeId(theme.id),
);

View File

@@ -0,0 +1,652 @@
import type { TerminalTheme } from '../../../domain/models';
export const classicTerminalThemes: TerminalTheme[] = [
{
id: 'romania-night',
name: 'Romania Night',
type: 'dark',
colors: {
background: '#1e1b32',
foreground: '#e0b5ff',
cursor: '#a78bfa',
selection: '#3d2e5e',
black: '#0d0a1a',
red: '#d96880',
green: '#7ac67c',
yellow: '#d4b896',
blue: '#7ca3d4',
magenta: '#a58cd6',
cyan: '#90bfe4',
white: '#e0b5ff',
brightBlack: '#6b5b7f',
brightRed: '#d96880',
brightGreen: '#7ac67c',
brightYellow: '#d4b896',
brightBlue: '#7ca3d4',
brightMagenta: '#a58cd6',
brightCyan: '#90bfe4',
brightWhite: '#e0b5ff'
}
},
{
id: 'romania-day',
name: 'Romania Day',
type: 'light',
colors: {
background: '#fdf3f7',
foreground: '#5e4c6f',
cursor: '#a78bfa',
selection: '#e8d4f0',
black: '#1e1b32',
red: '#a73a5e',
green: '#4a7c4e',
yellow: '#a38060',
blue: '#4a6fa5',
magenta: '#6b5b7f',
cyan: '#4a7fb2',
white: '#f5e0f0',
brightBlack: '#8b7c9f',
brightRed: '#c75880',
brightGreen: '#5a9c5e',
brightYellow: '#c49878',
brightBlue: '#5a8fb5',
brightMagenta: '#8b7b9f',
brightCyan: '#5a8fb5',
brightWhite: '#5e4c6f'
}
},
{
id: 'aubergine',
name: 'Aubergine',
type: 'dark',
colors: {
background: '#2d1b3d',
foreground: '#d4a5d4',
cursor: '#ff8ccc',
selection: '#5d3b6d',
black: '#1a0f2e',
red: '#d46a6a',
green: '#8fb06d',
yellow: '#d4a574',
blue: '#6d99d4',
magenta: '#9d7eca',
cyan: '#7ecad4',
white: '#d4a5d4',
brightBlack: '#5d3b6d',
brightRed: '#ff8888',
brightGreen: '#9fc66d',
brightYellow: '#ffc585',
brightBlue: '#8db5ff',
brightMagenta: '#c9a5ff',
brightCyan: '#a5e6ff',
brightWhite: '#ffd4ff'
}
},
{
id: 'peach-fresh',
name: 'Peach Fresh',
type: 'light',
colors: {
background: '#fef5ed',
foreground: '#5d4037',
cursor: '#ff8a65',
selection: '#ffe0b2',
black: '#2c2416',
red: '#d84315',
green: '#558b2f',
yellow: '#f57f17',
blue: '#01579b',
magenta: '#6a1b9a',
cyan: '#00838f',
white: '#f5deb3',
brightBlack: '#8d6e63',
brightRed: '#ff7043',
brightGreen: '#7cb342',
brightYellow: '#fbc02d',
brightBlue: '#0277bd',
brightMagenta: '#8e24aa',
brightCyan: '#00acc1',
brightWhite: '#ffccbc'
}
},
{
id: '1984-dark',
name: '1984 Dark',
type: 'dark',
colors: {
background: '#0c1014',
foreground: '#d4d4d4',
cursor: '#8fb5d9',
selection: '#1b2d42',
black: '#000000',
red: '#d71920',
green: '#00a600',
yellow: '#a6a614',
blue: '#2e7de8',
magenta: '#8e35ef',
cyan: '#5ebdab',
white: '#d4d4d4',
brightBlack: '#666666',
brightRed: '#ff6565',
brightGreen: '#66ff66',
brightYellow: '#ffff66',
brightBlue: '#6699ff',
brightMagenta: '#ff99ff',
brightCyan: '#99ffff',
brightWhite: '#ffffff'
}
},
{
id: '1984-light',
name: '1984 Light',
type: 'light',
colors: {
background: '#ffffff',
foreground: '#3d3d3d',
cursor: '#333333',
selection: '#e8e8e8',
black: '#000000',
red: '#d71920',
green: '#00a600',
yellow: '#a6a614',
blue: '#2e7de8',
magenta: '#8e35ef',
cyan: '#5ebdab',
white: '#ffffff',
brightBlack: '#999999',
brightRed: '#ff6565',
brightGreen: '#66ff66',
brightYellow: '#ffff66',
brightBlue: '#6699ff',
brightMagenta: '#ff99ff',
brightCyan: '#99ffff',
brightWhite: '#ffffff'
}
},
{
id: 'winter-night',
name: 'Winter Night',
type: 'dark',
colors: {
background: '#0d1117',
foreground: '#c9d1d9',
cursor: '#79c0ff',
selection: '#1f6feb',
black: '#0d1117',
red: '#f85149',
green: '#3fb950',
yellow: '#d29922',
blue: '#79c0ff',
magenta: '#bc8ef4',
cyan: '#39c5cf',
white: '#c9d1d9',
brightBlack: '#30363d',
brightRed: '#f85149',
brightGreen: '#3fb950',
brightYellow: '#d29922',
brightBlue: '#79c0ff',
brightMagenta: '#bc8ef4',
brightCyan: '#39c5cf',
brightWhite: '#c9d1d9'
}
},
{
id: 'winter-day',
name: 'Winter Day',
type: 'light',
colors: {
background: '#ffffff',
foreground: '#24292f',
cursor: '#0969da',
selection: '#e1e9f0',
black: '#24292f',
red: '#cf222e',
green: '#1a7f37',
yellow: '#9e6a03',
blue: '#0969da',
magenta: '#8250df',
cyan: '#0884d9',
white: '#ffffff',
brightBlack: '#57606a',
brightRed: '#d1242f',
brightGreen: '#238636',
brightYellow: '#b08900',
brightBlue: '#388bfd',
brightMagenta: '#a371f7',
brightCyan: '#56c2e6',
brightWhite: '#ffffff'
}
},
{
id: 'tokyo-night',
name: 'Tokyo Night',
type: 'dark',
colors: {
background: '#1a1b26',
foreground: '#c0caf5',
cursor: '#7dcfff',
selection: '#3c5870',
black: '#15161e',
red: '#f7768e',
green: '#9ece6a',
yellow: '#e0af68',
blue: '#7aa2f7',
magenta: '#bb9af7',
cyan: '#7dcfff',
white: '#a9b1d6',
brightBlack: '#414868',
brightRed: '#f7768e',
brightGreen: '#9ece6a',
brightYellow: '#e0af68',
brightBlue: '#7aa2f7',
brightMagenta: '#bb9af7',
brightCyan: '#7dcfff',
brightWhite: '#c0caf5'
}
},
{
id: 'tokyo-day',
name: 'Tokyo Day',
type: 'light',
colors: {
background: '#ffffff',
foreground: '#3760bf',
cursor: '#188092',
selection: '#e5e1ed',
black: '#1f2335',
red: '#f52a65',
green: '#587539',
yellow: '#8e8a2b',
blue: '#188092',
magenta: '#8c6cb8',
cyan: '#007197',
white: '#ffffff',
brightBlack: '#9699a3',
brightRed: '#f52a65',
brightGreen: '#587539',
brightYellow: '#8e8a2b',
brightBlue: '#188092',
brightMagenta: '#8c6cb8',
brightCyan: '#007197',
brightWhite: '#3760bf'
}
},
{
id: 'catppuccin-latte',
name: 'Catppuccin Latte',
type: 'light',
colors: {
background: '#eff1f5',
foreground: '#4c4f69',
cursor: '#dc8744',
selection: '#e6e9ef',
black: '#5c5f77',
red: '#d20f39',
green: '#40a02b',
yellow: '#df8e1d',
blue: '#1e66f5',
magenta: '#ea76cb',
cyan: '#04a5e5',
white: '#eff1f5',
brightBlack: '#6c6f85',
brightRed: '#d20f39',
brightGreen: '#40a02b',
brightYellow: '#df8e1d',
brightBlue: '#1e66f5',
brightMagenta: '#ea76cb',
brightCyan: '#04a5e5',
brightWhite: '#bcc0cc'
}
},
{
id: 'catppuccin-mocha',
name: 'Catppuccin Mocha',
type: 'dark',
colors: {
background: '#1e1e2e',
foreground: '#cdd6f4',
cursor: '#f5e0dc',
selection: '#313244',
black: '#45475a',
red: '#f38ba8',
green: '#a6e3a1',
yellow: '#f9e2af',
blue: '#89b4fa',
magenta: '#f5c2de',
cyan: '#94e2d5',
white: '#cdd6f4',
brightBlack: '#585b70',
brightRed: '#f38ba8',
brightGreen: '#a6e3a1',
brightYellow: '#f9e2af',
brightBlue: '#89b4fa',
brightMagenta: '#f5c2de',
brightCyan: '#94e2d5',
brightWhite: '#cdd6f4'
}
},
{
id: 'diwali',
name: 'Diwali',
type: 'dark',
colors: {
background: '#10100f',
foreground: '#e0e0e0',
cursor: '#ffb627',
selection: '#33332d',
black: '#1a1a1a',
red: '#d30102',
green: '#2e8c00',
yellow: '#d4af37',
blue: '#01a0e4',
magenta: '#ff00ff',
cyan: '#00ffff',
white: '#e0e0e0',
brightBlack: '#666666',
brightRed: '#ff3333',
brightGreen: '#33ff33',
brightYellow: '#ffff33',
brightBlue: '#3399ff',
brightMagenta: '#ff33ff',
brightCyan: '#33ffff',
brightWhite: '#ffffff'
}
},
{
id: 'movember',
name: 'Movember',
type: 'dark',
colors: {
background: '#2d1b00',
foreground: '#d4a574',
cursor: '#d4a574',
selection: '#5d3b1a',
black: '#1a0f00',
red: '#c85a54',
green: '#9fb154',
yellow: '#d4a574',
blue: '#8fa3b9',
magenta: '#a18a8e',
cyan: '#9a8f7f',
white: '#d4a574',
brightBlack: '#8d6e63',
brightRed: '#d87566',
brightGreen: '#b5d376',
brightYellow: '#ffc77d',
brightBlue: '#9fb154',
brightMagenta: '#c5949e',
brightCyan: '#a5a0a0',
brightWhite: '#ffffff'
}
},
{
id: 'atom-one-dark',
name: 'Atom One Dark',
type: 'dark',
colors: {
background: '#282c34',
foreground: '#abb2bf',
cursor: '#528b8b',
selection: '#3e4451',
black: '#1e1e1e',
red: '#e06c75',
green: '#98c379',
yellow: '#d19a66',
blue: '#61afef',
magenta: '#c678dd',
cyan: '#56b6c2',
white: '#abb2bf',
brightBlack: '#5c6370',
brightRed: '#e06c75',
brightGreen: '#98c379',
brightYellow: '#d19a66',
brightBlue: '#61afef',
brightMagenta: '#c678dd',
brightCyan: '#56b6c2',
brightWhite: '#ffffff'
}
},
{
id: 'atom-one-light',
name: 'Atom One Light',
type: 'light',
colors: {
background: '#fafafa',
foreground: '#383a42',
cursor: '#4078f2',
selection: '#e5ebf1',
black: '#383a42',
red: '#e45649',
green: '#50a14f',
yellow: '#c18401',
blue: '#4078f2',
magenta: '#a626a4',
cyan: '#0184bc',
white: '#fafafa',
brightBlack: '#a0a1a7',
brightRed: '#e45649',
brightGreen: '#50a14f',
brightYellow: '#c18401',
brightBlue: '#4078f2',
brightMagenta: '#a626a4',
brightCyan: '#0184bc',
brightWhite: '#383a42'
}
},
{
id: 'halloween',
name: 'Halloween',
type: 'dark',
colors: {
background: '#3d1f33',
foreground: '#f6d5a8',
cursor: '#ff6600',
selection: '#6d3f5f',
black: '#1a0f33',
red: '#ff3300',
green: '#ffcc00',
yellow: '#ff9900',
blue: '#6600cc',
magenta: '#ff0066',
cyan: '#00ffcc',
white: '#f6d5a8',
brightBlack: '#8b5a7d',
brightRed: '#ff6633',
brightGreen: '#ffff33',
brightYellow: '#ffcc33',
brightBlue: '#9933ff',
brightMagenta: '#ff33cc',
brightCyan: '#33ffff',
brightWhite: '#ffffff'
}
},
{
id: 'dia-de-muertos',
name: 'Dia De Muertos',
type: 'light',
colors: {
background: '#fef4e9',
foreground: '#4a4040',
cursor: '#ff6b35',
selection: '#ffe5cc',
black: '#2d1810',
red: '#f05a28',
green: '#04a777',
yellow: '#f7931e',
blue: '#003d7a',
magenta: '#6b4c8a',
cyan: '#008b8b',
white: '#f5e6d3',
brightBlack: '#8b7355',
brightRed: '#ff7043',
brightGreen: '#4caf50',
brightYellow: '#fbc02d',
brightBlue: '#2196f3',
brightMagenta: '#9c27b0',
brightCyan: '#00bcd4',
brightWhite: '#ffffff'
}
},
{
id: 'gruvbox-dark',
name: 'Gruvbox Dark',
type: 'dark',
colors: {
background: '#282828',
foreground: '#ebdbb2',
cursor: '#ebdbb2',
selection: '#504945',
black: '#282828',
red: '#cc241d',
green: '#98971a',
yellow: '#d79921',
blue: '#458588',
magenta: '#b16286',
cyan: '#689d6a',
white: '#a89984',
brightBlack: '#928374',
brightRed: '#fb4934',
brightGreen: '#b8bb26',
brightYellow: '#fabd2f',
brightBlue: '#83a598',
brightMagenta: '#d3869b',
brightCyan: '#8ec07c',
brightWhite: '#ebdbb2'
}
},
{
id: 'gruvbox-light',
name: 'Gruvbox Light',
type: 'light',
colors: {
background: '#fbf1c7',
foreground: '#3c3836',
cursor: '#3c3836',
selection: '#f2e5bc',
black: '#fbf1c7',
red: '#cc241d',
green: '#98971a',
yellow: '#d79921',
blue: '#458588',
magenta: '#b16286',
cyan: '#689d6a',
white: '#7c6f64',
brightBlack: '#928374',
brightRed: '#fb4934',
brightGreen: '#b8bb26',
brightYellow: '#fabd2f',
brightBlue: '#83a598',
brightMagenta: '#d3869b',
brightCyan: '#8ec07c',
brightWhite: '#3c3836'
}
},
{
id: 'material-dark',
name: 'Material Dark',
type: 'dark',
colors: {
background: '#263238',
foreground: '#eeffff',
cursor: '#eeffff',
selection: '#37474f',
black: '#000000',
red: '#f07178',
green: '#c3e88d',
yellow: '#ffcb8b',
blue: '#82b1ff',
magenta: '#c792ea',
cyan: '#89ddff',
white: '#ffffff',
brightBlack: '#546e7a',
brightRed: '#ef5350',
brightGreen: '#9ccc65',
brightYellow: '#ffeb3b',
brightBlue: '#42a5f5',
brightMagenta: '#ab47bc',
brightCyan: '#29b6f6',
brightWhite: '#ffffff'
}
},
{
id: 'material-light',
name: 'Material Light',
type: 'light',
colors: {
background: '#ffffff',
foreground: '#263238',
cursor: '#263238',
selection: '#eeebee',
black: '#000000',
red: '#e53935',
green: '#43a047',
yellow: '#fb8c00',
blue: '#1e88e5',
magenta: '#8e24aa',
cyan: '#00acc1',
white: '#ffffff',
brightBlack: '#bdbdbd',
brightRed: '#ef5350',
brightGreen: '#66bb6a',
brightYellow: '#ffb74d',
brightBlue: '#42a5f5',
brightMagenta: '#ab47bc',
brightCyan: '#26c6da',
brightWhite: '#263238'
}
},
{
id: 'manhatta',
name: 'Manhattan',
type: 'dark',
colors: {
background: '#1f1f1f',
foreground: '#e3e3e3',
cursor: '#c4c4c4',
selection: '#383838',
black: '#000000',
red: '#cc0000',
green: '#4e9e00',
yellow: '#ffff00',
blue: '#0066ff',
magenta: '#ff00ff',
cyan: '#00ffff',
white: '#e3e3e3',
brightBlack: '#666666',
brightRed: '#ff0000',
brightGreen: '#00ff00',
brightYellow: '#ffff00',
brightBlue: '#0099ff',
brightMagenta: '#ff00ff',
brightCyan: '#00ffff',
brightWhite: '#ffffff'
}
},
{
id: 'plastic-world',
name: 'Plastic World',
type: 'dark',
colors: {
background: '#ec008c',
foreground: '#00bfff',
cursor: '#00bfff',
selection: '#dc00c0',
black: '#000000',
red: '#ff006e',
green: '#00ff00',
yellow: '#ffff00',
blue: '#00bfff',
magenta: '#ff00ff',
cyan: '#00ffff',
white: '#ffffff',
brightBlack: '#999999',
brightRed: '#ff0000',
brightGreen: '#00ff00',
brightYellow: '#ffff00',
brightBlue: '#0099ff',
brightMagenta: '#ff00ff',
brightCyan: '#00ffff',
brightWhite: '#ffffff'
}
},
];

View File

@@ -0,0 +1,58 @@
import type { TerminalTheme } from '../../../domain/models';
export const coreTerminalThemes: TerminalTheme[] = [
{
id: 'netcatty-dark',
name: 'Netcatty Dark',
type: 'dark',
colors: {
background: '#0d1117',
foreground: '#c9d1d9',
cursor: '#58a6ff',
selection: '#264f78',
black: '#0d1117',
red: '#ff7b72',
green: '#3fb950',
yellow: '#d29922',
blue: '#58a6ff',
magenta: '#bc8cff',
cyan: '#39c5cf',
white: '#b1bac4',
brightBlack: '#6e7681',
brightRed: '#ffa198',
brightGreen: '#56d364',
brightYellow: '#e3b341',
brightBlue: '#79c0ff',
brightMagenta: '#d2a8ff',
brightCyan: '#56d4dd',
brightWhite: '#f0f6fc'
}
},
{
id: 'netcatty-light',
name: 'Netcatty Light',
type: 'light',
colors: {
background: '#f6f8fa',
foreground: '#24292f',
cursor: '#0969da',
selection: '#add6ff',
black: '#24292f',
red: '#cf222e',
green: '#116329',
yellow: '#9a6700',
blue: '#0969da',
magenta: '#8250df',
cyan: '#0e7574',
white: '#6e7781',
brightBlack: '#57606a',
brightRed: '#a40e26',
brightGreen: '#1a7f37',
brightYellow: '#7d4e00',
brightBlue: '#218bff',
brightMagenta: '#a475f9',
brightCyan: '#0c7875',
brightWhite: '#8c959f'
}
},
];

View File

@@ -0,0 +1,841 @@
import type { TerminalTheme } from '../../../domain/models';
export const extraTerminalThemes: TerminalTheme[] = [
{
id: 'basic',
name: 'Basic',
type: 'light',
colors: {
background: '#ffffff',
foreground: '#000000',
cursor: '#000000',
selection: '#e8e8e8',
black: '#000000',
red: '#990000',
green: '#009900',
yellow: '#999900',
blue: '#000099',
magenta: '#990099',
cyan: '#009999',
white: '#ffffff',
brightBlack: '#666666',
brightRed: '#ff0000',
brightGreen: '#00ff00',
brightYellow: '#ffff00',
brightBlue: '#0000ff',
brightMagenta: '#ff00ff',
brightCyan: '#00ffff',
brightWhite: '#ffffff'
}
},
{
id: 'homebrew',
name: 'Homebrew',
type: 'dark',
colors: {
background: '#000000',
foreground: '#00ff00',
cursor: '#00a6b2',
selection: '#00383d',
black: '#000000',
red: '#990000',
green: '#00a600',
yellow: '#999900',
blue: '#4d72ff',
magenta: '#b200b2',
cyan: '#00a6b2',
white: '#bfbfbf',
brightBlack: '#666666',
brightRed: '#e50000',
brightGreen: '#00d900',
brightYellow: '#e5e500',
brightBlue: '#5a7aff',
brightMagenta: '#e500e5',
brightCyan: '#00e5e5',
brightWhite: '#e5e5e5'
}
},
{
id: 'grass',
name: 'Grass',
type: 'light',
colors: {
background: '#a4e400',
foreground: '#333333',
cursor: '#333333',
selection: '#ccff00',
black: '#000000',
red: '#cc0000',
green: '#00cc00',
yellow: '#ffff00',
blue: '#0000ff',
magenta: '#ff00ff',
cyan: '#00ffff',
white: '#ffffff',
brightBlack: '#999999',
brightRed: '#ff0000',
brightGreen: '#00ff00',
brightYellow: '#ffff00',
brightBlue: '#0000ff',
brightMagenta: '#ff00ff',
brightCyan: '#00ffff',
brightWhite: '#ffffff'
}
},
{
id: 'man-page',
name: 'Man Page',
type: 'light',
colors: {
background: '#fef5e7',
foreground: '#2d2d2d',
cursor: '#2d2d2d',
selection: '#e8e8d0',
black: '#000000',
red: '#cc0000',
green: '#009900',
yellow: '#999900',
blue: '#000099',
magenta: '#990099',
cyan: '#009999',
white: '#cccccc',
brightBlack: '#999999',
brightRed: '#ff0000',
brightGreen: '#00ff00',
brightYellow: '#ffff00',
brightBlue: '#0000ff',
brightMagenta: '#ff00ff',
brightCyan: '#00ffff',
brightWhite: '#ffffff'
}
},
{
id: 'novel',
name: 'Novel',
type: 'light',
colors: {
background: '#eae6d8',
foreground: '#3d2817',
cursor: '#3d2817',
selection: '#ddd8ca',
black: '#2c2c2c',
red: '#8b0000',
green: '#006400',
yellow: '#8b8b00',
blue: '#00008b',
magenta: '#8b008b',
cyan: '#008b8b',
white: '#d3d3d3',
brightBlack: '#696969',
brightRed: '#ff0000',
brightGreen: '#00ff00',
brightYellow: '#ffff00',
brightBlue: '#0000ff',
brightMagenta: '#ff00ff',
brightCyan: '#00ffff',
brightWhite: '#ffffff'
}
},
{
id: 'ocean',
name: 'Ocean',
type: 'dark',
colors: {
background: '#000033',
foreground: '#ffffff',
cursor: '#0066ff',
selection: '#003366',
black: '#000000',
red: '#ff0000',
green: '#00ff00',
yellow: '#ffff00',
blue: '#0066ff',
magenta: '#ff00ff',
cyan: '#00ffff',
white: '#ffffff',
brightBlack: '#666666',
brightRed: '#ff6666',
brightGreen: '#66ff66',
brightYellow: '#ffff66',
brightBlue: '#6699ff',
brightMagenta: '#ff66ff',
brightCyan: '#66ffff',
brightWhite: '#ffffff'
}
},
{
id: 'pro',
name: 'Pro',
type: 'dark',
colors: {
background: '#000000',
foreground: '#ffffff',
cursor: '#ffffff',
selection: '#333333',
black: '#000000',
red: '#cc0000',
green: '#00cc00',
yellow: '#cccc00',
blue: '#0000ff',
magenta: '#ff00ff',
cyan: '#00ffff',
white: '#ffffff',
brightBlack: '#666666',
brightRed: '#ff0000',
brightGreen: '#00ff00',
brightYellow: '#ffff00',
brightBlue: '#0000ff',
brightMagenta: '#ff00ff',
brightCyan: '#00ffff',
brightWhite: '#ffffff'
}
},
{
id: 'red-sands',
name: 'Red Sands',
type: 'dark',
colors: {
background: '#3a2e2e',
foreground: '#d4a9a0',
cursor: '#f7f0e0',
selection: '#5d3f3f',
black: '#1a0f0f',
red: '#cc4d4d',
green: '#9fa877',
yellow: '#d4b896',
blue: '#7a9a9f',
magenta: '#a08080',
cyan: '#8b9f9f',
white: '#d4a9a0',
brightBlack: '#8b6f6f',
brightRed: '#ff6666',
brightGreen: '#b5d376',
brightYellow: '#ffc77d',
brightBlue: '#9fb5b5',
brightMagenta: '#c5a0a0',
brightCyan: '#a5b5b5',
brightWhite: '#ffffff'
}
},
{
id: 'solarized-dark',
name: 'Solarized Dark',
type: 'dark',
colors: {
background: '#002b36',
foreground: '#839496',
cursor: '#839496',
selection: '#073642',
black: '#073642',
red: '#dc322f',
green: '#859900',
yellow: '#b58900',
blue: '#268bd2',
magenta: '#d33682',
cyan: '#2aa198',
white: '#eee8d5',
brightBlack: '#002b36',
brightRed: '#cb4b16',
brightGreen: '#586e75',
brightYellow: '#657b83',
brightBlue: '#839496',
brightMagenta: '#6c71c4',
brightCyan: '#93a1a1',
brightWhite: '#fdf6e3'
}
},
{
id: 'solarized-light',
name: 'Solarized Light',
type: 'light',
colors: {
background: '#fdf6e3',
foreground: '#657b83',
cursor: '#657b83',
selection: '#eee8d5',
black: '#073642',
red: '#dc322f',
green: '#859900',
yellow: '#b58900',
blue: '#268bd2',
magenta: '#d33682',
cyan: '#2aa198',
white: '#fdf6e3',
brightBlack: '#586e75',
brightRed: '#cb4b16',
brightGreen: '#586e75',
brightYellow: '#657b83',
brightBlue: '#839496',
brightMagenta: '#6c71c4',
brightCyan: '#93a1a1',
brightWhite: '#fdf6e3'
}
},
{
id: 'silver-aerogel',
name: 'Silver Aerogel',
type: 'light',
colors: {
background: '#d4d4d4',
foreground: '#4d4d4d',
cursor: '#4d4d4d',
selection: '#c8c8c8',
black: '#2d2d2d',
red: '#d4666e',
green: '#8fa877',
yellow: '#d4b896',
blue: '#7a9a9f',
magenta: '#a08080',
cyan: '#8b9f9f',
white: '#d4d4d4',
brightBlack: '#8b8b8b',
brightRed: '#ff7f7f',
brightGreen: '#b5d376',
brightYellow: '#ffc77d',
brightBlue: '#9fb5b5',
brightMagenta: '#c5a0a0',
brightCyan: '#a5b5b5',
brightWhite: '#ffffff'
}
},
{
id: 'dracula',
name: 'Dracula',
type: 'dark',
colors: {
background: '#282a36',
foreground: '#f8f8f2',
cursor: '#f8f8f2',
selection: '#44475a',
black: '#000000',
red: '#ff5555',
green: '#50fa7b',
yellow: '#f1fa8c',
blue: '#61bfff',
magenta: '#ff79c6',
cyan: '#8be9fd',
white: '#bfbfbf',
brightBlack: '#575b86',
brightRed: '#ff6e6e',
brightGreen: '#69ff94',
brightYellow: '#ffffa5',
brightBlue: '#8be9fd',
brightMagenta: '#ff92df',
brightCyan: '#a4ffff',
brightWhite: '#ffffff'
}
},
{
id: 'monokai',
name: 'Monokai',
type: 'dark',
colors: {
background: '#272822',
foreground: '#f8f8f2',
cursor: '#f8f8f0',
selection: '#49483e',
black: '#272822',
red: '#f92672',
green: '#a6e22e',
yellow: '#e6db74',
blue: '#66d9ef',
magenta: '#ae81ff',
cyan: '#a1efe4',
white: '#f8f8f2',
brightBlack: '#75715e',
brightRed: '#f92672',
brightGreen: '#a6e22e',
brightYellow: '#e6db74',
brightBlue: '#66d9ef',
brightMagenta: '#ae81ff',
brightCyan: '#a1efe4',
brightWhite: '#f9f8f5'
}
},
{
id: 'nord-light',
name: 'Nord Light',
type: 'light',
colors: {
background: '#eceff4',
foreground: '#2e3440',
cursor: '#2e3440',
selection: '#d8dee9',
black: '#2e3440',
red: '#bf616a',
green: '#a3be8c',
yellow: '#ebcb8b',
blue: '#81a1c1',
magenta: '#b48ead',
cyan: '#88c0d0',
white: '#eceff4',
brightBlack: '#4c566a',
brightRed: '#bf616a',
brightGreen: '#a3be8c',
brightYellow: '#ebcb8b',
brightBlue: '#81a1c1',
brightMagenta: '#b48ead',
brightCyan: '#88c0d0',
brightWhite: '#2e3440'
}
},
{
id: 'nord-dark',
name: 'Nord Dark',
type: 'dark',
colors: {
background: '#2e3440',
foreground: '#d8dee9',
cursor: '#eceff4',
selection: '#434c5e',
black: '#3b4252',
red: '#bf616a',
green: '#a3be8c',
yellow: '#ebcb8b',
blue: '#81a1c1',
magenta: '#b48ead',
cyan: '#88c0d0',
white: '#d8dee9',
brightBlack: '#4c566a',
brightRed: '#bf616a',
brightGreen: '#a3be8c',
brightYellow: '#ebcb8b',
brightBlue: '#81a1c1',
brightMagenta: '#b48ead',
brightCyan: '#88c0d0',
brightWhite: '#eceff4'
}
},
{
id: 'flexoki-light',
name: 'Flexoki Light',
type: 'light',
colors: {
background: '#FFFCF0',
foreground: '#423F34',
cursor: '#423F34',
selection: '#F3EDE3',
black: '#423F34',
red: '#D74E35',
green: '#6E8326',
yellow: '#D0A215',
blue: '#4385BE',
magenta: '#8B7EC8',
cyan: '#3AA99F',
white: '#FFFCF0',
brightBlack: '#9B9582',
brightRed: '#E0A76F',
brightGreen: '#879A39',
brightYellow: '#E5C335',
brightBlue: '#6BA3D4',
brightMagenta: '#AE97D8',
brightCyan: '#5CB9B5',
brightWhite: '#423F34'
}
},
{
id: 'kanagawa-lotus',
name: 'Kanagawa Lotus',
type: 'light',
colors: {
background: '#F4F1DE',
foreground: '#545464',
cursor: '#C4B28A',
selection: '#E2DCC5',
black: '#2B2D2E',
red: '#C34043',
green: '#6B8E71',
yellow: '#C0A36E',
blue: '#7FB4CA',
magenta: '#957FB8',
cyan: '#7AA89F',
white: '#DCD7BA',
brightBlack: '#8A8C8E',
brightRed: '#D67878',
brightGreen: '#98BB6C',
brightYellow: '#E6C384',
brightBlue: '#7FB4CA',
brightMagenta: '#938AA9',
brightCyan: '#7AA89F',
brightWhite: '#f4f1de'
}
},
{
id: 'everforest-dark',
name: 'Everforest Dark',
type: 'dark',
colors: {
background: '#2d353b',
foreground: '#d3c6aa',
cursor: '#a7c957',
selection: '#3d484d',
black: '#0d0d0d',
red: '#f85552',
green: '#a7c957',
yellow: '#dbc074',
blue: '#7fbbb3',
magenta: '#d699b6',
cyan: '#83c092',
white: '#d3c6aa',
brightBlack: '#859289',
brightRed: '#f85552',
brightGreen: '#a7c957',
brightYellow: '#dbc074',
brightBlue: '#7fbbb3',
brightMagenta: '#d699b6',
brightCyan: '#83c092',
brightWhite: '#d3c6aa'
}
},
{
id: 'everforest-light',
name: 'Everforest Light',
type: 'light',
colors: {
background: '#faf4ed',
foreground: '#5c6d64',
cursor: '#7cb342',
selection: '#e8ddc9',
black: '#3d2817',
red: '#f85149',
green: '#7cb342',
yellow: '#d4a574',
blue: '#4a7fa7',
magenta: '#b58e8e',
cyan: '#597445',
white: '#faf4ed',
brightBlack: '#9da9a0',
brightRed: '#f85552',
brightGreen: '#a7c957',
brightYellow: '#dbc074',
brightBlue: '#7fbbb3',
brightMagenta: '#d699b6',
brightCyan: '#83c092',
brightWhite: '#5c6d64'
}
},
{
id: 'github-dark',
name: 'GitHub Dark',
type: 'dark',
colors: {
background: '#0d1117',
foreground: '#e6edf3',
cursor: '#2f81f7',
selection: '#264f78',
black: '#484f58',
red: '#ff7b72',
green: '#3fb950',
yellow: '#d29922',
blue: '#58a6ff',
magenta: '#bc8cff',
cyan: '#39c5cf',
white: '#b1bac4',
brightBlack: '#6e7681',
brightRed: '#ffa198',
brightGreen: '#56d364',
brightYellow: '#e3b341',
brightBlue: '#79c0ff',
brightMagenta: '#d2a8ff',
brightCyan: '#56d4dd',
brightWhite: '#ffffff',
}
},
{
id: 'github-light',
name: 'GitHub Light',
type: 'light',
colors: {
background: '#ffffff',
foreground: '#1f2328',
cursor: '#0969da',
selection: '#add6ff',
black: '#24292f',
red: '#cf222e',
green: '#116329',
yellow: '#4d2d00',
blue: '#0969da',
magenta: '#8250df',
cyan: '#1b7c83',
white: '#6e7781',
brightBlack: '#57606a',
brightRed: '#a40e26',
brightGreen: '#1a7f37',
brightYellow: '#633c01',
brightBlue: '#218bff',
brightMagenta: '#a475f9',
brightCyan: '#3192aa',
brightWhite: '#8c959f',
}
},
{
id: 'ubuntu',
name: 'Ubuntu',
type: 'dark',
colors: {
background: '#300a24',
foreground: '#eeeeec',
cursor: '#bbbbbb',
selection: '#b5d5ff',
black: '#2e3436',
red: '#cc0000',
green: '#4e9a06',
yellow: '#c4a000',
blue: '#3465a4',
magenta: '#75507b',
cyan: '#06989a',
white: '#d3d7cf',
brightBlack: '#555753',
brightRed: '#ef2929',
brightGreen: '#8ae234',
brightYellow: '#fce94f',
brightBlue: '#729fcf',
brightMagenta: '#ad7fa8',
brightCyan: '#34e2e2',
brightWhite: '#eeeeec',
}
},
{
id: 'one-dark-pro',
name: 'One Dark Pro',
type: 'dark',
colors: {
background: '#282c34',
foreground: '#abb2bf',
cursor: '#528bff',
selection: '#3e4452',
black: '#3f4451',
red: '#e05561',
green: '#8cc265',
yellow: '#d18f52',
blue: '#4aa5f0',
magenta: '#c162de',
cyan: '#42b3c2',
white: '#d7dae0',
brightBlack: '#4f5666',
brightRed: '#ff616e',
brightGreen: '#a5e075',
brightYellow: '#f0a45d',
brightBlue: '#4dc4ff',
brightMagenta: '#de73ff',
brightCyan: '#4cd1e0',
brightWhite: '#e6e6e6',
}
},
{
id: 'horizon-dark',
name: 'Horizon',
type: 'dark',
colors: {
background: '#1c1e26',
foreground: '#d5d8da',
cursor: '#6c6f93',
selection: '#6c6f93',
black: '#16161c',
red: '#e95678',
green: '#29d398',
yellow: '#fab795',
blue: '#26bbd9',
magenta: '#ee64ac',
cyan: '#59e1e3',
white: '#d5d8da',
brightBlack: '#6c6f93',
brightRed: '#ec6a88',
brightGreen: '#3fdaa4',
brightYellow: '#fbc3a7',
brightBlue: '#3fc4de',
brightMagenta: '#f075b5',
brightCyan: '#6be4e6',
brightWhite: '#ffffff',
}
},
{
id: 'palenight',
name: 'Palenight',
type: 'dark',
colors: {
background: '#292d3e',
foreground: '#bfc7d5',
cursor: '#ffcc00',
selection: '#7580b8',
black: '#292d3e',
red: '#ff5572',
green: '#a9c77d',
yellow: '#ffcb6b',
blue: '#82aaff',
magenta: '#c792ea',
cyan: '#89ddff',
white: '#d0d0d0',
brightBlack: '#676e95',
brightRed: '#ff5572',
brightGreen: '#c3e88d',
brightYellow: '#ffcb6b',
brightBlue: '#82aaff',
brightMagenta: '#c792ea',
brightCyan: '#89ddff',
brightWhite: '#ffffff',
}
},
{
id: 'panda',
name: 'Panda',
type: 'dark',
colors: {
background: '#292a2b',
foreground: '#e6e6e6',
cursor: '#ff4b82',
selection: '#454647',
black: '#757575',
red: '#ff2c6d',
green: '#19f9d8',
yellow: '#ffb86c',
blue: '#45a9f9',
magenta: '#ff75b5',
cyan: '#b084eb',
white: '#cdcdcd',
brightBlack: '#757575',
brightRed: '#ff2c6d',
brightGreen: '#19f9d8',
brightYellow: '#ffcc95',
brightBlue: '#6fc1ff',
brightMagenta: '#ff9ac1',
brightCyan: '#bcaafe',
brightWhite: '#e6e6e6',
}
},
{
id: 'snazzy',
name: 'Snazzy',
type: 'dark',
colors: {
background: '#1e1f29',
foreground: '#ebece6',
cursor: '#e4e4e4',
selection: '#81aec6',
black: '#000000',
red: '#fc4346',
green: '#50fb7c',
yellow: '#f0fb8c',
blue: '#49baff',
magenta: '#fc4cb4',
cyan: '#8be9fe',
white: '#ededec',
brightBlack: '#555555',
brightRed: '#fc4346',
brightGreen: '#50fb7c',
brightYellow: '#f0fb8c',
brightBlue: '#49baff',
brightMagenta: '#fc4cb4',
brightCyan: '#8be9fe',
brightWhite: '#ededec',
}
},
{
id: 'synthwave-84',
name: "Synthwave '84",
type: 'dark',
colors: {
background: '#262335',
foreground: '#f0eff1',
cursor: '#72f1b8',
selection: '#463465',
black: '#241b30',
red: '#fe4450',
green: '#72f1b8',
yellow: '#fede5d',
blue: '#03edf9',
magenta: '#ff7edb',
cyan: '#03edf9',
white: '#f0eff1',
brightBlack: '#7f7094',
brightRed: '#fe4450',
brightGreen: '#72f1b8',
brightYellow: '#f9f972',
brightBlue: '#aa54f9',
brightMagenta: '#ff7edb',
brightCyan: '#03edf9',
brightWhite: '#f2f2e3',
}
},
{
id: 'vesper',
name: 'Vesper',
type: 'dark',
colors: {
background: '#101010',
foreground: '#ffffff',
cursor: '#acb1ab',
selection: '#988049',
black: '#101010',
red: '#f5a191',
green: '#90b99f',
yellow: '#e6b99d',
blue: '#aca1cf',
magenta: '#e29eca',
cyan: '#ea83a5',
white: '#a0a0a0',
brightBlack: '#7e7e7e',
brightRed: '#ff8080',
brightGreen: '#99ffe4',
brightYellow: '#ffc799',
brightBlue: '#b9aeda',
brightMagenta: '#ecaad6',
brightCyan: '#f591b2',
brightWhite: '#ffffff',
}
},
{
id: 'kanso-dark',
name: 'Kanso Dark',
type: 'dark',
colors: {
background: '#090e13',
foreground: '#c5c9c7',
cursor: '#c5c9c7',
selection: '#393b44',
black: '#0d0c0c',
red: '#c4746e',
green: '#8a9a7b',
yellow: '#c4b28a',
blue: '#8ba4b0',
magenta: '#a292a3',
cyan: '#8ea4a2',
white: '#c8c093',
brightBlack: '#a4a7a4',
brightRed: '#e46876',
brightGreen: '#87a987',
brightYellow: '#e6c384',
brightBlue: '#7fbbb3',
brightMagenta: '#938aa9',
brightCyan: '#7aa89f',
brightWhite: '#c5c9c7',
}
},
{
id: 'kanso-light',
name: 'Kanso Light',
type: 'light',
colors: {
background: '#f2f1ef',
foreground: '#22262d',
cursor: '#22262d',
selection: '#e2e1df',
black: '#22262d',
red: '#c84053',
green: '#6f894e',
yellow: '#77713f',
blue: '#4d699b',
magenta: '#b35b79',
cyan: '#597b75',
white: '#545464',
brightBlack: '#6d6f6e',
brightRed: '#d7474b',
brightGreen: '#6e915f',
brightYellow: '#836f4a',
brightBlue: '#6693bf',
brightMagenta: '#624c83',
brightCyan: '#5e857a',
brightWhite: '#43436c',
}
},
];

View File

@@ -0,0 +1,512 @@
import type { TerminalTheme } from '../../../domain/models';
export const modernTerminalThemes: TerminalTheme[] = [
{ id: 'ui-pure-black', name: 'Pure Black', type: 'dark', colors: {
background: '#000000', foreground: '#c9d1d9', cursor: '#58a6ff', selection: '#264f78',
black: '#000000', red: '#ff7b72', green: '#3fb950', yellow: '#d29922',
blue: '#58a6ff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#b1bac4',
brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364', brightYellow: '#e3b341',
brightBlue: '#79c0ff', brightMagenta: '#d2a8ff', brightCyan: '#56d4dd', brightWhite: '#f0f6fc',
}},
{ id: 'ui-midnight', name: 'Midnight', type: 'dark', colors: {
background: '#141822', foreground: '#c9d1d9', cursor: '#58a6ff', selection: '#264f78',
black: '#141822', red: '#ff7b72', green: '#3fb950', yellow: '#d29922',
blue: '#58a6ff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#b1bac4',
brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364', brightYellow: '#e3b341',
brightBlue: '#79c0ff', brightMagenta: '#d2a8ff', brightCyan: '#56d4dd', brightWhite: '#f0f6fc',
}},
{ id: 'ui-deep-blue', name: 'Deep Blue', type: 'dark', colors: {
background: '#111722', foreground: '#c9d1d9', cursor: '#58a6ff', selection: '#264f78',
black: '#111722', red: '#ff7b72', green: '#3fb950', yellow: '#d29922',
blue: '#58a6ff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#b1bac4',
brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364', brightYellow: '#e3b341',
brightBlue: '#79c0ff', brightMagenta: '#d2a8ff', brightCyan: '#56d4dd', brightWhite: '#f0f6fc',
}},
{ id: 'ui-vscode', name: 'VS Code', type: 'dark', colors: {
background: '#1f1f1f', foreground: '#cccccc', cursor: '#aeafad', selection: '#264f78',
black: '#1f1f1f', red: '#f44747', green: '#6a9955', yellow: '#dcdcaa',
blue: '#569cd6', magenta: '#c586c0', cyan: '#4ec9b0', white: '#d4d4d4',
brightBlack: '#808080', brightRed: '#f44747', brightGreen: '#6a9955', brightYellow: '#dcdcaa',
brightBlue: '#569cd6', brightMagenta: '#c586c0', brightCyan: '#4ec9b0', brightWhite: '#e5e5e5',
}},
{ id: 'ui-graphite', name: 'Graphite', type: 'dark', colors: {
background: '#1c1e21', foreground: '#c9d1d9', cursor: '#58a6ff', selection: '#264f78',
black: '#1c1e21', red: '#ff7b72', green: '#3fb950', yellow: '#d29922',
blue: '#58a6ff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#b1bac4',
brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364', brightYellow: '#e3b341',
brightBlue: '#79c0ff', brightMagenta: '#d2a8ff', brightCyan: '#56d4dd', brightWhite: '#f0f6fc',
}},
{ id: 'ui-obsidian', name: 'Obsidian', type: 'dark', colors: {
background: '#131316', foreground: '#c9d1d9', cursor: '#a78bfa', selection: '#3b3175',
black: '#131316', red: '#ff7b72', green: '#3fb950', yellow: '#d29922',
blue: '#58a6ff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#b1bac4',
brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364', brightYellow: '#e3b341',
brightBlue: '#79c0ff', brightMagenta: '#d2a8ff', brightCyan: '#56d4dd', brightWhite: '#f0f6fc',
}},
{ id: 'ui-forest', name: 'Forest', type: 'dark', colors: {
background: '#161d1a', foreground: '#c9d1d9', cursor: '#34d399', selection: '#1a4a3a',
black: '#161d1a', red: '#ff7b72', green: '#3fb950', yellow: '#d29922',
blue: '#58a6ff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#b1bac4',
brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364', brightYellow: '#e3b341',
brightBlue: '#79c0ff', brightMagenta: '#d2a8ff', brightCyan: '#56d4dd', brightWhite: '#f0f6fc',
}},
{
id: 'flexoki-dark',
name: 'Flexoki Dark',
type: 'dark',
colors: {
background: '#100F0F',
foreground: '#CECDC3',
cursor: '#CECDC3',
selection: '#282726',
black: '#100F0F',
red: '#AF3029',
green: '#66800B',
yellow: '#AD8301',
blue: '#205EA6',
magenta: '#5E409D',
cyan: '#24837B',
white: '#CECDC3',
brightBlack: '#282726',
brightRed: '#D14D41',
brightGreen: '#879A39',
brightYellow: '#D0A215',
brightBlue: '#4385BE',
brightMagenta: '#8B7EC8',
brightCyan: '#3AA99F',
brightWhite: '#FFFCF0'
}
},
{
id: 'kanagawa-wave',
name: 'Kanagawa Wave',
type: 'dark',
colors: {
background: '#1F1F28',
foreground: '#DCD7BA',
cursor: '#C8C093',
selection: '#2D4F67',
black: '#090618',
red: '#C34043',
green: '#76946A',
yellow: '#C0A36E',
blue: '#7E9CD8',
magenta: '#957FB8',
cyan: '#6A9589',
white: '#C8C093',
brightBlack: '#727169',
brightRed: '#E82424',
brightGreen: '#98BB6C',
brightYellow: '#E6C384',
brightBlue: '#7FB4CA',
brightMagenta: '#938AA9',
brightCyan: '#7AA89F',
brightWhite: '#DCD7BA'
}
},
{
id: 'kanagawa-dragon',
name: 'Kanagawa Dragon',
type: 'dark',
colors: {
background: '#181616',
foreground: '#c5c9c5',
cursor: '#c8c093',
selection: '#2d4f67',
black: '#0d0c0c',
red: '#c4746e',
green: '#8a9a7b',
yellow: '#c4b28a',
blue: '#8ba4b0',
magenta: '#a292a3',
cyan: '#8ea4a2',
white: '#c5c9c5',
brightBlack: '#a6a69c',
brightRed: '#e46876',
brightGreen: '#87a987',
brightYellow: '#e6c384',
brightBlue: '#7fb4ca',
brightMagenta: '#938aa9',
brightCyan: '#7aa89f',
brightWhite: '#c5c9c5'
}
},
{
id: 'hacker-green',
name: 'Hacker Green',
type: 'dark',
colors: {
background: '#0d0208',
foreground: '#00ff41',
cursor: '#00ff41',
selection: '#003b00',
black: '#000000',
red: '#ff0000',
green: '#00ff41',
yellow: '#008F11',
blue: '#005F00',
magenta: '#00ff41',
cyan: '#00ff41',
white: '#00ff41',
brightBlack: '#001100',
brightRed: '#ff0000',
brightGreen: '#00ff41',
brightYellow: '#00ff41',
brightBlue: '#00ff41',
brightMagenta: '#00ff41',
brightCyan: '#00ff41',
brightWhite: '#ccffcc'
}
},
{
id: 'hacker-blue',
name: 'Hacker Blue',
type: 'dark',
colors: {
background: '#050a14',
foreground: '#00aaff',
cursor: '#00aaff',
selection: '#002a4d',
black: '#000000',
red: '#ff3333',
green: '#33ff33',
yellow: '#ffff33',
blue: '#00aaff',
magenta: '#ff33ff',
cyan: '#33ffff',
white: '#ffffff',
brightBlack: '#333333',
brightRed: '#ff6666',
brightGreen: '#66ff66',
brightYellow: '#ffff66',
brightBlue: '#66ccff',
brightMagenta: '#ff66ff',
brightCyan: '#66ffff',
brightWhite: '#ffffff'
}
},
{
id: 'night-owl',
name: 'Night Owl',
type: 'dark',
colors: {
background: '#011627',
foreground: '#d6deeb',
cursor: '#80a4c2',
selection: '#1d3b53',
black: '#011627',
red: '#ef5350',
green: '#22da6e',
yellow: '#addb67',
blue: '#82aaff',
magenta: '#c792ea',
cyan: '#21c7a8',
white: '#ffffff',
brightBlack: '#575656',
brightRed: '#ef5350',
brightGreen: '#22da6e',
brightYellow: '#ffeb95',
brightBlue: '#82aaff',
brightMagenta: '#c792ea',
brightCyan: '#7fdbca',
brightWhite: '#ffffff'
}
},
{
id: 'aura',
name: 'Aura',
type: 'dark',
colors: {
background: '#15141b',
foreground: '#eae8f5',
cursor: '#a594fd',
selection: '#332e55',
black: '#15141b',
red: '#f72585',
green: '#00d9ff',
yellow: '#f7e025',
blue: '#a594fd',
magenta: '#f72585',
cyan: '#00d9ff',
white: '#eae8f5',
brightBlack: '#433f5f',
brightRed: '#f72585',
brightGreen: '#00d9ff',
brightYellow: '#f7e025',
brightBlue: '#a594fd',
brightMagenta: '#f72585',
brightCyan: '#00d9ff',
brightWhite: '#eae8f5'
}
},
{
id: 'rose-pine',
name: 'Rose Pine',
type: 'dark',
colors: {
background: '#191724',
foreground: '#e0def4',
cursor: '#f6c6dc',
selection: '#403d52',
black: '#191724',
red: '#eb6f92',
green: '#31748f',
yellow: '#f6c177',
blue: '#9ccfd8',
magenta: '#c4a7e7',
cyan: '#ebbcba',
white: '#e0def4',
brightBlack: '#555169',
brightRed: '#eb6f92',
brightGreen: '#31748f',
brightYellow: '#f6c177',
brightBlue: '#9ccfd8',
brightMagenta: '#c4a7e7',
brightCyan: '#ebbcba',
brightWhite: '#e0def4'
}
},
{
id: 'rose-pine-moon',
name: 'Rose Pine Moon',
type: 'dark',
colors: {
background: '#232136',
foreground: '#e0def4',
cursor: '#f6c6dc',
selection: '#44415a',
black: '#232136',
red: '#eb6f92',
green: '#3e8fb0',
yellow: '#f6c177',
blue: '#9ccfd8',
magenta: '#c4a7e7',
cyan: '#ea9a97',
white: '#e0def4',
brightBlack: '#6e6a86',
brightRed: '#eb6f92',
brightGreen: '#3e8fb0',
brightYellow: '#f6c177',
brightBlue: '#9ccfd8',
brightMagenta: '#c4a7e7',
brightCyan: '#ea9a97',
brightWhite: '#e0def4'
}
},
{
id: 'rose-pine-dawn',
name: 'Rose Pine Dawn',
type: 'light',
colors: {
background: '#faf4ed',
foreground: '#575279',
cursor: '#d7828d',
selection: '#f2e9e1',
black: '#555169',
red: '#b4637a',
green: '#286983',
yellow: '#d7af70',
blue: '#56949f',
magenta: '#907aa9',
cyan: '#ea9d34',
white: '#faf4ed',
brightBlack: '#9893a5',
brightRed: '#b4637a',
brightGreen: '#286983',
brightYellow: '#d7af70',
brightBlue: '#56949f',
brightMagenta: '#907aa9',
brightCyan: '#ea9d34',
brightWhite: '#575279'
}
},
{
id: 'cobalt2',
name: 'Cobalt2',
type: 'dark',
colors: {
background: '#132738',
foreground: '#ffffff',
cursor: '#ffffff',
selection: '#1e3c41',
black: '#000000',
red: '#ff5d38',
green: '#3ad900',
yellow: '#ffb402',
blue: '#0088ff',
magenta: '#bc3fbc',
cyan: '#00d0d0',
white: '#d0d0d0',
brightBlack: '#808080',
brightRed: '#ff8070',
brightGreen: '#66ff00',
brightYellow: '#ffeb3b',
brightBlue: '#5ba3ff',
brightMagenta: '#ff4eff',
brightCyan: '#5ce8e8',
brightWhite: '#ffffff'
}
},
{
id: 'octocat-dark',
name: 'Octocat Dark',
type: 'dark',
colors: {
background: '#0d1117',
foreground: '#c9d1d9',
cursor: '#58a6ff',
selection: '#30363d',
black: '#0d1117',
red: '#f85149',
green: '#3fb950',
yellow: '#d29922',
blue: '#58a6ff',
magenta: '#bc8ef4',
cyan: '#39c5cf',
white: '#c9d1d9',
brightBlack: '#30363d',
brightRed: '#f85149',
brightGreen: '#3fb950',
brightYellow: '#d29922',
brightBlue: '#58a6ff',
brightMagenta: '#bc8ef4',
brightCyan: '#39c5cf',
brightWhite: '#c9d1d9'
}
},
{
id: 'octocat-light',
name: 'Octocat Light',
type: 'light',
colors: {
background: '#ffffff',
foreground: '#24292f',
cursor: '#0969da',
selection: '#e1e9f0',
black: '#24292f',
red: '#cf222e',
green: '#1a7f37',
yellow: '#9e6a03',
blue: '#0969da',
magenta: '#8250df',
cyan: '#0884d9',
white: '#ffffff',
brightBlack: '#57606a',
brightRed: '#d1242f',
brightGreen: '#238636',
brightYellow: '#b08900',
brightBlue: '#388bfd',
brightMagenta: '#a371f7',
brightCyan: '#56c2e6',
brightWhite: '#ffffff'
}
},
{
id: 'ayu-dark',
name: 'Ayu Dark',
type: 'dark',
colors: {
background: '#0f1419',
foreground: '#e6e1cf',
cursor: '#ffee99',
selection: '#1f2430',
black: '#000000',
red: '#ff3333',
green: '#86b300',
yellow: '#ffb454',
blue: '#36a3d9',
magenta: '#f07178',
cyan: '#95e1d3',
white: '#ffffff',
brightBlack: '#323232',
brightRed: '#ff6565',
brightGreen: '#b8e536',
brightYellow: '#ffc66d',
brightBlue: '#55b4d4',
brightMagenta: '#ff8888',
brightCyan: '#95e1d3',
brightWhite: '#ffffff'
}
},
{
id: 'ayu-light',
name: 'Ayu Light',
type: 'light',
colors: {
background: '#fafafa',
foreground: '#5c6773',
cursor: '#ffb454',
selection: '#f0eee4',
black: '#000000',
red: '#ff3333',
green: '#86b300',
yellow: '#ffb454',
blue: '#36a3d9',
magenta: '#f07178',
cyan: '#4dbf99',
white: '#ffffff',
brightBlack: '#949494',
brightRed: '#ff6565',
brightGreen: '#b8e536',
brightYellow: '#ffc66d',
brightBlue: '#55b4d4',
brightMagenta: '#ff8888',
brightCyan: '#4dbf99',
brightWhite: '#ffffff'
}
},
{
id: 'cyberpunk',
name: 'Cyberpunk',
type: 'dark',
colors: {
background: '#0d0221',
foreground: '#ff006e',
cursor: '#ffbe0b',
selection: '#3a0ca3',
black: '#0d0221',
red: '#ff006e',
green: '#8338ec',
yellow: '#ffbe0b',
blue: '#3a86ff',
magenta: '#ff006e',
cyan: '#06ffa5',
white: '#ffffff',
brightBlack: '#6c5b7b',
brightRed: '#ff006e',
brightGreen: '#8338ec',
brightYellow: '#ffbe0b',
brightBlue: '#3a86ff',
brightMagenta: '#ff006e',
brightCyan: '#06ffa5',
brightWhite: '#ffffff'
}
},
{
id: 'cyberpunk-scarlet',
name: 'Cyberpunk Scarlet',
type: 'dark',
colors: {
background: '#0d0221',
foreground: '#ff0050',
cursor: '#ff0050',
selection: '#3a0ca3',
black: '#0d0221',
red: '#ff0050',
green: '#ff0050',
yellow: '#ff0050',
blue: '#3a86ff',
magenta: '#ff0050',
cyan: '#06ffa5',
white: '#ffffff',
brightBlack: '#6c5b7b',
brightRed: '#ff0050',
brightGreen: '#ff0050',
brightYellow: '#ff0050',
brightBlue: '#3a86ff',
brightMagenta: '#ff0050',
brightCyan: '#06ffa5',
brightWhite: '#ffffff'
}
},
];

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
import type { TerminalTheme } from '../../../domain/models';
export const uiMatchTerminalThemes: TerminalTheme[] = [
// ====================================================================,
// UI-matched terminal themes — background color matched to each built-in,
// UI theme preset so the terminal blends seamlessly with the app chrome.,
// ANSI palette based on netcatty-light (for light) / netcatty-dark (for dark).,
// ====================================================================,
// Light UI matches,
{ id: 'ui-snow', name: 'Snow', type: 'light', colors: {
background: '#f1f4f8', foreground: '#24292f', cursor: '#0969da', selection: '#add6ff',
black: '#24292f', red: '#cf222e', green: '#116329', yellow: '#9a6700',
blue: '#0969da', magenta: '#8250df', cyan: '#0e7574', white: '#6e7781',
brightBlack: '#57606a', brightRed: '#a40e26', brightGreen: '#1a7f37', brightYellow: '#7d4e00',
brightBlue: '#218bff', brightMagenta: '#a475f9', brightCyan: '#0c7875', brightWhite: '#8c959f',
}},
{ id: 'ui-pure-white', name: 'Pure White', type: 'light', colors: {
background: '#ffffff', foreground: '#24292f', cursor: '#0969da', selection: '#add6ff',
black: '#24292f', red: '#cf222e', green: '#116329', yellow: '#9a6700',
blue: '#0969da', magenta: '#8250df', cyan: '#0e7574', white: '#6e7781',
brightBlack: '#57606a', brightRed: '#a40e26', brightGreen: '#1a7f37', brightYellow: '#7d4e00',
brightBlue: '#218bff', brightMagenta: '#a475f9', brightCyan: '#0c7875', brightWhite: '#8c959f',
}},
{ id: 'ui-ivory', name: 'Ivory', type: 'light', colors: {
background: '#f7f4ed', foreground: '#24292f', cursor: '#b45309', selection: '#fde68a',
black: '#24292f', red: '#cf222e', green: '#116329', yellow: '#9a6700',
blue: '#0969da', magenta: '#8250df', cyan: '#0e7574', white: '#6e7781',
brightBlack: '#57606a', brightRed: '#a40e26', brightGreen: '#1a7f37', brightYellow: '#7d4e00',
brightBlue: '#218bff', brightMagenta: '#a475f9', brightCyan: '#0c7875', brightWhite: '#8c959f',
}},
{ id: 'ui-mist', name: 'Mist', type: 'light', colors: {
background: '#f6f7f9', foreground: '#24292f', cursor: '#0891b2', selection: '#a5f3fc',
black: '#24292f', red: '#cf222e', green: '#116329', yellow: '#9a6700',
blue: '#0969da', magenta: '#8250df', cyan: '#0e7574', white: '#6e7781',
brightBlack: '#57606a', brightRed: '#a40e26', brightGreen: '#1a7f37', brightYellow: '#7d4e00',
brightBlue: '#218bff', brightMagenta: '#a475f9', brightCyan: '#0c7875', brightWhite: '#8c959f',
}},
{ id: 'ui-mint', name: 'Mint', type: 'light', colors: {
background: '#f2f8f5', foreground: '#24292f', cursor: '#059669', selection: '#a7f3d0',
black: '#24292f', red: '#cf222e', green: '#116329', yellow: '#9a6700',
blue: '#0969da', magenta: '#8250df', cyan: '#0e7574', white: '#6e7781',
brightBlack: '#57606a', brightRed: '#a40e26', brightGreen: '#1a7f37', brightYellow: '#7d4e00',
brightBlue: '#218bff', brightMagenta: '#a475f9', brightCyan: '#0c7875', brightWhite: '#8c959f',
}},
{ id: 'ui-sand', name: 'Sand', type: 'light', colors: {
background: '#f4f0eb', foreground: '#24292f', cursor: '#c2410c', selection: '#fed7aa',
black: '#24292f', red: '#cf222e', green: '#116329', yellow: '#9a6700',
blue: '#0969da', magenta: '#8250df', cyan: '#0e7574', white: '#6e7781',
brightBlack: '#57606a', brightRed: '#a40e26', brightGreen: '#1a7f37', brightYellow: '#7d4e00',
brightBlue: '#218bff', brightMagenta: '#a475f9', brightCyan: '#0c7875', brightWhite: '#8c959f',
}},
{ id: 'ui-lavender', name: 'Lavender', type: 'light', colors: {
background: '#f7f5fa', foreground: '#24292f', cursor: '#7c3aed', selection: '#ddd6fe',
black: '#24292f', red: '#cf222e', green: '#116329', yellow: '#9a6700',
blue: '#0969da', magenta: '#8250df', cyan: '#0e7574', white: '#6e7781',
brightBlack: '#57606a', brightRed: '#a40e26', brightGreen: '#1a7f37', brightYellow: '#7d4e00',
brightBlue: '#218bff', brightMagenta: '#a475f9', brightCyan: '#0c7875', brightWhite: '#8c959f',
}},
// Dark UI matches,
];

View File

@@ -0,0 +1,35 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { describe, it } from 'node:test';
/**
* Regression for #2647: Windows boot CSS used a higher-specificity
* `html.platform-win32 body { font-family: ... }` rule that beat
* `body { font-family: var(--font-sans) }` from index.css. Popovers /
* context menus inherit from body, so transfer / sync / quick-control
* panels and menu items ignored the Appearance UI font setting.
*/
const indexHtml = readFileSync(new URL('../../index.html', import.meta.url), 'utf8');
describe('index.html UI font bootstrap', () => {
it('applies the runtime --font-sans variable on body', () => {
assert.match(
indexHtml,
/body\s*\{[^}]*font-family:\s*var\(--font-sans\)/s,
'body must use var(--font-sans) so Appearance UI font updates reach portaled panels',
);
});
it('does not hardcode a Windows body font stack that overrides --font-sans', () => {
assert.doesNotMatch(
indexHtml,
/html\.platform-win32\s+body\s*\{[^}]*font-family:/s,
'platform-win32 must not set body font-family; emoji fallbacks belong on --font-sans',
);
assert.doesNotMatch(
indexHtml,
/body\s*\{[^}]*font-family:\s*['"]Space Grotesk['"]/s,
'body must not hardcode Space Grotesk ahead of the user UI font setting',
);
});
});

View File

@@ -0,0 +1,82 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
DEFAULT_UI_FONT_ID,
detectUiPlatform,
UI_FONTS,
withWindowsEmojiFallback,
WINDOWS_FLAG_EMOJI_FONT,
WINDOWS_UI_EMOJI_FONTS,
withUiCjkFallback,
} from './uiFonts';
describe('detectUiPlatform', () => {
it('detects Windows user agents', () => {
assert.equal(
detectUiPlatform('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'),
'win32',
);
});
it('detects macOS user agents', () => {
assert.equal(
detectUiPlatform('Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36'),
'darwin',
);
});
it('falls back to linux for other platforms', () => {
assert.equal(
detectUiPlatform('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'),
'linux',
);
});
});
describe('withWindowsEmojiFallback', () => {
const sampleFamily = '"Space Grotesk", system-ui, sans-serif';
it('prepends bundled flag font and Windows emoji fonts for host labels', () => {
const stack = withWindowsEmojiFallback(sampleFamily, 'win32');
assert.ok(stack.startsWith(WINDOWS_UI_EMOJI_FONTS));
assert.match(stack, /Noto Color Emoji Flags/);
assert.match(stack, /Space Grotesk/);
});
it('includes the bundled flag font before Segoe UI Emoji on Windows', () => {
const stack = withWindowsEmojiFallback(sampleFamily, 'win32');
const flagIndex = stack.indexOf(WINDOWS_FLAG_EMOJI_FONT);
const segoeIndex = stack.indexOf('Segoe UI Emoji');
assert.ok(flagIndex >= 0);
assert.ok(segoeIndex >= 0);
assert.ok(flagIndex < segoeIndex);
});
it('keeps the stack unchanged on non-Windows platforms', () => {
assert.equal(withWindowsEmojiFallback(sampleFamily, 'darwin'), sampleFamily);
assert.equal(withWindowsEmojiFallback(sampleFamily, 'linux'), sampleFamily);
});
it('does not double-prepend when emoji fonts are already present', () => {
const alreadyPrefixed = `${WINDOWS_UI_EMOJI_FONTS}, ${sampleFamily}`;
assert.equal(withWindowsEmojiFallback(alreadyPrefixed, 'win32'), alreadyPrefixed);
});
});
describe('withUiCjkFallback', () => {
it('still appends CJK fallbacks for UI font stacks', () => {
const stack = withUiCjkFallback('"Space Grotesk", system-ui');
assert.match(stack, /PingFang SC/);
assert.match(stack, /Space Grotesk/);
});
});
describe('default UI font', () => {
it('uses Mona Sans while keeping Chinese fallbacks in the stack', () => {
const defaultFont = UI_FONTS.find((font) => font.id === DEFAULT_UI_FONT_ID);
assert.equal(DEFAULT_UI_FONT_ID, 'mona-sans');
assert.equal(defaultFont?.name, 'Mona Sans');
assert.match(defaultFont?.family ?? '', /Mona Sans/);
assert.match(defaultFont?.family ?? '', /PingFang SC/);
});
});

View File

@@ -0,0 +1,190 @@
/**
* UI Fonts Configuration
* Includes general-purpose fonts suitable for application interface
*/
export interface UIFont {
id: string;
name: string;
family: string;
}
export type UiPlatform = 'darwin' | 'win32' | 'linux';
/**
* Bundled Noto Color Emoji subset (regional indicators U+1F1E61F1FF).
* Windows system fonts omit flag glyphs and show ISO country codes instead —
* see #1589, #1604.
*/
export const WINDOWS_FLAG_EMOJI_FONT = '"Noto Color Emoji Flags"';
/**
* Windows bundles UI fonts via @font-face. Their regional-indicator glyphs
* render as separate letters instead of composed flag emoji unless a color
* emoji font is consulted first — see #1589.
*/
export const WINDOWS_UI_EMOJI_FONTS = WINDOWS_FLAG_EMOJI_FONT;
/**
* Fallback fonts for CJK (Chinese, Japanese, Korean) support
*/
const CJK_FALLBACK_FONTS = [
'"PingFang SC"',
'"Hiragino Sans GB"',
'"Microsoft YaHei UI"',
'"Microsoft YaHei"',
'"Noto Sans CJK SC"',
'"Source Han Sans SC"',
'sans-serif',
];
const CJK_FALLBACK_STACK = CJK_FALLBACK_FONTS.join(', ');
export const withUiCjkFallback = (family: string) => {
const trimmed = family.trim();
// Avoid double-appending if a custom stack already includes one of these fonts.
if (CJK_FALLBACK_FONTS.some((f) => trimmed.includes(f.replace(/"/g, '')))) {
return trimmed;
}
return `${trimmed}, ${CJK_FALLBACK_STACK}`;
};
export function detectUiPlatform(userAgent: string): UiPlatform {
if (/Win/i.test(userAgent)) return 'win32';
if (/Mac|iPod|iPhone|iPad/i.test(userAgent)) return 'darwin';
return 'linux';
}
export function withWindowsEmojiFallback(
family: string,
platform: UiPlatform = typeof navigator !== 'undefined'
? detectUiPlatform(navigator.userAgent)
: 'linux',
): string {
if (platform !== 'win32') return family;
const trimmed = family.trim();
if (trimmed.includes('Noto Color Emoji Flags') || trimmed.includes('Segoe UI Emoji')) {
return trimmed;
}
return `${WINDOWS_UI_EMOJI_FONTS}, ${trimmed}, "Segoe UI Emoji", "Segoe UI Symbol"`;
}
const BASE_UI_FONTS: UIFont[] = [
{
id: 'mona-sans',
name: 'Mona Sans',
family: '"Mona Sans", system-ui',
},
{
id: 'space-grotesk',
name: 'Space Grotesk',
family: '"Space Grotesk", system-ui',
},
{
id: 'system-ui',
name: 'System UI',
family: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial',
},
{
id: 'inter',
name: 'Inter',
family: '"Inter", system-ui',
},
{
id: 'roboto',
name: 'Roboto',
family: '"Roboto", system-ui',
},
{
id: 'open-sans',
name: 'Open Sans',
family: '"Open Sans", system-ui',
},
{
id: 'lato',
name: 'Lato',
family: '"Lato", system-ui',
},
{
id: 'nunito',
name: 'Nunito',
family: '"Nunito", system-ui',
},
{
id: 'poppins',
name: 'Poppins',
family: '"Poppins", system-ui',
},
{
id: 'source-sans-pro',
name: 'Source Sans Pro',
family: '"Source Sans Pro", system-ui',
},
{
id: 'ubuntu',
name: 'Ubuntu',
family: '"Ubuntu", system-ui',
},
{
id: 'noto-sans',
name: 'Noto Sans',
family: '"Noto Sans", system-ui',
},
{
id: 'work-sans',
name: 'Work Sans',
family: '"Work Sans", system-ui',
},
{
id: 'dm-sans',
name: 'DM Sans',
family: '"DM Sans", system-ui',
},
{
id: 'montserrat',
name: 'Montserrat',
family: '"Montserrat", system-ui',
},
{
id: 'raleway',
name: 'Raleway',
family: '"Raleway", system-ui',
},
{
id: 'quicksand',
name: 'Quicksand',
family: '"Quicksand", system-ui',
},
{
id: 'ibm-plex-sans',
name: 'IBM Plex Sans',
family: '"IBM Plex Sans", system-ui',
},
{
id: 'outfit',
name: 'Outfit',
family: '"Outfit", system-ui',
},
{
id: 'plus-jakarta-sans',
name: 'Plus Jakarta Sans',
family: '"Plus Jakarta Sans", system-ui',
},
{
id: 'segoe-ui',
name: 'Segoe UI',
family: '"Segoe UI", system-ui',
},
{
id: 'sf-pro',
name: 'SF Pro',
family: '-apple-system, BlinkMacSystemFont, "SF Pro Display", system-ui',
},
];
export const UI_FONTS: UIFont[] = BASE_UI_FONTS.map((font) => ({
...font,
family: withUiCjkFallback(font.family),
}));
export const DEFAULT_UI_FONT_ID = 'mona-sans';

View File

@@ -0,0 +1,111 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
DARK_UI_THEMES,
EXTENDED_DARK_UI_THEMES,
EXTENDED_LIGHT_UI_THEMES,
LIGHT_UI_THEMES,
getUiThemeById,
} from "./uiThemes";
import { TERMINAL_THEMES } from "./terminalThemes";
const SYSTEM_PRESET_THEME_IDS = [
"a-cup-of-coffee",
"abolkog",
"aurora",
"ayu",
"base16-flat",
"base16-mocha",
"blue-dolphin",
"calm-days-sober-nights-sky",
"catppuccin",
"chai",
"chinolor",
"cyberdyne",
"desert",
"django-reborn-again",
"espresso",
"eyehealth",
"flexoki",
"fox",
"garbage-oracle",
"github",
"gruvbox-material",
"homebrew",
"ic-orange-ppl",
"ikki",
"kanso-ink",
"kary-pro-colors",
"light-purple",
"mondrian",
"monochrome",
"monochrome-stone",
"monokai-pro-spectrum",
"monospace",
"noctis-azureus",
"noctis-hibernus",
"noir-essence",
"nord-midnight",
"notionish",
"phonebook",
"polychrome",
"purplepeter",
"rainglow-codecourse",
"rainglow-crisp",
"rainglow-lavender",
"remedy-tilted",
"rose-pine",
"selene-selenized",
"soft-color",
"tearout",
"tokyo-night",
"tomorrow-night-eighties",
"vaporizer-turquoise",
"xotopio",
"yuttari",
"zenbones-rosebones",
"zhxo-red",
];
describe("system preset UI themes", () => {
it("adds every imported preset at the same level as the existing UI themes", () => {
assert.deepEqual(EXTENDED_LIGHT_UI_THEMES.map((theme) => theme.id), SYSTEM_PRESET_THEME_IDS);
assert.deepEqual(EXTENDED_DARK_UI_THEMES.map((theme) => theme.id), SYSTEM_PRESET_THEME_IDS);
for (const id of SYSTEM_PRESET_THEME_IDS) {
assert.equal(getUiThemeById("light", id).id, id);
assert.equal(getUiThemeById("dark", id).id, id);
}
assert.equal(EXTENDED_LIGHT_UI_THEMES.length, 55);
assert.equal(EXTENDED_DARK_UI_THEMES.length, 55);
assert.equal(LIGHT_UI_THEMES.length, 7 + 55);
assert.equal(DARK_UI_THEMES.length, 7 + 55);
assert.equal([...LIGHT_UI_THEMES, ...DARK_UI_THEMES].filter((theme) => theme.collection !== undefined && theme.collection !== "core").length, 0);
});
it("keeps theme accents distinct from the original default blue", () => {
const originalDefaultBlue = "221.2 83.2% 53.3%";
const accents = new Set([
...EXTENDED_LIGHT_UI_THEMES.map((theme) => theme.tokens.accent),
...EXTENDED_DARK_UI_THEMES.map((theme) => theme.tokens.accent),
]);
assert.ok(accents.size > 20);
assert.ok(!accents.has(originalDefaultBlue));
});
it("adds matching terminal themes for every imported light and dark UI preset", () => {
const terminalThemeIds = new Set(TERMINAL_THEMES.map((theme) => theme.id));
assert.equal(terminalThemeIds.size, TERMINAL_THEMES.length);
for (const id of SYSTEM_PRESET_THEME_IDS) {
const lightTerminalTheme = TERMINAL_THEMES.find((theme) => theme.id === `system-${id}-light`);
const darkTerminalTheme = TERMINAL_THEMES.find((theme) => theme.id === `system-${id}-dark`);
assert.equal(lightTerminalTheme?.type, "light", id);
assert.equal(darkTerminalTheme?.type, "dark", id);
}
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
XTERM_UNLIMITED_SCROLLBACK_CAP,
resolveXTermScrollback,
} from './xtermPerformance';
test('resolveXTermScrollback maps the unlimited sentinel to a 50000 row cap', () => {
assert.equal(XTERM_UNLIMITED_SCROLLBACK_CAP, 50000);
assert.equal(resolveXTermScrollback(0), 50000);
});
test('resolveXTermScrollback preserves explicit positive scrollback values', () => {
assert.equal(resolveXTermScrollback(10000), 10000);
assert.equal(resolveXTermScrollback(50000), 50000);
});

View File

@@ -0,0 +1,200 @@
/**
* XTerm.js Performance Optimization Configuration
*
* This file contains platform-specific optimizations for xterm performance.
* macOS has different performance characteristics than Windows due to:
* - Stricter GPU memory management
* - Different rendering pipeline (Metal vs DirectX)
* - Memory pressure handling
*/
export const XTERM_UNLIMITED_SCROLLBACK_CAP = 50000;
export function resolveXTermScrollback(scrollback: number): number {
// xterm.js treats 0 as "no scrollback". Keep the app's 0 sentinel useful
// without asking xterm to resize/reflow nearly one million buffer rows.
return scrollback === 0 ? XTERM_UNLIMITED_SCROLLBACK_CAP : scrollback;
}
export const XTERM_PERFORMANCE_CONFIG = {
// Memory and Scrollback Settings
scrollback: {
// Windows can handle larger buffers efficiently
default: 3000,
// macOS performance degrades with large scrollbacks
// due to more aggressive memory pressure
macOS: 1000,
// Mobile-like environments
lowMemory: 500,
},
// Rendering optimizations
rendering: {
// Disable cursor blinking - reduces render calls significantly
cursorBlink: false,
// Allow transparency is expensive on macOS with Metal
// Disabling it improves performance by 15-20%
allowTransparency: false,
// Custom glyphs: xterm.js draws box/block characters on canvas
// instead of using font glyphs, eliminating gaps between cells
customGlyphs: true,
// Font rendering settings
letterSpacing: 0,
lineHeight: 1,
// Keep viewport movement smooth without feeling sluggish.
smoothScrollDuration: 120,
},
// WebGL-specific optimizations
webgl: {
// Enable WebGL by default for GPU acceleration
enabled: true,
// User can choose DOM renderer on any platform (canvas removed in xterm 6.0)
preferDOM: false,
// Handle WebGL context loss gracefully
enableContextLoss: true,
},
// Event handling optimizations
events: {
// Use document override for better event routing on macOS
documentOverride: true,
// Standard tab width (8 spaces)
tabStopWidth: 8,
// Let the SSH daemon handle EOL conversion
convertEol: false,
// Allow bracketed paste mode for better paste handling
ignoreBracketedPasteMode: false,
},
// Logging (disable in production for performance)
logging: {
logLevel: 'off' as const, // 'off' | 'error' | 'warn' | 'info' | 'debug'
},
// Resize debouncing (macOS can get flooded with resize events)
resize: {
// Debounce delay in milliseconds
// Higher values reduce CPU usage but may feel less responsive
debounceMs: 50,
// Use requestAnimationFrame for resize fitting
useRAF: true,
},
// Performance monitoring thresholds
monitoring: {
// Log performance warning if render takes longer than this (ms)
slowRenderThreshold: 16, // 60fps = 16.67ms per frame
// Log warning if data buffer gets too large
largeBufferThreshold: 1024 * 1024, // 1MB
},
// Output-pressure quiet window shared by optional terminal side work.
highlighting: {
// Let large output settle before deferred work catches up.
largeOutputQuietMs: 480,
},
};
export type XTermPlatform = "darwin" | "win32" | "linux";
type RendererType = "dom";
type LogLevel = "off" | "error" | "warn" | "info" | "debug";
export type ResolvedXTermPerformance = {
options: {
scrollback: number;
cursorBlink: boolean;
allowTransparency: boolean;
customGlyphs: boolean;
letterSpacing: number;
lineHeight: number;
smoothScrollDuration: number;
documentOverride: boolean;
tabStopWidth: number;
convertEol: boolean;
ignoreBracketedPasteMode: boolean;
logLevel: LogLevel;
rendererType?: RendererType;
};
useWebGLAddon: boolean;
preferDOMRenderer: boolean;
};
const isLowMemoryDevice = (deviceMemoryGb?: number) =>
typeof deviceMemoryGb === "number" && deviceMemoryGb > 0 && deviceMemoryGb <= 4;
export type RendererPreference = "auto" | "webgl" | "dom";
/**
* Resolve a platform and hardware aware performance profile.
* When rendererType is 'auto', uses DOM on low-memory devices to avoid WebGL overhead.
*/
export function resolveXTermPerformanceConfig({
platform = "darwin",
deviceMemoryGb,
rendererType = "auto",
}: {
platform?: XTermPlatform;
deviceMemoryGb?: number;
rendererType?: RendererPreference;
} = {}): ResolvedXTermPerformance {
const baseConfig = XTERM_PERFORMANCE_CONFIG;
const lowMem = isLowMemoryDevice(deviceMemoryGb);
// Determine if we should use DOM renderer (canvas removed in xterm 6.0)
let resolvedPreferDOM: boolean;
if (rendererType === "dom") {
resolvedPreferDOM = true;
} else if (rendererType === "webgl") {
resolvedPreferDOM = false;
} else {
// Auto mode: use DOM on low-memory devices
resolvedPreferDOM = baseConfig.webgl.preferDOM || lowMem;
}
const scrollbackProfile = lowMem
? "lowMemory"
: platform === "darwin"
? "macOS"
: "default";
const resolvedRendererType = resolvedPreferDOM ? ("dom" as const) : undefined;
const baseOptions = {
scrollback: baseConfig.scrollback[scrollbackProfile],
cursorBlink: baseConfig.rendering.cursorBlink,
allowTransparency: baseConfig.rendering.allowTransparency,
customGlyphs: baseConfig.rendering.customGlyphs,
letterSpacing: baseConfig.rendering.letterSpacing,
lineHeight: baseConfig.rendering.lineHeight,
smoothScrollDuration: baseConfig.rendering.smoothScrollDuration,
documentOverride: baseConfig.events.documentOverride,
tabStopWidth: baseConfig.events.tabStopWidth,
convertEol: baseConfig.events.convertEol,
ignoreBracketedPasteMode: baseConfig.events.ignoreBracketedPasteMode,
logLevel: baseConfig.logging.logLevel,
};
const options = resolvedRendererType
? { ...baseOptions, rendererType: resolvedRendererType }
: baseOptions;
return {
options,
useWebGLAddon: baseConfig.webgl.enabled && !resolvedPreferDOM,
preferDOMRenderer: resolvedPreferDOM,
};
}