Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions docs/design/theme-families.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) 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 |
| 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.
Expand Down
7 changes: 5 additions & 2 deletions docs/development/v021-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

Expand All @@ -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

Expand Down
14 changes: 14 additions & 0 deletions docs/testing/v021-physical-qa.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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.
50 changes: 42 additions & 8 deletions src/appearance/ThemeStudio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Exclude<StudioTab, 'import'>, number>;
Expand Down Expand Up @@ -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<Exclude<NativePaletteId, 'custom'>> {
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;
Expand All @@ -369,15 +383,15 @@ 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. */
export function previewTheme(state: ThemeStudioState, context: StudioContext): {theme: CustomTheme; palette?: Exclude<NativePaletteId, 'custom'>} | undefined {
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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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') {
Expand All @@ -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)};
}
Expand Down Expand Up @@ -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'
Expand Down
Loading
Loading