ntermqt 0.1.1__tar.gz → 0.1.3__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 (66) hide show
  1. {ntermqt-0.1.1/ntermqt.egg-info → ntermqt-0.1.3}/PKG-INFO +118 -11
  2. {ntermqt-0.1.1 → ntermqt-0.1.3}/README.md +108 -10
  3. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/__main__.py +132 -9
  4. ntermqt-0.1.3/nterm/scripting/__init__.py +43 -0
  5. ntermqt-0.1.3/nterm/scripting/api.py +447 -0
  6. ntermqt-0.1.3/nterm/scripting/cli.py +305 -0
  7. ntermqt-0.1.3/nterm/session/local_terminal.py +225 -0
  8. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/session/pty_transport.py +105 -91
  9. {ntermqt-0.1.1 → ntermqt-0.1.3/ntermqt.egg-info}/PKG-INFO +118 -11
  10. {ntermqt-0.1.1 → ntermqt-0.1.3}/ntermqt.egg-info/SOURCES.txt +4 -0
  11. {ntermqt-0.1.1 → ntermqt-0.1.3}/ntermqt.egg-info/entry_points.txt +1 -0
  12. ntermqt-0.1.3/ntermqt.egg-info/requires.txt +30 -0
  13. {ntermqt-0.1.1 → ntermqt-0.1.3}/pyproject.toml +10 -3
  14. ntermqt-0.1.1/ntermqt.egg-info/requires.txt +0 -15
  15. {ntermqt-0.1.1 → ntermqt-0.1.3}/MANIFEST.in +0 -0
  16. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/__init__.py +0 -0
  17. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/askpass/__init__.py +0 -0
  18. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/askpass/server.py +0 -0
  19. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/config.py +0 -0
  20. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/connection/__init__.py +0 -0
  21. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/connection/profile.py +0 -0
  22. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/__init__.py +0 -0
  23. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/connect_dialog.py +0 -0
  24. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/editor.py +0 -0
  25. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/io.py +0 -0
  26. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/models.py +0 -0
  27. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/settings.py +0 -0
  28. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/tree.py +0 -0
  29. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/resources.py +0 -0
  30. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/session/__init__.py +0 -0
  31. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/session/askpass_ssh.py +0 -0
  32. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/session/base.py +0 -0
  33. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/session/interactive_ssh.py +0 -0
  34. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/session/ssh.py +0 -0
  35. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/__init__.py +0 -0
  36. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/bridge.py +0 -0
  37. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/terminal.html +0 -0
  38. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/terminal.js +0 -0
  39. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/xterm-addon-fit.min.js +0 -0
  40. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/xterm-addon-unicode11.min.js +0 -0
  41. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/xterm-addon-web-links.min.js +0 -0
  42. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/xterm.css +0 -0
  43. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/xterm.min.js +0 -0
  44. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/widget.py +0 -0
  45. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/__init__.py +0 -0
  46. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/engine.py +0 -0
  47. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/stylesheet.py +0 -0
  48. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/clean.yaml +0 -0
  49. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/default.yaml +0 -0
  50. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/dracula.yaml +0 -0
  51. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/enterprise_dark.yaml +0 -0
  52. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/enterprise_hybrid.yaml +0 -0
  53. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/enterprise_light.yaml +0 -0
  54. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/gruvbox_dark.yaml +0 -0
  55. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/gruvbox_hybrid.yaml +0 -0
  56. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/gruvbox_light.yaml +0 -0
  57. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/__init__.py +0 -0
  58. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/credential_manager.py +0 -0
  59. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/keychain.py +0 -0
  60. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/manager_ui.py +0 -0
  61. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/profile.py +0 -0
  62. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/resolver.py +0 -0
  63. {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/store.py +0 -0
  64. {ntermqt-0.1.1 → ntermqt-0.1.3}/ntermqt.egg-info/dependency_links.txt +0 -0
  65. {ntermqt-0.1.1 → ntermqt-0.1.3}/ntermqt.egg-info/top_level.txt +0 -0
  66. {ntermqt-0.1.1 → ntermqt-0.1.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ntermqt
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Modern SSH terminal widget for PyQt6 with credential vault and jump host support
5
5
  Author: Scott Peterman
6
6
  License: GPL-3.0
@@ -25,14 +25,23 @@ Requires-Dist: PyQt6-WebEngine>=6.4.0
25
25
  Requires-Dist: paramiko>=3.0.0
26
26
  Requires-Dist: cryptography>=41.0.0
27
27
  Requires-Dist: pyyaml>=6.0
28
+ Requires-Dist: click>=8.0.0
29
+ Requires-Dist: pexpect>=4.8.0; sys_platform != "win32"
30
+ Requires-Dist: pywinpty>=2.0.0; sys_platform == "win32"
28
31
  Provides-Extra: keyring
29
32
  Requires-Dist: keyring>=24.0.0; extra == "keyring"
33
+ Provides-Extra: scripting
34
+ Requires-Dist: ipython>=8.0.0; extra == "scripting"
30
35
  Provides-Extra: dev
31
36
  Requires-Dist: pytest; extra == "dev"
32
37
  Requires-Dist: black; extra == "dev"
33
38
  Requires-Dist: pyinstaller; extra == "dev"
34
39
  Requires-Dist: build; extra == "dev"
35
40
  Requires-Dist: twine; extra == "dev"
41
+ Requires-Dist: ipython>=8.0.0; extra == "dev"
42
+ Provides-Extra: all
43
+ Requires-Dist: keyring>=24.0.0; extra == "all"
44
+ Requires-Dist: ipython>=8.0.0; extra == "all"
36
45
 
37
46
  # nterm
38
47
 
@@ -74,6 +83,12 @@ Built for managing hundreds of devices through bastion hosts with hardware secur
74
83
  - Cross-platform keychain: macOS Keychain, Windows Credential Locker, Linux Secret Service
75
84
  - Full PyQt6 management UI
76
85
 
86
+ **Scripting API** *(Experimental)*
87
+ - Query device inventory and credentials programmatically
88
+ - Built-in IPython console with API pre-loaded
89
+ - CLI for shell scripts and automation
90
+ - Foundation for MCP tools and agentic workflows
91
+
77
92
  ---
78
93
 
79
94
  ## Screenshots
@@ -88,15 +103,48 @@ Built for managing hundreds of devices through bastion hosts with hardware secur
88
103
 
89
104
  ---
90
105
 
106
+ ## Dev Console
107
+
108
+ nterm includes a built-in development console accessible via **Dev → IPython** or **Dev → Shell**. Open in a tab alongside your SSH sessions, or pop out to a separate window.
109
+
110
+ ![IPython Console](https://raw.githubusercontent.com/scottpeterman/nterm/main/screenshots/ipython.png)
111
+
112
+ The IPython console runs in the same Python environment as nterm, with the scripting API pre-loaded. Query your device inventory, inspect credentials, and prototype automation workflows without leaving the app.
113
+
114
+ ```python
115
+ # Available immediately when IPython opens
116
+ api.devices() # List all saved devices
117
+ api.search("leaf") # Search by name/hostname
118
+ api.credentials() # List credentials (after api.unlock())
119
+ api.help() # Show all commands
120
+ ```
121
+
122
+ **Use cases:**
123
+ - Debug connection issues with live access to session objects
124
+ - Prototype automation scripts against your real device inventory
125
+ - Test credential resolution patterns
126
+ - Build and test MCP tools interactively
127
+
128
+ Requires the `scripting` extra: `pip install ntermqt[scripting]`
129
+
130
+ ---
131
+
91
132
  ## Installation
92
133
 
134
+ ### Be aware due to a naming conflict, the pypi package is actually "ntermqt"
135
+
93
136
  ### From PyPI
94
137
 
138
+ https://pypi.org/project/ntermqt/
139
+
95
140
  ```bash
96
- pip install nterm
141
+ pip install ntermqt
97
142
 
98
- # Optional: system keychain support
99
- pip install nterm[keyring]
143
+ # With optional scripting support (IPython)
144
+ pip install ntermqt[scripting]
145
+
146
+ # With all optional features
147
+ pip install ntermqt[all]
100
148
 
101
149
  # Run
102
150
  nterm
@@ -114,10 +162,7 @@ source .venv/bin/activate # Linux/macOS
114
162
  # .venv\Scripts\activate # Windows
115
163
 
116
164
  # Install in development mode
117
- pip install -e .
118
-
119
- # Optional: system keychain support
120
- pip install keyring
165
+ pip install -e ".[all]"
121
166
 
122
167
  # Run
123
168
  nterm
@@ -137,12 +182,70 @@ python -m nterm
137
182
 
138
183
  | Platform | PTY | Keychain |
139
184
  |----------|-----|----------|
140
- | Linux | ✅ Native | Secret Service |
141
- | macOS | ✅ Native | macOS Keychain |
185
+ | Linux | ✅ pexpect | Secret Service |
186
+ | macOS | ✅ pexpect | macOS Keychain |
142
187
  | Windows 10+ | ✅ pywinpty | Credential Locker |
143
188
 
144
189
  ---
145
190
 
191
+ ## Scripting API *(Experimental)*
192
+
193
+ nterm includes a scripting API for programmatic access to your device inventory and credential vault. Use it from IPython, CLI, or Python scripts.
194
+
195
+ ### IPython Console
196
+
197
+ Open **Dev → IPython → Open in Tab** to get an interactive console with the API pre-loaded:
198
+
199
+ ```python
200
+ api.devices() # List all saved devices
201
+ api.search("leaf") # Search by name/hostname
202
+ api.devices("eng-*") # Glob pattern filter
203
+
204
+ api.unlock("vault-password") # Unlock credential vault
205
+ api.credentials() # List credentials (metadata only)
206
+
207
+ api.help() # Show all commands
208
+ ```
209
+
210
+ ### CLI
211
+
212
+ ```bash
213
+ nterm-cli devices # List all devices
214
+ nterm-cli search leaf # Search devices
215
+ nterm-cli device eng-leaf-1 # Device details
216
+ nterm-cli credentials --unlock # List credentials
217
+ nterm-cli --json devices # JSON output for scripting
218
+ ```
219
+
220
+ ### Python Scripts
221
+
222
+ ```python
223
+ from nterm.scripting import NTermAPI
224
+
225
+ api = NTermAPI()
226
+
227
+ # Query devices
228
+ for device in api.devices("*spine*"):
229
+ print(f"{device.name}: {device.hostname}")
230
+
231
+ # Work with credentials
232
+ api.unlock("vault-password")
233
+ cred = api.credential("lab-admin")
234
+ print(f"Username: {cred.username}")
235
+ ```
236
+
237
+ ### Roadmap
238
+
239
+ The scripting API is the foundation for:
240
+
241
+ - **Command execution** — `api.connect()` and `api.send()` for programmatic device interaction
242
+ - **Batch operations** — Fan out commands across device groups
243
+ - **MCP tool integration** — Expose nterm capabilities to AI agents
244
+
245
+ See [scripting/README.md](nterm/scripting/README.md) for full API documentation.
246
+
247
+ ---
248
+
146
249
  ## Quick Start
147
250
 
148
251
  ### As a Widget
@@ -289,6 +392,7 @@ nterm/
289
392
  ├── session/
290
393
  │ ├── ssh.py # SSHSession (Paramiko) with legacy fallback
291
394
  │ ├── interactive_ssh.py # Native SSH + PTY
395
+ │ ├── local_terminal.py # Local shell/IPython sessions
292
396
  │ └── pty_transport.py # Cross-platform PTY
293
397
  ├── terminal/
294
398
  │ ├── widget.py # TerminalWidget (PyQt6 + xterm.js)
@@ -300,7 +404,10 @@ nterm/
300
404
  │ ├── store.py # Encrypted credential storage
301
405
  │ ├── resolver.py # Pattern-based resolution
302
406
  │ └── manager_ui.py # PyQt6 credential manager
303
- └── manager/ # Session tree, connection dialogs
407
+ ├── manager/ # Session tree, connection dialogs
408
+ └── scripting/ # API, CLI, automation support
409
+ ├── api.py # NTermAPI class
410
+ └── cli.py # nterm-cli entry point
304
411
  ```
305
412
 
306
413
  ---
@@ -38,6 +38,12 @@ Built for managing hundreds of devices through bastion hosts with hardware secur
38
38
  - Cross-platform keychain: macOS Keychain, Windows Credential Locker, Linux Secret Service
39
39
  - Full PyQt6 management UI
40
40
 
41
+ **Scripting API** *(Experimental)*
42
+ - Query device inventory and credentials programmatically
43
+ - Built-in IPython console with API pre-loaded
44
+ - CLI for shell scripts and automation
45
+ - Foundation for MCP tools and agentic workflows
46
+
41
47
  ---
42
48
 
43
49
  ## Screenshots
@@ -52,15 +58,48 @@ Built for managing hundreds of devices through bastion hosts with hardware secur
52
58
 
53
59
  ---
54
60
 
61
+ ## Dev Console
62
+
63
+ nterm includes a built-in development console accessible via **Dev → IPython** or **Dev → Shell**. Open in a tab alongside your SSH sessions, or pop out to a separate window.
64
+
65
+ ![IPython Console](https://raw.githubusercontent.com/scottpeterman/nterm/main/screenshots/ipython.png)
66
+
67
+ The IPython console runs in the same Python environment as nterm, with the scripting API pre-loaded. Query your device inventory, inspect credentials, and prototype automation workflows without leaving the app.
68
+
69
+ ```python
70
+ # Available immediately when IPython opens
71
+ api.devices() # List all saved devices
72
+ api.search("leaf") # Search by name/hostname
73
+ api.credentials() # List credentials (after api.unlock())
74
+ api.help() # Show all commands
75
+ ```
76
+
77
+ **Use cases:**
78
+ - Debug connection issues with live access to session objects
79
+ - Prototype automation scripts against your real device inventory
80
+ - Test credential resolution patterns
81
+ - Build and test MCP tools interactively
82
+
83
+ Requires the `scripting` extra: `pip install ntermqt[scripting]`
84
+
85
+ ---
86
+
55
87
  ## Installation
56
88
 
89
+ ### Be aware due to a naming conflict, the pypi package is actually "ntermqt"
90
+
57
91
  ### From PyPI
58
92
 
93
+ https://pypi.org/project/ntermqt/
94
+
59
95
  ```bash
60
- pip install nterm
96
+ pip install ntermqt
61
97
 
62
- # Optional: system keychain support
63
- pip install nterm[keyring]
98
+ # With optional scripting support (IPython)
99
+ pip install ntermqt[scripting]
100
+
101
+ # With all optional features
102
+ pip install ntermqt[all]
64
103
 
65
104
  # Run
66
105
  nterm
@@ -78,10 +117,7 @@ source .venv/bin/activate # Linux/macOS
78
117
  # .venv\Scripts\activate # Windows
79
118
 
80
119
  # Install in development mode
81
- pip install -e .
82
-
83
- # Optional: system keychain support
84
- pip install keyring
120
+ pip install -e ".[all]"
85
121
 
86
122
  # Run
87
123
  nterm
@@ -101,12 +137,70 @@ python -m nterm
101
137
 
102
138
  | Platform | PTY | Keychain |
103
139
  |----------|-----|----------|
104
- | Linux | ✅ Native | Secret Service |
105
- | macOS | ✅ Native | macOS Keychain |
140
+ | Linux | ✅ pexpect | Secret Service |
141
+ | macOS | ✅ pexpect | macOS Keychain |
106
142
  | Windows 10+ | ✅ pywinpty | Credential Locker |
107
143
 
108
144
  ---
109
145
 
146
+ ## Scripting API *(Experimental)*
147
+
148
+ nterm includes a scripting API for programmatic access to your device inventory and credential vault. Use it from IPython, CLI, or Python scripts.
149
+
150
+ ### IPython Console
151
+
152
+ Open **Dev → IPython → Open in Tab** to get an interactive console with the API pre-loaded:
153
+
154
+ ```python
155
+ api.devices() # List all saved devices
156
+ api.search("leaf") # Search by name/hostname
157
+ api.devices("eng-*") # Glob pattern filter
158
+
159
+ api.unlock("vault-password") # Unlock credential vault
160
+ api.credentials() # List credentials (metadata only)
161
+
162
+ api.help() # Show all commands
163
+ ```
164
+
165
+ ### CLI
166
+
167
+ ```bash
168
+ nterm-cli devices # List all devices
169
+ nterm-cli search leaf # Search devices
170
+ nterm-cli device eng-leaf-1 # Device details
171
+ nterm-cli credentials --unlock # List credentials
172
+ nterm-cli --json devices # JSON output for scripting
173
+ ```
174
+
175
+ ### Python Scripts
176
+
177
+ ```python
178
+ from nterm.scripting import NTermAPI
179
+
180
+ api = NTermAPI()
181
+
182
+ # Query devices
183
+ for device in api.devices("*spine*"):
184
+ print(f"{device.name}: {device.hostname}")
185
+
186
+ # Work with credentials
187
+ api.unlock("vault-password")
188
+ cred = api.credential("lab-admin")
189
+ print(f"Username: {cred.username}")
190
+ ```
191
+
192
+ ### Roadmap
193
+
194
+ The scripting API is the foundation for:
195
+
196
+ - **Command execution** — `api.connect()` and `api.send()` for programmatic device interaction
197
+ - **Batch operations** — Fan out commands across device groups
198
+ - **MCP tool integration** — Expose nterm capabilities to AI agents
199
+
200
+ See [scripting/README.md](nterm/scripting/README.md) for full API documentation.
201
+
202
+ ---
203
+
110
204
  ## Quick Start
111
205
 
112
206
  ### As a Widget
@@ -253,6 +347,7 @@ nterm/
253
347
  ├── session/
254
348
  │ ├── ssh.py # SSHSession (Paramiko) with legacy fallback
255
349
  │ ├── interactive_ssh.py # Native SSH + PTY
350
+ │ ├── local_terminal.py # Local shell/IPython sessions
256
351
  │ └── pty_transport.py # Cross-platform PTY
257
352
  ├── terminal/
258
353
  │ ├── widget.py # TerminalWidget (PyQt6 + xterm.js)
@@ -264,7 +359,10 @@ nterm/
264
359
  │ ├── store.py # Encrypted credential storage
265
360
  │ ├── resolver.py # Pattern-based resolution
266
361
  │ └── manager_ui.py # PyQt6 credential manager
267
- └── manager/ # Session tree, connection dialogs
362
+ ├── manager/ # Session tree, connection dialogs
363
+ └── scripting/ # API, CLI, automation support
364
+ ├── api.py # NTermAPI class
365
+ └── cli.py # nterm-cli entry point
268
366
  ```
269
367
 
270
368
  ---
@@ -22,6 +22,7 @@ from nterm.manager import (
22
22
  )
23
23
  from nterm.terminal.widget import TerminalWidget
24
24
  from nterm.session.ssh import SSHSession
25
+ from nterm.session.local_terminal import LocalTerminal
25
26
  from nterm.connection.profile import ConnectionProfile, AuthConfig
26
27
  from nterm.vault import CredentialManagerWidget
27
28
  from nterm.vault.resolver import CredentialResolver
@@ -225,6 +226,35 @@ class TerminalTab(QWidget):
225
226
  return True
226
227
 
227
228
 
229
+ class LocalTerminalTab(QWidget):
230
+ """A terminal tab for local processes (shell, IPython, etc.)."""
231
+
232
+ def __init__(self, name: str, session: LocalTerminal, parent=None):
233
+ super().__init__(parent)
234
+ self.name = name
235
+ self.local_session = session
236
+
237
+ layout = QVBoxLayout(self)
238
+ layout.setContentsMargins(0, 0, 0, 0)
239
+
240
+ self.terminal = TerminalWidget()
241
+ layout.addWidget(self.terminal)
242
+
243
+ self.terminal.attach_session(self.local_session)
244
+
245
+ def connect(self):
246
+ """Start the local process."""
247
+ self.local_session.connect()
248
+
249
+ def disconnect(self):
250
+ """Terminate the local process."""
251
+ self.local_session.disconnect()
252
+
253
+ def is_connected(self) -> bool:
254
+ """Check if the process is still running."""
255
+ return self.local_session.is_connected
256
+
257
+
228
258
  class MainWindow(QMainWindow):
229
259
  """
230
260
  Main application window with session tree and tabbed terminals.
@@ -274,7 +304,7 @@ class MainWindow(QMainWindow):
274
304
  # Apply multiline threshold to all open terminals
275
305
  for i in range(self.tab_widget.count()):
276
306
  tab = self.tab_widget.widget(i)
277
- if isinstance(tab, TerminalTab):
307
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)):
278
308
  tab.terminal.set_multiline_threshold(settings.multiline_paste_threshold)
279
309
 
280
310
 
@@ -401,6 +431,32 @@ class MainWindow(QMainWindow):
401
431
  action.triggered.connect(lambda checked, n=theme_name: self._apply_theme_by_name(n))
402
432
  theme_menu.addAction(action)
403
433
 
434
+ # Dev menu
435
+ dev_menu = menubar.addMenu("&Dev")
436
+
437
+ # IPython submenu
438
+ ipython_menu = dev_menu.addMenu("&IPython")
439
+
440
+ ipython_tab_action = QAction("Open in &Tab", self)
441
+ ipython_tab_action.setShortcut(QKeySequence("Ctrl+Shift+I"))
442
+ ipython_tab_action.triggered.connect(lambda: self._open_local("IPython", LocalTerminal.ipython(), "tab"))
443
+ ipython_menu.addAction(ipython_tab_action)
444
+
445
+ ipython_window_action = QAction("Open in &Window", self)
446
+ ipython_window_action.triggered.connect(lambda: self._open_local("IPython", LocalTerminal.ipython(), "window"))
447
+ ipython_menu.addAction(ipython_window_action)
448
+
449
+ # Shell submenu
450
+ shell_menu = dev_menu.addMenu("&Shell")
451
+
452
+ shell_tab_action = QAction("Open in &Tab", self)
453
+ shell_tab_action.triggered.connect(lambda: self._open_local("Shell", LocalTerminal(), "tab"))
454
+ shell_menu.addAction(shell_tab_action)
455
+
456
+ shell_window_action = QAction("Open in &Window", self)
457
+ shell_window_action.triggered.connect(lambda: self._open_local("Shell", LocalTerminal(), "window"))
458
+ shell_menu.addAction(shell_window_action)
459
+
404
460
  def _on_import_sessions(self):
405
461
  """Show import dialog."""
406
462
  dialog = ImportDialog(self.session_store, self)
@@ -445,7 +501,7 @@ class MainWindow(QMainWindow):
445
501
  # Update all open terminal tabs
446
502
  for i in range(self.tab_widget.count()):
447
503
  tab = self.tab_widget.widget(i)
448
- if isinstance(tab, TerminalTab):
504
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)):
449
505
  tab.terminal.set_theme(theme)
450
506
 
451
507
  def _apply_qt_theme(self, theme: Theme):
@@ -560,6 +616,23 @@ class MainWindow(QMainWindow):
560
616
  self._child_windows.append(window)
561
617
  window.destroyed.connect(lambda: self._child_windows.remove(window))
562
618
 
619
+ def _open_local(self, name: str, session: LocalTerminal, mode: str):
620
+ """Open a local terminal session (IPython, shell, etc.)."""
621
+ if mode == "tab":
622
+ tab = LocalTerminalTab(name, session)
623
+ tab.terminal.set_theme(self.current_theme)
624
+ idx = self.tab_widget.addTab(tab, name)
625
+ self.tab_widget.setCurrentIndex(idx)
626
+ self.tab_widget.setTabToolTip(idx, f"{name} (local)")
627
+ tab.connect()
628
+ else:
629
+ window = LocalTerminalWindow(name, session, self.current_theme)
630
+ window.show()
631
+ if not hasattr(self, '_child_windows'):
632
+ self._child_windows = []
633
+ self._child_windows.append(window)
634
+ window.destroyed.connect(lambda: self._child_windows.remove(window))
635
+
563
636
  # -------------------------------------------------------------------------
564
637
  # Tab Management (NEW)
565
638
  # -------------------------------------------------------------------------
@@ -569,7 +642,7 @@ class MainWindow(QMainWindow):
569
642
  count = 0
570
643
  for i in range(self.tab_widget.count()):
571
644
  tab = self.tab_widget.widget(i)
572
- if isinstance(tab, TerminalTab) and tab.is_connected():
645
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)) and tab.is_connected():
573
646
  count += 1
574
647
  return count
575
648
 
@@ -619,13 +692,29 @@ class MainWindow(QMainWindow):
619
692
 
620
693
  tab.disconnect()
621
694
 
695
+ elif isinstance(tab, LocalTerminalTab):
696
+ # Check if process is still running
697
+ if tab.is_connected():
698
+ reply = QMessageBox.question(
699
+ self,
700
+ "Close Tab",
701
+ f"'{tab.name}' is still running.\n\n"
702
+ "Terminate and close this tab?",
703
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
704
+ QMessageBox.StandardButton.No
705
+ )
706
+ if reply != QMessageBox.StandardButton.Yes:
707
+ return
708
+
709
+ tab.disconnect()
710
+
622
711
  self.tab_widget.removeTab(index)
623
712
 
624
713
  def _close_other_tabs(self, keep_index: int):
625
714
  """Close all tabs except the specified one."""
626
715
  active_count = self._get_active_session_count()
627
716
  tab_to_keep = self.tab_widget.widget(keep_index)
628
- keep_is_active = isinstance(tab_to_keep, TerminalTab) and tab_to_keep.is_connected()
717
+ keep_is_active = isinstance(tab_to_keep, (TerminalTab, LocalTerminalTab)) and tab_to_keep.is_connected()
629
718
  other_active = active_count - (1 if keep_is_active else 0)
630
719
 
631
720
  if other_active > 0:
@@ -644,7 +733,7 @@ class MainWindow(QMainWindow):
644
733
  for i in range(self.tab_widget.count() - 1, -1, -1):
645
734
  if i != keep_index:
646
735
  tab = self.tab_widget.widget(i)
647
- if isinstance(tab, TerminalTab):
736
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)):
648
737
  tab.disconnect()
649
738
  self.tab_widget.removeTab(i)
650
739
 
@@ -658,7 +747,7 @@ class MainWindow(QMainWindow):
658
747
  active_count = 0
659
748
  for i in range(index + 1, self.tab_widget.count()):
660
749
  tab = self.tab_widget.widget(i)
661
- if isinstance(tab, TerminalTab) and tab.is_connected():
750
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)) and tab.is_connected():
662
751
  active_count += 1
663
752
 
664
753
  if active_count > 0:
@@ -676,7 +765,7 @@ class MainWindow(QMainWindow):
676
765
  # Close from end to avoid index shifting
677
766
  for i in range(self.tab_widget.count() - 1, index, -1):
678
767
  tab = self.tab_widget.widget(i)
679
- if isinstance(tab, TerminalTab):
768
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)):
680
769
  tab.disconnect()
681
770
  self.tab_widget.removeTab(i)
682
771
 
@@ -702,7 +791,7 @@ class MainWindow(QMainWindow):
702
791
  # Close all tabs
703
792
  while self.tab_widget.count() > 0:
704
793
  tab = self.tab_widget.widget(0)
705
- if isinstance(tab, TerminalTab):
794
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)):
706
795
  tab.disconnect()
707
796
  self.tab_widget.removeTab(0)
708
797
 
@@ -741,7 +830,7 @@ class MainWindow(QMainWindow):
741
830
  # Disconnect all tabs
742
831
  for i in range(self.tab_widget.count()):
743
832
  tab = self.tab_widget.widget(i)
744
- if isinstance(tab, TerminalTab):
833
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)):
745
834
  tab.disconnect()
746
835
 
747
836
  self.session_store.close()
@@ -790,6 +879,40 @@ class TerminalWindow(QMainWindow):
790
879
  event.accept()
791
880
 
792
881
 
882
+ class LocalTerminalWindow(QMainWindow):
883
+ """Standalone window for local terminal sessions."""
884
+
885
+ def __init__(self, name: str, session: LocalTerminal, theme: Theme):
886
+ super().__init__()
887
+ self.setWindowTitle(f"{name} - Local")
888
+ self.resize(1000, 700)
889
+
890
+ self.setStyleSheet(generate_stylesheet(theme))
891
+
892
+ self.tab = LocalTerminalTab(name, session)
893
+ self.tab.terminal.set_theme(theme)
894
+ self.setCentralWidget(self.tab)
895
+
896
+ self.tab.connect()
897
+
898
+ def closeEvent(self, event):
899
+ """Terminate on close with confirmation."""
900
+ if self.tab.is_connected():
901
+ reply = QMessageBox.question(
902
+ self,
903
+ "Close Window",
904
+ f"'{self.tab.name}' is still running.\n\nTerminate and close?",
905
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
906
+ QMessageBox.StandardButton.No
907
+ )
908
+ if reply != QMessageBox.StandardButton.Yes:
909
+ event.ignore()
910
+ return
911
+
912
+ self.tab.disconnect()
913
+ event.accept()
914
+
915
+
793
916
  def main():
794
917
  app = QApplication(sys.argv)
795
918
 
@@ -0,0 +1,43 @@
1
+ """
2
+ nterm.scripting - Scripting API for nterm
3
+
4
+ Provides programmatic access to nterm's device sessions and credential vault.
5
+ Usable from IPython, CLI, scripts, or as foundation for MCP tools.
6
+
7
+ Quick Start (IPython):
8
+ from nterm.scripting import api
9
+
10
+ api.devices() # List all saved devices
11
+ api.search("leaf") # Search devices
12
+ api.device("eng-leaf-1") # Get specific device
13
+
14
+ api.unlock("vault-password") # Unlock credential vault
15
+ api.credentials() # List credentials
16
+
17
+ api.help() # Show all commands
18
+
19
+ Quick Start (CLI):
20
+ nterm-cli devices
21
+ nterm-cli search leaf
22
+ nterm-cli credentials --unlock
23
+ """
24
+
25
+ from .api import (
26
+ NTermAPI,
27
+ DeviceInfo,
28
+ CredentialInfo,
29
+ get_api,
30
+ reset_api,
31
+ )
32
+
33
+ # Convenience: pre-instantiated API
34
+ api = get_api()
35
+
36
+ __all__ = [
37
+ "NTermAPI",
38
+ "DeviceInfo",
39
+ "CredentialInfo",
40
+ "get_api",
41
+ "reset_api",
42
+ "api",
43
+ ]