shellscripts 0.23.1__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.
- shell_scripts/__init__.py +5 -0
- shell_scripts/__main__.py +5 -0
- shell_scripts/commands/__init__.py +81 -0
- shell_scripts/commands/_dc_common.py +138 -0
- shell_scripts/commands/ai_install.py +453 -0
- shell_scripts/commands/bin_links.py +78 -0
- shell_scripts/commands/claude.py +56 -0
- shell_scripts/commands/clean.py +98 -0
- shell_scripts/commands/codex.py +107 -0
- shell_scripts/commands/copilot.py +54 -0
- shell_scripts/commands/dicom2jpg.py +98 -0
- shell_scripts/commands/dicomviewer.py +83 -0
- shell_scripts/commands/diff_cmd.py +50 -0
- shell_scripts/commands/doxygen_cmd.py +159 -0
- shell_scripts/commands/edit_cmd.py +50 -0
- shell_scripts/commands/gemini.py +54 -0
- shell_scripts/commands/kiro.py +54 -0
- shell_scripts/commands/opencode.py +54 -0
- shell_scripts/commands/pdf_crop.py +520 -0
- shell_scripts/commands/pdf_merge.py +180 -0
- shell_scripts/commands/pdf_split_by_format.py +243 -0
- shell_scripts/commands/pdf_split_by_toc.py +197 -0
- shell_scripts/commands/pdf_tiler_090.py +61 -0
- shell_scripts/commands/pdf_tiler_100.py +61 -0
- shell_scripts/commands/pdf_toc_clean.py +175 -0
- shell_scripts/commands/pi.py +61 -0
- shell_scripts/commands/req_cmd.py +362 -0
- shell_scripts/commands/tests_cmd.py +87 -0
- shell_scripts/commands/ubuntu_dark_theme.py +64 -0
- shell_scripts/commands/venv_cmd.py +82 -0
- shell_scripts/commands/video2h264.py +89 -0
- shell_scripts/commands/video2h265.py +87 -0
- shell_scripts/commands/view_cmd.py +50 -0
- shell_scripts/commands/vscode_cmd.py +42 -0
- shell_scripts/commands/vsinsider_cmd.py +42 -0
- shell_scripts/config.py +328 -0
- shell_scripts/core.py +324 -0
- shell_scripts/utils.py +455 -0
- shell_scripts/version_check.py +323 -0
- shellscripts-0.23.1.dist-info/METADATA +310 -0
- shellscripts-0.23.1.dist-info/RECORD +45 -0
- shellscripts-0.23.1.dist-info/WHEEL +5 -0
- shellscripts-0.23.1.dist-info/entry_points.txt +3 -0
- shellscripts-0.23.1.dist-info/licenses/LICENSE +674 -0
- shellscripts-0.23.1.dist-info/top_level.txt +1 -0
shell_scripts/utils.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""@brief Shared CLI utility helpers for platform, subprocess, and output handling.
|
|
3
|
+
|
|
4
|
+
@details Provides reusable terminal formatting utilities, project-root
|
|
5
|
+
resolution, platform detection with runtime OS caching, command availability
|
|
6
|
+
checks, and terminal-state restoration primitives used by CLI command wrappers.
|
|
7
|
+
@satisfies CTN-003, CTN-005, DES-002, REQ-047, REQ-055, REQ-056, REQ-064, REQ-065
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import subprocess
|
|
13
|
+
import shutil
|
|
14
|
+
import shlex
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import overload
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import termios
|
|
20
|
+
except ImportError:
|
|
21
|
+
termios = None
|
|
22
|
+
|
|
23
|
+
RESET = "\033[0m"
|
|
24
|
+
BOLD = "\033[1m"
|
|
25
|
+
RED = "\033[31m"
|
|
26
|
+
GREEN = "\033[32m"
|
|
27
|
+
YELLOW = "\033[33m"
|
|
28
|
+
BLUE = "\033[34m"
|
|
29
|
+
MAGENTA = "\033[35m"
|
|
30
|
+
CYAN = "\033[36m"
|
|
31
|
+
WHITE = "\033[37m"
|
|
32
|
+
BRIGHT_RED = "\033[91m"
|
|
33
|
+
BRIGHT_GREEN = "\033[92m"
|
|
34
|
+
BRIGHT_YELLOW = "\033[93m"
|
|
35
|
+
BRIGHT_BLUE = "\033[94m"
|
|
36
|
+
BRIGHT_CYAN = "\033[96m"
|
|
37
|
+
BRIGHT_WHITE = "\033[97m"
|
|
38
|
+
|
|
39
|
+
## @var _RUNTIME_OS
|
|
40
|
+
# @brief Cached normalized runtime operating-system token.
|
|
41
|
+
# @details Initialized on first detection and reused to guarantee stable
|
|
42
|
+
# startup-level OS semantics across command execution flow.
|
|
43
|
+
# @satisfies DES-002, REQ-047
|
|
44
|
+
_RUNTIME_OS = None
|
|
45
|
+
|
|
46
|
+
## @var _MOUSE_OFF_ESCAPE_SEQUENCE
|
|
47
|
+
# @brief Escape-sequence payload that disables known xterm mouse-reporting modes.
|
|
48
|
+
# @details Concatenates CSI mode-off controls for legacy X10 mode (`?9l`) and
|
|
49
|
+
# common xterm mouse tracking modes (`?1000l`..`?1016l`).
|
|
50
|
+
# @satisfies REQ-064, REQ-065
|
|
51
|
+
_MOUSE_OFF_ESCAPE_SEQUENCE = (
|
|
52
|
+
"\x1b[?9l"
|
|
53
|
+
"\x1b[?1000l"
|
|
54
|
+
"\x1b[?1001l"
|
|
55
|
+
"\x1b[?1002l"
|
|
56
|
+
"\x1b[?1003l"
|
|
57
|
+
"\x1b[?1004l"
|
|
58
|
+
"\x1b[?1005l"
|
|
59
|
+
"\x1b[?1006l"
|
|
60
|
+
"\x1b[?1007l"
|
|
61
|
+
"\x1b[?1015l"
|
|
62
|
+
"\x1b[?1016l"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def color_enabled():
|
|
67
|
+
if os.environ.get("NO_COLOR"):
|
|
68
|
+
return False
|
|
69
|
+
return sys.stdout.isatty()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def c(text, color):
|
|
73
|
+
if color_enabled():
|
|
74
|
+
return f"{color}{text}{RESET}"
|
|
75
|
+
return str(text)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def print_info(msg):
|
|
79
|
+
print(c(f"[INFO] {msg}", BRIGHT_BLUE + BOLD))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def print_error(msg):
|
|
83
|
+
print(c(f"[ERROR] {msg}", BRIGHT_RED + BOLD), file=sys.stderr)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def print_warn(msg):
|
|
87
|
+
print(c(f"[WARN] {msg}", BRIGHT_YELLOW + BOLD), file=sys.stderr)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def print_success(msg):
|
|
91
|
+
print(c(f"[OK] {msg}", BRIGHT_GREEN + BOLD))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_project_root():
|
|
95
|
+
try:
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
98
|
+
capture_output=True, text=True, timeout=5
|
|
99
|
+
)
|
|
100
|
+
if result.returncode == 0:
|
|
101
|
+
return Path(result.stdout.strip())
|
|
102
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
103
|
+
pass
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def require_project_root():
|
|
108
|
+
require_commands("git")
|
|
109
|
+
root = get_project_root()
|
|
110
|
+
if root is None:
|
|
111
|
+
print_error("Cannot determine the project root. Are you inside a git repository?")
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
return root
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def detect_runtime_os():
|
|
117
|
+
"""@brief Detect and cache runtime operating-system token.
|
|
118
|
+
|
|
119
|
+
@details Normalizes `sys.platform` into deterministic categories
|
|
120
|
+
(`windows`, `linux`, `darwin`, `other`) and stores the result in module
|
|
121
|
+
cache for subsequent calls. Time complexity O(1).
|
|
122
|
+
@return {str} Normalized runtime operating-system token.
|
|
123
|
+
@satisfies DES-002, REQ-047
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
global _RUNTIME_OS
|
|
127
|
+
platform_token = sys.platform.lower()
|
|
128
|
+
if platform_token.startswith("win"):
|
|
129
|
+
_RUNTIME_OS = "windows"
|
|
130
|
+
elif platform_token.startswith("linux"):
|
|
131
|
+
_RUNTIME_OS = "linux"
|
|
132
|
+
elif platform_token.startswith("darwin"):
|
|
133
|
+
_RUNTIME_OS = "darwin"
|
|
134
|
+
else:
|
|
135
|
+
_RUNTIME_OS = "other"
|
|
136
|
+
return _RUNTIME_OS
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_runtime_os():
|
|
140
|
+
"""@brief Return cached runtime operating-system token.
|
|
141
|
+
|
|
142
|
+
@details Lazily initializes the cache via `detect_runtime_os` when unset,
|
|
143
|
+
preserving a single startup-consistent OS classification.
|
|
144
|
+
@return {str} Normalized runtime operating-system token.
|
|
145
|
+
@satisfies DES-002, REQ-047
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
if _RUNTIME_OS is None:
|
|
149
|
+
return detect_runtime_os()
|
|
150
|
+
return _RUNTIME_OS
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def is_windows():
|
|
154
|
+
"""@brief Check whether runtime operating system is Windows.
|
|
155
|
+
|
|
156
|
+
@details Evaluates cached runtime token from `get_runtime_os`.
|
|
157
|
+
@return {bool} `True` when runtime OS is Windows; otherwise `False`.
|
|
158
|
+
@satisfies DES-013, REQ-008, REQ-047
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
return get_runtime_os() == "windows"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def is_linux():
|
|
165
|
+
"""@brief Check whether runtime operating system is Linux.
|
|
166
|
+
|
|
167
|
+
@details Evaluates cached runtime token from `get_runtime_os`.
|
|
168
|
+
@return {bool} `True` when runtime OS is Linux; otherwise `False`.
|
|
169
|
+
@satisfies CTN-004, REQ-004, REQ-005, REQ-047
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
return get_runtime_os() == "linux"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _is_executable_file(path: Path) -> bool:
|
|
176
|
+
"""@brief Validate executable-file capability for a filesystem path.
|
|
177
|
+
|
|
178
|
+
@details Expands user-home markers, checks path kind, and applies platform
|
|
179
|
+
executable checks. On Windows, missing-extension candidates are validated
|
|
180
|
+
against `PATHEXT` suffixes.
|
|
181
|
+
@param path {Path} Candidate executable filesystem path.
|
|
182
|
+
@return {bool} `True` when the path resolves to an executable file.
|
|
183
|
+
@satisfies CTN-003, REQ-055
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
return _resolve_executable_file(path) is not None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _resolve_executable_file(path: Path) -> Path | None:
|
|
190
|
+
"""@brief Resolve an executable filesystem path for the runtime OS.
|
|
191
|
+
|
|
192
|
+
@details Expands user-home markers and resolves Windows extension-less
|
|
193
|
+
candidates through `PATHEXT` fallback checks.
|
|
194
|
+
@param path {Path} Candidate executable filesystem path.
|
|
195
|
+
@return {Path | None} Resolved executable path when runnable, else `None`.
|
|
196
|
+
@satisfies CTN-003, REQ-055
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
expanded = path.expanduser()
|
|
200
|
+
if expanded.suffix:
|
|
201
|
+
if expanded.is_file() and os.access(str(expanded), os.X_OK):
|
|
202
|
+
return expanded
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
if expanded.is_file() and os.access(str(expanded), os.X_OK):
|
|
206
|
+
return expanded
|
|
207
|
+
|
|
208
|
+
if get_runtime_os() != "windows":
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
path_ext = os.environ.get("PATHEXT", ".EXE;.CMD;.BAT;.COM")
|
|
212
|
+
for ext in [item for item in path_ext.split(";") if item]:
|
|
213
|
+
candidate = Path(f"{expanded}{ext}")
|
|
214
|
+
if candidate.is_file() and os.access(str(candidate), os.X_OK):
|
|
215
|
+
return candidate
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def resolve_executable_command(command: str) -> str | None:
|
|
220
|
+
"""@brief Resolve a runnable command token to an executable path.
|
|
221
|
+
|
|
222
|
+
@details Accepts command names or executable paths and returns the concrete
|
|
223
|
+
runnable executable path when available on the current runtime platform.
|
|
224
|
+
@param command {str} Command token or filesystem path to executable.
|
|
225
|
+
@return {str | None} Resolved executable path or `None` when not runnable.
|
|
226
|
+
@satisfies CTN-003, REQ-055
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
if not isinstance(command, str) or not command.strip():
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
normalized = command.strip()
|
|
233
|
+
path_like = os.sep in normalized or (os.altsep is not None and os.altsep in normalized)
|
|
234
|
+
if path_like or Path(normalized).is_absolute():
|
|
235
|
+
resolved_path = _resolve_executable_file(Path(normalized))
|
|
236
|
+
return str(resolved_path) if resolved_path is not None else None
|
|
237
|
+
|
|
238
|
+
return shutil.which(normalized)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def is_executable_command(command: str) -> bool:
|
|
242
|
+
"""@brief Determine whether an external command is executable on runtime OS.
|
|
243
|
+
|
|
244
|
+
@details Accepts command names or executable paths. Name-based checks use
|
|
245
|
+
`PATH` resolution via `shutil.which`; path-based checks verify executable
|
|
246
|
+
file metadata, including Windows `PATHEXT` variants.
|
|
247
|
+
@param command {str} Command token or filesystem path to executable.
|
|
248
|
+
@return {bool} `True` when command is executable; otherwise `False`.
|
|
249
|
+
@satisfies CTN-003, REQ-055
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
return resolve_executable_command(command) is not None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def command_exists(cmd):
|
|
256
|
+
return is_executable_command(cmd)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@overload
|
|
260
|
+
def require_commands(cmd: str, /) -> str:
|
|
261
|
+
...
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@overload
|
|
265
|
+
def require_commands(cmd1: str, cmd2: str, /, *cmds: str) -> list[str]:
|
|
266
|
+
...
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def require_commands(*cmds: str) -> str | list[str]:
|
|
270
|
+
resolved = []
|
|
271
|
+
for cmd in cmds:
|
|
272
|
+
resolved_cmd = resolve_executable_command(cmd)
|
|
273
|
+
if resolved_cmd is None:
|
|
274
|
+
print_error(f"Command not executable: {cmd}")
|
|
275
|
+
sys.exit(1)
|
|
276
|
+
resolved.append(resolved_cmd)
|
|
277
|
+
if len(resolved) == 1:
|
|
278
|
+
return resolved[0]
|
|
279
|
+
return resolved
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _is_shell_assignment_token(token):
|
|
283
|
+
"""@brief Check whether a token is a shell variable assignment.
|
|
284
|
+
|
|
285
|
+
@details Matches `NAME=value` form where `NAME` obeys shell identifier
|
|
286
|
+
syntax and therefore does not represent an executable token.
|
|
287
|
+
@param token {str} Shell token candidate.
|
|
288
|
+
@return {bool} `True` when token is an assignment expression.
|
|
289
|
+
@satisfies REQ-056
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
if "=" not in token:
|
|
293
|
+
return False
|
|
294
|
+
key = token.split("=", 1)[0]
|
|
295
|
+
return key.replace("_", "a").isalnum() and not key[0].isdigit()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def extract_shell_executables(command_line):
|
|
299
|
+
"""@brief Extract executable tokens from a shell command line.
|
|
300
|
+
|
|
301
|
+
@details Tokenizes command line using runtime-OS splitting mode and returns
|
|
302
|
+
ordered unique executable candidates at command boundaries (`&&`, `||`,
|
|
303
|
+
`;`, `|`) including wrapper commands such as `sudo`.
|
|
304
|
+
@param command_line {str} Raw shell command line.
|
|
305
|
+
@return {list[str]} Ordered executable token list.
|
|
306
|
+
@satisfies REQ-056
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
if not command_line.strip():
|
|
310
|
+
return []
|
|
311
|
+
try:
|
|
312
|
+
tokens = shlex.split(command_line, posix=get_runtime_os() != "windows")
|
|
313
|
+
except ValueError:
|
|
314
|
+
tokens = command_line.split()
|
|
315
|
+
|
|
316
|
+
boundaries = {"&&", "||", ";", "|"}
|
|
317
|
+
wrapper_commands = {"sudo", "env", "command", "nohup", "time"}
|
|
318
|
+
executables = []
|
|
319
|
+
expect_executable = True
|
|
320
|
+
|
|
321
|
+
for token in tokens:
|
|
322
|
+
if token in boundaries:
|
|
323
|
+
expect_executable = True
|
|
324
|
+
continue
|
|
325
|
+
if not expect_executable:
|
|
326
|
+
continue
|
|
327
|
+
if _is_shell_assignment_token(token):
|
|
328
|
+
continue
|
|
329
|
+
executables.append(token)
|
|
330
|
+
expect_executable = token in wrapper_commands
|
|
331
|
+
|
|
332
|
+
seen = set()
|
|
333
|
+
ordered = []
|
|
334
|
+
for token in executables:
|
|
335
|
+
if token in seen:
|
|
336
|
+
continue
|
|
337
|
+
seen.add(token)
|
|
338
|
+
ordered.append(token)
|
|
339
|
+
return ordered
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def require_shell_command_executables(command_line):
|
|
343
|
+
"""@brief Validate executable availability for a shell command line.
|
|
344
|
+
|
|
345
|
+
@details Extracts executable tokens from `command_line`, validates each
|
|
346
|
+
token via `is_executable_command`, prints deterministic error with missing
|
|
347
|
+
command name, and terminates process on first failure.
|
|
348
|
+
@param command_line {str} Raw shell command line.
|
|
349
|
+
@return {None} Process exits on validation failure.
|
|
350
|
+
@satisfies REQ-056
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
for token in extract_shell_executables(command_line):
|
|
354
|
+
if not is_executable_command(token):
|
|
355
|
+
print_error(f"Command not executable: {token}")
|
|
356
|
+
sys.exit(1)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _is_tty_stream(stream):
|
|
360
|
+
"""@brief Determine whether a stream is an attached TTY.
|
|
361
|
+
|
|
362
|
+
@details Performs capability checks (`isatty`, callable) and returns
|
|
363
|
+
deterministic boolean without raising on unsupported stream objects.
|
|
364
|
+
Time complexity O(1).
|
|
365
|
+
@param stream {object} Stream-like object (stdin/stdout/stderr candidate).
|
|
366
|
+
@return {bool} `True` when stream supports and reports TTY attachment.
|
|
367
|
+
@satisfies REQ-064
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
isatty = getattr(stream, "isatty", None)
|
|
371
|
+
if not callable(isatty):
|
|
372
|
+
return False
|
|
373
|
+
try:
|
|
374
|
+
return bool(isatty())
|
|
375
|
+
except OSError:
|
|
376
|
+
return False
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def capture_terminal_state():
|
|
380
|
+
"""@brief Capture current stdin terminal attributes when available.
|
|
381
|
+
|
|
382
|
+
@details Reads current TTY attributes via `termios.tcgetattr` only on
|
|
383
|
+
runtimes exposing `termios` and when stdin is a TTY. Returns `None` when
|
|
384
|
+
attributes are unavailable. Time complexity O(1).
|
|
385
|
+
@return {list[object] | None} Saved TTY attributes for later restoration.
|
|
386
|
+
@satisfies REQ-064
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
if termios is None or not _is_tty_stream(sys.stdin):
|
|
390
|
+
return None
|
|
391
|
+
try:
|
|
392
|
+
stdin_fd = sys.stdin.fileno()
|
|
393
|
+
except (AttributeError, OSError, ValueError):
|
|
394
|
+
return None
|
|
395
|
+
tcgetattr = getattr(termios, "tcgetattr", None)
|
|
396
|
+
if not callable(tcgetattr):
|
|
397
|
+
return None
|
|
398
|
+
try:
|
|
399
|
+
return tcgetattr(stdin_fd)
|
|
400
|
+
except OSError:
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def reset_terminal_state(saved_tty=None):
|
|
405
|
+
"""@brief Restore terminal raw/cbreak and mouse-tracking state.
|
|
406
|
+
|
|
407
|
+
@details Restores previously captured stdin termios attributes when present,
|
|
408
|
+
writes `_MOUSE_OFF_ESCAPE_SEQUENCE` (`?9l`, `?1000l`, `?1001l`, `?1002l`,
|
|
409
|
+
`?1003l`, `?1004l`, `?1005l`, `?1006l`, `?1007l`, `?1015l`, `?1016l`) to
|
|
410
|
+
TTY stdout, and best-effort runs `stty sane` for Git Bash/Unix-compatible
|
|
411
|
+
terminals. Failures are ignored to preserve wrapper exit semantics. Time
|
|
412
|
+
complexity O(1).
|
|
413
|
+
@param saved_tty {list[object] | None} Attributes from `capture_terminal_state`.
|
|
414
|
+
@return {None} Performs best-effort terminal-state restoration.
|
|
415
|
+
@satisfies REQ-064, REQ-065
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
tcsetattr = None
|
|
419
|
+
tcsa_drain = None
|
|
420
|
+
if saved_tty is not None and termios is not None and _is_tty_stream(sys.stdin):
|
|
421
|
+
tcsetattr = getattr(termios, "tcsetattr", None)
|
|
422
|
+
tcsa_drain = getattr(termios, "TCSADRAIN", None)
|
|
423
|
+
if (
|
|
424
|
+
saved_tty is not None
|
|
425
|
+
and _is_tty_stream(sys.stdin)
|
|
426
|
+
and callable(tcsetattr)
|
|
427
|
+
and tcsa_drain is not None
|
|
428
|
+
):
|
|
429
|
+
try:
|
|
430
|
+
tcsetattr(sys.stdin.fileno(), tcsa_drain, saved_tty)
|
|
431
|
+
except (AttributeError, OSError, ValueError):
|
|
432
|
+
pass
|
|
433
|
+
|
|
434
|
+
if _is_tty_stream(sys.stdout):
|
|
435
|
+
try:
|
|
436
|
+
sys.stdout.write(_MOUSE_OFF_ESCAPE_SEQUENCE)
|
|
437
|
+
sys.stdout.flush()
|
|
438
|
+
except OSError:
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
if _is_tty_stream(sys.stdin):
|
|
442
|
+
try:
|
|
443
|
+
subprocess.run(
|
|
444
|
+
["stty", "sane"],
|
|
445
|
+
stdin=sys.stdin,
|
|
446
|
+
stdout=subprocess.DEVNULL,
|
|
447
|
+
stderr=subprocess.DEVNULL,
|
|
448
|
+
check=False,
|
|
449
|
+
)
|
|
450
|
+
except OSError:
|
|
451
|
+
pass
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def run_cmd(cmd, **kwargs):
|
|
455
|
+
return subprocess.run(cmd, **kwargs)
|