ntermqt 0.1.10__py3-none-any.whl → 0.1.11__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 CHANGED
@@ -197,11 +197,28 @@ class TerminalTab(QWidget):
197
197
  self.terminal = TerminalWidget()
198
198
  layout.addWidget(self.terminal)
199
199
 
200
- # Pass the vault to SSHSession for credential resolution
201
- vault = credential_resolver if credential_resolver else None
202
- self.ssh_session = SSHSession(profile, vault=vault)
200
+ # Create initial session
201
+ self._create_and_attach_session()
202
+
203
+ # Handle reconnect by creating fresh session
204
+ self.terminal.reconnect_requested.connect(self._on_reconnect)
205
+
206
+ def _create_and_attach_session(self):
207
+ """Create fresh SSHSession and attach to terminal."""
208
+ vault = self.credential_resolver if self.credential_resolver else None
209
+ self.ssh_session = SSHSession(self.profile, vault=vault)
203
210
  self.terminal.attach_session(self.ssh_session)
204
211
 
212
+ def _on_reconnect(self):
213
+ """Handle reconnect by building fresh session."""
214
+ print(f"Reconnecting: {self.session.name}")
215
+ try:
216
+ self.ssh_session.disconnect()
217
+ except Exception:
218
+ pass # Old session may already be dead
219
+ self._create_and_attach_session()
220
+ self.ssh_session.connect()
221
+
205
222
  def connect(self):
206
223
  """Start the SSH connection."""
207
224
  self.ssh_session.connect()
@@ -227,7 +244,6 @@ class TerminalTab(QWidget):
227
244
  # Default to True if we can't determine - safer to warn
228
245
  return True
229
246
 
230
-
231
247
  class LocalTerminalTab(QWidget):
232
248
  """A terminal tab for local processes (shell, IPython, etc.)."""
233
249
 
nterm/terminal/widget.py CHANGED
@@ -374,8 +374,8 @@ class TerminalWidget(QWidget):
374
374
  self.hide_overlay()
375
375
  self.reconnect_requested.emit()
376
376
  # Actually trigger the reconnection
377
- if self._session:
378
- self._session.connect()
377
+ # if self._session:
378
+ # self._session.connect()
379
379
  else:
380
380
  # First keypress - show prompt
381
381
  self._awaiting_reconnect_confirm = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ntermqt
3
- Version: 0.1.10
3
+ Version: 0.1.11
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
@@ -1,12 +1,11 @@
1
1
  nterm/__init__.py,sha256=Liu1bya6xi3NnwO9KLqqlYyLD1eS-2HeEdEpJRc2480,1346
2
- nterm/__main__.py,sha256=sushq7oXsyBk6pLEff7qDSWck7Emk1QxxKLIfPHh0HY,35546
2
+ nterm/__main__.py,sha256=KLdsyVjic3YKYL9zg6J7DAYECRlSJVuznuTkJoPD8T4,36149
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
10
9
  nterm/manager/__init__.py,sha256=_QIeTap5CTL3jdTS1Q16fAt-PrqcNPUVr9gtJ22f0ng,774
11
10
  nterm/manager/connect_dialog.py,sha256=yd8g_gYttT_UdflRxSfyss8OQTfrvKLUOMg4Kj8FPNo,11711
12
11
  nterm/manager/editor.py,sha256=Fn2YWHJ1EwPYrhKhsi4GTBYwRfCYsHsqgKkLY-LQ8JI,8469
@@ -39,7 +38,7 @@ nterm/session/pty_transport.py,sha256=QwSFqKKuJhgcLWzv1CUKf3aCGDGbbkmmGwIB1L1A2P
39
38
  nterm/session/ssh.py,sha256=sGOxjBa9FX6GjVwkmfiKsupoLVsrPVk-LSREjlNmAdE,20942
40
39
  nterm/terminal/__init__.py,sha256=uFnG366Z166pK-ijT1dZanVSSFVZCiMGeNKXvss_sDg,184
41
40
  nterm/terminal/bridge.py,sha256=mSkxZr3UGyaFI14w08dzekCkOhfUetq0GIjrBtA3qI0,3199
42
- nterm/terminal/widget.py,sha256=mxUrQxFmigNR6S3vgnzHahTRGYQI2bNYTBqNg47yaR8,15716
41
+ nterm/terminal/widget.py,sha256=38EyMmv6oz19ry0I0tQ-9itSK8LoU5vz1uuXJFz8swg,15720
43
42
  nterm/terminal/resources/terminal.html,sha256=1onb3qUdDa0qzETR8XaKx0UR6BPlCm_ZpMFVgt36ZPA,7985
44
43
  nterm/terminal/resources/terminal.js,sha256=zW9n1MRujSXv66ENgU-gzk_mc75EpWye_f88ejChSW4,13852
45
44
  nterm/terminal/resources/xterm-addon-fit.min.js,sha256=x45XlcZIes3ySrQ2eY1KnOw4SBAbKBvGWwYfOdtxS-E,1789
@@ -67,8 +66,8 @@ nterm/vault/manager_ui.py,sha256=qle-W40j6L_pOR0AaOCeyU8myizFTRkISNrloCn0H_Y,345
67
66
  nterm/vault/profile.py,sha256=qM9TJf68RKdjtxo-sJehO7wS4iTi2G26BKbmlmHLA5M,6246
68
67
  nterm/vault/resolver.py,sha256=GWB2YR9H1MH98RGQBKvitIsjWT_-wSMLuddZNz4wbns,7800
69
68
  nterm/vault/store.py,sha256=_0Lfe0WKjm3uSAtxgn9qAPlpBOLCuq9SVgzqsE_qaGQ,21199
