sshplex 1.6.1__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.1/sshplex.egg-info → sshplex-1.6.2}/PKG-INFO +6 -112
  2. {sshplex-1.6.1 → sshplex-1.6.2}/README.md +5 -111
  3. {sshplex-1.6.1 → sshplex-1.6.2}/pyproject.toml +1 -1
  4. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/__init__.py +1 -1
  5. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/multiplexer/iterm2_native.py +19 -2
  6. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/multiplexer/tmux.py +16 -2
  7. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/ui/config_editor.py +91 -15
  8. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/ui/host_selector.py +6 -7
  9. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/ui/session_manager.py +144 -45
  10. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/main.py +3 -3
  11. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/sshplex_connector.py +1 -0
  12. {sshplex-1.6.1 → sshplex-1.6.2/sshplex.egg-info}/PKG-INFO +6 -112
  13. {sshplex-1.6.1 → sshplex-1.6.2}/LICENSE +0 -0
  14. {sshplex-1.6.1 → sshplex-1.6.2}/MANIFEST.in +0 -0
  15. {sshplex-1.6.1 → sshplex-1.6.2}/setup.cfg +0 -0
  16. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/cli.py +0 -0
  17. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/config-template.yaml +0 -0
  18. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/__init__.py +0 -0
  19. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/cache.py +0 -0
  20. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/config.py +0 -0
  21. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/logger.py +0 -0
  22. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/multiplexer/__init__.py +0 -0
  23. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/multiplexer/base.py +0 -0
  24. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/onboarding/__init__.py +0 -0
  25. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/onboarding/wizard.py +0 -0
  26. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/sot/__init__.py +0 -0
  27. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/sot/ansible.py +0 -0
  28. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/sot/base.py +0 -0
  29. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/sot/consul.py +0 -0
  30. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/sot/factory.py +0 -0
  31. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/sot/netbox.py +0 -0
  32. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/sot/static.py +0 -0
  33. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/ui/__init__.py +0 -0
  34. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/utils/__init__.py +0 -0
  35. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex/lib/utils/iterm2.py +0 -0
  36. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex.egg-info/SOURCES.txt +0 -0
  37. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex.egg-info/dependency_links.txt +0 -0
  38. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex.egg-info/entry_points.txt +0 -0
  39. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex.egg-info/requires.txt +0 -0
  40. {sshplex-1.6.1 → sshplex-1.6.2}/sshplex.egg-info/top_level.txt +0 -0
  41. {sshplex-1.6.1 → sshplex-1.6.2}/tests/test_cache.py +0 -0
  42. {sshplex-1.6.1 → 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.1
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,58 +94,6 @@ 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
-
120
- ## Sources of Truth
121
-
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
- ```
131
-
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
- ```
141
-
142
- ### Ansible
143
- ```yaml
144
- sot:
145
- import:
146
- - name: "inventory"
147
- type: ansible
148
- inventory_paths: ["/path/to/inventory.yml"]
149
- ```
150
-
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
- ```
164
97
 
165
98
  ## Local Demo (Consul + Ansible)
166
99
 
@@ -179,45 +112,12 @@ Demo files:
179
112
  - `demo/docker-compose.consul-demo.yml`
180
113
  - `demo/sshplex.demo.yaml`
181
114
 
182
- Example config snippet:
183
-
184
- ```yaml
185
- sot:
186
- providers: ["ansible", "consul"]
187
- import:
188
- - name: "demo-ansible"
189
- type: ansible
190
- inventory_paths:
191
- - "demo/ansible-inventory-demo.yml"
192
-
193
- - name: "demo-consul"
194
- type: consul
195
- config:
196
- host: "127.0.0.1"
197
- port: 8500
198
- token: ""
199
- scheme: "http"
200
- verify: false
201
- dc: "dc1"
202
- ```
203
-
204
115
  Run with the bundled demo config:
205
116
 
206
117
  ```bash
207
118
  sshplex --config demo/sshplex.demo.yaml
208
119
  ```
209
120
 
210
- ## CLI Reference
211
-
212
- ```bash
213
- sshplex # Launch TUI
214
- sshplex --onboarding # Interactive setup wizard
215
- sshplex --debug # Test provider connectivity
216
- sshplex --show-config # Show config paths
217
- sshplex --clear-cache # Clear host cache
218
- sshplex --config /path/to.yml # Use custom config
219
- ```
220
-
221
121
  ## Documentation
222
122
 
223
123
  | Guide | Description |
@@ -232,14 +132,8 @@ sshplex --config /path/to.yml # Use custom config
232
132
  # Basic (tmux only)
233
133
  pip install sshplex
234
134
 
235
- # With Consul support
236
- pip install "sshplex[consul]"
237
-
238
- # With iTerm2 native support (macOS)
239
- pip install "sshplex[iterm2]"
240
-
241
- # Development
242
- pip install -e ".[dev]"
135
+ # With Consul,DEV,Iterm2 support
136
+ pip install "sshplex[dev,consul,iterm2]"
243
137
  ```
244
138
 
245
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,58 +41,6 @@ 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
-
67
- ## Sources of Truth
68
-
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
- ```
78
-
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
- ```
88
-
89
- ### Ansible
90
- ```yaml
91
- sot:
92
- import:
93
- - name: "inventory"
94
- type: ansible
95
- inventory_paths: ["/path/to/inventory.yml"]
96
- ```
97
-
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
- ```
111
44
 
112
45
  ## Local Demo (Consul + Ansible)
113
46
 
@@ -126,45 +59,12 @@ Demo files:
126
59
  - `demo/docker-compose.consul-demo.yml`
127
60
  - `demo/sshplex.demo.yaml`
128
61
 
129
- Example config snippet:
130
-
131
- ```yaml
132
- sot:
133
- providers: ["ansible", "consul"]
134
- import:
135
- - name: "demo-ansible"
136
- type: ansible
137
- inventory_paths:
138
- - "demo/ansible-inventory-demo.yml"
139
-
140
- - name: "demo-consul"
141
- type: consul
142
- config:
143
- host: "127.0.0.1"
144
- port: 8500
145
- token: ""
146
- scheme: "http"
147
- verify: false
148
- dc: "dc1"
149
- ```
150
-
151
62
  Run with the bundled demo config:
152
63
 
153
64
  ```bash
154
65
  sshplex --config demo/sshplex.demo.yaml
155
66
  ```
156
67
 
157
- ## CLI Reference
158
-
159
- ```bash
160
- sshplex # Launch TUI
161
- sshplex --onboarding # Interactive setup wizard
162
- sshplex --debug # Test provider connectivity
163
- sshplex --show-config # Show config paths
164
- sshplex --clear-cache # Clear host cache
165
- sshplex --config /path/to.yml # Use custom config
166
- ```
167
-
168
68
  ## Documentation
169
69
 
170
70
  | Guide | Description |
@@ -179,14 +79,8 @@ sshplex --config /path/to.yml # Use custom config
179
79
  # Basic (tmux only)
180
80
  pip install sshplex
181
81
 
182
- # With Consul support
183
- pip install "sshplex[consul]"
184
-
185
- # With iTerm2 native support (macOS)
186
- pip install "sshplex[iterm2]"
187
-
188
- # Development
189
- pip install -e ".[dev]"
82
+ # With Consul,DEV,Iterm2 support
83
+ pip install "sshplex[dev,consul,iterm2]"
190
84
  ```
191
85
 
192
86
  ## Development
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sshplex"
7
- version = "1.6.1"
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.1"
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, Tuple
14
+ from typing import Any, List, Optional, Tuple
13
15
 
14
16
  from ..logger import get_logger
15
17
  from .base import MultiplexerBase
@@ -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)
@@ -87,10 +87,24 @@ class TmuxManager(MultiplexerBase):
87
87
  @staticmethod
