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.
Files changed (83) hide show
  1. zettcode/tui/__init__.py +422 -0
  2. zettcode/tui/_compat.py +32 -0
  3. zettcode/tui/capabilities.py +107 -0
  4. zettcode/tui/clipboard.py +91 -0
  5. zettcode/tui/console.py +229 -0
  6. zettcode/tui/core/__init__.py +63 -0
  7. zettcode/tui/core/app.py +658 -0
  8. zettcode/tui/core/events.py +134 -0
  9. zettcode/tui/core/focus.py +85 -0
  10. zettcode/tui/core/geometry.py +212 -0
  11. zettcode/tui/core/host.py +66 -0
  12. zettcode/tui/core/keymap.py +218 -0
  13. zettcode/tui/core/scheduler.py +105 -0
  14. zettcode/tui/core/screen.py +80 -0
  15. zettcode/tui/core/theme.py +248 -0
  16. zettcode/tui/core/widget.py +204 -0
  17. zettcode/tui/gallery.py +588 -0
  18. zettcode/tui/glyphs.py +73 -0
  19. zettcode/tui/input/__init__.py +18 -0
  20. zettcode/tui/input/bridge.py +46 -0
  21. zettcode/tui/input/decoder.py +234 -0
  22. zettcode/tui/input/events.py +64 -0
  23. zettcode/tui/input/keys.py +289 -0
  24. zettcode/tui/input/reader.py +147 -0
  25. zettcode/tui/layout/__init__.py +24 -0
  26. zettcode/tui/layout/box.py +153 -0
  27. zettcode/tui/layout/overlay.py +164 -0
  28. zettcode/tui/layout/scroll.py +477 -0
  29. zettcode/tui/layout/solver.py +95 -0
  30. zettcode/tui/layout/spacing.py +137 -0
  31. zettcode/tui/py.typed +0 -0
  32. zettcode/tui/render/__init__.py +71 -0
  33. zettcode/tui/render/ansi.py +371 -0
  34. zettcode/tui/render/canvas.py +153 -0
  35. zettcode/tui/render/code.py +456 -0
  36. zettcode/tui/render/color.py +177 -0
  37. zettcode/tui/render/renderer.py +151 -0
  38. zettcode/tui/render/rich_text.py +80 -0
  39. zettcode/tui/render/style.py +121 -0
  40. zettcode/tui/render/sweep.py +87 -0
  41. zettcode/tui/render/text.py +319 -0
  42. zettcode/tui/runner.py +316 -0
  43. zettcode/tui/terminal.py +222 -0
  44. zettcode/tui/testing/__init__.py +6 -0
  45. zettcode/tui/testing/harness.py +220 -0
  46. zettcode/tui/testing/snapshot.py +64 -0
  47. zettcode/tui/theme_file.py +300 -0
  48. zettcode/tui/themes/ayu-mirage.toml +44 -0
  49. zettcode/tui/themes/catppuccin-latte.toml +44 -0
  50. zettcode/tui/themes/catppuccin-mocha.toml +44 -0
  51. zettcode/tui/themes/dracula.toml +44 -0
  52. zettcode/tui/themes/github-dimmed.toml +44 -0
  53. zettcode/tui/themes/github-light.toml +44 -0
  54. zettcode/tui/themes/gruvbox-dark.toml +44 -0
  55. zettcode/tui/themes/gruvbox-light.toml +44 -0
  56. zettcode/tui/themes/kanagawa.toml +44 -0
  57. zettcode/tui/themes/nord.toml +44 -0
  58. zettcode/tui/themes/one-dark.toml +44 -0
  59. zettcode/tui/themes/rose-pine-dawn.toml +44 -0
  60. zettcode/tui/themes/rose-pine.toml +44 -0
  61. zettcode/tui/themes/solarized-dark.toml +44 -0
  62. zettcode/tui/themes/solarized-light.toml +44 -0
  63. zettcode/tui/themes/tokyo-night.toml +44 -0
  64. zettcode/tui/widgets/__init__.py +121 -0
  65. zettcode/tui/widgets/collapsible.py +116 -0
  66. zettcode/tui/widgets/completion.py +211 -0
  67. zettcode/tui/widgets/console.py +442 -0
  68. zettcode/tui/widgets/dialog.py +165 -0
  69. zettcode/tui/widgets/diff.py +236 -0
  70. zettcode/tui/widgets/list.py +293 -0
  71. zettcode/tui/widgets/list_page.py +121 -0
  72. zettcode/tui/widgets/markdown.py +657 -0
  73. zettcode/tui/widgets/progress.py +166 -0
  74. zettcode/tui/widgets/rich_text.py +84 -0
  75. zettcode/tui/widgets/status.py +106 -0
  76. zettcode/tui/widgets/table.py +101 -0
  77. zettcode/tui/widgets/tasks.py +130 -0
  78. zettcode/tui/widgets/text.py +111 -0
  79. zettcode/tui/widgets/textarea.py +671 -0
  80. zettcode/tui/widgets/toast.py +137 -0
  81. zettcode_tui-0.4.0.dist-info/METADATA +20 -0
  82. zettcode_tui-0.4.0.dist-info/RECORD +83 -0
  83. 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