11from __future__ import annotations
22
33from collections import defaultdict
4+ from itertools import groupby
45from typing import TYPE_CHECKING
56
67import rich .repr
1213from textual .containers import ScrollableContainer
1314from textual .reactive import reactive
1415from textual .widget import Widget
16+ from textual .widgets import Label
1517
1618if TYPE_CHECKING :
1719 from textual .screen import Screen
@@ -64,6 +66,7 @@ class FooterKey(Widget):
6466 """
6567
6668 compact = reactive (True )
69+ """Display compact style."""
6770
6871 def __init__ (
6972 self ,
@@ -95,18 +98,22 @@ def render(self) -> Text:
9598 "footer-key--description"
9699 ).padding
97100 description = self .description
98- label_text = Text .assemble (
99- (
100- " " * key_padding .left + key_display + " " * key_padding .right ,
101- key_style ,
102- ),
103- (
104- " " * description_padding .left
105- + description
106- + " " * description_padding .right ,
107- description_style ,
108- ),
109- )
101+ if description :
102+ label_text = Text .assemble (
103+ (
104+ " " * key_padding .left + key_display + " " * key_padding .right ,
105+ key_style ,
106+ ),
107+ (
108+ " " * description_padding .left
109+ + description
110+ + " " * description_padding .right ,
111+ description_style ,
112+ ),
113+ )
114+ else :
115+ label_text = Text .assemble ((key_display , key_style ))
116+
110117 label_text .stylize_before (self .rich_style )
111118 return label_text
112119
@@ -120,13 +127,17 @@ def _watch_compact(self, compact: bool) -> None:
120127 self .set_class (compact , "-compact" )
121128
122129
130+ class FooterLabel (Label ):
131+ pass
132+
133+
123134@rich .repr .auto
124135class Footer (ScrollableContainer , can_focus = False , can_focus_children = False ):
125136 ALLOW_SELECT = False
126137 DEFAULT_CSS = """
127138 Footer {
128- layout: grid ;
129- grid-columns: auto;
139+ layout: horizontal ;
140+ # grid-columns: auto;
130141 color: $footer-foreground;
131142 background: $footer-background;
132143 dock: bottom;
@@ -140,6 +151,11 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
140151 padding-right: 1;
141152 border-left: vkey $foreground 20%;
142153 }
154+ HorizontalGroup.binding-group {
155+ width: auto;
156+ height: 1;
157+ layout: horizontal;
158+ }
143159
144160 &:ansi {
145161 background: ansi_default;
@@ -164,6 +180,15 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
164180 border-left: vkey ansi_black;
165181 }
166182 }
183+ FooterKey.-grouped {
184+ margin: 0 1;
185+ }
186+ FooterLabel {
187+ margin: 0 1;
188+ background: red;
189+ color: $footer-description-foreground;
190+ background: $footer-description-background;
191+ }
167192 }
168193 """
169194
@@ -173,6 +198,8 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
173198 """True if the bindings are ready to be displayed."""
174199 show_command_palette = reactive (True )
175200 """Show the key to invoke the command palette."""
201+ combine_groups = reactive (True )
202+ """Combine bindings in the same group?"""
176203
177204 def __init__ (
178205 self ,
@@ -217,16 +244,33 @@ def compose(self) -> ComposeResult:
217244 action_to_bindings [binding .action ].append ((binding , enabled , tooltip ))
218245
219246 self .styles .grid_size_columns = len (action_to_bindings )
220- for multi_bindings in action_to_bindings .values ():
221- binding , enabled , tooltip = multi_bindings [0 ]
222- yield FooterKey (
223- binding .key ,
224- self .app .get_key_display (binding ),
225- binding .description ,
226- binding .action ,
227- disabled = not enabled ,
228- tooltip = tooltip ,
229- ).data_bind (Footer .compact )
247+
248+ for group , multi_bindings_iterable in groupby (
249+ action_to_bindings .values (),
250+ lambda multi_bindings : multi_bindings [0 ][0 ].group ,
251+ ):
252+ if group is not None :
253+ for multi_bindings in multi_bindings_iterable :
254+ binding , enabled , tooltip = multi_bindings [0 ]
255+ yield FooterKey (
256+ binding .key ,
257+ self .app .get_key_display (binding ),
258+ "" ,
259+ binding .action ,
260+ classes = "-grouped" ,
261+ ).data_bind (Footer .compact )
262+ yield FooterLabel (group .description )
263+ else :
264+ for multi_bindings in multi_bindings_iterable :
265+ binding , enabled , tooltip = multi_bindings [0 ]
266+ yield FooterKey (
267+ binding .key ,
268+ self .app .get_key_display (binding ),
269+ binding .description ,
270+ binding .action ,
271+ disabled = not enabled ,
272+ tooltip = tooltip ,
273+ ).data_bind (Footer .compact )
230274 if self .show_command_palette and self .app .ENABLE_COMMAND_PALETTE :
231275 try :
232276 _node , binding , enabled , tooltip = active_bindings [
0 commit comments