Repository navigation
CPLAT-12795: adopt tuikit for keys, toasts, render, prefs, charts, kitty and tmux - #177
Merged
Merged
Conversation
…from tuikit/keys Pin github.com/keyolk/tuikit at 74b15dc. - renderFooter lays hints out with keys.Footer (priority drop, pinned help/quit, CJK-safe ellipsis, hard width cap). Hints now read 'key label' separated by two spaces, the fleet footer format. - Keymap.Conflicts builds a keys.Keymap from the binding table (current keys plus live legacy aliases) and runs keys.Conflicts per scope, which adds the Korean-reachability check to the duplicate check. - The Hangul-to-Latin table and the reachable-uppercase set come from keys.Latin / keys.ReachableUnderCJK; the user langmap override layer stays. - displayKey is keys.Names with ccx's short spellings (sp, S-tab, ^G).
copiedMsg was a string every key cleared. It is now a toast.Queue with one
source ('status'), so a newer message still replaces the shown one, but:
- info messages expire on their own tick (4s) instead of on the next key;
- failures ('... failed', 'Error ...') stay until a key after they were seen;
- 'doing X…' progress stays until the next status replaces it;
- the footer line carries a level prefix (i / ✗ / ◌), so an error reads as
one under NO_COLOR.
Call sites were rewritten mechanically (go/ast): X.copiedMsg = v -> X.flash(v),
+= -> flashAppend, reads -> flashText(). Update schedules the expiry tick
with toast.Ticks(before, after), so handlers keep not returning commands.
- truncate, truncateContextText, truncCells, truncateDisplayWidth, padToWidth, padSelectedRow and the picker's padRightWidth/truncateWidth delegate to render.TruncateMarker/Truncate/PadRight: ANSI-safe, and measured the way lipgloss draws (runewidth counted East Asian ambiguous runes as two cells under a CJK locale, so those columns disagreed with the frame). - truncName sliced bytes: Hangul names came back as invalid UTF-8 and a budget under 1 panicked. Now render.Truncate (test added). - overlayCenter is render.Center: the frame stays exactly w x h even with a wide character on the modal's edge (test added). - The unused byte-based padRight in the picker is gone. Kept: overlayLine (clips at a caller-given column such as the split divider, which render.Overlay does not take) and truncateExact (deliberately counts ambiguous runes as two cells for the split-pane border).
Where the user left the list (grouping, preview mode, view, split ratio, folded groups, hidden badges, filter, editor-input choice) was re-marshalled into ~/.config/ccx/config.yaml synchronously on quit: a crash lost it, and every quit rewrote the user's config file. It now lives in $XDG_STATE_HOME/ccx/ui.json (tuikit/prefs): saved debounced while it changes, written off the UI loop with temp-file-plus-rename, and flushed on quit, which exits once that write reports back. config.yaml keeps the settings a person edits (worktree_dir, initial_focus). Migration: the config.yaml values are the defaults ui.json is read over, so the first run keeps what an older build saved; that quit drops the state fields from config.yaml. Pick mode still persists nothing. Tests: TestMain points XDG_STATE_HOME at the temp home and turns persistence off (a shared state file would carry one test's grouping into the next); persistence tests turn it on over private dirs.
sparkline() now draws with chart.Sparkline on a zero baseline: - a series longer than the width fills the width with equal-share buckets; the old ceil-sized buckets drew 9 points into 5 of 8 cells and averaged a partial last bucket as a whole one (test added); - maxWidth 0 no longer divides by zero; - under a CJK locale (or CCX_ASCII) the ramp is ASCII, since block elements are East Asian ambiguous and draw two cells there. A shorter series still takes one cell per point and an idle one sits on the floor, as before. histogramBar and the timestamp bucketing stay: whole-cell bars with a one-cell minimum for any non-zero count, which chart.Bar's eighth-cell fill does not promise.
internal/kitty is now a thin layer over tuikit/kitty. ccx still draws an image by appending cursor-placed, file-transmitted sequences to the frame; what moved to the kit: - Detection (kitty.Detect, CCX_KITTY still forces it) runs once in Init from main / the pickers, before the program. It used to run lazily inside the first View and, inside tmux, run tmux there - and also flip tmux's allow-passthrough synchronously. That promotion is now EnsurePassthroughCmd from Init, off the loop, and a failure is shown. - Escapes are Caps.TransmitFile + Caps.Place (cursor saved/restored, pane offset applied inside the passthrough) and Caps.DeleteAll. - The pane offset is no longer a sync.Once cache queried from View: resize, focus and blur ask tmux off the loop (PaneCmd) and the answer is recorded with SetPane. BlurMsg no longer runs tmux synchronously in Update. - The visibility watcher is kitty.WatchPane in both the TUI and the picker; the picker's 250ms tick that ran tmux in Update is gone. Tests no longer detect anything: without Init the terminal is unsupported, so no test runs tmux (before, a test run inside tmux could set the real server's allow-passthrough). Tests that need graphics use kitty.SetCaps.
internal/tmux keeps its API (and Claude-session discovery: FindPane, CurrentWindowClaudes, CurrentWindowClaudeSessionIDs, CurrentWindowKey, MarkLiveSessions), but every tmux call now goes through a tuikit/tmux Client and every process walk through a Procs snapshot: - ListPanes is Client.Panes: free-text fields are read by length, so a cwd or window name holding the old '|' separator, a tab or Hangul no longer shifts the columns after it (test added). - Calls target pane/window IDs, never session:window.pane; SwitchToPane is Jump; Pane gains WindowID; SpawnHiddenWindow returns the new pane's ID. - Key forwarding takes the tea.KeyMsg (SendKeyMsg): a Hangul syllable typed into the live preview used to be dropped, since only one-byte keys were sent as text (test added). SendKeys is SendLine, Ctrl-C is SendInterrupt, popups are DisplayPopup without a timeout (an editor waits on a person), and RunPopup / the input popups now report their error. - HasClaude, HasClaudeSession and the PPID walk share one memoized Procs snapshot (HasClaudeSession used a pgrep per pane and only saw direct children); walkToPane is Procs.WalkTo. - kitty.Init takes the same Client as its TmuxQuerier. Tests: TestMain in tmux, tui and cli installs an unscripted tmux.Fake and an empty ps table, so no test reaches the real tmux or process table even inside tmux (tui tests: ~40s -> ~7s). The window/attribution tests now fake claude processes instead of asserting only the empty case. The opt-in CCX_PROBE_WINDOW diagnostic is the one test that talks to the real tmux.
TestDayCursorMoveCost fails under go test -race on master too: the race detector makes a 3.7ms cursor move take 18.6ms, over the 16ms budget. The budget measures the code, not the instrumentation, so both budget tests skip under the race build tag; a plain run still enforces them.
|
| Commit | Scanned at | New | Resolved | Net |
|---|---|---|---|---|
a7ce9be |
2026-10-15 00:08 UTC | — | — | — |
a7ce9be < |
2026-10-15 00:08 UTC | 0 | 0 | 0 |
Last scanned: a7ce9be · 2026-10-15 00:08 UTC
Kairo-Kim
approved these changes
Oct 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
JIRA: https://sendbird.atlassian.net/browse/CPLAT-12795
Summary
ccx now uses the shared TUI kit keyolk/tuikit, pinned at
74b15dc, for keys, toasts, rendering, UI-state persistence, sparklines, Kitty graphics and tmux. The PR has one commit per step. ccx-specific logic stays in ccx: the keymap yaml and its legacy aliases, finding Claude sessions on top of the tmux panes and process table, and the inline image layer.Changes
keys (
bindings.go,keymap*.go)keys.Footer. It drops low-priority hints first, keeps help/quit pinned, uses a CJK-safe ellipsis and never exceeds the width. Hints now readkey labelwith two spaces between hints, which is the fleet footer format.Keymap.Conflictsbuilds akeys.Keymapfrom the binding table (current keys plus live legacy aliases) and runskeys.Conflictsper scope. This adds a check that every action can be typed under a Korean input source.langmapoverride stays.toast (
status_toast.go)copiedMsgstring that any key cleared. It is now atoast.Queuewith one source:doing X…messages stay until the next status replaces them.go/ast.render
render.TruncateMarker,TruncateandPadRight, andoverlayCenterisrender.Center. Widths are measured the way lipgloss draws.truncNamesliced bytes. It returned invalid UTF-8 for Hangul names and panicked when the budget was below 1.prefs (
ui_state.go)config.yamlto$XDG_STATE_HOME/ccx/ui.json. It is saved while it changes (debounced), written off the UI loop and flushed on quit. ccx exits once that write reports back.config.yamlkeeps only the settings a person edits (worktree_dir,initial_focus).config.yamlare the defaults thatui.jsonis read over. On that quit, ccx drops those fields fromconfig.yaml.chart (
stats_timeline.go)sparklineuseschart.Sparkline.kitty (
internal/kitty,kitty_watch.go, picker)Init, before the program starts. It used to run tmux inside the firstView, and it also changedallow-passthroughsynchronously. That change is nowEnsurePassthroughCmd, off the UI loop, and its error is shown.Caps.View. Resize, focus and blur ask tmux off the loop.kitty.WatchPane. The picker's 250ms tick that ran tmux inUpdateis gone.tmux (
internal/tmux)internal/tmuxkeeps its API, but every call goes through atmux.Client, and process walks use one sharedProcssnapshot.ListPanesnow reads each field by its length, so a cwd containing|, a tab or Hangul no longer shifts the columns after it.tea.KeyMsgtoSendKeyMsg.RunPopup, the input popups, and failed sends.Tests
psany more, even when the suite runs inside tmux.TestMainintmux,tuiandcliinstalls an unscriptedtmux.Fakeand an empty ps table. Kitty detection never runs in tests. Before this change, a test run inside tmux could change the real server'sallow-passthrough.internal/tuinow runs in about 7s instead of 40s.Conflictsui.jsonmigration, precedence and save-on-change, and that pick mode never savesListPaneswith an awkward cwdsend-keys -lTestDayCursorMoveCostalso fails under-raceon master: the race detector turns 3.7ms per move into 18.6ms. Both frame-budget tests now skip under the race build tag.go vet ./...,gofmt -l .,go test -race -count=1 ./...andenv -u LC_ALL -u LC_CTYPE LANG=ko_KR.UTF-8 go test -count=1 ./...are all clean.Not moved (and why)
switchoverKeymapfields, and the?overlay keeps its own rows. Moving roughly 94 dispatch sites tokeys.Resolveis a separate change.overlayLineclips at a column the caller gives (the split divider), andrender.Overlaydoes not take one.truncateExactcounts ambiguous-width runes as two cells on purpose, for the split border.histogramBarand the other whole-cell bars guarantee one cell for any non-zero count, andchart.Bardoes not.kitty.Imageplaceholders.tmux_async.go,pane_actions.go,worktree_async.go) keep their generation-guarded closures and their test seams. Only the tmux calls inside them changed.SpawnHiddenWindowandKillWindowfor the remote live pane still run insideUpdate, as before.