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

183 lines
5.4 KiB
Python

from __future__ import annotations
from collections import defaultdict
from itertools import groupby
from operator import itemgetter
from typing import TYPE_CHECKING
from rich import box
from rich.table import Table
from rich.text import Text
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import VerticalScroll
from textual.widgets import Static
if TYPE_CHECKING:
from textual.screen import Screen
class BindingsTable(Static):
"""A widget to display bindings."""
COMPONENT_CLASSES = {
"bindings-table--key",
"bindings-table--description",
"bindings-table--divider",
"bindings-table--header",
}
DEFAULT_CSS = """
BindingsTable {
width: auto;
height: auto;
}
"""
def render_bindings_table(self) -> Table:
"""Render a table with all the key bindings.
Returns:
A Rich Table.
"""
bindings = self.screen.active_bindings.values()
key_style = self.get_component_rich_style("bindings-table--key")
divider_transparent = (
self.get_component_styles("bindings-table--divider").color.a == 0
)
table = Table(
padding=(0, 0),
show_header=False,
box=box.SIMPLE if divider_transparent else box.HORIZONTALS,
border_style=self.get_component_rich_style("bindings-table--divider"),
)
table.add_column("", justify="right")
header_style = self.get_component_rich_style("bindings-table--header")
previous_namespace: object = None
for namespace, _bindings in groupby(bindings, key=itemgetter(0)):
table_bindings = list(_bindings)
if not table_bindings:
continue
if namespace.BINDING_GROUP_TITLE:
title = Text(namespace.BINDING_GROUP_TITLE, end="")
title.stylize(header_style)
table.add_row("", title)
action_to_bindings: defaultdict[str, list[tuple[Binding, bool, str]]]
action_to_bindings = defaultdict(list)
for _, binding, enabled, tooltip in table_bindings:
if not binding.system:
action_to_bindings[binding.action].append(
(binding, enabled, tooltip)
)
description_style = self.get_component_rich_style(
"bindings-table--description"
)
def render_description(binding: Binding) -> Text:
"""Render description text from a binding."""
text = Text.from_markup(
binding.description, end="", style=description_style
)
if binding.tooltip:
if binding.description:
text.append(" ")
text.append(binding.tooltip, "dim")
return text
get_key_display = self.app.get_key_display
for multi_bindings in action_to_bindings.values():
binding, enabled, tooltip = multi_bindings[0]
keys_display = " ".join(
dict.fromkeys( # Remove duplicates while preserving order
get_key_display(binding) for binding, _, _ in multi_bindings
)
)
table.add_row(
Text(keys_display, style=key_style),
render_description(binding),
)
if namespace != previous_namespace:
table.add_section()
previous_namespace = namespace
return table
def render(self) -> Table:
return self.render_bindings_table()
class KeyPanel(VerticalScroll, can_focus=False):
"""
Shows bindings for currently focused widget.
"""
DEFAULT_CSS = """
KeyPanel {
split: right;
width: 33%;
min-width: 30;
max-width: 60;
border-left: vkey $foreground 30%;
padding: 0 1;
height: 1fr;
padding-right: 1;
align: center top;
&> BindingsTable > .bindings-table--key {
color: $text-accent;
text-style: bold;
padding: 0 1;
}
&> BindingsTable > .bindings-table--description {
color: $foreground;
}
&> BindingsTable > .bindings-table--divider {
color: transparent;
}
&> BindingsTable > .bindings-table--header {
color: $text-primary;
text-style: underline;
}
#bindings-table {
width: auto;
height: auto;
}
}
"""
DEFAULT_CLASSES = "-textual-system"
def compose(self) -> ComposeResult:
yield BindingsTable(shrink=True, expand=False)
async def on_mount(self) -> None:
mount_screen = self.screen
async def bindings_changed(screen: Screen) -> None:
"""Update bindings."""
if not screen.app.app_focus:
return
if self.is_attached and screen is mount_screen:
await self.recompose()
def _bindings_changed(screen: Screen) -> None:
self.call_after_refresh(bindings_changed, screen)
self.set_class(self.app.ansi_color, "-ansi-scrollbar")
self.screen.bindings_updated_signal.subscribe(self, _bindings_changed)
def on_unmount(self) -> None:
self.screen.bindings_updated_signal.unsubscribe(self)