Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ The `model` package is split across files within a single package:
| `mode.go` | The `inputMode` enum and a handler per input mode |
| `commands.go` | All `tea.Cmd` constructors (fetch commands, update streaming) and re-fetch predicates |
| `render.go` | `View`, panel/card/status-bar/gauge/overlay renderers, mouse handling. The two list/card builders return their line index alongside the text: `buildCard` → clickable lines, `buildToolRows` → the tool-index ↔ screen-line maps. Carries the single-entry `changelogRenderCache` — the card is rebuilt on every spinner frame, so the release-notes conversion must not repeat |
| `readme.go` | `renderReadme` — markdown → ANSI via glamour, with a single-entry render cache |
| `readme.go` | `renderReadme` — panel `[3]`'s pipeline: sanitize → preprocess → glamour, with a single-entry render cache |
| `readme_clean.go` | `cleanReadmeMarkdown` — the pure README preprocessor. Fenced blocks and inline spans are segmented out first (code is never rewritten), then badges, hrefs, HTML and emoji are removed from what is left |
| `readme_style.go` | `keepkitStyle(dark)` — panel `[3]`'s glamour theme: the standard config cloned, its accents replaced from `internal/ui`'s palette |
| `textutil.go` | Pure text helpers (`wrapText`, `stripANSI`, `colorizeHelp`, `parseHelpEntries`, `markdownToLines` — the card's release-notes markdown → pre-wrapped tagged lines, …) |
| `browser.go` | Opening URLs per `GOOS` |

Expand Down Expand Up @@ -131,8 +133,27 @@ the `keepkit` screen. The protection has two layers:
A library that probes the terminal counts as the same hazard: `glamour.WithAutoStyle()`
is never used, because its termenv OSC background query reads stdin and races Bubble
Tea's input reader. Dark/light is resolved once at construction via lipgloss's cached
`HasDarkBackground()` (`m.darkBG`) and passed to glamour as a fixed `WithStandardStyle`.
The README body itself is bounded (`readmeMaxBytes`) and sanitized before rendering.
`HasDarkBackground()` (`m.darkBG`) and picks one of `keepkitStyle`'s two variants, which
glamour receives as a fixed `WithStyles`. The README body itself is bounded
(`readmeMaxBytes`) and sanitized before rendering.

Between the sanitizer and glamour sits `cleanReadmeMarkdown`, the house-style
preprocessor: a README is written for a browser, so badges, logos, `<picture>` wrappers,
hrefs nobody can click in a TTY and emoji a terminal font renders as tofu are removed
before rendering. It rests on one rule — **code is never rewritten** — because a fenced
block and an inline span are exactly how a README *shows* the markup being deleted, so
fences are segmented out (an unterminated one protects to EOF, since the
`readmeMaxBytes` cut can land mid-fence) and inline spans are masked for the duration.
Autolinks and bare URLs survive: there the URL *is* the content. Every rule that could
swallow ordinary prose is gated — a `[label]` form is unwrapped only for labels a
definition declared (ungated, `arr[i][j]` became `arri`), and a definition line is
deleted only when it cannot be part of a paragraph. Because the pass runs inside
`Update()` on up to `readmeMaxBytes`, no step in it may be superlinear;
`TestCleanReadmeMarkdownPathologicalInputIsFast` is the guard. `keepkitStyle` then
clones the standard config rather than building one, so a glamour upgrade that adds a
field cannot leave the panel with a hole in it — and because the package globals it
clones hold pointers that `styles.DefaultStyles` aliases, every override assigns a fresh
pointer instead of writing through a shared one.

## TUI state machine

Expand Down Expand Up @@ -429,8 +450,9 @@ packages fails if the isolation is ever dropped.
Per-test setup still uses the internal seams: `testConfigDir`, `testCacheDir`,
`testTokenDir`, `testAPIBase`, `testBrewPrefix` (one copy in `version`, a second in
`updater` — each private to its package), `updater`'s `testHomeDir`, and
`model`'s `testReadmeStyle` (forces the glamour construction failure so the
plain-text fallback is covered). `testAPIBase` is private to `version`, so a `model`
`model`'s `testReadmeStyle` (routes the renderer through a named standard style
instead of `keepkitStyle`; an unknown name forces the glamour construction failure,
which is how the plain-text fallback is covered). `testAPIBase` is private to `version`, so a `model`
test cannot redirect a fetch at an httptest server — a network command is executed
there only when the cache can answer it (`seedSelfReleaseCache` for the self-check);
otherwise `Init` batches are asserted by length, never run. The races are real (mutexes
Expand Down
11 changes: 9 additions & 2 deletions CLAUDE.md

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ Pure TUI, no subcommands; the only flags are `--version` and `--help`.
after the update one more key restarts keepkit in place, in the same terminal tab
- **Docs panel** — panel `[3]` switches between the rendered repository README,
`--help` output and the `man` page by hotkeys; in `--help` / `man` mode `j` / `k`
walk flags and subcommands with the current entry spotlighted
walk flags and subcommands with the current entry spotlighted. Every README is
rendered in one house style: badges, logos, HTML wrappers, unclickable link URLs
and the pictographic emoji a terminal font cannot draw are stripped, and the
headings follow keepkit's own palette — code samples are left exactly as written
- **Clickable card** — the repository and release links on the tool card open in the
browser by mouse click, or by hotkeys for the repo and changelog pages
- **Tags and grouping** — one tag per tool; `space` regroups the flat list under tag
Expand Down Expand Up @@ -119,8 +122,10 @@ Run `keepkit` — a three-panel interface opens:
versions with the release date, status, note and tag. From here tools are updated,
the repo or changelog opened in the browser, and the card data force-refreshed.
- **`[3] Readme / Help / Man`** — the docs panel: the rendered repository README (the
default), the tool's `--help` output or its `man` page. While an update runs, this
panel shows its live log instead.
default), the tool's `--help` output or its `man` page. A README is cleaned up
before rendering — badges, logos, HTML and pictographic emoji go, link text stays
without its URL, and code blocks are untouched. While an update runs, this panel
shows its live log instead.

Focus moves with `←` / `→` or the digits `1` / `2` / `3` (each panel's number is in
its title). Each panel lists its keys in the help bar; press `?` any time for the
Expand Down Expand Up @@ -291,6 +296,7 @@ subprocess sandbox — is described in [ARCHITECTURE.md](ARCHITECTURE.md).
- [Bubbles](https://github.com/charmbracelet/bubbles) — text input, viewport, spinner
- [Lip Gloss](https://github.com/charmbracelet/lipgloss) — styling
- [Glamour](https://github.com/charmbracelet/glamour) — markdown rendering for the README panel
- [goldmark-emoji](https://github.com/yuin/goldmark-emoji) — GitHub's `:shortcode:` dictionary, used to strip them from a README
- [x/ansi](https://github.com/charmbracelet/x) — stripping escape sequences from captured tool output
- [termenv](https://github.com/muesli/termenv) — terminal color-profile detection
- [go-runewidth](https://github.com/mattn/go-runewidth) — glyph width measurement
Expand Down
Loading
Loading