Repository navigation
Conversation
Add App-level infrastructure for Tab-swap between ItemsEditor and ColorMenu screens. Introduces activeWidgetId state for widget-level position tracking, handleTabSwap callback to toggle between 'items' and 'colors' screens, and handleWidgetHighlight callback to update the active widget. Passes onTabSwap, onWidgetHighlight, and initialWidgetId props to both editors. Adds optional prop definitions to ItemsEditorProps and ColorMenuProps interfaces.
Add onTabSwap optional callback to HandleNormalInputModeArgs and Tab key handling that invokes it when the selected widget supports colors. Separators and flex-separators are excluded from Tab activation.
- Destructure onTabSwap, onWidgetHighlight, and initialWidgetId props - Use lazy useState initializer to position cursor from initialWidgetId on mount - Add useEffect to track cursor position and call onWidgetHighlight - Pass onTabSwap through to handleNormalInputMode - Add Tab hint in help text, grayed out when widget is not colorable
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
… hidden when unavailable
Verify Tab key behavior: calls onTabSwap for colorable widgets, skips separators and flex-separators, and does nothing when onTabSwap is not provided.
|
Running into some issues with this one. If you have a powerline managed theme turned on, go to the widget editor and press Tab, it takes you in to edit the colors (which should not be allowed when a managed theme is active). Changing the colors has no effect (as expected when a theme is active). Pressing ESC while editing the colors takes you to the screen with the message "⚠ Colors are currently managed by the Powerline theme: Minimal", and pressing any key takes you back to the main menu with the 'Edit Colors' item highlighted. Throughout the TUI, the Esc key always takes you back to the previous screen, but pressing Tab to go to the color editor breaks that UX. I feel, at a bare minimum, going into color editing mode via Tab should take you out of color editing mode and back to the widget editor when either Tab or Esc is pressed to maintain the user's navigation context. If a powerline theme is active, an appropriate solution might be to prompt the user to customize the theme (using the same 'c' key functionality when on the theme list), while warning them that the theme's colors for each widget will be copied and then they can customize them. I'm not sure I fully like this flow though, as customizing a theme disables auto-applying the theme to newly added widgets. Everything added after customizing a theme gets the default, unthemed colors. So by presenting the Tab as a shortcut to easily customize widget colors, then prompting them to customize it at that point in the flow might confuse users why the theme they started with is no longer being applied. I'm open to suggestions on this, I like the quick access but it doesn't work well when a theme is active. |
When a non-custom powerline theme is active, the renderer ignores widget color fields entirely, so navigating to the color editor via Tab leads to edits that silently have no effect. Gate onTabSwap at the App level so both editors suppress the hint and keypress.
Fixed in cbcd811 — when a managed theme is active, onTabSwap is no longer passed to either editor, suppressing both the hint and the keypress. This is consistent with the existing blockIfPowerlineActive guard on the canonical color editing path. If a user wants per-widget color control, they can already customize the theme via Powerline Settings → Theme Selector → c, which bakes the theme colors into widget fields and switches to custom mode — Tab swap then becomes available. Did you want a different behaviour, or is this consistent with your intent? |
Tab already swaps back, so that works. For ESC, the current behaviour was deliberate — Tab is treated as a lateral jump between parallel editors, not a navigation that creates a back-stack. So ESC follows the canonical path for whichever editor you're in (ColorMenu → colorLines → main). The alternative would be to track that you arrived via Tab and have ESC return you to the originating editor. If we go that route, it would be a single breadcrumb — repeated Tab presses just toggle between editors, they don't accumulate, so you'd never need to ESC multiple times to unwind. Happy to implement either approach — do you have a preference? |
|
@sirmalloc - did you want me to implement the breadcrumbs as described? Or do you have other ideas? |
@hangie Hang tight for a couple days on this one. I wanted to modify the theme rendering pipeline first to allow keeping a theme on and overriding colors for individual widgets, then I was considering removing the Color editor entirely from the main menu in favor of making the widget editor the sole entry point into color customization. I think it flows better and if we can apply colors while themes are on it'll feel pretty natural no matter what mode the user is in. I'll probably keep the main menu item around for a couple weeks and direct people where to edit colors now so existing users aren't confused. Thanks for your patience on this one. |
4f7a07b to
ec28376
Compare
# Conflicts: # src/tui/App.tsx
|
Sorry I've been slow on this. Still on my radar, making time to get through all these PRs soon. |
|
@sirmalloc — checking in on this one. It's been a few weeks, so no rush, but wanted to see where your thinking landed. I've kept the branch current with main (merges clean, tests green). From what I can see, the theme-rendering changes — keeping a theme on while overriding per-widget colors — haven't landed yet, so the current cbcd811 guard (disabling Tab-swap under a managed theme) still holds for today's behaviour. Happy to treat this PR as a building block for the broader change you described, or to adjust it to fit whatever shape you land on. Do you want me to proceed with the breadcrumb ESC behaviour now, hold for your pipeline work, or rethink the entry point? |
# Conflicts: # src/tui/components/ItemsEditor.tsx # src/tui/components/items-editor/input-handlers.ts
|
@sirmalloc - any updates? |
@hangie I've been a bit busy lately with some career changes so I haven't been able to give the project proper attention aside from merging some low hanging fruit. Here's where I'm at, and if you want to try to update this PR to accommodate that, I'll be happy to merge it:
One hard requirement that should be codified into regression tests:
Again, sorry for dropping the ball on this one. Let me know if you want to discuss further, I'm open to suggestions. |
|
@sirmalloc — no worries at all on the timing, and thanks for laying out where you've landed. I'm glad to take this on and see it through. Your five points all make sense and I'm on board, including the details: ESC dropping back to the line selector, colour editing becoming a "Colour Editing mode" that keeps the widget editor's shape with TAB to switch back, the old top-level colour menu turning into a signpost, and the theme switcher prompting to keep/remove overrides when the theme changes. My managed-theme guard comes out as part of this — there'll be real behaviour behind it now. Here's the shape I intend for the colour model, since your hard requirement (no appearance change for existing themed configs) is what drives it:
Your keep/remove prompt on a theme change (point 5) then maps cleanly onto clearing pins. I'll follow up with a short written plan — data model, render precedence, the editing flow, and the regression-test shape — so you can gut-check it before I go deep. Happy to hop on a thread if that's easier around your schedule. Thanks again for getting this unblocked. |
|
@hangie Sounds good on all your points. I'd be happy to review either a full plan or just test the implementation, either way. |
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Per-channel pins let a widget's own colour override an active Powerline theme. Optional so existing settings load with no migration. Co-Authored-By: Claude Opus 4.8 <[email protected]>
A pinned foreground/background keeps the widget's own colour instead of the theme's; unpinned channels are unchanged. The theme colour index still advances for pinned widgets so sibling widgets' colours don't shift. preserveColors keeps precedence over the fg pin. Guarantee: an unpinned colour persisted before a theme was enabled stays dormant, so existing themed configs render identically (no migration). Co-Authored-By: Claude Opus 4.8 <[email protected]>
pinWidgetColor sets a channel's pin and non-destructively surfaces the existing colour (seeds only when unset). unpinWidgetColor removes the pin flag but keeps the colour (not an undo). clearAllPins unpins every widget. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Removes the managed-theme guard so the colour editor is reachable while a Powerline theme is active. Editing a colour (hex/ansi256/gradient/cycle) auto-pins that channel so it overrides the theme, and (p) explicitly pins/unpins the highlighted widget's current channel (surfacing its existing colour, seeding a default only when it has none). Help text shows (p)in/unpin under a theme. Co-Authored-By: Claude Opus 4.8 <[email protected]>
When the user commits a theme change and any widget has pinned colour overrides, prompt whether to keep them (carry to the new theme) or remove them (clearAllPins) so the new theme fully applies. No prompt when nothing is pinned or the theme is unchanged. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds a [PINNED] / [unpinned - theme applies] indicator to the current foreground/background row when a theme is active, so the editor is honest about pinned vs dormant. Fixes two phase-1 UX defects: no way to tell if a channel is pinned, and a dormant stored colour being shown as if effective. Co-Authored-By: Claude Opus 4.8 <[email protected]>
The top-level "Edit Colors" entry no longer opens the standalone color line-selector. It now lands on a notice screen explaining that color editing lives in the widget editor (Tab from a highlighted widget), with an action that jumps straight to the line selector for items. The 'colorLines'/'colors' screens are left in place - 'colors' is still reached via Tab from the widget editor - so the change is reversible. Main-menu navigation is extracted into getMainMenuScreenTarget() so the routing decision is unit-testable alongside the other App helpers. Co-Authored-By: Claude Opus 4.8 <[email protected]>
sirmalloc asked for escape from color editing to go back one menu to the
line selector. Color editing is now reached by pressing Tab inside the
widget editor, so "the line selector" is the one the widget editor backs
out to ("Select Line to Edit Items") - escape lands in the same place
whichever mode you were in, and the selected line is preserved.
Tab behaviour is unchanged; the toggle and the shared back target are
extracted as getTabSwapScreen()/getEditorBackScreen() so both are
covered by tests.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Nothing routes to the 'colorLines' screen any more - the main menu signposts the widget editor instead, and Tab/escape use the items line selector. Removing the screen leaves LineSelector's blockIfPowerlineActive prop (and the "colors are managed by the Powerline theme, press any key to go back" block it rendered) with no callers; that guard also contradicts this branch, which exists so colors can be pinned over an active theme. allowEditing goes the same way: the single remaining caller passes true, so the read-only variant of the line selector was unreachable too. Co-Authored-By: Claude Opus 4.8 <[email protected]>
The selector's two ink-driven tests advanced on 25ms sleeps, so they failed whenever the machine was busy - the preview test failed every time when the file ran in isolation, and both started failing in full-suite runs once more ink tests were added alongside them. Each step now polls for the state it is actually waiting on (the preview update, the remove-pins prompt, the selector closing) with a labelled timeout, so a stall names the step that stalled. A short settle remains before each follow-up keystroke: ink writes the frame before the next screen's input handler attaches, so a keypress sent the instant the frame appears is dropped. Co-Authored-By: Claude Opus 4.8 <[email protected]>
sirmalloc asked that color editing keep the widget editor's shape. The two editors built their rows separately, so the same widget rendered differently in each: "1. Model (merged→)" in one, a tinted "1: Model" in the other. Extract WidgetRow plus getWidgetRowLabel/getWidgetRowTags so both modes share numbering, indicator and the dim structure markers, and title the widget editor "Edit Line N [WIDGETS]" so the two modes share a title stem. ColorMenu adopts the same row in the following commit. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Color editing now reads as the same screen in a different mode rather than a separate one: - title is "Edit Line N [COLORS]", matching the widget editor's stem, with [BACKGROUND] as a sub-mode tag - rows come from the shared WidgetRow, so they carry the same numbering, indicator and structure markers, tinted with the color they hold - rows are numbered by position in the full line, so a widget keeps its number across modes and skipped widgets leave a gap - ink-select-input is gone; ColorMenu owns its up/down navigation, which also removes the remount-on-highlight hack and the duplicate static list used during hex/ansi256 entry - the in-list "← Back" row is dropped (the widget editor has none, and ESC is documented in both help texts) - (s)how separators folds into the help line and the VSCode contrast warning collapses to one line, so both modes put their list in the same place Row parity is deliberately not attempted: Tab only enters color editing from a colorable widget, so padding the list with unselectable rows would confuse more than the renumbering it would fix. Co-Authored-By: Claude Opus 4.8 <[email protected]>
ColorMenu was the only consumer and it now renders the shared WidgetRow list, so nothing in the repo imports ink-select-input. Also switch the ColorMenu test's ANSI stripping to the strip-ansi package the other TUI tests already use, instead of a hand-rolled regex. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Both editor modes put conditions that override what you do here next to the title - the powerline warning in the widget editor, the global colour override warning here. The VSCode contrast note is not that: it never changes with state and is environmental, so wearing the same ⚠ glyph made it read as a misplaced peer of those warnings rather than as a footnote. Also drops a stray '.' that separated the title from the global override warning. Co-Authored-By: Claude Opus 4.8 <[email protected]>
The color editor needs to know which theme color a widget actually renders in, and re-deriving that would drift from the renderer the first time either side changed. assignPowerlineThemeSlots() now owns the rule - merged widgets share a slot, separators break a merge run, widgets that render nothing take no slot - and both callers read from it. The renderer replaces its inline index counter with a lookup, countPowerlineThemeSlots() is expressed in terms of the assignment, and getEffectiveThemeColors() maps widget id to the theme colors that win over the widget's own, leaving pinned channels (and a preserve-colors custom command's foreground) undefined so callers fall back to the stored color. getActiveThemeColors() also replaces the renderer's inline theme lookup, so the color-level key mapping lives in one place too. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…heme An unpinned channel under an active theme renders the theme's colour, but the editor showed the widget's dormant stored value - both on the current-style row and in the row tinting - so the editor disagreed with the preview about what was on screen. Both now read from getEffectiveThemeColors, so an unpinned row is tinted with its theme colour and the current-style row reports it as "(theme)" instead of a palette position that does not apply. Pinned channels are unchanged: they still show the widget's own colour with [PINNED]. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Theme colours are positional - a widget's colour comes from its slot, and slots move when you reorder or merge widgets. That is precisely what the widget editor does, so it was the one place you could not see the effect without tabbing across to the colour editor and back. Rows are now tinted through the same styleWidgetRowLabel helper the colour editor uses, so outside move mode the two lists render identically - a new parity test asserts the rows are byte-identical, ANSI included. Selection is shown by the indicator alone, as it already is in colour mode. Move mode drops the tint and keeps the blue row, so the row being dragged stays trackable; the change of rendering doubles as the mode signal. A custom command preserving its own output colours is left untinted, since its colours are not ours to predict. This inverts the direction of sirmalloc's request - it changes the widget editor to match the colour editor rather than the reverse - and it costs the green selected-row highlight, so it is deliberately the last commit on the branch and can be dropped on its own. Co-Authored-By: Claude Opus 4.8 <[email protected]>
The list no longer has a "← Back" row, so highlightedItemId can never be 'back' and the nine guards checking for it were unreachable conditions. Co-Authored-By: Claude Opus 4.8 <[email protected]>
The feature had no user-facing documentation: pinning a colour over a Powerline theme, the (p) keybind, the automatic pin when editing under a theme, and the fact that colour editing is now reached with Tab from the widget editor were all undocumented. Adds a "Pinning Colors Over a Powerline Theme" section covering the pin model (independent per channel, non-destructive unpin, dormant colours left alone) and notes the Tab route and the shared escape target in the widget editor keybinds. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Pin state is per widget and per channel, but it was reported on a status line that only ever describes the highlighted widget - so you could not see which widgets carried overrides without visiting each one. That line also restated the channel being edited, which the header already showed in background mode, and paired "(theme)" with "[unpinned - theme applies]", saying the same thing twice. Pins now appear as row tags - (fg pinned), (bg pinned), (fg+bg pinned) - alongside (merged→) and friends, shown only when a theme is active and in both editor modes, since the row renderer is shared. The header names the channel symmetrically ([FOREGROUND] as well as [BACKGROUND]), and the status line is left with just the value it exists to show. isPowerlineThemeActive() replaces ColorMenu's inline theme check, so that condition now lives with the rest of the theme logic. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Cycling a colour under a theme used to pin the channel automatically. That made a stray arrow key destructive: it pinned AND overwrote the widget's stored colour, and because unpinning is non-destructive by design, the original value was gone with no way back - exactly the dormant colour the no-appearance-change guarantee promises to leave alone. The colour keys sit one row from the navigation keys. Under a theme, the colour keys (arrows, hex, ansi256, gradient) now do nothing until the channel is pinned, and the current-style row says "- theme applies, press (p) to override" rather than leaving them looking broken. Bold, dim, reset and clear-all are not theme-driven and stay live, and nothing changes when no theme is active. Pinning now seeds from the theme colour the widget is actually rendering when it has no colour of its own, instead of the widget's default. That was the behaviour promised on the PR, it makes taking control leave the appearance untouched, and it means edits start from the value on screen rather than jumping to an unrelated one. commitColorEdit goes away with the auto-pin it existed for. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two conflicts, both from this branch's own refactors meeting new work on main: - src/tui/App.tsx: main added applyTuiImport beside the navigation helpers this branch extracted, and added exportConfig/importConfig cases to the main-menu switch that this branch had replaced with getMainMenuScreenTarget. Kept both additions, and moved main's two new options into the helper with the other direct navigations rather than leaving two mechanisms for the same job. Covered by App.test.ts. - PowerlineThemeSelector.test.ts: main independently hit the same ink flake and added waitForInkCondition, polling with a silent 1s timeout. Kept main's name so future merges do not reintroduce it, with this branch's semantics - a named step and a throw on timeout, so a stall reports which step stalled instead of failing an assertion later. The label is optional, so main's call style still compiles. Co-Authored-By: Claude Opus 4.8 <[email protected]>
The colour editor computed powerline theme slots from different inputs than the renderer, so under a theme it could preview - and pin - a colour the status line never used. getEffectiveThemeColors called assignPowerlineThemeSlots with placeholder content for every widget and the default startIndex of 0, while the renderer passes real pre-rendered content and the cross-line offset. Two divergences followed: with continueThemeAcrossLines every line after the first was previewed in the previous line's colours, and any widget that rendered empty consumed a slot in the editor but not in the renderer, shifting everything after it. getEffectiveThemeColors now requires a ThemeSlotContext holding both inputs, so no call site can silently default them. App owns a single memoised pre-render and derives per-line contexts from it; StatusLinePreview consumes that instead of pre-rendering again, which also stops custom commands being executed twice per keystroke. Also in the same area: - applyCustomPowerlineTheme walked its own slot index, so (c)ustomize wrote different colours than the theme it promised to copy - merged widgets most visibly. It now uses the shared helper. It passes placeholder content on purpose, because it bakes colours in for every future render. - renderer.ts re-implemented keepsOwnForeground inline. Both sides now read the one predicate, so the pin gate and the render cannot disagree. - (r)eset and (c)lear-all bypassed the pin gate and deleted colours the theme was hiding, with no visible change to warn the user. Under a theme they now clear bold, dim and pinned channels only. - Cycling a pinned channel onto the palette's Default entry left it pinned with no colour, suppressing the theme with nothing to replace it. Pinned channels no longer offer that entry. - Adding a widget under a theme assigned a random background that the theme then hid, which pinning would later seed from. That assignment is now limited to the case where the widget's own background is what renders. USAGE.md dropped the claim that pinning never changes appearance: it does when the widget has a dormant colour, which is the case the feature exists for. It now documents that, the reset/clear behaviour, and that theme colours follow position among the widgets that actually render. eslint.config.js ignores .claude/, whose nested worktrees are outside this project's tsconfig and made `bun run lint` fail with parser errors. bun run lint clean; bun test 1950 pass, 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Follow-up to c178ed7, covering the rest of the review findings. Data loss and unreachable exits: - The remove-pins prompt applied the theme on Yes, No and ESC alike, so the "ESC cancel" the screen advertises committed a theme the user was only previewing. ConfirmDialog gained an optional onEscape for dialogs whose "No" still commits something; the prompt now restores the original settings. - That prompt is reached by Enter and had "Yes" preselected, so a repeated Enter wiped every pin. ConfirmDialog gained defaultChoice, defaulting to 'yes' so no other dialog changes, and the prompt asks for 'no'. - Selecting the 'custom' theme offered to remove pins that override nothing there, and (c)ustomize kept pins that had become redundant - invisible, because the row tag needs a real theme, and revived by the next theme change. The prompt now skips 'custom' and the customizer drops the pins. - Pins survived a theme being turned off with no way to see or clear them: the tag, the (p) key and the help text were all gated on an active theme, while the baked colour kept rendering. Tags now show as "(fg pinned, inactive)" and (p) unpins without a theme, though pinning still needs one. Editor swap: - editingBackground and showSeparators were local state, and Tab swaps by changing screen, so both silently reset - the header could read [FOREGROUND] while the user believed they were editing a background. App owns them now. - Tab was refused on rows the colour editor can still highlight, making it a one-way door out of the widget editor. It is a mode switch, so it always works; the colour editor already falls back to its first colourable row. - Tab was also swallowed on the empty colour list, sending the user to the line selector from the screen that says to go add a widget. - Flex separators were listed as colourable, because getWidget returns null for them and the filter treats unknown types as colourable. They render no text and take no theme slot, so they are excluded - which also stops a meaningless pin latching hasPins on for good. Row tinting reproduced the theme but not the rest of the renderer's precedence, so rows advertised colours the status line does not use. Global foreground/background overrides now win, globalBold ORs with the widget's own bold, and under powerline a gradient collapses to its first stop. Tests and docs: the editor-row-parity fixture was all-colourable, so it could not catch divergence; a mixed fixture now pins the real contract - the widget editor lists 1,2,3,4,5 where the colour editor lists 1,5, leaving gaps rather than renumbering. USAGE.md no longer claims both modes list the same widgets. Cleanup: isMergedIntoPreviousWidget exported instead of duplicated; getEffectiveThemeColors and the derived row lists memoised, which also stabilises the highlight-repair effect's dependencies; getEditorBackScreen() became the EDITOR_BACK_SCREEN constant; getMainMenuScreenTarget enumerates its action-only options behind a never check so a new entry cannot compile clean and do nothing; a tinted selected row is underlined, since the selection colour cannot apply to a label that carries its own; the dead digit guard and the stale ink-select-input comments are gone. .at(-1) and Object.hasOwn are replaced at all four runtime call sites rather than just the one on the theme path - engines.node >=14 and the Node 14 build target lower syntax but do not polyfill prototype methods, so fixing one site would have achieved nothing. bun run lint clean; bun test 1960 pass, 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Follow-up to a6de0b5, from a review of that commit. Row tinting reproduced the renderer's precedence as if both render paths shared it, so a row could still advertise a colour the status line does not use: - A global background override was applied to every row, but only the standard path reads overrideBackgroundColor - renderPowerlineStatusLine never mentions it. Under a theme every row showed the override while the line showed the theme. It now applies with powerline off, which is where it renders. - A gradient foreground override was treated as a solid per-widget colour, which neither path does: it is painted across the finished line, so each widget shows a slice of it. At ansi16 that pass is a no-op and the widget keeps its own colour, which the row now does too. Above ansi16 the row paints the gradient across its label rather than claiming one stop for every row, since no row can know its own slice. - Collapsing a widget's own gradient under powerline went through a hand-rolled hex: conversion, and getColorAnsiCode's hex: branch ignores colorLevel - so it emitted truecolor at ansi16, where the renderer emits nothing, and at ansi256, where it emits a 256-color escape. applyColors gained collapseGradient, which takes the existing fall-through to getColorAnsiCode instead, and the local first-stop helper is gone. Editor swap: - Tab is checked before the input-mode guards so it works on an empty line, but it was also reaching across them: from half-typed hex it discarded the entry, and from the clear-all prompt it abandoned the dialog, both without asking. A confirmation or text entry now holds the key, as it did before Tab existed. - Swapping from a row the colour editor cannot show - a flex separator, or a separator while hidden - made it fall back to its first colourable row and report that as the highlight, overwriting the caller's cursor. Two Tabs from row 4 landed on row 1. A fallback is a guess, not a choice, so it is no longer reported; the guess is spent once the highlight moves. showSeparators moved to App in a6de0b5 and so no longer resets on entry, which made it unresettable: the (s) toggle is refused under powerline or a default separator, but the row filter was not, so rows turned on beforehand stayed visible with a dead toggle. Both now read one canShowSeparators predicate, along with the help text that offers the key. bun run lint clean; bun test 1977 pass, 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Follow-up to 8a725bd, from a review of that commit. Two of the three were introduced by it. A default separator does not replace separator widgets already on a line. It only supplies the character for separators that have none: renderer.ts:1053 uses widget.character ?? settings.defaultSeparator and colours with widget.color ?? 'gray', so [text, separator '#', text] with defaultSeparator '~' renders "AAA~#~BBB". Gating the row list on it therefore hid rows for widgets that still render, and neither the list nor the (s) toggle could bring them back - trading the dead toggle 8a725bd fixed for unreachable rows. Separator rows are now listed whenever they render, which powerline alone decides, and the toggle reads the same predicate so the two cannot disagree. This does relax a pre-existing gate: (s) previously refused while a default separator was set. That rule belongs to adding NEW separators, which is what widgets.ts:46 governs; recolouring the ones already on the line is a different question, and refusing it left rendered output unreachable. Also: - guessedHighlightId stored the guessed id directly, so "no guess" and "guessed null" were the same value. On a line with nothing colourable the guess is null, the guard did not fire, and onWidgetHighlight(null) cleared the caller's cursor - reintroducing the jump to row 1 that 8a725bd set out to prevent. The ref now wraps the id so the two states are distinct. - The gradient override branch keyed off isGradientSpec, which is prefix-only, while what renders is decided by parseGradientSpec - null below two resolvable stops. For a spec like "gradient:FF0000" the row handed applyColors something that produces no code at all. The paths differ here, so both are reproduced: powerline keeps the widget's own foreground, while the standard path has already dropped it and then paints no gradient over it. Verified by rendering each case through preRenderAllWidgets/renderStatusLine and comparing the row to the line. bun run lint clean; bun test 1981 pass, 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
@sirmalloc - let me know what you think. Implementation complete. |
# Conflicts: # bun.lock # src/tui/components/ItemsEditor.tsx # src/tui/components/color-menu/mutations.ts # src/utils/jsonl-metrics.ts # src/utils/renderer.ts
|
I merged this branch with current
Here's what I found along the way. Bugs
Behavior worth a look
Housekeeping
|
Summary
This PR started as Tab navigation between the item and color editors. Following @sirmalloc's five points in the thread below, it now also implements per-widget color overrides under Powerline themes and reshapes color editing into a mode of the widget editor.
Related to #259.
The five points
1. Allow editing colors when a theme is applied; save modified colors with their widgets.
A widget can pin its foreground and/or background, independently. A pinned channel keeps the widget's own color and overrides the theme; an unpinned channel takes the theme. Colors are stored on the widget as they always were — the pin is two optional booleans (
pinColor,pinBackgroundColor), so there is no new color storage and no settings migration.The guard that blocked color editing under a managed theme is gone.
2. Main menu color entry becomes a signpost.
🎨 Edit Colorsno longer opens the standalone color flow. It lands on a notice explaining that color editing lives in the widget editor, with an action that jumps straight to the line selector. The entry is left in place for you to remove when you're ready.The old color line-selector screen became unreachable and was deleted, which also removed the "colors are managed by the Powerline theme, press any key to go back" block — the wall this PR exists to replace.
3. Tab to color editing, keeping the widget editor's shape; Esc back one menu.
Both editors now render their rows through one shared component, so a row is the same row in both modes: same numbering, same indicator, same structure markers (
(merged→),(raw value),(no-align)), tinted with the color the widget actually renders in. Titles share a stem and differ by a mode tag —Edit Line 1 [WIDGETS]/Edit Line 1 [COLORS] [FOREGROUND]. A parity test asserts the rows are byte-identical between modes, ANSI included.Esc backs out to the line selector from either mode, and the same one.
ink-select-inputis gone from the color editor (it made a shared row impossible), which also removed a remount-on-highlight hack and a duplicate static list. Nothing else imported it, so it's dropped frompackage.json.4. Color editing stays familiar; the rendering path honours per-widget overrides.
renderPowerlineStatusLineapplies a pinned channel over the theme.preserveColorsstill wins for the foreground of a custom command. The theme color index still advances for pinned widgets, so pinning one widget doesn't shift its siblings' colors.The editor now shows what actually renders: an unpinned channel displays the theme's color and reports it as
(theme), rather than the dormant stored value it used to show. Both the row tinting and the status line read from one helper that derives colors from the same slot assignment the renderer uses, so the preview cannot drift from the output.5. Theme switcher prompts to keep or remove overrides.
Changing the theme with pins present prompts to keep or clear them.
The hard requirement
Pins are opt-in and optional, so an existing config has none: every channel takes the theme exactly as before, and nothing is rewritten on load. The tricky case you named — colors persisted before a theme was enabled — stays dormant, because only a conscious pin makes a color win.
src/utils/__tests__/renderer-theme-color-override.test.tscodifies this, including the pre-existing-color case.UX decisions worth a look
←/→destructive: it pinned and overwrote the dormant color, and since unpinning deliberately keeps the color, the original was unrecoverable. Under a theme the color keys now do nothing until you pressp, and the status line says- theme applies, press (p) to overrideso they never look broken. Bold, dim, reset and clear-all aren't theme-driven and stay live. Nothing changes when no theme is active.(fg pinned),(bg pinned),(fg+bg pinned)) in both modes, so every override is visible at a glance rather than only for the highlighted widget.ink-select-inputis gone, full parity is a small change if you'd prefer it.feat(items-editor): tint widget rows with the colour they render inmakes the widget editor adopt color-mode rendering, which costs its green selected-row highlight (selection is the▶indicator, as in color mode; move mode keeps its blue row). That inverts the direction of your request, so it sits last and can be dropped on its own without touching anything else.Also included
PowerlineThemeSelector.test.ts: two ink-driven tests advanced on fixed 25 ms sleeps and failed under load. They now poll for the state each step waits on. (Ink writes a frame before the next screen's input handler attaches, so a keypress sent the instant a frame appears is dropped — worth knowing for other TUI tests.)docs/USAGE.mddocuments the color editing mode, the pin model, and the keybinds.Not included, deliberately
Enabling Powerline permanently deletes manual separators and disabling doesn't restore them. That's pre-existing and yours to decide on, so it's raised separately as #531 rather than changed here.
Test plan
bun run lintclean (tsc + eslint, no warnings)bun test— 1715 pass, 0 failbun run buildsucceedspreserveColors, and index advancement across pinned and merged widgets