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.
@@ -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)