Add KitFooter: a Footer with a guaranteed zero horizontal overflow - #1
Merged
Conversation
DESIGN.md's doctrine is "Footer shows the ~7 keys that matter", but nothing in ricekit enforces it β every app hand-picks which Bindings get show=True and hopes the count and terminal width cooperate. tuistore shipped with 12 show=True bindings, and at 80 columns the stock Textual Footer (a plain ScrollableContainer with an invisible scrollbar, scrollbar-size: 0 0) overflowed by 46 columns with no visual sign anything was missing. tuistore's fix was a one-off show=False pass on five bindings, tuned by hand for that app at that binding count β it doesn't stop the same mistake from happening again there or anywhere else in the suite. KitFooter subclasses Footer (compact=True by default, everything else identical) and composes exactly as the stock widget does. On mount and on every resize, it measures the real arranged width of its children via Footer's own arrange() against the available width. If it overflows, it hides (never removes) trailing children in DOM order β the ones from bindings declared latest in BINDINGS, the same priority stock Footer already renders in β one at a time, re-measuring after each, until the rest fits. Widening the terminal brings hidden keys back; nothing is a one-way hide. The docked command-palette key is budgeted for but is never itself a trim candidate. This is a safety net, not a redesign of the ~7-keys doctrine: it never shows more than an app already marked show=True, only ever less. Adds the project's first test suite (tests/test_footer.py, run via unittest) covering: a small binding set left untouched, 20+ bindings never overflowing from 40 to 120 columns, hidden keys reappearing on resize, and the command-palette key surviving heavy trimming while its width is still counted against the budget. Also adds tests.yml (ubuntu / macos / windows x Python 3.11/3.12/3.13 via astral-sh/setup-uv), since there was previously no CI test coverage at all. Updates the widgets table, __init__ docstring example, and the README quickstart to use KitFooter in place of the stock Footer.
β¦ not object Two issues surfaced running the suite on the full CI matrix: - A single pilot.pause() only drains one call_after_refresh hop. Getting a footer to its steady state after mount or resize is several hops deep (screen's bindings-updated signal -> bindings_changed -> recompose -> KitFooter's own overflow check chained onto the same refresh), which a fast local machine absorbs in one pause but a loaded CI runner does not - windows-latest in particular was asserting against a footer that hadn't finished composing yet. Pausing a handful of times drains the whole chain deterministically instead of asserting mid-flight. - The narrow/wide/narrow round-trip test compared visible children by widget identity. A recompose mounts fresh FooterKey instances for the same bindings, so two settle points holding the same visible keys could still legitimately be disjoint sets of widget objects. Compare by key_display instead, which is stable across a recompose.
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.
Problem
DESIGN.md's own doctrine is "Footer shows the ~7 keys that matter; `?` opens the full HelpModal" β but nothing in ricekit enforces it. Every app on top of ricekit hand-picks which of its own `Binding`s get `show=True` and hopes the count and the terminal width cooperate.
tuistore shipped with 12 `show=True` bindings (not ~7). At a plain 80-column terminal, the stock Textual `Footer` β a `ScrollableContainer` with an invisible scrollbar (`scrollbar-size: 0 0`) β silently overflowed by 46 columns. Nearly a third of the footer's content was simply inaccessible, with no visual indication anything was missing.
tuistore's own fix (already merged there) was a hand-tuned `show=False` pass on five bindings β a static, per-app workaround tuned for that exact binding count. It doesn't stop the same mistake from happening again, in tuistore or any other app in the suite, the next time someone adds a binding.
Design
`KitFooter` subclasses Textual's `Footer` β same constructor, same CSS, same `compose()` (untouched, no FooterKey/KeyGroup reimplementation), `compact=True` by default instead of `False`.
On mount and on every resize, it measures the real arranged width of its children β via `Footer`'s own `arrange()`, the same machinery Textual uses internally to derive `virtual_size`/`max_scroll_x` β against the available width. If it overflows, it hides (`display = False`, never removes) trailing children in DOM order: the ones from bindings declared latest in the app's `BINDINGS` list, which is the same priority order stock `Footer`'s own `compose()` already renders in. It re-measures after each hide and stops as soon as it fits. Widening the terminal reveals hidden keys again β every pass starts by un-hiding everything and re-trimming from scratch, so nothing is a one-way hide.
The docked command-palette key is budgeted for (its width reduces the space available to the trimmable flow, exactly as Textual's own dock/scroll-spacing math already accounts for) but is never itself a trim candidate.
This is a safety net, not a redesign of the "~7 keys" doctrine: it never shows more than an app already marked `show=True` β only ever less. An app that already fits inside ~7 keys should see this do nothing at any reasonable width.
Tests
ricekit had no test suite at all before this. Added `tests/test_footer.py` (stdlib `unittest`, Textual's `App.run_test`/`IsolatedAsyncioTestCase` harness) covering:
`uv run python -m unittest discover tests -v` passes with zero failures on 3.11, 3.12, and 3.13.
Beyond the automated tests, I ran a throwaway headless app reproducing tuistore's actual 12-binding `BINDINGS` list at 40/60/80/120 columns and dumped the real visible/hidden key sets β at 80 columns it converges to exactly 7 visible keys (search, install, readme, update, remove, star, open) plus the command-palette key, with zero overflow, matching DESIGN.md's target almost exactly without any app-side tuning.
Also adds `.github/workflows/tests.yml` (ubuntu-latest / macos-latest / windows-latest Γ Python 3.11/3.12/3.13, via `astral-sh/setup-uv`) β there was previously zero CI test coverage in this repo.
Docs
Updated the widgets table and the `## use it` code sample in README.md, and the `ricekit/init.py` docstring example, to reference `KitFooter` in place of the stock Textual `Footer`.