88
88
  def _split_window(window: Any, vertical: bool = True) -> Any:
89
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
+
90
98
  split = getattr(window, "split", None)
91
99
  if callable(split):
92
- return split(vertical=vertical)
93
- return window.split_window(vertical=vertical)
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")
94
108
 
95
109
  def create_session(self) -> bool:
96
110
  """Create a new tmux session with SSHplex branding."""
@@ -1,5 +1,6 @@
1
1
  """SSHplex Configuration Editor Screen."""
2
2
 
3
+ from pathlib import Path
3
4
  from typing import Any, Dict, List
4
5
 
5
6
  import yaml
@@ -42,6 +43,7 @@ class ConfigEditorScreen(ModalScreen[bool]):
42
43
 
43
44
  BINDINGS = [
44
45
  Binding("escape", "cancel", "Cancel", show=True),
46
+ Binding("q", "cancel", "Cancel", show=False),
45
47
  Binding("ctrl+s", "save", "Save", show=True, priority=True),
46
48
  ]
47
49
 
@@ -77,15 +79,17 @@ class ConfigEditorScreen(ModalScreen[bool]):
77
79
  }
78
80
 
79
81
  #editor-buttons {
80
- height: 3;
82
+ height: 5;
81
83
  dock: bottom;
82
84
  align: center middle;
83
- margin-top: 1;
85
+ padding: 1 0;
84
86
  }
85
87
 
86
88
  #editor-buttons Button {
89
+ height: 3;
87
90
  min-width: 18;
88
91
  margin: 0 2;
92
+ content-align: center middle;
89
93
  }
90
94
 
91
95
  TabbedContent {
@@ -153,13 +157,22 @@ class ConfigEditorScreen(ModalScreen[bool]):
153
157
  }
