sshplex 1.6.3__tar.gz → 1.6.4__tar.gz
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.
- {sshplex-1.6.3/sshplex.egg-info → sshplex-1.6.4}/PKG-INFO +1 -1
- {sshplex-1.6.3 → sshplex-1.6.4}/pyproject.toml +1 -1
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/__init__.py +1 -1
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/commands.py +0 -1
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/factory.py +25 -2
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/ui/session_manager.py +17 -11
- {sshplex-1.6.3 → sshplex-1.6.4/sshplex.egg-info}/PKG-INFO +1 -1
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex.egg-info/SOURCES.txt +2 -0
- sshplex-1.6.4/tests/test_commands.py +49 -0
- sshplex-1.6.4/tests/test_iterm2_session_manager.py +61 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/LICENSE +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/MANIFEST.in +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/README.md +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/setup.cfg +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/cli.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/config-template.yaml +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/__init__.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/cache.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/config.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/logger.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/multiplexer/__init__.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/multiplexer/base.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/multiplexer/iterm2_native.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/multiplexer/tmux.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/onboarding/__init__.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/onboarding/wizard.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/__init__.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/ansible.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/base.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/consul.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/netbox.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/static.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/ui/__init__.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/ui/config_editor.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/ui/host_selector.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/utils/__init__.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/utils/iterm2.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/utils/ssh_config.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/main.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/sshplex_connector.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex.egg-info/dependency_links.txt +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex.egg-info/entry_points.txt +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex.egg-info/requires.txt +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/sshplex.egg-info/top_level.txt +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/tests/test_cache.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/tests/test_config.py +0 -0
- {sshplex-1.6.3 → sshplex-1.6.4}/tests/test_main.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sshplex"
|
|
7
|
-
version = "1.6.
|
|
7
|
+
version = "1.6.4"
|
|
8
8
|
description = "Multiplex your SSH connections with style"
|
|
9
9
|
authors = [{name = "MJAHED Sabri", email = "contact@sabrimjahed.com"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -56,15 +56,38 @@ class SoTFactory:
|
|
|
56
56
|
self.logger.error("No import configurations found in sot.import")
|
|
57
57
|
return False
|
|
58
58
|
|
|
59
|
+
sot_config = self.config.sot
|
|
60
|
+
providers_explicitly_set = True
|
|
61
|
+
if hasattr(sot_config, "model_fields_set"):
|
|
62
|
+
providers_explicitly_set = "providers" in getattr(
|
|
63
|
+
sot_config,
|
|
64
|
+
"model_fields_set",
|
|
65
|
+
set(),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
provider_types_source = "config"
|
|
59
69
|
enabled_provider_types = {
|
|
60
70
|
str(provider_type).strip()
|
|
61
|
-
for provider_type in (getattr(
|
|
71
|
+
for provider_type in (getattr(sot_config, "providers", []) or [])
|
|
62
72
|
if str(provider_type).strip()
|
|
63
73
|
}
|
|
64
74
|
|
|
75
|
+
if not providers_explicitly_set:
|
|
76
|
+
provider_types_source = "imports"
|
|
77
|
+
enabled_provider_types = {
|
|
78
|
+
str(getattr(import_config, "type", "")).strip()
|
|
79
|
+
for import_config in configured_imports
|
|
80
|
+
if str(getattr(import_config, "type", "")).strip()
|
|
81
|
+
}
|
|
82
|
+
if enabled_provider_types:
|
|
83
|
+
self.logger.info(
|
|
84
|
+
"No explicit sot.providers configured; inferred enabled provider "
|
|
85
|
+
f"types from imports: {sorted(enabled_provider_types)}"
|
|
86
|
+
)
|
|
87
|
+
|
|
65
88
|
if enabled_provider_types:
|
|
66
89
|
self.logger.info(
|
|
67
|
-
f"Enabled provider types from
|
|
90
|
+
f"Enabled provider types from {provider_types_source}: {sorted(enabled_provider_types)}"
|
|
68
91
|
)
|
|
69
92
|
|
|
70
93
|
for import_config in configured_imports:
|
|
@@ -116,6 +116,7 @@ class ITerm2SessionManager(ModalScreen):
|
|
|
116
116
|
self.logger = get_logger()
|
|
117
117
|
self.config = config
|
|
118
118
|
self.tabs: List[ITerm2ManagedTab] = []
|
|
119
|
+
self.visible_tabs: List[ITerm2ManagedTab] = []
|
|
119
120
|
self.table: Optional[DataTable] = None
|
|
120
121
|
self.current_session_name = current_session_name
|
|
121
122
|
self.show_current_only = bool(current_session_name)
|
|
@@ -198,11 +199,12 @@ class ITerm2SessionManager(ModalScreen):
|
|
|
198
199
|
try:
|
|
199
200
|
self.tabs = await asyncio.to_thread(self._fetch_tabs_blocking)
|
|
200
201
|
self.populate_table()
|
|
201
|
-
if self.table and self.
|
|
202
|
+
if self.table and self.visible_tabs:
|
|
202
203
|
self.table.move_cursor(row=0)
|
|
203
204
|
self.logger.info(f"SSHplex: Loaded {len(self.tabs)} managed iTerm2 tabs")
|
|
204
205
|
except Exception as e:
|
|
205
206
|
self.logger.error(f"SSHplex: Failed to load iTerm2 managed tabs: {e}")
|
|
207
|
+
self.visible_tabs = []
|
|
206
208
|
if self.table is not None:
|
|
207
209
|
self.table.clear()
|
|
208
210
|
self.table.add_row("Error loading tabs", str(e), "-", "-")
|
|
@@ -212,15 +214,17 @@ class ITerm2SessionManager(ModalScreen):
|
|
|
212
214
|
return
|
|
213
215
|
|
|
214
216
|
self.table.clear()
|
|
215
|
-
visible_tabs = self.tabs
|
|
217
|
+
self.visible_tabs = list(self.tabs)
|
|
216
218
|
if self.show_current_only and self.current_session_name:
|
|
217
|
-
visible_tabs = [
|
|
219
|
+
self.visible_tabs = [
|
|
220
|
+
tab for tab in self.tabs if tab.session_name == self.current_session_name
|
|
221
|
+
]
|
|
218
222
|
|
|
219
|
-
if not visible_tabs:
|
|
223
|
+
if not self.visible_tabs:
|
|
220
224
|
self.table.add_row("No SSHplex iTerm2 tabs", "-", "-", "-")
|
|
221
225
|
return
|
|
222
226
|
|
|
223
|
-
for tab in visible_tabs:
|
|
227
|
+
for tab in self.visible_tabs:
|
|
224
228
|
self.table.add_row(
|
|
225
229
|
tab.hostname,
|
|
226
230
|
tab.session_name,
|
|
@@ -230,26 +234,26 @@ class ITerm2SessionManager(ModalScreen):
|
|
|
230
234
|
)
|
|
231
235
|
|
|
232
236
|
def action_move_up(self) -> None:
|
|
233
|
-
if self.table and self.
|
|
237
|
+
if self.table and self.visible_tabs:
|
|
234
238
|
current_row = self.table.cursor_row
|
|
235
239
|
if current_row > 0:
|
|
236
240
|
self.table.move_cursor(row=current_row - 1)
|
|
237
241
|
|
|
238
242
|
def action_move_down(self) -> None:
|
|
239
|
-
if self.table and self.
|
|
243
|
+
if self.table and self.visible_tabs:
|
|
240
244
|
current_row = self.table.cursor_row
|
|
241
|
-
if current_row < len(self.
|
|
245
|
+
if current_row < len(self.visible_tabs) - 1:
|
|
242
246
|
self.table.move_cursor(row=current_row + 1)
|
|
243
247
|
|
|
244
248
|
def action_kill_session(self) -> None:
|
|
245
|
-
if not self.table or not self.
|
|
249
|
+
if not self.table or not self.visible_tabs:
|
|
246
250
|
return
|
|
247
251
|
|
|
248
252
|
cursor_row = self.table.cursor_row
|
|
249
|
-
if cursor_row < 0 or cursor_row >= len(self.
|
|
253
|
+
if cursor_row < 0 or cursor_row >= len(self.visible_tabs):
|
|
250
254
|
return
|
|
251
255
|
|
|
252
|
-
tab = self.
|
|
256
|
+
tab = self.visible_tabs[cursor_row]
|
|
253
257
|
self._do_kill_tab(tab)
|
|
254
258
|
|
|
255
259
|
def _do_kill_tab(self, tab_item: ITerm2ManagedTab) -> None:
|
|
@@ -341,6 +345,8 @@ class ITerm2SessionManager(ModalScreen):
|
|
|
341
345
|
)
|
|
342
346
|
self.app.notify(f"Showing {mode}", timeout=2)
|
|
343
347
|
self.populate_table()
|
|
348
|
+
if self.table and self.visible_tabs:
|
|
349
|
+
self.table.move_cursor(row=0)
|
|
344
350
|
|
|
345
351
|
def action_close_manager(self) -> None:
|
|
346
352
|
self.dismiss()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Tests for shared command helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
from sshplex.lib.commands import clear_cache
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _minimal_config() -> SimpleNamespace:
|
|
12
|
+
return SimpleNamespace(cache=SimpleNamespace(cache_dir="~/.cache/sshplex", ttl_hours=24))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_clear_cache_still_attempts_delete_when_metadata_missing(capsys) -> None:
|
|
16
|
+
"""clear_cache should delete files even when metadata can't be read."""
|
|
17
|
+
config = _minimal_config()
|
|
18
|
+
logger = MagicMock()
|
|
19
|
+
|
|
20
|
+
fake_cache = MagicMock()
|
|
21
|
+
fake_cache.get_cache_info.return_value = None
|
|
22
|
+
fake_cache.clear_cache.return_value = True
|
|
23
|
+
|
|
24
|
+
with patch("sshplex.lib.commands.HostCache", return_value=fake_cache):
|
|
25
|
+
result = clear_cache(config, logger, no_cache_message="Clearing cache...")
|
|
26
|
+
|
|
27
|
+
assert result == 0
|
|
28
|
+
fake_cache.clear_cache.assert_called_once()
|
|
29
|
+
captured = capsys.readouterr().out
|
|
30
|
+
assert "Clearing cache" in captured
|
|
31
|
+
assert "Cache cleared successfully" in captured
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_clear_cache_returns_failure_when_delete_fails(capsys) -> None:
|
|
35
|
+
"""clear_cache should return non-zero when file deletion fails."""
|
|
36
|
+
config = _minimal_config()
|
|
37
|
+
logger = MagicMock()
|
|
38
|
+
|
|
39
|
+
fake_cache = MagicMock()
|
|
40
|
+
fake_cache.get_cache_info.return_value = None
|
|
41
|
+
fake_cache.clear_cache.return_value = False
|
|
42
|
+
|
|
43
|
+
with patch("sshplex.lib.commands.HostCache", return_value=fake_cache):
|
|
44
|
+
result = clear_cache(config, logger, no_cache_message="Clearing cache...")
|
|
45
|
+
|
|
46
|
+
assert result == 1
|
|
47
|
+
fake_cache.clear_cache.assert_called_once()
|
|
48
|
+
captured = capsys.readouterr().out
|
|
49
|
+
assert "Failed to clear cache" in captured
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Regression tests for iTerm2 session manager row targeting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
|
|
7
|
+
from sshplex.lib.ui.session_manager import ITerm2ManagedTab, ITerm2SessionManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FakeTable:
|
|
11
|
+
"""Simple DataTable test double with the subset we need."""
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
self.cursor_row = 0
|
|
15
|
+
self.rows: list[tuple[tuple[str, ...], str | None]] = []
|
|
16
|
+
|
|
17
|
+
def clear(self) -> None:
|
|
18
|
+
self.rows.clear()
|
|
19
|
+
|
|
20
|
+
def add_row(self, *values: str, key: str | None = None) -> None:
|
|
21
|
+
self.rows.append((values, key))
|
|
22
|
+
|
|
23
|
+
def move_cursor(self, row: int) -> None:
|
|
24
|
+
self.cursor_row = row
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_kill_session_targets_visible_filtered_tab() -> None:
|
|
28
|
+
"""Kill action should map to the visible filtered row, not raw tabs index."""
|
|
29
|
+
manager = ITerm2SessionManager(config=SimpleNamespace(), current_session_name="current")
|
|
30
|
+
manager.table = FakeTable()
|
|
31
|
+
manager.tabs = [
|
|
32
|
+
ITerm2ManagedTab(
|
|
33
|
+
tab_id="tab-other",
|
|
34
|
+
window_id="w1",
|
|
35
|
+
session_name="other",
|
|
36
|
+
hostname="other-host",
|
|
37
|
+
pane_count=1,
|
|
38
|
+
),
|
|
39
|
+
ITerm2ManagedTab(
|
|
40
|
+
tab_id="tab-current",
|
|
41
|
+
window_id="w1",
|
|
42
|
+
session_name="current",
|
|
43
|
+
hostname="current-host",
|
|
44
|
+
pane_count=2,
|
|
45
|
+
),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
killed: dict[str, ITerm2ManagedTab] = {}
|
|
49
|
+
|
|
50
|
+
def _capture_kill(tab: ITerm2ManagedTab) -> None:
|
|
51
|
+
killed["tab"] = tab
|
|
52
|
+
|
|
53
|
+
manager._do_kill_tab = _capture_kill # type: ignore[method-assign]
|
|
54
|
+
manager.populate_table()
|
|
55
|
+
|
|
56
|
+
assert [tab.tab_id for tab in manager.visible_tabs] == ["tab-current"]
|
|
57
|
+
manager.table.cursor_row = 0
|
|
58
|
+
|
|
59
|
+
manager.action_kill_session()
|
|
60
|
+
|
|
61
|
+
assert killed["tab"].tab_id == "tab-current"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|