Coverage for src/lilbee/cli/tui/widgets/stable_footer.py: 100%
22 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
1"""Footer that skips a recompose when the shown bindings have not changed."""
3from __future__ import annotations
5from textual.screen import Screen
6from textual.widget import Widget
7from textual.widgets import Footer
9_BindingSignature = tuple[tuple[str, str, str, bool, bool], ...]
12def _bindings_signature(screen: Screen) -> _BindingSignature:
13 """The (key, action, description, shown, enabled) tuples a Footer would render."""
14 return tuple(
15 sorted(
16 (key, binding.action, binding.description, binding.show, enabled)
17 for key, (_namespace, binding, enabled, _tooltip) in screen.active_bindings.items()
18 )
19 )
22class StableFooter(Footer):
23 """Footer that recomposes only when its rendered key row would change.
25 Textual's own ``Footer.bindings_changed`` recomposes on every screen
26 ``refresh_bindings()`` call, which fires on every focus change (once for
27 the blur, once for the focus). Each recompose tears down and remounts
28 every ``FooterKey``, which costs a full CSS stylesheet apply per new
29 widget. A screen with many focusable fields where most transitions
30 leave the same bindings enabled (moving between two Input rows, for
31 example) pays that cost on every Tab press for no visible change.
32 """
34 def __init__(
35 self,
36 *children: Widget,
37 name: str | None = None,
38 id: str | None = None,
39 classes: str | None = None,
40 disabled: bool = False,
41 show_command_palette: bool = True,
42 compact: bool = False,
43 ) -> None:
44 super().__init__(
45 *children,
46 name=name,
47 id=id,
48 classes=classes,
49 disabled=disabled,
50 show_command_palette=show_command_palette,
51 compact=compact,
52 )
53 self._last_bindings_signature: _BindingSignature | None = None
55 def bindings_changed(self, screen: Screen) -> None:
56 self._bindings_ready = True
57 if not screen.app.app_focus:
58 return
59 if not (self.is_attached and screen is self.screen):
60 return
61 signature = _bindings_signature(screen)
62 if signature == self._last_bindings_signature:
63 return
64 self._last_bindings_signature = signature
65 self.call_after_refresh(self.recompose)