154
158
  """
155
159
 
156
- def __init__(self, config: Config) -> None:
160
+ def __init__(self, config: Config, config_path: str = "") -> None:
157
161
  super().__init__()
158
162
  self.config = config
163
+ self.config_path = config_path
159
164
  self._proxy_counter = 0
160
165
  self._import_counter = 0
161
166
  self._import_types: Dict[str, str] = {} # idx -> current type
162
167
  self._mux_backend: str = "tmux" # current mux backend
168
+ self._table_column_presets: Dict[str, List[str]] = {
169
+ "custom": [],
170
+ "minimal": ["name", "ip"],
171
+ "standard": ["name", "ip", "cluster", "role", "tags"],
172
+ "operational": ["name", "ip", "status", "role", "cluster", "source"],
173
+ "inventory": ["name", "ip", "site", "platform", "env", "role", "status"],
174
+ }
175
+ self._table_columns_hint = self._build_table_columns_hint()
163
176
 
164
177
  @staticmethod
165
178
  def _safe_select_initial(value: str, allowed: List[str], default: str) -> str:
@@ -168,6 +181,49 @@ class ConfigEditorScreen(ModalScreen[bool]):
168
181
  return value
169
182
  return default
170
183
 
184
+ def _detect_table_columns_preset(self) -> str:
185
+ """Detect which preset matches current table columns."""
186
+ current = [str(c).strip() for c in self.config.ui.table_columns]
187
+ for preset, cols in self._table_column_presets.items():
188
+ if preset == "custom":
189
+ continue
190
+ if current == cols:
191
+ return preset
192
+ return "custom"
193
+
194
+ def _build_table_columns_hint(self) -> str:
195
+ """Build a user-friendly hint for available table columns."""
196
+ common = ["name", "ip", "cluster", "role", "tags", "status", "source", "site", "platform", "env"]
197
+ metadata_keys: List[str] = []
198
+
199
+ try:
200
+ cache_dir = Path(str(getattr(self.config.cache, "cache_dir", "~/cache/sshplex"))).expanduser()
201
+ cache_file = cache_dir / "hosts.yaml"
202
+ if cache_file.exists():
203
+ with open(cache_file) as f:
204
+ hosts_data = yaml.safe_load(f) or []
205
+
206
+ keys: set[str] = set()
207
+ for host in hosts_data[:200]:
208
+ if not isinstance(host, dict):
209
+ continue
210
+ metadata = host.get("metadata", {})
211
+ if isinstance(metadata, dict):
212
+ keys.update(str(k) for k in metadata)
213
+
214
+ metadata_keys = sorted(k for k in keys if k not in {"name", "ip", "metadata"})
215
+ except Exception:
216
+ metadata_keys = []
217
+
218
+ if metadata_keys:
219
+ sample = ", ".join(metadata_keys[:8])
220
+ return (
221
+ "Presets available. Common: " + ", ".join(common) +
222
+ f" | Cached metadata keys: {sample}"
223
+ )
224
+
225
+ return "Presets available. Common columns: " + ", ".join(common)
226
+
171
227
  def compose(self) -> ComposeResult:
172
228
  with Vertical(id="config-editor-dialog"):
173
229
  yield Static("SSHplex Configuration Editor", id="editor-title")
@@ -192,11 +248,26 @@ class ConfigEditorScreen(ModalScreen[bool]):
192
248
  "Log Panel Height (%)",
193
249
  Input(value=str(self.config.ui.log_panel_height)),
194
250
  )
251
+ yield _form_field(
252
+ "cfg-ui-table_columns_preset",
253
+ "Table Columns Preset",
254
+ Select(
255
+ [
256
+ ("Custom", "custom"),
257
+ ("Minimal", "minimal"),
258
+ ("Standard", "standard"),
259
+ ("Operational", "operational"),
260
+ ("Inventory", "inventory"),
261
+ ],
262
+ value=self._detect_table_columns_preset(),
263
+ ),
264
+ "Choose a preset or keep Custom",
265
+ )
195
266
  yield _form_field(
196
267
  "cfg-ui-table_columns",
197
268
  "Table Columns",
198
269
  Input(value=", ".join(self.config.ui.table_columns)),
199
- "Comma-separated column names",
270
+ self._table_columns_hint,
200
271
  )
201
272
  yield Static("Logging", classes="section-header")
202
273
  yield _form_field(
@@ -343,6 +414,12 @@ class ConfigEditorScreen(ModalScreen[bool]):
343
414
  Switch(value=self.config.tmux.broadcast),
344
415
  "Start with broadcast enabled",
345
416
  )
417
+ yield _form_field(
418
+ "cfg-mux-register_history",
419
+ "Register SSHPlex Commands in Shell History",
420
+ Switch(value=not bool(getattr(self.config.tmux, 'iterm2_native_hide_from_history', True))),
421
+ "iTerm2-native only. OFF means commands are hidden from history.",
422
+ )
346
423
  yield _form_field(
347
424
  "cfg-mux-window_name",
348
425
  "Window Name",
@@ -469,13 +546,6 @@ class ConfigEditorScreen(ModalScreen[bool]):
469
546
  ),
470
547
  "Pane split pattern",
471
548
  ))
472
- children.append(_form_field(
473
- "cfg-mux-iterm2_native_hide_from_history",
474
- "Hide from Shell History",
475
- Switch(value=bool(getattr(self.config.tmux, 'iterm2_native_hide_from_history', True))),
476
- "Prefix dispatched commands with a leading space",
477
- ))
478
-
479
549
  return Vertical(*children, id="mux-backend-fields-container")
480
550
 
481
551
  # --- Dynamic proxy list ---
@@ -628,6 +698,14 @@ class ConfigEditorScreen(ModalScreen[bool]):
628
698
  self.run_worker(self._rebuild_mux_backend_fields(new_backend))
629
699
  return
630
700
 
701
+ # Handle table-column presets
702
+ if select_id == "cfg-ui-table_columns_preset":
703
+ preset = str(event.value)
704
+ if preset in self._table_column_presets and preset != "custom":
705
+ columns_input = self.query_one("#cfg-ui-table_columns", Input)
706
+ columns_input.value = ", ".join(self._table_column_presets[preset])
707
+ return
708
+
631
709
  # Handle import type changes
632
710
  if not select_id.startswith("import-") or not select_id.endswith("-type"):
633
711
  return
@@ -785,6 +863,7 @@ class ConfigEditorScreen(ModalScreen[bool]):
785
863
  "use_panes": self._get_select_value("cfg-mux-use_panes", "panes") == "panes",
786
864
  "layout": self._get_select_value("cfg-mux-layout", "tiled"),
787
865
  "broadcast": self._get_switch_value("cfg-mux-broadcast"),
866
+ "iterm2_native_hide_from_history": not self._get_switch_value("cfg-mux-register_history", False),
788
867
  "window_name": self._get_input_value("cfg-mux-window_name", "sshplex"),
789
868
  "max_panes_per_window": int(self._get_input_value("cfg-mux-max_panes_per_window", "5")),
790
869
  }
@@ -800,9 +879,6 @@ class ConfigEditorScreen(ModalScreen[bool]):
800
879
  mux_data["iterm2_native_target"] = self._get_select_value("cfg-mux-iterm2_native_target", "current-window")
801
880
  mux_data["iterm2_profile"] = self._get_input_value("cfg-mux-iterm2_profile", "Default")
802
881
  mux_data["iterm2_split_pattern"] = self._get_select_value("cfg-mux-iterm2_split_pattern", "alternate")
803
- mux_data["iterm2_native_hide_from_history"] = self._get_switch_value(
804
- "cfg-mux-iterm2_native_hide_from_history"
805
- )
806
882
 
807
883
  data["tmux"] = mux_data
808
884
 
@@ -946,7 +1022,7 @@ class ConfigEditorScreen(ModalScreen[bool]):
946
1022
  # Remove None values for cleaner YAML
947
1023
  yaml_data = _clean_none(yaml_data)
948
1024
 
949
- config_path = get_default_config_path()
1025
+ config_path = get_default_config_path() if not self.config_path else Path(self.config_path)
950
1026
  config_path.parent.mkdir(parents=True, exist_ok=True)
951
1027
 
952
1028
  try:
@@ -235,7 +235,7 @@ class HostSelector(App):
235
235
  use_panes: reactive[bool] = reactive(True) # True for panes, False for tabs
236
236
  use_broadcast: reactive[bool] = reactive(False) # True for broadcast enabled, False for disabled
237
237
 
238
- def __init__(self, config: Any) -> None:
238
+ def __init__(self, config: Any, config_path: str = "") -> None:
239
239
  """Initialize the host selector.
