Repository navigation
refactor(widgets): share one base class across the Git count widgets - #22
Closed
eric-engberg wants to merge 15 commits into
Closed
eric-engberg wants to merge 15 commits into
eric-engberg wants to merge 15 commits into
Conversation
The existing tests for Git Staged, Unstaged and Untracked Files, Insertions, Deletions and Changes cover each widget's counts and placeholders, but not the editor label and hide-state labels, the file counts' numeric value, glyph overrides outside the preview, a raw item on the widgets without a raw mode, or Git Changes staying visible with the zero state hidden while only one of its counts is zero. Cover all six against the same repositories: dirty, clean, only removed lines, only added lines, only an untracked file, and none. Git is answered by a mocked execFileSync, like the other Git widget tests.
Git Staged, Unstaged and Untracked Files, Insertions, Deletions and Changes each carried the same editor label, "no git" and "zero" hide states and render: the preview sample, then "(no git)" outside a work tree, or nothing when that state is hidden, then nothing for a zero count when the zero state is hidden. The three file-count widgets were also one class written three times, and Insertions and Deletions nearly so. GitCountWidgetBase now holds that render and the hide states; each widget says when its counts are zero and how they show. GitFileCountWidget adds the file counts' git query, raw-or-labeled format and numeric value, so those three only name their count and label. GitLineCountWidget adds the line counts' git query, preview sample, glyph key and editor for Insertions, Deletions and Changes. Behavior is unchanged.
eric-engberg
marked this pull request as ready for review
October 4, 2026 21:56
eric-engberg
added a commit
that referenced
this pull request
Oct 6, 2026
sirmalloc#601 lets Tokens Input, Output, Cached and Total, and Git Staged, Unstaged and Untracked Files, offer their labels to the label editor one widget at a time. #16 and #22 move those widgets onto shared base classes, so the merges kept their shape and the labels went with it: an edited label still drew, but the editor never offered (e). Each base now offers its subclass's label, once for all of them.
…sh style (sirmalloc#665) Fish style replaced any cwd that merely started with the home path, so with home /Users/al the sibling /Users/alex/project rendered as "~ex/project". Reuse the boundary-checked abbreviateHomeDir that the plain ~ option already uses. Co-authored-by: effectustasi <[email protected]>
…ith 10 updates (sirmalloc#662) Bumps the dev-dependencies group with 10 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@remotion/cli](https://github.com/remotion-dev/remotion) | `4.0.524` | `4.0.532` | | [chalk](https://github.com/chalk/chalk) | `6.0.0` | `6.0.1` | | [eslint](https://github.com/eslint/eslint) | `10.10.0` | `10.11.0` | | [react](https://github.com/react/react/tree/HEAD/packages/react) | `19.2.8` | `19.3.0` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.18` | `19.3.0` | | [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) | `19.2.8` | `19.3.0` | | [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.7` | `19.3.0` | | [remotion](https://github.com/remotion-dev/remotion) | `4.0.524` | `4.0.532` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.70.0` | `8.70.1` | | [zod](https://github.com/colinhacks/zod) | `4.5.4` | `4.6.5` | Updates `@remotion/cli` from 4.0.524 to 4.0.532 - [Release notes](https://github.com/remotion-dev/remotion/releases) - [Commits](remotion-dev/remotion@v4.0.524...v4.0.532) Updates `chalk` from 6.0.0 to 6.0.1 - [Release notes](https://github.com/chalk/chalk/releases) - [Commits](chalk/chalk@v6.0.0...v6.0.1) Updates `eslint` from 10.10.0 to 10.11.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](eslint/eslint@v10.10.0...v10.11.0) Updates `react` from 19.2.8 to 19.3.0 - [Release notes](https://github.com/react/react/releases) - [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/react/react/commits/v19.3.0/packages/react) Updates `@types/react` from 19.2.18 to 19.3.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `react-dom` from 19.2.8 to 19.3.0 - [Release notes](https://github.com/react/react/releases) - [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/react/react/commits/v19.3.0/packages/react-dom) Updates `@types/react-dom` from 19.2.7 to 19.3.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) Updates `remotion` from 4.0.524 to 4.0.532 - [Release notes](https://github.com/remotion-dev/remotion/releases) - [Commits](remotion-dev/remotion@v4.0.524...v4.0.532) Updates `typescript-eslint` from 8.70.0 to 8.70.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.1/packages/typescript-eslint) Updates `zod` from 4.5.4 to 4.6.5 - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](colinhacks/zod@v4.5.4...v4.6.5) --- updated-dependencies: - dependency-name: "@remotion/cli" dependency-version: 4.0.532 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: chalk dependency-version: 6.0.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: eslint dependency-version: 10.11.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: react dependency-version: 19.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@types/react" dependency-version: 19.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: react-dom dependency-version: 19.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@types/react-dom" dependency-version: 19.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: remotion dependency-version: 4.0.532 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: typescript-eslint dependency-version: 8.70.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: zod dependency-version: 4.6.5 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…c#661) The status page fetch behind the Claude Status widget was only tested for a response stream that aborts or errors. Its other outcomes had no tests: a body split across chunks, a non-200 or empty response, a request that errors or times out (and must be destroyed so the socket doesn't outlive the render), and HTTPS_PROXY handling, where a blank value must be ignored and a value that isn't a URL must settle as a failed lookup rather than throw out of the render. Also cover the 16-color fallback for incident colors (major drops from orange to red, critical to bright red). The fake response helper now takes the final stream event, so the existing abort and error tests share it.
…irmalloc#660) Powerline separator colors (same, single and missing backgrounds, both normal and inverted), caps on widgets without a background, bold resets at flex gaps and end caps, preserved command colors, and auto-align padding for merged groups had no tests, and neither did inherited and override colors on standard-mode separators and padding. These paths decide whether a status line keeps a continuous color band or shows gaps, wrong arrow colors and leaked bold. The ANSI parser gets direct tests for 8-bit C1 CSI/OSC, unterminated sequences, two-character escapes, flags, ZWJ emoji sequences and hyperlink closing on truncation, so widths and truncation stay right for custom command output. Claude settings gain tests for loadClaudeSettingsSync, refresh-interval reads and writes against unreadable settings, a failed .orig backup not blocking install, invalid ccstatusline configs not syncing hooks, and the getRemoteControlStatus manifest lookup.
The line picker behind Edit Lines and Edit Colors had no tests. These drive it through Ink with one key per write: listing lines with their widget counts, moving and selecting, going back by ESC or the Back entry, appending, the delete confirmation (Yes, No, ESC), move mode with its wrap-around swap, the Powerline theme warning, and new props from the parent. Each key waits for its visible effect before the next one. Keys that must do nothing wait until Ink has read them from stdin, then check that nothing was redrawn.
Render UpdateCheckerMenu in each state it shows (checking, up to date, registry failure, update available with runnable, missing and no commands, auto-update) and drive its keys: Enter on each entry, Down to Check again and Back, and Escape, checking which callback fires.
… busy machine (sirmalloc#655) "returns successful output when a background job keeps stdout open" gave the command 200ms to start, print and exit, and the background job 1.2s to live. On a loaded machine starting node alone can take longer than 200ms, so the command timed out and the test failed. It then waited a fixed 1.3s for the job's file, which a slow start also outran. The command now gets 1.5s and the job lives 3s, so the test still shows the command returns without waiting for the job, with room to start. It polls for the job's file instead of sleeping. The overflow-tree test gets a long timeout too, so only the overflow can end it, which is what it asserts; the timeout-tree test keeps its short one.
The config tests used a fixed /tmp/ccstatusline-config-test-home and deleted it before every test. Two test runs at once (an editor's test runner plus a terminal run, or parallel CI jobs on one machine) deleted each other's settings files mid-test and failed at random. Run three at a time, the file failed 128 of 345 tests on main. A directory from mkdtempSync is unique to each run, so they no longer collide.
…loc#652) The Ink component tests slept a fixed 25ms after rendering and after each key, then asserted. Ink redraws, and React re-attaches Ink's input listener, on their own schedule, so on a loaded machine the assertion ran before the redraw, or the next key arrived before the listener was back and was dropped. Under CPU load these tests failed intermittently: TerminalWidthMenu, RefreshIntervalMenu and the timezone editor most often. Each test now waits for the visible effect of its key (the selection moving, a prompt appearing, a callback firing), with a timeout, through a small shared helper in src/tui/__tests__/helpers/wait-for-ink.ts. After each successful wait it yields two setImmediate turns so React can re-attach the listener before the next key. No assertions change.
…ls (sirmalloc#645) The codebase mixed the two spellings: 39 parseInt vs 6 Number.parseInt, 14 isNaN vs 14 Number.isNaN (Number.isFinite and Number.parseFloat were already used exclusively). parseInt -> Number.parseInt is a pure rename (they are the same function). Number.isNaN differs from the global only for non-number arguments, which it doesn't coerce; every one of the 14 calls checks the result of parseInt(..., 10), a number, so behavior is unchanged. ESLint's built-in no-restricted-globals now rejects global parseInt, parseFloat, isNaN and isFinite with a message pointing at the Number.* form, so the mix doesn't come back. No new dependency.
The codebase mixed two styles for Node's own modules: a handful of files
imported 'node:fs', 'node:os' and friends while the rest used bare ids
such as 'fs' and 'child_process'. Use the node: prefix everywhere, so
each import says plainly that it is Node's module and cannot resolve to
an npm package of the same name.
This covers static imports and re-exports, type-only imports, dynamic
import(), require() (including the script the Custom Command widget
hands to a child runtime and the scripts tests write to disk), and
vi.mock() ids in tests. Vitest and Bun resolve 'child_process' and
'node:child_process' to the same module, for both mocks and spies, so
every mock still applies to the code under test. ESLint's import order
moved five imports to keep builtins sorted.
No behavior change. The bundle already loads node: imports, and every
Node version that can load the bundle also supports require('node:...').
Co-authored-by: Matthew Breedlove <[email protected]>
|
Owner
Author
|
Merged upstream as sirmalloc#654. |
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.



Demo
After the refactor: in the line editor, r switches Git Staged Files to its raw value and h hides it at zero, then g sets Git Insertions' glyph and both of Git Changes' glyphs. Every case also renders byte-for-byte the same as before; see Testing.
Powerline
Plain
The same six widgets piped from a real repository with staged, unstaged and untracked changes, a clean one, and a directory outside any repository:
Powerline
Plain
Summary
Git Staged, Unstaged and Untracked Files, Insertions, Deletions and Changes each carried their own copy of:
render: the preview sample, then(no git)outside a work tree, or nothing when that state is hidden, then nothing for a zero count when the zero state is hidden.The three file-count widgets were also one class written three times, differing only in the count they show, its label, name and color. Insertions and Deletions were nearly the same class too.
GitCountWidgetBase(widgets/shared/git-count-widget.ts) now holds that render and the hide states. Each widget says when its counts are zero and how they show; the preview sample goes through the same formatting. On top of it:GitFileCountWidgetholds the file counts' git query, the raw-or-labeled format and the numeric value, so Staged, Unstaged and Untracked Files only name their count and label.GitLineCountWidgetholds the line counts' git query, the preview sample, and the glyph key and editor. Insertions, Deletions and Changes each keep their own zero rule and format.7 source files, 173 lines added and 330 removed, plus a test file. No visual change.
Testing
The existing tests for these widgets cover each one's counts and placeholders. The first commit adds
widgets/__tests__/GitCountWidgets.test.ts(63 tests) for the rest, checked against the current code before the refactor:It passes at both commits under Bun and under Node. Breaking Git Changes' zero rule, a zero-state label, Git Untracked Files' numeric value, Git Deletions' glyph editor, Git Insertions' no-git hide state or Git Staged Files' raw value each makes it fail.
bun run lintandbun test(2,421 pass).Before/after render check: all six widgets rendered on
mainand on this branch, in every state their own keys reach, raw and labeled, at all three color levels. It ran in the TUI preview and against seven real repositories: staged, unstaged and untracked changes; only staged; only untracked; a merge conflict; clean; no commits yet; and a directory outside any repository. All 420 cases (60 per repository) are byte-identical, as are each widget's editor label and keys. A wider sweep over the same repositories also varied the hide states, glyph overrides (set and emptied) and raw value (on, off, unset): all 4,320 renders are identical, as are each widget's numeric value, hide states, keys and glyph-editor rows.Built
dist/and piped a status line with all six widgets through it, from the dirty and the clean repository, in plain and Powerline mode under Bun and Node 16, 18, 20, 22, 24 and 26: every runtime agrees, and the output matchesmain. The TUI opens and exits cleanly on each.Under Node with Vitest, the full suite fails in the same 17 files as
main(all from spying on Node built-ins), and the new tests pass.