sshplex 1.6.0__tar.gz → 1.6.2__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 (42) hide show
  1. {sshplex-1.6.0/sshplex.egg-info → sshplex-1.6.2}/PKG-INFO +19 -80
  2. {sshplex-1.6.0 → sshplex-1.6.2}/README.md +18 -79
  3. {sshplex-1.6.0 → sshplex-1.6.2}/pyproject.toml +1 -1
  4. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/__init__.py +1 -1
  5. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/multiplexer/iterm2_native.py +22 -5
  6. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/multiplexer/tmux.py +25 -3
  7. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/ui/config_editor.py +133 -22
  8. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/ui/host_selector.py +6 -7
  9. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/ui/session_manager.py +211 -67
  10. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/main.py +6 -4
  11. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/sshplex_connector.py +31 -20
  12. {sshplex-1.6.0 → sshplex-1.6.2/sshplex.egg-info}/PKG-INFO +19 -80
  13. {sshplex-1.6.0 → sshplex-1.6.2}/LICENSE +0 -0
  14. {sshplex-1.6.0 → sshplex-1.6.2}/MANIFEST.in +0 -0
  15. {sshplex-1.6.0 → sshplex-1.6.2}/setup.cfg +0 -0
  16. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/cli.py +0 -0
  17. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/config-template.yaml +0 -0
  18. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/__init__.py +0 -0
  19. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/cache.py +0 -0
  20. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/config.py +0 -0
  21. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/logger.py +0 -0
  22. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/multiplexer/__init__.py +0 -0
  23. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/multiplexer/base.py +0 -0
  24. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/onboarding/__init__.py +0 -0
  25. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/onboarding/wizard.py +0 -0
  26. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/sot/__init__.py +0 -0
  27. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/sot/ansible.py +0 -0
  28. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/sot/base.py +0 -0
  29. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/sot/consul.py +0 -0
  30. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/sot/factory.py +0 -0
  31. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/sot/netbox.py +0 -0
  32. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/sot/static.py +0 -0
  33. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/ui/__init__.py +0 -0
  34. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/utils/__init__.py +0 -0
  35. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex/lib/utils/iterm2.py +0 -0
  36. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex.egg-info/SOURCES.txt +0 -0
  37. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex.egg-info/dependency_links.txt +0 -0
  38. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex.egg-info/entry_points.txt +0 -0
  39. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex.egg-info/requires.txt +0 -0
  40. {sshplex-1.6.0 → sshplex-1.6.2}/sshplex.egg-info/top_level.txt +0 -0
  41. {sshplex-1.6.0 → sshplex-1.6.2}/tests/test_cache.py +0 -0
  42. {sshplex-1.6.0 → sshplex-1.6.2}/tests/test_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sshplex
3
- Version: 1.6.0
3
+ Version: 1.6.2
4
4
  Summary: Multiplex your SSH connections with style
5
5
  Author-email: MJAHED Sabri <contact@sabrimjahed.com>
6
6
  License: MIT
@@ -61,8 +61,8 @@ SSHplex is a Python-based SSH connection multiplexer with a modern TUI. Connect
61
61
 
62
62
  - 🖥️ **Modern TUI** - Textual-based host selector with search, sort, and multi-select
63
63
  - 🔌 **Multiple Sources** - NetBox, Ansible, Consul, static lists - use them together
64
- - 📦 **3 Backends** - tmux standalone, tmux + iTerm2, or iTerm2 native (macOS)
65
- - ✏️ **Config Editor** - Built-in YAML editor with validation (`e` key)
64
+ - 📦 **3 Mux Backends** - tmux standalone, tmux + iTerm2, or iTerm2 native (macOS)
65
+ - ✏️ **Config Editor** - Built-in YAML editor with validation
66
66
  - 🔄 **Broadcast Input** - Sync commands across multiple SSH sessions
67
67
  - 🔐 **SSH Security** - Configurable host key checking and retry logic
