Skip to content

refactor: small cleanups to the font install, auto-align loop, GUID and root dir widgets - #809

Merged
sirmalloc merged 4 commits into
sirmalloc:mainfrom
eric-engberg:refactor/small-cleanups
Nov 6, 2026
Merged

sirmalloc merged 4 commits into
sirmalloc:mainfrom
eric-engberg:refactor/small-cleanups

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

What

Four small cleanups. None of them changes what the status line or the TUI shows.

  • The Powerline font install loses a fallback result that could never be returned.
  • The Powerline auto-align padding loop no longer moves its for counter from inside the loop body.
  • generateGuid drops the fraction of its random digit with Math.trunc instead of | 0.
  • Git Root Dir and JJ Root Dir drop a trailing-separator strip that the split after it already does.

I also went through the regular expressions that can take quadratic time on long input. The two in the root dir widgets go with the strip above. The others stay, with the reason for each under How.

Why

Font install. After the clone, installPowerlineFonts either runs install.sh (macOS and Linux) or copies the font files (Windows). Each branch returns its result or throws. An unsupported platform gets its own message before the clone. So the Platform-specific installation not implemented result after the two branches could never be returned. TypeScript accepts the function without it: bun run lint is clean.

Auto-align loop. The padding pass in renderPowerlineStatusLine used for (let i = 0; ...; i++), then set i = j inside the body to jump past a merge group. A reader had to find that assignment to know how the loop moved. The jump matters in one case: an auto-align exclusion on a widget merged into the previous one. That exclusion is ignored because the loop never visits the widget. No test covered that case.

generateGuid. (Math.random() * 16) | 0 and Math.trunc(Math.random() * 16) give the same digit for every value in [0, 16). I compared them on 1,000,035 values of Math.random() (0, the smallest positive number, the bottom of each of the 16 steps and a value just below its top, the largest value Math.random() can return, and a million random draws), under Bun 1.4.2 and Node 26.10.0, and got 0 mismatches. They only differ for values Math.random() * 16 can't produce:

Value | 0 Math.trunc
-0.5 0 -0 (prints as 0)
2 ** 31 -2147483648 2147483648
NaN 0 NaN
Infinity 0 Infinity

Root dir widgets. Both widgets did this:

const trimmedRootDir = rootDir.replace(/[\\/]+$/, '');
const normalizedRootDir = trimmedRootDir.length > 0 ? trimmedRootDir : rootDir;
const parts = normalizedRootDir.split(/[\\/]/).filter(Boolean);

filter(Boolean) already drops the empty parts that trailing separators leave, so the strip didn't change the name. The fallback only runs when no part is left. That happens only for a path made of separators alone, such as /, and then the strip had produced '' and the code fell back to rootDir anyway. I checked every string of up to 8 characters drawn from /, \, a, :, space and newline (2,015,539 strings): the old and new code give the same result for all of them, under Bun and Node.

The strip's pattern was also the slow one here. With a long run of separators it starts a match at every separator in the run and scans to the end of the run each time:

Input Bun 1.4.2, old Bun 1.4.2, new Node 26.10.0, old Node 26.10.0, new
50,000 / then x 1,621 ms 0.8 ms 4,014 ms 1.5 ms

git and jj never print such a path. The change is worth making because the code gets shorter.

The other patterns. Each one can go quadratic on a crafted 50,000-character string:

Pattern Where Input Bun 1.4.2 Node 26.10.0
/\.?0+$/ number-format.ts 1., 50,000 zeros, 1 1,193 ms 3,592 ms
/^hostname\s+(.+)$/i git-review-cache.ts hostname, 50,000 spaces, a\rb 1,600 ms 3,713 ms
/[\\/]+$/ App.tsx, global-package-manager.ts 50,000 / then x 1,590 ms 4,019 ms
/\/+$/ global-command-resolution.ts, global-package-manager.ts 50,000 / then x 1,195 ms 3,635 ms
/(\d+)\s+insertions?/ (and deletions?) git.ts, jj.ts 50,000 digits 1,359 ms 3,577 ms
/(\d+) insertion/ (and deletion) JjChanges.ts 50,000 digits 1,410 ms 3,611 ms
/\s*\(.*\)$/ Model.ts 50,000 ( 1,400 ms 3,775 ms

None of them gets input like that in practice. Where the input has a hard limit, here is the pattern at that limit:

Pattern Largest real input Bun 1.4.2 Node 26.10.0
/\.?0+$/ toFixed output: at most 29 characters (a minus sign, 21 digits, the point and 6 decimals), since decimals is capped at 6 0.002 ms 0.002 ms
/[\\/]+$/ a 4,096-separator run (Linux PATH_MAX) 10.7 ms 26.8 ms
/[\\/]+$/ a 1,024-separator run (macOS PATH_MAX) 0.7 ms 1.7 ms
/(\d+)\s+insertions?/ a 255-digit run (the longest file name) 0.04 ms 0.1 ms

How

  • powerline.ts: removed the unreachable return after the platform branches in installPowerlineFonts.
  • renderer.ts: the padding pass is now a while loop. Each turn sets next to the following widget. When the widget heads a column that has a width, next is set past the widgets merged into it instead. The loop moves to next at the end of the turn. The rest of the body is unchanged. The old if (!element) continue; is now element?. in the exclusion check and element && in the column check, so a missing element still just moves on.
  • guid.ts: Math.trunc in place of | 0.
  • GitRootDir.ts, JjRootDir.ts: split rootDir directly and fall back to rootDir. A comment says why there is no strip.
  • New tests:
    • renderer-exclude-auto-align.test.ts: an exclusion on a widget merged into the previous one is ignored, and the columns after the group are still padded. A line with no column widths keeps its natural widths.
    • guid.test.ts (new file): the UUID v4 shape, and the exact GUID that Math.random() at 0, 1/16, 1/2 and its largest value gives.
    • JjRootDir.test.ts: trailing Windows separators and /.
    • GitRootDir.test.ts: trailing Windows separators and a path of separators alone.

Left as is:

  • calculateMaxWidthsFromPreRendered moves its loop counter the same way. Another branch of mine rewrites that loop as a while loop, so this one doesn't touch it.
  • number-format.ts /\.?0+$/: it only sees toFixed output of at most 29 characters. A bounded form needs an alternation with a lookbehind, which is harder to read than the line it replaces.
  • git-review-cache.ts /^hostname\s+(.+)$/i: it reads local ssh -G output, which puts one space after the keyword. It is only slow with a long run of spaces followed later by a lone line terminator such as \r. The host taken from the remote URL can't carry that: ssh refuses it with hostname contains invalid characters. Only a quoted Hostname value in an ssh config file can, and that has no length limit (OpenSSH 10.3 prints one with 50,000 leading spaces).
  • App.tsx and global-package-manager.ts /[\\/]+$/, and global-command-resolution.ts and global-package-manager.ts /\/+$/: these strip trailing separators from install directories and PATH entries in the user's own environment. They are only slow for a run of thousands of separators. A bounded form needs a lookbehind or a loop, and neither is simpler than the one-liner.
  • git.ts /(\d+)\s+insertions?/ and deletions?: git diff --shortstat prints one summary line, with numbers of a few digits.
  • jj.ts /(\d+)\s+insertions?/ and deletions?: in jj diff --stat output, a long run of digits can only come from a file name, which is at most 255 bytes. Adding (?<!\d) would bound the pattern, but it costs a lookbehind and no real input needs it.
  • JjChanges.ts /(\d+) insertion/ and deletion: these only read the summary line of jj diff --stat.
  • Model.ts /\s*\(.*\)$/: it reads Claude Code's model display name, which is a few dozen characters. Another branch of mine replaces this line with a string search.

Demo

None: nothing visible changes, and the built CLI prints byte for byte what main prints in plain and Powerline mode, also for a merge group whose second widget is excluded from auto-align (see Testing).

Testing

  • On main as of fix(powerline): keep the Custom theme when Powerline is enabled again #793 (a0bedc0): it merges cleanly. bun run lint is clean, and bun test gives 4158 pass, 0 fail (4147 on main). The built CLI under Bun 1.4.4 and Node 26.13.0 agrees across runtimes and matches main in plain and Powerline mode, and the TUI opens and exits cleanly under both. It adds no JavaScript or Node API newer than Node 14, the build's target.
  • The 11 new tests also pass against main's source, because they pin behaviour the refactors must keep. When I removed the merge-group skip from the loop, the new merged-exclusion test failed, and it was the only failure in the full suite.
  • Node 26.10.0, vitest on the changed test files plus powerline-install.test.ts: 45 pass, 2 fail (main: 34 pass, 2 fail). The 2 failures are the same on both. They are the existing ESM spy failures in powerline-install.test.ts (vi.spyOn(os, 'homedir')).
  • Built dist under Bun 1.4.2 and Node 26.10.0:
    • With the baseline configs, the runtimes agree with each other and with main in plain and Powerline mode.
    • With a two-line config (a merge group whose second widget is excluded from auto-align, then a widget, then Git Root Dir in a scratch repo; below it, three widgets), the result is the same.
    • In Powerline mode, where auto-align is on, every column lines up across the two lines. Git Root Dir shows the repo's name in both modes.
    • The TUI smoke test passes on both runtimes.

After the clone, installPowerlineFonts either runs install.sh (macOS and
Linux) or copies the font files (Windows), and each branch returns its
result or throws. The "Platform-specific installation not implemented"
result after them could never be returned, and an unsupported platform
already returns its own message before the clone.
…while loop

The padding pass jumped its for-loop counter past each merge group from
inside the body, so the loop header no longer said how it stepped. A
while loop now sets the next widget explicitly: the following one, or
the one after a merge group once the group is padded.

The output is unchanged. Two new tests pin the cases that depend on the
stepping and had no test: an auto-align exclusion on a widget merged
into the previous one is ignored and the columns after the group are
still padded, and a line rendered without column widths keeps its
natural widths.
generateGuid dropped the fraction of Math.random() * 16 with a bitwise
"| 0". Math.trunc says that directly. For the values this sees, [0, 16),
both give the same digit: they differ only past 2^31, for NaN and for
Infinity, none of which Math.random() * 16 can produce.

New tests pin the UUID v4 shape and the digits Math.random() at 0,
1/16, 1/2 and its largest value turn into, including the variant
digit's 8 to b range.
Git Root Dir and JJ Root Dir stripped trailing separators from the root
path, then split it on separators and dropped the empty parts. The split
already drops the empty parts trailing separators leave, so the strip
changed nothing:
- With a name in the path, both give the same last part.
- With no name, as for "/", the strip left an empty string and the code
  fell back to the original path, which is what the fallback now uses
  directly.

The strip's pattern also rescanned a long run of separators from every
position in it: 50,000 slashes before a name took 1.6s under Bun and
4.0s under Node. git and jj never print such a path, but the split
alone does it in about a millisecond.

An exhaustive check over every string of up to 8 characters drawn from
"/", "\", "a", ":", " " and a newline gives the same name both ways. New
tests cover trailing Windows separators in both widgets, "/" for JJ Root
Dir and a path of separators alone for Git Root Dir.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants