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 +20 -4
- nterm/terminal/widget.py +2 -2
- {ntermqt-0.1.10.dist-info → ntermqt-0.1.11.dist-info}/METADATA +1 -1
- {ntermqt-0.1.10.dist-info → ntermqt-0.1.11.dist-info}/RECORD +7 -8
- nterm/examples/basic_terminal.py +0 -415
- {ntermqt-0.1.10.dist-info → ntermqt-0.1.11.dist-info}/WHEEL +0 -0
- {ntermqt-0.1.10.dist-info → ntermqt-0.1.11.dist-info}/entry_points.txt +0 -0
- {ntermqt-0.1.10.dist-info → ntermqt-0.1.11.dist-info}/top_level.txt +0 -0
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
|
-
#
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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,12 +1,11 @@
|
|
|
1
1
|
nterm/__init__.py,sha256=Liu1bya6xi3NnwO9KLqqlYyLD1eS-2HeEdEpJRc2480,1346
|
|
2
|
-
nterm/__main__.py,sha256=
|
|
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=
|
|
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.
|
|
71
|
-
ntermqt-0.1.
|
|
72
|
-
ntermqt-0.1.
|
|
73
|
-
ntermqt-0.1.
|
|
74
|
-
ntermqt-0.1.
|
|
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,,
|
nterm/examples/basic_terminal.py
DELETED
|
@@ -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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|