ntermqt 0.1.1__py3-none-any.whl → 0.1.4__py3-none-any.whl
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.
- nterm/__main__.py +132 -9
- nterm/examples/basic_terminal.py +415 -0
- nterm/manager/tree.py +125 -42
- nterm/scripting/__init__.py +43 -0
- nterm/scripting/api.py +447 -0
- nterm/scripting/cli.py +305 -0
- nterm/session/local_terminal.py +225 -0
- nterm/session/pty_transport.py +105 -91
- nterm/terminal/bridge.py +10 -0
- nterm/terminal/resources/terminal.html +9 -4
- nterm/terminal/resources/terminal.js +14 -1
- nterm/terminal/widget.py +73 -2
- nterm/theme/engine.py +45 -0
- nterm/theme/themes/nord_hybrid.yaml +43 -0
- nterm/vault/store.py +3 -3
- {ntermqt-0.1.1.dist-info → ntermqt-0.1.4.dist-info}/METADATA +157 -21
- {ntermqt-0.1.1.dist-info → ntermqt-0.1.4.dist-info}/RECORD +20 -14
- {ntermqt-0.1.1.dist-info → ntermqt-0.1.4.dist-info}/entry_points.txt +1 -0
- {ntermqt-0.1.1.dist-info → ntermqt-0.1.4.dist-info}/WHEEL +0 -0
- {ntermqt-0.1.1.dist-info → ntermqt-0.1.4.dist-info}/top_level.txt +0 -0
nterm/terminal/widget.py
CHANGED
|
@@ -6,11 +6,12 @@ from __future__ import annotations
|
|
|
6
6
|
import base64
|
|
7
7
|
import json
|
|
8
8
|
import logging
|
|
9
|
+
import re
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import Optional
|
|
11
|
+
from typing import Optional, BinaryIO
|
|
11
12
|
|
|
12
13
|
from PyQt6.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot
|
|
13
|
-
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QApplication
|
|
14
|
+
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QApplication, QFileDialog
|
|
14
15
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
|
15
16
|
from PyQt6.QtWebEngineCore import QWebEngineSettings
|
|
16
17
|
from PyQt6.QtWebChannel import QWebChannel
|
|
@@ -29,6 +30,9 @@ from nterm.resources import resources
|
|
|
29
30
|
# Default threshold for multiline paste warning
|
|
30
31
|
MULTILINE_PASTE_THRESHOLD = 1
|
|
31
32
|
|
|
33
|
+
# ANSI escape sequence pattern for stripping from capture logs
|
|
34
|
+
ANSI_ESCAPE = re.compile(rb'\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b\[[\?0-9;]*[hl]')
|
|
35
|
+
|
|
32
36
|
|
|
33
37
|
class TerminalWidget(QWidget):
|
|
34
38
|
"""
|
|
@@ -55,6 +59,10 @@ class TerminalWidget(QWidget):
|
|
|
55
59
|
self._multiline_threshold = multiline_threshold
|
|
56
60
|
self._pending_paste: Optional[bytes] = None # held during confirmation
|
|
57
61
|
|
|
62
|
+
# Session capture
|
|
63
|
+
self._capture_file: Optional[BinaryIO] = None
|
|
64
|
+
self._capture_path: Optional[Path] = None
|
|
65
|
+
|
|
58
66
|
self._setup_ui()
|
|
59
67
|
self._setup_bridge()
|
|
60
68
|
|
|
@@ -99,6 +107,9 @@ class TerminalWidget(QWidget):
|
|
|
99
107
|
self._bridge.paste_confirmed.connect(self._on_paste_confirmed)
|
|
100
108
|
self._bridge.paste_cancelled.connect(self._on_paste_cancelled)
|
|
101
109
|
|
|
110
|
+
# Capture signals
|
|
111
|
+
self._bridge.capture_toggled.connect(self._on_capture_toggle)
|
|
112
|
+
|
|
102
113
|
# Load terminal HTML
|
|
103
114
|
try:
|
|
104
115
|
html_path = resources.get_path("terminal", "resources", "terminal.html")
|
|
@@ -130,6 +141,9 @@ class TerminalWidget(QWidget):
|
|
|
130
141
|
self._awaiting_reconnect_confirm = False
|
|
131
142
|
logger.debug("Detached session")
|
|
132
143
|
|
|
144
|
+
# Stop any active capture
|
|
145
|
+
self.stop_capture()
|
|
146
|
+
|
|
133
147
|
def set_theme(self, theme: Theme) -> None:
|
|
134
148
|
"""
|
|
135
149
|
Apply theme to terminal.
|
|
@@ -157,6 +171,11 @@ class TerminalWidget(QWidget):
|
|
|
157
171
|
Args:
|
|
158
172
|
data: Bytes to display
|
|
159
173
|
"""
|
|
174
|
+
# Session capture - strip ANSI escapes for clean text
|
|
175
|
+
if self._capture_file:
|
|
176
|
+
clean = ANSI_ESCAPE.sub(b'', data)
|
|
177
|
+
self._capture_file.write(clean)
|
|
178
|
+
|
|
160
179
|
if self._ready:
|
|
161
180
|
data_b64 = base64.b64encode(data).decode('ascii')
|
|
162
181
|
self._bridge.write_data.emit(data_b64)
|
|
@@ -190,6 +209,58 @@ class TerminalWidget(QWidget):
|
|
|
190
209
|
if self._ready:
|
|
191
210
|
self._bridge.hide_overlay.emit()
|
|
192
211
|
|
|
212
|
+
# -------------------------------------------------------------------------
|
|
213
|
+
# Session capture
|
|
214
|
+
# -------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def is_capturing(self) -> bool:
|
|
218
|
+
"""Check if session capture is active."""
|
|
219
|
+
return self._capture_file is not None
|
|
220
|
+
|
|
221
|
+
def start_capture(self, path: Path) -> None:
|
|
222
|
+
"""
|
|
223
|
+
Start capturing session output to file.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
path: File path to write captured output
|
|
227
|
+
"""
|
|
228
|
+
self.stop_capture() # Close any existing capture
|
|
229
|
+
self._capture_path = path
|
|
230
|
+
self._capture_file = open(path, 'wb')
|
|
231
|
+
self._bridge.set_capture_state.emit(True, path.name)
|
|
232
|
+
logger.info(f"Started capture: {path}")
|
|
233
|
+
|
|
234
|
+
def stop_capture(self) -> None:
|
|
235
|
+
"""Stop capturing session output."""
|
|
236
|
+
if self._capture_file:
|
|
237
|
+
self._capture_file.close()
|
|
238
|
+
logger.info(f"Stopped capture: {self._capture_path}")
|
|
239
|
+
self._capture_file = None
|
|
240
|
+
self._capture_path = None
|
|
241
|
+
self._bridge.set_capture_state.emit(False, "")
|
|
242
|
+
|
|
243
|
+
@pyqtSlot()
|
|
244
|
+
def _on_capture_toggle(self):
|
|
245
|
+
"""Handle capture menu item click."""
|
|
246
|
+
if self._capture_file:
|
|
247
|
+
self.stop_capture()
|
|
248
|
+
else:
|
|
249
|
+
# Show file save dialog
|
|
250
|
+
default_name = "session.log"
|
|
251
|
+
if self._session:
|
|
252
|
+
# Use hostname if available for default filename
|
|
253
|
+
default_name = f"session_{self._session.hostname}.log" if hasattr(self._session, 'hostname') else "session.log"
|
|
254
|
+
|
|
255
|
+
path, _ = QFileDialog.getSaveFileName(
|
|
256
|
+
self,
|
|
257
|
+
"Save Session Capture",
|
|
258
|
+
str(Path.home() / default_name),
|
|
259
|
+
"Log Files (*.log *.txt);;All Files (*)"
|
|
260
|
+
)
|
|
261
|
+
if path:
|
|
262
|
+
self.start_capture(Path(path))
|
|
263
|
+
|
|
193
264
|
# -------------------------------------------------------------------------
|
|
194
265
|
# Clipboard operations
|
|
195
266
|
# -------------------------------------------------------------------------
|
nterm/theme/engine.py
CHANGED
|
@@ -564,6 +564,50 @@ class Theme:
|
|
|
564
564
|
overlay_text_color="#3c3836",
|
|
565
565
|
)
|
|
566
566
|
|
|
567
|
+
@classmethod
|
|
568
|
+
def nord_hybrid(cls) -> Theme:
|
|
569
|
+
"""
|
|
570
|
+
Nord Hybrid theme.
|
|
571
|
+
|
|
572
|
+
Polar Night UI + Snow Storm terminal.
|
|
573
|
+
Dark chrome, soft light terminal (not harsh white).
|
|
574
|
+
"""
|
|
575
|
+
return cls(
|
|
576
|
+
name="nord_hybrid",
|
|
577
|
+
terminal_colors={
|
|
578
|
+
"background": "#eceff4",
|
|
579
|
+
"foreground": "#2e3440",
|
|
580
|
+
"cursor": "#2e3440",
|
|
581
|
+
"cursorAccent": "#eceff4",
|
|
582
|
+
"selectionBackground": "#d8dee9",
|
|
583
|
+
"selectionForeground": "#2e3440",
|
|
584
|
+
"black": "#2e3440",
|
|
585
|
+
"red": "#bf616a",
|
|
586
|
+
"green": "#a3be8c",
|
|
587
|
+
"yellow": "#d08770",
|
|
588
|
+
"blue": "#5e81ac",
|
|
589
|
+
"magenta": "#b48ead",
|
|
590
|
+
"cyan": "#88c0d0",
|
|
591
|
+
"white": "#d8dee9",
|
|
592
|
+
"brightBlack": "#4c566a",
|
|
593
|
+
"brightRed": "#bf616a",
|
|
594
|
+
"brightGreen": "#a3be8c",
|
|
595
|
+
"brightYellow": "#ebcb8b",
|
|
596
|
+
"brightBlue": "#81a1c1",
|
|
597
|
+
"brightMagenta": "#b48ead",
|
|
598
|
+
"brightCyan": "#8fbcbb",
|
|
599
|
+
"brightWhite": "#eceff4",
|
|
600
|
+
},
|
|
601
|
+
font_family="JetBrains Mono, Cascadia Code, Consolas, Menlo, monospace",
|
|
602
|
+
font_size=14,
|
|
603
|
+
background_color="#2e3440",
|
|
604
|
+
foreground_color="#d8dee9",
|
|
605
|
+
border_color="#3b4252",
|
|
606
|
+
accent_color="#88c0d0",
|
|
607
|
+
overlay_background="rgba(46, 52, 64, 0.95)",
|
|
608
|
+
overlay_text_color="#eceff4",
|
|
609
|
+
)
|
|
610
|
+
|
|
567
611
|
@classmethod
|
|
568
612
|
def gruvbox_hybrid(cls) -> Theme:
|
|
569
613
|
"""
|
|
@@ -630,6 +674,7 @@ class ThemeEngine:
|
|
|
630
674
|
self._themes["default"] = Theme.default()
|
|
631
675
|
self._themes["dracula"] = Theme.dracula()
|
|
632
676
|
self._themes["nord"] = Theme.nord()
|
|
677
|
+
self._themes["nord_hybrid"] = Theme.nord_hybrid()
|
|
633
678
|
self._themes["solarized_dark"] = Theme.solarized_dark()
|
|
634
679
|
self._themes["gruvbox_dark"] = Theme.gruvbox_dark()
|
|
635
680
|
self._themes["gruvbox_light"] = Theme.gruvbox_light()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: nord_hybrid
|
|
2
|
+
|
|
3
|
+
# Nord Hybrid theme
|
|
4
|
+
# Polar Night UI + softer Snow Storm terminal
|
|
5
|
+
# Adjusted palette for better contrast on light background
|
|
6
|
+
|
|
7
|
+
terminal_colors:
|
|
8
|
+
# Softer terminal - nord4 instead of nord6
|
|
9
|
+
background: "#d8dee9"
|
|
10
|
+
foreground: "#2e3440"
|
|
11
|
+
cursor: "#2e3440"
|
|
12
|
+
cursorAccent: "#d8dee9"
|
|
13
|
+
selectionBackground: "#4c566a"
|
|
14
|
+
selectionForeground: "#eceff4"
|
|
15
|
+
# Darkened palette for light background contrast
|
|
16
|
+
black: "#2e3440"
|
|
17
|
+
red: "#a54242"
|
|
18
|
+
green: "#4e6a3d"
|
|
19
|
+
yellow: "#a07040"
|
|
20
|
+
blue: "#3b6186"
|
|
21
|
+
magenta: "#8a4b7c"
|
|
22
|
+
cyan: "#2b7694"
|
|
23
|
+
white: "#4c566a"
|
|
24
|
+
brightBlack: "#3b4252"
|
|
25
|
+
brightRed: "#bf616a"
|
|
26
|
+
brightGreen: "#5c8045"
|
|
27
|
+
brightYellow: "#d08770"
|
|
28
|
+
brightBlue: "#5e81ac"
|
|
29
|
+
brightMagenta: "#b48ead"
|
|
30
|
+
brightCyan: "#4e9a9a"
|
|
31
|
+
brightWhite: "#2e3440"
|
|
32
|
+
|
|
33
|
+
font_family: "JetBrains Mono, Cascadia Code, Consolas, monospace"
|
|
34
|
+
font_size: 14
|
|
35
|
+
|
|
36
|
+
# Polar Night UI chrome (unchanged)
|
|
37
|
+
background_color: "#2e3440"
|
|
38
|
+
foreground_color: "#d8dee9"
|
|
39
|
+
border_color: "#3b4252"
|
|
40
|
+
accent_color: "#88c0d0"
|
|
41
|
+
|
|
42
|
+
overlay_background: "rgba(46, 52, 64, 0.95)"
|
|
43
|
+
overlay_text_color: "#eceff4"
|
nterm/vault/store.py
CHANGED
|
@@ -295,7 +295,7 @@ class CredentialStore:
|
|
|
295
295
|
|
|
296
296
|
# Encrypt sensitive fields
|
|
297
297
|
password_enc = self._encrypt(password) if password else None
|
|
298
|
-
ssh_key_enc = self._encrypt(ssh_key) if ssh_key else None
|
|
298
|
+
ssh_key_enc = self._encrypt(ssh_key.strip()) if ssh_key else None
|
|
299
299
|
ssh_key_pass_enc = self._encrypt(ssh_key_passphrase) if ssh_key_passphrase else None
|
|
300
300
|
|
|
301
301
|
# Serialize lists
|
|
@@ -523,8 +523,8 @@ class CredentialStore:
|
|
|
523
523
|
updates['password_enc'] = self._encrypt(kwargs['password']) if kwargs['password'] else None
|
|
524
524
|
|
|
525
525
|
if 'ssh_key' in kwargs:
|
|
526
|
-
updates['ssh_key_enc'] = self._encrypt(kwargs['ssh_key']) if kwargs['ssh_key'] else None
|
|
527
|
-
|
|
526
|
+
updates['ssh_key_enc'] = self._encrypt(kwargs['ssh_key'].strip()) if kwargs['ssh_key'] else None
|
|
527
|
+
|
|
528
528
|
if 'ssh_key_passphrase' in kwargs:
|
|
529
529
|
updates['ssh_key_passphrase_enc'] = self._encrypt(kwargs['ssh_key_passphrase']) if kwargs['ssh_key_passphrase'] else None
|
|
530
530
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ntermqt
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
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
|
|
|
@@ -50,9 +59,11 @@ Built for managing hundreds of devices through bastion hosts with hardware secur
|
|
|
50
59
|
|
|
51
60
|
**Terminal**
|
|
52
61
|
- xterm.js rendering via QWebEngineView — full VT100/ANSI support
|
|
53
|
-
-
|
|
62
|
+
- 12 built-in themes: Catppuccin, Dracula, Nord, Solarized, Gruvbox, Enterprise variants
|
|
63
|
+
- Hybrid themes: dark UI chrome with light terminal for readability
|
|
54
64
|
- Custom YAML themes with independent terminal and UI colors
|
|
55
65
|
- Tab or window per session — pop sessions to separate windows
|
|
66
|
+
- Session capture to file (clean text, ANSI stripped)
|
|
56
67
|
- Unicode, emoji, box-drawing characters
|
|
57
68
|
|
|
58
69
|
**Authentication**
|
|
@@ -74,6 +85,12 @@ Built for managing hundreds of devices through bastion hosts with hardware secur
|
|
|
74
85
|
- Cross-platform keychain: macOS Keychain, Windows Credential Locker, Linux Secret Service
|
|
75
86
|
- Full PyQt6 management UI
|
|
76
87
|
|
|
88
|
+
**Scripting API** *(Experimental)*
|
|
89
|
+
- Query device inventory and credentials programmatically
|
|
90
|
+
- Built-in IPython console with API pre-loaded
|
|
91
|
+
- CLI for shell scripts and automation
|
|
92
|
+
- Foundation for MCP tools and agentic workflows
|
|
93
|
+
|
|
77
94
|
---
|
|
78
95
|
|
|
79
96
|
## Screenshots
|
|
@@ -88,15 +105,48 @@ Built for managing hundreds of devices through bastion hosts with hardware secur
|
|
|
88
105
|
|
|
89
106
|
---
|
|
90
107
|
|
|
108
|
+
## Dev Console
|
|
109
|
+
|
|
110
|
+
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.
|
|
111
|
+
|
|
112
|
+

