altero 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.
- altero/__init__.py +9 -0
- altero/__main__.py +6 -0
- altero/appearance.py +219 -0
- altero/assets/menubar-template.png +0 -0
- altero/assets/menubar-template@2x.png +0 -0
- altero/autoswitch.py +2859 -0
- altero/bundle.py +120 -0
- altero/cache.py +44 -0
- altero/claude_locks.py +187 -0
- altero/cli.py +1989 -0
- altero/credentials.py +1849 -0
- altero/exceptions.py +91 -0
- altero/fsutil.py +101 -0
- altero/json_output.py +289 -0
- altero/launch_agent.py +923 -0
- altero/locking.py +269 -0
- altero/logging_config.py +64 -0
- altero/macos_keychain.py +226 -0
- altero/mappings.py +142 -0
- altero/menubar.py +1288 -0
- altero/migrations.py +537 -0
- altero/models.py +211 -0
- altero/oauth.py +806 -0
- altero/pace.py +196 -0
- altero/paths.py +110 -0
- altero/poll_policy.py +270 -0
- altero/printer.py +232 -0
- altero/process_detection.py +350 -0
- altero/session.py +1499 -0
- altero/settings.py +527 -0
- altero/snapshot_json.py +295 -0
- altero/snapshot_source.py +100 -0
- altero/state_watch.py +110 -0
- altero/switcher.py +7449 -0
- altero/transfer.py +645 -0
- altero/tui/__init__.py +66 -0
- altero/tui/altero.tcss +205 -0
- altero/tui/app.py +467 -0
- altero/tui/autoview.py +454 -0
- altero/tui/dashboard.py +432 -0
- altero/tui/data.py +194 -0
- altero/tui/modals.py +159 -0
- altero/tui/theme.py +141 -0
- altero/tui/widgets.py +403 -0
- altero/update_check.py +215 -0
- altero/usage_history.py +145 -0
- altero/usage_store.py +1234 -0
- altero/widget_requests.py +217 -0
- altero-0.1.0.dist-info/METADATA +166 -0
- altero-0.1.0.dist-info/RECORD +53 -0
- altero-0.1.0.dist-info/WHEEL +4 -0
- altero-0.1.0.dist-info/entry_points.txt +2 -0
- altero-0.1.0.dist-info/licenses/LICENSE +22 -0
altero/__init__.py
ADDED
altero/__main__.py
ADDED
altero/appearance.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Terminal appearance detection and theme resolution.
|
|
2
|
+
|
|
3
|
+
Determines whether the terminal has a light or dark background by querying it
|
|
4
|
+
with OSC 11 (``ESC ] 11 ; ? BEL``) followed by DA1 (``ESC [ c``), reading
|
|
5
|
+
through the ordered DA1 reply, and classifying the preceding ``rgb:…`` reply by
|
|
6
|
+
perceived luminance. Cross-cutting: both the CLI printer and the TUI resolve
|
|
7
|
+
their theme through here.
|
|
8
|
+
|
|
9
|
+
The query MUST happen while this process owns the terminal in cooked mode —
|
|
10
|
+
before Textual's input driver starts — or the reply is reissued as keystrokes.
|
|
11
|
+
Everything fails safe to ``None`` (→ resolved ``dark``): a terminal that doesn't
|
|
12
|
+
answer, a pipe, Windows, or a parse failure never blocks indefinitely and never
|
|
13
|
+
errors.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import select
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
_QUERY = b"\x1b]11;?\x07" # OSC 11, BEL-terminated
|
|
25
|
+
_DA1_QUERY = b"\x1b[c" # ordered response boundary
|
|
26
|
+
# DA1 lets unsupported terminals return promptly; the cap mainly covers SSH
|
|
27
|
+
# latency or a non-conforming terminal that answers neither query.
|
|
28
|
+
_TIMEOUT_S = 1.0
|
|
29
|
+
_MAX_REPLY = 256
|
|
30
|
+
# The full `ESC ]11;` opener is required so interleaved or echoed input (e.g.
|
|
31
|
+
# a shell echoing back a pasted escape sequence) can't be misparsed as a
|
|
32
|
+
# background reply just because `]11;rgb:`/`]11;#` appears somewhere in the
|
|
33
|
+
# buffer without the ESC that actually starts an OSC sequence.
|
|
34
|
+
_RGB = re.compile(
|
|
35
|
+
rb"\x1b\]11;rgb:([0-9a-fA-F]+)/([0-9a-fA-F]+)/([0-9a-fA-F]+)"
|
|
36
|
+
rb"(?:\x07|\x1b\\)"
|
|
37
|
+
)
|
|
38
|
+
_HEX = re.compile(rb"\x1b\]11;#([0-9a-fA-F]{6})(?:\x07|\x1b\\)")
|
|
39
|
+
# A primary device-attributes reply is CSI ? Ps c. Requiring ``?`` and at
|
|
40
|
+
# least the private marker keeps an echoed DA1 query (CSI c) from looking
|
|
41
|
+
# complete; Ps itself may legally be empty.
|
|
42
|
+
_DA1_REPLY = re.compile(rb"(?:\x1b\[|\x9b)\?[0-9;:]*c")
|
|
43
|
+
|
|
44
|
+
# Cache: the terminal background can't change within a process, so query once.
|
|
45
|
+
_UNSET = object()
|
|
46
|
+
_cache: object | str | None = _UNSET
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _reset_cache() -> None:
|
|
50
|
+
"""Test helper: forget any cached detection result."""
|
|
51
|
+
global _cache
|
|
52
|
+
_cache = _UNSET
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_osc11(reply: bytes) -> tuple[float, float, float] | None:
|
|
56
|
+
"""Parse an OSC 11 reply into (r, g, b) each normalised to 0..1."""
|
|
57
|
+
m = _RGB.search(reply)
|
|
58
|
+
if m:
|
|
59
|
+
return tuple(
|
|
60
|
+
int(h, 16) / (16 ** len(h) - 1) for h in m.groups()
|
|
61
|
+
) # type: ignore[return-value]
|
|
62
|
+
m = _HEX.search(reply)
|
|
63
|
+
if m:
|
|
64
|
+
h = m.group(1).decode("ascii")
|
|
65
|
+
return tuple(int(h[i:i + 2], 16) / 255 for i in (0, 2, 4)) # type: ignore[return-value]
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _classify(reply: bytes) -> str | None:
|
|
70
|
+
"""Light/dark from an OSC 11 reply, or None if unparseable."""
|
|
71
|
+
rgb = _parse_osc11(reply)
|
|
72
|
+
if rgb is None:
|
|
73
|
+
return None
|
|
74
|
+
r, g, b = rgb
|
|
75
|
+
luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b # BT.709-weighted brightness on gamma-encoded channels (approx.)
|
|
76
|
+
return "light" if luminance > 0.5 else "dark"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _query_terminal_background() -> bytes | None:
|
|
80
|
+
"""Send OSC 11 + DA1 and read through the DA1 reply. None on any failure.
|
|
81
|
+
|
|
82
|
+
Isolated so tests can substitute a canned reply without a real tty.
|
|
83
|
+
|
|
84
|
+
Terminals process queries in order. DA1 is widely supported, so its reply
|
|
85
|
+
marks the point after any OSC 11 reply and prevents a slower colour reply
|
|
86
|
+
from being reissued as shell input after this process restores the tty.
|
|
87
|
+
The timeout remains a safety cap for high-latency or non-conforming
|
|
88
|
+
terminals.
|
|
89
|
+
|
|
90
|
+
``TERM=dumb`` and the Linux console don't support this colour query.
|
|
91
|
+
Likewise, tmux and screen don't pass it through to the outer terminal by
|
|
92
|
+
default. Those environments short-circuit to ``None`` (resolving to
|
|
93
|
+
``dark``) without probing. Fails safe either way.
|
|
94
|
+
"""
|
|
95
|
+
if os.name == "nt":
|
|
96
|
+
return None
|
|
97
|
+
if os.environ.get("TERM") in ("dumb", "linux"):
|
|
98
|
+
return None
|
|
99
|
+
if os.environ.get("TMUX") or os.environ.get("STY"):
|
|
100
|
+
return None
|
|
101
|
+
try:
|
|
102
|
+
import termios
|
|
103
|
+
import tty
|
|
104
|
+
except ImportError:
|
|
105
|
+
return None
|
|
106
|
+
try:
|
|
107
|
+
if not (sys.stdin.isatty() and sys.stdout.isatty()):
|
|
108
|
+
return None
|
|
109
|
+
except (ValueError, OSError):
|
|
110
|
+
# isatty() can raise on a closed stream.
|
|
111
|
+
return None
|
|
112
|
+
try:
|
|
113
|
+
fd = sys.stdin.fileno()
|
|
114
|
+
old = termios.tcgetattr(fd)
|
|
115
|
+
except (termios.error, OSError):
|
|
116
|
+
return None
|
|
117
|
+
try:
|
|
118
|
+
# TCSANOW (not TCSADRAIN): draining first can block under terminal
|
|
119
|
+
# flow control, and there's no pending output to drain anyway.
|
|
120
|
+
tty.setcbreak(fd, termios.TCSANOW)
|
|
121
|
+
sys.stdout.write((_QUERY + _DA1_QUERY).decode("latin-1"))
|
|
122
|
+
sys.stdout.flush()
|
|
123
|
+
deadline = time.monotonic() + _TIMEOUT_S
|
|
124
|
+
buf = b""
|
|
125
|
+
while time.monotonic() < deadline and len(buf) < _MAX_REPLY:
|
|
126
|
+
remaining = deadline - time.monotonic()
|
|
127
|
+
ready, _, _ = select.select([fd], [], [], max(0.0, remaining))
|
|
128
|
+
if not ready:
|
|
129
|
+
break
|
|
130
|
+
chunk = os.read(fd, 32)
|
|
131
|
+
if not chunk:
|
|
132
|
+
break
|
|
133
|
+
buf += chunk
|
|
134
|
+
# Do not stop at the OSC reply: the DA1 response is the ordered
|
|
135
|
+
# boundary proving that no colour-query bytes are still in flight.
|
|
136
|
+
if _DA1_REPLY.search(buf) is not None:
|
|
137
|
+
break
|
|
138
|
+
return buf or None
|
|
139
|
+
except (termios.error, OSError, ValueError):
|
|
140
|
+
return None
|
|
141
|
+
finally:
|
|
142
|
+
try:
|
|
143
|
+
termios.tcsetattr(fd, termios.TCSANOW, old)
|
|
144
|
+
except (termios.error, OSError):
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def detect_terminal_background() -> str | None:
|
|
149
|
+
"""'light' | 'dark' from the terminal background, or None if undetectable.
|
|
150
|
+
|
|
151
|
+
Cached per process. MUST be first called in cooked mode (before app.run()).
|
|
152
|
+
"""
|
|
153
|
+
global _cache
|
|
154
|
+
if _cache is _UNSET:
|
|
155
|
+
reply = _query_terminal_background()
|
|
156
|
+
_cache = _classify(reply) if reply is not None else None
|
|
157
|
+
return _cache # type: ignore[return-value]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def resolve_theme(setting: str, detect=detect_terminal_background) -> str:
|
|
161
|
+
"""Resolve a ui.theme setting to a concrete 'light'/'dark'.
|
|
162
|
+
|
|
163
|
+
'dark'/'light' pass through without probing; 'auto' follows ``detect()``,
|
|
164
|
+
falling back to 'dark' when detection yields None.
|
|
165
|
+
"""
|
|
166
|
+
if setting in ("dark", "light"):
|
|
167
|
+
return setting
|
|
168
|
+
return detect() or "dark"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def cli_should_probe(argv: list[str], *, colors_enabled: bool) -> bool:
|
|
172
|
+
"""Whether the CLI should probe the terminal background before dispatch.
|
|
173
|
+
|
|
174
|
+
False when colors are off (nothing will render the theme anyway), when
|
|
175
|
+
the first token is ``run`` (execs a child that takes over the terminal)
|
|
176
|
+
or ``snapshot`` (stdout is always machine-readable there), or when
|
|
177
|
+
``--json`` is present (the OSC query must never precede machine-readable
|
|
178
|
+
output on stdout).
|
|
179
|
+
"""
|
|
180
|
+
if not colors_enabled:
|
|
181
|
+
return False
|
|
182
|
+
if argv and argv[0] in ("run", "snapshot"):
|
|
183
|
+
return False
|
|
184
|
+
if "--json" in argv:
|
|
185
|
+
return False
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def cli_theme(setting: str, *, detect=detect_terminal_background, colors: bool) -> str:
|
|
190
|
+
"""Resolve a theme for a plain-CLI invocation: probe only when color will
|
|
191
|
+
actually be emitted; otherwise auto degrades to dark without a tty query."""
|
|
192
|
+
if setting == "auto" and not colors:
|
|
193
|
+
return "dark"
|
|
194
|
+
return resolve_theme(setting, detect=detect)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def drain_stdin() -> None:
|
|
198
|
+
"""Discard any pending terminal input (e.g. a late OSC reply) so it isn't
|
|
199
|
+
reissued as keystrokes once Textual takes over. Best-effort; POSIX only.
|
|
200
|
+
|
|
201
|
+
A reply that arrives after the detection deadline and after this drain
|
|
202
|
+
can, in principle, still reach the running app as stray keystrokes —
|
|
203
|
+
inherent to any finite-timeout OSC probe, not fully closeable here.
|
|
204
|
+
"""
|
|
205
|
+
if os.name == "nt":
|
|
206
|
+
return
|
|
207
|
+
try:
|
|
208
|
+
import termios
|
|
209
|
+
except ImportError:
|
|
210
|
+
return
|
|
211
|
+
try:
|
|
212
|
+
if not sys.stdin.isatty():
|
|
213
|
+
return
|
|
214
|
+
except (ValueError, OSError):
|
|
215
|
+
return
|
|
216
|
+
try:
|
|
217
|
+
termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
|
|
218
|
+
except (termios.error, OSError):
|
|
219
|
+
pass
|
|
Binary file
|
|
Binary file
|