ptn 0.4.2__py3-none-any.whl → 0.4.6__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 +13 -5
- porterminal/_version.py +2 -2
- porterminal/cli/args.py +46 -5
- porterminal/cli/script_discovery.py +175 -21
- porterminal/domain/values/rate_limit_config.py +3 -3
- porterminal/infrastructure/config/shell_detector.py +27 -4
- {ptn-0.4.2.dist-info → ptn-0.4.6.dist-info}/METADATA +1 -1
- {ptn-0.4.2.dist-info → ptn-0.4.6.dist-info}/RECORD +11 -11
- {ptn-0.4.2.dist-info → ptn-0.4.6.dist-info}/WHEEL +0 -0
- {ptn-0.4.2.dist-info → ptn-0.4.6.dist-info}/entry_points.txt +0 -0
- {ptn-0.4.2.dist-info → ptn-0.4.6.dist-info}/licenses/LICENSE +0 -0
porterminal/__init__.py
CHANGED
|
@@ -19,7 +19,7 @@ import subprocess
|
|
|
19
19
|
import sys
|
|
20
20
|
import time
|
|
21
21
|
from pathlib import Path
|
|
22
|
-
from threading import Thread
|
|
22
|
+
from threading import Event, Thread
|
|
23
23
|
|
|
24
24
|
from rich.console import Console
|
|
25
25
|
|
|
@@ -287,9 +287,15 @@ def main() -> int:
|
|
|
287
287
|
if tunnel_process is not None:
|
|
288
288
|
Thread(target=drain_process_output, args=(tunnel_process,), daemon=True).start()
|
|
289
289
|
|
|
290
|
-
#
|
|
290
|
+
# Use an event for responsive Ctrl+C handling on Windows
|
|
291
|
+
shutdown_event = Event()
|
|
292
|
+
|
|
293
|
+
def signal_handler(signum: int, frame: object) -> None:
|
|
294
|
+
shutdown_event.set()
|
|
295
|
+
|
|
296
|
+
old_handler = signal.signal(signal.SIGINT, signal_handler)
|
|
291
297
|
try:
|
|
292
|
-
while
|
|
298
|
+
while not shutdown_event.is_set():
|
|
293
299
|
if server_process is not None and server_process.poll() is not None:
|
|
294
300
|
code = server_process.returncode
|
|
295
301
|
if code == 0 or code < 0:
|
|
@@ -304,9 +310,11 @@ def main() -> int:
|
|
|
304
310
|
else:
|
|
305
311
|
console.print(f"\n[yellow]Tunnel stopped (exit code {code})[/yellow]")
|
|
306
312
|
break
|
|
307
|
-
|
|
313
|
+
shutdown_event.wait(0.1)
|
|
314
|
+
finally:
|
|
315
|
+
signal.signal(signal.SIGINT, old_handler)
|
|
308
316
|
|
|
309
|
-
|
|
317
|
+
if shutdown_event.is_set():
|
|
310
318
|
console.print("\n[dim]Shutting down...[/dim]")
|
|
311
319
|
|
|
312
320
|
# Cleanup - terminate gracefully, then kill if needed
|
porterminal/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.4.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 4,
|
|
31
|
+
__version__ = version = '0.4.6'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 4, 6)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
porterminal/cli/args.py
CHANGED
|
@@ -65,8 +65,11 @@ def parse_args() -> argparse.Namespace:
|
|
|
65
65
|
parser.add_argument(
|
|
66
66
|
"-i",
|
|
67
67
|
"--init",
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
nargs="?",
|
|
69
|
+
const=True,
|
|
70
|
+
default=False,
|
|
71
|
+
metavar="URL_OR_PATH",
|
|
72
|
+
help="Create .ptn/ptn.yaml config (optionally from URL or file path)",
|
|
70
73
|
)
|
|
71
74
|
parser.add_argument(
|
|
72
75
|
"-p",
|
|
@@ -108,7 +111,7 @@ def parse_args() -> argparse.Namespace:
|
|
|
108
111
|
sys.exit(0 if success else 1)
|
|
109
112
|
|
|
110
113
|
if args.init:
|
|
111
|
-
_init_config()
|
|
114
|
+
_init_config(args.init if args.init is not True else None)
|
|
112
115
|
# Continue to launch ptn after creating config
|
|
113
116
|
|
|
114
117
|
if args.default_password:
|
|
@@ -118,9 +121,16 @@ def parse_args() -> argparse.Namespace:
|
|
|
118
121
|
return args
|
|
119
122
|
|
|
120
123
|
|
|
121
|
-
def _init_config() -> None:
|
|
122
|
-
"""Create .ptn/ptn.yaml in current directory
|
|
124
|
+
def _init_config(source: str | None = None) -> None:
|
|
125
|
+
"""Create .ptn/ptn.yaml in current directory.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
source: Optional URL or file path to use as config source.
|
|
129
|
+
If None, auto-discovers scripts and creates default config.
|
|
130
|
+
"""
|
|
123
131
|
from pathlib import Path
|
|
132
|
+
from urllib.error import URLError
|
|
133
|
+
from urllib.request import urlopen
|
|
124
134
|
|
|
125
135
|
import yaml
|
|
126
136
|
|
|
@@ -130,6 +140,37 @@ def _init_config() -> None:
|
|
|
130
140
|
config_dir = cwd / ".ptn"
|
|
131
141
|
config_file = config_dir / "ptn.yaml"
|
|
132
142
|
|
|
143
|
+
# If source is provided, fetch/copy it
|
|
144
|
+
if source:
|
|
145
|
+
config_dir.mkdir(exist_ok=True)
|
|
146
|
+
|
|
147
|
+
if source.startswith(("http://", "https://")):
|
|
148
|
+
# Download from URL
|
|
149
|
+
try:
|
|
150
|
+
print(f"Downloading config from {source}...")
|
|
151
|
+
with urlopen(source, timeout=10) as response:
|
|
152
|
+
content = response.read().decode("utf-8")
|
|
153
|
+
config_file.write_text(content)
|
|
154
|
+
print(f"Created: {config_file}")
|
|
155
|
+
except (URLError, OSError, TimeoutError) as e:
|
|
156
|
+
print(f"Error downloading config: {e}")
|
|
157
|
+
return
|
|
158
|
+
else:
|
|
159
|
+
# Copy from local file
|
|
160
|
+
source_path = Path(source).expanduser().resolve()
|
|
161
|
+
if not source_path.exists():
|
|
162
|
+
print(f"Error: File not found: {source_path}")
|
|
163
|
+
return
|
|
164
|
+
try:
|
|
165
|
+
content = source_path.read_text(encoding="utf-8")
|
|
166
|
+
config_file.write_text(content)
|
|
167
|
+
print(f"Created: {config_file} (from {source_path})")
|
|
168
|
+
except OSError as e:
|
|
169
|
+
print(f"Error reading config: {e}")
|
|
170
|
+
return
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# No source - use auto-discovery
|
|
133
174
|
if config_file.exists():
|
|
134
175
|
print(f"Config already exists: {config_file}")
|
|
135
176
|
return
|
|
@@ -5,15 +5,61 @@ import re
|
|
|
5
5
|
import tomllib
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
8
10
|
# Pattern for safe script names (alphanumeric, hyphens, underscores only)
|
|
9
11
|
_SAFE_NAME = re.compile(r"^[a-zA-Z0-9_-]+$")
|
|
10
12
|
|
|
13
|
+
# Maximum buttons to return from each discovery function
|
|
14
|
+
_MAX_BUTTONS = 6
|
|
15
|
+
|
|
11
16
|
|
|
12
17
|
def _is_safe_name(name: str) -> bool:
|
|
13
18
|
"""Check if script name contains only safe characters."""
|
|
14
19
|
return bool(_SAFE_NAME.match(name)) and len(name) <= 50
|
|
15
20
|
|
|
16
21
|
|
|
22
|
+
def _find_file(base: Path, filenames: list[str]) -> Path | None:
|
|
23
|
+
"""Find the first existing file from a list of candidates."""
|
|
24
|
+
for filename in filenames:
|
|
25
|
+
path = base / filename
|
|
26
|
+
if path.exists():
|
|
27
|
+
return path
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_buttons(
|
|
32
|
+
tasks: dict,
|
|
33
|
+
priority: list[str],
|
|
34
|
+
command_prefix: str,
|
|
35
|
+
*,
|
|
36
|
+
priority_only: bool = False,
|
|
37
|
+
) -> list[dict]:
|
|
38
|
+
"""Build button configs from tasks dict with priority ordering.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
tasks: Dict of task names to their definitions
|
|
42
|
+
priority: List of task names to prioritize
|
|
43
|
+
command_prefix: Command prefix (e.g., "deno task", "task", "just")
|
|
44
|
+
priority_only: If True, only include tasks from priority list
|
|
45
|
+
"""
|
|
46
|
+
buttons = []
|
|
47
|
+
priority_set = set(priority)
|
|
48
|
+
|
|
49
|
+
# Add priority tasks first
|
|
50
|
+
for name in priority:
|
|
51
|
+
if name in tasks and _is_safe_name(name):
|
|
52
|
+
buttons.append({"label": name, "send": f"{command_prefix} {name}\r", "row": 2})
|
|
53
|
+
|
|
54
|
+
# Add remaining tasks (unless priority_only is set)
|
|
55
|
+
if not priority_only:
|
|
56
|
+
for name in tasks:
|
|
57
|
+
if name not in priority_set and _is_safe_name(name) and len(buttons) < _MAX_BUTTONS:
|
|
58
|
+
buttons.append({"label": name, "send": f"{command_prefix} {name}\r", "row": 2})
|
|
59
|
+
|
|
60
|
+
return buttons[:_MAX_BUTTONS]
|
|
61
|
+
|
|
62
|
+
|
|
17
63
|
def discover_scripts(cwd: Path | None = None) -> list[dict]:
|
|
18
64
|
"""Discover project scripts in current directory.
|
|
19
65
|
|
|
@@ -24,9 +70,13 @@ def discover_scripts(cwd: Path | None = None) -> list[dict]:
|
|
|
24
70
|
buttons = []
|
|
25
71
|
|
|
26
72
|
# Check each project type (only those with explicit scripts)
|
|
27
|
-
|
|
73
|
+
# Order matters: first match wins for deduplication
|
|
74
|
+
buttons.extend(_discover_npm_scripts(base)) # Also handles Bun
|
|
75
|
+
buttons.extend(_discover_deno_tasks(base))
|
|
28
76
|
buttons.extend(_discover_python_scripts(base))
|
|
29
77
|
buttons.extend(_discover_makefile_targets(base))
|
|
78
|
+
buttons.extend(_discover_just_recipes(base))
|
|
79
|
+
buttons.extend(_discover_taskfile_tasks(base))
|
|
30
80
|
|
|
31
81
|
# Dedupe by label, keep first occurrence
|
|
32
82
|
unique: dict[str, dict] = {}
|
|
@@ -36,7 +86,11 @@ def discover_scripts(cwd: Path | None = None) -> list[dict]:
|
|
|
36
86
|
|
|
37
87
|
|
|
38
88
|
def _discover_npm_scripts(base: Path) -> list[dict]:
|
|
39
|
-
"""Extract scripts from package.json.
|
|
89
|
+
"""Extract scripts from package.json.
|
|
90
|
+
|
|
91
|
+
Uses 'bun run' if bun.lockb exists, otherwise 'npm run'.
|
|
92
|
+
Only includes scripts from the priority list (build, dev, start, etc.).
|
|
93
|
+
"""
|
|
40
94
|
pkg_file = base / "package.json"
|
|
41
95
|
if not pkg_file.exists():
|
|
42
96
|
return []
|
|
@@ -45,21 +99,21 @@ def _discover_npm_scripts(base: Path) -> list[dict]:
|
|
|
45
99
|
data = json.loads(pkg_file.read_text(encoding="utf-8"))
|
|
46
100
|
scripts = data.get("scripts", {})
|
|
47
101
|
|
|
48
|
-
#
|
|
102
|
+
# Detect package manager: bun if bun.lockb exists
|
|
103
|
+
runner = "bun run" if (base / "bun.lockb").exists() else "npm run"
|
|
49
104
|
priority = ["build", "dev", "start", "test", "lint", "format", "watch"]
|
|
50
105
|
|
|
51
|
-
|
|
52
|
-
for name in priority:
|
|
53
|
-
if name in scripts:
|
|
54
|
-
buttons.append({"label": name, "send": f"npm run {name}\r", "row": 2})
|
|
55
|
-
|
|
56
|
-
return buttons[:6] # Limit to 6 buttons
|
|
106
|
+
return _build_buttons(scripts, priority, runner, priority_only=True)
|
|
57
107
|
except Exception:
|
|
58
108
|
return []
|
|
59
109
|
|
|
60
110
|
|
|
61
111
|
def _discover_python_scripts(base: Path) -> list[dict]:
|
|
62
|
-
"""Extract scripts from pyproject.toml.
|
|
112
|
+
"""Extract scripts from pyproject.toml.
|
|
113
|
+
|
|
114
|
+
Checks [project.scripts] (PEP 621) first, then [tool.poetry.scripts].
|
|
115
|
+
Takes up to 4 from each source, deduplicates, and caps at 6 total.
|
|
116
|
+
"""
|
|
63
117
|
toml_file = base / "pyproject.toml"
|
|
64
118
|
if not toml_file.exists():
|
|
65
119
|
return []
|
|
@@ -68,19 +122,20 @@ def _discover_python_scripts(base: Path) -> list[dict]:
|
|
|
68
122
|
data = tomllib.loads(toml_file.read_text(encoding="utf-8"))
|
|
69
123
|
buttons = []
|
|
70
124
|
|
|
71
|
-
# Check [project.scripts] (PEP 621)
|
|
125
|
+
# Check [project.scripts] (PEP 621) - take up to 4
|
|
72
126
|
project_scripts = data.get("project", {}).get("scripts", {})
|
|
73
127
|
for name in list(project_scripts.keys())[:4]:
|
|
74
128
|
if _is_safe_name(name):
|
|
75
129
|
buttons.append({"label": name, "send": f"{name}\r", "row": 2})
|
|
76
130
|
|
|
77
|
-
# Check [tool.poetry.scripts]
|
|
131
|
+
# Check [tool.poetry.scripts] - take up to 4, skip duplicates
|
|
132
|
+
existing_labels = {b["label"] for b in buttons}
|
|
78
133
|
poetry_scripts = data.get("tool", {}).get("poetry", {}).get("scripts", {})
|
|
79
134
|
for name in list(poetry_scripts.keys())[:4]:
|
|
80
|
-
if _is_safe_name(name) and
|
|
135
|
+
if _is_safe_name(name) and name not in existing_labels:
|
|
81
136
|
buttons.append({"label": name, "send": f"{name}\r", "row": 2})
|
|
82
137
|
|
|
83
|
-
return buttons[:
|
|
138
|
+
return buttons[:_MAX_BUTTONS]
|
|
84
139
|
except Exception:
|
|
85
140
|
return []
|
|
86
141
|
|
|
@@ -98,15 +153,114 @@ def _discover_makefile_targets(base: Path) -> list[dict]:
|
|
|
98
153
|
pattern = r"^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:"
|
|
99
154
|
targets = re.findall(pattern, content, re.MULTILINE)
|
|
100
155
|
|
|
101
|
-
#
|
|
156
|
+
# Convert list to dict for _build_buttons compatibility
|
|
157
|
+
targets_dict = {t: True for t in targets}
|
|
102
158
|
priority = ["build", "test", "run", "clean", "install", "dev", "lint", "all"]
|
|
103
|
-
priority_set = set(priority)
|
|
104
|
-
target_set = set(targets)
|
|
105
159
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
160
|
+
return _build_buttons(targets_dict, priority, "make")
|
|
161
|
+
except Exception:
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _discover_deno_tasks(base: Path) -> list[dict]:
|
|
166
|
+
"""Extract tasks from deno.json or deno.jsonc."""
|
|
167
|
+
deno_file = _find_file(base, ["deno.json", "deno.jsonc"])
|
|
168
|
+
if not deno_file:
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
content = deno_file.read_text(encoding="utf-8")
|
|
173
|
+
if deno_file.suffix == ".jsonc":
|
|
174
|
+
content = _strip_json_comments(content)
|
|
175
|
+
|
|
176
|
+
tasks = json.loads(content).get("tasks", {})
|
|
177
|
+
priority = ["build", "dev", "start", "test", "lint", "format", "check"]
|
|
178
|
+
|
|
179
|
+
return _build_buttons(tasks, priority, "deno task")
|
|
180
|
+
except Exception:
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _strip_json_comments(content: str) -> str:
|
|
185
|
+
"""Strip comments from JSON content (for .jsonc files)."""
|
|
186
|
+
result = []
|
|
187
|
+
i = 0
|
|
188
|
+
in_string = False
|
|
189
|
+
escape_next = False
|
|
190
|
+
|
|
191
|
+
while i < len(content):
|
|
192
|
+
char = content[i]
|
|
193
|
+
|
|
194
|
+
if escape_next:
|
|
195
|
+
result.append(char)
|
|
196
|
+
escape_next = False
|
|
197
|
+
i += 1
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
if char == "\\" and in_string:
|
|
201
|
+
result.append(char)
|
|
202
|
+
escape_next = True
|
|
203
|
+
i += 1
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
if char == '"' and not escape_next:
|
|
207
|
+
in_string = not in_string
|
|
208
|
+
result.append(char)
|
|
209
|
+
i += 1
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
if not in_string:
|
|
213
|
+
# Single-line comment
|
|
214
|
+
if content[i : i + 2] == "//":
|
|
215
|
+
while i < len(content) and content[i] != "\n":
|
|
216
|
+
i += 1
|
|
217
|
+
continue
|
|
218
|
+
# Multi-line comment
|
|
219
|
+
if content[i : i + 2] == "/*":
|
|
220
|
+
i += 2
|
|
221
|
+
while i < len(content) - 1 and content[i : i + 2] != "*/":
|
|
222
|
+
i += 1
|
|
223
|
+
i += 2
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
result.append(char)
|
|
227
|
+
i += 1
|
|
228
|
+
|
|
229
|
+
return "".join(result)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _discover_just_recipes(base: Path) -> list[dict]:
|
|
233
|
+
"""Extract recipes from justfile."""
|
|
234
|
+
justfile = _find_file(base, ["justfile", "Justfile", ".justfile"])
|
|
235
|
+
if not justfile:
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
content = justfile.read_text(encoding="utf-8")
|
|
240
|
+
# Match recipe definitions: "recipe:" or "recipe arg:" at start of line
|
|
241
|
+
# Exclude private recipes (starting with _) and recipes with @ prefix
|
|
242
|
+
pattern = r"^([a-zA-Z][a-zA-Z0-9_-]*)\s*(?:[^:]*)?:"
|
|
243
|
+
recipes = re.findall(pattern, content, re.MULTILINE)
|
|
244
|
+
|
|
245
|
+
# Convert list to dict for _build_buttons compatibility
|
|
246
|
+
recipes_dict = {r: True for r in recipes}
|
|
247
|
+
priority = ["build", "test", "run", "dev", "check", "lint", "fmt", "clean"]
|
|
248
|
+
|
|
249
|
+
return _build_buttons(recipes_dict, priority, "just")
|
|
250
|
+
except Exception:
|
|
251
|
+
return []
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _discover_taskfile_tasks(base: Path) -> list[dict]:
|
|
255
|
+
"""Extract tasks from Taskfile.yml."""
|
|
256
|
+
taskfile = _find_file(base, ["Taskfile.yml", "Taskfile.yaml", "taskfile.yml", "taskfile.yaml"])
|
|
257
|
+
if not taskfile:
|
|
258
|
+
return []
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
tasks = yaml.safe_load(taskfile.read_text(encoding="utf-8")).get("tasks", {})
|
|
262
|
+
priority = ["build", "test", "run", "dev", "lint", "fmt", "clean", "default"]
|
|
109
263
|
|
|
110
|
-
return
|
|
264
|
+
return _build_buttons(tasks, priority, "task")
|
|
111
265
|
except Exception:
|
|
112
266
|
return []
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
|
|
5
|
-
#
|
|
6
|
-
DEFAULT_RATE =
|
|
7
|
-
DEFAULT_BURST =
|
|
5
|
+
# Defaults: 1KB/s sustained, 16KB burst (allows reasonable paste operations)
|
|
6
|
+
DEFAULT_RATE = 1000.0
|
|
7
|
+
DEFAULT_BURST = 16384
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@dataclass(frozen=True, slots=True)
|
|
@@ -61,11 +61,19 @@ class ShellDetector:
|
|
|
61
61
|
def _get_platform_candidates(self) -> list[tuple[str, str, str, list[str]]]:
|
|
62
62
|
"""Get shell candidates for current platform.
|
|
63
63
|
|
|
64
|
+
On Windows, discovers shells from:
|
|
65
|
+
- Windows Terminal profiles (includes WSL distros)
|
|
66
|
+
- Hardcoded common shells (PowerShell, CMD, Git Bash)
|
|
67
|
+
- Visual Studio Developer shells
|
|
68
|
+
|
|
69
|
+
Note: WSL distros are detected from Windows Terminal profiles only.
|
|
70
|
+
We don't probe inside WSL for individual shells to avoid crossing
|
|
71
|
+
environment boundaries.
|
|
72
|
+
|
|
64
73
|
Returns:
|
|
65
74
|
List of (name, id, command, args) tuples.
|
|
66
75
|
"""
|
|
67
76
|
if sys.platform == "win32":
|
|
68
|
-
# Get Windows Terminal profiles first, then hardcoded defaults
|
|
69
77
|
wt_profiles = self._get_windows_terminal_profiles()
|
|
70
78
|
hardcoded = [
|
|
71
79
|
("PS 7", "pwsh", "pwsh.exe", ["-NoLogo"]),
|
|
@@ -74,15 +82,18 @@ class ShellDetector:
|
|
|
74
82
|
("WSL", "wsl", "wsl.exe", []),
|
|
75
83
|
("Git Bash", "gitbash", r"C:\Program Files\Git\bin\bash.exe", ["--login"]),
|
|
76
84
|
]
|
|
77
|
-
# Merge WT profiles with hardcoded (dedupe by command)
|
|
78
85
|
merged = self._merge_candidates(wt_profiles, hardcoded)
|
|
79
|
-
# Add VS dev shells (no deduplication - they're unique due to args)
|
|
80
86
|
vs_shells = self._get_visual_studio_shells()
|
|
81
87
|
return merged + vs_shells
|
|
82
88
|
return [
|
|
83
89
|
("Bash", "bash", "bash", ["--login"]),
|
|
84
90
|
("Zsh", "zsh", "zsh", ["--login"]),
|
|
85
91
|
("Fish", "fish", "fish", []),
|
|
92
|
+
("Nu", "nu", "nu", []),
|
|
93
|
+
("Ion", "ion", "ion", []),
|
|
94
|
+
("Dash", "dash", "dash", []),
|
|
95
|
+
("Ksh", "ksh", "ksh", ["--login"]),
|
|
96
|
+
("Tcsh", "tcsh", "tcsh", []),
|
|
86
97
|
("Sh", "sh", "sh", []),
|
|
87
98
|
]
|
|
88
99
|
|
|
@@ -118,8 +129,20 @@ class ShellDetector:
|
|
|
118
129
|
for profile in profile_list:
|
|
119
130
|
name = profile.get("name", "")
|
|
120
131
|
commandline = profile.get("commandline", "")
|
|
132
|
+
source = profile.get("source", "")
|
|
133
|
+
|
|
134
|
+
if not name:
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Handle WSL distro profiles (they use source instead of commandline)
|
|
138
|
+
if source == "Windows.Terminal.Wsl" and not commandline:
|
|
139
|
+
# Use wsl.exe -d <distro> to launch the specific distro
|
|
140
|
+
shell_id = self._slugify(name)
|
|
141
|
+
short_name = self._abbreviate_name(name)
|
|
142
|
+
profiles.append((short_name, shell_id, "wsl.exe", ["-d", name]))
|
|
143
|
+
continue
|
|
121
144
|
|
|
122
|
-
if not
|
|
145
|
+
if not commandline:
|
|
123
146
|
continue
|
|
124
147
|
|
|
125
148
|
# Parse commandline into command and args
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
porterminal/__init__.py,sha256=
|
|
1
|
+
porterminal/__init__.py,sha256=DvabdkAk4SwWv8_n9noLr-ESlhd0M_INWRf_CkijOQQ,12916
|
|
2
2
|
porterminal/__main__.py,sha256=XLo21rqmISrIZFiaHC58Trgq8E0gxH4Wb3driD4JA7c,137
|
|
3
|
-
porterminal/_version.py,sha256=
|
|
3
|
+
porterminal/_version.py,sha256=18TAUheMEe2us-SdlWuaPoE-optSFboNqdVGdL_jZ0I,704
|
|
4
4
|
porterminal/app.py,sha256=Q3w-61i6h2Atz4wirAgrT--xTH_qCp4Ig2zgnAIyJ9U,15220
|
|
5
5
|
porterminal/asgi.py,sha256=P76H7k03T3GYBAmjWqLaCZXV-YIou6NMhfRySO8He1A,1276
|
|
6
6
|
porterminal/composition.py,sha256=H1QJszJqGX4BXZE4cFk3I46yxybZPtNG35srqRi35vo,6799
|
|
@@ -18,9 +18,9 @@ porterminal/application/services/session_service.py,sha256=ng1B4GQOVBCjI6gqkb_cO
|
|
|
18
18
|
porterminal/application/services/tab_service.py,sha256=0_S978dYhBW2-eaZN2wOlcZOY4V4iPt2MMNOyoZQxPs,8307
|
|
19
19
|
porterminal/application/services/terminal_service.py,sha256=0zaQWgZ8Uz8JmDfTqww4p2rJfH6AIyG8dVmSH4JIp2I,22054
|
|
20
20
|
porterminal/cli/__init__.py,sha256=A3y-QgKrT-vdAYV-xsZjeyMkiPymZaZzYUGQ-_3cXmQ,305
|
|
21
|
-
porterminal/cli/args.py,sha256=
|
|
21
|
+
porterminal/cli/args.py,sha256=_QlvJWyV_-wIrLN8vbAJlw9LtGcMditmLrE3YZV9PDM,7543
|
|
22
22
|
porterminal/cli/display.py,sha256=PgLnRj9odMKYERMCLyrEhTrpYeYPNYC-a1nWcz_fFE0,5433
|
|
23
|
-
porterminal/cli/script_discovery.py,sha256=
|
|
23
|
+
porterminal/cli/script_discovery.py,sha256=E70bAWP1gCypkaPtLEPXmzyrTWrLz1_6bTQ51vRwZBM,8777
|
|
24
24
|
porterminal/domain/__init__.py,sha256=16WEj1SAKhIqkrQHqaan66pV3zsUn1aEz7oymN54xFQ,1428
|
|
25
25
|
porterminal/domain/entities/__init__.py,sha256=dIQp6T0M-Bl_DJpSOGQGBD29Rl_FDR0XfVrDL08ck50,463
|
|
26
26
|
porterminal/domain/entities/output_buffer.py,sha256=TqqgGk3NEM_htNbAhZQCJM_dek4ajhwViPaVE3XXCeI,4220
|
|
@@ -37,7 +37,7 @@ porterminal/domain/services/session_limits.py,sha256=EqW_tqr0Br3a2gzgl5Dg80dx1um
|
|
|
37
37
|
porterminal/domain/services/tab_limits.py,sha256=nokX8gP9_41MNIdQ7Fri2JLAs4_Ch-fy3CgpKHTZnT8,1434
|
|
38
38
|
porterminal/domain/values/__init__.py,sha256=SuUiW8EINXgbpaCuqmdTGnVrmP0x2FuoJz3HsUzB7M0,516
|
|
39
39
|
porterminal/domain/values/environment_rules.py,sha256=ii3D3CP4v8V-4H13VadzE76Z8EyZdbx-h7CYnEvXmpw,3946
|
|
40
|
-
porterminal/domain/values/rate_limit_config.py,sha256=
|
|
40
|
+
porterminal/domain/values/rate_limit_config.py,sha256=kngpLF87aFnHmHG0PWeA38C4Vq_QVxh7gjndZPkAyX0,582
|
|
41
41
|
porterminal/domain/values/session_id.py,sha256=dYBSZdG6WWjp06mUbTZwjufb30A6lSuK-NKwqxAAavY,461
|
|
42
42
|
porterminal/domain/values/shell_command.py,sha256=9U723nsdDCsW_M17qBp__cu3r9uuBVuur0kifaoiEF0,1102
|
|
43
43
|
porterminal/domain/values/tab_id.py,sha256=zKZZ1lBQUmXZ3PQlH0jejUQ_AA_SVTW-Z7jCI7Tq37A,557
|
|
@@ -49,7 +49,7 @@ porterminal/infrastructure/cloudflared.py,sha256=E7VtcZv4U5HeO1t4JYAO-97JavlD811
|
|
|
49
49
|
porterminal/infrastructure/network.py,sha256=XnYbEXKQA8BnrLFl9b4OTTixFG1pVFxfeVuhN-XPkfU,1275
|
|
50
50
|
porterminal/infrastructure/server.py,sha256=n6lSZjd6m380QzvdAZAu7CbbRoTvTAL8Dp-mcMaNFmY,5803
|
|
51
51
|
porterminal/infrastructure/config/__init__.py,sha256=kcoM8mQYa83rkxn8TUfh0nETKynrsLmVwD9ySIfvdPQ,139
|
|
52
|
-
porterminal/infrastructure/config/shell_detector.py,sha256=
|
|
52
|
+
porterminal/infrastructure/config/shell_detector.py,sha256=VcFQ7IJdTxvH0P-j3Zw_uXZDGe5DOlUkZ1vKcg1aQw8,17463
|
|
53
53
|
porterminal/infrastructure/registry/__init__.py,sha256=reNbIYRr1alNps7zm_oENuS6QWNKiiO-WG09LT5MLmM,157
|
|
54
54
|
porterminal/infrastructure/registry/user_connection_registry.py,sha256=u9KOSijHiRITnwnOBKwXuYOKbFguh2NbxmNMxIoo8vc,3398
|
|
55
55
|
porterminal/infrastructure/repositories/__init__.py,sha256=UyF9lpobXgKb9Ti6ucC6fDcC9CkMAGDThggEG-Rf-Aw,250
|
|
@@ -67,8 +67,8 @@ porterminal/static/icon.svg,sha256=y7-MIl7F_wVQMLHWmmC7MrZHZK5ikLg1BR_0jbqnTkc,1
|
|
|
67
67
|
porterminal/static/index.html,sha256=xLfeiS_g9ujuJooN0QZWgt38q2ub1UcHs_anNS4ONgU,6401
|
|
68
68
|
porterminal/static/assets/app-DlWNJWFE.js,sha256=HxwLXBSp5CrSOMhaI6O0zHxBY3J8dQlW5IuC_L8LnoQ,486711
|
|
69
69
|
porterminal/static/assets/app-xPAM7YhQ.css,sha256=dYk-DAJSmvxP8fn5ABN5Jy_djhlpREDFjcIGKpb2gjw,18949
|
|
70
|
-
ptn-0.4.
|
|
71
|
-
ptn-0.4.
|
|
72
|
-
ptn-0.4.
|
|
73
|
-
ptn-0.4.
|
|
74
|
-
ptn-0.4.
|
|
70
|
+
ptn-0.4.6.dist-info/METADATA,sha256=A0IQdX6yCq99NpW5z1rmrOFx0ShVqi8VmI_tqzN3Dzg,8124
|
|
71
|
+
ptn-0.4.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
72
|
+
ptn-0.4.6.dist-info/entry_points.txt,sha256=Ftj1zSu_7G0yD5mAtGN3RoewyIuoBOfP_noSISe73tU,41
|
|
73
|
+
ptn-0.4.6.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
|
74
|
+
ptn-0.4.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|