zettcode 0.1.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/__init__.py +6 -0
- zettcode/cli.py +78 -0
- zettcode/config.py +72 -0
- zettcode/runtime.py +121 -0
- zettcode/tui.py +924 -0
- zettcode/tui_framework/__init__.py +29 -0
- zettcode/tui_framework/application.py +91 -0
- zettcode/tui_framework/components.py +821 -0
- zettcode/tui_framework/events.py +37 -0
- zettcode/tui_framework/input.py +163 -0
- zettcode/tui_framework/screen.py +214 -0
- zettcode/tui_framework/terminal.py +118 -0
- zettcode-0.1.0.dist-info/METADATA +109 -0
- zettcode-0.1.0.dist-info/RECORD +17 -0
- zettcode-0.1.0.dist-info/WHEEL +4 -0
- zettcode-0.1.0.dist-info/entry_points.txt +2 -0
- zettcode-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""ZettCode's internally implemented terminal UI framework."""
|
|
2
|
+
|
|
3
|
+
from .application import Application
|
|
4
|
+
from .components import BoxChild, Component, HBox, Rect, Rule, ScrollableText, Text, TextInput, VBox
|
|
5
|
+
from .events import EventType, InputEvent, MouseAction
|
|
6
|
+
from .screen import Canvas, DifferentialRenderer, Span, Style, TextLine
|
|
7
|
+
from .terminal import Terminal
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Application",
|
|
11
|
+
"BoxChild",
|
|
12
|
+
"Canvas",
|
|
13
|
+
"Component",
|
|
14
|
+
"DifferentialRenderer",
|
|
15
|
+
"EventType",
|
|
16
|
+
"HBox",
|
|
17
|
+
"InputEvent",
|
|
18
|
+
"MouseAction",
|
|
19
|
+
"Rect",
|
|
20
|
+
"Rule",
|
|
21
|
+
"ScrollableText",
|
|
22
|
+
"Span",
|
|
23
|
+
"Style",
|
|
24
|
+
"Terminal",
|
|
25
|
+
"Text",
|
|
26
|
+
"TextInput",
|
|
27
|
+
"TextLine",
|
|
28
|
+
"VBox",
|
|
29
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Single-loop component application with differential repainting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import base64
|
|
7
|
+
|
|
8
|
+
import pyperclip
|
|
9
|
+
|
|
10
|
+
from .components import Component, Rect
|
|
11
|
+
from .events import EventType, InputEvent
|
|
12
|
+
from .screen import Canvas, DifferentialRenderer
|
|
13
|
+
from .terminal import AsyncInput, Terminal
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Application:
|
|
17
|
+
"""Own terminal state, focus, input dispatch, and frame scheduling."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, root: Component, *, terminal: Terminal | None = None) -> None:
|
|
20
|
+
self.root = root
|
|
21
|
+
self.terminal = terminal or Terminal()
|
|
22
|
+
self.renderer = DifferentialRenderer(self.terminal.output)
|
|
23
|
+
self.queue: asyncio.Queue[InputEvent] = asyncio.Queue()
|
|
24
|
+
self.focused: Component | None = None
|
|
25
|
+
self.running = False
|
|
26
|
+
self._render_pending = False
|
|
27
|
+
|
|
28
|
+
async def run(self) -> None:
|
|
29
|
+
self.running = True
|
|
30
|
+
with self.terminal:
|
|
31
|
+
input_reader = AsyncInput(self.terminal, self.publish)
|
|
32
|
+
input_reader.start()
|
|
33
|
+
try:
|
|
34
|
+
self.invalidate()
|
|
35
|
+
while self.running:
|
|
36
|
+
event = await self.queue.get()
|
|
37
|
+
if event.type == EventType.RENDER:
|
|
38
|
+
self._render_pending = False
|
|
39
|
+
self._render()
|
|
40
|
+
continue
|
|
41
|
+
if event.type == EventType.RESIZE:
|
|
42
|
+
self.renderer.reset()
|
|
43
|
+
handled = self.root.handle(event, self)
|
|
44
|
+
if handled or event.type == EventType.RESIZE:
|
|
45
|
+
self.invalidate()
|
|
46
|
+
finally:
|
|
47
|
+
input_reader.close()
|
|
48
|
+
|
|
49
|
+
def publish(self, event: InputEvent) -> None:
|
|
50
|
+
self.queue.put_nowait(event)
|
|
51
|
+
|
|
52
|
+
def invalidate(self) -> None:
|
|
53
|
+
if self._render_pending:
|
|
54
|
+
return
|
|
55
|
+
self._render_pending = True
|
|
56
|
+
self.publish(InputEvent(EventType.RENDER))
|
|
57
|
+
|
|
58
|
+
def focus(self, component: Component) -> None:
|
|
59
|
+
if self.focused is component:
|
|
60
|
+
return
|
|
61
|
+
if self.focused is not None and hasattr(self.focused, "focused"):
|
|
62
|
+
self.focused.focused = False
|
|
63
|
+
self.focused = component
|
|
64
|
+
if hasattr(component, "focused"):
|
|
65
|
+
component.focused = True
|
|
66
|
+
|
|
67
|
+
def copy(self, text: str) -> None:
|
|
68
|
+
if not text:
|
|
69
|
+
return
|
|
70
|
+
try:
|
|
71
|
+
pyperclip.copy(text)
|
|
72
|
+
except pyperclip.PyperclipException:
|
|
73
|
+
encoded = base64.b64encode(text.encode()).decode()
|
|
74
|
+
self.terminal.write(f"\x1b]52;c;{encoded}\x07")
|
|
75
|
+
|
|
76
|
+
def refresh(self) -> None:
|
|
77
|
+
"""Force a complete repaint, matching the conventional Ctrl-L action."""
|
|
78
|
+
self.renderer.reset()
|
|
79
|
+
self.invalidate()
|
|
80
|
+
|
|
81
|
+
def exit(self) -> None:
|
|
82
|
+
self.running = False
|
|
83
|
+
self.publish(InputEvent(EventType.RENDER))
|
|
84
|
+
|
|
85
|
+
def _render(self) -> None:
|
|
86
|
+
width, height = self.terminal.size
|
|
87
|
+
canvas = Canvas(width, height)
|
|
88
|
+
self.root.layout(Rect(0, 0, width, height))
|
|
89
|
+
self.root.render(canvas)
|
|
90
|
+
cursor = self.focused.cursor() if self.focused is not None else self.root.cursor()
|
|
91
|
+
self.renderer.render(canvas, cursor=cursor)
|