Skip to content

Add KitFooter: a Footer with a guaranteed zero horizontal overflow - #1

Merged
Gheat1 merged 2 commits into
mainfrom
add-kitfooter
Jul 22, 2026
Merged

Add KitFooter: a Footer with a guaranteed zero horizontal overflow#1
Gheat1 merged 2 commits into
mainfrom
add-kitfooter

Conversation

@Gheat1

@Gheat1 Gheat1 commented Jul 22, 2026

Copy link
Copy Markdown
Owner

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:

  • a small binding set (~6 keys) renders untouched at 80 columns, nothing hidden
  • 20+ bindings never overflow (`max_scroll_x == 0`) at 40/60/80/120 columns, with excess correctly trimmed
  • narrower widths never show more than wider widths
  • resizing narrow β†’ wide reveals previously-hidden keys, and a narrow β†’ wide β†’ narrow round trip lands on the same visible set
  • the command-palette key survives heavy trimming and stays displayed
  • disabling the command-palette key leaves no docked child at all
  • the command-palette key's width is actually budgeted (fewer flow keys fit with it enabled than without, at the same width)
  • `compact` defaults to `True` and can still be overridden

`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`.

Gheat1 added 2 commits July 22, 2026 16:37
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.
@Gheat1
Gheat1 merged commit 5cb9dbe into main Jul 22, 2026
9 checks passed
@Gheat1
Gheat1 deleted the add-kitfooter branch July 22, 2026 23:44
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.

1 participant