240
240
 
241
241
  Args:
@@ -243,6 +243,7 @@ class HostSelector(App):
243
243
  """
244
244
  super().__init__()
245
245
  self.config = config
246
+ self.config_path = config_path
246
247
  self.logger = get_logger()
247
248
  self.hosts: List[Host] = []
248
249
  self.filtered_hosts: List[Host] = []
@@ -693,8 +694,7 @@ class HostSelector(App):
693
694
  control_with_iterm2 = bool(getattr(self.config.tmux, "control_with_iterm2", False))
694
695
  if backend == "iterm2-native":
695
696
  self.log_message("Opening iTerm2 native session manager...")
696
- session_manager = ITerm2SessionManager(self.config, self.latest_native_session_name)
697
- self.push_screen(session_manager)
697
+ self.push_screen(ITerm2SessionManager(self.config, self.latest_native_session_name))
698
698
  return
699
699
 
700
700
  if control_with_iterm2:
@@ -705,8 +705,7 @@ class HostSelector(App):
705
705
  return
706
706
 
707
707
  self.log_message("Opening tmux session manager...")
708
- session_manager = TmuxSessionManager(self.config)
709
- self.push_screen(session_manager)
708
+ self.push_screen(TmuxSessionManager(self.config))
710
709
 
711
710
  def action_edit_config(self) -> None:
712
711
  """Open the configuration editor modal."""
@@ -716,14 +715,14 @@ class HostSelector(App):
716
715
  if saved:
717
716
  self.run_worker(self._reload_config_runtime(), name="reload_config_runtime")
718
717
 
719
- editor = ConfigEditorScreen(self.config)
718
+ editor = ConfigEditorScreen(self.config, self.config_path)
720
719
  self.push_screen(editor, callback=_on_editor_close)
721
720
 
722
721
  async def _reload_config_runtime(self) -> None:
723
722
  """Reload configuration from disk and apply it live."""
724
723
  try:
725
724
  old_sot = self.config.sot.model_dump()
726
- new_config = load_config()
725
+ new_config = load_config(self.config_path or None)
727
726
  await self._apply_runtime_config(new_config)
728
727
  self.log_message("Configuration reloaded successfully")
729
728
  if old_sot != new_config.sot.model_dump():
@@ -1,6 +1,8 @@
1
1
  """SSHplex TUI tmux session manager widget."""
2
2
 
3
3
  import asyncio
4
+ import contextlib
5
+ import io
4
6
  from typing import Any, List, Optional
5
7
 
6
8
  import libtmux
@@ -91,16 +93,31 @@ class ConfirmDialog(ModalScreen[bool]):
91
93
  class TmuxSession:
92
94
  """Simple tmux session data structure."""
93
95
 
94
- def __init__(self, name: str, session_id: str, created: str, windows: int, attached: bool = False):
96
+ def __init__(
97
+ self,
98
+ name: str,
99
+ session_id: str,
100
+ created: str,
101
+ age: str,
102
+ windows: int,
103
+ panes: int,
104
+ clients: int,
105
+ active_cmd: str,
106
+ broadcast: bool = False,
107
+ ):
95
108
  self.name = name
96
109
  self.session_id = session_id
97
110
  self.created = created
111
+ self.age = age
98
112
  self.windows = windows
99
- self.attached = attached
113
+ self.panes = panes
114
+ self.clients = clients
115
+ self.active_cmd = active_cmd
116
+ self.broadcast = broadcast
100
117
 
101
118
  def __str__(self) -> str:
102
- status = "📎" if self.attached else "💤"
103
- return f"{status} {self.name} ({self.windows} windows)"
119
+ status = "ON" if self.broadcast else "OFF"
120
+ return f"{self.name} ({self.windows} windows, broadcast {status})"
104
121
 
105
122
 
106
123
  class ITerm2ManagedTab:
@@ -130,20 +147,20 @@ class ITerm2SessionManager(ModalScreen):
130
147
  }
131
148
 
132
149
  #session-dialog {
133
- width: 90;
134
- height: 22;
150
+ width: 96%;
151
+ height: 88%;
135
152
  border: thick $primary 60%;
136
153
  background: $surface;
137
154
  }
138
155
 
139
156
  #session-table {
140
157
  height: 1fr;
141
- margin: 1;
158
+ margin: 0 1;
142
159
  }
143
160
 
144
161
  #session-header {
145
162
  height: 3;
146
- margin: 1;
163
+ margin: 1 1 0 1;
147
164
  text-align: center;
148
165
  background: $primary;
149
166
  color: $text;
@@ -151,7 +168,7 @@ class ITerm2SessionManager(ModalScreen):
151
168
 
152
169
  #session-footer {
153
170
  height: 3;
154
- margin: 1;
171
+ margin: 0 1 1 1;
155
172
  text-align: center;
156
173
  background: $surface;
157
174
  color: $text-muted;
@@ -237,7 +254,9 @@ class ITerm2SessionManager(ModalScreen):
237
254
  )
238
255
 
239
256
  try:
240
- iterm2.run_until_complete(_load)
257
+ noisy_stderr = io.StringIO()
258
+ with contextlib.redirect_stderr(noisy_stderr):
259
+ iterm2.run_until_complete(_load)
241
260
  except SystemExit as e:
242
261
  raise RuntimeError("Failed to connect to iTerm2 API") from e
243
262
  return loaded
@@ -322,7 +341,9 @@ class ITerm2SessionManager(ModalScreen):
322
341
  return
323
342
 
324
343
  try:
325
- iterm2.run_until_complete(_kill)
344
+ noisy_stderr = io.StringIO()
345
+ with contextlib.redirect_stderr(noisy_stderr):
346
+ iterm2.run_until_complete(_kill)
326
347
  except SystemExit as e:
327
348
  raise RuntimeError("Failed to connect to iTerm2 API") from e
328
349
 
@@ -381,36 +402,36 @@ class TmuxSessionManager(ModalScreen):
381
402
  }
382
403
 
383
404
  #session-dialog {
384
- width: 80;
385
- height: 20;
405
+ width: 96%;
406
+ height: 88%;
386
407
  border: thick $primary 60%;
387
408
  background: $surface;
388
409
  }
389
410
 
390
411
  #session-table {
391
412
  height: 1fr;
392
- margin: 1;
413
+ margin: 0 1;
393
414
  }
394
415
 
395
416
  #session-header {
396
- height: 3;
397
- margin: 1;
417
+ height: 2;
418
+ margin: 1 1 0 1;
398
419
  text-align: center;
399
420
  background: $primary;
400
421
  color: $text;
401
422
  }
402
423
 
403
424
  #broadcast-status {
404
- height: 2;
405
- margin: 1;
425
+ height: 1;
426
+ margin: 0 1;
406
427
  text-align: center;
407
428
  background: $secondary;
408
429
  color: $text;
409
430
  }
410
431
 
411
432
  #session-footer {
412
- height: 3;
413
- margin: 1;
433
+ height: 2;
434
+ margin: 0 1 1 1;
414
435
  text-align: center;
415
436
  background: $surface;
416
437
  color: $text-muted;
@@ -443,10 +464,24 @@ class TmuxSessionManager(ModalScreen):
443
464
  @staticmethod
444
465
  def _split_window(window: Any, vertical: bool = True) -> Any:
445
466
  """Split tmux window with libtmux version compatibility."""
467
+ split_window = getattr(window, "split_window", None)
468
+ if callable(split_window):
469
+ try:
470
+ return split_window(vertical=vertical)
471
+ except Exception as exc:
472
+ if "deprecated" not in str(exc).lower() and "removed" not in str(exc).lower():
473
+ raise
474
+
446
475
  split = getattr(window, "split", None)
447
476
  if callable(split):
448
- return split(vertical=vertical)
449
- return window.split_window(vertical=vertical)
477
+ try:
478
+ from libtmux.window import PaneDirection
479
+ direction = PaneDirection.Below if vertical else PaneDirection.Right
480
+ return split(direction=direction)
481
+ except Exception:
482
+ return split()
483
+
484
+ raise RuntimeError("No compatible tmux split method found")
450
485
 
451
486
  def _find_tmux_session(self, session_name: str) -> Optional[Any]:
452
487
  """Find tmux session with libtmux compatibility fallbacks."""
@@ -484,10 +519,14 @@ class TmuxSessionManager(ModalScreen):
484
519
  self.table = self.query_one("#session-table", DataTable)
485
520
 
486
521
  # Setup table columns
487
- self.table.add_column("Status", width=8)
488
- self.table.add_column("Session Name", width=25)
489
- self.table.add_column("Created", width=20)
490
- self.table.add_column("Windows", width=8)
522
+ self.table.add_column("Broadcast", width=9)
523
+ self.table.add_column("Session Name", width=26)
524
+ self.table.add_column("Age", width=9)
525
+ self.table.add_column("Clients", width=8)
526
+ self.table.add_column("Active Cmd", width=18)
527
+ self.table.add_column("Created", width=16)
528
+ self.table.add_column("Windows", width=7)
529
+ self.table.add_column("Panes", width=6)
491
530
 
492
531
  # Load sessions first
493
532
  self.load_sessions()
@@ -510,22 +549,37 @@ class TmuxSessionManager(ModalScreen):
510
549
  self.sessions.clear()
511
550
 
512
551
  for session in tmux_sessions:
513
- # Check if session is attached (use session.attached property or check windows)
514
- try:
515
- # libtmux sessions have an 'attached' property or we can check if it has windows
516
- attached = hasattr(session, 'attached') and session.attached
517
- if not hasattr(session, 'attached'):
518
- # Fallback: check if session has active windows/panes
519
- attached = len(session.windows) > 0 and any(len(w.panes) > 0 for w in session.windows)
520
- except (AttributeError, RuntimeError):
521
- attached = False
522
-
523
552
  # Get window count safely
524
553
  try:
525
554
  window_count = len(session.windows) if hasattr(session, 'windows') else 0
526
555
  except (AttributeError, RuntimeError):
527
556
  window_count = 0
528
557
 
558
+ # Get pane count safely
559
+ try:
560
+ pane_count = sum(len(w.panes) for w in session.windows)
561
+ except (AttributeError, RuntimeError, TypeError):
562
+ pane_count = 0
563
+
564
+ # Read synchronize-panes state across session windows
565
+ try:
566
+ broadcast_on = any(
567
+ bool(window.show_options().get("synchronize-panes"))
568
+ for window in session.windows
569
+ )
570
+ except Exception:
571
+ broadcast_on = False
572
+
573
+ # Get attached client count
574
+ try:
575
+ attached_result = session.cmd('display-message', '-p', '#{session_attached}')
576
+ if attached_result and getattr(attached_result, 'stdout', None):
577
+ clients = int(attached_result.stdout[0])
578
+ else:
579
+ clients = 0
580
+ except Exception:
581
+ clients = 0
582
+
529
583
  # Get creation time - libtmux doesn't provide session.created directly
530
584
  try:
531
585
  # Try to get session creation time from tmux itself
@@ -535,17 +589,54 @@ class TmuxSessionManager(ModalScreen):
535
589
  timestamp = int(result.stdout[0])
536
590
  created_dt = datetime.datetime.fromtimestamp(timestamp)
537
591
  created = created_dt.strftime("%Y-%m-%d %H:%M:%S")
592
+ now_dt = datetime.datetime.now()
593
+ age_delta = max((now_dt - created_dt).total_seconds(), 0)
594
+ if age_delta < 60:
595
+ age = f"{int(age_delta)}s"
596
+ elif age_delta < 3600:
597
+ age = f"{int(age_delta // 60)}m"
598
+ elif age_delta < 86400:
599
+ age = f"{int(age_delta // 3600)}h"
600
+ else:
601
+ age = f"{int(age_delta // 86400)}d"
538
602
  else:
539
603
  created = "Unknown"
604
+ age = "-"
540
605
  except (ValueError, AttributeError, IndexError, OSError):
541
606
  created = "Unknown"
607
+ age = "-"
608
+
609
+ # Summarize active pane commands in this session
610
+ cmd_counts: dict[str, int] = {}
611
+ try:
612
+ for window in session.windows:
613
+ for pane in window.panes:
614
+ try:
615
+ pane_cmd = str(getattr(pane, 'pane_current_command', '') or '')
616
+ except Exception:
617
+ pane_cmd = ''
618
+ if not pane_cmd:
619
+ pane_cmd = '?'
620
+ cmd_counts[pane_cmd] = cmd_counts.get(pane_cmd, 0) + 1
621
+ except Exception:
622
+ pass
623
+
624
+ if cmd_counts:
625
+ top = sorted(cmd_counts.items(), key=lambda x: x[1], reverse=True)[:2]
626
+ active_cmd = ", ".join(f"{name}({count})" for name, count in top)
627
+ else:
628
+ active_cmd = "-"
542
629
 
543
630
  tmux_session = TmuxSession(
544
631
  name=session.session_name or "Unknown",
545
632
  session_id=session.session_id or "Unknown",
546
633
  created=created,
634
+ age=age,
547
635
  windows=window_count,
548
- attached=attached
636
+ panes=pane_count,
637
+ clients=clients,
638
+ active_cmd=active_cmd,
639
+ broadcast=broadcast_on
549
640
  )
550
641
  self.sessions.append(tmux_session)
551
642
 
@@ -559,7 +650,7 @@ class TmuxSessionManager(ModalScreen):
559
650
  # Show error in table
560
651
  if self.table is not None:
561
652
  self.table.clear()
562
- self.table.add_row("", "tmux error", str(e), "0")
653
+ self.table.add_row("-", "tmux error", "-", "-", "-", str(e), "0", "0")
563
654
 
564
655
  def populate_table(self) -> None:
565
656
  """Populate the table with session data."""
@@ -570,19 +661,20 @@ class TmuxSessionManager(ModalScreen):
570
661
  self.table.clear()
571
662
 
572
663
  if not self.sessions:
573
- self.table.add_row("ℹ️", "No tmux sessions found", "Create one with SSHplex", "0")
664
+ self.table.add_row("-", "No tmux sessions found", "-", "-", "-", "Create one with SSHplex", "0", "0")
574
665
  return
575
666
 
576
667
  # Add sessions to table
577
668
  for session in self.sessions:
578
- status_icon = "📎" if session.attached else "💤"
579
- status_text = "Active" if session.attached else "Detached"
580
-
581
669
  self.table.add_row(
582
- f"{status_icon} {status_text}",
670
+ "ON" if session.broadcast else "OFF",
583
671
  session.name,
672
+ session.age,
673
+ str(session.clients),
674
+ session.active_cmd,
584
675
  session.created,
585
676
  str(session.windows),
677
+ str(session.panes),
586
678
  key=session.name
587
679
  )
588
680
 
@@ -755,8 +847,12 @@ class TmuxSessionManager(ModalScreen):
755
847
  self.logger.error(f"SSHplex: Session '{session.name}' not found")
756
848
  return
757
849
 
758
- # Toggle broadcast mode
759
- self.broadcast_enabled = not self.broadcast_enabled
850
+ # Toggle from actual current state
851
+ current_enabled = any(
852
+ bool(window.show_options().get("synchronize-panes"))
853
+ for window in tmux_session.windows
854
+ )
855
+ self.broadcast_enabled = not current_enabled
760
856
 
761
857
  if self.broadcast_enabled:
762
858
  # Enable synchronize-panes for all windows in the session
@@ -778,6 +874,9 @@ class TmuxSessionManager(ModalScreen):
778
874
  status_widget = self.query_one("#broadcast-status", Static)
779
875
  status_widget.update("📡 Broadcast: OFF")
780
876
 
877
+ # Refresh table to update per-session broadcast column
878
+ self.load_sessions()
879
+
781
880
  except Exception as e:
782
881
  self.logger.error(f"SSHplex: Failed to toggle broadcast for session '{session.name}': {e}")
783
882
  else:
@@ -108,7 +108,7 @@ Examples:
108
108
  return debug_mode(config, logger)
109
109
  else:
110
110
  # TUI mode - main application
111
- return tui_mode(config, logger)
111
+ return tui_mode(config, logger, args.config)
112
112
 
113
113
  except FileNotFoundError as e:
114
114
  print(f"Error: {e}")
@@ -236,13 +236,13 @@ def debug_mode(config: Any, logger: Any) -> int:
236
236
  return 0
237
237
 
238
238
 
239
- def tui_mode(config: Any, logger: Any) -> int:
239
+ def tui_mode(config: Any, logger: Any, config_path: Optional[str] = None) -> int:
240
240
  """Run in TUI mode for interactive host selection and connection."""
241
241
  logger.info("Starting TUI mode - interactive host selection")
242
242
 
243
243
  try:
244
244
  # Start the host selector TUI
245
- app = HostSelector(config=config)
245
+ app = HostSelector(config=config, config_path=config_path or "")
246
246
  selected_hosts = app.run()
247
247
 
248
248
  if not selected_hosts:
@@ -44,6 +44,7 @@ class SSHplexConnector:
44
44
  self.backend = getattr(config.tmux, 'backend', 'tmux') if config else 'tmux'
45
45
  self.last_success_count = 0
46
46
  self.last_failed_hosts: List[str] = []
47
+ self.multiplexer: Any
47
48
 
48
49
  # Initialize multiplexer based on backend config
49
50
  if self.backend == "iterm2-native":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sshplex
3
- Version: 1.6.1
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,58 +94,6 @@ 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
-
120
- ## Sources of Truth
121
-
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
- ```
131
-
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
- ```
141
-
142
- ### Ansible
143
- ```yaml
144
- sot:
145
- import:
146
- - name: "inventory"
147
- type: ansible
148
- inventory_paths: ["/path/to/inventory.yml"]
149
- ```
150
-
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
- ```
164
97
 
165
98
  ## Local Demo (Consul + Ansible)
166
99
 
@@ -179,45 +112,12 @@ Demo files:
179
112
  - `demo/docker-compose.consul-demo.yml`
180
113
  - `demo/sshplex.demo.yaml`
181
114
 
182
- Example config snippet:
183
-
184
- ```yaml
185
- sot:
186
- providers: ["ansible", "consul"]
187
- import:
188
- - name: "demo-ansible"
189
- type: ansible
190
- inventory_paths:
191
- - "demo/ansible-inventory-demo.yml"
192
-
193
- - name: "demo-consul"
194
- type: consul
195
- config:
196
- host: "127.0.0.1"
197
- port: 8500
198
- token: ""
199
- scheme: "http"
200
- verify: false
201
- dc: "dc1"
202
- ```
203
-
204
115
  Run with the bundled demo config:
205
116
 
206
117
  ```bash
207
118
  sshplex --config demo/sshplex.demo.yaml
208
119
  ```
209
120
 
210
- ## CLI Reference
211
-
212
- ```bash
213
- sshplex # Launch TUI
214
- sshplex --onboarding # Interactive setup wizard
215
- sshplex --debug # Test provider connectivity
216
- sshplex --show-config # Show config paths
217
- sshplex --clear-cache # Clear host cache
218
- sshplex --config /path/to.yml # Use custom config
219
- ```
220
-
221
121
  ## Documentation
222
122
 
223
123
  | Guide | Description |
@@ -232,14 +132,8 @@ sshplex --config /path/to.yml # Use custom config
232
132
  # Basic (tmux only)
233
133
  pip install sshplex
234
134
 
235
- # With Consul support
236
- pip install "sshplex[consul]"
237
-
238
- # With iTerm2 native support (macOS)
239
- pip install "sshplex[iterm2]"
240
-
241
- # Development
242
- pip install -e ".[dev]"
135
+ # With Consul,DEV,Iterm2 support
136
+ pip install "sshplex[dev,consul,iterm2]"
243
137
  ```
244
138
 
245
139
  ## Development
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