68
68
  - 🚀 **Fast Startup** - Intelligent caching with configurable TTL
@@ -83,24 +83,9 @@ sshplex
83
83
  ### Prerequisites
84
84
 
85
85
  - Python 3.8+
86
- - tmux (Linux/macOS) or iTerm2 (macOS)
86
+ - tmux (Linux/macOS) and/or iTerm2 (macOS)
87
87
  - SSH key configured for target hosts
88
88
 
89
- ## Usage
90
-
91
- | Key | Action |
92
- |-----|--------|
93
- | `Space` | Toggle host selection |
94
- | `a` / `d` | Select / Deselect all |
95
- | `Enter` | Connect to selected hosts |
96
- | `/` | Search/filter hosts |
97
- | `p` | Toggle panes/tabs mode |
98
- | `b` | Toggle broadcast mode |
99
- | `e` | Open config editor |
100
- | `s` | Open session manager |
101
- | `h` | Show keyboard shortcuts |
102
- | `q` | Quit |
103
-
104
89
  ## Multiplexer Backends
105
90
 
106
91
  | Backend | Platform | Best For |
@@ -109,68 +94,28 @@ sshplex
109
94
  | **tmux + iTerm2** | macOS | Native UI + persistence |
110
95
  | **iTerm2 native** | macOS | Simple setup, no tmux dependency |
111
96
 
112
- ```yaml
113
- # ~/.config/sshplex/sshplex.yaml
114
- tmux:
115
- backend: "tmux" # or "iterm2-native" on macOS
116
- layout: "tiled"
117
- max_panes_per_window: 5
118
- ```
119
97
 
120
- ## Sources of Truth
98
+ ## Local Demo (Consul + Ansible)
121
99
 
122
- ### Static Hosts
123
- ```yaml
124
- sot:
125
- import:
126
- - name: "my-servers"
127
- type: static
128
- hosts:
129
- - {name: "web-01", ip: "192.168.1.10", tags: ["web"]}
130
- ```
100
+ This repo includes a small demo setup that uses the same IP (`192.168.31.216`) with different host names.
131
101
 
132
- ### NetBox
133
- ```yaml
134
- sot:
135
- import:
136
- - name: "prod"
137
- type: netbox
138
- url: "https://netbox.example.com/"
139
- token: "your-api-token"
140
- ```
102
+ ```bash
103
+ # Start Consul + seed 3 demo nodes
104
+ docker compose -f demo/docker-compose.consul-demo.yml up -d
141
105
 
142
- ### Ansible
143
- ```yaml
144
- sot:
145
- import:
146
- - name: "inventory"
147
- type: ansible
148
- inventory_paths: ["/path/to/inventory.yml"]
106
+ # Optional: inspect nodes
107
+ curl -s http://localhost:8500/v1/catalog/nodes | jq
149
108
  ```
150
109
 
151
- ### Consul
152
- ```bash
153
- pip install "sshplex[consul]"
154
- ```
155
- ```yaml
156
- sot:
157
- import:
158
- - name: "dc1"
159
- type: consul
160
- config:
161
- host: "consul.example.com"
162
- token: "your-token"
163
- ```
110
+ Demo files:
111
+ - `demo/ansible-inventory-demo.yml`
112
+ - `demo/docker-compose.consul-demo.yml`
113
+ - `demo/sshplex.demo.yaml`
164
114
 
165
- ## CLI Reference
115
+ Run with the bundled demo config:
166
116
 
167
117
  ```bash
168
- sshplex # Launch TUI
169
- sshplex --onboarding # Interactive setup wizard
170
- sshplex --debug # Test provider connectivity
171
- sshplex --show-config # Show config paths
172
- sshplex --clear-cache # Clear host cache
173
- sshplex --config /path/to.yml # Use custom config
118
+ sshplex --config demo/sshplex.demo.yaml
174
119
  ```
175
120
 
176
121
  ## Documentation
@@ -187,14 +132,8 @@ sshplex --config /path/to.yml # Use custom config
187
132
  # Basic (tmux only)
188
133
  pip install sshplex
189
134
 
190
- # With Consul support
191
- pip install "sshplex[consul]"
192
-
193
- # With iTerm2 native support (macOS)
194
- pip install "sshplex[iterm2]"
195
-
196
- # Development
197
- pip install -e ".[dev]"
135
+ # With Consul,DEV,Iterm2 support
136
+ pip install "sshplex[dev,consul,iterm2]"
198
137
  ```
