syncmill 0.2.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.
- syncmill/__init__.py +11 -0
- syncmill/agents.py +2064 -0
- syncmill/artifact_io.py +201 -0
- syncmill/artifacts.py +1946 -0
- syncmill/board.py +2060 -0
- syncmill/breaker.py +327 -0
- syncmill/cli.py +4986 -0
- syncmill/command_errors.py +32 -0
- syncmill/config.py +1118 -0
- syncmill/dashboard.py +935 -0
- syncmill/gates.py +179 -0
- syncmill/gateway.py +234 -0
- syncmill/git_env.py +99 -0
- syncmill/isolated_session.py +431 -0
- syncmill/matcher.py +127 -0
- syncmill/memory.py +1067 -0
- syncmill/memory_backend.py +1907 -0
- syncmill/memory_migration.py +962 -0
- syncmill/model_routing.py +482 -0
- syncmill/models.py +244 -0
- syncmill/monitor.py +775 -0
- syncmill/phoenix_exporter.py +339 -0
- syncmill/planning.py +512 -0
- syncmill/platform_support.py +14 -0
- syncmill/preflight.py +1129 -0
- syncmill/project.py +1067 -0
- syncmill/report.py +502 -0
- syncmill/review_candidates.py +107 -0
- syncmill/room_compare.py +2380 -0
- syncmill/room_raw.py +484 -0
- syncmill/room_viewer.py +604 -0
- syncmill/rooms.py +4044 -0
- syncmill/session.py +444 -0
- syncmill/strategies/__init__.py +82 -0
- syncmill/strategies/base.py +910 -0
- syncmill/strategies/compete.py +233 -0
- syncmill/strategies/council.py +429 -0
- syncmill/strategies/decompose.py +570 -0
- syncmill/strategies/pipeline.py +221 -0
- syncmill/strategies/route.py +130 -0
- syncmill/supervisor.py +1423 -0
- syncmill/teamfile.py +571 -0
- syncmill/telemetry_hook.py +231 -0
- syncmill/terminal.py +245 -0
- syncmill/tracer.py +548 -0
- syncmill/tui.py +3537 -0
- syncmill/tui_collaboration.py +499 -0
- syncmill/tui_jobs.py +1407 -0
- syncmill/tui_render.py +156 -0
- syncmill/workflow.py +3746 -0
- syncmill/worktree.py +1045 -0
- syncmill-0.2.0.dist-info/METADATA +853 -0
- syncmill-0.2.0.dist-info/RECORD +56 -0
- syncmill-0.2.0.dist-info/WHEEL +4 -0
- syncmill-0.2.0.dist-info/entry_points.txt +2 -0
- syncmill-0.2.0.dist-info/licenses/LICENSE +191 -0
syncmill/tui_render.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""What the Textual apps (``tui``, ``dashboard``) put on the terminal (#210).
|
|
2
|
+
|
|
3
|
+
Textual removes only BEL, BS, VT, FF and CR from text it displays; ESC, C1
|
|
4
|
+
controls (U+009B is CSI) and bidi overrides reach the terminal, so stored or
|
|
5
|
+
agent-authored text could write the clipboard (OSC 52), clear the screen or
|
|
6
|
+
reorder what a line appears to say. Three pieces close that:
|
|
7
|
+
|
|
8
|
+
- ``install_render_guard`` replaces ``textual.strip.Strip.render``, through which
|
|
9
|
+
every compositor update turns widgets into terminal bytes. Each dangerous
|
|
10
|
+
character in a segment becomes a character of the same cell width that a
|
|
11
|
+
terminal does not interpret: U+200B, or U+FFFD where an old rich Unicode table
|
|
12
|
+
(``UNICODE_VERSION``) gives the character a width. Layout was computed before
|
|
13
|
+
this point, so a replacement must not change any width; deleting would (a
|
|
14
|
+
ZWJ next to a removed ESC joins a different grapheme). A link whose URL has
|
|
15
|
+
such a character is dropped.
|
|
16
|
+
- ``error_console`` replaces ``App.error_console``, which prints fatal errors
|
|
17
|
+
outside ``Strip.render``; its output is escaped and uncoloured.
|
|
18
|
+
- ``PlainStatic``/``PlainLabel`` and ``plain`` keep markup off for text syncmill
|
|
19
|
+
did not write: Textual parses ``[link=...]`` and ``[/b]`` in a string, and a
|
|
20
|
+
malformed tag raises while the widget renders.
|
|
21
|
+
|
|
22
|
+
The guard drops characters silently; showing them as escapes, as the CLI does,
|
|
23
|
+
is the job of the renderer that builds the text.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from rich.cells import cell_len
|
|
29
|
+
from rich.console import Console
|
|
30
|
+
from rich.text import Text
|
|
31
|
+
from textual.widgets import Label, Select, Static
|
|
32
|
+
from textual.widgets.option_list import Option
|
|
33
|
+
|
|
34
|
+
from syncmill.artifacts import printable_name, printable_text
|
|
35
|
+
from syncmill.terminal import EscapedStderr
|
|
36
|
+
|
|
37
|
+
_BIDI = "\u061c\u200e\u200f\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069"
|
|
38
|
+
DANGEROUS = frozenset(
|
|
39
|
+
[chr(c) for c in range(0x20)] + [chr(c) for c in range(0x7F, 0xA0)]
|
|
40
|
+
+ list(_BIDI) + ["\u2028", "\u2029"]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _substitutions() -> dict[int, str]:
|
|
45
|
+
"""Each dangerous character's same-width replacement, under the rich
|
|
46
|
+
Unicode table this process uses (read once, like rich itself)."""
|
|
47
|
+
table = {}
|
|
48
|
+
for ch in DANGEROUS:
|
|
49
|
+
width = cell_len(ch)
|
|
50
|
+
if width == cell_len("\u200b"):
|
|
51
|
+
table[ord(ch)] = "\u200b"
|
|
52
|
+
elif width == cell_len("\ufffd"):
|
|
53
|
+
table[ord(ch)] = "\ufffd"
|
|
54
|
+
else:
|
|
55
|
+
raise RuntimeError(f"no same-width replacement for U+{ord(ch):04X}")
|
|
56
|
+
return table
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _safe_link(url: str) -> bool:
|
|
60
|
+
return printable_name(url) == url
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def install_render_guard() -> None:
|
|
64
|
+
"""Replace ``Strip.render`` with the guarded version, once per process.
|
|
65
|
+
|
|
66
|
+
Same output as Textual's for text without dangerous characters, same
|
|
67
|
+
caching; called by the apps' entry points before ``App.run``, never at
|
|
68
|
+
import."""
|
|
69
|
+
from rich.color import ColorSystem
|
|
70
|
+
from textual.strip import Strip
|
|
71
|
+
|
|
72
|
+
if getattr(Strip.render, "_syncmill_guard", False):
|
|
73
|
+
return
|
|
74
|
+
table = _substitutions()
|
|
75
|
+
|
|
76
|
+
def render(self: Strip, console: Console) -> str:
|
|
77
|
+
if self._render_cache is None:
|
|
78
|
+
color_system = console._color_system or ColorSystem.TRUECOLOR
|
|
79
|
+
render_style = self.render_style
|
|
80
|
+
parts = []
|
|
81
|
+
for text, style, _ in self._segments:
|
|
82
|
+
text = text.translate(table)
|
|
83
|
+
if style is None:
|
|
84
|
+
parts.append(text)
|
|
85
|
+
continue
|
|
86
|
+
if style.link is not None and not _safe_link(style.link):
|
|
87
|
+
style = style.update_link(None)
|
|
88
|
+
parts.append(render_style(style, text, color_system=color_system))
|
|
89
|
+
self._render_cache = "".join(parts)
|
|
90
|
+
return self._render_cache
|
|
91
|
+
|
|
92
|
+
render._syncmill_guard = True
|
|
93
|
+
Strip.render = render
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def error_console() -> Console:
|
|
97
|
+
"""A console for ``App.error_console``: Textual prints fatal tracebacks and
|
|
98
|
+
exit messages through it, not through ``Strip.render``. No colour, so rich
|
|
99
|
+
writes no escape sequence of its own, and everything is escaped."""
|
|
100
|
+
return Console(
|
|
101
|
+
file=EscapedStderr(), markup=False, highlight=False, color_system=None
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def plain(value: object) -> Text:
|
|
106
|
+
"""``value`` as plain, one-line text for a widget that parses markup in a
|
|
107
|
+
``str`` (DataTable cells and column labels, Select labels), escaped as the
|
|
108
|
+
CLI shows a one-line value."""
|
|
109
|
+
return Text(printable_name(str(value)))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _shown(content: object) -> object:
|
|
113
|
+
"""A string as the CLI shows a body (``printable_text``, idempotent on text
|
|
114
|
+
a renderer already escaped); anything else unchanged."""
|
|
115
|
+
return printable_text(content) if isinstance(content, str) else content
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class PlainStatic(Static):
|
|
119
|
+
"""A ``Static`` that never parses markup and shows a string escaped: what
|
|
120
|
+
it shows is often stored text, errors and statuses."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, content: object = "", *args, **kwargs) -> None:
|
|
123
|
+
kwargs["markup"] = False
|
|
124
|
+
super().__init__(_shown(content), *args, **kwargs)
|
|
125
|
+
|
|
126
|
+
def update(self, content: object = "", *args, **kwargs) -> None:
|
|
127
|
+
super().update(_shown(content), *args, **kwargs)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _one_line(content: object) -> object:
|
|
131
|
+
"""A string as the CLI shows a one-line value (``printable_name``)."""
|
|
132
|
+
return printable_name(content) if isinstance(content, str) else content
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class PlainLabel(Label):
|
|
136
|
+
"""A ``Label`` that never parses markup and shows a string as one escaped
|
|
137
|
+
line: labels are titles and field names."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, content: object = "", *args, **kwargs) -> None:
|
|
140
|
+
kwargs["markup"] = False
|
|
141
|
+
super().__init__(_one_line(content), *args, **kwargs)
|
|
142
|
+
|
|
143
|
+
def update(self, content: object = "", *args, **kwargs) -> None:
|
|
144
|
+
super().update(_one_line(content), *args, **kwargs)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def plain_select(options, **kwargs) -> Select:
|
|
148
|
+
"""A ``Select`` whose option labels are plain text."""
|
|
149
|
+
return Select([(plain(label), value) for label, value in options], **kwargs)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class PlainOption(Option):
|
|
153
|
+
"""An ``OptionList`` entry whose string prompt is shown as one escaped line."""
|
|
154
|
+
|
|
155
|
+
def __init__(self, prompt: object, id: str | None = None, disabled: bool = False):
|
|
156
|
+
super().__init__(_one_line(prompt), id=id, disabled=disabled)
|