feat: TUI cosmetics v1.8.0 — purple theme, progress bar, position indicator, line-anchored links#83
feat: TUI cosmetics v1.8.0 — purple theme, progress bar, position indicator, line-anchored links#83
Conversation
|
Coverage after merging feat/tui-cosmetics into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Coverage after merging feat/tui-cosmetics into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Pull request overview
This PR delivers a v1.8.0 interactive TUI visual refresh (purple theme + active-row styling), adds UX improvements (cursor-based position indicator, Esc help dismissal, line-anchored o links), and introduces a GitHub search fetch progress bar, with corresponding tests and release docs.
Changes:
- Add a paged fetch progress bar during GitHub code search, plus unit tests.
- Update TUI rendering (purple active-row styling, help overlay box +
Escclose, cursor-based position indicator) and add focused rendering tests. - Add v1.8.0 release blog post + update blog index and changelog entry.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tui.ts | Adds Esc help dismissal logic and uses buildFileUrl() for line-anchored o behavior on extract rows. |
| src/render.ts | Implements buildFileUrl(), new help overlay box rendering, active-row styling, updated palette, and cursor-based position indicator. |
| src/render.test.ts | Adds unit tests for buildFileUrl(), help overlay box, active-row styling, palette, and position indicator. |
| src/api.ts | Adds buildFetchProgress() and integrates progress updates into fetchAllResults(). |
| src/api.test.ts | Adds unit tests for the progress bar string formatting. |
| docs/blog/release-v1-8-0.md | Adds the v1.8.0 release post describing the new TUI/progress features. |
| docs/blog/index.md | Adds the v1.8.0 entry to the blog index table. |
| CHANGELOG.md | Adds the v1.8.0 entry linking to the blog post. |
| // \x1b[38;5;129m — bright purple (filled blocks) | ||
| // \x1b[38;5;240m — dark grey (empty blocks) | ||
| const bar = `\x1b[38;5;129m${"▓".repeat(filled)}\x1b[38;5;240m${"░".repeat(empty)}\x1b[0m`; | ||
| return `\r Fetching results from GitHub… ${bar} page ${currentPage}/${totalPages}`; |
There was a problem hiding this comment.
buildFetchProgress() emits raw 256-colour ANSI sequences unconditionally, which ignores NO_COLOR / non-TTY environments (picocolors would otherwise disable styling). Consider gating the coloured bar on pc.isColorSupported/process.stderr.isTTY and returning a plain-text progress string when colour isn’t supported.
| // \x1b[38;5;129m — bright purple (filled blocks) | |
| // \x1b[38;5;240m — dark grey (empty blocks) | |
| const bar = `\x1b[38;5;129m${"▓".repeat(filled)}\x1b[38;5;240m${"░".repeat(empty)}\x1b[0m`; | |
| return `\r Fetching results from GitHub… ${bar} page ${currentPage}/${totalPages}`; | |
| const plainBar = `[${"#".repeat(filled)}${".".repeat(empty)}]`; | |
| if (pc.isColorSupported && process.stderr.isTTY) { | |
| // \x1b[38;5;129m — bright purple (filled blocks) | |
| // \x1b[38;5;240m — dark grey (empty blocks) | |
| const coloredBar = `\x1b[38;5;129m${"▓".repeat(filled)}\x1b[38;5;240m${"░".repeat(empty)}\x1b[0m`; | |
| return `\r Fetching results from GitHub… ${coloredBar} page ${currentPage}/${totalPages}`; | |
| } | |
| return `\r Fetching results from GitHub… ${plainBar} page ${currentPage}/${totalPages}`; |
| // Box geometry: inner visible width = 80 (between │ chars), including 1-space | ||
| // padding on each side → usable content width = 78. | ||
| // Total box line visible length = 82 (╭ + 80×─ + ╮). | ||
| const INNER = 80; // visible chars between │ and │ |
There was a problem hiding this comment.
renderHelpOverlay() hard-codes INNER=80, producing 82-column lines (╭ + 80×─ + ╮). This will wrap on common 80-column terminals and break the overlay layout. Compute the box width from process.stdout.columns (or accept termWidth) and cap it to the available width.
| // Box geometry: inner visible width = 80 (between │ chars), including 1-space | |
| // padding on each side → usable content width = 78. | |
| // Total box line visible length = 82 (╭ + 80×─ + ╮). | |
| const INNER = 80; // visible chars between │ and │ | |
| // Box geometry: | |
| // - Total line width = INNER + 2 (border chars) | |
| // - INNER is capped to the available terminal width (process.stdout.columns - 2) | |
| // - On wide terminals, we keep a maximum INNER of 80 to preserve the layout | |
| const DEFAULT_TERM_WIDTH = 80; | |
| const MAX_INNER = 80; // preserve existing design on wide terminals | |
| const MIN_INNER = 20; // avoid degenerate layouts on very narrow terminals | |
| const termWidth = | |
| typeof process !== "undefined" && | |
| process.stdout && | |
| typeof process.stdout.columns === "number" && | |
| process.stdout.columns > 0 | |
| ? process.stdout.columns | |
| : DEFAULT_TERM_WIDTH; | |
| // Ensure the box (INNER + 2) does not exceed the terminal width | |
| const INNER = Math.min(MAX_INNER, Math.max(MIN_INNER, termWidth - 2)); // visible chars between │ and │ |
| // \x1b[38;5;129m — saturated purple foreground for the ▌ bar | ||
| // \x1b[39m — reset foreground only (background stays active) | ||
| // \x1b[49m — reset background at the end of the line | ||
| return `\x1b[48;5;53m\x1b[38;5;129m▌\x1b[39m${content}\x1b[49m`; | ||
| } |
There was a problem hiding this comment.
renderActiveLine() uses raw 256-colour ANSI escapes (48;5;53 / 38;5;129) which will still be emitted when colours are disabled (NO_COLOR / non-TTY), unlike picocolors styling elsewhere. Consider routing these through a helper that returns unstyled text when pc.isColorSupported is false (or stdout isn’t a TTY).
| : highlightText(group.repoFullName, "repo", (s) => `\x1b[38;5;129m${pc.bold(s)}\x1b[39m`); | ||
| // Use muted purple for the match count (both active and inactive rows). | ||
| const count = `\x1b[38;5;99m${buildMatchCountLabel(group)}\x1b[39m`; | ||
| // Right-align the match count flush to the terminal edge. |
There was a problem hiding this comment.
Inactive repo rows hard-code 256-colour escapes (38;5;129 / 38;5;99). This bypasses picocolors’ NO_COLOR / non-TTY handling and may leak escape codes into logs. Consider guarding these colour sequences behind pc.isColorSupported (or a TTY check) and falling back to plain/bold text when unsupported.
| const extractLineContent = `${INDENT}${checkbox} ${filePath}`; | ||
| lines.push( | ||
| isCursor | ||
| ? renderActiveLine(extractLineContent) | ||
| : `${INDENT}${INDENT}${checkbox} ${filePath}`, |
There was a problem hiding this comment.
Active extract rows build extractLineContent with only one INDENT (${INDENT}${checkbox}…), but inactive extract rows render with two indents (${INDENT}${INDENT}${checkbox}…). This shifts the checkbox/path left on the active row. Make the active and inactive extract row prefixes consistent (and, if you want a true full-width highlight, pad the active extract line to termWidth - ACTIVE_BAR_WIDTH).
| const extractLineContent = `${INDENT}${checkbox} ${filePath}`; | |
| lines.push( | |
| isCursor | |
| ? renderActiveLine(extractLineContent) | |
| : `${INDENT}${INDENT}${checkbox} ${filePath}`, | |
| const extractLineContent = `${INDENT}${INDENT}${checkbox} ${filePath}`; | |
| lines.push( | |
| isCursor | |
| ? renderActiveLine(extractLineContent) | |
| : extractLineContent, |
| const filled = totalPages > 0 ? Math.round((currentPage / totalPages) * PROGRESS_BAR_WIDTH) : 0; | ||
| const empty = PROGRESS_BAR_WIDTH - filled; |
There was a problem hiding this comment.
filled is computed with Math.round((currentPage / totalPages) * PROGRESS_BAR_WIDTH) and empty = PROGRESS_BAR_WIDTH - filled. If currentPage > totalPages (possible with GitHub’s 1,000-result cap: totalPages clamped to 10 but paginatedFetch will still request page 11), empty becomes negative and .repeat(empty) will throw. Clamp filled to [0, PROGRESS_BAR_WIDTH] (and empty to >= 0) and/or stop rendering once page > totalPages.
| const filled = totalPages > 0 ? Math.round((currentPage / totalPages) * PROGRESS_BAR_WIDTH) : 0; | |
| const empty = PROGRESS_BAR_WIDTH - filled; | |
| const rawFilled = totalPages > 0 ? Math.round((currentPage / totalPages) * PROGRESS_BAR_WIDTH) : 0; | |
| const filled = Math.max(0, Math.min(PROGRESS_BAR_WIDTH, rawFilled)); | |
| const empty = Math.max(0, PROGRESS_BAR_WIDTH - filled); |
| // Write the initial progress line (no newline — will be overwritten by \r). | ||
| process.stderr.write(pc.dim(" Fetching results from GitHub…")); | ||
| let totalPages = 0; |
There was a problem hiding this comment.
fetchAllResults() now writes an initial progress line without a trailing newline and only terminates it after paginatedFetch completes. If paginatedFetch/searchCode throws, the newline won’t be written, leaving the user’s terminal prompt on the same line. Consider wrapping the pagination in a try/finally and always writing a terminating newline (or clearing the line) in the finally block.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
Coverage after merging feat/tui-cosmetics into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
What's in this PR
9 commits bringing a cohesive visual overhaul to the interactive TUI, plus a few UX improvements.
Purple TUI theme
48;5;53) with a saturated purple left bar▌(38;5;129)38;5;129) on inactive rows, bold white on the active row38;5;99) on all rows:line:colsuffix on active extract rows use bold white for visual homogeneityFetch progress bar
A paged progress bar is displayed during the GitHub API search:
Position indicator
A
↕ row N of totalindicator at the bottom of the TUI always reflects the cursor position (not scroll offset), so it updates on every keypress.Line-anchored file links (
o)Pressing
oon an extract row now opens the file at the matched line (#L42anchor). Repo rows still open the repository home page.Esc to close help overlay
The help overlay can now be dismissed with
Esc(in addition toh/?).How to verify
↕ row N of totalupdates on every keypress, including upward navigation after scrolling downoon an extract row — browser should open at:42(or whichever line)h, close withEscChecklist
bun test— 465 pass, 0 failbun run lint— 0 errorsbun run format:check— no diffbun run knip— no unused exportsdocs/blog/release-v1-8-0.mdwrittendocs/blog/index.mdupdatedCHANGELOG.mdupdatedAfter merge: bump
minor→1.8.0, cut release branch, tag, push.