199
138
 
200
139
  ## Development
@@ -8,8 +8,8 @@ SSHplex is a Python-based SSH connection multiplexer with a modern TUI. Connect
8
8
 
9
9
  - 🖥️ **Modern TUI** - Textual-based host selector with search, sort, and multi-select
10
10
  - 🔌 **Multiple Sources** - NetBox, Ansible, Consul, static lists - use them together
11
- - 📦 **3 Backends** - tmux standalone, tmux + iTerm2, or iTerm2 native (macOS)
12
- - ✏️ **Config Editor** - Built-in YAML editor with validation (`e` key)
11
+ - 📦 **3 Mux Backends** - tmux standalone, tmux + iTerm2, or iTerm2 native (macOS)
12
+ - ✏️ **Config Editor** - Built-in YAML editor with validation
13
13
  - 🔄 **Broadcast Input** - Sync commands across multiple SSH sessions
14
14
  - 🔐 **SSH Security** - Configurable host key checking and retry logic
15
15
  - 🚀 **Fast Startup** - Intelligent caching with configurable TTL
@@ -30,24 +30,9 @@ sshplex
30
30
  ### Prerequisites
31
31
 
32
32
  - Python 3.8+
33
- - tmux (Linux/macOS) or iTerm2 (macOS)
33
+ - tmux (Linux/macOS) and/or iTerm2 (macOS)
34
34
  - SSH key configured for target hosts
35
35
 
36
- ## Usage
37
-
38
- | Key | Action |
39
- |-----|--------|
40
- | `Space` | Toggle host selection |
41
- | `a` / `d` | Select / Deselect all |
42
- | `Enter` | Connect to selected hosts |
43
- | `/` | Search/filter hosts |
44
- | `p` | Toggle panes/tabs mode |
45
- | `b` | Toggle broadcast mode |
46
- | `e` | Open config editor |
47
- | `s` | Open session manager |
48
- | `h` | Show keyboard shortcuts |
49
- | `q` | Quit |
50
-
51
36
  ## Multiplexer Backends
52
37
 
53
38
  | Backend | Platform | Best For |
@@ -56,68 +41,28 @@ sshplex
56
41
  | **tmux + iTerm2** | macOS | Native UI + persistence |
57
42
  | **iTerm2 native** | macOS | Simple setup, no tmux dependency |
58
43
 
59
- ```yaml
60
- # ~/.config/sshplex/sshplex.yaml
61
- tmux:
62
- backend: "tmux" # or "iterm2-native" on macOS
63
- layout: "tiled"
64
- max_panes_per_window: 5
65
- ```
66
44
 
67
- ## Sources of Truth
45
+ ## Local Demo (Consul + Ansible)
68
46
 
69
- ### Static Hosts
70
- ```yaml
71
- sot:
72
- import:
73
- - name: "my-servers"
74
- type: static
75
- hosts:
76
- - {name: "web-01", ip: "192.168.1.10", tags: ["web"]}
77
- ```
47
+ This repo includes a small demo setup that uses the same IP (`192.168.31.216`) with different host names.
78
48
 
