AQIT 0.1.0
Loading...
Searching...
No Matches
detect.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""Detect whether the current environment can run the Aquin Textual TUI."""
3
4from __future__ import annotations
5
6import importlib.util
7import os
8import sys
9from collections.abc import Callable, Mapping, Sequence
10from dataclasses import dataclass
11from typing import Literal
12
13Platform = Literal["linux", "darwin", "windows", "unknown"]
14
15_DUMB_TERMS = frozenset({"", "dumb", "unknown"})
16_CI_TRUTHY = frozenset({"1", "true", "yes", "on"})
18
19@dataclass(frozen=True)
21 platform: Platform
22 tty_ok: bool
23 term_ok: bool
24 textual_ok: bool
25 tui_ok: bool
26 reason: str
27
28
29def detect_platform(*, sys_platform: str | None = None) -> Platform:
30 """Map ``sys.platform`` to a coarse OS bucket used for TUI routing."""
31 raw = (sys_platform if sys_platform is not None else sys.platform).lower()
32 if raw.startswith("linux"):
33 return "linux"
34 if raw == "darwin":
35 return "darwin"
36 if raw == "win32":
37 return "windows"
38 return "unknown"
39
40
41def _truthy_env(value: str | None) -> bool:
42 return (value or "").strip().lower() in _CI_TRUTHY
43
44
45def _opt_out_reason(argv: Sequence[str], environ: Mapping[str, str]) -> str | None:
46 if "--plain" in argv:
47 return "opt_out: --plain"
48 if _truthy_env(environ.get("NO_TUI")):
49 return "opt_out: NO_TUI"
50 if _truthy_env(environ.get("CI")):
51 return "opt_out: CI"
52 return None
53
54
55def _tty_ok(
56 *,
57 stdin_isatty: Callable[[], bool],
58 stdout_isatty: Callable[[], bool],
59) -> tuple[bool, str]:
60 if not stdin_isatty():
61 return False, "not_tty: stdin"
62 if not stdout_isatty():
63 return False, "not_tty: stdout"
64 return True, "ok"
65
66
67def _posix_term_ok(environ: Mapping[str, str]) -> tuple[bool, str]:
68 term = (environ.get("TERM") or "").strip().lower()
69 if term in _DUMB_TERMS:
70 if not term:
71 return False, "term_dumb: TERM unset"
72 return False, f"term_dumb: TERM={term}"
73 return True, "ok"
74
75
76def _windows_vt_from_environ(environ: Mapping[str, str]) -> bool:
77 if environ.get("WT_SESSION"):
78 return True
79 if environ.get("TERM_PROGRAM") in {"vscode", "Cursor", "iTerm.app"}:
80 return True
81 # Cursor / VS Code integrated terminals often expose these.
82 if environ.get("VSCODE_INJECTION") or environ.get("CURSOR_TRACE_ID"):
83 return True
84 term = (environ.get("TERM") or "").strip().lower()
85 if term.startswith("xterm") or term.startswith("tmux") or term == "ansi":
86 return True
87 return False
88
89
90def _windows_vt_capable_default() -> bool:
91 """Return whether stdout is a VT-capable Windows console."""
92 if _windows_vt_from_environ(os.environ):
93 return True
95 try:
96 import ctypes
97 from ctypes import wintypes
98 except Exception:
99 return False
100
101 kernel32 = ctypes.windll.kernel32
102 std_output_handle = -11
103 enable_virtual_terminal_processing = 0x0004
104
105 handle = kernel32.GetStdHandle(std_output_handle)
106 if handle == wintypes.HANDLE(-1).value:
107 return False
108
109 mode = ctypes.c_uint()
110 if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
111 return False
112
113 if mode.value & enable_virtual_terminal_processing:
114 return True
115
116 new_mode = mode.value | enable_virtual_terminal_processing
117 if kernel32.SetConsoleMode(handle, new_mode):
118 kernel32.SetConsoleMode(handle, mode.value)
119 return True
120 return False
121
122
123def _term_ok(
124 platform: Platform,
125 environ: Mapping[str, str],
126 *,
127 windows_vt_capable: Callable[[], bool],
128) -> tuple[bool, str]:
129 if platform == "windows":
130 if _windows_vt_from_environ(environ) or windows_vt_capable():
131 return True, "ok"
132 return False, "term_not_vt_capable"
133
134 # Linux, Darwin, and unknown POSIX-like hosts use TERM.
135 return _posix_term_ok(environ)
136
137
138def _textual_importable_default() -> bool:
139 return importlib.util.find_spec("textual") is not None
140
141
143 *,
144 argv: Sequence[str] | None = None,
145 environ: Mapping[str, str] | None = None,
146 stdin_isatty: Callable[[], bool] | None = None,
147 stdout_isatty: Callable[[], bool] | None = None,
148 sys_platform: str | None = None,
149 textual_importable: Callable[[], bool] | None = None,
150 windows_vt_capable: Callable[[], bool] | None = None,
151) -> TuiCapability:
152 """Return whether the Aquin Textual TUI should launch for this process."""
153 argv = list(argv if argv is not None else sys.argv)
154 environ = dict(environ if environ is not None else os.environ)
155 stdin_isatty = stdin_isatty or sys.stdin.isatty
156 stdout_isatty = stdout_isatty or sys.stdout.isatty
157 textual_importable = textual_importable or _textual_importable_default
158 windows_vt_capable = windows_vt_capable or _windows_vt_capable_default
159
160 platform = detect_platform(sys_platform=sys_platform)
161
162 opt_out = _opt_out_reason(argv, environ)
163 if opt_out:
164 return TuiCapability(
165 platform=platform,
166 tty_ok=False,
167 term_ok=False,
168 textual_ok=False,
169 tui_ok=False,
170 reason=opt_out,
171 )
172
173 tty_ok, tty_reason = _tty_ok(
174 stdin_isatty=stdin_isatty,
175 stdout_isatty=stdout_isatty,
176 )
177 if not tty_ok:
178 return TuiCapability(
179 platform=platform,
180 tty_ok=False,
181 term_ok=False,
182 textual_ok=False,
183 tui_ok=False,
184 reason=tty_reason,
185 )
186
187 term_ok, term_reason = _term_ok(
188 platform,
189 environ,
190 windows_vt_capable=windows_vt_capable,
191 )
192 if not term_ok:
193 return TuiCapability(
194 platform=platform,
195 tty_ok=True,
196 term_ok=False,
197 textual_ok=False,
198 tui_ok=False,
199 reason=term_reason,
200 )
201
202 textual_ok = textual_importable()
203 if not textual_ok:
204 return TuiCapability(
205 platform=platform,
206 tty_ok=True,
207 term_ok=True,
208 textual_ok=False,
209 tui_ok=False,
210 reason="textual_not_installed",
211 )
212
213 return TuiCapability(
214 platform=platform,
215 tty_ok=True,
216 term_ok=True,
217 textual_ok=True,
218 tui_ok=True,
219 reason="ok",
220 )
tuple[bool, str] _term_ok(Platform platform, Mapping[str, str] environ, *, Callable[[], bool] windows_vt_capable)
Definition detect.py:132
TuiCapability detect_tui_capability(*, Sequence[str]|None argv=None, Mapping[str, str]|None environ=None, Callable[[], bool]|None stdin_isatty=None, Callable[[], bool]|None stdout_isatty=None, str|None sys_platform=None, Callable[[], bool]|None textual_importable=None, Callable[[], bool]|None windows_vt_capable=None)
Definition detect.py:155
bool _windows_vt_capable_default()
Definition detect.py:94
bool _truthy_env(str|None value)
Definition detect.py:45
bool _windows_vt_from_environ(Mapping[str, str] environ)
Definition detect.py:80
str|None _opt_out_reason(Sequence[str] argv, Mapping[str, str] environ)
Definition detect.py:49
Platform detect_platform(*, str|None sys_platform=None)
Definition detect.py:33
tuple[bool, str] _posix_term_ok(Mapping[str, str] environ)
Definition detect.py:71
tuple[bool, str] _tty_ok(*, Callable[[], bool] stdin_isatty, Callable[[], bool] stdout_isatty)
Definition detect.py:63
bool _textual_importable_default()
Definition detect.py:142