|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# Available immediately when IPython opens
|
|
118
|
+
api.devices() # List all saved devices
|
|
119
|
+
api.search("leaf") # Search by name/hostname
|
|
120
|
+
api.credentials() # List credentials (after api.unlock())
|
|
121
|
+
api.help() # Show all commands
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Use cases:**
|
|
125
|
+
- Debug connection issues with live access to session objects
|
|
126
|
+
- Prototype automation scripts against your real device inventory
|
|
127
|
+
- Test credential resolution patterns
|
|
128
|
+
- Build and test MCP tools interactively
|
|
129
|
+
|
|
130
|
+
Requires the `scripting` extra: `pip install ntermqt[scripting]`
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
91
134
|
## Installation
|
|
92
135
|
|
|
136
|
+
### Be aware due to a naming conflict, the pypi package is actually "ntermqt"
|
|
137
|
+
|
|
93
138
|
### From PyPI
|
|
94
139
|
|
|
140
|
+
https://pypi.org/project/ntermqt/
|
|
141
|
+
|
|
95
142
|
```bash
|
|
96
|
-
pip install
|
|
143
|
+
pip install ntermqt
|
|
144
|
+
|
|
145
|
+
# With optional scripting support (IPython)
|
|
146
|
+
pip install ntermqt[scripting]
|
|
97
147
|
|
|
98
|
-
#
|
|
99
|
-
pip install
|
|
148
|
+
# With all optional features
|
|
149
|
+
pip install ntermqt[all]
|
|
100
150
|
|
|
101
151
|
# Run
|
|
102
152
|
nterm
|
|
@@ -114,10 +164,7 @@ source .venv/bin/activate # Linux/macOS
|
|
|
114
164
|
# .venv\Scripts\activate # Windows
|
|
115
165
|
|
|
116
166
|
# Install in development mode
|
|
117
|
-
pip install -e .
|
|
118
|
-
|
|
119
|
-
# Optional: system keychain support
|
|
120
|
-
pip install keyring
|
|
167
|
+
pip install -e ".[all]"
|
|
121
168
|
|
|
122
169
|
# Run
|
|
123
170
|
nterm
|
|
@@ -137,12 +184,70 @@ python -m nterm
|
|
|
137
184
|
|
|
138
185
|
| Platform | PTY | Keychain |
|
|
139
186
|
|----------|-----|----------|
|
|
140
|
-
| Linux | ✅
|
|
141
|
-
| macOS | ✅
|
|
187
|
+
| Linux | ✅ pexpect | Secret Service |
|
|
188
|
+
| macOS | ✅ pexpect | macOS Keychain |
|
|
142
189
|
| Windows 10+ | ✅ pywinpty | Credential Locker |
|
|
143
190
|
|
|
144
191
|
---
|
|
145
192
|
|
|
193
|
+
## Scripting API *(Experimental)*
|
|
194
|
+
|
|
195
|
+
nterm includes a scripting API for programmatic access to your device inventory and credential vault. Use it from IPython, CLI, or Python scripts.
|
|
196
|
+
|
|
197
|
+
### IPython Console
|
|
198
|
+
|
|
199
|
+
Open **Dev → IPython → Open in Tab** to get an interactive console with the API pre-loaded:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
api.devices() # List all saved devices
|
|
203
|
+
api.search("leaf") # Search by name/hostname
|
|
204
|
+
api.devices("eng-*") # Glob pattern filter
|
|
205
|
+
|
|
206
|
+
api.unlock("vault-password") # Unlock credential vault
|
|
207
|
+
api.credentials() # List credentials (metadata only)
|
|
208
|
+
|
|
209
|
+
api.help() # Show all commands
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### CLI
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
nterm-cli devices # List all devices
|
|
216
|
+
nterm-cli search leaf # Search devices
|
|
217
|
+
nterm-cli device eng-leaf-1 # Device details
|
|
218
|
+
nterm-cli credentials --unlock # List credentials
|
|
219
|
+
nterm-cli --json devices # JSON output for scripting
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Python Scripts
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from nterm.scripting import NTermAPI
|
|
226
|
+
|
|
227
|
+
api = NTermAPI()
|
|
228
|
+
|
|
229
|
+
# Query devices
|
|
230
|
+
for device in api.devices("*spine*"):
|
|
231
|
+
print(f"{device.name}: {device.hostname}")
|
|
232
|
+
|
|
233
|
+
# Work with credentials
|
|
234
|
+
api.unlock("vault-password")
|
|
235
|
+
cred = api.credential("lab-admin")
|
|
236
|
+
print(f"Username: {cred.username}")
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Roadmap
|
|
240
|
+
|
|
241
|
+
The scripting API is the foundation for:
|
|
242
|
+
|
|
243
|
+
- **Command execution** — `api.connect()` and `api.send()` for programmatic device interaction
|
|
244
|
+
- **Batch operations** — Fan out commands across device groups
|
|
245
|
+
- **MCP tool integration** — Expose nterm capabilities to AI agents
|
|
246
|
+
|
|
247
|
+
See [scripting/README.md](nterm/scripting/README.md) for full API documentation.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
146
251
|
## Quick Start
|
|
147
252
|
|
|
148
253
|
### As a Widget
|
|
@@ -203,19 +308,33 @@ session.connect()
|
|
|
203
308
|
|
|
204
309
|
## Themes
|
|
205
310
|
|
|
206
|
-
|
|
311
|
+
nterm includes 12 built-in themes covering dark, light, and hybrid styles.
|
|
312
|
+
|
|
313
|
+
### Built-in Themes
|
|
207
314
|
|
|
208
315
|
```python
|
|
209
|
-
|
|
210
|
-
Theme.
|
|
211
|
-
Theme.
|
|
212
|
-
Theme.
|
|
213
|
-
Theme.
|
|
214
|
-
Theme.
|
|
215
|
-
Theme.
|
|
316
|
+
# Dark themes
|
|
317
|
+
Theme.default() # Catppuccin Mocha
|
|
318
|
+
Theme.dracula() # Dracula
|
|
319
|
+
Theme.nord() # Nord
|
|
320
|
+
Theme.solarized_dark() # Solarized Dark
|
|
321
|
+
Theme.gruvbox_dark() # Gruvbox Dark
|
|
322
|
+
Theme.enterprise_dark() # Microsoft-inspired dark
|
|
323
|
+
|
|
324
|
+
# Light themes
|
|
325
|
+
Theme.gruvbox_light() # Gruvbox Light
|
|
326
|
+
Theme.enterprise_light() # Microsoft-inspired light
|
|
327
|
+
Theme.clean() # Warm paper tones
|
|
328
|
+
|
|
329
|
+
# Hybrid themes (dark UI + light terminal)
|
|
330
|
+
Theme.gruvbox_hybrid() # Gruvbox dark chrome, light terminal
|
|
331
|
+
Theme.nord_hybrid() # Nord polar night chrome, snow storm terminal
|
|
332
|
+
Theme.enterprise_hybrid() # VS Code-style dark/light split
|
|
216
333
|
```
|
|
217
334
|
|
|
218
|
-
|
|
335
|
+
**Hybrid themes** combine a dark application chrome (menus, tabs, sidebars) with a light terminal for maximum readability — ideal for long sessions reviewing configs or logs.
|
|
336
|
+
|
|
337
|
+
### Custom YAML Themes
|
|
219
338
|
|
|
220
339
|
```yaml
|
|
221
340
|
# ~/.nterm/themes/my-theme.yaml
|
|
@@ -244,6 +363,19 @@ accent_color: "#7aa2f7"
|
|
|
244
363
|
|
|
245
364
|
---
|
|
246
365
|
|
|
366
|
+
## Session Capture
|
|
367
|
+
|
|
368
|
+
Capture session output to a file for documentation, auditing, or extracting config snippets.
|
|
369
|
+
|
|
370
|
+
**Right-click in terminal → Start Capture...** to begin recording. Output is saved as clean text with ANSI escape sequences stripped — ready for grep, diff, or pasting into tickets.
|
|
371
|
+
|
|
372
|
+
- Per-session capture (each tab independent)
|
|
373
|
+
- File dialog for save location
|
|
374
|
+
- Menu shows active capture filename
|
|
375
|
+
- Auto-stops when session closes
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
247
379
|
## Jump Hosts
|
|
248
380
|
|
|
249
381
|
```python
|
|
@@ -289,6 +421,7 @@ nterm/
|
|
|
289
421
|
├── session/
|
|
290
422
|
│ ├── ssh.py # SSHSession (Paramiko) with legacy fallback
|
|
291
423
|
│ ├── interactive_ssh.py # Native SSH + PTY
|
|
424
|
+
│ ├── local_terminal.py # Local shell/IPython sessions
|
|
292
425
|
│ └── pty_transport.py # Cross-platform PTY
|
|
293
426
|
├── terminal/
|
|
294
427
|
│ ├── widget.py # TerminalWidget (PyQt6 + xterm.js)
|
|
@@ -300,7 +433,10 @@ nterm/
|
|
|
300
433
|
│ ├── store.py # Encrypted credential storage
|
|
301
434
|
│ ├── resolver.py # Pattern-based resolution
|
|
302
435
|
│ └── manager_ui.py # PyQt6 credential manager
|
|
303
|
-
|
|
436
|
+
├── manager/ # Session tree, connection dialogs
|
|
437
|
+
└── scripting/ # API, CLI, automation support
|
|
438
|
+
├── api.py # NTermAPI class
|
|
439
|
+
└── cli.py # nterm-cli entry point
|
|
304
440
|
```
|
|
305
441
|
|
|
306
442
|
---
|
|
@@ -1,36 +1,41 @@
|
|
|
1
1
|
nterm/__init__.py,sha256=Liu1bya6xi3NnwO9KLqqlYyLD1eS-2HeEdEpJRc2480,1346
|
|
2
|
-
nterm/__main__.py,sha256=
|
|
2
|
+
nterm/__main__.py,sha256=X4R51lyR9j3IteX-Bzs1Q-UcbCJFfvcVitaPWDFBdck,33765
|
|
3
3
|
nterm/config.py,sha256=19T28opP-rdLRuxXCGP-qrklAlh4HNbXNTyAwveBhu8,4690
|
|
4
4
|
nterm/resources.py,sha256=SYC8JeF7vVfER93KKRd-tt5b25t0tHTkd7fSJqVDXnI,1447
|
|
5
5
|
nterm/askpass/__init__.py,sha256=UpJBk0EOm0nkRwMVv7YdIB4v75ZJpSYmNsU_GlgzbUg,495
|
|
6
6
|
nterm/askpass/server.py,sha256=5tvjYryyfu-n8Cw2KbucwaZfWiqYnFk-iBAVBI8FMfw,12873
|
|
7
7
|
nterm/connection/__init__.py,sha256=2qQ9LGxUxmwem8deOD2WZVkeD6rIVlTlx5Zh2cUEmxY,261
|
|
8
8
|
nterm/connection/profile.py,sha256=4RMgnRNKCc-dFGEIpmQc_bob5MtzxO04_PljP-qUGLs,9450
|
|
9
|
+
nterm/examples/basic_terminal.py,sha256=vbDI1xl-Radv6GYZ0yC6QUafQp_tSX2pWIf7tk58W8E,15256
|
|
9
10
|
nterm/manager/__init__.py,sha256=_QIeTap5CTL3jdTS1Q16fAt-PrqcNPUVr9gtJ22f0ng,774
|
|
10
11
|
nterm/manager/connect_dialog.py,sha256=yd8g_gYttT_UdflRxSfyss8OQTfrvKLUOMg4Kj8FPNo,11711
|
|
11
12
|
nterm/manager/editor.py,sha256=Fn2YWHJ1EwPYrhKhsi4GTBYwRfCYsHsqgKkLY-LQ8JI,8469
|
|
12
13
|
nterm/manager/io.py,sha256=R5ksWgpEz0VdVCokcgTN5G3PFgp5QYhjjt40OypSWkY,21687
|
|
13
14
|
nterm/manager/models.py,sha256=cvC2HzCRadNG1EYsnZN4C9YS6uolHGcUGGZtt-wzGF4,12237
|
|
14
15
|
nterm/manager/settings.py,sha256=r6MTw_9r1Wl2UX_ALpXIuPbDvJ0D91Y8wRKq6Bfr_3g,9210
|
|
15
|
-
nterm/manager/tree.py,sha256=
|
|
16
|
+
nterm/manager/tree.py,sha256=I78wSjkSuyM6903II-XNyPug9saMSODUNBCHCDrq4ls,22397
|
|
17
|
+
nterm/scripting/__init__.py,sha256=4WvwvJfJNMwXW6jas8wFreIzKBgjvAhMQnR2cnA_mEE,967
|
|
18
|
+
nterm/scripting/api.py,sha256=O-EyV0ksj7LATMSSPrDJShE3x4JPuEBs0SsPZdc2yUo,13931
|
|
19
|
+
nterm/scripting/cli.py,sha256=W2DK4ZnuutaArye_to7CBchg0ogClURxVbGsMdnj1y0,9187
|
|
16
20
|
nterm/session/__init__.py,sha256=FkgHF1WPz78JBOWHSC7LLynG2NqoR6aanNTRlEzsO6I,1612
|
|
17
21
|
nterm/session/askpass_ssh.py,sha256=U-frmLBIXwE2L5ZCEtai91G1dVRSWKLCtxn88t_PqGs,14083
|
|
18
22
|
nterm/session/base.py,sha256=NNFt2uy-rTkwigrHcdnfREk_QZDxNe0CoP16C-7oIWs,2475
|
|
19
23
|
nterm/session/interactive_ssh.py,sha256=qBhVGFdkx4hRyEzx0ZdBZZeiuwCav6BR4UtKqPnCssM,17846
|
|
20
|
-
nterm/session/
|
|
24
|
+
nterm/session/local_terminal.py,sha256=sG2lFAOpItMiT93dYCi05nrGRS-MB52XG4J-iZbcoLM,7066
|
|
25
|
+
nterm/session/pty_transport.py,sha256=QwSFqKKuJhgcLWzv1CUKf3aCGDGbbkmmGwIB1L1A2PU,17176
|
|
21
26
|
nterm/session/ssh.py,sha256=sGOxjBa9FX6GjVwkmfiKsupoLVsrPVk-LSREjlNmAdE,20942
|
|
22
27
|
nterm/terminal/__init__.py,sha256=uFnG366Z166pK-ijT1dZanVSSFVZCiMGeNKXvss_sDg,184
|
|
23
|
-
nterm/terminal/bridge.py,sha256=
|
|
24
|
-
nterm/terminal/widget.py,sha256=
|
|
25
|
-
nterm/terminal/resources/terminal.html,sha256=
|
|
26
|
-
nterm/terminal/resources/terminal.js,sha256=
|
|
28
|
+
nterm/terminal/bridge.py,sha256=mSkxZr3UGyaFI14w08dzekCkOhfUetq0GIjrBtA3qI0,3199
|
|
29
|
+
nterm/terminal/widget.py,sha256=mxUrQxFmigNR6S3vgnzHahTRGYQI2bNYTBqNg47yaR8,15716
|
|
30
|
+
nterm/terminal/resources/terminal.html,sha256=1onb3qUdDa0qzETR8XaKx0UR6BPlCm_ZpMFVgt36ZPA,7985
|
|
31
|
+
nterm/terminal/resources/terminal.js,sha256=zW9n1MRujSXv66ENgU-gzk_mc75EpWye_f88ejChSW4,13852
|
|
27
32
|
nterm/terminal/resources/xterm-addon-fit.min.js,sha256=x45XlcZIes3ySrQ2eY1KnOw4SBAbKBvGWwYfOdtxS-E,1789
|
|
28
33
|
nterm/terminal/resources/xterm-addon-unicode11.min.js,sha256=_sT7CbMSksBfUPmKZYj29IDjq7LMjiwciFs0iGNomBM,7500
|
|
29
34
|
nterm/terminal/resources/xterm-addon-web-links.min.js,sha256=_iizzOZ3_DRg6y7iu111muLnWVW8bzC9V6_EAPu0hK8,3219
|
|
30
35
|
nterm/terminal/resources/xterm.css,sha256=gy8_LGA7Q61DUf8ElwFQzHqHMBQnbbEmpgZcbdgeSHI,5383
|
|
31
36
|
nterm/terminal/resources/xterm.min.js,sha256=_B3TGyIePl-SlIbgeoC0d6iq-dzitPnD_-fdJfNwZV0,283670
|
|
32
37
|
nterm/theme/__init__.py,sha256=ZTywoJliQcFre0Gh7I30n-_7RrPmdR1NHnE4wSkSCsQ,130
|
|
33
|
-
nterm/theme/engine.py,sha256=
|
|
38
|
+
nterm/theme/engine.py,sha256=0C3K9hoFOdEVJv3xJXmPs1DPGT2mSVJNtA0dDm4w-uA,26340
|
|
34
39
|
nterm/theme/stylesheet.py,sha256=Ycy-y_kiP-SLcQFrAEdJtbSDtKm4yvBfxEe-N26qlDg,9004
|
|
35
40
|
nterm/theme/themes/clean.yaml,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
41
|
nterm/theme/themes/default.yaml,sha256=niUrI_K8fayPCZDy1gc3hueLtkWjfmm1p1R33JjYgS4,810
|
|
@@ -41,15 +46,16 @@ nterm/theme/themes/enterprise_light.yaml,sha256=Q6H5lSsStoFVJNFS63IPp0FaBhkjN9uB
|
|
|
41
46
|
nterm/theme/themes/gruvbox_dark.yaml,sha256=cAr-67R7QhW80ncHptpyyrZuUqD65xoSuLtmHeDgQM0,815
|
|
42
47
|
nterm/theme/themes/gruvbox_hybrid.yaml,sha256=Ml7Ed3sTBjcSYVJ9t961KhiG3DwMAdVdRBtzI4eWZg0,936
|
|
43
48
|
nterm/theme/themes/gruvbox_light.yaml,sha256=InqYF-TsLLIzhCHpSSHqSxnest5tu28htQ4AaFN4BFY,820
|
|
49
|
+
nterm/theme/themes/nord_hybrid.yaml,sha256=QAT056Jo2UAdQPmbc3GezjpD7Mge-GQSl4wPeSiaqSE,1065
|
|
44
50
|
nterm/vault/__init__.py,sha256=e1W3GZKOf0FXNerSp1mojl-yaidYIsygnRwTGBd6mfM,708
|
|
45
51
|
nterm/vault/credential_manager.py,sha256=TWAMfjpntPXEJ-4AauDz2PPS0q140sUebFk8AjvC-A0,5347
|
|
46
52
|
nterm/vault/keychain.py,sha256=_2-yUhc2ro-An2zvFlJHYyxozM55iJ4bSseOVKMCNGo,4229
|
|
47
53
|
nterm/vault/manager_ui.py,sha256=qle-W40j6L_pOR0AaOCeyU8myizFTRkISNrloCn0H_Y,34530
|
|
48
54
|
nterm/vault/profile.py,sha256=qM9TJf68RKdjtxo-sJehO7wS4iTi2G26BKbmlmHLA5M,6246
|
|
49
55
|
nterm/vault/resolver.py,sha256=GWB2YR9H1MH98RGQBKvitIsjWT_-wSMLuddZNz4wbns,7800
|
|
50
|
-
nterm/vault/store.py,sha256=
|
|
51
|
-
ntermqt-0.1.
|
|
52
|
-
ntermqt-0.1.
|
|
53
|
-
ntermqt-0.1.
|
|
54
|
-
ntermqt-0.1.
|
|
55
|
-
ntermqt-0.1.
|
|
56
|
+
nterm/vault/store.py,sha256=_0Lfe0WKjm3uSAtxgn9qAPlpBOLCuq9SVgzqsE_qaGQ,21199
|
|
57
|
+
ntermqt-0.1.4.dist-info/METADATA,sha256=riL6P_O6COkgWjf4eWq-7cEFiFiorlhlNCvTdr4xhvg,13573
|
|
58
|
+
ntermqt-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
59
|
+
ntermqt-0.1.4.dist-info/entry_points.txt,sha256=Gunr-_3w-aSpfqoMuGKM2PJSCRo9hZ7K1BksUtp1yd8,130
|
|
60
|
+
ntermqt-0.1.4.dist-info/top_level.txt,sha256=bZdnNLTHNRNqo9jsOQGUWF7h5st0xW_thH0n2QOxWUo,6
|
|
61
|
+
ntermqt-0.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|