ptn 0.2.5__py3-none-any.whl → 0.3.2__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.
- porterminal/__init__.py +63 -11
- porterminal/_version.py +2 -2
- porterminal/app.py +25 -1
- porterminal/application/ports/__init__.py +2 -0
- porterminal/application/ports/connection_registry_port.py +46 -0
- porterminal/application/services/management_service.py +2 -3
- porterminal/cli/args.py +53 -0
- porterminal/cli/display.py +1 -1
- porterminal/composition.py +6 -0
- porterminal/config.py +8 -0
- porterminal/container.py +4 -0
- porterminal/infrastructure/auth.py +131 -0
- porterminal/infrastructure/cloudflared.py +5 -1
- porterminal/infrastructure/config/shell_detector.py +341 -1
- porterminal/infrastructure/server.py +28 -3
- porterminal/static/assets/app-BkHv5qu0.css +32 -0
- porterminal/static/assets/app-CaIGfw7i.js +72 -0
- porterminal/static/assets/app-D9ELFbEO.js +72 -0
- porterminal/static/assets/app-DF3nl_io.js +72 -0
- porterminal/static/assets/{app-By4EXMHC.js → app-DoBiVkTD.js} +12 -12
- porterminal/static/assets/app-azbHOsRw.css +32 -0
- porterminal/static/assets/app-nMNFwMa6.css +32 -0
- porterminal/static/index.html +14 -2
- {ptn-0.2.5.dist-info → ptn-0.3.2.dist-info}/METADATA +36 -13
- {ptn-0.2.5.dist-info → ptn-0.3.2.dist-info}/RECORD +28 -20
- {ptn-0.2.5.dist-info → ptn-0.3.2.dist-info}/WHEEL +0 -0
- {ptn-0.2.5.dist-info → ptn-0.3.2.dist-info}/entry_points.txt +0 -0
- {ptn-0.2.5.dist-info → ptn-0.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
"""Shell detection for available shells on the system."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
3
8
|
import shutil
|
|
9
|
+
import subprocess
|
|
4
10
|
import sys
|
|
5
11
|
from pathlib import Path
|
|
6
12
|
|
|
7
13
|
from porterminal.domain import ShellCommand
|
|
8
14
|
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
9
17
|
|
|
10
18
|
class ShellDetector:
|
|
11
19
|
"""Detect available shells on the current platform."""
|
|
@@ -48,13 +56,20 @@ class ShellDetector:
|
|
|
48
56
|
List of (name, id, command, args) tuples.
|
|
49
57
|
"""
|
|
50
58
|
if sys.platform == "win32":
|
|
51
|
-
|
|
59
|
+
# Get Windows Terminal profiles first, then hardcoded defaults
|
|
60
|
+
wt_profiles = self._get_windows_terminal_profiles()
|
|
61
|
+
hardcoded = [
|
|
52
62
|
("PS 7", "pwsh", "pwsh.exe", ["-NoLogo"]),
|
|
53
63
|
("PS", "powershell", "powershell.exe", ["-NoLogo"]),
|
|
54
64
|
("CMD", "cmd", "cmd.exe", []),
|
|
55
65
|
("WSL", "wsl", "wsl.exe", []),
|
|
56
66
|
("Git Bash", "gitbash", r"C:\Program Files\Git\bin\bash.exe", ["--login"]),
|
|
57
67
|
]
|
|
68
|
+
# Merge WT profiles with hardcoded (dedupe by command)
|
|
69
|
+
merged = self._merge_candidates(wt_profiles, hardcoded)
|
|
70
|
+
# Add VS dev shells (no deduplication - they're unique due to args)
|
|
71
|
+
vs_shells = self._get_visual_studio_shells()
|
|
72
|
+
return merged + vs_shells
|
|
58
73
|
return [
|
|
59
74
|
("Bash", "bash", "bash", ["--login"]),
|
|
60
75
|
("Zsh", "zsh", "zsh", ["--login"]),
|
|
@@ -62,6 +77,298 @@ class ShellDetector:
|
|
|
62
77
|
("Sh", "sh", "sh", []),
|
|
63
78
|
]
|
|
64
79
|
|
|
80
|
+
def _get_windows_terminal_profiles(self) -> list[tuple[str, str, str, list[str]]]:
|
|
81
|
+
"""Read shell profiles from Windows Terminal settings.json.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
List of (name, id, command, args) tuples from Windows Terminal.
|
|
85
|
+
"""
|
|
86
|
+
settings_path = Path(
|
|
87
|
+
os.environ.get("LOCALAPPDATA", ""),
|
|
88
|
+
"Packages",
|
|
89
|
+
"Microsoft.WindowsTerminal_8wekyb3d8bbwe",
|
|
90
|
+
"LocalState",
|
|
91
|
+
"settings.json",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if not settings_path.exists():
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
# Read and parse JSON (WT uses JSON with comments, strip them)
|
|
99
|
+
content = settings_path.read_text(encoding="utf-8")
|
|
100
|
+
content = self._strip_json_comments(content)
|
|
101
|
+
data = json.loads(content)
|
|
102
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
103
|
+
logger.warning("Failed to read Windows Terminal settings: %s", e)
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
profiles = []
|
|
107
|
+
profile_list = data.get("profiles", {}).get("list", [])
|
|
108
|
+
|
|
109
|
+
for profile in profile_list:
|
|
110
|
+
name = profile.get("name", "")
|
|
111
|
+
commandline = profile.get("commandline", "")
|
|
112
|
+
|
|
113
|
+
if not name or not commandline:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
# Parse commandline into command and args
|
|
117
|
+
command, args = self._parse_commandline(commandline)
|
|
118
|
+
if not command:
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
# Expand environment variables in command path
|
|
122
|
+
command = os.path.expandvars(command)
|
|
123
|
+
|
|
124
|
+
# Create a slug ID from the name
|
|
125
|
+
shell_id = self._slugify(name)
|
|
126
|
+
|
|
127
|
+
# Shorten display name
|
|
128
|
+
short_name = self._abbreviate_name(name)
|
|
129
|
+
|
|
130
|
+
profiles.append((short_name, shell_id, command, args))
|
|
131
|
+
|
|
132
|
+
return profiles
|
|
133
|
+
|
|
134
|
+
def _get_visual_studio_shells(self) -> list[tuple[str, str, str, list[str]]]:
|
|
135
|
+
"""Detect Visual Studio Developer Command Prompts and PowerShells.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of (name, id, command, args) tuples for VS dev shells.
|
|
139
|
+
"""
|
|
140
|
+
vswhere = Path(
|
|
141
|
+
os.environ.get("ProgramFiles(x86)", ""),
|
|
142
|
+
"Microsoft Visual Studio",
|
|
143
|
+
"Installer",
|
|
144
|
+
"vswhere.exe",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if not vswhere.exists():
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
result = subprocess.run(
|
|
152
|
+
[
|
|
153
|
+
str(vswhere),
|
|
154
|
+
"-all",
|
|
155
|
+
"-prerelease",
|
|
156
|
+
"-property",
|
|
157
|
+
"installationPath",
|
|
158
|
+
],
|
|
159
|
+
capture_output=True,
|
|
160
|
+
text=True,
|
|
161
|
+
timeout=5,
|
|
162
|
+
)
|
|
163
|
+
vs_paths = [p.strip() for p in result.stdout.strip().split("\n") if p.strip()]
|
|
164
|
+
except (subprocess.TimeoutExpired, OSError) as e:
|
|
165
|
+
logger.warning("Failed to run vswhere: %s", e)
|
|
166
|
+
return []
|
|
167
|
+
|
|
168
|
+
shells = []
|
|
169
|
+
for vs_path in vs_paths:
|
|
170
|
+
vs_path = Path(vs_path)
|
|
171
|
+
# Extract VS version and edition from path
|
|
172
|
+
# e.g., "C:\Program Files\Microsoft Visual Studio\2022\Community"
|
|
173
|
+
edition = vs_path.name # Community, Professional, Enterprise
|
|
174
|
+
year = vs_path.parent.name # 2022, 2019, etc.
|
|
175
|
+
|
|
176
|
+
# Developer Command Prompt (CMD)
|
|
177
|
+
vsdevcmd = vs_path / "Common7" / "Tools" / "VsDevCmd.bat"
|
|
178
|
+
if vsdevcmd.exists():
|
|
179
|
+
name = f"Dev CMD {year}"
|
|
180
|
+
shell_id = f"devcmd-{year}-{edition.lower()}"
|
|
181
|
+
# cmd.exe /k "path\to\VsDevCmd.bat"
|
|
182
|
+
shells.append(
|
|
183
|
+
(
|
|
184
|
+
name,
|
|
185
|
+
shell_id,
|
|
186
|
+
"cmd.exe",
|
|
187
|
+
["/k", str(vsdevcmd)],
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Developer PowerShell
|
|
192
|
+
launch_ps = vs_path / "Common7" / "Tools" / "Launch-VsDevShell.ps1"
|
|
193
|
+
if launch_ps.exists():
|
|
194
|
+
name = f"Dev PS {year}"
|
|
195
|
+
shell_id = f"devps-{year}-{edition.lower()}"
|
|
196
|
+
# powershell.exe -NoExit -Command "& 'path\to\Launch-VsDevShell.ps1'"
|
|
197
|
+
shells.append(
|
|
198
|
+
(
|
|
199
|
+
name,
|
|
200
|
+
shell_id,
|
|
201
|
+
"powershell.exe",
|
|
202
|
+
["-NoExit", "-Command", f"& '{launch_ps}'"],
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return shells
|
|
207
|
+
|
|
208
|
+
def _strip_json_comments(self, content: str) -> str:
|
|
209
|
+
"""Strip comments from JSON content while preserving strings.
|
|
210
|
+
|
|
211
|
+
Handles:
|
|
212
|
+
- Single-line comments: // comment
|
|
213
|
+
- Multi-line comments: /* comment */
|
|
214
|
+
- Preserves // inside quoted strings (e.g., URLs)
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
content: JSON content with possible comments
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
JSON content without comments
|
|
221
|
+
"""
|
|
222
|
+
result = []
|
|
223
|
+
i = 0
|
|
224
|
+
in_string = False
|
|
225
|
+
escape_next = False
|
|
226
|
+
|
|
227
|
+
while i < len(content):
|
|
228
|
+
char = content[i]
|
|
229
|
+
|
|
230
|
+
if escape_next:
|
|
231
|
+
result.append(char)
|
|
232
|
+
escape_next = False
|
|
233
|
+
i += 1
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
if char == "\\" and in_string:
|
|
237
|
+
result.append(char)
|
|
238
|
+
escape_next = True
|
|
239
|
+
i += 1
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
if char == '"' and not escape_next:
|
|
243
|
+
in_string = not in_string
|
|
244
|
+
result.append(char)
|
|
245
|
+
i += 1
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
if not in_string:
|
|
249
|
+
# Check for single-line comment
|
|
250
|
+
if content[i : i + 2] == "//":
|
|
251
|
+
# Skip to end of line
|
|
252
|
+
while i < len(content) and content[i] != "\n":
|
|
253
|
+
i += 1
|
|
254
|
+
continue
|
|
255
|
+
# Check for multi-line comment
|
|
256
|
+
if content[i : i + 2] == "/*":
|
|
257
|
+
i += 2
|
|
258
|
+
while i < len(content) - 1 and content[i : i + 2] != "*/":
|
|
259
|
+
i += 1
|
|
260
|
+
i += 2 # Skip */
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
result.append(char)
|
|
264
|
+
i += 1
|
|
265
|
+
|
|
266
|
+
return "".join(result)
|
|
267
|
+
|
|
268
|
+
def _parse_commandline(self, commandline: str) -> tuple[str, list[str]]:
|
|
269
|
+
"""Parse a commandline string into command and args.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
commandline: The command line string (e.g., 'cmd.exe /k "vcvars64.bat"')
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Tuple of (command, args_list)
|
|
276
|
+
"""
|
|
277
|
+
try:
|
|
278
|
+
# Use shlex to properly handle quoted arguments
|
|
279
|
+
parts = shlex.split(commandline, posix=False)
|
|
280
|
+
if not parts:
|
|
281
|
+
return "", []
|
|
282
|
+
return parts[0], parts[1:]
|
|
283
|
+
except ValueError:
|
|
284
|
+
# Fallback: simple split on first space
|
|
285
|
+
parts = commandline.split(None, 1)
|
|
286
|
+
if not parts:
|
|
287
|
+
return "", []
|
|
288
|
+
return parts[0], parts[1:] if len(parts) > 1 else []
|
|
289
|
+
|
|
290
|
+
def _abbreviate_name(self, name: str) -> str:
|
|
291
|
+
"""Abbreviate a shell name for display.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
name: Full shell name (e.g., "Windows PowerShell")
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Abbreviated name (e.g., "WinPS")
|
|
298
|
+
"""
|
|
299
|
+
# Common abbreviations
|
|
300
|
+
abbrevs = {
|
|
301
|
+
"Windows PowerShell": "WinPS",
|
|
302
|
+
"Command Prompt": "CMD",
|
|
303
|
+
"PowerShell": "PS",
|
|
304
|
+
"Developer Command Prompt": "DevCMD",
|
|
305
|
+
"Developer PowerShell": "DevPS",
|
|
306
|
+
"Azure Cloud Shell": "Azure",
|
|
307
|
+
"Git Bash": "GitBash",
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# Check for exact or prefix match
|
|
311
|
+
for full, short in abbrevs.items():
|
|
312
|
+
if name == full or name.startswith(full):
|
|
313
|
+
# Append suffix if there's more (e.g., "for VS 2022")
|
|
314
|
+
suffix = name[len(full) :].strip()
|
|
315
|
+
if suffix:
|
|
316
|
+
# Extract version/year if present
|
|
317
|
+
parts = suffix.split()
|
|
318
|
+
for part in parts:
|
|
319
|
+
if part.isdigit() and len(part) == 4: # Year like 2022
|
|
320
|
+
return f"{short} {part}"
|
|
321
|
+
return short
|
|
322
|
+
|
|
323
|
+
# Fallback: first 8 chars if name is long
|
|
324
|
+
if len(name) > 10:
|
|
325
|
+
return name[:8].strip()
|
|
326
|
+
return name
|
|
327
|
+
|
|
328
|
+
def _slugify(self, name: str) -> str:
|
|
329
|
+
"""Convert a profile name to a slug ID.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
name: Profile name (e.g., "Developer PowerShell for VS 2022")
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Slug ID (e.g., "developer-powershell-for-vs-2022")
|
|
336
|
+
"""
|
|
337
|
+
# Lowercase, replace non-alphanumeric with hyphens, collapse multiple hyphens
|
|
338
|
+
slug = re.sub(r"[^a-z0-9]+", "-", name.lower())
|
|
339
|
+
return slug.strip("-")
|
|
340
|
+
|
|
341
|
+
def _merge_candidates(
|
|
342
|
+
self,
|
|
343
|
+
primary: list[tuple[str, str, str, list[str]]],
|
|
344
|
+
secondary: list[tuple[str, str, str, list[str]]],
|
|
345
|
+
) -> list[tuple[str, str, str, list[str]]]:
|
|
346
|
+
"""Merge two candidate lists, deduplicating by command executable.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
primary: First list (takes priority)
|
|
350
|
+
secondary: Second list (skipped if command already in primary)
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Merged and deduplicated list
|
|
354
|
+
"""
|
|
355
|
+
result = list(primary)
|
|
356
|
+
seen_commands = set()
|
|
357
|
+
|
|
358
|
+
# Track commands from primary (normalize to lowercase basename)
|
|
359
|
+
for _, _, command, _ in primary:
|
|
360
|
+
cmd_name = Path(command).name.lower()
|
|
361
|
+
seen_commands.add(cmd_name)
|
|
362
|
+
|
|
363
|
+
# Add secondary items if command not already seen
|
|
364
|
+
for item in secondary:
|
|
365
|
+
cmd_name = Path(item[2]).name.lower()
|
|
366
|
+
if cmd_name not in seen_commands:
|
|
367
|
+
result.append(item)
|
|
368
|
+
seen_commands.add(cmd_name)
|
|
369
|
+
|
|
370
|
+
return result
|
|
371
|
+
|
|
65
372
|
def _get_windows_default(self) -> str:
|
|
66
373
|
"""Get default shell ID for Windows."""
|
|
67
374
|
if shutil.which("pwsh.exe"):
|
|
@@ -72,14 +379,47 @@ class ShellDetector:
|
|
|
72
379
|
|
|
73
380
|
def _get_macos_default(self) -> str:
|
|
74
381
|
"""Get default shell ID for macOS."""
|
|
382
|
+
# Check user's configured shell from $SHELL
|
|
383
|
+
user_shell = self._get_user_shell_id()
|
|
384
|
+
if user_shell:
|
|
385
|
+
return user_shell
|
|
386
|
+
# Fallback to zsh (macOS default since Catalina)
|
|
75
387
|
if shutil.which("zsh"):
|
|
76
388
|
return "zsh"
|
|
77
389
|
return "bash"
|
|
78
390
|
|
|
79
391
|
def _get_linux_default(self) -> str:
|
|
80
392
|
"""Get default shell ID for Linux."""
|
|
393
|
+
# Check user's configured shell from $SHELL
|
|
394
|
+
user_shell = self._get_user_shell_id()
|
|
395
|
+
if user_shell:
|
|
396
|
+
return user_shell
|
|
397
|
+
# Fallback
|
|
81
398
|
if shutil.which("bash"):
|
|
82
399
|
return "bash"
|
|
83
400
|
if shutil.which("zsh"):
|
|
84
401
|
return "zsh"
|
|
85
402
|
return "sh"
|
|
403
|
+
|
|
404
|
+
def _get_user_shell_id(self) -> str | None:
|
|
405
|
+
"""Get shell ID from user's $SHELL environment variable.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Shell ID if $SHELL is set and matches a known shell, None otherwise.
|
|
409
|
+
"""
|
|
410
|
+
shell_path = os.environ.get("SHELL", "")
|
|
411
|
+
if not shell_path:
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
# Extract shell name from path (e.g., /usr/bin/fish -> fish)
|
|
415
|
+
shell_name = Path(shell_path).name.lower()
|
|
416
|
+
|
|
417
|
+
# Map common shell names to IDs
|
|
418
|
+
shell_map = {
|
|
419
|
+
"bash": "bash",
|
|
420
|
+
"zsh": "zsh",
|
|
421
|
+
"fish": "fish",
|
|
422
|
+
"sh": "sh",
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return shell_map.get(shell_name)
|
|
@@ -76,15 +76,22 @@ def start_server(host: str, port: int, *, verbose: bool = False) -> subprocess.P
|
|
|
76
76
|
"--no-access-log", # Disable access logging
|
|
77
77
|
]
|
|
78
78
|
|
|
79
|
+
# On Windows, use CREATE_NEW_PROCESS_GROUP to prevent Ctrl+C from propagating
|
|
80
|
+
# to the child process - we handle cleanup ourselves
|
|
81
|
+
kwargs = {}
|
|
82
|
+
if sys.platform == "win32":
|
|
83
|
+
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
84
|
+
|
|
79
85
|
if verbose:
|
|
80
86
|
# Let output go directly to console
|
|
81
|
-
process = subprocess.Popen(cmd)
|
|
87
|
+
process = subprocess.Popen(cmd, **kwargs)
|
|
82
88
|
else:
|
|
83
89
|
process = subprocess.Popen(
|
|
84
90
|
cmd,
|
|
85
91
|
stdout=subprocess.PIPE,
|
|
86
92
|
stderr=subprocess.STDOUT,
|
|
87
93
|
text=True,
|
|
94
|
+
**kwargs,
|
|
88
95
|
)
|
|
89
96
|
|
|
90
97
|
return process
|
|
@@ -120,6 +127,11 @@ def start_cloudflared(port: int) -> tuple[subprocess.Popen, str | None]:
|
|
|
120
127
|
# Point to a non-existent config to force quick tunnel mode
|
|
121
128
|
env["TUNNEL_CONFIG"] = os.devnull
|
|
122
129
|
|
|
130
|
+
# On Windows, use CREATE_NEW_PROCESS_GROUP to prevent Ctrl+C from propagating
|
|
131
|
+
kwargs = {}
|
|
132
|
+
if sys.platform == "win32":
|
|
133
|
+
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
134
|
+
|
|
123
135
|
process = subprocess.Popen(
|
|
124
136
|
cmd,
|
|
125
137
|
stdout=subprocess.PIPE,
|
|
@@ -127,6 +139,7 @@ def start_cloudflared(port: int) -> tuple[subprocess.Popen, str | None]:
|
|
|
127
139
|
text=True,
|
|
128
140
|
bufsize=1,
|
|
129
141
|
env=env,
|
|
142
|
+
**kwargs,
|
|
130
143
|
)
|
|
131
144
|
|
|
132
145
|
# Parse URL from cloudflared output (flexible pattern for different Cloudflare domains)
|
|
@@ -151,7 +164,7 @@ def start_cloudflared(port: int) -> tuple[subprocess.Popen, str | None]:
|
|
|
151
164
|
|
|
152
165
|
|
|
153
166
|
def drain_process_output(process: subprocess.Popen) -> None:
|
|
154
|
-
"""Drain process output silently (only print errors).
|
|
167
|
+
"""Drain process output silently (only print errors and security warnings).
|
|
155
168
|
|
|
156
169
|
Args:
|
|
157
170
|
process: Subprocess to drain output from.
|
|
@@ -161,8 +174,20 @@ def drain_process_output(process: subprocess.Popen) -> None:
|
|
|
161
174
|
if not line:
|
|
162
175
|
break
|
|
163
176
|
line = line.strip()
|
|
177
|
+
if not line:
|
|
178
|
+
continue
|
|
179
|
+
# Always print security warnings and related messages
|
|
180
|
+
if any(
|
|
181
|
+
kw in line.lower()
|
|
182
|
+
for kw in (
|
|
183
|
+
"security warning",
|
|
184
|
+
"authentication attempts",
|
|
185
|
+
"url may have been leaked",
|
|
186
|
+
)
|
|
187
|
+
):
|
|
188
|
+
console.print(f"[bold red]{line}[/bold red]")
|
|
164
189
|
# Print errors, but ignore harmless ICMP/ping warnings
|
|
165
|
-
|
|
190
|
+
elif "error" in line.lower() and not _is_icmp_warning(line):
|
|
166
191
|
console.print(f"[red]{line}[/red]")
|
|
167
192
|
except (OSError, ValueError):
|
|
168
193
|
pass
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
3
|
+
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
4
|
+
* https://github.com/chjj/term.js
|
|
5
|
+
* @license MIT
|
|
6
|
+
*
|
|
7
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
* in the Software without restriction, including without limitation the rights
|
|
10
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
* furnished to do so, subject to the following conditions:
|
|
13
|
+
*
|
|
14
|
+
* The above copyright notice and this permission notice shall be included in
|
|
15
|
+
* all copies or substantial portions of the Software.
|
|
16
|
+
*
|
|
17
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
* THE SOFTWARE.
|
|
24
|
+
*
|
|
25
|
+
* Originally forked from (with the author's permission):
|
|
26
|
+
* Fabrice Bellard's javascript vt100 for jslinux:
|
|
27
|
+
* http://bellard.org/jslinux/
|
|
28
|
+
* Copyright (c) 2011 Fabrice Bellard
|
|
29
|
+
* The original design remains. The terminal itself
|
|
30
|
+
* has been extended to include xterm CSI codes, among
|
|
31
|
+
* other features.
|
|
32
|
+
*/.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}:root{--row-height: 30px;--content-padding: 12px;--scrollbar-width: 8px;--transition-fast: .15s ease;--radius-sm: 4px;--radius-md: 6px;--radius-lg: 8px;--radius-xl: 12px;--bg-primary: #1e1e1e;--bg-elevated: #252525;--bg-surface: #2d2d2d;--bg-hover: #3a3a3a;--bg-gradient-top: #232323;--bg-gradient-bottom: #1a1a1a;--text-primary: #cccccc;--text-high: rgba(255, 255, 255, .9);--text-white: #fff;--text-secondary: rgba(255, 255, 255, .7);--text-muted: rgba(255, 255, 255, .5);--text-disabled: rgba(255, 255, 255, .4);--border-subtle: rgba(255, 255, 255, .05);--border-light: rgba(255, 255, 255, .1);--border-medium: rgba(255, 255, 255, .15);--border-active: rgba(255, 255, 255, .2);--color-success: rgba(100, 220, 100, .8);--color-success-text: rgba(130, 230, 150, 1);--color-success-bg: rgba(100, 200, 120, .15);--color-success-border: rgba(100, 200, 120, .3);--bg-success-gradient-top: #2a3a2a;--bg-success-gradient-bottom: #1f2f1f;--color-danger: rgba(255, 100, 100, .8);--color-danger-muted: rgba(255, 120, 120, .7);--color-danger-text: rgba(255, 150, 150, 1);--color-danger-bg: rgba(255, 80, 80, .15);--color-danger-border: rgba(255, 120, 120, .3);--bg-danger-gradient-top: #3a2a2a;--bg-danger-gradient-bottom: #2f1f1f;--color-accent: rgba(80, 160, 255, .8);--color-accent-text: rgba(150, 200, 255, 1);--color-accent-bg: rgba(80, 160, 255, .3);--color-accent-strong: rgba(80, 160, 255, .5);--color-locked: rgba(255, 160, 60, .8);--color-locked-bg: rgba(255, 160, 60, .5);--glow-locked: 0 0 8px rgba(255, 160, 60, .5);--color-tmux: rgba(0, 180, 0, .8);--color-tmux-text: rgba(100, 220, 100, 1);--color-tmux-bg: rgba(0, 180, 0, .15);--hover-overlay: rgba(255, 255, 255, .05);--active-overlay: rgba(255, 255, 255, .08);--scrollbar-thumb: rgba(255, 255, 255, .15);--scrollbar-hover: rgba(255, 255, 255, .25);--cursor-color: #aeafad;--selection-bg: rgba(38, 79, 120, .5);--shadow-elevated: 0 4px 16px rgba(0, 0, 0, .5);--glow-success: 0 0 6px rgba(100, 220, 100, .4);--glow-danger: 0 0 6px rgba(255, 100, 100, .4);--glow-accent: 0 0 8px rgba(80, 160, 255, .4);--overlay-bg: rgba(0, 0, 0, .9)}.tool-btn,.tab-btn{-webkit-touch-callout:none}html,body{margin:0;padding:0;height:100%;overflow:hidden;background:var(--bg-primary);color:var(--text-primary);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:13px;position:fixed;width:100%;touch-action:none;overscroll-behavior:none}#app{display:flex;flex-direction:column;height:100%;height:100dvh;height:100svh;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px);overflow:hidden;overscroll-behavior:none}#tab-bar{display:flex;align-items:center;height:var(--row-height);min-height:var(--row-height);background:linear-gradient(to bottom,var(--bg-gradient-top),var(--bg-gradient-bottom));border-bottom:1px solid var(--border-subtle);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}#tab-bar::-webkit-scrollbar{display:none}.tab-btn{display:flex;align-items:center;gap:2px;height:var(--row-height);padding:0 8px;background:transparent;border:none;border-right:1px solid var(--border-subtle);color:var(--text-muted);font-size:12px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:all var(--transition-fast)}.tab-btn:active{background:var(--hover-overlay)}.tab-btn.active{background:var(--active-overlay);color:var(--text-high)}.tab-btn.tab-add{color:var(--text-disabled);font-size:18px;padding:0 10px;border-right:none}.tab-btn.tab-add:active{color:var(--text-high)}.tab-label{max-width:100px;overflow:hidden;text-overflow:ellipsis}.tab-close{display:flex;align-items:center;justify-content:center;width:16px;height:16px;margin-left:2px;border-radius:var(--radius-sm);font-size:14px;color:var(--text-disabled);transition:all var(--transition-fast);position:relative;overflow:hidden}.tab-close:before{content:"";position:absolute;top:0;left:0;width:100%;height:100%;background:var(--color-danger-bg);transform:scaleX(0);transform-origin:left;transition:transform .4s ease-out;z-index:-1}.tab-close.holding:before{transform:scaleX(1)}.tab-close.holding{color:var(--color-danger-text)}.tab-close.ready{background:var(--color-danger-bg);color:var(--color-danger-text);animation:pulseReady .3s ease}@keyframes pulseReady{0%,to{transform:scale(1)}50%{transform:scale(1.1)}}#shell-selector{margin-left:auto;display:flex;align-items:center;padding:0 var(--content-padding);height:var(--row-height);border-left:1px solid var(--border-subtle)}#shell-select{background:transparent;border:none;color:var(--text-secondary);font-size:11px;padding:4px 8px;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none}#connection-dot{width:6px;height:6px;border-radius:50%;background:var(--color-danger);margin-left:var(--content-padding);margin-right:8px;box-shadow:var(--glow-danger);flex-shrink:0}#connection-dot.connected{background:var(--color-success);box-shadow:var(--glow-success)}#btn-info,#btn-textview,#btn-shutdown{background:transparent;border:none;color:var(--text-disabled);font-size:14px;padding:4px;margin-left:4px;cursor:pointer;transition:color var(--transition-fast)}#btn-textview{padding:4px 0;margin-left:2px;margin-right:4px;font-size:11px;font-weight:500}#btn-info:active,#btn-textview:active{color:var(--color-accent)}#btn-shutdown{color:var(--color-danger-muted)}#btn-shutdown:active{color:var(--color-danger-text)}#terminal-container{flex:1;overflow:hidden;background:var(--bg-primary);padding:0 0 0 var(--content-padding);-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;touch-action:none;overscroll-behavior:contain;contain:strict;isolation:isolate}#terminal{height:100%!important;contain:layout paint}.terminal-instance{height:100%;width:100%;contain:layout paint}.xterm-screen canvas,.xterm canvas{transform:translateZ(0);backface-visibility:hidden}#terminal .xterm-viewport{background:var(--bg-primary)!important;overflow-y:overlay!important;touch-action:none;overscroll-behavior:contain;contain:paint}#terminal .xterm-viewport::-webkit-scrollbar{width:var(--scrollbar-width)}#terminal .xterm-viewport::-webkit-scrollbar-track{background:transparent}#terminal .xterm-viewport::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}#terminal .xterm-viewport::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-hover)}#terminal .xterm-screen{-webkit-user-select:none;user-select:none;-webkit-touch-callout:none}#terminal .xterm-helper-textarea,.xterm-helper-textarea{-webkit-text-security:none!important;font-size:16px!important;-webkit-user-select:text;user-select:text}.xterm-helper-textarea::-webkit-contacts-auto-fill-button,.xterm-helper-textarea::-webkit-credentials-auto-fill-button{visibility:hidden;display:none!important;pointer-events:none;position:absolute;right:0;width:0;height:0}#toolbar{display:flex;flex-direction:column;background:linear-gradient(to bottom,var(--bg-gradient-top),var(--bg-gradient-bottom));border-top:1px solid var(--border-subtle);padding-bottom:env(safe-area-inset-bottom,0px)}.toolbar-row{display:flex;align-items:center;justify-content:center;height:var(--row-height);min-height:var(--row-height);gap:0;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.toolbar-row:first-child{border-bottom:1px solid var(--border-subtle)}.toolbar-row::-webkit-scrollbar{display:none}.tool-btn{display:flex;align-items:center;justify-content:center;height:var(--row-height);padding:0 10px;background:transparent;border:none;border-right:1px solid var(--border-subtle);border-radius:0;color:var(--text-muted);font-size:12px;cursor:pointer;transition:all var(--transition-fast);-webkit-touch-callout:none;flex-shrink:0}.tool-btn:active{background:var(--hover-overlay);color:var(--text-high)}.tool-btn.arrow{padding:0 10px}.tool-btn.danger{color:var(--color-danger-muted)}.tool-btn.danger:active{background:var(--color-danger-bg);color:var(--color-danger-text)}.tool-btn.tmux{color:var(--color-tmux)}.tool-btn.tmux:active{background:var(--color-tmux-bg);color:var(--color-tmux-text)}.tool-btn.modifier{padding:0 10px}.tool-btn.icon{font-size:18px}.tool-btn.enter{color:var(--color-success)}.tool-btn.enter:active{background:var(--color-success-bg);color:var(--color-success-text)}.tool-btn.sticky{background:var(--color-accent-bg);color:var(--color-accent-text)}.tool-btn.locked{background:var(--color-locked-bg);color:var(--text-white);box-shadow:var(--glow-locked)}#disconnect-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:var(--overlay-bg);z-index:1000;display:flex;align-items:center;justify-content:center}#disconnect-content{text-align:center}#disconnect-icon{font-size:48px;color:var(--color-danger);margin-bottom:16px}#disconnect-text{color:var(--text-secondary);font-size:18px;margin-bottom:24px}#disconnect-retry{padding:var(--content-padding) 32px;background:var(--border-light);border:1px solid var(--border-medium);border-radius:var(--radius-lg);color:var(--text-high);font-size:14px;cursor:pointer;transition:all var(--transition-fast)}#disconnect-retry:active{background:var(--border-active);color:var(--text-white)}#auth-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:var(--overlay-bg);z-index:1001;display:flex;align-items:center;justify-content:center;padding:20px}#auth-content{background:var(--bg-elevated);border:1px solid var(--border-light);border-radius:var(--radius-xl);max-width:320px;width:100%;box-shadow:var(--shadow-elevated);animation:helpAppear var(--transition-fast)}#auth-header{display:flex;align-items:center;justify-content:center;padding:var(--content-padding) 16px;border-bottom:1px solid var(--border-subtle);color:var(--text-high);font-size:14px;font-weight:500}#auth-body{padding:16px;display:flex;flex-direction:column;gap:12px}#auth-error{padding:8px 12px;background:var(--color-danger-bg);border:1px solid var(--color-danger-border);border-radius:var(--radius-md);color:var(--color-danger-text);font-size:12px;text-align:center}#auth-password{padding:10px 12px;background:var(--bg-surface);border:1px solid var(--border-medium);border-radius:var(--radius-md);color:var(--text-high);font-size:14px;outline:none;transition:border-color var(--transition-fast)}#auth-password:focus{border-color:var(--color-accent)}#auth-password::placeholder{color:var(--text-disabled)}#auth-submit{padding:var(--content-padding) 16px;background:var(--border-light);border:1px solid var(--border-medium);border-radius:var(--radius-lg);color:var(--text-high);font-size:14px;cursor:pointer;transition:all var(--transition-fast)}#auth-submit:active{background:var(--border-active);color:var(--text-white)}#auth-submit:disabled{opacity:.5;cursor:not-allowed}#copy-button{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:8px 20px;background:linear-gradient(to bottom,var(--bg-surface),var(--bg-elevated));border:1px solid var(--border-light);border-radius:var(--radius-md);color:var(--text-high);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:13px;font-weight:500;cursor:pointer;z-index:10000;box-shadow:var(--shadow-elevated);display:none;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;touch-action:manipulation;transition:all var(--transition-fast)}#copy-button:active{background:linear-gradient(to bottom,var(--bg-hover),var(--bg-surface));transform:translate(-50%,-50%) scale(.97)}#copy-button.visible{display:block;animation:copyButtonAppear .1s ease-out}#copy-button.success{background:linear-gradient(to bottom,var(--bg-success-gradient-top),var(--bg-success-gradient-bottom));border-color:var(--color-success-border);color:var(--color-success-text)}#copy-button.error{background:linear-gradient(to bottom,var(--bg-danger-gradient-top),var(--bg-danger-gradient-bottom));border-color:var(--color-danger-border);color:var(--color-danger-text)}@keyframes copyButtonAppear{0%{opacity:0;transform:translate(-50%,-50%) scale(.9)}to{opacity:1;transform:translate(-50%,-50%) scale(1)}}#help-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:var(--overlay-bg);z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px}#help-content{background:var(--bg-elevated);border:1px solid var(--border-light);border-radius:var(--radius-xl);max-width:320px;width:100%;box-shadow:var(--shadow-elevated);animation:helpAppear var(--transition-fast)}@keyframes helpAppear{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}#help-header{display:flex;align-items:center;justify-content:space-between;padding:var(--content-padding) 16px;border-bottom:1px solid var(--border-subtle);color:var(--text-high);font-size:14px;font-weight:500}#help-close{background:transparent;border:none;color:var(--text-muted);font-size:20px;cursor:pointer;padding:4px 8px;line-height:1}#help-close:active{color:var(--text-high)}#help-body{padding:var(--content-padding) 16px}.help-section{margin-bottom:var(--content-padding)}.help-section:last-child{margin-bottom:0}.help-title{color:var(--text-muted);font-size:10px;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}.help-item{display:flex;align-items:center;gap:10px;padding:4px 0;color:var(--text-secondary);font-size:12px}.help-key{display:inline-block;min-width:80px;padding:2px 6px;background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);color:var(--text-high);font-size:11px;text-align:center}#textview-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:var(--bg-primary);z-index:1000;display:flex;flex-direction:column;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)}#textview-content{display:flex;flex-direction:column;width:100%;height:100%;background:var(--bg-primary)}#textview-header{display:flex;align-items:center;height:var(--row-height);min-height:var(--row-height);background:linear-gradient(to bottom,var(--bg-gradient-top),var(--bg-gradient-bottom));border-bottom:1px solid var(--border-subtle);flex-shrink:0}#textview-title{padding:0 var(--content-padding);color:var(--text-muted);font-size:12px;border-right:1px solid var(--border-subtle)}.textview-zoom-btn{display:flex;align-items:center;justify-content:center;height:var(--row-height);padding:0 var(--content-padding);background:transparent;border:none;border-right:1px solid var(--border-subtle);color:var(--text-muted);font-size:14px;cursor:pointer;transition:all var(--transition-fast)}.textview-zoom-btn:active{background:var(--hover-overlay);color:var(--text-high)}#textview-close{margin-left:auto;display:flex;align-items:center;justify-content:center;height:var(--row-height);padding:0 var(--content-padding);background:transparent;border:none;border-left:1px solid var(--border-subtle);color:var(--text-muted);font-size:16px;cursor:pointer;transition:all var(--transition-fast)}#textview-close:active{background:var(--hover-overlay);color:var(--text-high)}#textview-body{flex:1;min-height:0;margin:0;padding:8px var(--content-padding);background:var(--bg-primary);border:none;color:var(--text-primary);font-family:Menlo,Monaco,Consolas,monospace;font-size:9px;line-height:1.3;overflow-y:auto;white-space:pre-wrap;word-wrap:break-word;user-select:text;-webkit-user-select:text}.hidden{display:none!important}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;transition-duration:.01ms!important}}
|