ptn 0.1.4__py3-none-any.whl → 0.2.5__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 +19 -3
- porterminal/_version.py +34 -0
- porterminal/app.py +8 -4
- porterminal/application/services/terminal_service.py +116 -28
- porterminal/asgi.py +8 -3
- porterminal/cli/args.py +50 -0
- porterminal/composition.py +13 -5
- porterminal/config.py +54 -70
- porterminal/container.py +0 -11
- porterminal/domain/__init__.py +0 -2
- porterminal/domain/entities/output_buffer.py +0 -4
- porterminal/domain/ports/__init__.py +1 -2
- porterminal/domain/ports/pty_port.py +0 -29
- porterminal/domain/ports/tab_repository.py +0 -5
- porterminal/infrastructure/config/__init__.py +0 -2
- porterminal/infrastructure/config/shell_detector.py +1 -0
- porterminal/infrastructure/repositories/in_memory_tab.py +0 -4
- porterminal/infrastructure/server.py +10 -3
- porterminal/static/assets/app-By4EXMHC.js +72 -0
- porterminal/static/assets/app-DQePboVd.css +32 -0
- porterminal/static/index.html +16 -25
- porterminal/updater.py +115 -168
- ptn-0.2.5.dist-info/METADATA +148 -0
- {ptn-0.1.4.dist-info → ptn-0.2.5.dist-info}/RECORD +27 -29
- porterminal/infrastructure/config/yaml_loader.py +0 -34
- porterminal/static/assets/app-BQiuUo6Q.css +0 -32
- porterminal/static/assets/app-YNN_jEhv.js +0 -71
- porterminal/static/manifest.json +0 -31
- porterminal/static/sw.js +0 -66
- ptn-0.1.4.dist-info/METADATA +0 -191
- {ptn-0.1.4.dist-info → ptn-0.2.5.dist-info}/WHEEL +0 -0
- {ptn-0.1.4.dist-info → ptn-0.2.5.dist-info}/entry_points.txt +0 -0
- {ptn-0.1.4.dist-info → ptn-0.2.5.dist-info}/licenses/LICENSE +0 -0
porterminal/config.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"""Configuration loading and validation using Pydantic."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import shutil
|
|
4
|
-
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
import yaml
|
|
8
8
|
from pydantic import BaseModel, Field, field_validator
|
|
9
9
|
|
|
10
|
+
from porterminal.infrastructure.config import ShellDetector
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class ServerConfig(BaseModel):
|
|
12
14
|
"""Server configuration."""
|
|
@@ -37,67 +39,6 @@ class ShellConfig(BaseModel):
|
|
|
37
39
|
raise ValueError(f"Shell executable not found: {v}")
|
|
38
40
|
|
|
39
41
|
|
|
40
|
-
def detect_available_shells() -> list[ShellConfig]:
|
|
41
|
-
"""Auto-detect available shells based on the platform."""
|
|
42
|
-
shells = []
|
|
43
|
-
|
|
44
|
-
if sys.platform == "win32":
|
|
45
|
-
# Windows shells
|
|
46
|
-
candidates = [
|
|
47
|
-
("PowerShell", "powershell", "powershell.exe", ["-NoLogo"]),
|
|
48
|
-
("PowerShell 7", "pwsh", "pwsh.exe", ["-NoLogo"]),
|
|
49
|
-
("CMD", "cmd", "cmd.exe", []),
|
|
50
|
-
("WSL", "wsl", "wsl.exe", []),
|
|
51
|
-
("Git Bash", "gitbash", r"C:\Program Files\Git\bin\bash.exe", ["--login"]),
|
|
52
|
-
]
|
|
53
|
-
else:
|
|
54
|
-
# Unix-like shells (Linux, macOS)
|
|
55
|
-
candidates = [
|
|
56
|
-
("Bash", "bash", "bash", ["--login"]),
|
|
57
|
-
("Zsh", "zsh", "zsh", ["--login"]),
|
|
58
|
-
("Fish", "fish", "fish", []),
|
|
59
|
-
("Sh", "sh", "sh", []),
|
|
60
|
-
]
|
|
61
|
-
|
|
62
|
-
for name, shell_id, command, args in candidates:
|
|
63
|
-
# Check if shell exists
|
|
64
|
-
shell_path = shutil.which(command)
|
|
65
|
-
if shell_path or Path(command).exists():
|
|
66
|
-
shells.append(
|
|
67
|
-
ShellConfig(
|
|
68
|
-
name=name,
|
|
69
|
-
id=shell_id,
|
|
70
|
-
command=shell_path or command,
|
|
71
|
-
args=args,
|
|
72
|
-
)
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
return shells
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def get_default_shell_id() -> str:
|
|
79
|
-
"""Get the default shell ID for the current platform."""
|
|
80
|
-
if sys.platform == "win32":
|
|
81
|
-
# Prefer PowerShell 7, then PowerShell, then CMD
|
|
82
|
-
if shutil.which("pwsh"):
|
|
83
|
-
return "pwsh"
|
|
84
|
-
if shutil.which("powershell"):
|
|
85
|
-
return "powershell"
|
|
86
|
-
return "cmd"
|
|
87
|
-
elif sys.platform == "darwin":
|
|
88
|
-
# macOS defaults to zsh
|
|
89
|
-
if shutil.which("zsh"):
|
|
90
|
-
return "zsh"
|
|
91
|
-
return "bash"
|
|
92
|
-
else:
|
|
93
|
-
# Linux - prefer bash
|
|
94
|
-
if shutil.which("bash"):
|
|
95
|
-
return "bash"
|
|
96
|
-
if shutil.which("zsh"):
|
|
97
|
-
return "zsh"
|
|
98
|
-
return "sh"
|
|
99
|
-
|
|
100
|
-
|
|
101
42
|
class TerminalConfig(BaseModel):
|
|
102
43
|
"""Terminal configuration."""
|
|
103
44
|
|
|
@@ -118,7 +59,7 @@ class ButtonConfig(BaseModel):
|
|
|
118
59
|
"""Custom button configuration."""
|
|
119
60
|
|
|
120
61
|
label: str
|
|
121
|
-
send: str
|
|
62
|
+
send: str | list[str | int] = "" # string or list of strings/ints (ints = wait ms)
|
|
122
63
|
|
|
123
64
|
|
|
124
65
|
class CloudflareConfig(BaseModel):
|
|
@@ -128,6 +69,13 @@ class CloudflareConfig(BaseModel):
|
|
|
128
69
|
access_aud: str = ""
|
|
129
70
|
|
|
130
71
|
|
|
72
|
+
class UpdateConfig(BaseModel):
|
|
73
|
+
"""Update checker configuration."""
|
|
74
|
+
|
|
75
|
+
notify_on_startup: bool = True # Show "update available" on startup
|
|
76
|
+
check_interval: int = Field(default=86400, ge=0) # Seconds between checks (0 = always)
|
|
77
|
+
|
|
78
|
+
|
|
131
79
|
class Config(BaseModel):
|
|
132
80
|
"""Application configuration."""
|
|
133
81
|
|
|
@@ -135,13 +83,46 @@ class Config(BaseModel):
|
|
|
135
83
|
terminal: TerminalConfig = Field(default_factory=TerminalConfig)
|
|
136
84
|
buttons: list[ButtonConfig] = Field(default_factory=list)
|
|
137
85
|
cloudflare: CloudflareConfig = Field(default_factory=CloudflareConfig)
|
|
86
|
+
update: UpdateConfig = Field(default_factory=UpdateConfig)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def find_config_file(cwd: Path | None = None) -> Path | None:
|
|
90
|
+
"""Find config file in standard locations.
|
|
138
91
|
|
|
92
|
+
Search order:
|
|
93
|
+
1. PORTERMINAL_CONFIG_PATH env var (if set)
|
|
94
|
+
2. ptn.yaml in cwd
|
|
95
|
+
3. .ptn/ptn.yaml in cwd
|
|
96
|
+
4. ~/.ptn/ptn.yaml (user home directory)
|
|
97
|
+
"""
|
|
98
|
+
# Check env var first
|
|
99
|
+
if env_path := os.environ.get("PORTERMINAL_CONFIG_PATH"):
|
|
100
|
+
return Path(env_path)
|
|
139
101
|
|
|
140
|
-
|
|
102
|
+
base = cwd or Path.cwd()
|
|
103
|
+
|
|
104
|
+
# Search order: cwd first, then home
|
|
105
|
+
candidates = [
|
|
106
|
+
base / "ptn.yaml",
|
|
107
|
+
base / ".ptn" / "ptn.yaml",
|
|
108
|
+
Path.home() / ".ptn" / "ptn.yaml",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
for path in candidates:
|
|
112
|
+
if path.exists():
|
|
113
|
+
return path
|
|
114
|
+
|
|
115
|
+
return None # No config found, use defaults
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def load_config(config_path: Path | str | None = None) -> Config:
|
|
141
119
|
"""Load configuration from YAML file."""
|
|
142
|
-
config_path
|
|
120
|
+
if config_path is None:
|
|
121
|
+
config_path = find_config_file()
|
|
143
122
|
|
|
144
|
-
|
|
123
|
+
detector = ShellDetector()
|
|
124
|
+
|
|
125
|
+
if config_path is None or not Path(config_path).exists():
|
|
145
126
|
data = {}
|
|
146
127
|
else:
|
|
147
128
|
with open(config_path, encoding="utf-8") as f:
|
|
@@ -162,10 +143,13 @@ def load_config(config_path: Path | str = "config.yaml") -> Config:
|
|
|
162
143
|
except Exception:
|
|
163
144
|
pass
|
|
164
145
|
|
|
165
|
-
# If no valid shells from config, auto-detect
|
|
146
|
+
# If no valid shells from config, auto-detect using ShellDetector
|
|
166
147
|
if not valid_shells:
|
|
167
|
-
detected =
|
|
168
|
-
terminal_data["shells"] = [
|
|
148
|
+
detected = detector.detect_shells()
|
|
149
|
+
terminal_data["shells"] = [
|
|
150
|
+
{"id": s.id, "name": s.name, "command": s.command, "args": list(s.args)}
|
|
151
|
+
for s in detected
|
|
152
|
+
]
|
|
169
153
|
else:
|
|
170
154
|
terminal_data["shells"] = valid_shells
|
|
171
155
|
|
|
@@ -173,7 +157,7 @@ def load_config(config_path: Path | str = "config.yaml") -> Config:
|
|
|
173
157
|
default_shell = terminal_data.get("default_shell", "")
|
|
174
158
|
shell_ids = [s.get("id") or s.get("name", "").lower() for s in terminal_data.get("shells", [])]
|
|
175
159
|
if not default_shell or default_shell not in shell_ids:
|
|
176
|
-
terminal_data["default_shell"] = get_default_shell_id()
|
|
160
|
+
terminal_data["default_shell"] = detector.get_default_shell_id()
|
|
177
161
|
# Make sure the default shell is in the list
|
|
178
162
|
if terminal_data["default_shell"] not in shell_ids and terminal_data.get("shells"):
|
|
179
163
|
terminal_data["default_shell"] = terminal_data["shells"][0].get("id", "")
|
porterminal/container.py
CHANGED
|
@@ -52,14 +52,3 @@ class Container:
|
|
|
52
52
|
|
|
53
53
|
# Working directory
|
|
54
54
|
cwd: str | None = None
|
|
55
|
-
|
|
56
|
-
def get_shell(self, shell_id: str | None = None) -> ShellCommand | None:
|
|
57
|
-
"""Get shell by ID or default."""
|
|
58
|
-
target_id = shell_id or self.default_shell_id
|
|
59
|
-
|
|
60
|
-
for shell in self.available_shells:
|
|
61
|
-
if shell.id == target_id:
|
|
62
|
-
return shell
|
|
63
|
-
|
|
64
|
-
# Return first available if target not found
|
|
65
|
-
return self.available_shells[0] if self.available_shells else None
|
porterminal/domain/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Domain ports - interfaces for infrastructure to implement."""
|
|
2
2
|
|
|
3
|
-
from .pty_port import
|
|
3
|
+
from .pty_port import PTYPort
|
|
4
4
|
from .session_repository import SessionRepository
|
|
5
5
|
from .tab_repository import TabRepository
|
|
6
6
|
|
|
@@ -8,5 +8,4 @@ __all__ = [
|
|
|
8
8
|
"SessionRepository",
|
|
9
9
|
"TabRepository",
|
|
10
10
|
"PTYPort",
|
|
11
|
-
"PTYFactory",
|
|
12
11
|
]
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
|
|
5
|
-
from ..values.shell_command import ShellCommand
|
|
6
5
|
from ..values.terminal_dimensions import TerminalDimensions
|
|
7
6
|
|
|
8
7
|
|
|
@@ -50,31 +49,3 @@ class PTYPort(ABC):
|
|
|
50
49
|
def dimensions(self) -> TerminalDimensions:
|
|
51
50
|
"""Get current terminal dimensions."""
|
|
52
51
|
...
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class PTYFactory(ABC):
|
|
56
|
-
"""Abstract interface for PTY creation.
|
|
57
|
-
|
|
58
|
-
Infrastructure provides platform-specific factory.
|
|
59
|
-
"""
|
|
60
|
-
|
|
61
|
-
@abstractmethod
|
|
62
|
-
def create(
|
|
63
|
-
self,
|
|
64
|
-
shell: ShellCommand,
|
|
65
|
-
dimensions: TerminalDimensions,
|
|
66
|
-
environment: dict[str, str],
|
|
67
|
-
working_directory: str | None = None,
|
|
68
|
-
) -> PTYPort:
|
|
69
|
-
"""Create and spawn a new PTY.
|
|
70
|
-
|
|
71
|
-
Args:
|
|
72
|
-
shell: Shell command to run.
|
|
73
|
-
dimensions: Initial terminal dimensions.
|
|
74
|
-
environment: Sanitized environment variables.
|
|
75
|
-
working_directory: Optional working directory.
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
PTY port implementation.
|
|
79
|
-
"""
|
|
80
|
-
...
|
|
@@ -53,6 +53,7 @@ class ShellDetector:
|
|
|
53
53
|
("PS", "powershell", "powershell.exe", ["-NoLogo"]),
|
|
54
54
|
("CMD", "cmd", "cmd.exe", []),
|
|
55
55
|
("WSL", "wsl", "wsl.exe", []),
|
|
56
|
+
("Git Bash", "gitbash", r"C:\Program Files\Git\bin\bash.exe", ["--login"]),
|
|
56
57
|
]
|
|
57
58
|
return [
|
|
58
59
|
("Bash", "bash", "bash", ["--login"]),
|
|
@@ -118,7 +118,3 @@ class InMemoryTabRepository(TabRepository):
|
|
|
118
118
|
def count_for_user(self, user_id: UserId) -> int:
|
|
119
119
|
"""Get tab count for a user."""
|
|
120
120
|
return len(self._user_tabs.get(str(user_id), set()))
|
|
121
|
-
|
|
122
|
-
def all_tabs(self) -> list[Tab]:
|
|
123
|
-
"""Get all tabs."""
|
|
124
|
-
return list(self._tabs.values())
|
|
@@ -13,6 +13,12 @@ from rich.console import Console
|
|
|
13
13
|
console = Console()
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def _is_icmp_warning(line: str) -> bool:
|
|
17
|
+
"""Check if line is a harmless ICMP/ping warning from cloudflared."""
|
|
18
|
+
lower = line.lower()
|
|
19
|
+
return "icmp" in lower or "ping_group" in lower or "ping group" in lower
|
|
20
|
+
|
|
21
|
+
|
|
16
22
|
def wait_for_server(host: str, port: int, timeout: int = 30) -> bool:
|
|
17
23
|
"""Wait for the server to be ready and verify it's Porterminal.
|
|
18
24
|
|
|
@@ -137,8 +143,8 @@ def start_cloudflared(port: int) -> tuple[subprocess.Popen, str | None]:
|
|
|
137
143
|
url = match.group(0)
|
|
138
144
|
break
|
|
139
145
|
|
|
140
|
-
# Also check for errors
|
|
141
|
-
if "error" in line.lower():
|
|
146
|
+
# Also check for errors (ignore ICMP/ping warnings - harmless)
|
|
147
|
+
if "error" in line.lower() and not _is_icmp_warning(line):
|
|
142
148
|
console.print(f"[red]Cloudflared error:[/red] {line.strip()}")
|
|
143
149
|
|
|
144
150
|
return process, url
|
|
@@ -155,7 +161,8 @@ def drain_process_output(process: subprocess.Popen) -> None:
|
|
|
155
161
|
if not line:
|
|
156
162
|
break
|
|
157
163
|
line = line.strip()
|
|
158
|
-
|
|
164
|
+
# Print errors, but ignore harmless ICMP/ping warnings
|
|
165
|
+
if line and "error" in line.lower() and not _is_icmp_warning(line):
|
|
159
166
|
console.print(f"[red]{line}[/red]")
|
|
160
167
|
except (OSError, ValueError):
|
|
161
168
|
pass
|