zettcode-tui 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zettcode/tui/__init__.py +422 -0
- zettcode/tui/_compat.py +32 -0
- zettcode/tui/capabilities.py +107 -0
- zettcode/tui/clipboard.py +91 -0
- zettcode/tui/console.py +229 -0
- zettcode/tui/core/__init__.py +63 -0
- zettcode/tui/core/app.py +658 -0
- zettcode/tui/core/events.py +134 -0
- zettcode/tui/core/focus.py +85 -0
- zettcode/tui/core/geometry.py +212 -0
- zettcode/tui/core/host.py +66 -0
- zettcode/tui/core/keymap.py +218 -0
- zettcode/tui/core/scheduler.py +105 -0
- zettcode/tui/core/screen.py +80 -0
- zettcode/tui/core/theme.py +248 -0
- zettcode/tui/core/widget.py +204 -0
- zettcode/tui/gallery.py +588 -0
- zettcode/tui/glyphs.py +73 -0
- zettcode/tui/input/__init__.py +18 -0
- zettcode/tui/input/bridge.py +46 -0
- zettcode/tui/input/decoder.py +234 -0
- zettcode/tui/input/events.py +64 -0
- zettcode/tui/input/keys.py +289 -0
- zettcode/tui/input/reader.py +147 -0
- zettcode/tui/layout/__init__.py +24 -0
- zettcode/tui/layout/box.py +153 -0
- zettcode/tui/layout/overlay.py +164 -0
- zettcode/tui/layout/scroll.py +477 -0
- zettcode/tui/layout/solver.py +95 -0
- zettcode/tui/layout/spacing.py +137 -0
- zettcode/tui/py.typed +0 -0
- zettcode/tui/render/__init__.py +71 -0
- zettcode/tui/render/ansi.py +371 -0
- zettcode/tui/render/canvas.py +153 -0
- zettcode/tui/render/code.py +456 -0
- zettcode/tui/render/color.py +177 -0
- zettcode/tui/render/renderer.py +151 -0
- zettcode/tui/render/rich_text.py +80 -0
- zettcode/tui/render/style.py +121 -0
- zettcode/tui/render/sweep.py +87 -0
- zettcode/tui/render/text.py +319 -0
- zettcode/tui/runner.py +316 -0
- zettcode/tui/terminal.py +222 -0
- zettcode/tui/testing/__init__.py +6 -0
- zettcode/tui/testing/harness.py +220 -0
- zettcode/tui/testing/snapshot.py +64 -0
- zettcode/tui/theme_file.py +300 -0
- zettcode/tui/themes/ayu-mirage.toml +44 -0
- zettcode/tui/themes/catppuccin-latte.toml +44 -0
- zettcode/tui/themes/catppuccin-mocha.toml +44 -0
- zettcode/tui/themes/dracula.toml +44 -0
- zettcode/tui/themes/github-dimmed.toml +44 -0
- zettcode/tui/themes/github-light.toml +44 -0
- zettcode/tui/themes/gruvbox-dark.toml +44 -0
- zettcode/tui/themes/gruvbox-light.toml +44 -0
- zettcode/tui/themes/kanagawa.toml +44 -0
- zettcode/tui/themes/nord.toml +44 -0
- zettcode/tui/themes/one-dark.toml +44 -0
- zettcode/tui/themes/rose-pine-dawn.toml +44 -0
- zettcode/tui/themes/rose-pine.toml +44 -0
- zettcode/tui/themes/solarized-dark.toml +44 -0
- zettcode/tui/themes/solarized-light.toml +44 -0
- zettcode/tui/themes/tokyo-night.toml +44 -0
- zettcode/tui/widgets/__init__.py +121 -0
- zettcode/tui/widgets/collapsible.py +116 -0
- zettcode/tui/widgets/completion.py +211 -0
- zettcode/tui/widgets/console.py +442 -0
- zettcode/tui/widgets/dialog.py +165 -0
- zettcode/tui/widgets/diff.py +236 -0
- zettcode/tui/widgets/list.py +293 -0
- zettcode/tui/widgets/list_page.py +121 -0
- zettcode/tui/widgets/markdown.py +657 -0
- zettcode/tui/widgets/progress.py +166 -0
- zettcode/tui/widgets/rich_text.py +84 -0
- zettcode/tui/widgets/status.py +106 -0
- zettcode/tui/widgets/table.py +101 -0
- zettcode/tui/widgets/tasks.py +130 -0
- zettcode/tui/widgets/text.py +111 -0
- zettcode/tui/widgets/textarea.py +671 -0
- zettcode/tui/widgets/toast.py +137 -0
- zettcode_tui-0.4.0.dist-info/METADATA +20 -0
- zettcode_tui-0.4.0.dist-info/RECORD +83 -0
- zettcode_tui-0.4.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""A full-screen page built around a scrollable list."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
|
+
|
|
7
|
+
from zettcode.tui import SEPARATOR
|
|
8
|
+
|
|
9
|
+
from ..core.events import KeyEvent
|
|
10
|
+
from ..core.geometry import Rect
|
|
11
|
+
from ..core.host import Host
|
|
12
|
+
from ..core.widget import Widget
|
|
13
|
+
from ..render import Canvas, Style
|
|
14
|
+
from .list import ListItem, ListView, Parts, draw_parts
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ListPage(Widget):
|
|
18
|
+
"""An opaque page with a title, a scrolling list, and a footer hint.
|
|
19
|
+
|
|
20
|
+
Shape::
|
|
21
|
+
|
|
22
|
+
Title <- one row below the top edge
|
|
23
|
+
Subtitle <- when there is one, over the rows
|
|
24
|
+
> row one <- ListView: marker, band, scroll
|
|
25
|
+
row two
|
|
26
|
+
enter select \u00b7 esc back <- footer, on the bottom row
|
|
27
|
+
|
|
28
|
+
The page owns the frame so a caller only maps data to rows: it fills its
|
|
29
|
+
rectangle with ``surface_alt`` so whatever is underneath cannot show
|
|
30
|
+
through, bounds the list to ``visible_rows``, and turns Escape into
|
|
31
|
+
``on_cancel`` before the list can consume the key. Its rows wrap, as the
|
|
32
|
+
slash menu's already do: a picker whose last row refuses Down reads as one
|
|
33
|
+
that ignores the key, and with four models the reader presses Up once to reach
|
|
34
|
+
the last instead of three times to reach nothing. Paging wraps with it, since
|
|
35
|
+
one flag governs every move. It composes
|
|
36
|
+
:class:`ListView` rather than inheriting it, which keeps that list
|
|
37
|
+
general-purpose.
|
|
38
|
+
|
|
39
|
+
It paints into whatever rectangle it is given: the caller decides whether
|
|
40
|
+
that is the whole screen or a panel anchored to the bottom edge.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
items: Rows to show; the list scrolls past ``visible_rows``.
|
|
44
|
+
title: Drawn one row below the page's top edge.
|
|
45
|
+
subtitle: Drawn under the title for a page with a second fact to name —
|
|
46
|
+
the file a report came from, say. It costs one row of the page's
|
|
47
|
+
height, and is left out of the layout entirely when absent.
|
|
48
|
+
subtitle_parts: The subtitle in colours, which must read as ``subtitle`` when joined; ``None`` draws the
|
|
49
|
+
subtitle in the muted colour.
|
|
50
|
+
footer: Hint drawn on the bottom row.
|
|
51
|
+
on_select: Receives the row committed with Enter.
|
|
52
|
+
on_cancel: Called when the user presses Escape; ``None`` lets the key
|
|
53
|
+
pass through untouched.
|
|
54
|
+
selected: Index highlighted first, clamped into range.
|
|
55
|
+
visible_rows: Most rows shown at once; the list scrolls when there are
|
|
56
|
+
more. ``None`` fills the rectangle the page is given, which is what
|
|
57
|
+
a panel wants: its height already bounds the list.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
items: Sequence[ListItem],
|
|
63
|
+
*,
|
|
64
|
+
title: str,
|
|
65
|
+
subtitle: str | None = None,
|
|
66
|
+
subtitle_parts: Parts | None = None,
|
|
67
|
+
footer: str = f"enter select {SEPARATOR} esc back",
|
|
68
|
+
on_select: Callable[[ListItem], None] | None = None,
|
|
69
|
+
on_cancel: Callable[[], None] | None = None,
|
|
70
|
+
selected: int = 0,
|
|
71
|
+
visible_rows: int | None = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Build the page and its list, highlighting ``selected`` without notifying."""
|
|
74
|
+
super().__init__()
|
|
75
|
+
self.title = title
|
|
76
|
+
self.subtitle = subtitle
|
|
77
|
+
self.subtitle_parts = subtitle_parts
|
|
78
|
+
self.footer = footer
|
|
79
|
+
self.on_cancel = on_cancel
|
|
80
|
+
self.visible_rows = None if visible_rows is None else max(1, visible_rows)
|
|
81
|
+
self.list = ListView(items, on_select=on_select, wrap=True, band=True)
|
|
82
|
+
if items:
|
|
83
|
+
self.list.select(selected, notify=False)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def children(self) -> tuple[Widget, ...]:
|
|
87
|
+
"""Expose the list so it lays out, paints, and takes focus."""
|
|
88
|
+
return (self.list,)
|
|
89
|
+
|
|
90
|
+
def layout(self, rect: Rect) -> None:
|
|
91
|
+
"""Inset the list under the title, capped to the visible rows."""
|
|
92
|
+
super().layout(rect)
|
|
93
|
+
over = 1 if self.subtitle is not None else 0
|
|
94
|
+
room = max(0, rect.height - 5 - over)
|
|
95
|
+
height = room if self.visible_rows is None else min(self.visible_rows, room)
|
|
96
|
+
self.list.layout(Rect(rect.x + 2, rect.y + 3 + over, max(0, rect.width - 4), height))
|
|
97
|
+
|
|
98
|
+
def render(self, canvas: Canvas) -> None:
|
|
99
|
+
"""Fill the page, then draw the title, the list, and the footer."""
|
|
100
|
+
if self.rect.empty:
|
|
101
|
+
return
|
|
102
|
+
theme = self.theme
|
|
103
|
+
canvas.fill(self.rect.x, self.rect.y, self.rect.width, self.rect.height, Style(background=theme.surface_alt))
|
|
104
|
+
heading = Style(foreground=theme.text, background=theme.surface_alt, bold=True)
|
|
105
|
+
hint = Style(foreground=theme.muted, background=theme.surface_alt)
|
|
106
|
+
room = max(0, self.rect.width - 4)
|
|
107
|
+
canvas.draw_text(self.rect.x + 2, self.rect.y + 1, self.title, heading, max_width=room)
|
|
108
|
+
if self.subtitle is not None:
|
|
109
|
+
if self.subtitle_parts is None:
|
|
110
|
+
canvas.draw_text(self.rect.x + 2, self.rect.y + 2, self.subtitle, hint, max_width=room)
|
|
111
|
+
else:
|
|
112
|
+
draw_parts(canvas, self.rect.x + 2, self.rect.y + 2, self.subtitle_parts(theme), hint, max_width=room)
|
|
113
|
+
self.list.render(canvas)
|
|
114
|
+
canvas.draw_text(self.rect.x + 2, self.rect.bottom - 1, self.footer, hint, max_width=room)
|
|
115
|
+
|
|
116
|
+
def capture_event(self, event, host: Host) -> bool:
|
|
117
|
+
"""Close on Escape before the focused list can see the key."""
|
|
118
|
+
if isinstance(event, KeyEvent) and event.key == "escape" and self.on_cancel is not None:
|
|
119
|
+
self.on_cancel()
|
|
120
|
+
return True
|
|
121
|
+
return False
|