From a6a3068167a95bf863505ae92ffa48bdc1cba128 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:49:04 +0530 Subject: [PATCH 1/2] Theme families: Ayu, Kanagawa, Everforest, Flexoki and Night Owl (#401) Twelve variants with colors copied exactly from each project's MIT-licensed source at a recorded revision (sources, revisions, licenses and role mapping in docs/design/theme-families.md). Every role fill keeps 4.5:1 module text contrast; light variants keep NMSh's text tiers. Existing ids and saved choices are unchanged; Lavender stays the default. Oxocarbon (grays computed at runtime in HSLuv), GitHub Primer (values from a built primitives release), Material/Palenight and Monokai (licensing not verified) are deferred with reasons. Theme Studio's Built-in list gains / to filter by name, family or dark/light; Esc clears the filter before closing. --- CHANGELOG.md | 1 + docs/design/theme-families.md | 29 ++++++++++++++ docs/development/v021-ledger.md | 7 +++- docs/testing/v021-physical-qa.md | 14 +++++++ src/appearance/ThemeStudio.ts | 50 ++++++++++++++++++++---- src/appearance/themeFamilies.ts | 66 +++++++++++++++++++++++++++++++- src/appearance/themeSelection.ts | 3 +- src/prompt/configuration.ts | 8 +++- tests/themeFamilies.test.ts | 42 +++++++++++++++++++- 9 files changed, 205 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61060a40..862aad11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **Semantic Output** for test runs and builds ([#386](https://github.com/raiseCatError/notMyShell/issues/386), first phase). A completed block whose output NMSh recognizes gets one overview row with the counts the tool printed (`✔ node --test · 1,772 passed · 0 failed · 25 skipped`, `✘ TypeScript · 3 errors in 2 files`) and, when it listed failures or diagnostics, a details row that opens them, taken verbatim from the output. Recognized: Node's test runner (spec and TAP), Jest, Vitest, pytest, `cargo test`, `go test`, TypeScript, Rust and GCC/Clang diagnostics; anything else stays raw only, and short passing output gets nothing. Details start open when the raw output is folded; click the details row or use Actions → Show or hide details. The output itself, its folding, search, selection and plain `/copy` are unchanged. `/copy --semantic` copies the reading (raw for an output without one, as the note says) and `/copy --both` the raw output and the reading under labelled NMSh separators. Settings → Transcript → Semantic Output turns it off. - **Native modules: Bun, Nix shell and container** ([#364](https://github.com/raiseCatError/notMyShell/issues/364)). Bun's requested version comes from `.bun-version` or `package.json` (`packageManager`, `engines.bun`) and the active one from its install location; the Nix shell module shows `pure`/`impure` from `IN_NIX_SHELL`; the container module names Docker, Podman, a dev container or Codespaces from marker files and host variables. None runs anything; all are off until enabled in `/modules`, which recommends Bun and Nix when their files are present. - **`/modules explain` and `/modules timings`** ([#365](https://github.com/raiseCatError/notMyShell/issues/365)) write a plain-text report into the transcript: each configured module's surface, whether it is on screen and why, and its sources' state, age and last collection time; or the slowest last collections. They read what the Context Engine already has and collect nothing. +- **Five more bundled theme families** ([#401](https://github.com/raiseCatError/notMyShell/issues/401)): Ayu (Dark, Mirage, Light), Kanagawa (Wave, Dragon, Lotus), Everforest (Dark, Light), Flexoki (Dark, Light) and Night Owl (Night Owl, Light Owl), with colors copied exactly from each project's MIT-licensed source at a recorded revision. They theme NMSh's own prompt fills and chrome only; the terminal's own colors and configuration are never changed. Existing theme ids and saved choices are unchanged, and Lavender stays the default. Theme Studio's Built-in list gains `/` to filter by name, family, or `dark`/`light`. - **`/track`** keeps one live result of a finite command in the transcript, refreshed in place ([#393](https://github.com/raiseCatError/notMyShell/issues/393)). `/track df -h` or `/track --every 5s -- docker system df` writes the command once, then shows a status row (`live · every 5s · updated 13:49:25 · exit 0`, a failing exit, a timeout, paused, stale, refreshing) and the latest result under it, with a stable height. The shell stays free; nothing accumulates per refresh, and find, selection and `/copy` never include the live rows. `/track copy` copies the latest result, `/track refresh`, `pause`, `resume` and `stop` control it, and stopping leaves the last result in the transcript once. Samples run `sh -c` from the starting directory with your shell's PATH (not its aliases or functions), never overlap, and are bounded (200 kB, a timeout, at most 8 trackers); destructive and unknown commands are refused or need a Yes, as for `/watch`. Trackers stop with the window that started them. `/watch` keeps its manager and its behavior; both share one scheduler, which gains stricter interval parsing, refusals for substitutions, background jobs and heredocs, UTF-8-safe bounded capture, visible timeouts and a stop grace period (from draft #394). ### Fixed diff --git a/docs/design/theme-families.md b/docs/design/theme-families.md index 95ebf25b..6f6ae928 100644 --- a/docs/design/theme-families.md +++ b/docs/design/theme-families.md @@ -27,6 +27,11 @@ are derived, so older configurations load unchanged: | Nord | Nord | | | Solarized | Dark, Light | | | One Dark | One Dark, One Light | | +| Ayu | Dark, Mirage, Light | v0.21; Mirage is the family default | +| Kanagawa | Wave, Dragon, Lotus | v0.21 | +| Everforest | Dark, Light | v0.21; medium contrast (soft/hard change only the editor background) | +| Flexoki | Dark, Light | v0.21 | +| Night Owl | Night Owl, Light Owl | v0.21 | | Custom | the user's theme (`customTheme`) | NMSh Theme JSON | Settings shows `Theme family`, then nested `Variant` (hidden for one-variant @@ -54,6 +59,30 @@ NMSh never fetches palettes at runtime. | Nord | github.com/nordtheme/nord `src/nord.css` | MIT | Copyright (c) 2016-present Sven Greb | | Solarized | github.com/altercation/solarized (README) | MIT | Copyright (c) 2011 Ethan Schoonover | | One Dark / One Light | github.com/atom/one-dark-syntax, one-light-syntax `styles/colors.less` (HSL converted to hex) | MIT | Copyright (c) 2016 GitHub Inc. | +| Ayu | github.com/ayu-theme/ayu-colors `themes/dark.yaml`, `mirage.yaml`, `light.yaml` (d461f029) | MIT | Copyright (c) 2016 Ike Ku | +| Kanagawa | github.com/rebelot/kanagawa.nvim `lua/kanagawa/colors.lua`, `themes.lua` (bb85e4bf) | MIT | Copyright (c) 2021 Tommaso Laurenzi | +| Everforest | github.com/sainnhe/everforest `palette.md` (85a86eb6) | MIT | Copyright (c) 2019 Sainnhe Park | +| Flexoki | github.com/kepano/flexoki `README.md` palette tables (8d723bac) | MIT | Copyright (c) 2023 Steph Ango | +| Night Owl | github.com/sdras/night-owl-vscode-theme `themes/Night Owl-color-theme.json`, `Night Owl-Light-color-theme.json` (cc291eba) | MIT | Copyright (c) 2018 Sarah Drasner | + +v0.21 mapping notes. Values are used exactly as each source writes them; the mapping to NMSh roles follows the +theme's own semantics where it has them: + +- **Ayu**: the YAML's base palette colors (not the generated syntax tones), `editor.fg`, `ui.fg`, `ui.panel.bg`, + `common.accent.tint` and `common.error`. Ayu defines one text color, so primary and secondary are the same. +- **Kanagawa**: each theme's roles from `themes.lua`: project = `fun`, path = `bg_p2`, branch = `keyword`, + Node = `string`, Go = `type`, Python = `identifier`, separator = `nontext`, subtle = `comment`, selection = + `bg_visual`, status = `ok`/`error`. +- **Everforest**: backgrounds by their position in `palette.md` (bg_dim, bg0…bg5), the seven accents and three greys. +- **Flexoki**: dark uses the 400 accents and base tiers (tx base-200, tx-2 base-500, tx-3 base-700, ui-2 base-850, + ui-3 base-800); light uses the 600 accents, as Flexoki documents its standard palette. +- **Night Owl**: editor, selection, line-number and terminal ANSI colors, and the comment token color. + +Considered for v0.21 and not bundled: **Oxocarbon** (its grays are computed at runtime in HSLuv by the theme's own +code; reproducing them would need running that code or re-implementing it, and the issue asks for no +approximations), **GitHub Primer** (values come from a built `@primer/primitives` release, not the theme +repository), **Material/Palenight** and **classic Monokai** (licensing and canonical source not verified), and +Tier 2 families. The Built-in list in Theme Studio gains `/` to filter by name, family, or dark/light. Only color values are used; no code, assets or names beyond the theme names for identification. Random gallery or theme-site copies were not used. diff --git a/docs/development/v021-ledger.md b/docs/development/v021-ledger.md index 10d08c22..4cedd383 100644 --- a/docs/development/v021-ledger.md +++ b/docs/development/v021-ledger.md @@ -21,7 +21,7 @@ Automated results here are not physical-terminal validation. | 4 | Semantic Output | #386 | `feature/v021-semantic` (stacked on slice 3) | | PR open | | 5 | `/track` | #393 (#394) | `feature/v021-track` (stacked on slice 4) | | PR open | | 6 | Modules and diagnostics | #364, #365 | `feature/v021-modules` (stacked on slice 5) | | PR open | -| 7 | Themes and controls | #401, #336 | | | | +| 7 | Themes and controls | #401, #336 | `feature/v021-themes` (stacked on slice 6) | | PR open | ## Decisions @@ -32,6 +32,9 @@ Automated results here are not physical-terminal validation. - `/track` stays window-owned like `/watch` in v0.21; service-owned scheduling is deferred and documented. - PR #394's `WatchTasks.ts`/`WatchPanel.ts` were adopted as written (neither file changed on `dev` since its base) and its tests kept; its other edits were re-applied by hand. #394 itself is superseded by the slice 5 PR and was not merged. +- #336: the editor's unsaved-changes and conflict dialogs use the existing `src/ui/actionControls.ts` renderer (slice 3); + no other v0.21 surface needed a new control, so no further consolidation was made. +- #401: Oxocarbon, GitHub Primer, Material/Palenight and Monokai were deferred (see theme-families.md for why). - `/track` samples run `sh -c` (as `/watch` always has) with the live shell's PATH, not the session shell, so `/watch` semantics stay the same for Fish users. @@ -40,7 +43,7 @@ Automated results here are not physical-terminal validation. Checklist: [v0.21 physical QA](../testing/v021-physical-qa.md). Slice 1: section 1 (multi-row progress in Ghostty and Terminal.app, queued highlight colors). Slice 2: section 2 (`/view` keys, rendering, refusals, hostile text, NO_COLOR). Slice 3: section 3 (`/edit` editing keys, saving, conflicts, refusals). Slice 4: section 4 (overview rows, details -toggle, copy flags, Off). Slice 5: section 5 (`/track` block, refresh, copy, stop, cleanup). Slice 6: section 6 (Bun, Nix, container, reports). +toggle, copy flags, Off). Slice 5: section 5 (`/track` block, refresh, copy, stop, cleanup). Slice 6: section 6 (Bun, Nix, container, reports). Slice 7: section 7 (theme families, filter, contrast, host untouched). ## Verification notes diff --git a/docs/testing/v021-physical-qa.md b/docs/testing/v021-physical-qa.md index 3fbe93a7..b4fe9763 100644 --- a/docs/testing/v021-physical-qa.md +++ b/docs/testing/v021-physical-qa.md @@ -24,6 +24,8 @@ | 5 `/track` | Ghostty | macOS | zsh | | Not run | | | 5 `/track` | Terminal.app | macOS | Fish | | Not run | | | 6 Modules | Ghostty | macOS | zsh | | Not run | | +| 7 Themes | Ghostty | macOS | zsh | | Not run | | +| 7 Themes | Terminal.app | macOS | zsh | | Not run | | ## 1. Multi-row progress redraws (#388) @@ -114,3 +116,15 @@ - [ ] 6.3 In a Docker or Podman container (or a Codespace), the container module names it; on the host it shows nothing. - [ ] 6.4 `/modules explain` lists every configured module with its surface and reason; `/modules timings` lists the slowest collections; neither changes what is collected (the Status Strip CPU stays as it was). + +## 7. Theme families + +- [ ] 7.1 `/theme`: the Built-in list shows Ayu, Kanagawa, Everforest, Flexoki and Night Owl; `/` then `kanagawa` leaves + three; `/` then `light` leaves the light variants; Esc clears the filter, Esc again closes. +- [ ] 7.2 Activate each new variant on a dark terminal: prompt fills, branch, status, selection band and transcript + chrome are readable; module text stays readable on every fill. +- [ ] 7.3 Activate the light variants (Ayu Light, Kanagawa Lotus, Everforest Light, Flexoki Light, Light Owl) on a light + and on a dark terminal: accents and rules change, NMSh's text tiers stay readable. +- [ ] 7.4 `NO_COLOR=1` and a 256-color terminal (`TERM=xterm-256color`, no `COLORTERM`): nothing breaks. +- [ ] 7.5 Switching themes never changes Ghostty's or Terminal.app's own colors or configuration files; a config saved + with an older theme id still loads that theme. diff --git a/src/appearance/ThemeStudio.ts b/src/appearance/ThemeStudio.ts index 679ab433..794b4906 100644 --- a/src/appearance/ThemeStudio.ts +++ b/src/appearance/ThemeStudio.ts @@ -26,7 +26,7 @@ import {categoryOf, findTheme, IMPORTER_VERSION, provenanceLabel, type ThemeAsse import {assetRef, builtinRef, builtinTheme, type ThemeRef} from './themeRefs.js'; import {IMPORT_FORMAT_CHOICES, IMPORT_SIZE_LIMIT, importFormatLabel, importThemeSource, type ImportOutcome, type ImportPreview} from './themeImporters.js'; import {BRIDGE_TARGET_LABELS, type BridgeTargetId} from '../themeBridge/model.js'; -import type {CatppuccinAccent} from './themeFamilies.js'; +import {THEME_FAMILIES, themeVariant, type CatppuccinAccent} from './themeFamilies.js'; /** * Theme Studio (/theme): create, edit, import, manage and select NMSh Native @@ -300,6 +300,8 @@ export interface StudioContext { export interface ThemeStudioState { tab: StudioTab; + /** Built-in tab: `/` narrows the list by name, family, or dark/light. */ + builtinFilter?: {text: string; editing: boolean}; focus: 'tabs' | 'list'; /** Selected row per library tab. */ selected: Record, number>; @@ -354,8 +356,20 @@ function libraryItems(context: StudioContext, tab: 'imported' | 'custom'): Theme return context.themes.filter(asset => categoryOf(asset) === tab); } +/** The built-in themes the filter keeps (every one without a filter): each word must match the name, family or dark/light. */ +function builtinList(state: ThemeStudioState): Array> { + const query = state.builtinFilter?.text.trim().toLowerCase(); + if (!query) return BUILTIN_PALETTES; + return BUILTIN_PALETTES.filter(palette => { + const variant = themeVariant(palette); + const family = THEME_FAMILIES.find(item => item.id === (variant?.family ?? 'nmsh'))?.label ?? 'NMSh'; + const words = `${builtinTheme(palette, 'mauve').name} ${family} ${variant ? (variant.dark ? 'dark' : 'light') : ''}`.toLowerCase(); + return query.split(/\s+/u).every(term => words.includes(term)); + }); +} + function rowCount(state: ThemeStudioState, context: StudioContext): number { - if (state.tab === 'builtin') return BUILTIN_PALETTES.length; + if (state.tab === 'builtin') return builtinList(state).length; if (state.tab === 'imported') return libraryItems(context, 'imported').length; if (state.tab === 'custom') return libraryItems(context, 'custom').length + CUSTOM_ACTIONS; return 0; @@ -369,7 +383,7 @@ export function selectedAsset(state: ThemeStudioState, context: StudioContext): } export function selectedBuiltinRef(state: ThemeStudioState, context: StudioContext): ThemeRef { - return builtinRef(BUILTIN_PALETTES[state.selected.builtin] ?? 'lavender', context.accent); + return builtinRef(builtinList(state)[state.selected.builtin] ?? 'lavender', context.accent); } /** The theme the studio is showing: the editor draft, the import preview, or the selected row. */ @@ -377,7 +391,7 @@ export function previewTheme(state: ThemeStudioState, context: StudioContext): { if (state.editor) return {theme: state.editor.draft}; if (state.tab === 'import') return state.importPreview ? {theme: state.importPreview.theme} : undefined; if (state.tab === 'builtin') { - const palette = BUILTIN_PALETTES[state.selected.builtin] ?? 'lavender'; + const palette = builtinList(state)[state.selected.builtin] ?? 'lavender'; return {theme: builtinTheme(palette, context.accent), palette}; } const asset = selectedAsset(state, context); @@ -394,7 +408,7 @@ const CUSTOM_ACTIONS = 2; /** C toggles the local preview Chroma wherever no text is being typed. */ function typing(state: ThemeStudioState): boolean { - return Boolean(state.rename || state.editor?.picker || state.editor?.editingName !== undefined || (state.tab === 'import' && state.importField === 'path' && state.focus === 'list' && !state.importPreview && !state.editor)); + return Boolean(state.rename || (state.tab === 'builtin' && state.builtinFilter?.editing) || state.editor?.picker || state.editor?.editingName !== undefined || (state.tab === 'import' && state.importField === 'path' && state.focus === 'list' && !state.importPreview && !state.editor)); } export function studioKey(state: ThemeStudioState, key: Key, level: ColorLevel, cwd: string, context: StudioContext): StudioAction | undefined { @@ -438,6 +452,20 @@ export function studioKey(state: ThemeStudioState, key: Key, level: ColorLevel, const tabStep = tabCycleDelta(key); if (tabStep && !(state.tab === 'import' && state.focus !== 'tabs' && state.importField === 'path')) { switchTab(state, tabStep); return undefined; } if (state.tab === 'import') return importKey(state, key, cwd); + if (state.tab === 'builtin' && state.builtinFilter) { + const filter = state.builtinFilter; + // Esc clears a filter before it closes the studio. + if (key.kind === 'escape') { state.builtinFilter = undefined; state.selected.builtin = 0; return undefined; } + if (filter.editing) { + if (key.kind === 'enter') { filter.editing = false; return undefined; } + const next = editText(filter.text, key); + if (next !== undefined) { filter.text = next.slice(0, 40); state.selected.builtin = 0; return undefined; } + } + } + if (state.tab === 'builtin' && state.focus === 'list' && key.kind === 'text' && key.value === '/') { + state.builtinFilter = {text: state.builtinFilter?.text ?? '', editing: true}; + return undefined; + } if (key.kind === 'escape' || key.kind === 'interrupt') return {kind: 'close'}; if (key.kind === 'left' || key.kind === 'right') { switchTab(state, key.kind === 'left' ? -1 : 1); return undefined; } if (state.focus === 'tabs') { @@ -458,7 +486,7 @@ export function studioKey(state: ThemeStudioState, key: Key, level: ColorLevel, if (letter === 'd') return {kind: 'duplicateBuiltin', ref}; if (letter === 'e') { // Built-ins are immutable: editing starts a new Custom theme from it. - const palette = BUILTIN_PALETTES[state.selected.builtin] ?? 'lavender'; + const palette = builtinList(state)[state.selected.builtin] ?? 'lavender'; state.editor = createThemeEditor(undefined, palette); state.editor.draft = {...builtinTheme(palette, context.accent), name: `My ${builtinTheme(palette, context.accent).name}`.slice(0, 48)}; } @@ -565,14 +593,20 @@ export function renderThemeStudio(state: ThemeStudioState, context: StudioContex if (state.tab === 'builtin') { body.push(` ${subtle}NMSh and bundled theme families. Built-ins are immutable; duplicate one to edit it.${RESET}`); - const {start, items} = window(BUILTIN_PALETTES, state.selected.builtin); + const visible = builtinList(state); + if (state.builtinFilter) { + const caret = state.builtinFilter.editing ? `${accent}▏${RESET}` : ''; + body.push(` ${accent}Filter:${RESET} ${state.builtinFilter.text.replace(/[\u0000-\u001f\u007f-\u009f\p{Cf}]/gu, '')}${caret}${subtle} · ${visible.length} of ${BUILTIN_PALETTES.length}${RESET}`); + if (!visible.length) body.push(` ${subtle}No theme matches; Esc clears the filter.${RESET}`); + } + const {start, items} = window(visible, state.selected.builtin); items.forEach((palette, offset) => { const index = start + offset; const theme = builtinTheme(palette, context.accent); const ref = builtinRef(palette, context.accent); body.push(`${mark(index === state.selected.builtin)} ${focusForeground(index === state.selected.builtin)}${padCells(theme.name, 26)}${RESET}${swatches(Object.values(theme.prompt).slice(0, 6), level)}${active(ref)}${pinned(ref)}`); }); - controls.push(['Enter', 'set active'], ['D', 'duplicate to Custom'], ['E', 'edit a copy']); + controls.push(['Enter', 'set active'], ['/', 'filter'], ['D', 'duplicate to Custom'], ['E', 'edit a copy']); } else if (state.tab === 'imported' || state.tab === 'custom') { const items = libraryItems(context, state.tab); const rows: Array<{label: string; detail: string; ref?: ThemeRef; colors?: string[]}> = state.tab === 'custom' diff --git a/src/appearance/themeFamilies.ts b/src/appearance/themeFamilies.ts index 48cad325..87de31cb 100644 --- a/src/appearance/themeFamilies.ts +++ b/src/appearance/themeFamilies.ts @@ -44,7 +44,8 @@ export interface ThemeVariant { accents?: Record; } -export type ThemeFamilyId = 'nmsh' | 'catppuccin' | 'dracula' | 'tokyonight' | 'gruvbox' | 'rosepine' | 'nord' | 'solarized' | 'onedark' | 'custom'; +export type ThemeFamilyId = 'nmsh' | 'catppuccin' | 'dracula' | 'tokyonight' | 'gruvbox' | 'rosepine' | 'nord' | 'solarized' | 'onedark' + | 'ayu' | 'kanagawa' | 'everforest' | 'flexoki' | 'nightowl' | 'custom'; export interface ThemeFamily { id: ThemeFamilyId; @@ -65,6 +66,11 @@ export const THEME_FAMILIES: readonly ThemeFamily[] = [ {id: 'nord', label: 'Nord', variantLabel: 'Variant', source: 'https://github.com/nordtheme/nord', license: 'MIT'}, {id: 'solarized', label: 'Solarized', variantLabel: 'Variant', source: 'https://github.com/altercation/solarized', license: 'MIT'}, {id: 'onedark', label: 'One Dark', variantLabel: 'Variant', source: 'https://github.com/atom/one-dark-syntax', license: 'MIT'}, + {id: 'ayu', label: 'Ayu', variantLabel: 'Version', source: 'https://github.com/ayu-theme/ayu-colors', license: 'MIT'}, + {id: 'kanagawa', label: 'Kanagawa', variantLabel: 'Theme', source: 'https://github.com/rebelot/kanagawa.nvim', license: 'MIT'}, + {id: 'everforest', label: 'Everforest', variantLabel: 'Variant', source: 'https://github.com/sainnhe/everforest', license: 'MIT'}, + {id: 'flexoki', label: 'Flexoki', variantLabel: 'Variant', source: 'https://github.com/kepano/flexoki', license: 'MIT'}, + {id: 'nightowl', label: 'Night Owl', variantLabel: 'Variant', source: 'https://github.com/sdras/night-owl-vscode-theme', license: 'MIT'}, {id: 'custom', label: 'Custom', variantLabel: 'Theme'}, ]; @@ -172,6 +178,64 @@ export const THEME_VARIANTS: readonly ThemeVariant[] = [ roles: {project: '#4078f2', cwd: '#d4d4d6', gitBranch: '#a626a4', node: '#50a14f', go: '#0184bc', python: '#c18401', docker: '#986801', kubernetes: '#a0a1a7', success: '#50a14f', failure: '#e45649'}, ui: {accent: '#4078f2', separator: '#a0a1a7', success: '#50a14f', failure: '#e45649'}}, + // ayu-theme/ayu-colors themes/{dark,mirage,light}.yaml (d461f029): the palette's base colors, editor and UI + // foregrounds, panel background and accent tint as written; syntax tones derived from them are not used. + {id: 'ayuDark', family: 'ayu', variant: 'Dark', label: 'Ayu Dark', description: 'Ayu Dark, warm accents on near black', dark: true, + roles: {project: '#e6b450', cwd: '#5a6673', gitBranch: '#d2a6ff', node: '#aad94c', go: '#95e6cb', python: '#e6c08a', docker: '#59c2ff', + kubernetes: '#39bae6', success: '#aad94c', failure: '#f07178'}, + ui: {accent: '#e6b450', separator: '#5a6378', success: '#aad94c', failure: '#d95757', primary: '#bfbdb6', secondary: '#bfbdb6', subtle: '#5a6378', selection: '#141821'}}, + {id: 'ayuMirage', family: 'ayu', variant: 'Mirage', label: 'Ayu Mirage', description: 'Ayu Mirage, softer on blue-gray', dark: true, + roles: {project: '#ffcc66', cwd: '#6e7c8f', gitBranch: '#dfbfff', node: '#d5ff80', go: '#95e6cb', python: '#d9be98', docker: '#73d0ff', + kubernetes: '#5ccfe6', success: '#d5ff80', failure: '#f28779'}, + ui: {accent: '#ffcc66', separator: '#707a8c', success: '#d5ff80', failure: '#ff6666', primary: '#cccac2', secondary: '#cccac2', subtle: '#707a8c', selection: '#282e3b'}}, + {id: 'ayuLight', family: 'ayu', variant: 'Light', label: 'Ayu Light', description: 'Ayu Light, bright and clear', dark: false, + roles: {project: '#f29718', cwd: '#adaeb1', gitBranch: '#a37acc', node: '#86b300', go: '#4cbf99', python: '#eba400', docker: '#22a4e6', + kubernetes: '#55b4d4', success: '#86b300', failure: '#f07171'}, + ui: {accent: '#f29718', separator: '#828e9f', success: '#86b300', failure: '#e65050'}}, + // rebelot/kanagawa.nvim lua/kanagawa/colors.lua and themes.lua (bb85e4bf): each theme's own role colors + // (fun, keyword, string, type, identifier, bg_p2, nontext, comment, bg_visual, ok, error). + {id: 'kanagawaWave', family: 'kanagawa', variant: 'Wave', label: 'Kanagawa Wave', description: 'Kanagawa Wave, ink and wave blue', dark: true, + roles: {project: '#7e9cd8', cwd: '#363646', gitBranch: '#957fb8', node: '#98bb6c', go: '#7aa89f', python: '#e6c384', docker: '#7fb4ca', + kubernetes: '#d27e99', success: '#98bb6c', failure: '#e82424'}, + ui: {accent: '#7e9cd8', separator: '#54546d', success: '#98bb6c', failure: '#e82424', primary: '#dcd7ba', secondary: '#c8c093', subtle: '#727169', selection: '#223249'}}, + {id: 'kanagawaDragon', family: 'kanagawa', variant: 'Dragon', label: 'Kanagawa Dragon', description: 'Kanagawa Dragon, dark and muted', dark: true, + roles: {project: '#8ba4b0', cwd: '#393836', gitBranch: '#8992a7', node: '#8a9a7b', go: '#8ea4a2', python: '#c4b28a', docker: '#658594', + kubernetes: '#a292a3', success: '#98bb6c', failure: '#e82424'}, + ui: {accent: '#8ba4b0', separator: '#625e5a', success: '#98bb6c', failure: '#e82424', primary: '#c5c9c5', secondary: '#c8c093', subtle: '#737c73', selection: '#223249'}}, + {id: 'kanagawaLotus', family: 'kanagawa', variant: 'Lotus', label: 'Kanagawa Lotus', description: 'Kanagawa Lotus, the light theme', dark: false, + roles: {project: '#4d699b', cwd: '#e4d794', gitBranch: '#624c83', node: '#6f894e', go: '#597b75', python: '#77713f', docker: '#6693bf', + kubernetes: '#b35b79', success: '#6f894e', failure: '#e82424'}, + ui: {accent: '#4d699b', separator: '#a09cac', success: '#6f894e', failure: '#e82424'}}, + // sainnhe/everforest palette.md (85a86eb6), medium contrast: backgrounds by position (bg0…bg5), accents, greys. + // Soft and hard change only the editor background, which NMSh does not paint. + {id: 'everforestDark', family: 'everforest', variant: 'Dark', label: 'Everforest Dark', description: 'Everforest Dark, green-based and calm', dark: true, + roles: {project: '#a7c080', cwd: '#475258', gitBranch: '#d699b6', node: '#83c092', go: '#7fbbb3', python: '#dbbc7f', docker: '#7fbbb3', + kubernetes: '#e69875', success: '#a7c080', failure: '#e67e80'}, + ui: {accent: '#a7c080', separator: '#7a8478', success: '#a7c080', failure: '#e67e80', primary: '#d3c6aa', secondary: '#9da9a0', subtle: '#7a8478', selection: '#3d484d'}}, + {id: 'everforestLight', family: 'everforest', variant: 'Light', label: 'Everforest Light', description: 'Everforest Light, warm paper', dark: false, + roles: {project: '#8da101', cwd: '#bdc3af', gitBranch: '#df69ba', node: '#35a77c', go: '#3a94c5', python: '#dfa000', docker: '#3a94c5', + kubernetes: '#f57d26', success: '#8da101', failure: '#f85552'}, + ui: {accent: '#8da101', separator: '#a6b0a0', success: '#8da101', failure: '#f85552'}}, + // kepano/flexoki README (8d723bac): dark uses the 400 accents and base tiers (tx base-200, tx-2 base-500, + // tx-3 base-700, ui-2 base-850, ui-3 base-800); light uses the 600 accents (ui-3 base-200, tx-3 base-300). + {id: 'flexokiDark', family: 'flexoki', variant: 'Dark', label: 'Flexoki Dark', description: 'Flexoki Dark, inky and warm', dark: true, + roles: {project: '#4385be', cwd: '#403e3c', gitBranch: '#8b7ec8', node: '#879a39', go: '#3aa99f', python: '#d0a215', docker: '#da702c', + kubernetes: '#ce5d97', success: '#879a39', failure: '#d14d41'}, + ui: {accent: '#4385be', separator: '#575653', success: '#879a39', failure: '#d14d41', primary: '#cecdc3', secondary: '#878580', subtle: '#575653', selection: '#343331'}}, + {id: 'flexokiLight', family: 'flexoki', variant: 'Light', label: 'Flexoki Light', description: 'Flexoki Light, paper and ink', dark: false, + roles: {project: '#205ea6', cwd: '#cecdc3', gitBranch: '#5e409d', node: '#66800b', go: '#24837b', python: '#ad8301', docker: '#bc5215', + kubernetes: '#a02f6f', success: '#66800b', failure: '#af3029'}, + ui: {accent: '#205ea6', separator: '#b7b5ac', success: '#66800b', failure: '#af3029'}}, + // sdras/night-owl-vscode-theme themes/Night Owl-color-theme.json and Night Owl-Light-color-theme.json (cc291eba): + // editor, selection, line-number and terminal ANSI colors, and the comment token color. + {id: 'nightOwl', family: 'nightowl', variant: 'Night Owl', label: 'Night Owl', description: 'Night Owl, for night owls', dark: true, + roles: {project: '#82aaff', cwd: '#1d3b53', gitBranch: '#c792ea', node: '#22da6e', go: '#7fdbca', python: '#ffeb95', docker: '#21c7a8', + kubernetes: '#f78c6c', success: '#22da6e', failure: '#ef5350'}, + ui: {accent: '#82aaff', separator: '#4b6479', success: '#22da6e', failure: '#ef5350', primary: '#d6deeb', secondary: '#d6deeb', subtle: '#637777', selection: '#1d3b53'}}, + {id: 'nightOwlLight', family: 'nightowl', variant: 'Light Owl', label: 'Light Owl', description: 'Night Owl\'s light theme', dark: false, + roles: {project: '#288ed7', cwd: '#e0e0e0', gitBranch: '#d6438a', node: '#08916a', go: '#2aa298', python: '#e0af02', docker: '#288ed7', + kubernetes: '#d6438a', success: '#08916a', failure: '#de3d3b'}, + ui: {accent: '#288ed7', separator: '#90a7b2', success: '#08916a', failure: '#e64d49'}}, ]; export function themeVariant(id: string): ThemeVariant | undefined { diff --git a/src/appearance/themeSelection.ts b/src/appearance/themeSelection.ts index 4936dea7..33e1e9b2 100644 --- a/src/appearance/themeSelection.ts +++ b/src/appearance/themeSelection.ts @@ -27,7 +27,8 @@ export function variantOptions(family: ThemeFamilyId): VariantOption[] { /** The variant a family opens on: the documented default for each family. */ const DEFAULT_VARIANT: Record = { nmsh: 'lavender', catppuccin: 'catppuccinMocha', dracula: 'dracula', tokyonight: 'tokyonightNight', gruvbox: 'gruvboxDark', - rosepine: 'rosePine', nord: 'nord', solarized: 'solarizedDark', onedark: 'oneDark', custom: 'custom', + rosepine: 'rosePine', nord: 'nord', solarized: 'solarizedDark', onedark: 'oneDark', + ayu: 'ayuMirage', kanagawa: 'kanagawaWave', everforest: 'everforestDark', flexoki: 'flexokiDark', nightowl: 'nightOwl', custom: 'custom', }; const UI_DEFAULT_HEX = (): Record => { diff --git a/src/prompt/configuration.ts b/src/prompt/configuration.ts index 9fe28ccf..7fef4d49 100644 --- a/src/prompt/configuration.ts +++ b/src/prompt/configuration.ts @@ -144,10 +144,14 @@ export type NativePaletteId = 'lavender' | 'brand' | 'cool' | 'warm' | 'grayscal /** Bundled third-party variants (see appearance/themeFamilies). */ export type ThirdPartyPaletteId = 'catppuccinLatte' | 'catppuccinFrappe' | 'catppuccinMacchiato' | 'catppuccinMocha' | 'dracula' | 'tokyonightNight' | 'tokyonightStorm' | 'tokyonightMoon' | 'tokyonightDay' | 'gruvboxDark' | 'gruvboxLight' - | 'rosePine' | 'rosePineMoon' | 'rosePineDawn' | 'nord' | 'solarizedDark' | 'solarizedLight' | 'oneDark' | 'oneLight'; + | 'rosePine' | 'rosePineMoon' | 'rosePineDawn' | 'nord' | 'solarizedDark' | 'solarizedLight' | 'oneDark' | 'oneLight' + | 'ayuDark' | 'ayuMirage' | 'ayuLight' | 'kanagawaWave' | 'kanagawaDragon' | 'kanagawaLotus' | 'everforestDark' | 'everforestLight' + | 'flexokiDark' | 'flexokiLight' | 'nightOwl' | 'nightOwlLight'; export const THIRD_PARTY_PALETTE_IDS: readonly ThirdPartyPaletteId[] = ['catppuccinLatte', 'catppuccinFrappe', 'catppuccinMacchiato', 'catppuccinMocha', 'dracula', 'tokyonightNight', 'tokyonightStorm', 'tokyonightMoon', 'tokyonightDay', 'gruvboxDark', 'gruvboxLight', - 'rosePine', 'rosePineMoon', 'rosePineDawn', 'nord', 'solarizedDark', 'solarizedLight', 'oneDark', 'oneLight']; + 'rosePine', 'rosePineMoon', 'rosePineDawn', 'nord', 'solarizedDark', 'solarizedLight', 'oneDark', 'oneLight', + 'ayuDark', 'ayuMirage', 'ayuLight', 'kanagawaWave', 'kanagawaDragon', 'kanagawaLotus', 'everforestDark', 'everforestLight', + 'flexokiDark', 'flexokiLight', 'nightOwl', 'nightOwlLight']; export type NativeGapChoice = 'off' | 'compact' | 'normal' | 'wide'; /** * Rich Git state colors: `semantic` keeps meaningful Git colors under any diff --git a/tests/themeFamilies.test.ts b/tests/themeFamilies.test.ts index 281fade2..c326e3f6 100644 --- a/tests/themeFamilies.test.ts +++ b/tests/themeFamilies.test.ts @@ -63,7 +63,8 @@ test('Starship and Oh My Posh imports preview lossy mapping before saving and pr }); test('theme families: canonical variants, valid ids, licenses recorded, NMSh themes unchanged', () => { - assert.deepEqual(THEME_FAMILIES.map(family => family.id), ['nmsh', 'catppuccin', 'dracula', 'tokyonight', 'gruvbox', 'rosepine', 'nord', 'solarized', 'onedark', 'custom']); + assert.deepEqual(THEME_FAMILIES.map(family => family.id), ['nmsh', 'catppuccin', 'dracula', 'tokyonight', 'gruvbox', 'rosepine', 'nord', 'solarized', 'onedark', + 'ayu', 'kanagawa', 'everforest', 'flexoki', 'nightowl', 'custom']); for (const family of THEME_FAMILIES.filter(item => item.id !== 'nmsh' && item.id !== 'custom')) { assert.ok(family.source?.startsWith('https://github.com/'), family.id); assert.ok(family.license, family.id); @@ -83,6 +84,18 @@ test('theme families: canonical variants, valid ids, licenses recorded, NMSh the assert.equal(themeVariant('solarizedDark')!.roles.project, '#268bd2'); assert.equal(themeVariant('rosePine')!.roles.project, '#c4a7e7'); assert.equal(themeVariant('oneDark')!.roles.project, '#61afef'); + // v0.21 families, against the pinned upstream files named in docs/design/theme-families.md. + assert.deepEqual(familyVariants('ayu').map(variant => variant.variant), ['Dark', 'Mirage', 'Light']); + assert.deepEqual(familyVariants('kanagawa').map(variant => variant.variant), ['Wave', 'Dragon', 'Lotus']); + assert.deepEqual(familyVariants('everforest').map(variant => variant.variant), ['Dark', 'Light']); + assert.deepEqual(familyVariants('flexoki').map(variant => variant.variant), ['Dark', 'Light']); + assert.deepEqual(familyVariants('nightowl').map(variant => variant.variant), ['Night Owl', 'Light Owl']); + assert.equal(themeVariant('ayuMirage')!.ui.accent, '#ffcc66', 'ayu mirage accent tint'); + assert.equal(themeVariant('kanagawaWave')!.ui.primary, '#dcd7ba', 'fujiWhite'); + assert.equal(themeVariant('kanagawaWave')!.roles.project, '#7e9cd8', 'crystalBlue (fun)'); + assert.equal(themeVariant('everforestDark')!.ui.primary, '#d3c6aa'); + assert.equal(themeVariant('flexokiLight')!.roles.project, '#205ea6', 'blue-600'); + assert.equal(themeVariant('nightOwl')!.ui.selection, '#1d3b53', 'editor.selectionBackground'); assert.equal(NATIVE_PALETTE_IDS.length, 12, 'the NMSh family keeps its twelve themes'); }); @@ -314,3 +327,30 @@ test('color picker: truecolor field and hue, 256-color grid, NO_COLOR channels, try { assert.ok(renderColorPicker(createColorPicker('#ffffff', 'truecolor'), 'x', 50, 'truecolor').some(row => row.includes('+'))); } finally { setIconStyle('nerd'); } }); + +test('Theme Studio: / filters the built-in list by name, family or dark/light; Esc clears before it closes', () => { + const context = {themes: [], accent: 'mauve' as const, pinnedTo: () => []}; + const state = createThemeStudio(context); + const press = (...keys: Array) => { + let action; + for (const key of keys) action = studioKey(state, (typeof key === 'string' ? {kind: 'text', value: key} : key) as never, 'none', '/', context); + return action; + }; + press('/', ...'kanagawa'.split('')); + const shown = plain(renderThemeStudio(state, context, 80, 30, 'none', [])); + assert.match(shown, /Filter: kanagawa▏ · 3 of \d+/u); + assert.match(shown, /Kanagawa Wave[\s\S]*Kanagawa Dragon[\s\S]*Kanagawa Lotus/u); + assert.doesNotMatch(shown, /Lavender Native/u); + press({kind: 'enter'}, {kind: 'down'}); + assert.deepEqual(press({kind: 'enter'}), {kind: 'activate', ref: 'builtin:kanagawaDragon'}, 'selection follows the filtered list'); + assert.equal(state.previewChroma, false); + press({kind: 'escape'}); + assert.equal(state.builtinFilter, undefined, 'Esc cleared the filter'); + press('/', ...'light ayu'.split('')); + assert.match(plain(renderThemeStudio(state, context, 80, 30, 'none', [])), /1 of \d+[\s\S]*Ayu Light/u); + press({kind: 'escape'}, '/', ...'catppuccin latte'.split('')); + assert.match(plain(renderThemeStudio(state, context, 80, 30, 'none', [])), /1 of \d+[\s\S]*Catppuccin Latte/u); + assert.equal(state.previewChroma, false, 'c typed into the filter is text, not the Chroma toggle'); + press({kind: 'escape'}); + assert.deepEqual(press({kind: 'escape'}), {kind: 'close'}); +}); From 7d8e15290533253fab0f3ca7d8fbaa98e5644f58 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:49:29 +0530 Subject: [PATCH 2/2] Theme families: copyright lines as the upstream LICENSE files state them --- docs/design/theme-families.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design/theme-families.md b/docs/design/theme-families.md index 6f6ae928..6dbe2bd9 100644 --- a/docs/design/theme-families.md +++ b/docs/design/theme-families.md @@ -59,9 +59,9 @@ NMSh never fetches palettes at runtime. | Nord | github.com/nordtheme/nord `src/nord.css` | MIT | Copyright (c) 2016-present Sven Greb | | Solarized | github.com/altercation/solarized (README) | MIT | Copyright (c) 2011 Ethan Schoonover | | One Dark / One Light | github.com/atom/one-dark-syntax, one-light-syntax `styles/colors.less` (HSL converted to hex) | MIT | Copyright (c) 2016 GitHub Inc. | -| Ayu | github.com/ayu-theme/ayu-colors `themes/dark.yaml`, `mirage.yaml`, `light.yaml` (d461f029) | MIT | Copyright (c) 2016 Ike Ku | +| Ayu | github.com/ayu-theme/ayu-colors `themes/dark.yaml`, `mirage.yaml`, `light.yaml` (d461f029) | MIT | Copyright (c) Konstantin Pschera (LICENSE gives no year) | | Kanagawa | github.com/rebelot/kanagawa.nvim `lua/kanagawa/colors.lua`, `themes.lua` (bb85e4bf) | MIT | Copyright (c) 2021 Tommaso Laurenzi | -| Everforest | github.com/sainnhe/everforest `palette.md` (85a86eb6) | MIT | Copyright (c) 2019 Sainnhe Park | +| Everforest | github.com/sainnhe/everforest `palette.md` (85a86eb6) | MIT | Copyright (c) 2019 sainnhe | | Flexoki | github.com/kepano/flexoki `README.md` palette tables (8d723bac) | MIT | Copyright (c) 2023 Steph Ango | | Night Owl | github.com/sdras/night-owl-vscode-theme `themes/Night Owl-color-theme.json`, `Night Owl-Light-color-theme.json` (cc291eba) | MIT | Copyright (c) 2018 Sarah Drasner |