183 lines
5.4 KiB
Python
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)
|