79
- ### NetBox
80
- ```yaml
81
- sot:
82
- import:
83
- - name: "prod"
84
- type: netbox
85
- url: "https://netbox.example.com/"
86
- token: "your-api-token"
87
- ```
49
+ ```bash
50
+ # Start Consul + seed 3 demo nodes
51
+ docker compose -f demo/docker-compose.consul-demo.yml up -d
88
52
 
89
- ### Ansible
90
- ```yaml
91
- sot:
92
- import:
93
- - name: "inventory"
94
- type: ansible
95
- inventory_paths: ["/path/to/inventory.yml"]
53
+ # Optional: inspect nodes
54
+ curl -s http://localhost:8500/v1/catalog/nodes | jq
96
55
  ```
97
56
 
98
- ### Consul
99
- ```bash
100
- pip install "sshplex[consul]"
101
- ```
102
- ```yaml
103
- sot:
104
- import:
105
- - name: "dc1"
106
- type: consul
107
- config:
108
- host: "consul.example.com"
109
- token: "your-token"
110
- ```
57
+ Demo files:
58
+ - `demo/ansible-inventory-demo.yml`
59
+ - `demo/docker-compose.consul-demo.yml`
60
+ - `demo/sshplex.demo.yaml`
111
61
 
112
- ## CLI Reference
62
+ Run with the bundled demo config:
113
63
 
114
64
  ```bash
115
- sshplex # Launch TUI
116
- sshplex --onboarding # Interactive setup wizard
117
- sshplex --debug # Test provider connectivity
118
- sshplex --show-config # Show config paths
119
- sshplex --clear-cache # Clear host cache
120
- sshplex --config /path/to.yml # Use custom config
65
+ sshplex --config demo/sshplex.demo.yaml
121
66
  ```
122
67
 
123
68
  ## Documentation
@@ -134,14 +79,8 @@ sshplex --config /path/to.yml # Use custom config
134
79
  # Basic (tmux only)
135
80
  pip install sshplex
136
81
 
137
- # With Consul support
138
- pip install "sshplex[consul]"
139
-
140
- # With iTerm2 native support (macOS)
141
- pip install "sshplex[iterm2]"
142
-
143
- # Development
144
- pip install -e ".[dev]"
82
+ # With Consul,DEV,Iterm2 support
83
+ pip install "sshplex[dev,consul,iterm2]"
145
84
  ```
146
85
 
147
86
  ## Development
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sshplex"
7
- version = "1.6.0"
7
+ version = "1.6.2"
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.0"
2
+ __version__ = "1.6.2"
3
3
  __author__ = "MJAHED Sabri"
4
4
  __email__ = "contact@sabrimjahed.com"
@@ -7,9 +7,11 @@ Backend options:
7
7
  - backend: "iterm2-native" - Pure iTerm2 Python API (no tmux dependency)
