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

1"""Footer that skips a recompose when the shown bindings have not changed.""" 

2 

3from __future__ import annotations 

4 

5from textual.screen import Screen 

6from textual.widget import Widget 

7from textual.widgets import Footer 

8 

9_BindingSignature = tuple[tuple[str, str, str, bool, bool], ...] 

10 

11 

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 ) 

20 

21 

22class StableFooter(Footer): 

23 """Footer that recomposes only when its rendered key row would change. 

24 

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 """ 

33 

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 

54 

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)