susops 3.0.0rc3.dev1__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.
- susops/__init__.py +4 -0
- susops/client.py +230 -0
- susops/core/__init__.py +0 -0
- susops/core/browsers.py +330 -0
- susops/core/config.py +253 -0
- susops/core/log_style.py +92 -0
- susops/core/pac.py +185 -0
- susops/core/ports.py +57 -0
- susops/core/process.py +167 -0
- susops/core/rpc_protocol.py +186 -0
- susops/core/rpc_server.py +131 -0
- susops/core/services_daemon.py +312 -0
- susops/core/share.py +323 -0
- susops/core/socat.py +200 -0
- susops/core/ssh.py +330 -0
- susops/core/ssh_config.py +40 -0
- susops/core/status.py +245 -0
- susops/core/types.py +171 -0
- susops/facade.py +2237 -0
- susops/tray/__init__.py +20 -0
- susops/tray/base.py +650 -0
- susops/tray/linux.py +1623 -0
- susops/tray/mac.py +3105 -0
- susops/tui/__init__.py +0 -0
- susops/tui/__main__.py +44 -0
- susops/tui/app.py +191 -0
- susops/tui/cli.py +665 -0
- susops/tui/screens/__init__.py +114 -0
- susops/tui/screens/connections.py +871 -0
- susops/tui/screens/dashboard.py +935 -0
- susops/tui/screens/shares.py +357 -0
- susops/tui/widgets/__init__.py +0 -0
- susops/tui/widgets/connection_card.py +137 -0
- susops/version.py +12 -0
- susops-3.0.0rc3.dev1.dist-info/METADATA +977 -0
- susops-3.0.0rc3.dev1.dist-info/RECORD +40 -0
- susops-3.0.0rc3.dev1.dist-info/WHEEL +5 -0
- susops-3.0.0rc3.dev1.dist-info/entry_points.txt +7 -0
- susops-3.0.0rc3.dev1.dist-info/licenses/LICENSE +674 -0
- susops-3.0.0rc3.dev1.dist-info/top_level.txt +1 -0
susops/tui/__init__.py
ADDED
|
File without changes
|
susops/tui/__main__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""SusOps TUI / CLI entrypoint.
|
|
2
|
+
|
|
3
|
+
When stdout is a TTY and no subcommand is given: launch the Textual TUI.
|
|
4
|
+
Otherwise: dispatch the CLI command and exit with the appropriate code.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
from susops.tui.cli import build_parser, dispatch
|
|
13
|
+
|
|
14
|
+
parser = build_parser()
|
|
15
|
+
args = parser.parse_args()
|
|
16
|
+
|
|
17
|
+
# Launch interactive TUI if:
|
|
18
|
+
# - running in a terminal (isatty)
|
|
19
|
+
# - no subcommand was given
|
|
20
|
+
if sys.stdout.isatty() and args.command is None:
|
|
21
|
+
try:
|
|
22
|
+
from susops.tui.app import SusOpsTuiApp
|
|
23
|
+
app = SusOpsTuiApp(verbose=args.verbose)
|
|
24
|
+
app.run()
|
|
25
|
+
except ImportError as exc:
|
|
26
|
+
print(
|
|
27
|
+
f"Error: Textual is not installed ({exc}).\n"
|
|
28
|
+
"Install with: pip install 'susops[tui]'\n"
|
|
29
|
+
"Or use a subcommand directly, e.g.: susops ps",
|
|
30
|
+
file=sys.stderr,
|
|
31
|
+
)
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
elif args.command is None:
|
|
34
|
+
# Non-TTY with no subcommand: print status
|
|
35
|
+
args.command = "ps"
|
|
36
|
+
from susops.tui.cli import cmd_ps
|
|
37
|
+
args.func = cmd_ps
|
|
38
|
+
sys.exit(dispatch(args))
|
|
39
|
+
else:
|
|
40
|
+
sys.exit(dispatch(args))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|
susops/tui/app.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""SusOps Textual TUI application."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from textual.app import App, ComposeResult
|
|
5
|
+
from textual.binding import Binding
|
|
6
|
+
from textual.command import Hit, Hits, Provider
|
|
7
|
+
from textual.containers import Horizontal
|
|
8
|
+
from textual.screen import ModalScreen
|
|
9
|
+
from textual.widgets import Button, Label, Static
|
|
10
|
+
|
|
11
|
+
from susops.tui.screens.connections import ConnectionsScreen
|
|
12
|
+
from susops.tui.screens.dashboard import DashboardScreen
|
|
13
|
+
from susops.tui.screens.shares import SharesScreen
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _SusOpsCommands(Provider):
|
|
17
|
+
"""Command palette provider for quick actions."""
|
|
18
|
+
|
|
19
|
+
async def search(self, query: str) -> Hits:
|
|
20
|
+
app: SusOpsTuiApp = self.app # type: ignore[assignment]
|
|
21
|
+
commands = [
|
|
22
|
+
("Start tunnels", app.action_start_all, "Start all SSH tunnels"),
|
|
23
|
+
("Stop tunnels", app.action_stop_all, "Stop all SSH tunnels"),
|
|
24
|
+
("Restart tunnels", app.action_restart_all, "Restart all SSH tunnels"),
|
|
25
|
+
("Dashboard", lambda: app.push_screen("dashboard"), "Go to dashboard"),
|
|
26
|
+
("Connections", lambda: app.push_screen("connections"), "Manage connections"),
|
|
27
|
+
("Shares", lambda: app.push_screen("shares"), "Share/fetch files"),
|
|
28
|
+
("Launch Browser", app.action_launch_browser, "Open a browser with the daemon's PAC URL"),
|
|
29
|
+
("Open Proxy Settings", app.action_launch_browser, "chrome://net-internals/#proxy in a chosen browser"),
|
|
30
|
+
("Config", lambda: app.action_show_config(), "View config.yaml"),
|
|
31
|
+
("PAC file", lambda: app.action_show_pac(), "View PAC proxy config"),
|
|
32
|
+
("Quit", app.action_quit, "Quit SusOps"),
|
|
33
|
+
]
|
|
34
|
+
q = query.lower()
|
|
35
|
+
for name, action, description in commands:
|
|
36
|
+
if q in name.lower() or q in description.lower():
|
|
37
|
+
yield Hit(
|
|
38
|
+
score=1.0,
|
|
39
|
+
match_display=name,
|
|
40
|
+
command=action,
|
|
41
|
+
help=description,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _ConfigErrorScreen(ModalScreen):
|
|
46
|
+
"""Shown when config.yaml fails to load. Allows the user to open $EDITOR to fix it."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, error: str, config_path: str) -> None:
|
|
49
|
+
super().__init__()
|
|
50
|
+
self._error = error
|
|
51
|
+
self._config_path = config_path
|
|
52
|
+
|
|
53
|
+
def compose(self) -> ComposeResult:
|
|
54
|
+
with Static(classes="modal-dialog"):
|
|
55
|
+
yield Label("[bold red]Config file error[/bold red]")
|
|
56
|
+
yield Label(f"[dim]{self._config_path}[/dim]")
|
|
57
|
+
yield Label(self._error)
|
|
58
|
+
yield Label("\nFix the file and restart susops, or press [bold]e[/bold] to open in $EDITOR.")
|
|
59
|
+
with Horizontal(classes="modal-btn-row"):
|
|
60
|
+
yield Button("Open in $EDITOR", id="btn-edit", variant="warning")
|
|
61
|
+
yield Button("Quit", id="btn-quit", variant="error")
|
|
62
|
+
|
|
63
|
+
def on_button_pressed(self, event) -> None:
|
|
64
|
+
import os
|
|
65
|
+
import subprocess
|
|
66
|
+
if event.button.id == "btn-edit":
|
|
67
|
+
editor = os.environ.get("EDITOR", "nano")
|
|
68
|
+
subprocess.run([editor, self._config_path])
|
|
69
|
+
self.dismiss(None)
|
|
70
|
+
else:
|
|
71
|
+
self.app.exit(1)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SusOpsTuiApp(App):
|
|
75
|
+
"""SusOps Textual TUI — SSH tunnel + PAC proxy manager."""
|
|
76
|
+
|
|
77
|
+
TITLE = "SusOps"
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def SUB_TITLE(self) -> str: # type: ignore[override]
|
|
81
|
+
import susops
|
|
82
|
+
return f"SSH Tunnel & PAC Manager v{susops.__version__}"
|
|
83
|
+
|
|
84
|
+
CSS_PATH = "app.tcss"
|
|
85
|
+
|
|
86
|
+
BINDINGS = [
|
|
87
|
+
Binding("c", "push_screen('connections')", "Connections", show=False),
|
|
88
|
+
Binding("f", "push_screen('shares')", "Shares", show=False),
|
|
89
|
+
Binding("b", "launch_browser", "Browser", show=False),
|
|
90
|
+
Binding("s", "start_all", "Start", show=False),
|
|
91
|
+
Binding("x", "stop_all", "Stop", show=False),
|
|
92
|
+
Binding("r", "restart_all", "Restart", show=False),
|
|
93
|
+
Binding("ctrl+p", "command_palette", "Commands"),
|
|
94
|
+
Binding("q", "quit", "Quit"),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
SCREENS = {
|
|
98
|
+
"dashboard": DashboardScreen,
|
|
99
|
+
"connections": ConnectionsScreen,
|
|
100
|
+
"shares": SharesScreen,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
COMMANDS = App.COMMANDS | {_SusOpsCommands}
|
|
104
|
+
|
|
105
|
+
def __init__(self, verbose: bool = False) -> None:
|
|
106
|
+
super().__init__()
|
|
107
|
+
self._verbose = verbose
|
|
108
|
+
self.manager = None # type: ignore[assignment]
|
|
109
|
+
|
|
110
|
+
def on_mount(self) -> None:
|
|
111
|
+
from pathlib import Path
|
|
112
|
+
from susops.client import SusOpsClient
|
|
113
|
+
workspace = Path.home() / ".susops"
|
|
114
|
+
try:
|
|
115
|
+
self.manager = SusOpsClient(workspace=workspace)
|
|
116
|
+
except Exception as exc:
|
|
117
|
+
config_path = str(workspace / "config.yaml")
|
|
118
|
+
self.push_screen(_ConfigErrorScreen(str(exc), config_path))
|
|
119
|
+
return
|
|
120
|
+
self.push_screen("dashboard")
|
|
121
|
+
|
|
122
|
+
def action_quit(self) -> None:
|
|
123
|
+
if self.manager.app_config.stop_on_quit:
|
|
124
|
+
# Stops SSH tunnels + share servers in the daemon; PAC stays up
|
|
125
|
+
# via stopped-marker tombstones unless the user reset.
|
|
126
|
+
self.manager.stop_quick()
|
|
127
|
+
# No detach calls — the daemon is already a separate process and
|
|
128
|
+
# outlives the TUI.
|
|
129
|
+
self.exit()
|
|
130
|
+
|
|
131
|
+
def action_start_all(self) -> None:
|
|
132
|
+
self._bg_start()
|
|
133
|
+
|
|
134
|
+
def action_stop_all(self) -> None:
|
|
135
|
+
self._bg_stop()
|
|
136
|
+
|
|
137
|
+
def action_restart_all(self) -> None:
|
|
138
|
+
self._bg_restart()
|
|
139
|
+
|
|
140
|
+
def action_open_github(self) -> None:
|
|
141
|
+
import webbrowser
|
|
142
|
+
webbrowser.open("https://github.com/mashb1t/susops")
|
|
143
|
+
|
|
144
|
+
def action_show_config(self) -> None:
|
|
145
|
+
from susops.tui.screens.dashboard import DashboardScreen
|
|
146
|
+
screen = self.query_one(DashboardScreen)
|
|
147
|
+
screen.action_show_tab("tab-config")
|
|
148
|
+
|
|
149
|
+
def action_show_pac(self) -> None:
|
|
150
|
+
from susops.tui.screens.dashboard import DashboardScreen
|
|
151
|
+
screen = self.query_one(DashboardScreen)
|
|
152
|
+
screen.action_show_tab("tab-pac")
|
|
153
|
+
|
|
154
|
+
def action_launch_browser(self) -> None:
|
|
155
|
+
"""Open the modal browser picker. Works from any screen via global `b`
|
|
156
|
+
binding or the Ctrl+P command palette."""
|
|
157
|
+
from susops.tui.screens.dashboard import BrowserScreen
|
|
158
|
+
self.push_screen(BrowserScreen())
|
|
159
|
+
|
|
160
|
+
def _bg_start(self) -> None:
|
|
161
|
+
self.run_worker(self._do_start, thread=True)
|
|
162
|
+
|
|
163
|
+
def _bg_stop(self) -> None:
|
|
164
|
+
self.run_worker(self._do_stop, thread=True)
|
|
165
|
+
|
|
166
|
+
def _bg_restart(self) -> None:
|
|
167
|
+
self.run_worker(self._do_restart, thread=True)
|
|
168
|
+
|
|
169
|
+
def _do_start(self) -> None:
|
|
170
|
+
result = self.manager.start()
|
|
171
|
+
self.call_from_thread(
|
|
172
|
+
self.notify,
|
|
173
|
+
result.message,
|
|
174
|
+
severity="information" if result.success else "error",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def _do_stop(self) -> None:
|
|
178
|
+
result = self.manager.stop()
|
|
179
|
+
self.call_from_thread(
|
|
180
|
+
self.notify,
|
|
181
|
+
result.message,
|
|
182
|
+
severity="information" if result.success else "error",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def _do_restart(self) -> None:
|
|
186
|
+
result = self.manager.restart()
|
|
187
|
+
self.call_from_thread(
|
|
188
|
+
self.notify,
|
|
189
|
+
result.message,
|
|
190
|
+
severity="information" if result.success else "error",
|
|
191
|
+
)
|