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.
Files changed (47) hide show
  1. {sshplex-1.6.3/sshplex.egg-info → sshplex-1.6.4}/PKG-INFO +1 -1
  2. {sshplex-1.6.3 → sshplex-1.6.4}/pyproject.toml +1 -1
  3. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/__init__.py +1 -1
  4. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/commands.py +0 -1
  5. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/factory.py +25 -2
  6. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/ui/session_manager.py +17 -11
  7. {sshplex-1.6.3 → sshplex-1.6.4/sshplex.egg-info}/PKG-INFO +1 -1
  8. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex.egg-info/SOURCES.txt +2 -0
  9. sshplex-1.6.4/tests/test_commands.py +49 -0
  10. sshplex-1.6.4/tests/test_iterm2_session_manager.py +61 -0
  11. {sshplex-1.6.3 → sshplex-1.6.4}/LICENSE +0 -0
  12. {sshplex-1.6.3 → sshplex-1.6.4}/MANIFEST.in +0 -0
  13. {sshplex-1.6.3 → sshplex-1.6.4}/README.md +0 -0
  14. {sshplex-1.6.3 → sshplex-1.6.4}/setup.cfg +0 -0
  15. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/cli.py +0 -0
  16. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/config-template.yaml +0 -0
  17. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/__init__.py +0 -0
  18. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/cache.py +0 -0
  19. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/config.py +0 -0
  20. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/logger.py +0 -0
  21. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/multiplexer/__init__.py +0 -0
  22. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/multiplexer/base.py +0 -0
  23. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/multiplexer/iterm2_native.py +0 -0
  24. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/multiplexer/tmux.py +0 -0
  25. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/onboarding/__init__.py +0 -0
  26. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/onboarding/wizard.py +0 -0
  27. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/__init__.py +0 -0
  28. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/ansible.py +0 -0
  29. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/base.py +0 -0
  30. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/consul.py +0 -0
  31. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/netbox.py +0 -0
  32. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/sot/static.py +0 -0
  33. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/ui/__init__.py +0 -0
  34. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/ui/config_editor.py +0 -0
  35. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/ui/host_selector.py +0 -0
  36. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/utils/__init__.py +0 -0
  37. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/utils/iterm2.py +0 -0
  38. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/lib/utils/ssh_config.py +0 -0
  39. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/main.py +0 -0
  40. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex/sshplex_connector.py +0 -0
  41. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex.egg-info/dependency_links.txt +0 -0
  42. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex.egg-info/entry_points.txt +0 -0
  43. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex.egg-info/requires.txt +0 -0
  44. {sshplex-1.6.3 → sshplex-1.6.4}/sshplex.egg-info/top_level.txt +0 -0
  45. {sshplex-1.6.3 → sshplex-1.6.4}/tests/test_cache.py +0 -0
  46. {sshplex-1.6.3 → sshplex-1.6.4}/tests/test_config.py +0 -0
  47. {sshplex-1.6.3 → sshplex-1.6.4}/tests/test_main.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sshplex
3
- Version: 1.6.3
3
+ Version: 1.6.4
4
4
  Summary: Multiplex your SSH connections with style
5
5
  Author-email: MJAHED Sabri <contact@sabrimjahed.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sshplex"
7
- version = "1.6.3"
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"
@@ -1,4 +1,4 @@
1
1
  """SSHplex - SSH Connection Multiplexer"""
2
- __version__ = "1.6.3"
2
+ __version__ = "1.6.4"
3
3
  __author__ = "MJAHED Sabri"
4
4
  __email__ = "contact@sabrimjahed.com"
@@ -45,7 +45,6 @@ def clear_cache(config: Any, logger: Any, no_cache_message: str = "No cache to c
45
45
  )
46
46
  else:
47
47
  print(f"🗑️ {no_cache_message}")
48
- return 0
49
48
 
50
49
  if cache.clear_cache():
51
50
  print("✅ Cache cleared successfully")
@@ -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(self.config.sot, 'providers', []) or [])
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 config: {sorted(enabled_provider_types)}"
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.tabs:
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 = [tab for tab in self.tabs if tab.session_name == self.current_session_name]
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.tabs:
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.tabs:
243
+ if self.table and self.visible_tabs:
240
244
  current_row = self.table.cursor_row
241
- if current_row < len(self.tabs) - 1:
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.tabs:
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.tabs):
253
+ if cursor_row < 0 or cursor_row >= len(self.visible_tabs):
250
254
  return
251
255
 
252
- tab = self.tabs[cursor_row]
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sshplex
3
- Version: 1.6.3
3
+ Version: 1.6.4
4
4
  Summary: Multiplex your SSH connections with style
5
5
  Author-email: MJAHED Sabri <contact@sabrimjahed.com>
6
6
  License: MIT
@@ -39,5 +39,7 @@ sshplex/lib/utils/__init__.py
39
39
  sshplex/lib/utils/iterm2.py
40
40
  sshplex/lib/utils/ssh_config.py
41
41
  tests/test_cache.py
42
+ tests/test_commands.py
42
43
  tests/test_config.py
44
+ tests/test_iterm2_session_manager.py
43
45
  tests/test_main.py
@@ -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