From 9796db13d514cde13fbd640721c8cb76df097240 Mon Sep 17 00:00:00 2001 From: echobt <154886644+echobt@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:36:26 +0000 Subject: [PATCH 1/3] fix(tui): replace violet accent with banner green Use the banner's #1F4945 green and contrasting focus backing. Update palette assertions, headless checks and theme documentation. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- docs/customization/themes.md | 10 ++- docs/media/tui-lock-v2/SPEC.md | 22 +++--- docs/media/tui-lock-v2/index.md | 8 +- docs/media/tui-lock-v2/tools/boards.py | 36 ++++----- .../media/tui-lock-v2/tools/render_lock_v2.py | 8 +- .../txt/120x40/settings-theme-submenu.txt | 4 +- docs/media/tui-lock/README.md | 2 +- src/cortex-core/src/markdown/languages.rs | 2 +- src/cortex-core/src/markdown/theme.rs | 2 +- src/cortex-core/src/style.rs | 71 +++++++++++------ src/cortex-tui-capture/src/demo/mod.rs | 2 +- src/cortex-tui/src/cards/help.rs | 2 +- src/cortex-tui/src/interactive/renderer.rs | 28 ++++--- src/cortex-tui/src/lock_boards.rs | 60 ++++++++------ src/cortex-tui/src/lock_proof.rs | 79 +++++++++++-------- src/cortex-tui/src/lock_v2.rs | 8 +- src/cortex-tui/src/modal/help.rs | 2 +- src/cortex-tui/src/modal/models.rs | 2 +- .../src/modal/sessions/rendering.rs | 2 +- src/cortex-tui/src/readme_hero.rs | 10 +-- src/cortex-tui/src/runner/login_screen.rs | 16 ++-- src/cortex-tui/src/runner/trust_screen.rs | 10 ++- src/cortex-tui/src/ui/chrome.rs | 13 +-- src/cortex-tui/src/ui/colors.rs | 44 +++++------ .../src/views/minimal_session/rendering.rs | 2 +- .../src/views/minimal_session/tests.rs | 7 +- .../src/views/minimal_session/view.rs | 13 ++- src/cortex-tui/src/views/question_prompt.rs | 26 +++--- src/cortex-tui/src/widgets/action_bar.rs | 2 +- .../src/widgets/approval_overlay.rs | 8 +- src/cortex-tui/src/widgets/autocomplete.rs | 6 +- .../src/widgets/backtrack_overlay.rs | 12 ++- .../src/widgets/command_palette/widget.rs | 4 +- src/cortex-tui/src/widgets/form/colors.rs | 2 +- src/cortex-tui/src/widgets/form/modal.rs | 9 ++- .../src/widgets/help_browser/render.rs | 7 +- src/cortex-tui/src/widgets/key_hints.rs | 2 +- src/cortex-tui/src/widgets/mention_popup.rs | 12 ++- src/cortex-tui/src/widgets/model_picker.rs | 2 +- .../src/widgets/scrollable_dropdown.rs | 6 +- src/cortex-tui/src/widgets/selection_list.rs | 2 +- src/cortex-tui/src/widgets/settings_modal.rs | 8 +- 42 files changed, 327 insertions(+), 246 deletions(-) diff --git a/docs/customization/themes.md b/docs/customization/themes.md index e1eda844..fbb6bddd 100644 --- a/docs/customization/themes.md +++ b/docs/customization/themes.md @@ -5,7 +5,7 @@ text, tool rows, borders, status and accents. | Theme id | Name | Description | |----------|------|-------------| -| `dark` | Dark | The default. Gray chrome on the host terminal background; the Cortex violet marks the focused selection. | +| `dark` | Dark | The default. Gray chrome on the host terminal background; the Cortex banner green marks the focused selection. | | `light` | Light | Light background with dark text. | | `ocean_dark` | Ocean Dark | Deep blue and cyan. Also accepted as `ocean`. | | `monokai` | Monokai | Classic code-editor colours. | @@ -38,17 +38,17 @@ For reference — these are the colours the demo recording on the | Role | Colour | |------|--------| -| Selection accent (`>` caret + focused label; focused composer `>`) | `#A78BFA` | +| Selection accent (`>` caret + focused label; focused composer `>`) | `#1F4945` | | Background | terminal default (`Color::Reset` — never painted) | | Charcoal panel (tips / info) | `#141414` | | Past user turn bar | `#1C1C1C` | -| Selection bar | `#262626` (violet caret + label, dim description, never inverted — never a violet wash) | +| Selection bar | `#262626` (focused green glyphs have a `#F5F5F5` backing for contrast; descriptions stay dim) | | Hairline (above / below the prompt, around search fields) | `#3A3A3A` | | Focused border | `#525252` (gray — the accent never outlines a box) | | Text | `#F5F5F5` | | Dim text (placeholders, hints, descriptions) | `#6B7280` | | Muted text | `#4B5563` | -| Success `✓` and diff additions `+N` | `#4ADE80` (the only green) | +| Success `✓` and diff additions `+N` | `#4ADE80` (bright semantic green) | | Warning (`warn` in diagnostics) | `#FFC857` | | Error (`error`, `× Stopped`, quota, failed MCP `x`) | `#F87171` | | Thinking status | `#C9A95C` (the only gold) | @@ -56,6 +56,8 @@ For reference — these are the colours the demo recording on the The footer is gray: the model on the left (`Cortex Mini 1 · Agent · 92% context`), one shortcut hint on the right (`shift+tab to cycle modes`). +The accent is the dominant background pixel of the supplied banner: RGB `(31, 73, 69)` (`#1F4945`). Focused green glyphs use a near-white backing rather than disappearing on black. Success and diff colours are unchanged. + ## Related display settings ```toml diff --git a/docs/media/tui-lock-v2/SPEC.md b/docs/media/tui-lock-v2/SPEC.md index 3b599250..7f72b2be 100644 --- a/docs/media/tui-lock-v2/SPEC.md +++ b/docs/media/tui-lock-v2/SPEC.md @@ -34,7 +34,7 @@ Brand rule: the only product name on any pixel or in this document is **Cortex** "bar_user": "#1C1C1C", "bar_hover": "#1A1A1A", "bar_selected": "#262626", - "accent": "#A78BFA", + "accent": "#1F4945", "success": "#4ADE80", "warning": "#FFC857", "error": "#F87171" @@ -44,9 +44,9 @@ Brand rule: the only product name on any pixel or in this document is **Cortex** "success": "ONLY `✓` and diff `+N` / `+` lines", "warning": "ONLY warnings: diagnostics `warn`, context counter ≥ 90 %", "error": "ONLY errors: `×` titles, diagnostics `error`, diff `−N` / `-` lines, failed exit codes, exhausted quota bar", - "hairline": "always gray — never violet, never coloured", + "hairline": "always gray — never banner green, never coloured", "bg": "inky #000 — the TUI paints the whole alternate screen; no wash, no frame", - "retired": ["thinking gold #C9A95C", "mint #00F5D4", "cyan #7DD3FC", "violet wash #221A38 as a default bar"] + "retired": ["thinking gold #C9A95C", "mint #00F5D4", "cyan #7DD3FC", "retired violet wash #221A38 as a default bar"] }, "font": { "family": "IBM Plex Mono", @@ -102,7 +102,7 @@ Header-left is intentionally empty on launch. It never paints a shell echo ## 3. Component recipes Legend for recipes: `T` text `#F5F5F5` · `D` dim `#6B7280` · `M` muted `#4B5563` · -`H` hairline `#3A3A3A` · `V` accent `#A78BFA` · `G` success · `A` warning · `R` error. +`H` hairline `#3A3A3A` · `V` accent `#1F4945` · `G` success · `A` warning · `R` error. ### 3.1 Header @@ -118,7 +118,7 @@ Legend for recipes: `T` text `#F5F5F5` · `D` dim `#6B7280` · `M` muted `#4B556 ``` > hey 12:49 AM ← bar #1C1C1C, cols 1..118 ``` -- `>` and text `T` (never violet — history is not focus). Timestamp `D`, right-aligned inside the bar. +- `>` and text `T` (never banner green — history is not focus). Timestamp `D`, right-aligned inside the bar. - 12-hour clock `hh:mm AM`. Hidden when `Show timestamps = off`, in compact mode, and at < 80 cols. - One blank row after the bar. `&` and `!` and `/btw` prefixes stay as typed. @@ -219,7 +219,7 @@ Stacked directly above the composer's top hairline, newest/first row on top, ful > /model Choose the model for this session ← focused: bar #262626, `>` V, name T, desc D /mode Switch between Agent, Plan and Ask ← plain /permissions Set the approval policy for edits and commands - /plan Draft a plan before writing any code ← hover: bar #1A1A1A, no violet + /plan Draft a plan before writing any code ← hover: bar #1A1A1A, no banner green … 87 more — keep typing to filter ← trailer M ``` - Marker col 3, name col 5, description at `5 + name_w` where `name_w ≥ longest name + 2`. @@ -280,7 +280,7 @@ Composer loses focus (`>` `D`, placeholder `Choose an option above`). Hover row "Git": [["Co-author","on"],["Auto commit","off"],["Sign commits","off"]], "Cloud": [["Cloud sync","off"],["Auto save","on"],["Session history","on"]], "Privacy": [["Telemetry","off"],["Analytics","off"]], - "Theme submenu": [["Cortex Night","Default inky chrome · violet on focus only","current"],["Cortex Day","Light chrome for bright rooms"],["Ocean Dark","Deep blue and cyan accents"],["Monokai","Classic code-editor colors"]] + "Theme submenu": [["Cortex Night","Default inky chrome · banner green on focus only","current"],["Cortex Day","Light chrome for bright rooms"],["Ocean Dark","Deep blue and cyan accents"],["Monokai","Classic code-editor colors"]] } ``` @@ -326,17 +326,17 @@ Narrow: no bars, `used / total pct%`. "row (settings, slash, model, effort, radios, pickers)": { "idle": { "bar": "none", "marker": "none / ▸ dim", "label": "text" }, "hover": { "bar": "#1A1A1A", "marker": "unchanged", "label": "text", "accent": false }, - "focus": { "bar": "#262626", "marker": "> or ▸ in #A78BFA", "label": "text (Bold in settings)", "accent": true }, + "focus": { "bar": "#262626", "marker": "> or ▸ in #1F4945", "label": "text (Bold in settings)", "accent": true }, "focus+hover": "focus wins" }, "composer": { "idle": { "hairline": "#3A3A3A", "caret_sigil": "#6B7280" }, "hover": { "hairline": "#525252", "caret_sigil": "unchanged" }, - "focus": { "hairline": "#3A3A3A", "caret_sigil": "#A78BFA", "caret": "2 px bar, blinking" } + "focus": { "hairline": "#3A3A3A", "caret_sigil": "#1F4945", "caret": "2 px bar, blinking" } }, "footer chunk / banner button / chip": { "hover": { "underline": true, "text": "#F5F5F5", "bar": "#1A1A1A" }, - "focus": { "text": "#A78BFA", "bar": "#262626" } + "focus": { "text": "#1F4945", "bar": "#262626" } }, "typed slash match": { "accent": true, "note": "matched characters in the palette and the command token in the composer" } } @@ -474,3 +474,5 @@ Narrow (40×12) set: `welcome-cortex`, `welcome-agent`, `first-run-tips`, `sessi approval / plan / clear / question prompts render inline (§3.10) using the same recipe. - Settings hub (7 rows) is replaced by the categorised modal (§3.11); `/settings` and F2 open it. - Every changed surface needs a unit test plus a headless snapshot; the `txt/` grids in this pack are the expected buffers. + +Banner accent update: focused `#1F4945` glyphs use `#F5F5F5` backing for readable contrast. The surrounding selection row remains `#262626`; semantic success and diff green stay `#4ADE80`. Existing image exports predate this palette update; the renderer uses the updated palette. diff --git a/docs/media/tui-lock-v2/index.md b/docs/media/tui-lock-v2/index.md index 7e0987b4..8a4bd2e1 100644 --- a/docs/media/tui-lock-v2/index.md +++ b/docs/media/tui-lock-v2/index.md @@ -27,7 +27,7 @@ Regenerate: `python3 tools/render_lock_v2.py --index` (fetches IBM Plex Mono on | `session-worked` | `Worked for Xs` after a reply | [120x40](120x40/session-worked.png) | — | | `session-optin` | `Help improve Cortex` banner — Opt out | Opt in | [120x40](120x40/session-optin.png) | [40x12](40x12/session-optin.png) | | `session-optin-hover` | Banner with the mouse over `[Opt in]` | [120x40](120x40/session-optin-hover.png) | — | -| `composer-empty` | Empty composer — caret before the placeholder, violet `>` | [120x40](120x40/composer-empty.png) | [40x12](40x12/composer-empty.png) | +| `composer-empty` | Empty composer — caret before the placeholder, banner green `>` | [120x40](120x40/composer-empty.png) | [40x12](40x12/composer-empty.png) | | `composer-typing` | Mid-type, caret on | [120x40](120x40/composer-typing.png) | [40x12](40x12/composer-typing.png) | | `composer-typing-blink` | Mid-type, caret off (blink phase) | [120x40](120x40/composer-typing-blink.png) | — | | `composer-hover` | Mouse over the composer — hairline lifts to #525252 | [120x40](120x40/composer-hover.png) | [40x12](40x12/composer-hover.png) | @@ -43,7 +43,7 @@ Regenerate: `python3 tools/render_lock_v2.py --index` (fetches IBM Plex Mono on | Board | State | Wide | Narrow | |---|---|---|---| | `slash-palette` | `/` palette — focused row + hover row + `… more` trailer | [120x40](120x40/slash-palette.png) | [40x12](40x12/slash-palette.png) | -| `slash-model-typed` | `/mod` typed — violet matched chars, ghost completion | [120x40](120x40/slash-model-typed.png) | [40x12](40x12/slash-model-typed.png) | +| `slash-model-typed` | `/mod` typed — banner green matched chars, ghost completion | [120x40](120x40/slash-model-typed.png) | [40x12](40x12/slash-model-typed.png) | | `model-list` | `/model` — Cortex Mini 1 · Cortex 1 · Cortex Max 1 | [120x40](120x40/model-list.png) | [40x12](40x12/model-list.png) | | `model-list-hover` | Model list with mouse over row 3 | [120x40](120x40/model-list-hover.png) | — | | `model-effort-high` | Effort radios — High focused | [120x40](120x40/model-effort-high.png) | [40x12](40x12/model-effort-high.png) | @@ -58,7 +58,7 @@ Regenerate: `python3 tools/render_lock_v2.py --index` (fetches IBM Plex Mono on | `settings-appearance` | Settings modal — Appearance, Compact mode focused | [120x40](120x40/settings-appearance.png) | [40x12](40x12/settings-appearance.png) | | `settings-mouse` | Settings scrolled to Mouse / Behavior | [120x40](120x40/settings-mouse.png) | [40x12](40x12/settings-mouse.png) | | `settings-row-hover` | Keyboard focus on Compact mode, mouse over Show timestamps | [120x40](120x40/settings-row-hover.png) | [40x12](40x12/settings-row-hover.png) | -| `settings-search` | `/ scro` search — filtered rows, violet match | [120x40](120x40/settings-search.png) | — | +| `settings-search` | `/ scro` search — filtered rows, banner green match | [120x40](120x40/settings-search.png) | — | | `settings-theme-submenu` | Theme submenu — Cortex Night / Cortex Day / Ocean Dark / Monokai | [120x40](120x40/settings-theme-submenu.png) | [40x12](40x12/settings-theme-submenu.png) | ## E. Modes / tools / errors @@ -99,7 +99,7 @@ Regenerate: `python3 tools/render_lock_v2.py --index` (fetches IBM Plex Mono on | `clear-confirm` | `/clear` confirm radios | [120x40](120x40/clear-confirm.png) | — | | `plan-confirm` | `Implement this plan?` radios | [120x40](120x40/plan-confirm.png) | — | | `queue` | Follow-up queue while a step runs | [120x40](120x40/queue.png) | — | -| `files-picker` | `@` file picker — violet matched chars, hover row | [120x40](120x40/files-picker.png) | — | +| `files-picker` | `@` file picker — banner green matched chars, hover row | [120x40](120x40/files-picker.png) | — | | `jobs` | `/jobs` — cloud agent, subagent, queued | [120x40](120x40/jobs.png) | — | | `skills` | `/skills` — search field + skill rows | [120x40](120x40/skills.png) | — | | `todos` | Working 2/5 checklist — ✓ done · › current · ○ pending | [120x40](120x40/todos.png) | — | diff --git a/docs/media/tui-lock-v2/tools/boards.py b/docs/media/tui-lock-v2/tools/boards.py index 12f7efd7..528233a9 100644 --- a/docs/media/tui-lock-v2/tools/boards.py +++ b/docs/media/tui-lock-v2/tools/boards.py @@ -13,8 +13,8 @@ (left), model chip in the bottom border (right) * row H-1 footer — shortcut strip ``Key:label | Key:label`` -Focus vs hover: keyboard focus = ``#262626`` bar + violet marker/caret; -mouse hover = ``#161616`` bar (or ``#525252`` hairline) and *no* violet. +Focus vs hover: keyboard focus = ``#262626`` bar + banner green marker/caret; +mouse hover = ``#161616`` bar (or ``#525252`` hairline) and *no* banner green. """ from __future__ import annotations @@ -45,7 +45,7 @@ S_OK, S_WARN, TEXT, - VIOLET, + ACCENT, Cell, Screen, St, @@ -239,9 +239,9 @@ def optin_banner(s: Screen, c: Ctx, y: int, hover: str | None = None, focus: str if hover == "out": out_st = St(fg=TEXT, u=True, bg=BAR_HOV) if focus == "in": - in_st = St(fg=VIOLET, b=True, bg=BAR_SEL) + in_st = St(fg=ACCENT, b=True, bg=BAR_SEL) if focus == "out": - out_st = St(fg=VIOLET, b=True, bg=BAR_SEL) + out_st = St(fg=ACCENT, b=True, bg=BAR_SEL) s.right_spans(0 + y, [("[Opt out]", out_st), (" ", S), ("[Opt in]", in_st)], c.x1) if c.narrow: s.put(c.x0, y + 1, "Off by default · anytime in /settings", S_DIM) @@ -369,7 +369,7 @@ def command(self, cmd: str): self.y += 1 def options(self, opts, focused: int = 0, hover: int | None = None, numbered: bool = True): - """Inline numbered radios: focused = `#262626` bar + violet `>`; hover = `#1A1A1A` bar.""" + """Inline numbered radios: focused = `#262626` bar + banner green `>`; hover = `#1A1A1A` bar.""" for i, label in enumerate(opts): if not self.ok(): return @@ -378,7 +378,7 @@ def options(self, opts, focused: int = 0, hover: int | None = None, numbered: bo bg = BAR_SEL if is_focus else (BAR_HOV if is_hover else None) if bg: self.s.fill(self.x0, self.y, self.x1, bg) - self.s.put(self.tx, self.y, ">" if is_focus else " ", St(fg=VIOLET, bg=bg) if is_focus else S) + self.s.put(self.tx, self.y, ">" if is_focus else " ", St(fg=ACCENT, bg=bg) if is_focus else S) x = self.tx + 2 if numbered: x = self.s.put(x, self.y, f"{i + 1} ", St(fg=DIM, bg=bg) if bg else S_DIM) @@ -401,7 +401,7 @@ def menu(s: Screen, c: Ctx, y_bottom: int, rows, focused: int = 0, hover: int | bg = BAR_SEL if is_focus else (BAR_HOV if is_hover else None) if bg: s.fill(c.x0, y, c.x1, bg) - s.put(c.x0 + 2, y, ">" if is_focus else " ", St(fg=VIOLET, bg=bg) if is_focus else S) + s.put(c.x0 + 2, y, ">" if is_focus else " ", St(fg=ACCENT, bg=bg) if is_focus else S) x = c.x0 + 4 for text, st in name_parts: x = s.put(x, y, text, st(bg=bg) if bg else st, max_x=c.x1 - 1) @@ -545,7 +545,7 @@ def search_field(s: Screen, x: int, y: int, w: int, typed: str = "", placeholder ] THEMES = [ - ("Cortex Night", "Default inky chrome · violet on focus only", True), + ("Cortex Night", "Default inky chrome · banner green on focus only", True), ("Cortex Day", "Light chrome for bright rooms", False), ("Ocean Dark", "Deep blue and cyan accents", False), ("Monokai", "Classic code-editor colors", False), @@ -984,7 +984,7 @@ def paint_settings(s: Screen, c: Ctx, focus_label: str, hover_label: str | None bg = BAR_SEL if is_focus else (BAR_HOV if is_hover else None) if bg: s.fill(x + 1, row_y, x + w - 1, bg) - marker_st = St(fg=VIOLET, bg=bg) if is_focus else St(fg=DIM, bg=bg) if bg else S_DIM + marker_st = St(fg=ACCENT, bg=bg) if is_focus else St(fg=DIM, bg=bg) if bg else S_DIM s.put(ix0 + 1, row_y, "▸", marker_st) lab_st = St(fg=TEXT, b=is_focus, bg=bg) if bg else S val_st = St(fg=TEXT if value != "off" else DIM, bg=bg) if bg else (S if value != "off" else S_DIM) @@ -995,7 +995,7 @@ def paint_settings(s: Screen, c: Ctx, focus_label: str, hover_label: str | None if search and search.lower() in label.lower(): k = label.lower().find(search.lower()) pre, mid, post = label[:k], label[k : k + len(search)], label[k + len(search) :] - s.spans(ix0 + 3, row_y, [(pre, lab_st), (mid, St(fg=VIOLET, b=is_focus, bg=bg)), (post, lab_st)]) + s.spans(ix0 + 3, row_y, [(pre, lab_st), (mid, St(fg=ACCENT, b=is_focus, bg=bg)), (post, lab_st)]) else: s.put(ix0 + 3, row_y, label, lab_st) if sub: @@ -1066,7 +1066,7 @@ def board_settings_theme_submenu(s, c): if bg: s.fill(x + 1, row_y, x + w - 1, bg) s.put(ix0 + 1, row_y, "●" if current else "○", St(fg=TEXT if current else DIM, bg=bg) if bg else (S if current else S_DIM)) - s.put(ix0 + 3, row_y, name, St(fg=VIOLET if is_focus else TEXT, b=is_focus, bg=bg) if bg else S) + s.put(ix0 + 3, row_y, name, St(fg=ACCENT if is_focus else TEXT, b=is_focus, bg=bg) if bg else S) if not c.narrow: s.put(ix0 + 18, row_y, desc, St(fg=DIM, bg=bg) if bg else S_DIM) if current: @@ -1076,7 +1076,7 @@ def board_settings_theme_submenu(s, c): legend(s, y + h - 3, x + 1, x + w - 1, [("Enter", "select"), ("←", "back")]) legend(s, y + h - 2, x + 1, x + w - 1, [("F2/Esc", "close")]) else: - s.center_spans(y + h - 4, [("Themes never change the accent rule: violet on focus only.", S_MUTED)], x + 1, x + w - 1) + s.center_spans(y + h - 4, [("Themes never change the accent rule: banner green on focus only.", S_MUTED)], x + 1, x + w - 1) legend(s, y + h - 3, x + 1, x + w - 1, [("↑/↓/j/k", "nav"), ("Enter", "select"), ("←", "back"), ("d", "reset")]) legend(s, y + h - 2, x + 1, x + w - 1, [("F2/Esc", "close")]) @@ -1480,7 +1480,7 @@ def board_login(s, c): yy = y + 4 + k if k == 0: s.fill(c.x0, yy, c.x1, BAR_SEL) - s.put(c.x0 + 1, yy, ">", St(fg=VIOLET, bg=BAR_SEL)) + s.put(c.x0 + 1, yy, ">", St(fg=ACCENT, bg=BAR_SEL)) x = s.put(c.x0 + 3, yy, label, St(fg=TEXT, bg=BAR_SEL)) if not c.narrow: s.put(c.x0 + 30, yy, desc, St(fg=DIM, bg=BAR_SEL)) @@ -1765,7 +1765,7 @@ def board_btw(s, c): ("session-worked", board_session_worked, False, "B", "`Worked for Xs` after a reply"), ("session-optin", board_session_optin, True, "B", "`Help improve Cortex` banner — Opt out | Opt in"), ("session-optin-hover", board_session_optin_hover, False, "B", "Banner with the mouse over `[Opt in]`"), - ("composer-empty", board_composer_empty, True, "B", "Empty composer — caret before the placeholder, violet `>`"), + ("composer-empty", board_composer_empty, True, "B", "Empty composer — caret before the placeholder, banner green `>`"), ("composer-typing", board_composer_typing, True, "B", "Mid-type, caret on"), ("composer-typing-blink", board_composer_typing_blink, False, "B", "Mid-type, caret off (blink phase)"), ("composer-hover", board_composer_hover, True, "B", "Mouse over the composer — hairline lifts to #525252"), @@ -1777,7 +1777,7 @@ def board_btw(s, c): ("compact-chat", board_compact_chat, True, "B", "Compact mode — edge-to-edge bars, no timestamps"), # C. Slash + model ("slash-palette", board_slash_palette, True, "C", "`/` palette — focused row + hover row + `… more` trailer"), - ("slash-model-typed", board_slash_model_typed, True, "C", "`/mod` typed — violet matched chars, ghost completion"), + ("slash-model-typed", board_slash_model_typed, True, "C", "`/mod` typed — banner green matched chars, ghost completion"), ("model-list", board_model_list, True, "C", "`/model` — Cortex Mini 1 · Cortex 1 · Cortex Max 1"), ("model-list-hover", board_model_list_hover, False, "C", "Model list with mouse over row 3"), ("model-effort-high", board_model_effort_high, True, "C", "Effort radios — High focused"), @@ -1788,7 +1788,7 @@ def board_btw(s, c): ("settings-appearance", board_settings_appearance, True, "D", "Settings modal — Appearance, Compact mode focused"), ("settings-mouse", board_settings_mouse, True, "D", "Settings scrolled to Mouse / Behavior"), ("settings-row-hover", board_settings_row_hover, True, "D", "Keyboard focus on Compact mode, mouse over Show timestamps"), - ("settings-search", board_settings_search, False, "D", "`/ scro` search — filtered rows, violet match"), + ("settings-search", board_settings_search, False, "D", "`/ scro` search — filtered rows, banner green match"), ("settings-theme-submenu", board_settings_theme_submenu, True, "D", "Theme submenu — Cortex Night / Cortex Day / Ocean Dark / Monokai"), # E. Modes / tools / errors ("mode-agent", board_mode_agent, False, "E", "Agent mode — dim chip in the composer border"), @@ -1825,7 +1825,7 @@ def board_btw(s, c): ("clear-confirm", board_clear_confirm, False, "E", "`/clear` confirm radios"), ("plan-confirm", board_plan_confirm, False, "E", "`Implement this plan?` radios"), ("queue", board_queue, False, "E", "Follow-up queue while a step runs"), - ("files-picker", board_files_picker, False, "E", "`@` file picker — violet matched chars, hover row"), + ("files-picker", board_files_picker, False, "E", "`@` file picker — banner green matched chars, hover row"), ("jobs", board_jobs, False, "E", "`/jobs` — cloud agent, subagent, queued"), ("skills", board_skills, False, "E", "`/skills` — search field + skill rows"), ("todos", board_todos, False, "E", "Working 2/5 checklist — ✓ done · › current · ○ pending"), diff --git a/docs/media/tui-lock-v2/tools/render_lock_v2.py b/docs/media/tui-lock-v2/tools/render_lock_v2.py index 0191d13e..734f7a47 100644 --- a/docs/media/tui-lock-v2/tools/render_lock_v2.py +++ b/docs/media/tui-lock-v2/tools/render_lock_v2.py @@ -43,7 +43,7 @@ "bar_user": "#1C1C1C", "bar_hover": "#1A1A1A", "bar_selected": "#262626", - "accent": "#A78BFA", + "accent": "#1F4945", "success": "#4ADE80", "warning": "#FFC857", "error": "#F87171", @@ -59,7 +59,7 @@ BAR_USER = TOKENS["bar_user"] BAR_HOV = TOKENS["bar_hover"] BAR_SEL = TOKENS["bar_selected"] -VIOLET = TOKENS["accent"] +ACCENT = TOKENS["accent"] GREEN = TOKENS["success"] AMBER = TOKENS["warning"] RED = TOKENS["error"] @@ -91,7 +91,7 @@ def __call__(self, **kw) -> "St": S_DIM = St(fg=DIM) S_MUTED = St(fg=MUTED) S_HAIR = St(fg=HAIR) -S_ACC = St(fg=VIOLET) +S_ACC = St(fg=ACCENT) S_BOLD = St(b=True) S_OK = St(fg=GREEN) S_WARN = St(fg=AMBER) @@ -136,6 +136,8 @@ def put(self, x: int, y: int, s: str, st: St = S, max_x: int | None = None) -> i c.fg = st.fg if st.bg is not None: c.bg = st.bg + if st.fg == ACCENT: + c.bg = TEXT c.b, c.i, c.u = st.b, st.i, st.u x += 1 return x diff --git a/docs/media/tui-lock-v2/txt/120x40/settings-theme-submenu.txt b/docs/media/tui-lock-v2/txt/120x40/settings-theme-submenu.txt index cf0124d6..f58b58b5 100644 --- a/docs/media/tui-lock-v2/txt/120x40/settings-theme-submenu.txt +++ b/docs/media/tui-lock-v2/txt/120x40/settings-theme-submenu.txt @@ -3,7 +3,7 @@ > hey │ Appearance › Theme │ 12:49 AM │ ──────────────────────────────────────────────────────────────────────────────────────────── │ ♦ Thought│ │ - │ ● Cortex Night Default inky chrome · violet on focus only current │ + │ ● Cortex Night Default inky chrome · banner green on focus only current │ Hey — wha│ ○ Cortex Day Light chrome for bright rooms │ 12:49 AM │ ○ Ocean Dark Deep blue and cyan accents │ Worked fo│ ○ Monokai Classic code-editor colors │ @@ -33,7 +33,7 @@ │ │ │ │ │ │ - ╭─ Agent ──│ Themes never change the accent rule: violet on focus only. │──────────╮ + ╭─ Agent ──│ Themes never change the accent rule: banner green on focus only. │──────────╮ │ > Plan, s│ ↑/↓/j/k nav | Enter select | ← back | d reset │ │ ╰──────────│ F2/Esc close │(medium) ─╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/docs/media/tui-lock/README.md b/docs/media/tui-lock/README.md index ebb4f088..ed68842f 100644 --- a/docs/media/tui-lock/README.md +++ b/docs/media/tui-lock/README.md @@ -21,7 +21,7 @@ Each scene is captured at **40×12** (narrow) and **120×40** (wide), twice: Chrome rules: structure is gray — `#3A3A3A` hairlines above and below the `> ` composer and around search fields, `#141414` charcoal panels for tips, `#1C1C1C` bars behind past user turns, `#6B7280` secondary copy, white -primary copy. The one accent is the Cortex violet `#A78BFA`, on the focused +primary copy. The one accent is the Cortex banner green `#1F4945`, on the focused selection only (the `>` caret and the selected label on the `#262626` gray bar, never inverted, never a `#221A38` wash); unselected rows lead with a dim middot. Green `#4ADE80` appears only on `✓` diff --git a/src/cortex-core/src/markdown/languages.rs b/src/cortex-core/src/markdown/languages.rs index f9bbca67..ec0ce0d5 100644 --- a/src/cortex-core/src/markdown/languages.rs +++ b/src/cortex-core/src/markdown/languages.rs @@ -110,7 +110,7 @@ pub fn normalize_language_name(lang: &str) -> Option<&'static str> { /// The gray-chrome syntax theme: keywords bold white, strings and comments /// dim, everything else the plain white — a fence never introduces a colour -/// of its own, the violet stays on the focused selection. +/// of its own, the banner green stays on the focused selection. pub fn monochrome_theme() -> Theme { let white = Color::from_rgb_u8(0xFF, 0xFF, 0xFF); let dim = Color::from_rgb_u8(0x6B, 0x72, 0x80); diff --git a/src/cortex-core/src/markdown/theme.rs b/src/cortex-core/src/markdown/theme.rs index 1dc8ad19..6ce35f59 100644 --- a/src/cortex-core/src/markdown/theme.rs +++ b/src/cortex-core/src/markdown/theme.rs @@ -585,7 +585,7 @@ impl MarkdownTheme { impl Default for MarkdownTheme { /// The gray chrome: white headings and emphasis, dim bullets and rules, /// hairline `#3A3A3A` table / fence / quote borders, green only on the - /// checked `✓`. No accent colour anywhere in a reply — the violet belongs + /// checked `✓`. No accent colour anywhere in a reply — the banner green belongs /// to the focused selection alone. fn default() -> Self { Self { diff --git a/src/cortex-core/src/style.rs b/src/cortex-core/src/style.rs index 272e6b80..01d5f851 100644 --- a/src/cortex-core/src/style.rs +++ b/src/cortex-core/src/style.rs @@ -1,24 +1,25 @@ //! Cortex Theme — inky chrome on a painted `#000000` alternate screen. //! //! Structure comes from gray — hairlines, filled charcoal panels, dim -//! secondary copy, white primary copy. The one accent is the Cortex violet -//! `#A78BFA`, reserved for keyboard focus: the `>` caret, the focused row -//! marker, and typed slash-command matches. Green exists only for `✓` +//! secondary copy, white primary copy. The one accent is the Cortex banner green +//! `#1F4945`, reserved for keyboard focus: the `>` caret, the focused row +//! marker, and typed slash-command matches. Bright green exists only for `✓` //! success and `+` diff additions; red and amber only on diagnostics. -//! Hover bars are `#1A1A1A` with no violet. Gold, mint, cyan, and the -//! `#221A38` violet wash are never painted. +//! Hover bars are `#1A1A1A` with no banner green. Gold, mint, cyan, and the +//! `#221A38` retired violet wash are never painted. use ratatui::style::{Color, Modifier, Style}; // ============================================================ -// ACCENT - one violet, focused selection only +// ACCENT - one banner green, focused selection only // ============================================================ -/// Primary accent - Cortex violet for the focused selection (`>` caret + label) -pub const ACCENT: Color = Color::Rgb(167, 139, 250); // #A78BFA +/// Banner background, sampled from the dominant JPEG pixel (31, 73, 69). +/// Pair with `TEXT` behind focused glyphs: this dark green is illegible on black. +pub const ACCENT: Color = Color::Rgb(31, 73, 69); // #1F4945 /// Legacy brand slot. Widgets that once painted titles, icons and cursors in -/// the brand colour now get a light gray, so the violet stays on the focused +/// the brand colour now get a light gray, so the banner green stays on the focused /// selection only — use `ACCENT` for that. pub const CYAN_PRIMARY: Color = SKY_BLUE; @@ -41,7 +42,7 @@ pub const TEAL: Color = Color::Rgb(75, 85, 99); // #4B5563 /// Inky background — painted on the whole alternate screen (`#000000`). pub const VOID: Color = Color::Rgb(0, 0, 0); -/// Hover bar — `#1A1A1A`, no violet. Distinct from the user-turn bar. +/// Hover bar — `#1A1A1A`, no banner green. Distinct from the user-turn bar. pub const BAR_HOVER: Color = Color::Rgb(26, 26, 26); // #1A1A1A /// Filled charcoal panel for tips / info blocks @@ -104,8 +105,8 @@ pub const INFO: Color = DEEP_CYAN; // #9CA3AF pub const HIGHLIGHT: Color = ELECTRIC_BLUE; // #E5E7EB /// Focused-row background — dark gray `#262626`. The caret / `▸` on it -/// are `#A78BFA`; the label stays white (bold in Settings). Never invert -/// onto the accent, never a violet wash. +/// use `#1F4945` on a near-white backing; other text stays white. Never invert +/// onto the accent, never a banner green wash. pub const SELECTION_BG: Color = SURFACE_2; // #262626 // ============================================================ @@ -210,15 +211,15 @@ impl ThemeColors { } } - /// Dark theme (default) - gray chrome, violet selection on dark background + /// Dark theme (default) - gray chrome, banner green selection on dark background pub fn dark() -> Self { Self::ocean_cyan() } - /// Light theme - darker violet selection, gray chrome on a light background + /// Light theme - darker banner green selection, gray chrome on a light background pub fn light() -> Self { Self { - primary: Color::Rgb(124, 58, 237), + primary: ACCENT, secondary: Color::Rgb(82, 82, 91), accent: Color::Rgb(39, 39, 42), background: Color::Rgb(255, 255, 255), @@ -336,10 +337,10 @@ impl CortexStyle { .add_modifier(Modifier::BOLD) } - /// Selected item style: violet marker on the dark gray bar — never inverted + /// Selected glyph: banner green on near-white, within the dark gray row. #[inline] pub fn selected() -> Style { - Style::default().fg(ACCENT).bg(SELECTION_BG) + Style::default().fg(ACCENT).bg(TEXT) } /// Error style: lock red text for error messages @@ -434,7 +435,7 @@ impl CortexStyle { Style::default().fg(BORDER) } - /// Focused border style: a lighter gray — violet never outlines a box + /// Focused border style: a lighter gray — banner green never outlines a box #[inline] pub fn border_focused() -> Style { Style::default().fg(BORDER_FOCUS) @@ -481,7 +482,7 @@ impl CortexStyle { pub fn brain_cyan(brightness: f32) -> Style { let b = brightness.clamp(0.0, 1.0); // CYAN_PRIMARY is RGB(0, 255, 255) - // Scale the brightness while keeping the violet hue + // Scale the brightness while keeping the banner green hue let r = (0.0 * b) as u8; let g = (255.0 * b) as u8; let bl = (255.0 * b) as u8; @@ -592,7 +593,7 @@ mod tests { #[test] fn test_brain_cyan_brightness() { - // Full brightness should be violet + // Full brightness should be banner green let style_full = CortexStyle::brain_cyan(1.0); assert_eq!(style_full.fg, Some(Color::Rgb(0, 255, 255))); @@ -644,12 +645,32 @@ mod tests { assert_eq!(BORDER_HIGHLIGHT, BORDER_FOCUS); } + #[test] + fn banner_accent_has_accessible_focus_contrast() { + fn luminance(color: Color) -> f64 { + let Color::Rgb(r, g, b) = color else { + panic!("expected RGB") + }; + let linear = |v: u8| { + let v = f64::from(v) / 255.0; + if v <= 0.04045 { + v / 12.92 + } else { + ((v + 0.055) / 1.055).powf(2.4) + } + }; + 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b) + } + assert!((luminance(TEXT) + 0.05) / (luminance(ACCENT) + 0.05) >= 4.5); + assert_eq!(ThemeColors::light().primary, ACCENT); + } + #[test] fn gray_chrome_palette_is_locked() { - // One accent: the Cortex violet, for the focused selection only. - assert_eq!(ACCENT, Color::Rgb(0xA7, 0x8B, 0xFA)); + // One accent: the Cortex banner green, for the focused selection only. + assert_eq!(ACCENT, Color::Rgb(0x1F, 0x49, 0x45)); assert_eq!(CortexStyle::selected().fg, Some(ACCENT)); - assert_eq!(CortexStyle::selected().bg, Some(SELECTION_BG)); + assert_eq!(CortexStyle::selected().bg, Some(TEXT)); // Green covers `✓` and `+diff` — the same green. assert_eq!(SUCCESS, DIFF_ADD); assert_eq!(SUCCESS, Color::Rgb(0x4A, 0xDE, 0x80)); @@ -672,7 +693,7 @@ mod tests { assert!(r == g && g == b, "{gray:?} is not neutral"); } assert_eq!(SELECTION_BG, Color::Rgb(0x26, 0x26, 0x26)); - // The violet lives in `ACCENT` only. Mint is banned. + // The banner green lives in `ACCENT` only. Mint is banned. for color in [ SUCCESS, BORDER_FOCUS, @@ -683,7 +704,7 @@ mod tests { DEEP_CYAN, TEAL, ] { - assert_ne!(color, ACCENT, "violet leaked off the accent"); + assert_ne!(color, ACCENT, "banner green leaked off the accent"); assert_ne!(color, Color::Rgb(0x00, 0xF5, 0xD4), "mint leaked"); assert_ne!(color, Color::Rgb(0x7D, 0xD3, 0xFC), "cyan leaked"); } diff --git a/src/cortex-tui-capture/src/demo/mod.rs b/src/cortex-tui-capture/src/demo/mod.rs index e7571719..062e56dd 100644 --- a/src/cortex-tui-capture/src/demo/mod.rs +++ b/src/cortex-tui-capture/src/demo/mod.rs @@ -1,7 +1,7 @@ //! Frame I/O for the README hero recording. //! //! Scene painting lives in `cortex-tui::readme_hero` so the GIF is the signed -//! lock TUI (dual hairline, violet `>`, splash → typing → working). This +//! lock TUI (dual hairline, banner green `>`, splash → typing → working). This //! module owns the manifest format `scripts/ansi-frames-to-gif.py` consumes. use std::path::{Path, PathBuf}; diff --git a/src/cortex-tui/src/cards/help.rs b/src/cortex-tui/src/cards/help.rs index a433146f..60a297b2 100644 --- a/src/cortex-tui/src/cards/help.rs +++ b/src/cortex-tui/src/cards/help.rs @@ -432,7 +432,7 @@ impl HelpCard { // Empty line, nothing to render } ContentLine::Header(title) => { - // Section header in violet + // Section header in banner green buf.set_string(area.x + 1, y, title, Style::default().fg(CYAN_PRIMARY)); } ContentLine::Item { key, description } => { diff --git a/src/cortex-tui/src/interactive/renderer.rs b/src/cortex-tui/src/interactive/renderer.rs index cc453ceb..2e8edfd8 100644 --- a/src/cortex-tui/src/interactive/renderer.rs +++ b/src/cortex-tui/src/interactive/renderer.rs @@ -205,13 +205,13 @@ impl<'a> Widget for InteractiveWidget<'a> { let is_active = i == self.state.active_tab; let is_hovered = self.state.hovered_tab == Some(i); let tab_text = format!(" {} ", tab.label); - // Active tab: the focused selection — violet on the dark gray + // Active tab: the focused selection — banner green on the dark gray // bar. Never inverted onto the accent. let style = if is_active { Style::default() .fg(ACCENT) - .bg(SELECTION_BG) .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT) } else if is_hovered { Style::default().fg(TEXT) } else { @@ -389,7 +389,7 @@ impl<'a> InteractiveWidget<'a> { is_hovered: bool, is_checked: bool, ) { - // Selected row: the dark gray bar with a violet `>` and a violet label — + // Selected row: the dark gray bar with a banner green `>` and a banner green label — // never inverted onto the accent. Unselected rows lead with a dim // middot and keep white copy. let selected_bar = is_selected && !item.disabled && !item.is_separator; @@ -419,7 +419,10 @@ impl<'a> InteractiveWidget<'a> { let mut x = area.x; if !item.is_separator { let (marker, marker_style) = if selected_bar { - ("> ", Style::default().fg(ACCENT).bg(SELECTION_BG)) + ( + "> ", + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), + ) } else if item.disabled { (" ", Style::default().fg(TEXT_MUTED)) } else { @@ -514,7 +517,7 @@ impl<'a> InteractiveWidget<'a> { area.x, y, "> ", - Style::default().fg(ACCENT).bg(SELECTION_BG), + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), ); } else if hovered { for dx in 0..area.width { @@ -591,7 +594,7 @@ impl<'a> InteractiveWidget<'a> { /// Render inline form for configuration within the panel. fn render_form(&self, form: &InlineFormState, area: Rect, buf: &mut Buffer) { // Draw border with form title — square corners, zero rounded frames, - // gray hairline: violet never outlines a box. + // gray hairline: banner green never outlines a box. let block = Block::default() .borders(Borders::ALL) .border_style(Style::default().fg(BORDER_FOCUS)) @@ -642,9 +645,12 @@ impl<'a> InteractiveWidget<'a> { ) { let x = area.x + 1; - // Label: the focused field is the selection — violet; the rest dim. + // Label: the focused field is the selection — banner green; the rest dim. let label_style = if is_focused { - Style::default().fg(ACCENT).add_modifier(Modifier::BOLD) + Style::default() + .fg(ACCENT) + .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT_DIM) }; @@ -781,7 +787,7 @@ mod tests { } #[test] - fn selected_row_is_violet_on_the_gray_bar_and_search_is_framed() { + fn selected_row_is_banner_green_on_the_gray_bar_and_search_is_framed() { let items = vec![ InteractiveItem::new("model", "Model").with_description("Cortex Mini 1"), InteractiveItem::new("mode", "Mode").with_description("Agent"), @@ -803,10 +809,10 @@ mod tests { assert!(rows[4].chars().all(|c| c == '─'), "{text}"); assert_eq!(buf[(0, 2)].style().fg, Some(HAIRLINE), "{text}"); - // Selected row: violet `>` on the gray bar, white label, dim description. + // Selected row: banner green `>` on the gray bar, white label, dim description. assert!(rows[5].starts_with("> Model"), "{text}"); assert_eq!(buf[(0, 5)].style().fg, Some(ACCENT)); - assert_eq!(buf[(0, 5)].style().bg, Some(SELECTION_BG)); + assert_eq!(buf[(0, 5)].style().bg, Some(TEXT)); assert_eq!(buf[(2, 5)].style().fg, Some(TEXT)); let desc_x = rows[5].find("Cortex").expect("description") as u16; assert_eq!(buf[(desc_x, 5)].style().fg, Some(TEXT_DIM)); diff --git a/src/cortex-tui/src/lock_boards.rs b/src/cortex-tui/src/lock_boards.rs index 4b2383e4..f7b5f982 100644 --- a/src/cortex-tui/src/lock_boards.rs +++ b/src/cortex-tui/src/lock_boards.rs @@ -2,7 +2,7 @@ //! //! These scenes share the gray session chrome — a past user turn on its gray //! bar, the hairline-framed `> ` composer, and the `model · hint` footer — -//! and Cortex product copy only. The one accent is the violet of a focused +//! and Cortex product copy only. The one accent is the banner green of a focused //! selection; green covers `✓` and `+diff`; red and amber stay on //! diagnostics; the Thinking status is the muted gold. @@ -250,7 +250,10 @@ fn paint_composer(area: Rect, buf: &mut Buffer, y: u16, composer: Composer<'_>) } let prefix = if i == 0 { "> " } else { " " }; let prefix_style = if i == 0 { - Style::default().fg(ACCENT).add_modifier(Modifier::BOLD) + Style::default() + .fg(ACCENT) + .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT) }; @@ -428,7 +431,7 @@ fn paint_search_field(area: Rect, buf: &mut Buffer, y: u16, query: &str, placeho /// One picker option at row `y`. /// -/// Selected: the dark gray bar, a violet `>`, the white number, the violet label, +/// Selected: the dark gray bar, a banner green `>`, the white number, the banner green label, /// dim `meta` right-aligned, and the dim description on the bar's second /// row. Unselected: a dim `·`, white number and label, dim meta and /// description. Returns the rows used; nothing is painted past `limit`. @@ -456,7 +459,7 @@ fn picker_option( } } let marker_style = if selected { - Style::default().fg(ACCENT).bg(SELECTION_BG) + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT_DIM) }; @@ -470,8 +473,8 @@ fn picker_option( let label_style = if selected { Style::default() .fg(ACCENT) - .bg(SELECTION_BG) .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT) }; @@ -923,11 +926,11 @@ fn board_palette(area: Rect, buf: &mut Buffer) { let (marker, marker_style, cmd_style) = if selected { ( "> ", - Style::default().fg(ACCENT).bg(SELECTION_BG), + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), Style::default() .fg(ACCENT) - .bg(SELECTION_BG) - .add_modifier(Modifier::BOLD), + .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT), ) } else { ( @@ -1443,7 +1446,7 @@ fn board_permission(area: Rect, buf: &mut Buffer) { fill_row(buf, area, y, SELECTION_BG); } let style = if *selected { - Style::default().fg(ACCENT).bg(SELECTION_BG) + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT) }; @@ -1548,8 +1551,8 @@ fn board_plan(area: Rect, buf: &mut Buffer) { part, Style::default() .fg(ACCENT) - .bg(SELECTION_BG) - .add_modifier(Modifier::BOLD), + .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT), ); } } @@ -2219,7 +2222,10 @@ fn board_files(area: Rect, buf: &mut Buffer) { } let mut x = area.x; let (marker, marker_style) = if selected { - ("> ", Style::default().fg(ACCENT).bg(SELECTION_BG)) + ( + "> ", + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), + ) } else { ("· ", Style::default().fg(TEXT_DIM)) }; @@ -2229,7 +2235,7 @@ fn board_files(area: Rect, buf: &mut Buffer) { let mut used = 0usize; for span in &mut spans { if selected { - span.style = span.style.fg(ACCENT).bg(SELECTION_BG); + span.style = span.style.fg(ACCENT).bg(cortex_core::style::TEXT); } let content = span.content.to_string(); let take = first_fitting_line(&content, path_budget.saturating_sub(used)); @@ -2409,7 +2415,10 @@ fn board_jobs(area: Rect, buf: &mut Buffer) { Color::Reset }; let (marker, marker_style) = if job.selected { - ("> ", Style::default().fg(ACCENT).bg(bg)) + ( + "> ", + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), + ) } else { ("· ", Style::default().fg(TEXT_DIM)) }; @@ -2423,8 +2432,8 @@ fn board_jobs(area: Rect, buf: &mut Buffer) { let title_style = if job.selected { Style::default() .fg(ACCENT) - .bg(bg) .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT) }; @@ -2852,7 +2861,7 @@ fn board_config(area: Rect, buf: &mut Buffer) { area.x, y, "> ", - Style::default().fg(ACCENT).bg(SELECTION_BG), + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), ); buf.set_string( area.x + 2, @@ -2860,8 +2869,8 @@ fn board_config(area: Rect, buf: &mut Buffer) { &label, Style::default() .fg(ACCENT) - .bg(SELECTION_BG) - .add_modifier(Modifier::BOLD), + .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT), ); // The selected value keeps its column gap and never ends on a // dangling `·` when the `⏎ edit` affordance takes the right edge. @@ -3192,8 +3201,8 @@ fn board_question(area: Rect, buf: &mut Buffer) { let style = if *selected { Style::default() .fg(ACCENT) - .bg(SELECTION_BG) .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT) }; @@ -3238,7 +3247,7 @@ fn board_skills(area: Rect, buf: &mut Buffer) { area.x, y, "> ", - Style::default().fg(ACCENT).bg(SELECTION_BG), + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), ); buf.set_string( area.x + 2, @@ -3246,8 +3255,8 @@ fn board_skills(area: Rect, buf: &mut Buffer) { cmd, Style::default() .fg(ACCENT) - .bg(SELECTION_BG) - .add_modifier(Modifier::BOLD), + .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT), ); buf.set_string( area.x + 2 + cmd.len() as u16 + 2, @@ -3883,7 +3892,10 @@ fn board_multi_diff(area: Rect, buf: &mut Buffer) { } }; let (marker, marker_style) = if selected { - ("> ", Style::default().fg(ACCENT).bg(SELECTION_BG)) + ( + "> ", + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), + ) } else { ("· ", Style::default().fg(TEXT_DIM)) }; @@ -3891,8 +3903,8 @@ fn board_multi_diff(area: Rect, buf: &mut Buffer) { let path_style = if selected { Style::default() .fg(ACCENT) - .bg(SELECTION_BG) .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT) }; diff --git a/src/cortex-tui/src/lock_proof.rs b/src/cortex-tui/src/lock_proof.rs index 8b7ae004..b63f5dc0 100644 --- a/src/cortex-tui/src/lock_proof.rs +++ b/src/cortex-tui/src/lock_proof.rs @@ -537,8 +537,8 @@ mod tests { /// Locked diff green `#4ADE80` as an SGR foreground. const GREEN_FG: &str = "38;2;74;222;128"; - /// Selection violet `#A78BFA` as an SGR foreground. - const ACCENT_FG: &str = "38;2;167;139;250"; + /// Selection banner green `#1F4945` as an SGR foreground. + const ACCENT_FG: &str = "38;2;31;73;69"; /// Error red `#F87171` as an SGR foreground. const ERROR_FG: &str = "38;2;248;113;113"; @@ -756,8 +756,8 @@ mod tests { ]; #[test] - fn violet_is_reserved_for_selection_and_focused_composer() { - // Violet is the focused composer `>` (between hairlines) and the + fn banner_green_is_reserved_for_selection_and_focused_composer() { + // Banner green is the focused composer `>` (between hairlines) and the // focused picker `>` + label on the gray selection bar. Past user // `>` stays white. for id in lock_scene_ids() { @@ -768,7 +768,12 @@ mod tests { if cell.style().fg != Some(ACCENT) || cell.symbol() == " " { continue; } - let on_bar = cell.style().bg == Some(SELECTION_BG); + assert_eq!(cell.fg, Color::Rgb(31, 73, 69)); + assert_eq!( + cell.bg, TEXT, + "{id}: accent needs a readable backing at {size:?} ({x},{y})" + ); + let on_bar = (0..buf.area.width).any(|col| buf[(col, y)].bg == SELECTION_BG); let boxed = y > 0 && y + 1 < buf.area.height && row_text(buf, y - 1).contains('╭') @@ -781,16 +786,16 @@ mod tests { let fuzzy_or_slash = cell.style().bg != Some(USER_TURN_BG); assert!( on_bar || composer_caret || fuzzy_or_slash, - "{id} paints violet {:?} off the selection bar and composer at {size:?} ({x},{y}):\n{}", + "{id} paints banner green {:?} off the selection bar and composer at {size:?} ({x},{y}):\n{}", cell.symbol(), frame.plain ); } - let violet = painted_chars(&frame.ansi, ACCENT_FG); + let banner_green = painted_chars(&frame.ansi, ACCENT_FG); if SELECTION_SCENES.contains(id) || COMPOSER_SCENES.contains(id) { assert!( - violet.contains('>'), - "{id} must paint a violet `>` at {size:?}: {violet:?}" + banner_green.contains('>'), + "{id} must paint a banner green `>` at {size:?}: {banner_green:?}" ); } } @@ -798,17 +803,17 @@ mod tests { } #[test] - fn selection_rows_are_violet_on_the_selection_bar_never_inverted() { - // Violet is never a background (no inverted bar). The selection bar + fn selection_rows_are_banner_green_on_the_selection_bar_never_inverted() { + // Banner green is never a background (no inverted bar). The selection bar // is the locked `#221A38` wash. - const ACCENT_BG: &str = "48;2;167;139;250"; + const ACCENT_BG: &str = "48;2;31;73;69"; const SELECTION_WASH: &str = "48;2;38;38;38"; for id in lock_scene_ids() { for size in SIZES { let frame = render_lock_scene(id, size.0, size.1).expect(id); assert!( !frame.ansi.contains(ACCENT_BG), - "{id} paints an inverted violet bar at {size:?}" + "{id} paints an inverted banner green bar at {size:?}" ); } } @@ -823,11 +828,12 @@ mod tests { (0..120u16).any(|x| { frame.buffer[(x, *y)].symbol() == ">" && frame.buffer[(x, *y)].style().fg == Some(ACCENT) - && frame.buffer[(x, *y)].style().bg == Some(SELECTION_BG) + && frame.buffer[(x, *y)].style().bg == Some(TEXT) + && (0..120u16).any(|col| frame.buffer[(col, *y)].bg == SELECTION_BG) }) }) - .unwrap_or_else(|| panic!("{id} has no violet `>` row:\n{}", frame.plain)); - // The focused row carries the selection bar and a violet caret. + .unwrap_or_else(|| panic!("{id} has no banner green `>` row:\n{}", frame.plain)); + // The focused row carries the selection bar and a banner green caret. let has_bar = (0..120u16).any(|x| frame.buffer[(x, row)].style().bg == Some(SELECTION_BG)); assert!( @@ -897,7 +903,7 @@ mod tests { fn every_edit_plus_count_is_green() { // Rule from states 10 (Edit +9), 24 (queued Edit +58) and 30 (MAX // footer +214): the `+N` of an Edit / Write / commit is the diff - // green at both sizes — never gray or violet. Scan every scene for + // green at both sizes — never gray or banner green. Scan every scene for // `+N` tokens on Edit, Write and Committed lines. for id in lock_scene_ids() { for size in SIZES { @@ -1001,10 +1007,10 @@ mod tests { #[test] fn banned_colors_never_painted() { - // The interim violet highlight is gone with the mint one: no scene - // paints violet, the old `#221A38` violet wash, the mint pair, the old + // The interim banner green highlight is gone with the mint one: no scene + // paints banner green, the old `#221A38` retired violet wash, the mint pair, the old // brand green, or the navy wash — the host terminal owns the - // background and the violet lives on the focused selection alone. + // background and the banner green lives on the focused selection alone. // Mint `#00F5D4` / `#1A3330` never painted. Selection bar `#221A38` // is allowed; inverted accent as a background is not (checked above). const BANNED: [&str; 4] = ["0;245;212", "26;51;48", "0;255;163", "10;22;40"]; @@ -1046,7 +1052,7 @@ mod tests { frame.plain ); }; - // The focused composer `>` is violet; a placeholder is dim. + // The focused composer `>` is banner green; a placeholder is dim. assert_eq!(buf[(x, y)].style().fg, Some(ACCENT), "{id} at {size:?}"); let row = row_text(buf, y); assert!( @@ -1170,9 +1176,9 @@ mod tests { } #[test] - fn past_user_caret_is_white_focused_composer_is_violet() { + fn past_user_caret_is_white_focused_composer_is_banner_green() { // Past user `>` (on the gray bar) is never accented. The focused - // composer `>` (between hairlines) is violet `#A78BFA`. + // composer `>` (between hairlines) is banner green `#1F4945`. for id in lock_scene_ids() { for size in SIZES { let frame = render_lock_scene(id, size.0, size.1).expect(id); @@ -1204,7 +1210,7 @@ mod tests { assert_eq!( cell.style().fg, Some(ACCENT), - "{id} composer caret must be violet at {size:?}:\n{}", + "{id} composer caret must be banner green at {size:?}:\n{}", row_text(buf, y) ); } @@ -1213,15 +1219,15 @@ mod tests { } // A typed composer still accents only the caret, not the typed copy. let typing = render_lock_scene("typing", 120, 40).expect("typing"); - let violet = painted_chars(&typing.ansi, ACCENT_FG); + let banner_green = painted_chars(&typing.ansi, ACCENT_FG); assert!( - violet.contains('>'), - "typing composer `>` must be violet: {violet:?}" + banner_green.contains('>'), + "typing composer `>` must be banner green: {banner_green:?}" ); let footer_max = render_lock_scene("footer_max", 120, 40).expect("footer_max"); assert!( painted_chars(&footer_max.ansi, ACCENT_FG).contains('>'), - "footer_max composer `>` must be violet" + "footer_max composer `>` must be banner green" ); } @@ -2281,12 +2287,12 @@ mod tests { } #[test] - fn slash_palette_rows_are_middot_or_violet_caret() { + fn slash_palette_rows_are_middot_or_banner_green_caret() { for size in SIZES { let frame = render_lock_scene("palette", size.0, size.1).expect("palette"); assert!( frame.plain.contains("> /model") || frame.plain.contains("/model"), - "the focused command leads with the violet caret at {size:?}:\n{}", + "the focused command leads with the banner green caret at {size:?}:\n{}", frame.plain ); assert!( @@ -2301,7 +2307,7 @@ mod tests { ); } // The wide selected row keeps the dim description on the bar, never a - // bright (or violet) description. + // bright (or banner green) description. let wide = render_lock_scene("palette", 120, 40).expect("palette wide"); let buf = &wide.buffer; let y = (0..40u16) @@ -2429,7 +2435,7 @@ mod tests { #[test] fn login_is_a_numbered_picker_with_live_sub_states() { // `login_select` is the picker with the selection moved to option 2: - // the violet `>` and the gray bar sit on `Paste an API key`, option 1 + // the banner green `>` and the gray bar sit on `Paste an API key`, option 1 // falls back to the dim middot. let select = render_lock_scene("login_select", 120, 40).expect("select"); assert!(select.plain.contains("Welcome to Cortex CLI!")); @@ -2462,7 +2468,12 @@ mod tests { ); assert_eq!(buf[(0, row)].style().fg, Some(ACCENT)); for x in 0..120u16 { - assert_eq!(buf[(x, row)].style().bg, Some(SELECTION_BG), "col {x}"); + let expected = if buf[(x, row)].fg == ACCENT { + TEXT + } else { + SELECTION_BG + }; + assert_eq!(buf[(x, row)].style().bg, Some(expected), "col {x}"); assert_eq!(buf[(x, row + 1)].style().bg, Some(SELECTION_BG), "col {x}"); } let other = (0..40u16) @@ -2648,7 +2659,7 @@ mod tests { #[test] fn tool_tile_dots_are_white() { - // Every tool tile paints its `●` status dot white — never the violet accent, + // Every tool tile paints its `●` status dot white — never the banner green accent, // never green. Labels stay white too. let tiles = [ "tool_tiles", diff --git a/src/cortex-tui/src/lock_v2.rs b/src/cortex-tui/src/lock_v2.rs index 7d2135c2..05faca82 100644 --- a/src/cortex-tui/src/lock_v2.rs +++ b/src/cortex-tui/src/lock_v2.rs @@ -720,7 +720,7 @@ Tell me what you'd like to do.", ); state.add_message( Message::assistant( - "**Plan**\n1. Recapture every SPEC §7 board from the live session.\n2. Keep violet on keyboard focus only.\n3. Do not merge until Designer signs off.", + "**Plan**\n1. Recapture every SPEC §7 board from the live session.\n2. Keep banner green on keyboard focus only.\n3. Do not merge until Designer signs off.", ) .with_timestamp("09:04 AM"), ); @@ -1375,11 +1375,11 @@ mod tests { } } } - assert!(found_accent, "expected violet caret on welcome"); + assert!(found_accent, "expected banner green caret on welcome"); } #[test] - fn slash_hover_is_not_violet_wash() { + fn slash_hover_is_not_banner_green_wash() { let mut state = palette_state("/"); state.autocomplete.hovered = Some(3); let config = capture_config(120, 40); @@ -1398,7 +1398,7 @@ mod tests { found_hover = true; } if buf[(x, y)].bg == ratatui::style::Color::Rgb(0x22, 0x1A, 0x38) { - panic!("retired violet wash at {x},{y}"); + panic!("retired banner green wash at {x},{y}"); } } } diff --git a/src/cortex-tui/src/modal/help.rs b/src/cortex-tui/src/modal/help.rs index 686c77ce..81f095d3 100644 --- a/src/cortex-tui/src/modal/help.rs +++ b/src/cortex-tui/src/modal/help.rs @@ -297,7 +297,7 @@ impl Modal for HelpModal { // Empty line, nothing to render } ContentLine::Header(title) => { - // Section header in violet + // Section header in banner green buf.set_string(area.x + 1, y, title, Style::default().fg(CYAN_PRIMARY)); } ContentLine::Item { key, description } => { diff --git a/src/cortex-tui/src/modal/models.rs b/src/cortex-tui/src/modal/models.rs index 0b70da57..cc3e5bf2 100644 --- a/src/cortex-tui/src/modal/models.rs +++ b/src/cortex-tui/src/modal/models.rs @@ -327,7 +327,7 @@ impl ModelsModal { is_selected: bool, ) { // Determine styles - // Selected rows: the violet accent on the dark gray bar — never inverted. + // Selected rows: the banner green accent on the dark gray bar — never inverted. let (bg, fg, prefix_fg) = if is_selected { (SELECTION_BG, ACCENT, ACCENT) } else { diff --git a/src/cortex-tui/src/modal/sessions/rendering.rs b/src/cortex-tui/src/modal/sessions/rendering.rs index a1638286..bde63582 100644 --- a/src/cortex-tui/src/modal/sessions/rendering.rs +++ b/src/cortex-tui/src/modal/sessions/rendering.rs @@ -82,7 +82,7 @@ pub fn render_search_bar(search_query: &str, area: Rect, buf: &mut Buffer) { /// Render a single session row. pub fn render_session_row(session: &SessionInfo, is_selected: bool, area: Rect, buf: &mut Buffer) { - // Selected rows: the violet accent on the dark gray bar — never inverted. + // Selected rows: the banner green accent on the dark gray bar — never inverted. let (bg, fg, prefix_fg) = if is_selected { (SELECTION_BG, ACCENT, ACCENT) } else { diff --git a/src/cortex-tui/src/readme_hero.rs b/src/cortex-tui/src/readme_hero.rs index 82ddfc79..b79bf365 100644 --- a/src/cortex-tui/src/readme_hero.rs +++ b/src/cortex-tui/src/readme_hero.rs @@ -191,10 +191,10 @@ mod tests { "missing hairline below composer:\n{}", first.plain ); - // Violet caret `#A78BFA`. + // Banner green caret `#1F4945`. assert!( - first.ansi.contains("\x1b[38;2;167;139;250m"), - "splash missing the violet caret" + first.ansi.contains("\x1b[38;2;31;73;69m"), + "splash missing the banner green caret" ); assert!( !first.ansi.contains("\x1b[38;2;0;245;212m"), @@ -247,8 +247,8 @@ mod tests { ); } assert!( - last.ansi.contains("\x1b[38;2;167;139;250m"), - "working missing the violet caret" + last.ansi.contains("\x1b[38;2;31;73;69m"), + "working missing the banner green caret" ); assert!(!last.plain.contains("▄█▀▀▀▀█▄")); assert!(!last.plain.contains("BUILD")); diff --git a/src/cortex-tui/src/runner/login_screen.rs b/src/cortex-tui/src/runner/login_screen.rs index f7377da0..8370bf3b 100644 --- a/src/cortex-tui/src/runner/login_screen.rs +++ b/src/cortex-tui/src/runner/login_screen.rs @@ -304,7 +304,7 @@ impl LoginScreen { } /// The sign-in picker: title, question, numbered options — the focused - /// one is a violet `>` and label on the dark gray bar, the others a dim `·` + /// one is a banner green `>` and label on the dark gray bar, the others a dim `·` /// with white copy — each with its dim description under the title, then /// the key hints. The version sits alone in the footer. fn render_select_method(&self, f: &mut ratatui::Frame, area: Rect) { @@ -352,7 +352,7 @@ impl LoginScreen { area.x, y, "> ", - Style::default().fg(ACCENT).bg(SELECTION_BG), + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), ); buf.set_string( area.x + 2, @@ -366,8 +366,8 @@ impl LoginScreen { &label, Style::default() .fg(ACCENT) - .bg(SELECTION_BG) - .add_modifier(Modifier::BOLD), + .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT), ); buf.set_string( area.x + 4, @@ -986,14 +986,14 @@ mod tests { assert!(!text.contains("▄█▀▀▀▀█▄"), "{text}"); assert!(!text.to_lowercase().contains("grok")); - // Violet is the focused `>` and label only; the number stays white, the + // Banner green is the focused `>` and label only; the number stays white, the // description dim, and the whole two-row option sits on the gray bar. let buf = terminal.backend().buffer(); let row = (0..24u16) .find(|y| buf[(0, *y)].symbol() == ">") .expect("selected row"); assert_eq!(buf[(0, row)].style().fg, Some(ACCENT)); - assert_eq!(buf[(0, row)].style().bg, Some(SELECTION_BG)); + assert_eq!(buf[(0, row)].style().bg, Some(TEXT)); assert_eq!(buf[(2, row)].style().fg, Some(TEXT)); assert_eq!(buf[(4, row)].style().fg, Some(ACCENT)); assert_eq!(buf[(4, row + 1)].style().fg, Some(TEXT_DIM)); @@ -1007,7 +1007,7 @@ mod tests { assert_eq!(buf[(4, other)].style().fg, Some(TEXT)); assert_ne!(buf[(4, other)].style().bg, Some(SELECTION_BG)); // The selected row uses the locked selection wash `#221A38`. - // Violet is never a background — no inverted bar. + // Banner green is never a background — no inverted bar. for y in 0..24u16 { for x in 0..80u16 { let cell = &buf[(x, y)]; @@ -1040,7 +1040,7 @@ mod tests { .find(|y| buf[(0, *y)].symbol() == ">") .expect("focused row"); assert_eq!(buf[(0, row)].style().fg, Some(ACCENT)); - assert_eq!(buf[(4, row)].style().bg, Some(SELECTION_BG)); + assert_eq!(buf[(4, row)].style().bg, Some(TEXT)); } // `lock_select` is option 1. let screen = LoginScreen::lock_select("1.0.0", None); diff --git a/src/cortex-tui/src/runner/trust_screen.rs b/src/cortex-tui/src/runner/trust_screen.rs index 35d0326c..e5fcdc5d 100644 --- a/src/cortex-tui/src/runner/trust_screen.rs +++ b/src/cortex-tui/src/runner/trust_screen.rs @@ -134,7 +134,7 @@ impl TrustScreen { } /// The trust prompt: title and workspace path, the why in dim copy, then - /// two numbered options — the focused one a violet `>` and label on the + /// two numbered options — the focused one a banner green `>` and label on the /// dark gray bar, the other a dim `·` — each with its description under /// the title, and the key hints at the bottom. fn render(&self, f: &mut ratatui::Frame) { @@ -211,13 +211,15 @@ pub fn render_trust_prompt( } } let bar = Style::default().bg(SELECTION_BG); - buf.set_string(area.x, y, "> ", bar.fg(ACCENT)); + buf.set_string(area.x, y, "> ", bar.fg(ACCENT).bg(cortex_core::style::TEXT)); buf.set_string(area.x + 2, y, format!("{number} "), bar.fg(TEXT)); buf.set_string( area.x + 4, y, &label, - bar.fg(ACCENT).add_modifier(Modifier::BOLD), + bar.fg(ACCENT) + .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT), ); buf.set_string(area.x + 4, y + 1, &description, bar.fg(TEXT_DIM)); } else { @@ -312,7 +314,7 @@ mod tests { .expect("selected"); assert_eq!(buf[(0, row)].style().fg, Some(ACCENT)); assert_eq!(buf[(4, row)].style().fg, Some(ACCENT)); - assert_eq!(buf[(4, row)].style().bg, Some(SELECTION_BG)); + assert_eq!(buf[(4, row)].style().bg, Some(TEXT)); assert_eq!(buf[(4, row + 1)].style().fg, Some(TEXT_DIM)); let other = (0..h) .find(|y| buf[(0, *y)].symbol() == "·") diff --git a/src/cortex-tui/src/ui/chrome.rs b/src/cortex-tui/src/ui/chrome.rs index 75056f76..a52f32e1 100644 --- a/src/cortex-tui/src/ui/chrome.rs +++ b/src/cortex-tui/src/ui/chrome.rs @@ -524,16 +524,19 @@ pub fn paint_footer(area: Rect, buf: &mut Buffer, set: FooterSet, hovered: Optio } } -/// True when the composer caret should be violet (keyboard focus). +/// True when the composer caret should be banner green (keyboard focus). pub fn composer_caret_style(focused: bool) -> Style { if focused { - Style::default().fg(ACCENT).add_modifier(Modifier::BOLD) + Style::default() + .fg(ACCENT) + .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT_DIM) } } -/// Hover bar fill — `#1A1A1A`, no violet. +/// Hover bar fill — `#1A1A1A`, no banner green. pub fn paint_hover_bar(area: Rect, buf: &mut Buffer) { for dx in 0..area.width { if let Some(cell) = buf.cell_mut((area.x + dx, area.y)) { @@ -596,9 +599,7 @@ pub fn paint_opt_in_banner(area: Rect, buf: &mut Buffer, hover: Option, focu fn banner_btn_style(hover: bool, focus: bool, recommended: bool) -> Style { if focus { - Style::default() - .fg(ACCENT) - .bg(cortex_core::style::SELECTION_BG) + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT) } else if hover { Style::default() .fg(TEXT) diff --git a/src/cortex-tui/src/ui/colors.rs b/src/cortex-tui/src/ui/colors.rs index f201a10b..15289589 100644 --- a/src/cortex-tui/src/ui/colors.rs +++ b/src/cortex-tui/src/ui/colors.rs @@ -71,7 +71,7 @@ pub fn detect_terminal_bg() -> Option<(u8, u8, u8)> { /// Adaptive color palette that adjusts to terminal background #[derive(Debug, Clone)] pub struct AdaptiveColors { - /// Selection accent — the Cortex violet, for the focused `>` caret and label only + /// Selection accent — the Cortex banner green, for the focused `>` caret and label only pub accent: Color, /// Primary text color pub text: Color, @@ -148,8 +148,8 @@ impl AdaptiveColors { /// Create light theme colors adapted to the given background pub fn light_theme(bg: (u8, u8, u8)) -> Self { - // Darker violet for contrast on light backgrounds - let accent_rgb = (0x7C, 0x3A, 0xED); + // Darker banner green for contrast on light backgrounds + let accent_rgb = (0x1F, 0x49, 0x45); // Blend colors with background for better integration let text_dim_rgb = blend((0x60, 0x60, 0x60), bg, 0.9); @@ -183,18 +183,18 @@ impl AdaptiveColors { /// palette from `cortex_core::style`. pub fn default_dark() -> Self { Self { - accent: cortex_core::style::ACCENT, // #A78BFA violet - text: cortex_core::style::TEXT, // #F5F5F5 - text_dim: cortex_core::style::TEXT_DIM, // #6B7280 - text_muted: cortex_core::style::TEXT_MUTED, // #4B5563 - user_bg: cortex_core::style::USER_TURN_BG, // #1C1C1C - panel_bg: cortex_core::style::PANEL_BG, // #141414 - border: cortex_core::style::HAIRLINE, // #3A3A3A - success: cortex_core::style::SUCCESS, // #4ADE80 - diff_add: cortex_core::style::DIFF_ADD, // #4ADE80 - error: cortex_core::style::ERROR, // #F87171 - warning: cortex_core::style::WARNING, // #FFC857 - thinking: cortex_core::style::THINKING, // dim + accent: cortex_core::style::ACCENT, // #1F4945 banner green + text: cortex_core::style::TEXT, // #F5F5F5 + text_dim: cortex_core::style::TEXT_DIM, // #6B7280 + text_muted: cortex_core::style::TEXT_MUTED, // #4B5563 + user_bg: cortex_core::style::USER_TURN_BG, // #1C1C1C + panel_bg: cortex_core::style::PANEL_BG, // #141414 + border: cortex_core::style::HAIRLINE, // #3A3A3A + success: cortex_core::style::SUCCESS, // #4ADE80 + diff_add: cortex_core::style::DIFF_ADD, // #4ADE80 + error: cortex_core::style::ERROR, // #F87171 + warning: cortex_core::style::WARNING, // #FFC857 + thinking: cortex_core::style::THINKING, // dim selection: cortex_core::style::SELECTION_BG, // #262626 hover: cortex_core::style::BAR_HOVER, hairline_hover: cortex_core::style::BORDER_FOCUS, @@ -281,8 +281,8 @@ mod tests { #[test] fn test_default_dark_colors() { let colors = AdaptiveColors::default_dark(); - // The accent is the selection violet. - assert!(matches!(colors.accent, Color::Rgb(0xA7, 0x8B, 0xFA))); + // The accent is the selection banner green. + assert!(matches!(colors.accent, Color::Rgb(0x1F, 0x49, 0x45))); // Green covers `✓` and diff additions alike. assert!(matches!(colors.diff_add, Color::Rgb(0x4A, 0xDE, 0x80))); assert_eq!(colors.success, colors.diff_add); @@ -300,7 +300,7 @@ mod tests { #[test] fn test_dark_theme() { let colors = AdaptiveColors::dark_theme((0x1A, 0x1A, 0x1A)); - assert!(matches!(colors.accent, Color::Rgb(0xA7, 0x8B, 0xFA))); + assert!(matches!(colors.accent, Color::Rgb(0x1F, 0x49, 0x45))); // Structural grays stay neutral after blending with the background. for color in [colors.user_bg, colors.border] { let Color::Rgb(r, g, b) = color else { @@ -314,14 +314,14 @@ mod tests { fn test_light_theme() { let colors = AdaptiveColors::light_theme((255, 255, 255)); // Light theme should have darker accent for contrast - assert!(matches!(colors.accent, Color::Rgb(0x7C, 0x3A, 0xED))); + assert!(matches!(colors.accent, Color::Rgb(0x1F, 0x49, 0x45))); } #[test] fn test_from_theme_name() { let dark_colors = AdaptiveColors::from_theme_name("dark"); - // The session accent is the theme's primary — the selection violet. - assert!(matches!(dark_colors.accent, Color::Rgb(0xA7, 0x8B, 0xFA))); + // The session accent is the theme's primary — the selection banner green. + assert!(matches!(dark_colors.accent, Color::Rgb(0x1F, 0x49, 0x45))); // Dark themes carry the locked selection bar. assert!(matches!( dark_colors.selection, @@ -330,7 +330,7 @@ mod tests { let light_colors = AdaptiveColors::from_theme_name("light"); // Light theme should have different accent - assert!(matches!(light_colors.accent, Color::Rgb(0x7C, 0x3A, 0xED))); + assert!(matches!(light_colors.accent, Color::Rgb(0x1F, 0x49, 0x45))); let monokai_colors = AdaptiveColors::from_theme_name("monokai"); // Monokai has green accent diff --git a/src/cortex-tui/src/views/minimal_session/rendering.rs b/src/cortex-tui/src/views/minimal_session/rendering.rs index 39d8d411..10e99421 100644 --- a/src/cortex-tui/src/views/minimal_session/rendering.rs +++ b/src/cortex-tui/src/views/minimal_session/rendering.rs @@ -266,7 +266,7 @@ pub fn render_tool_call( (Some(frame.to_string()), colors.text_dim) } // Completed tiles carry a white status dot; the label stays white. - // Violet is the selection accent only, green is `✓` and `+diff` only. + // Banner green is the selection accent only, green is `✓` and `+diff` only. ToolStatus::Completed => (Some("●".to_string()), colors.text), ToolStatus::Failed => (Some("●".to_string()), colors.error), }; diff --git a/src/cortex-tui/src/views/minimal_session/tests.rs b/src/cortex-tui/src/views/minimal_session/tests.rs index ac3dcf5a..39535bf5 100644 --- a/src/cortex-tui/src/views/minimal_session/tests.rs +++ b/src/cortex-tui/src/views/minimal_session/tests.rs @@ -187,7 +187,7 @@ mod harness_snapshots { } #[test] - fn autocomplete_selected_row_is_violet_on_the_gray_bar() { + fn autocomplete_selected_row_is_banner_green_on_the_gray_bar() { use cortex_core::style::{ACCENT, SELECTION_BG, TEXT, TEXT_DIM}; use crate::app::{AutocompleteItem, AutocompleteTrigger}; @@ -219,7 +219,10 @@ mod harness_snapshots { if buf[(60, y)].style().bg != Some(SELECTION_BG) || !row.contains('/') { continue; } - assert!(row.contains("> /"), "violet caret leads the row: {row}"); + assert!( + row.contains("> /"), + "banner green caret leads the row: {row}" + ); let caret_x = row.find("> /").expect("caret") as u16; assert_eq!(buf[(caret_x, y)].style().fg, Some(ACCENT), "{row}"); let label_at = row.find('/').expect("selected command label"); diff --git a/src/cortex-tui/src/views/minimal_session/view.rs b/src/cortex-tui/src/views/minimal_session/view.rs index ca3583d5..f12d5458 100644 --- a/src/cortex-tui/src/views/minimal_session/view.rs +++ b/src/cortex-tui/src/views/minimal_session/view.rs @@ -98,7 +98,12 @@ pub fn paint_composer_contents( Style::default().fg(TEXT_BRIGHT), ); } else { - buf.set_string(col, y, ch.to_string(), Style::default().fg(fg)); + let style = if fg == ACCENT { + Style::default().fg(fg).bg(TEXT) + } else { + Style::default().fg(fg) + }; + buf.set_string(col, y, ch.to_string(), style); } col = col.saturating_add(1); if col >= x + width { @@ -371,9 +376,9 @@ impl<'a> MinimalSessionView<'a> { /// Renders autocomplete suggestions inline above the composer. /// - /// Focused row: dark gray bar, violet `>` , label in text with matched - /// characters in violet. Hovered (not focused) row: `#1A1A1A` bar, no - /// violet. Trailer is muted. + /// Focused row: dark gray bar, banner green `>` , label in text with matched + /// characters in banner green. Hovered (not focused) row: `#1A1A1A` bar, no + /// banner green. Trailer is muted. fn render_autocomplete_inline(&self, area: Rect, buf: &mut Buffer) { if area.is_empty() { return; diff --git a/src/cortex-tui/src/views/question_prompt.rs b/src/cortex-tui/src/views/question_prompt.rs index ea3110d4..f00549df 100644 --- a/src/cortex-tui/src/views/question_prompt.rs +++ b/src/cortex-tui/src/views/question_prompt.rs @@ -115,7 +115,7 @@ impl Widget for QuestionPromptView<'_> { // Clear the modal area Clear.render(layout.modal_area, buf); - // Draw border — gray hairline, white title; violet never outlines a box. + // Draw border — gray hairline, white title; banner green never outlines a box. let border_style = Style::default().fg(colors.border); let block = Block::default() .borders(Borders::ALL) @@ -155,10 +155,10 @@ impl QuestionPromptView<'_> { let is_hovered = self.hovered_tab == Some(i); let is_answered = !self.state.answers[i].is_empty(); - // The active tab is the focused selection: violet on the gray bar, + // The active tab is the focused selection: banner green on the gray bar, // never inverted onto the accent. Answered tabs read white. let (fg, bg) = if is_active { - (colors.accent, colors.selection) + (colors.accent, cortex_core::style::TEXT) } else if is_hovered { (colors.text, colors.user_bg) } else { @@ -193,7 +193,7 @@ impl QuestionPromptView<'_> { let is_hovered = self.hovered_tab == Some(self.state.request.questions.len()); let (fg, bg) = if is_active { - (colors.accent, colors.selection) + (colors.accent, cortex_core::style::TEXT) } else if is_hovered { (colors.text, colors.user_bg) } else { @@ -264,10 +264,10 @@ impl QuestionPromptView<'_> { let prefix = format!("{}. {}", i + 1, checkbox); let label = &opt.label; - // Focused option: violet on the gray selection bar; a picked option + // Focused option: banner green on the gray selection bar; a picked option // stays white — its `✓` carries the green. let (fg, bg) = if is_selected { - (colors.accent, colors.selection) + (colors.accent, cortex_core::style::TEXT) } else if is_hovered { (colors.text, colors.user_bg) } else { @@ -330,7 +330,7 @@ impl QuestionPromptView<'_> { let label = "Type your own answer"; let (fg, bg) = if is_selected { - (colors.accent, colors.selection) + (colors.accent, cortex_core::style::TEXT) } else if is_hovered { (colors.text, colors.user_bg) } else { @@ -607,7 +607,7 @@ pub enum QuestionHit { mod tests { use super::*; use crate::question::{Question, QuestionRequest, QuestionState, QuestionType}; - use cortex_core::style::{ACCENT, HAIRLINE, SELECTION_BG, TEXT}; + use cortex_core::style::{ACCENT, HAIRLINE, TEXT}; fn request() -> QuestionRequest { QuestionRequest { @@ -640,7 +640,7 @@ mod tests { } #[test] - fn focused_option_is_violet_on_the_gray_bar_and_the_frame_is_gray() { + fn focused_option_is_banner_green_on_the_gray_bar_and_the_frame_is_gray() { let state = QuestionState::new(request()); let view = QuestionPromptView::new(&state); let area = Rect::new(0, 0, 100, 24); @@ -653,8 +653,8 @@ mod tests { for x in 0..100u16 { let cell = &buf[(x, y)]; if cell.style().fg == Some(ACCENT) && cell.symbol() != " " { - // Violet only ever sits on the selection bar. - assert_eq!(cell.style().bg, Some(SELECTION_BG), "({x},{y})"); + // Banner green has a contrasting near-white backing. + assert_eq!(cell.style().bg, Some(cortex_core::style::TEXT), "({x},{y})"); focused = Some(y); } if matches!(cell.symbol(), "─" | "│" | "┌" | "┐" | "└" | "┘") { @@ -668,9 +668,9 @@ mod tests { .map(|x| buf[(x, y)].symbol().to_string()) .collect(); assert!(row.contains("1. Middleware on POST"), "{row}"); - // The whole row is the gray bar. + // The focused option has a contrasting backing. let x0 = row.find('1').expect("prefix") as u16; - assert_eq!(buf[(x0, y)].style().bg, Some(SELECTION_BG)); + assert_eq!(buf[(x0, y)].style().bg, Some(cortex_core::style::TEXT)); assert_eq!(buf[(x0, y)].style().fg, Some(ACCENT)); assert!(frame_cells > 0, "the prompt keeps its square frame"); // The title is white, not the accent. diff --git a/src/cortex-tui/src/widgets/action_bar.rs b/src/cortex-tui/src/widgets/action_bar.rs index 71d8fd9a..1e5451a4 100644 --- a/src/cortex-tui/src/widgets/action_bar.rs +++ b/src/cortex-tui/src/widgets/action_bar.rs @@ -10,7 +10,7 @@ use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget}; /// Style for action items #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ActionStyle { - /// Primary action (violet) - main actions like Add, Select + /// Primary action (banner green) - main actions like Add, Select Primary, /// Danger action (red) - destructive actions like Delete Danger, diff --git a/src/cortex-tui/src/widgets/approval_overlay.rs b/src/cortex-tui/src/widgets/approval_overlay.rs index 20a5f725..5588b113 100644 --- a/src/cortex-tui/src/widgets/approval_overlay.rs +++ b/src/cortex-tui/src/widgets/approval_overlay.rs @@ -548,23 +548,23 @@ impl ApprovalOverlay { let mut col = area.x; - // Selection indicator: violet `>` on the focused row, a dim middot + // Selection indicator: banner green `>` on the focused row, a dim middot // on the others. let prefix = if is_selected { ">" } else { "·" }; let prefix_style = if is_selected { - Style::default().fg(ACCENT).bg(SELECTION_BG) + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT_DIM).bg(SURFACE_0) }; buf.set_string(col, y, prefix, prefix_style); col += 2; - // Option label: the violet accent when focused, white otherwise. + // Option label: the banner green accent when focused, white otherwise. let label_style = if is_selected { Style::default() .fg(ACCENT) - .bg(SELECTION_BG) .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT).bg(SURFACE_0) }; diff --git a/src/cortex-tui/src/widgets/autocomplete.rs b/src/cortex-tui/src/widgets/autocomplete.rs index 5ca12c36..726b843e 100644 --- a/src/cortex-tui/src/widgets/autocomplete.rs +++ b/src/cortex-tui/src/widgets/autocomplete.rs @@ -129,12 +129,12 @@ impl<'a> AutocompletePopup<'a> { } // Label - // The selected label is the violet accent on the gray bar. + // The selected label is the banner green accent on the gray bar. let label_style = if is_selected { Style::default() .fg(ACCENT) - .bg(bg) .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT).bg(bg) }; @@ -178,7 +178,7 @@ impl<'a> AutocompletePopup<'a> { // Selection indicator if is_selected { - let indicator_style = Style::default().fg(ACCENT).bg(bg); + let indicator_style = Style::default().fg(ACCENT).bg(cortex_core::style::TEXT); if let Some(cell) = buf.cell_mut((area.x, area.y)) { cell.set_char('>').set_style(indicator_style); } diff --git a/src/cortex-tui/src/widgets/backtrack_overlay.rs b/src/cortex-tui/src/widgets/backtrack_overlay.rs index 4485fff7..498b598d 100644 --- a/src/cortex-tui/src/widgets/backtrack_overlay.rs +++ b/src/cortex-tui/src/widgets/backtrack_overlay.rs @@ -105,7 +105,10 @@ impl<'a> BacktrackOverlay<'a> { // Selection indicator if is_selected { - let indicator_style = Style::default().fg(ACCENT).bg(bg).bold(); + let indicator_style = Style::default() + .fg(ACCENT) + .bold() + .bg(cortex_core::style::TEXT); if let Some(cell) = buf.cell_mut((x, area.y)) { cell.set_char('>').set_style(indicator_style); } @@ -149,9 +152,12 @@ impl<'a> BacktrackOverlay<'a> { // Content preview let max_content_width = (area.width as usize).saturating_sub((x - area.x) as usize + 12); let content_preview = Self::truncate_content(&snapshot.content, max_content_width); - // The selected snapshot is the violet accent on the gray bar. + // The selected snapshot is the banner green accent on the gray bar. let content_style = if is_selected { - Style::default().fg(ACCENT).bg(bg).bold() + Style::default() + .fg(ACCENT) + .bold() + .bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT).bg(bg) }; diff --git a/src/cortex-tui/src/widgets/command_palette/widget.rs b/src/cortex-tui/src/widgets/command_palette/widget.rs index 4ac8d23b..099d4e0d 100644 --- a/src/cortex-tui/src/widgets/command_palette/widget.rs +++ b/src/cortex-tui/src/widgets/command_palette/widget.rs @@ -241,10 +241,10 @@ impl<'a> CommandPalette<'a> { selected: bool, buf: &mut Buffer, ) { - // Selected rows: the violet accent on the dark gray bar — never + // Selected rows: the banner green accent on the dark gray bar — never // inverted. let style = if selected { - Style::default().fg(ACCENT).bg(SELECTION_BG) + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT).bg(SURFACE_0) }; diff --git a/src/cortex-tui/src/widgets/form/colors.rs b/src/cortex-tui/src/widgets/form/colors.rs index 2edf08f4..e10a660e 100644 --- a/src/cortex-tui/src/widgets/form/colors.rs +++ b/src/cortex-tui/src/widgets/form/colors.rs @@ -6,7 +6,7 @@ use cortex_core::style::{ use ratatui::prelude::Color; /// Colors used by the form modal — the gray chrome: charcoal panel, hairline -/// borders, white/dim copy, the violet accent on the focused field only. +/// borders, white/dim copy, the banner green accent on the focused field only. #[derive(Debug, Clone, Copy)] pub struct FormModalColors { pub background: Color, diff --git a/src/cortex-tui/src/widgets/form/modal.rs b/src/cortex-tui/src/widgets/form/modal.rs index 064ad90b..b566e9c4 100644 --- a/src/cortex-tui/src/widgets/form/modal.rs +++ b/src/cortex-tui/src/widgets/form/modal.rs @@ -91,7 +91,10 @@ impl<'a> Widget for FormModal<'a> { let is_focused = idx == self.state.focus_index; let label_style = if is_focused { - Style::default().fg(self.colors.accent).bold() + Style::default() + .fg(self.colors.accent) + .bg(cortex_core::style::TEXT) + .bold() } else { Style::default().fg(self.colors.text) }; @@ -230,12 +233,12 @@ impl<'a> Widget for FormModal<'a> { let submit_x = inner_area.x + (inner_area.width.saturating_sub(submit_text.len() as u16)) / 2; - // The focused submit button is the violet accent on the gray bar — + // The focused submit button is the banner green accent on the gray bar — // never inverted onto the accent. let submit_style = if self.state.is_submit_focused() { Style::default() .fg(self.colors.accent) - .bg(self.colors.surface) + .bg(cortex_core::style::TEXT) .bold() } else { Style::default().fg(self.colors.text_dim) diff --git a/src/cortex-tui/src/widgets/help_browser/render.rs b/src/cortex-tui/src/widgets/help_browser/render.rs index 97d12fb9..6d149b12 100644 --- a/src/cortex-tui/src/widgets/help_browser/render.rs +++ b/src/cortex-tui/src/widgets/help_browser/render.rs @@ -1,8 +1,7 @@ //! Help browser widget rendering. use cortex_core::style::{ - ACCENT, BORDER, BORDER_FOCUS, ELECTRIC_BLUE, SELECTION_BG, SURFACE_1, TEXT, TEXT_DIM, - TEXT_MUTED, + ACCENT, BORDER, BORDER_FOCUS, ELECTRIC_BLUE, SURFACE_1, TEXT, TEXT_DIM, TEXT_MUTED, }; use ratatui::prelude::*; use ratatui::widgets::Widget; @@ -128,10 +127,10 @@ impl<'a> HelpBrowser<'a> { let is_selected = i == self.state.selected_section; let prefix = if is_selected { "> " } else { " " }; - // The focused section is the violet accent on the gray bar; the + // The focused section is the banner green accent on the gray bar; the // remembered one stays white, the rest dim. let style = if is_selected && is_focused { - Style::default().fg(ACCENT).bg(SELECTION_BG) + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT) } else if is_selected { Style::default().fg(TEXT) } else { diff --git a/src/cortex-tui/src/widgets/key_hints.rs b/src/cortex-tui/src/widgets/key_hints.rs index 2d09a605..10f33911 100644 --- a/src/cortex-tui/src/widgets/key_hints.rs +++ b/src/cortex-tui/src/widgets/key_hints.rs @@ -222,7 +222,7 @@ impl KeyHints { let mut spans = Vec::new(); let separator_style = Style::default().fg(self.colors.text_muted); - // Hint rows are uniformly dim in the gray chrome — violet stays on the + // Hint rows are uniformly dim in the gray chrome — banner green stays on the // focused selection only. let key_style = Style::default().fg(self.colors.text_dim); let desc_style = Style::default().fg(self.colors.text_dim); diff --git a/src/cortex-tui/src/widgets/mention_popup.rs b/src/cortex-tui/src/widgets/mention_popup.rs index 535aecc8..641070c9 100644 --- a/src/cortex-tui/src/widgets/mention_popup.rs +++ b/src/cortex-tui/src/widgets/mention_popup.rs @@ -144,7 +144,10 @@ impl<'a> MentionPopup<'a> { // Selection indicator if is_selected { - let style = Style::default().fg(ACCENT).bg(bg).bold(); + let style = Style::default() + .fg(ACCENT) + .bold() + .bg(cortex_core::style::TEXT); if let Some(cell) = buf.cell_mut((x, area.y)) { cell.set_char('>').set_style(style); } @@ -167,9 +170,12 @@ impl<'a> MentionPopup<'a> { // File path let path_str = path.to_string_lossy(); - // The selected path is the violet accent on the gray bar. + // The selected path is the banner green accent on the gray bar. let style = if is_selected { - Style::default().fg(ACCENT).bg(bg).bold() + Style::default() + .fg(ACCENT) + .bold() + .bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT).bg(bg) }; diff --git a/src/cortex-tui/src/widgets/model_picker.rs b/src/cortex-tui/src/widgets/model_picker.rs index f6d8c000..4f9446be 100644 --- a/src/cortex-tui/src/widgets/model_picker.rs +++ b/src/cortex-tui/src/widgets/model_picker.rs @@ -380,7 +380,7 @@ impl ModelPicker<'_> { let is_selected = start + i == self.state.selected; - // Selection highlight: the violet label on the dark gray bar — + // Selection highlight: the banner green label on the dark gray bar — // never inverted onto the accent. let (bg, fg) = if is_selected { (SELECTION_BG, ACCENT) diff --git a/src/cortex-tui/src/widgets/scrollable_dropdown.rs b/src/cortex-tui/src/widgets/scrollable_dropdown.rs index 589b25b8..93f22d77 100644 --- a/src/cortex-tui/src/widgets/scrollable_dropdown.rs +++ b/src/cortex-tui/src/widgets/scrollable_dropdown.rs @@ -280,12 +280,12 @@ impl<'a> ScrollableDropdown<'a> { } // Label - // The selected label is the violet accent on the gray bar. + // The selected label is the banner green accent on the gray bar. let label_style = if is_selected { Style::default() .fg(ACCENT) - .bg(bg) .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT).bg(bg) }; @@ -329,7 +329,7 @@ impl<'a> ScrollableDropdown<'a> { // Selection indicator if is_selected { - let indicator_style = Style::default().fg(ACCENT).bg(bg); + let indicator_style = Style::default().fg(ACCENT).bg(cortex_core::style::TEXT); if let Some(cell) = buf.cell_mut((area.x, area.y)) { cell.set_char('>').set_style(indicator_style); } diff --git a/src/cortex-tui/src/widgets/selection_list.rs b/src/cortex-tui/src/widgets/selection_list.rs index d4a2275b..83ca53b2 100644 --- a/src/cortex-tui/src/widgets/selection_list.rs +++ b/src/cortex-tui/src/widgets/selection_list.rs @@ -522,7 +522,7 @@ impl SelectionList { buf: &mut Buffer, ) { // Determine styles based on selection and disabled state. The - // selected row is the one violet accent — caret and label — on the dark + // selected row is the one banner green accent — caret and label — on the dark // gray bar, never inverted; other rows stay white. let (bg, fg, prefix_fg) = if is_selected { (SELECTION_BG, ACCENT, ACCENT) diff --git a/src/cortex-tui/src/widgets/settings_modal.rs b/src/cortex-tui/src/widgets/settings_modal.rs index 1f19edaf..b412a215 100644 --- a/src/cortex-tui/src/widgets/settings_modal.rs +++ b/src/cortex-tui/src/widgets/settings_modal.rs @@ -479,7 +479,7 @@ const THEME_OPTIONS: &[(&str, &str, &str)] = &[ ( "dark", "Cortex Night", - "Default inky chrome · violet on focus only", + "Default inky chrome · banner green on focus only", ), ("light", "Cortex Day", "Light chrome for bright rooms"), ("ocean_dark", "Ocean Dark", "Deep blue and cyan accents"), @@ -780,7 +780,7 @@ impl Widget for SettingsModal<'_> { ); // Search let search_style = if self.state.search_focused { - Style::default().fg(ACCENT) + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT_DIM) }; @@ -870,7 +870,7 @@ fn paint_rows(inner: Rect, buf: &mut Buffer, state: &SettingsModalState) { } let marker = "▸ "; let marker_style = if focused { - Style::default().fg(ACCENT).bg(SELECTION_BG) + Style::default().fg(ACCENT).bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT_DIM) }; @@ -977,8 +977,8 @@ fn paint_theme_submenu(inner: Rect, buf: &mut Buffer, state: &SettingsModalState let lab_style = if focused { Style::default() .fg(ACCENT) - .bg(SELECTION_BG) .add_modifier(Modifier::BOLD) + .bg(cortex_core::style::TEXT) } else { Style::default().fg(TEXT) }; From 77122e0e948cd7f92628f8371c4a02f5370b62c6 Mon Sep 17 00:00:00 2001 From: echobt <154886644+echobt@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:51:12 +0000 Subject: [PATCH 2/3] fix(tui): reuse focus styles and dropdown rendering Remove source-quality regressions without changing the banner green palette or its accessible focus backing. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- src/cortex-tui/src/lock_boards.rs | 84 ++++------------ src/cortex-tui/src/lock_proof.rs | 17 +--- src/cortex-tui/src/widgets/autocomplete.rs | 96 ++----------------- .../src/widgets/scrollable_dropdown.rs | 28 ++++-- 4 files changed, 52 insertions(+), 173 deletions(-) diff --git a/src/cortex-tui/src/lock_boards.rs b/src/cortex-tui/src/lock_boards.rs index f7b5f982..34d4a2f5 100644 --- a/src/cortex-tui/src/lock_boards.rs +++ b/src/cortex-tui/src/lock_boards.rs @@ -8,8 +8,8 @@ use cortex_core::markdown::{TableBuilder, render_table}; use cortex_core::style::{ - ACCENT, DIFF_ADD, ERROR, HAIRLINE, PANEL_BG, SELECTION_BG, SUCCESS, SURFACE_2, TEXT, TEXT_DIM, - THINKING, USER_TURN_BG, WARNING, + ACCENT, CortexStyle, DIFF_ADD, ERROR, HAIRLINE, PANEL_BG, SELECTION_BG, SUCCESS, SURFACE_2, + TEXT, TEXT_DIM, THINKING, USER_TURN_BG, WARNING, }; use ratatui::buffer::Buffer; use ratatui::layout::Rect; @@ -250,10 +250,7 @@ fn paint_composer(area: Rect, buf: &mut Buffer, y: u16, composer: Composer<'_>) } let prefix = if i == 0 { "> " } else { " " }; let prefix_style = if i == 0 { - Style::default() - .fg(ACCENT) - .add_modifier(Modifier::BOLD) - .bg(cortex_core::style::TEXT) + CortexStyle::selected().add_modifier(Modifier::BOLD) } else { Style::default().fg(TEXT) }; @@ -459,7 +456,7 @@ fn picker_option( } } let marker_style = if selected { - Style::default().fg(ACCENT).bg(cortex_core::style::TEXT) + CortexStyle::selected() } else { Style::default().fg(TEXT_DIM) }; @@ -471,10 +468,7 @@ fn picker_option( } let indent = (x - area.x) as usize; let label_style = if selected { - Style::default() - .fg(ACCENT) - .add_modifier(Modifier::BOLD) - .bg(cortex_core::style::TEXT) + CortexStyle::selected().add_modifier(Modifier::BOLD) } else { Style::default().fg(TEXT) }; @@ -926,11 +920,8 @@ fn board_palette(area: Rect, buf: &mut Buffer) { let (marker, marker_style, cmd_style) = if selected { ( "> ", - Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), - Style::default() - .fg(ACCENT) - .add_modifier(Modifier::BOLD) - .bg(cortex_core::style::TEXT), + CortexStyle::selected(), + CortexStyle::selected().add_modifier(Modifier::BOLD), ) } else { ( @@ -1446,7 +1437,7 @@ fn board_permission(area: Rect, buf: &mut Buffer) { fill_row(buf, area, y, SELECTION_BG); } let style = if *selected { - Style::default().fg(ACCENT).bg(cortex_core::style::TEXT) + CortexStyle::selected() } else { Style::default().fg(TEXT) }; @@ -1549,10 +1540,7 @@ fn board_plan(area: Rect, buf: &mut Buffer) { area.x + 4, y, part, - Style::default() - .fg(ACCENT) - .add_modifier(Modifier::BOLD) - .bg(cortex_core::style::TEXT), + CortexStyle::selected().add_modifier(Modifier::BOLD), ); } } @@ -2222,10 +2210,7 @@ fn board_files(area: Rect, buf: &mut Buffer) { } let mut x = area.x; let (marker, marker_style) = if selected { - ( - "> ", - Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), - ) + ("> ", CortexStyle::selected()) } else { ("· ", Style::default().fg(TEXT_DIM)) }; @@ -2415,10 +2400,7 @@ fn board_jobs(area: Rect, buf: &mut Buffer) { Color::Reset }; let (marker, marker_style) = if job.selected { - ( - "> ", - Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), - ) + ("> ", CortexStyle::selected()) } else { ("· ", Style::default().fg(TEXT_DIM)) }; @@ -2430,10 +2412,7 @@ fn board_jobs(area: Rect, buf: &mut Buffer) { Style::default().fg(job.icon_color).bg(bg), ); let title_style = if job.selected { - Style::default() - .fg(ACCENT) - .add_modifier(Modifier::BOLD) - .bg(cortex_core::style::TEXT) + CortexStyle::selected().add_modifier(Modifier::BOLD) } else { Style::default().fg(TEXT) }; @@ -2857,20 +2836,12 @@ fn board_config(area: Rect, buf: &mut Buffer) { let value_fit = first_fitting_line(value, w.saturating_sub(label.chars().count() + 4)); if *selected { fill_row(buf, area, y, SELECTION_BG); - buf.set_string( - area.x, - y, - "> ", - Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), - ); + buf.set_string(area.x, y, "> ", CortexStyle::selected()); buf.set_string( area.x + 2, y, &label, - Style::default() - .fg(ACCENT) - .add_modifier(Modifier::BOLD) - .bg(cortex_core::style::TEXT), + CortexStyle::selected().add_modifier(Modifier::BOLD), ); // The selected value keeps its column gap and never ends on a // dangling `·` when the `⏎ edit` affordance takes the right edge. @@ -3199,10 +3170,7 @@ fn board_question(area: Rect, buf: &mut Buffer) { fill_row(buf, area, y, SELECTION_BG); } let style = if *selected { - Style::default() - .fg(ACCENT) - .add_modifier(Modifier::BOLD) - .bg(cortex_core::style::TEXT) + CortexStyle::selected().add_modifier(Modifier::BOLD) } else { Style::default().fg(TEXT) }; @@ -3243,20 +3211,12 @@ fn board_skills(area: Rect, buf: &mut Buffer) { let desc_fit = ellipsis_fit(desc, w.saturating_sub(cmd.len() + 4)); if selected { fill_row(buf, area, y, SELECTION_BG); - buf.set_string( - area.x, - y, - "> ", - Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), - ); + buf.set_string(area.x, y, "> ", CortexStyle::selected()); buf.set_string( area.x + 2, y, cmd, - Style::default() - .fg(ACCENT) - .add_modifier(Modifier::BOLD) - .bg(cortex_core::style::TEXT), + CortexStyle::selected().add_modifier(Modifier::BOLD), ); buf.set_string( area.x + 2 + cmd.len() as u16 + 2, @@ -3892,19 +3852,13 @@ fn board_multi_diff(area: Rect, buf: &mut Buffer) { } }; let (marker, marker_style) = if selected { - ( - "> ", - Style::default().fg(ACCENT).bg(cortex_core::style::TEXT), - ) + ("> ", CortexStyle::selected()) } else { ("· ", Style::default().fg(TEXT_DIM)) }; buf.set_string(area.x, y, marker, marker_style); let path_style = if selected { - Style::default() - .fg(ACCENT) - .add_modifier(Modifier::BOLD) - .bg(cortex_core::style::TEXT) + CortexStyle::selected().add_modifier(Modifier::BOLD) } else { Style::default().fg(TEXT) }; diff --git a/src/cortex-tui/src/lock_proof.rs b/src/cortex-tui/src/lock_proof.rs index b63f5dc0..1778fc0f 100644 --- a/src/cortex-tui/src/lock_proof.rs +++ b/src/cortex-tui/src/lock_proof.rs @@ -823,7 +823,7 @@ mod tests { frame.ansi.contains(SELECTION_WASH), "{id} selection bar must be #262626" ); - let row = (0..40u16) + (0..40u16) .find(|y| { (0..120u16).any(|x| { frame.buffer[(x, *y)].symbol() == ">" @@ -833,14 +833,6 @@ mod tests { }) }) .unwrap_or_else(|| panic!("{id} has no banner green `>` row:\n{}", frame.plain)); - // The focused row carries the selection bar and a banner green caret. - let has_bar = - (0..120u16).any(|x| frame.buffer[(x, row)].style().bg == Some(SELECTION_BG)); - assert!( - has_bar, - "{id} selection bar missing:\n{}", - row_text(&frame.buffer, row) - ); } } @@ -1007,12 +999,7 @@ mod tests { #[test] fn banned_colors_never_painted() { - // The interim banner green highlight is gone with the mint one: no scene - // paints banner green, the old `#221A38` retired violet wash, the mint pair, the old - // brand green, or the navy wash — the host terminal owns the - // background and the banner green lives on the focused selection alone. - // Mint `#00F5D4` / `#1A3330` never painted. Selection bar `#221A38` - // is allowed; inverted accent as a background is not (checked above). + // Retired mint, brand green, and navy colors must never paint. const BANNED: [&str; 4] = ["0;245;212", "26;51;48", "0;255;163", "10;22;40"]; for id in lock_scene_ids() { for size in SIZES { diff --git a/src/cortex-tui/src/widgets/autocomplete.rs b/src/cortex-tui/src/widgets/autocomplete.rs index 726b843e..5c51cdf9 100644 --- a/src/cortex-tui/src/widgets/autocomplete.rs +++ b/src/cortex-tui/src/widgets/autocomplete.rs @@ -16,8 +16,9 @@ //! frame.render_widget(popup, area); //! ``` +use super::scrollable_dropdown::ScrollableDropdown; use crate::app::{AutocompleteItem, AutocompleteState, AutocompleteTrigger}; -use cortex_core::style::{ACCENT, HAIRLINE, SELECTION_BG, SURFACE_1, TEXT, TEXT_DIM, TEXT_MUTED}; +use cortex_core::style::{HAIRLINE, SURFACE_1, TEXT, TEXT_MUTED}; use ratatui::prelude::*; use ratatui::widgets::{ Block, Borders, Clear, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget, @@ -101,90 +102,6 @@ impl<'a> AutocompletePopup<'a> { (width, height) } - /// Renders a single item. - fn render_item( - &self, - item: &AutocompleteItem, - is_selected: bool, - area: Rect, - buf: &mut Buffer, - ) { - // Background - let bg = if is_selected { SELECTION_BG } else { SURFACE_1 }; - for x in area.x..area.x + area.width { - if let Some(cell) = buf.cell_mut((x, area.y)) { - cell.set_bg(bg); - } - } - - let mut x = area.x + 1; - - // Icon - if item.icon != '\0' { - let icon_style = Style::default().fg(TEXT_DIM).bg(bg); - if let Some(cell) = buf.cell_mut((x, area.y)) { - cell.set_char(item.icon).set_style(icon_style); - } - x += 2; - } - - // Label - // The selected label is the banner green accent on the gray bar. - let label_style = if is_selected { - Style::default() - .fg(ACCENT) - .add_modifier(Modifier::BOLD) - .bg(cortex_core::style::TEXT) - } else { - Style::default().fg(TEXT).bg(bg) - }; - - for ch in item.label.chars() { - if x >= area.x + area.width - 1 { - break; - } - if let Some(cell) = buf.cell_mut((x, area.y)) { - cell.set_char(ch).set_style(label_style); - } - x += 1; - } - - // Description (if there's room) - if !item.description.is_empty() && x < area.x + area.width - 5 { - // Add separator - let sep_style = Style::default().fg(TEXT_MUTED).bg(bg); - for ch in " - ".chars() { - if x >= area.x + area.width - 1 { - break; - } - if let Some(cell) = buf.cell_mut((x, area.y)) { - cell.set_char(ch).set_style(sep_style); - } - x += 1; - } - - // Description text - let desc_style = Style::default().fg(TEXT_DIM).bg(bg); - for ch in item.description.chars() { - if x >= area.x + area.width - 1 { - break; - } - if let Some(cell) = buf.cell_mut((x, area.y)) { - cell.set_char(ch).set_style(desc_style); - } - x += 1; - } - } - - // Selection indicator - if is_selected { - let indicator_style = Style::default().fg(ACCENT).bg(cortex_core::style::TEXT); - if let Some(cell) = buf.cell_mut((area.x, area.y)) { - cell.set_char('>').set_style(indicator_style); - } - } - } - /// Gets the title based on trigger type. fn get_title(&self) -> &'static str { match self.state.trigger { @@ -253,7 +170,14 @@ impl Widget for AutocompletePopup<'_> { }; let is_selected = self.state.scroll_offset + i == self.state.selected; - self.render_item(item, is_selected, item_area, buf); + ScrollableDropdown::render_item( + &item.label, + &item.description, + item.icon, + is_selected, + item_area, + buf, + ); } // Render scrollbar if needed diff --git a/src/cortex-tui/src/widgets/scrollable_dropdown.rs b/src/cortex-tui/src/widgets/scrollable_dropdown.rs index 93f22d77..c0c60c7c 100644 --- a/src/cortex-tui/src/widgets/scrollable_dropdown.rs +++ b/src/cortex-tui/src/widgets/scrollable_dropdown.rs @@ -259,7 +259,14 @@ impl<'a> ScrollableDropdown<'a> { } /// Renders a single item. - fn render_item(&self, item: &DropdownItem, is_selected: bool, area: Rect, buf: &mut Buffer) { + pub(super) fn render_item( + label: &str, + description: &str, + icon: char, + is_selected: bool, + area: Rect, + buf: &mut Buffer, + ) { // Background let bg = if is_selected { SELECTION_BG } else { SURFACE_1 }; for x in area.x..area.x + area.width { @@ -271,10 +278,10 @@ impl<'a> ScrollableDropdown<'a> { let mut x = area.x + 1; // Icon - if item.icon != '\0' { + if icon != '\0' { let icon_style = Style::default().fg(TEXT_DIM).bg(bg); if let Some(cell) = buf.cell_mut((x, area.y)) { - cell.set_char(item.icon).set_style(icon_style); + cell.set_char(icon).set_style(icon_style); } x += 2; } @@ -290,7 +297,7 @@ impl<'a> ScrollableDropdown<'a> { Style::default().fg(TEXT).bg(bg) }; - for ch in item.label.chars() { + for ch in label.chars() { if x >= area.x + area.width - 1 { break; } @@ -301,7 +308,7 @@ impl<'a> ScrollableDropdown<'a> { } // Description (if there's room) - if !item.description.is_empty() && x < area.x + area.width - 5 { + if !description.is_empty() && x < area.x + area.width - 5 { // Add separator let sep_style = Style::default().fg(TEXT_MUTED).bg(bg); for ch in " - ".chars() { @@ -316,7 +323,7 @@ impl<'a> ScrollableDropdown<'a> { // Description text let desc_style = Style::default().fg(TEXT_DIM).bg(bg); - for ch in item.description.chars() { + for ch in description.chars() { if x >= area.x + area.width - 1 { break; } @@ -437,7 +444,14 @@ impl Widget for ScrollableDropdown<'_> { }; let is_selected = self.scroll_offset + i == self.selected; - self.render_item(item, is_selected, item_area, buf); + Self::render_item( + &item.label, + &item.description, + item.icon, + is_selected, + item_area, + buf, + ); } // Render scrollbar From d30e3c29c82d34265ad87927a3d31d837113037b Mon Sep 17 00:00:00 2001 From: echobt <154886644+echobt@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:11:45 +0000 Subject: [PATCH 3/3] fix(tui): preserve focus contrast across pickers and themes Use readable backing for selected labels and derive themed focus backing from accent luminance. Cover focused cells across built-in themes. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- src/cortex-tui/src/modal/models.rs | 39 ++++++++-- .../src/modal/sessions/rendering.rs | 53 +++++++++++-- src/cortex-tui/src/ui/colors.rs | 37 +++++++++ src/cortex-tui/src/views/question_prompt.rs | 76 ++++++++++++++++++- src/cortex-tui/src/widgets/form/modal.rs | 4 +- src/cortex-tui/src/widgets/form/tests.rs | 36 +++++++++ src/cortex-tui/src/widgets/model_picker.rs | 41 ++++++++-- src/cortex-tui/src/widgets/selection_list.rs | 40 ++++++++-- 8 files changed, 298 insertions(+), 28 deletions(-) diff --git a/src/cortex-tui/src/modal/models.rs b/src/cortex-tui/src/modal/models.rs index cc3e5bf2..37e5ca87 100644 --- a/src/cortex-tui/src/modal/models.rs +++ b/src/cortex-tui/src/modal/models.rs @@ -4,7 +4,7 @@ //! Models are grouped by provider with section headers for easy navigation. use cortex_core::style::{ - ACCENT, BORDER, CYAN_PRIMARY, SELECTION_BG, SURFACE_0, TEXT, TEXT_DIM, TEXT_MUTED, + BORDER, CYAN_PRIMARY, CortexStyle, SELECTION_BG, SURFACE_0, TEXT, TEXT_DIM, TEXT_MUTED, }; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::buffer::Buffer; @@ -327,9 +327,9 @@ impl ModelsModal { is_selected: bool, ) { // Determine styles - // Selected rows: the banner green accent on the dark gray bar — never inverted. + // Selected glyphs get a light backing; the rest of the row stays dark gray. let (bg, fg, prefix_fg) = if is_selected { - (SELECTION_BG, ACCENT, ACCENT) + (SELECTION_BG, TEXT, TEXT) } else { (SURFACE_0, TEXT, TEXT_DIM) }; @@ -343,11 +343,24 @@ impl ModelsModal { // Selection prefix: ">" for selected, " " for others let prefix = if is_selected { ">" } else { " " }; - buf.set_string(col, y, prefix, Style::default().fg(prefix_fg).bg(bg)); + buf.set_string( + col, + y, + prefix, + if is_selected { + CortexStyle::selected() + } else { + Style::default().fg(prefix_fg).bg(bg) + }, + ); col += 2; // Model name — English product name, never the served slug. - let name_style = Style::default().fg(fg).bg(bg); + let name_style = if is_selected { + CortexStyle::selected() + } else { + Style::default().fg(fg).bg(bg) + }; let display = crate::ui::text_utils::model_display_name(&model.name); let max_name_len = 35.min(width.saturating_sub(30) as usize); let truncated_name = if display.len() > max_name_len && max_name_len > 3 { @@ -722,4 +735,20 @@ mod tests { let _action_bar = modal.build_action_bar(); // ActionBar is created successfully (basic smoke test) } + #[test] + fn focus_label_and_caret_cells_have_contrasting_backing() { + let model = ModelInfo::new("test", "Test", "Cortex"); + let modal = ModelsModal::new(vec![model.clone()], None); + for width in [40, 120] { + let mut buf = Buffer::empty(Rect::new(0, 0, width, 12)); + modal.render_model_row(0, 0, width, &mut buf, &model, true); + assert_eq!(buf[(0, 0)].symbol(), ">"); + assert_eq!(buf[(2, 0)].symbol(), "T"); + for x in [0, 2, 3, 4, 5] { + assert_eq!(buf[(x, 0)].fg, cortex_core::style::ACCENT); + assert_eq!(buf[(x, 0)].bg, TEXT); + } + assert_eq!(buf[(1, 0)].bg, SELECTION_BG); + } + } } diff --git a/src/cortex-tui/src/modal/sessions/rendering.rs b/src/cortex-tui/src/modal/sessions/rendering.rs index bde63582..cbf79897 100644 --- a/src/cortex-tui/src/modal/sessions/rendering.rs +++ b/src/cortex-tui/src/modal/sessions/rendering.rs @@ -5,8 +5,8 @@ use ratatui::layout::Rect; use ratatui::style::{Color, Modifier, Style}; use cortex_core::style::{ - ACCENT, BORDER, CYAN_PRIMARY, SELECTION_BG, SURFACE_0, SURFACE_1, TEXT, TEXT_DIM, TEXT_MUTED, - YELLOW, + BORDER, CYAN_PRIMARY, CortexStyle, SELECTION_BG, SURFACE_0, SURFACE_1, TEXT, TEXT_DIM, + TEXT_MUTED, YELLOW, }; use super::session_action::SessionAction; @@ -82,9 +82,9 @@ pub fn render_search_bar(search_query: &str, area: Rect, buf: &mut Buffer) { /// Render a single session row. pub fn render_session_row(session: &SessionInfo, is_selected: bool, area: Rect, buf: &mut Buffer) { - // Selected rows: the banner green accent on the dark gray bar — never inverted. + // Selected glyphs get a light backing; the rest of the row stays dark gray. let (bg, fg, prefix_fg) = if is_selected { - (SELECTION_BG, ACCENT, ACCENT) + (SELECTION_BG, TEXT, TEXT) } else { (SURFACE_0, TEXT, TEXT_DIM) }; @@ -98,11 +98,24 @@ pub fn render_session_row(session: &SessionInfo, is_selected: bool, area: Rect, // Selection indicator let prefix = if is_selected { ">" } else { " " }; - buf.set_string(col, area.y, prefix, Style::default().fg(prefix_fg).bg(bg)); + buf.set_string( + col, + area.y, + prefix, + if is_selected { + CortexStyle::selected() + } else { + Style::default().fg(prefix_fg).bg(bg) + }, + ); col += 2; // Session name (left-aligned) - let name_style = Style::default().fg(fg).bg(bg); + let name_style = if is_selected { + CortexStyle::selected() + } else { + Style::default().fg(fg).bg(bg) + }; // Build metadata: "2h ago 15 msgs Cortex Mini 1" let time_ago = session.relative_time(); @@ -218,3 +231,31 @@ pub fn render_confirmation( buf.set_string(warn_x, area.y + 4, warn, warn_style); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn focus_label_and_caret_cells_have_contrasting_backing() { + let session = SessionInfo::new( + std::path::PathBuf::from("/test"), + "Test", + "test", + chrono::Utc::now(), + 0, + ); + for width in [40, 120] { + let area = Rect::new(0, 0, width, 12); + let mut buf = Buffer::empty(area); + render_session_row(&session, true, Rect::new(0, 0, width, 1), &mut buf); + assert_eq!(buf[(0, 0)].symbol(), ">"); + assert_eq!(buf[(2, 0)].symbol(), "T"); + for x in [0, 2, 3, 4, 5] { + assert_eq!(buf[(x, 0)].fg, cortex_core::style::ACCENT); + assert_eq!(buf[(x, 0)].bg, TEXT); + } + assert_eq!(buf[(1, 0)].bg, SELECTION_BG); + } + } +} diff --git a/src/cortex-tui/src/ui/colors.rs b/src/cortex-tui/src/ui/colors.rs index 15289589..dc7182f3 100644 --- a/src/cortex-tui/src/ui/colors.rs +++ b/src/cortex-tui/src/ui/colors.rs @@ -105,6 +105,28 @@ pub struct AdaptiveColors { pub background: Color, } +/// Back focused RGB accent glyphs with a contrasting neutral surface. +pub fn focus_background(accent: Color) -> Color { + // ponytail: built-in accents are RGB; resolve terminal palettes if indexed themes ship. + let Color::Rgb(r, g, b) = accent else { + return cortex_core::style::TEXT; + }; + let linear = |channel: u8| { + let value = f64::from(channel) / 255.0; + if value <= 0.04045 { + value / 12.92 + } else { + ((value + 0.055) / 1.055).powf(2.4) + } + }; + let luminance = 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b); + if luminance > 0.169 { + Color::Black + } else { + cortex_core::style::TEXT + } +} + impl AdaptiveColors { /// Create colors by auto-detecting terminal background pub fn from_terminal() -> Self { @@ -365,4 +387,19 @@ mod tests { assert!(themes.contains(&"ocean_dark")); assert!(themes.contains(&"monokai")); } + #[test] + fn focus_backing_matches_builtin_accent_luminance() { + for name in AdaptiveColors::available_themes() { + let colors = AdaptiveColors::from_theme_name(name); + let expected = match *name { + "ocean_dark" | "monokai" => Color::Black, + _ => cortex_core::style::TEXT, + }; + assert_eq!( + crate::ui::colors::focus_background(colors.accent), + expected, + "{name}" + ); + } + } } diff --git a/src/cortex-tui/src/views/question_prompt.rs b/src/cortex-tui/src/views/question_prompt.rs index f00549df..256ff16f 100644 --- a/src/cortex-tui/src/views/question_prompt.rs +++ b/src/cortex-tui/src/views/question_prompt.rs @@ -158,7 +158,10 @@ impl QuestionPromptView<'_> { // The active tab is the focused selection: banner green on the gray bar, // never inverted onto the accent. Answered tabs read white. let (fg, bg) = if is_active { - (colors.accent, cortex_core::style::TEXT) + ( + colors.accent, + crate::ui::colors::focus_background(colors.accent), + ) } else if is_hovered { (colors.text, colors.user_bg) } else { @@ -193,7 +196,10 @@ impl QuestionPromptView<'_> { let is_hovered = self.hovered_tab == Some(self.state.request.questions.len()); let (fg, bg) = if is_active { - (colors.accent, cortex_core::style::TEXT) + ( + colors.accent, + crate::ui::colors::focus_background(colors.accent), + ) } else if is_hovered { (colors.text, colors.user_bg) } else { @@ -267,7 +273,10 @@ impl QuestionPromptView<'_> { // Focused option: banner green on the gray selection bar; a picked option // stays white — its `✓` carries the green. let (fg, bg) = if is_selected { - (colors.accent, cortex_core::style::TEXT) + ( + colors.accent, + crate::ui::colors::focus_background(colors.accent), + ) } else if is_hovered { (colors.text, colors.user_bg) } else { @@ -330,7 +339,10 @@ impl QuestionPromptView<'_> { let label = "Type your own answer"; let (fg, bg) = if is_selected { - (colors.accent, cortex_core::style::TEXT) + ( + colors.accent, + crate::ui::colors::focus_background(colors.accent), + ) } else if is_hovered { (colors.text, colors.user_bg) } else { @@ -687,4 +699,60 @@ mod tests { .expect("title"); assert_eq!(buf[(title_x, title_y)].style().fg, Some(TEXT)); } + #[test] + fn focus_tabs_and_options_are_accessible_in_every_builtin_theme() { + for name in AdaptiveColors::available_themes() { + let colors = AdaptiveColors::from_theme_name(name); + let expected_bg = match *name { + "ocean_dark" | "monokai" => Color::Black, + _ => TEXT, + }; + for width in [40, 120] { + let mut req = request(); + req.questions[0].allow_custom = true; + req.questions[0].question = "Home".into(); + req.questions.push(req.questions[0].clone()); + let mut state = QuestionState::new(req); + for confirm in [false, true] { + state.on_confirm_tab = confirm; + let view = QuestionPromptView::new(&state).with_colors(colors.clone()); + let area = Rect::new(0, 0, width, 12); + let mut buf = Buffer::empty(area); + view.render_tabs(&Rect::new(0, 0, width, 1), &mut buf, &colors); + let label = if confirm { + "Confirm".to_string() + } else { + state.get_header(0) + }; + let row: String = (0..width).map(|x| buf[(x, 0)].symbol()).collect(); + let start = row.find(&label).expect("active tab label") as u16; + for x in start..start + label.chars().count() as u16 { + assert_eq!(buf[(x, 0)].fg, colors.accent, "{name}: {label}"); + assert_eq!(buf[(x, 0)].bg, expected_bg, "{name}: {label}"); + } + } + state.on_confirm_tab = false; + for selected in [0, 2] { + state.selected_index[0] = selected; + let view = QuestionPromptView::new(&state).with_colors(colors.clone()); + let mut buf = Buffer::empty(Rect::new(0, 0, width, 12)); + view.render_question( + &Rect::new(0, 0, width, 1), + &Rect::new(0, 2, width, 10), + &mut buf, + &colors, + ); + let focused: Vec<_> = buf + .content + .iter() + .filter(|cell| cell.fg == colors.accent && cell.symbol() != " ") + .collect(); + assert!(!focused.is_empty(), "{name}: option {selected}"); + for cell in focused { + assert_eq!(cell.bg, expected_bg, "{name}: option {selected}"); + } + } + } + } + } } diff --git a/src/cortex-tui/src/widgets/form/modal.rs b/src/cortex-tui/src/widgets/form/modal.rs index b566e9c4..cd506a9c 100644 --- a/src/cortex-tui/src/widgets/form/modal.rs +++ b/src/cortex-tui/src/widgets/form/modal.rs @@ -93,7 +93,7 @@ impl<'a> Widget for FormModal<'a> { let label_style = if is_focused { Style::default() .fg(self.colors.accent) - .bg(cortex_core::style::TEXT) + .bg(crate::ui::colors::focus_background(self.colors.accent)) .bold() } else { Style::default().fg(self.colors.text) @@ -238,7 +238,7 @@ impl<'a> Widget for FormModal<'a> { let submit_style = if self.state.is_submit_focused() { Style::default() .fg(self.colors.accent) - .bg(cortex_core::style::TEXT) + .bg(crate::ui::colors::focus_background(self.colors.accent)) .bold() } else { Style::default().fg(self.colors.text_dim) diff --git a/src/cortex-tui/src/widgets/form/tests.rs b/src/cortex-tui/src/widgets/form/tests.rs index 2f8124db..c3d793a0 100644 --- a/src/cortex-tui/src/widgets/form/tests.rs +++ b/src/cortex-tui/src/widgets/form/tests.rs @@ -375,4 +375,40 @@ mod tests { assert_eq!(state.fields[0].value, "AX🎉B"); assert_eq!(state.fields[0].cursor_pos, 2); } + #[test] + fn focus_field_and_submit_backing_tracks_the_accent() { + use crate::ui::colors::AdaptiveColors; + use crate::widgets::form::colors::FormModalColors; + use ratatui::{buffer::Buffer, style::Color, widgets::Widget}; + for name in AdaptiveColors::available_themes() { + let accent = AdaptiveColors::from_theme_name(name).accent; + let expected = match *name { + "ocean_dark" | "monokai" => Color::Black, + _ => cortex_core::style::TEXT, + }; + let mut state = FormState::new("Test", "test", vec![FormField::text("name", "Name")]); + for focus in [0, 1] { + state.focus_index = focus; + for (width, height) in [(60, 20), (120, 40)] { + let area = Rect::new(0, 0, width, height); + let mut buf = Buffer::empty(area); + FormModal::new(&state) + .colors(FormModalColors { + accent, + ..Default::default() + }) + .render(area, &mut buf); + let focused: Vec<_> = buf + .content + .iter() + .filter(|cell| cell.fg == accent && cell.symbol() != " ") + .collect(); + assert!(!focused.is_empty(), "{name}: focus {focus}"); + for cell in focused { + assert_eq!(cell.bg, expected, "{name}: focus {focus}"); + } + } + } + } + } } diff --git a/src/cortex-tui/src/widgets/model_picker.rs b/src/cortex-tui/src/widgets/model_picker.rs index 4f9446be..74921d5e 100644 --- a/src/cortex-tui/src/widgets/model_picker.rs +++ b/src/cortex-tui/src/widgets/model_picker.rs @@ -16,7 +16,7 @@ use crate::providers::models::{ModelInfo, get_models_for_provider, get_popular_models}; use cortex_core::style::{ - ACCENT, BORDER_FOCUS, SELECTION_BG, SURFACE_0, TEXT, TEXT_DIM, TEXT_MUTED, + BORDER_FOCUS, CortexStyle, SELECTION_BG, SURFACE_0, TEXT, TEXT_DIM, TEXT_MUTED, }; use ratatui::prelude::*; use ratatui::widgets::{ @@ -380,10 +380,9 @@ impl ModelPicker<'_> { let is_selected = start + i == self.state.selected; - // Selection highlight: the banner green label on the dark gray bar — - // never inverted onto the accent. + // Keep the row dark gray; back the focused label separately. let (bg, fg) = if is_selected { - (SELECTION_BG, ACCENT) + (SELECTION_BG, TEXT) } else { (SURFACE_0, TEXT) }; @@ -419,7 +418,16 @@ impl ModelPicker<'_> { } else { display }; - buf.set_string(x + 2, y, &name, Style::default().fg(fg).bg(bg)); + buf.set_string( + x + 2, + y, + &name, + if is_selected { + CortexStyle::selected() + } else { + Style::default().fg(fg).bg(bg) + }, + ); // Provider let provider_x = x + name_width as u16 + 3; @@ -587,4 +595,27 @@ mod tests { state.select_prev(); assert_eq!(state.selected, 0); } + #[test] + fn focus_label_cells_have_contrasting_backing() { + let mut state = ModelPickerState::new(); + state.filtered_models.push(ModelItem { + id: "test".into(), + name: "Test".into(), + provider: "cortex".into(), + context_window: None, + is_current: false, + is_popular: false, + }); + for width in [40, 120] { + let area = Rect::new(0, 0, width, 12); + let mut buf = Buffer::empty(area); + ModelPicker::new(&state).render_model_list(area, &mut buf); + assert_eq!(buf[(3, 0)].symbol(), "T"); + for x in 3..7 { + assert_eq!(buf[(x, 0)].fg, cortex_core::style::ACCENT); + assert_eq!(buf[(x, 0)].bg, TEXT); + } + assert_eq!(buf[(0, 0)].bg, SELECTION_BG); + } + } } diff --git a/src/cortex-tui/src/widgets/selection_list.rs b/src/cortex-tui/src/widgets/selection_list.rs index 83ca53b2..7be35451 100644 --- a/src/cortex-tui/src/widgets/selection_list.rs +++ b/src/cortex-tui/src/widgets/selection_list.rs @@ -31,7 +31,9 @@ //! frame.render_widget(&list, area); //! ``` -use cortex_core::style::{ACCENT, SELECTION_BG, SURFACE_0, SURFACE_1, TEXT, TEXT_DIM, TEXT_MUTED}; +use cortex_core::style::{ + CortexStyle, SELECTION_BG, SURFACE_0, SURFACE_1, TEXT, TEXT_DIM, TEXT_MUTED, +}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::buffer::Buffer; use ratatui::layout::Rect; @@ -522,10 +524,9 @@ impl SelectionList { buf: &mut Buffer, ) { // Determine styles based on selection and disabled state. The - // selected row is the one banner green accent — caret and label — on the dark - // gray bar, never inverted; other rows stay white. + // selected caret and label have a light backing on the dark gray row. let (bg, fg, prefix_fg) = if is_selected { - (SELECTION_BG, ACCENT, ACCENT) + (SELECTION_BG, TEXT, TEXT) } else if item.disabled { (SURFACE_0, TEXT_MUTED, TEXT_MUTED) } else { @@ -541,7 +542,16 @@ impl SelectionList { // Selection prefix: ">" for selected, " " for others let prefix = if is_selected { ">" } else { " " }; - buf.set_string(col, y, prefix, Style::default().fg(prefix_fg).bg(bg)); + buf.set_string( + col, + y, + prefix, + if is_selected { + CortexStyle::selected() + } else { + Style::default().fg(prefix_fg).bg(bg) + }, + ); col += 2; // Shortcut in brackets if present @@ -557,7 +567,9 @@ impl SelectionList { } // Item name - let name_style = if item.disabled { + let name_style = if is_selected { + CortexStyle::selected() + } else if item.disabled { Style::default().fg(fg).bg(bg).add_modifier(Modifier::DIM) } else { Style::default().fg(fg).bg(bg) @@ -894,4 +906,20 @@ mod tests { list.handle_key(KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL)); assert_eq!(list.selected_idx, 0); } + #[test] + fn focus_label_and_caret_cells_have_contrasting_backing() { + let item = SelectionItem::new("Test"); + let list = SelectionList::new(vec![item.clone()]); + for width in [40, 120] { + let mut buf = Buffer::empty(Rect::new(0, 0, width, 12)); + list.render_item(&item, true, 0, 0, width, &mut buf); + assert_eq!(buf[(0, 0)].symbol(), ">"); + assert_eq!(buf[(2, 0)].symbol(), "T"); + for x in [0, 2, 3, 4, 5] { + assert_eq!(buf[(x, 0)].fg, cortex_core::style::ACCENT); + assert_eq!(buf[(x, 0)].bg, TEXT); + } + assert_eq!(buf[(1, 0)].bg, SELECTION_BG); + } + } }