ai-station/.venv/lib/python3.12/site-packages/textual/widgets/_footer.py

355 lines
11 KiB
Python

from __future__ import annotations
from collections import defaultdict
from itertools import groupby
from typing import TYPE_CHECKING
import rich.repr
from rich.text import Text
from textual import events
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import HorizontalGroup, ScrollableContainer
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Label
if TYPE_CHECKING:
from textual.screen import Screen
@rich.repr.auto
class KeyGroup(HorizontalGroup):
DEFAULT_CSS = """
KeyGroup {
width: auto;
}
"""
@rich.repr.auto
class FooterKey(Widget):
ALLOW_SELECT = False
COMPONENT_CLASSES = {
"footer-key--key",
"footer-key--description",
}
DEFAULT_CSS = """
FooterKey {
width: auto;
height: 1;
text-wrap: nowrap;
background: $footer-item-background;
.footer-key--key {
color: $footer-key-foreground;
background: $footer-key-background;
text-style: bold;
padding: 0 1;
}
.footer-key--description {
padding: 0 1 0 0;
color: $footer-description-foreground;
background: $footer-description-background;
}
&:hover {
color: $footer-key-foreground;
background: $block-hover-background;
}
&.-disabled {
text-style: dim;
}
&.-compact {
.footer-key--key {
padding: 0;
}
.footer-key--description {
padding: 0 0 0 1;
}
}
}
"""
compact = reactive(True)
"""Display compact style."""
def __init__(
self,
key: str,
key_display: str,
description: str,
action: str,
disabled: bool = False,
tooltip: str = "",
classes="",
) -> None:
self.key = key
self.key_display = key_display
self.description = description
self.action = action
self._disabled = disabled
if disabled:
classes += " -disabled"
super().__init__(classes=classes)
self.shrink = False
if tooltip:
self.tooltip = tooltip
def render(self) -> Text:
key_style = self.get_component_rich_style("footer-key--key")
description_style = self.get_component_rich_style("footer-key--description")
key_display = self.key_display
key_padding = self.get_component_styles("footer-key--key").padding
description_padding = self.get_component_styles(
"footer-key--description"
).padding
description = self.description
if description:
label_text = Text.assemble(
(
" " * key_padding.left + key_display + " " * key_padding.right,
key_style,
),
(
" " * description_padding.left
+ description
+ " " * description_padding.right,
description_style,
),
)
else:
label_text = Text.assemble((key_display, key_style))
label_text.stylize_before(self.rich_style)
return label_text
def on_mouse_down(self) -> None:
if self._disabled:
self.app.bell()
else:
self.app.simulate_key(self.key)
def _watch_compact(self, compact: bool) -> None:
self.set_class(compact, "-compact")
class FooterLabel(Label):
"""Text displayed in the footer (used by binding groups)."""
@rich.repr.auto
class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
ALLOW_SELECT = False
DEFAULT_CSS = """
Footer {
layout: horizontal;
color: $footer-foreground;
background: $footer-background;
dock: bottom;
height: 1;
scrollbar-size: 0 0;
&.-compact {
FooterLabel {
margin: 0;
}
FooterKey {
margin-right: 1;
}
FooterKey.-grouped {
margin: 0 1;
}
FooterKey.-command-palette {
padding-right: 0;
}
}
FooterKey.-command-palette {
dock: right;
padding-right: 1;
border-left: vkey $foreground 20%;
}
HorizontalGroup.binding-group {
width: auto;
height: 1;
layout: horizontal;
}
KeyGroup.-compact {
FooterKey.-grouped {
margin: 0;
}
margin: 0 1 0 0;
padding-left: 1;
}
FooterKey.-grouped {
margin: 0 1;
}
FooterLabel {
margin: 0 1 0 0;
color: $footer-description-foreground;
background: $footer-description-background;
}
&:ansi {
background: ansi_default;
.footer-key--key {
background: ansi_default;
color: ansi_magenta;
}
.footer-key--description {
background: ansi_default;
color: ansi_default;
}
FooterKey:hover {
text-style: underline;
background: ansi_default;
color: ansi_default;
.footer-key--key {
background: ansi_default;
}
}
FooterKey.-command-palette {
background: ansi_default;
border-left: vkey ansi_black;
}
}
}
"""
compact = reactive(False, toggle_class="-compact")
"""Display in compact style."""
_bindings_ready = reactive(False, repaint=False)
"""True if the bindings are ready to be displayed."""
show_command_palette = reactive(True)
"""Show the key to invoke the command palette."""
combine_groups = reactive(True)
"""Combine bindings in the same group?"""
def __init__(
self,
*children: Widget,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
show_command_palette: bool = True,
compact: bool = False,
) -> None:
"""A footer to show key bindings.
Args:
*children: Child widgets.
name: The name of the widget.
id: The ID of the widget in the DOM.
classes: The CSS classes for the widget.
disabled: Whether the widget is disabled or not.
show_command_palette: Show key binding to invoke the command palette, on the right of the footer.
compact: Display a compact style (less whitespace) footer.
"""
super().__init__(
*children,
name=name,
id=id,
classes=classes,
disabled=disabled,
)
self.set_reactive(Footer.show_command_palette, show_command_palette)
self.compact = compact
def compose(self) -> ComposeResult:
if not self._bindings_ready:
return
active_bindings = self.screen.active_bindings
bindings = [
(binding, enabled, tooltip)
for (_, binding, enabled, tooltip) in active_bindings.values()
if binding.show
]
action_to_bindings: defaultdict[str, list[tuple[Binding, bool, str]]]
action_to_bindings = defaultdict(list)
for binding, enabled, tooltip in bindings:
action_to_bindings[binding.action].append((binding, enabled, tooltip))
self.styles.grid_size_columns = len(action_to_bindings)
for group, multi_bindings_iterable in groupby(
action_to_bindings.values(),
lambda multi_bindings_: multi_bindings_[0][0].group,
):
multi_bindings = list(multi_bindings_iterable)
if group is not None and len(multi_bindings) > 1:
with KeyGroup(classes="-compact" if group.compact else ""):
for multi_bindings in multi_bindings:
binding, enabled, tooltip = multi_bindings[0]
yield FooterKey(
binding.key,
self.app.get_key_display(binding),
"",
binding.action,
disabled=not enabled,
tooltip=tooltip or binding.description,
classes="-grouped",
).data_bind(compact=Footer.compact)
yield FooterLabel(group.description)
else:
for multi_bindings in multi_bindings:
binding, enabled, tooltip = multi_bindings[0]
yield FooterKey(
binding.key,
self.app.get_key_display(binding),
binding.description,
binding.action,
disabled=not enabled,
tooltip=tooltip,
).data_bind(compact=Footer.compact)
if self.show_command_palette and self.app.ENABLE_COMMAND_PALETTE:
try:
_node, binding, enabled, tooltip = active_bindings[
self.app.COMMAND_PALETTE_BINDING
]
except KeyError:
pass
else:
yield FooterKey(
binding.key,
self.app.get_key_display(binding),
binding.description,
binding.action,
classes="-command-palette",
disabled=not enabled,
tooltip=binding.tooltip or binding.description,
)
def bindings_changed(self, screen: Screen) -> None:
self._bindings_ready = True
if not screen.app.app_focus:
return
if self.is_attached and screen is self.screen:
self.call_after_refresh(self.recompose)
def _on_mouse_scroll_down(self, event: events.MouseScrollDown) -> None:
if self.allow_horizontal_scroll:
self.release_anchor()
if self._scroll_right_for_pointer(animate=True):
event.stop()
event.prevent_default()
def _on_mouse_scroll_up(self, event: events.MouseScrollUp) -> None:
if self.allow_horizontal_scroll:
self.release_anchor()
if self._scroll_left_for_pointer(animate=True):
event.stop()
event.prevent_default()
def on_mount(self) -> None:
self.screen.bindings_updated_signal.subscribe(self, self.bindings_changed)
def on_unmount(self) -> None:
self.screen.bindings_updated_signal.unsubscribe(self)