8
8
  """
9
9
 
10
+ import contextlib
11
+ import io
10
12
  import platform
11
13
  import re
12
- from typing import Any, Dict, List, Optional
14
+ from typing import Any, List, Optional, Tuple
13
15
 
14
16
  from ..logger import get_logger
15
17
  from .base import MultiplexerBase
@@ -66,7 +68,7 @@ class ITerm2NativeManager(MultiplexerBase):
66
68
  self._check_iterm2_api()
67
69
 
68
70
  # Session tracking: hostname -> SSH command
69
- self._pending_sessions: Dict[str, str] = {}
71
+ self._pending_sessions: List[Tuple[str, str]] = []
70
72
 
71
73
  # Broadcast state
72
74
  self._broadcast_enabled = False
@@ -184,7 +186,7 @@ class ITerm2NativeManager(MultiplexerBase):
184
186
  if command is None:
185
187
  command = f"ssh {hostname}"
186
188
 
187
- self._pending_sessions[hostname] = command
189
+ self._pending_sessions.append((hostname, command))
188
190
  self._max_panes_per_tab = max_panes_per_window
189
191
  self.logger.info(f"SSHplex: Queued iTerm2 pane for '{hostname}'")
190
192
  return True
@@ -293,7 +295,7 @@ class ITerm2NativeManager(MultiplexerBase):
293
295
  return
294
296
 
295
297
  # Build sessions list for the async function
296
- sessions_data = list(self._pending_sessions.items())
298
+ sessions_data = list(self._pending_sessions)
297
299
  max_panes = self._max_panes_per_tab
298
300
  profile = self._profile
299
301
  split_pattern = self._split_pattern
@@ -544,7 +546,22 @@ class ITerm2NativeManager(MultiplexerBase):
544
546
  # Run with iTerm2's run_until_complete to maintain connection
545
547
  try:
546
548
  print(f"\n🚀 Creating iTerm2 session with {len(sessions_data)} SSH connections...")
547
- iterm2.run_until_complete(_create_sessions)
549
+ noisy_stderr = io.StringIO()
550
+ with contextlib.redirect_stderr(noisy_stderr):
551
+ iterm2.run_until_complete(_create_sessions)
552
+
553
+ stderr_text = noisy_stderr.getvalue().strip()
554
+ if stderr_text:
555
+ benign_markers = [
556
+ "ConnectionClosedError",
557
+ "CancelledError",
558
+ "sent 1000 (OK)",
559
+ "no close frame received",
560
+ ]
561
+ if any(marker in stderr_text for marker in benign_markers):
562
+ self.logger.debug("SSHplex: Suppressed benign iTerm2 websocket shutdown noise")
563
+ else:
564
+ self.logger.warning(f"SSHplex: iTerm2 stderr output: {stderr_text}")
548
565
  self.logger.info("SSHplex: iTerm2 session created successfully")
549
566
  except Exception as e:
550
567
  error_msg = str(e)
@@ -84,6 +84,28 @@ class TmuxManager(MultiplexerBase):
84
84
  self.logger.error(f"SSHplex: Failed to initialize tmux server: {e}")
85
85
  return False
86
86
 
87
+ @staticmethod
88
+ def _split_window(window: Any, vertical: bool = True) -> Any:
89
+ """Split tmux window with libtmux version compatibility."""
90
+ split_window = getattr(window, "split_window", None)
91
+ if callable(split_window):
92
+ try:
93
+ return split_window(vertical=vertical)
94
+ except Exception as exc:
95
+ if "deprecated" not in str(exc).lower() and "removed" not in str(exc).lower():
96
+ raise
97
+
98
+ split = getattr(window, "split", None)
99
+ if callable(split):
100
+ try:
101
+ from libtmux.window import PaneDirection
102
+ direction = PaneDirection.Below if vertical else PaneDirection.Right
103
+ return split(direction=direction)
104
+ except Exception:
105
+ return split()
106
+
107
+ raise RuntimeError("No compatible tmux split method found")
108
+
87
109
  def create_session(self) -> bool:
88
110
  """Create a new tmux session with SSHplex branding."""
89
111
  try:
@@ -179,20 +201,20 @@ class TmuxManager(MultiplexerBase):
179
201
  # Additional panes - attempt split with fallback
180
202
  vertical_split = (self.current_window_pane_count % 2 == 0)
181
203
  try:
182
- pane = self.current_window.split_window(vertical=vertical_split)
204
+ pane = self._split_window(self.current_window, vertical=vertical_split)
183
205
  except Exception as e:
184
206
  # Handle "no space" error by resizing or creating a new window
185
207
  self.logger.warning(f"Pane split failed ({e}), attempting layout adjustment")
186
208
  try:
187
209
  # Resize window to fit more panes
188
210
  self.current_window.resize(height=80, width=200)
189
- pane = self.current_window.split_window(vertical=vertical_split)
211
+ pane = self._split_window(self.current_window, vertical=vertical_split)
190
212
  except Exception:
191
213
  # If still fails, create a new window
192
214
  self.logger.info("Creating new window due to insufficient space")
193
215
  ensure_window_available()
194
216
  vertical_split = True # first split in new window
195
- pane = self.current_window.split_window(vertical=vertical_split)
217
+ pane = self._split_window(self.current_window, vertical=vertical_split)
196
218
 
197
219
  if pane is None:
198
220
  raise RuntimeError(f"Failed to create tmux pane for {hostname}")