70
- ntermqt-0.1.10.dist-info/METADATA,sha256=Pa4Oytg8jq0W5pU0bmmJR9nryjKRwewy54qejJValjM,16041
71
- ntermqt-0.1.10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
72
- ntermqt-0.1.10.dist-info/entry_points.txt,sha256=Gunr-_3w-aSpfqoMuGKM2PJSCRo9hZ7K1BksUtp1yd8,130
73
- ntermqt-0.1.10.dist-info/top_level.txt,sha256=bZdnNLTHNRNqo9jsOQGUWF7h5st0xW_thH0n2QOxWUo,6
74
- ntermqt-0.1.10.dist-info/RECORD,,
69
+ ntermqt-0.1.11.dist-info/METADATA,sha256=VC3c1CiPEAwtFhWzHIQK6spEj4MtRRNtZ6QUvwAkdIc,16041
70
+ ntermqt-0.1.11.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
71
+ ntermqt-0.1.11.dist-info/entry_points.txt,sha256=Gunr-_3w-aSpfqoMuGKM2PJSCRo9hZ7K1BksUtp1yd8,130
72
+ ntermqt-0.1.11.dist-info/top_level.txt,sha256=bZdnNLTHNRNqo9jsOQGUWF7h5st0xW_thH0n2QOxWUo,6
73
+ ntermqt-0.1.11.dist-info/RECORD,,
@@ -1,415 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Example nterm application.
4
-
5
- Demonstrates basic usage of the terminal widget with different session types:
6
- - SSHSession: Paramiko-based (for password/key auth)
7
- - AskpassSSHSession: Native SSH with GUI prompts (recommended for YubiKey)
8
- - InteractiveSSHSession: Native SSH with PTY
9
- """
10
-
11
- import sys
12
- import logging
13
- from PyQt6.QtWidgets import (
14
- QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
15
- QComboBox, QLabel, QPushButton, QStatusBar, QLineEdit, QSpinBox,
16
- QGroupBox, QFormLayout, QMessageBox, QDialog, QDialogButtonBox,
17
- QInputDialog
18
- )
19
- from PyQt6.QtCore import Qt, pyqtSignal, QObject
20
-
21
- from nterm import (
22
- ConnectionProfile, AuthConfig, AuthMethod, JumpHostConfig,
23
- SSHSession, SessionState, TerminalWidget, Theme, ThemeEngine,
24
- InteractiveSSHSession, is_pty_available
25
- )
26
- from nterm.session import AskpassSSHSession
27
-
28
- logging.basicConfig(level=logging.DEBUG)
29
- logger = logging.getLogger(__name__)
30
-
31
-
32
- class YubiKeyDialog(QDialog):
33
- """Dialog shown when YubiKey touch is required."""
34
-
35
- def __init__(self, prompt: str, parent=None):
36
- super().__init__(parent)
37
- self.setWindowTitle("YubiKey Authentication")
38
- self.setModal(True)
39
- self.setMinimumWidth(350)
40
-
41
- layout = QVBoxLayout(self)
42
-
43
- # Icon/visual indicator
44
- icon_label = QLabel("🔑")
45
- icon_label.setStyleSheet("font-size: 48px;")
46
- icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
47
- layout.addWidget(icon_label)
48
-
49
- # Prompt
50
- prompt_label = QLabel(prompt)
51
- prompt_label.setWordWrap(True)
52
- prompt_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
53
- prompt_label.setStyleSheet("font-size: 14px; margin: 10px;")
54
- layout.addWidget(prompt_label)
55
-
56
- # Instructions
57
- instructions = QLabel("Touch your YubiKey to authenticate...")
58
- instructions.setAlignment(Qt.AlignmentFlag.AlignCenter)
59
- instructions.setStyleSheet("color: gray;")
60
- layout.addWidget(instructions)
61
-
62
- # Cancel button
63
- buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel)
64
- buttons.rejected.connect(self.reject)
65
- layout.addWidget(buttons)
66
-
67
-
68
- class NTermWindow(QMainWindow):
69
- """Main application window."""
70
-
71
- def __init__(self):
72
- super().__init__()
73
- self.setWindowTitle("nterm - SSH Terminal")
74
- self.resize(1200, 800)
75
-
76
- self._session = None
77
- self._theme_engine = ThemeEngine()
78
- self._yubikey_dialog = None
79
-
80
- self._setup_ui()
81
- self._apply_theme("default")
82
-
83
- def _setup_ui(self):
84
- """Set up the user interface."""
85
- central = QWidget()
86
- self.setCentralWidget(central)
87
- layout = QVBoxLayout(central)
88
- layout.setContentsMargins(0, 0, 0, 0)
89
- layout.setSpacing(0)
90
-
91
- # Toolbar
92
- toolbar = self._create_toolbar()
93
- layout.addWidget(toolbar)
94
-
95
- # Terminal
96
- self._terminal = TerminalWidget()
97
- self._terminal.session_state_changed.connect(self._on_state_changed)
98
- self._terminal.interaction_required.connect(self._on_interaction)
99
- layout.addWidget(self._terminal, 1)
100
-
101
- # Status bar
102
- self._status = QStatusBar()
103
- self.setStatusBar(self._status)
104
- self._status.showMessage("Disconnected")
105
-
106
- def _create_toolbar(self) -> QWidget:
107
- """Create connection toolbar."""
108
- toolbar = QWidget()
109
- toolbar.setFixedHeight(100)
110
- layout = QHBoxLayout(toolbar)
111
- layout.setContentsMargins(8, 8, 8, 8)
112
-
113
- # Connection group
114
- conn_group = QGroupBox("Connection")
115
- conn_layout = QFormLayout(conn_group)
116
- conn_layout.setContentsMargins(8, 4, 8, 4)
117
-
118
- self._host_input = QLineEdit()
119
- self._host_input.setPlaceholderText("hostname or IP")
120
- self._host_input.setText("localhost")
121
- conn_layout.addRow("Host:", self._host_input)
122
-
123
- port_layout = QHBoxLayout()
124
- self._port_input = QSpinBox()
125
- self._port_input.setRange(1, 65535)
126
- self._port_input.setValue(22)
127
- port_layout.addWidget(self._port_input)
128
-
129
- self._user_input = QLineEdit()
130
- self._user_input.setPlaceholderText("username")
131
- port_layout.addWidget(QLabel("User:"))
132
- port_layout.addWidget(self._user_input)
133
- conn_layout.addRow("Port:", port_layout)
134
-
135
- layout.addWidget(conn_group)
136
-
137
- # Session type group
138
- session_group = QGroupBox("Session Type")
139
- session_layout = QVBoxLayout(session_group)
140
- session_layout.setContentsMargins(8, 4, 8, 4)
141
-
142
- self._session_combo = QComboBox()
143
- self._session_combo.addItem("Askpass (YubiKey GUI)", "askpass")
144
- self._session_combo.addItem("Interactive (PTY)", "interactive")
145
- self._session_combo.addItem("Paramiko", "paramiko")
146
- self._session_combo.currentIndexChanged.connect(self._on_session_type_changed)
147
- session_layout.addWidget(self._session_combo)
148
-
149
- # Status indicator
150
- self._pty_label = QLabel("✓ GUI auth prompts" if is_pty_available() else "⚠ Limited")
151
- self._pty_label.setStyleSheet("color: green;" if is_pty_available() else "color: orange;")
152
- session_layout.addWidget(self._pty_label)
153
-
154
- layout.addWidget(session_group)
155
-
156
- # Auth group (for Paramiko mode)
157
- self._auth_group = QGroupBox("Authentication")
158
- auth_layout = QFormLayout(self._auth_group)
159
- auth_layout.setContentsMargins(8, 4, 8, 4)
160
-
161
- self._auth_combo = QComboBox()
162
- self._auth_combo.addItems(["Agent", "Password", "Key File"])
163
- auth_layout.addRow("Method:", self._auth_combo)
164
-
165
- self._password_input = QLineEdit()
166
- self._password_input.setEchoMode(QLineEdit.EchoMode.Password)
167
- self._password_input.setPlaceholderText("(for password auth)")
168
- auth_layout.addRow("Password:", self._password_input)
169
-
170
- self._auth_group.setVisible(False)
171
- layout.addWidget(self._auth_group)
172
-
173
- # Jump host group
174
- jump_group = QGroupBox("Jump Host (Optional)")
175
- jump_layout = QFormLayout(jump_group)
176
- jump_layout.setContentsMargins(8, 4, 8, 4)
177
-
178
- self._jump_host_input = QLineEdit()
179
- self._jump_host_input.setPlaceholderText("bastion.example.com")
180
- jump_layout.addRow("Host:", self._jump_host_input)
181
-
182
- self._jump_user_input = QLineEdit()
183
- self._jump_user_input.setPlaceholderText("(same as main if empty)")
184
- jump_layout.addRow("User:", self._jump_user_input)
185
-
186
- layout.addWidget(jump_group)
187
-
188
- # Theme selector
189
- theme_group = QGroupBox("Theme")
190
- theme_layout = QVBoxLayout(theme_group)
191
- theme_layout.setContentsMargins(8, 4, 8, 4)
192
-
193
- self._theme_combo = QComboBox()
194
- self._theme_combo.addItems(self._theme_engine.list_themes())
195
- self._theme_combo.currentTextChanged.connect(self._apply_theme)
196
- theme_layout.addWidget(self._theme_combo)
197
-
198
- layout.addWidget(theme_group)
199
-
200
- # Buttons
201
- btn_layout = QVBoxLayout()
202
-
203
- self._connect_btn = QPushButton("Connect")
204
- self._connect_btn.clicked.connect(self._connect)
205
- self._connect_btn.setDefault(True)
206
- btn_layout.addWidget(self._connect_btn)
207
-
208
- self._disconnect_btn = QPushButton("Disconnect")
209
- self._disconnect_btn.clicked.connect(self._disconnect)
210
- self._disconnect_btn.setEnabled(False)
211
- btn_layout.addWidget(self._disconnect_btn)
212
-
213
- layout.addLayout(btn_layout)
214
- layout.addStretch()
215
-
216
- return toolbar
217
-
218
- def _on_session_type_changed(self, index: int):
219
- """Handle session type change."""
220
- session_type = self._session_combo.currentData()
221
- self._auth_group.setVisible(session_type == "paramiko")
222
-
223
- # Update status label
224
- if session_type == "askpass":
225
- self._pty_label.setText("✓ GUI auth prompts")
226
- self._pty_label.setStyleSheet("color: green;")
227
- elif session_type == "interactive":
228
- self._pty_label.setText("⚠ Console prompts")
229
- self._pty_label.setStyleSheet("color: orange;")
230
- else:
231
- self._pty_label.setText("✓ Programmatic auth")
232
- self._pty_label.setStyleSheet("color: green;")
233
-
234
- def _apply_theme(self, theme_name: str):
235
- """Apply selected theme."""
236
- theme = self._theme_engine.get_theme(theme_name)
237
- if theme:
238
- self._terminal.set_theme(theme)
239
-
240
- def _connect(self):
241
- """Establish connection."""
242
- hostname = self._host_input.text().strip()
243
- port = self._port_input.value()
244
- username = self._user_input.text().strip()
245
- session_type = self._session_combo.currentData()
246
-
247
- if not hostname:
248
- QMessageBox.warning(self, "Error", "Please enter a hostname")
249
- return
250
-
251
- if not username:
252
- QMessageBox.warning(self, "Error", "Please enter a username")
253
- return
254
-
255
- # Build auth config
256
- if session_type in ("askpass", "interactive"):
257
- auth = AuthConfig.agent_auth(username)
258
- else:
259
- auth_method = self._auth_combo.currentText()
260
- if auth_method == "Agent":
261
- auth = AuthConfig.agent_auth(username)
262
- elif auth_method == "Password":
263
- password = self._password_input.text()
264
- if not password:
265
- QMessageBox.warning(self, "Error", "Please enter a password")
266
- return
267
- auth = AuthConfig.password_auth(username, password)
268
- else:
269
- auth = AuthConfig.agent_auth(username, allow_fallback=True)
270
-
271
- # Build jump host config if specified
272
- jump_hosts = []
273
- jump_host = self._jump_host_input.text().strip()
274
- if jump_host:
275
- jump_user = self._jump_user_input.text().strip() or username
276
- jump_hosts.append(JumpHostConfig(
277
- hostname=jump_host,
278
- auth=AuthConfig.agent_auth(jump_user),
279
- ))
280
-
281
- # Create profile
282
- profile = ConnectionProfile(
283
- name=f"{username}@{hostname}",
284
- hostname=hostname,
285
- port=port,
286
- auth_methods=[auth],
287
- jump_hosts=jump_hosts,
288
- auto_reconnect=False, # Disable for testing
289
- )
290
-
291
- # Create appropriate session type
292
- if session_type == "askpass":
293
- if not is_pty_available():
294
- QMessageBox.warning(self, "Error", "PTY support required")
295
- return
296
- self._session = AskpassSSHSession(profile)
297
- elif session_type == "interactive":
298
- if not is_pty_available():
299
- QMessageBox.warning(self, "Error", "PTY support required")
300
- return
301
- self._session = InteractiveSSHSession(profile)
302
- else:
303
- self._session = SSHSession(profile)
304
-
305
- self._terminal.attach_session(self._session)
306
-
307
- # Connect
308
- self._session.connect()
309
- self._connect_btn.setEnabled(False)
310
- self._disconnect_btn.setEnabled(True)
311
-
312
- def _disconnect(self):
313
- """Disconnect session."""
314
- # Close any open dialogs
315
- if self._yubikey_dialog:
316
- self._yubikey_dialog.close()
317
- self._yubikey_dialog = None
318
-
319
- if self._session:
320
- self._session.disconnect()
321
- self._terminal.detach_session()
322
- self._session = None
323
-
324
- self._connect_btn.setEnabled(True)
325
- self._disconnect_btn.setEnabled(False)
326
-
327
- def _on_state_changed(self, state: SessionState, message: str):
328
- """Handle session state changes."""
329
- status_text = {
330
- SessionState.DISCONNECTED: "Disconnected",
331
- SessionState.CONNECTING: "Connecting...",
332
- SessionState.AUTHENTICATING: "Authenticating...",
333
- SessionState.CONNECTED: "Connected",
334
- SessionState.RECONNECTING: f"Reconnecting: {message}",
335
- SessionState.FAILED: f"Failed: {message}",
336
- }.get(state, str(state))
337
-
338
- self._status.showMessage(status_text)
339
-
340
- # Close YubiKey dialog on connect/disconnect
341
- if state in (SessionState.CONNECTED, SessionState.DISCONNECTED, SessionState.FAILED):
342
- if self._yubikey_dialog:
343
- self._yubikey_dialog.close()
344
- self._yubikey_dialog = None
345
-
346
- if state == SessionState.CONNECTED:
347
- self._connect_btn.setEnabled(False)
348
- self._disconnect_btn.setEnabled(True)
349
- self._terminal.focus()
350
- elif state in (SessionState.DISCONNECTED, SessionState.FAILED):
351
- self._connect_btn.setEnabled(True)
352
- self._disconnect_btn.setEnabled(False)
353
-
354
- def _on_interaction(self, prompt: str, interaction_type: str):
355
- """Handle SSH authentication prompts."""
356
- logger.info(f"Interaction required: {interaction_type} - {prompt}")
357
-
358
- if not isinstance(self._session, AskpassSSHSession):
359
- return
360
-
361
- if interaction_type == "yubikey_touch":
362
- # Show YubiKey dialog
363
- self._yubikey_dialog = YubiKeyDialog(prompt, self)
364
- result = self._yubikey_dialog.exec()
365
- self._yubikey_dialog = None
366
-
367
- if result == QDialog.DialogCode.Rejected:
368
- # User cancelled
369
- self._session.provide_askpass_response(False, error="Cancelled by user")
370
- else:
371
- # YubiKey was touched (dialog closed by external event)
372
- self._session.provide_askpass_response(True, value="")
373
-
374
- elif interaction_type == "password":
375
- # Show password dialog
376
- password, ok = QInputDialog.getText(
377
- self, "SSH Authentication", prompt,
378
- QLineEdit.EchoMode.Password
379
- )
380
-
381
- if ok and password:
382
- self._session.provide_askpass_response(True, value=password)
383
- else:
384
- self._session.provide_askpass_response(False, error="Cancelled by user")
385
-
386
- else:
387
- # Generic input
388
- text, ok = QInputDialog.getText(
389
- self, "SSH Authentication", prompt
390
- )
391
-
392
- if ok:
393
- self._session.provide_askpass_response(True, value=text)
394
- else:
395
- self._session.provide_askpass_response(False, error="Cancelled by user")
396
-
397
- def closeEvent(self, event):
398
- """Handle window close."""
399
- if self._session:
400
- self._session.disconnect()
401
- event.accept()
402
-
403
-
404
- def main():
405
- app = QApplication(sys.argv)
406
- app.setApplicationName("nterm")
407
-
408
- window = NTermWindow()
409
- window.show()
410
-
411
- sys.exit(app.exec())
412
-
413
-
414
- if __name__ == "__main__":
415
- main()