ntermqt 0.1.0__py3-none-any.whl → 0.1.3__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 +339 -13
- 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/theme/engine.py +238 -0
- nterm/theme/themes/enterprise_dark.yaml +42 -0
- nterm/theme/themes/enterprise_hybrid.yaml +44 -0
- nterm/theme/themes/enterprise_light.yaml +42 -0
- {ntermqt-0.1.0.dist-info → ntermqt-0.1.3.dist-info}/METADATA +119 -12
- {ntermqt-0.1.0.dist-info → ntermqt-0.1.3.dist-info}/RECORD +15 -8
- {ntermqt-0.1.0.dist-info → ntermqt-0.1.3.dist-info}/entry_points.txt +1 -0
- {ntermqt-0.1.0.dist-info → ntermqt-0.1.3.dist-info}/WHEEL +0 -0
- {ntermqt-0.1.0.dist-info → ntermqt-0.1.3.dist-info}/top_level.txt +0 -0
nterm/__main__.py
CHANGED
|
@@ -11,7 +11,7 @@ from PyQt6.QtWidgets import (
|
|
|
11
11
|
QApplication, QMainWindow, QSplitter, QTabWidget,
|
|
12
12
|
QWidget, QVBoxLayout, QHBoxLayout, QMessageBox,
|
|
13
13
|
QDialog, QLabel, QLineEdit, QPushButton, QCheckBox,
|
|
14
|
-
QMenuBar, QMenu
|
|
14
|
+
QMenuBar, QMenu, QTabBar
|
|
15
15
|
)
|
|
16
16
|
from PyQt6.QtCore import Qt
|
|
17
17
|
from PyQt6.QtGui import QAction, QKeySequence
|
|
@@ -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
|
|
@@ -207,6 +208,52 @@ class TerminalTab(QWidget):
|
|
|
207
208
|
"""Disconnect the session."""
|
|
208
209
|
self.ssh_session.disconnect()
|
|
209
210
|
|
|
211
|
+
def is_connected(self) -> bool:
|
|
212
|
+
"""Check if the session is currently connected."""
|
|
213
|
+
# Try multiple ways to detect connection state
|
|
214
|
+
if hasattr(self.ssh_session, 'is_connected'):
|
|
215
|
+
return self.ssh_session.is_connected
|
|
216
|
+
if hasattr(self.ssh_session, 'connected'):
|
|
217
|
+
return self.ssh_session.connected
|
|
218
|
+
if hasattr(self.ssh_session, '_connected'):
|
|
219
|
+
return self.ssh_session._connected
|
|
220
|
+
if hasattr(self.ssh_session, '_channel'):
|
|
221
|
+
return self.ssh_session._channel is not None
|
|
222
|
+
if hasattr(self.ssh_session, '_transport'):
|
|
223
|
+
transport = self.ssh_session._transport
|
|
224
|
+
return transport is not None and transport.is_active()
|
|
225
|
+
# Default to True if we can't determine - safer to warn
|
|
226
|
+
return True
|
|
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
|
+
|
|
210
257
|
|
|
211
258
|
class MainWindow(QMainWindow):
|
|
212
259
|
"""
|
|
@@ -249,12 +296,6 @@ class MainWindow(QMainWindow):
|
|
|
249
296
|
|
|
250
297
|
# Apply initial stylesheet
|
|
251
298
|
self._apply_qt_theme(self.current_theme)
|
|
252
|
-
self._setup_ui()
|
|
253
|
-
self._connect_signals()
|
|
254
|
-
self._refresh_credentials()
|
|
255
|
-
|
|
256
|
-
# Apply initial stylesheet
|
|
257
|
-
self._apply_qt_theme(self.current_theme)
|
|
258
299
|
|
|
259
300
|
def _on_settings_changed(self, settings):
|
|
260
301
|
"""Handle settings changes from dialog."""
|
|
@@ -263,7 +304,7 @@ class MainWindow(QMainWindow):
|
|
|
263
304
|
# Apply multiline threshold to all open terminals
|
|
264
305
|
for i in range(self.tab_widget.count()):
|
|
265
306
|
tab = self.tab_widget.widget(i)
|
|
266
|
-
if isinstance(tab, TerminalTab):
|
|
307
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)):
|
|
267
308
|
tab.terminal.set_multiline_threshold(settings.multiline_paste_threshold)
|
|
268
309
|
|
|
269
310
|
|
|
@@ -298,6 +339,10 @@ class MainWindow(QMainWindow):
|
|
|
298
339
|
self.tab_widget.setDocumentMode(True)
|
|
299
340
|
splitter.addWidget(self.tab_widget)
|
|
300
341
|
|
|
342
|
+
# Enable tab context menu
|
|
343
|
+
self.tab_widget.tabBar().setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
344
|
+
self.tab_widget.tabBar().customContextMenuRequested.connect(self._show_tab_context_menu)
|
|
345
|
+
|
|
301
346
|
# Set initial sizes (tree: 250px, tabs: rest)
|
|
302
347
|
splitter.setSizes([250, 950])
|
|
303
348
|
|
|
@@ -323,6 +368,20 @@ class MainWindow(QMainWindow):
|
|
|
323
368
|
|
|
324
369
|
file_menu.addSeparator()
|
|
325
370
|
|
|
371
|
+
# --- NEW: Tab management actions ---
|
|
372
|
+
close_tab_action = QAction("Close Tab", self)
|
|
373
|
+
close_tab_action.setShortcut(QKeySequence("Ctrl+W"))
|
|
374
|
+
close_tab_action.triggered.connect(self._close_current_tab)
|
|
375
|
+
file_menu.addAction(close_tab_action)
|
|
376
|
+
|
|
377
|
+
close_all_action = QAction("Close All Tabs", self)
|
|
378
|
+
close_all_action.setShortcut(QKeySequence("Ctrl+Shift+W"))
|
|
379
|
+
close_all_action.triggered.connect(self._close_all_tabs)
|
|
380
|
+
file_menu.addAction(close_all_action)
|
|
381
|
+
|
|
382
|
+
file_menu.addSeparator()
|
|
383
|
+
# --- END NEW ---
|
|
384
|
+
|
|
326
385
|
import_action = QAction("&Import Sessions...", self)
|
|
327
386
|
import_action.setShortcut(QKeySequence("Ctrl+I"))
|
|
328
387
|
import_action.triggered.connect(self._on_import_sessions)
|
|
@@ -372,6 +431,32 @@ class MainWindow(QMainWindow):
|
|
|
372
431
|
action.triggered.connect(lambda checked, n=theme_name: self._apply_theme_by_name(n))
|
|
373
432
|
theme_menu.addAction(action)
|
|
374
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
|
+
|
|
375
460
|
def _on_import_sessions(self):
|
|
376
461
|
"""Show import dialog."""
|
|
377
462
|
dialog = ImportDialog(self.session_store, self)
|
|
@@ -416,7 +501,7 @@ class MainWindow(QMainWindow):
|
|
|
416
501
|
# Update all open terminal tabs
|
|
417
502
|
for i in range(self.tab_widget.count()):
|
|
418
503
|
tab = self.tab_widget.widget(i)
|
|
419
|
-
if isinstance(tab, TerminalTab):
|
|
504
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)):
|
|
420
505
|
tab.terminal.set_theme(theme)
|
|
421
506
|
|
|
422
507
|
def _apply_qt_theme(self, theme: Theme):
|
|
@@ -531,15 +616,192 @@ class MainWindow(QMainWindow):
|
|
|
531
616
|
self._child_windows.append(window)
|
|
532
617
|
window.destroyed.connect(lambda: self._child_windows.remove(window))
|
|
533
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
|
+
|
|
636
|
+
# -------------------------------------------------------------------------
|
|
637
|
+
# Tab Management (NEW)
|
|
638
|
+
# -------------------------------------------------------------------------
|
|
639
|
+
|
|
640
|
+
def _get_active_session_count(self) -> int:
|
|
641
|
+
"""Count tabs with active connections."""
|
|
642
|
+
count = 0
|
|
643
|
+
for i in range(self.tab_widget.count()):
|
|
644
|
+
tab = self.tab_widget.widget(i)
|
|
645
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)) and tab.is_connected():
|
|
646
|
+
count += 1
|
|
647
|
+
return count
|
|
648
|
+
|
|
649
|
+
def _show_tab_context_menu(self, pos):
|
|
650
|
+
"""Show context menu for tab bar."""
|
|
651
|
+
tab_bar = self.tab_widget.tabBar()
|
|
652
|
+
index = tab_bar.tabAt(pos)
|
|
653
|
+
|
|
654
|
+
menu = QMenu(self)
|
|
655
|
+
|
|
656
|
+
if index >= 0:
|
|
657
|
+
# Clicked on a tab
|
|
658
|
+
menu.addAction("Close", lambda: self._close_tab(index))
|
|
659
|
+
menu.addAction("Close Others", lambda: self._close_other_tabs(index))
|
|
660
|
+
menu.addAction("Close Tabs to the Right", lambda: self._close_tabs_to_right(index))
|
|
661
|
+
menu.addSeparator()
|
|
662
|
+
|
|
663
|
+
if self.tab_widget.count() > 0:
|
|
664
|
+
menu.addAction("Close All Tabs", self._close_all_tabs)
|
|
665
|
+
|
|
666
|
+
if menu.actions():
|
|
667
|
+
menu.exec(tab_bar.mapToGlobal(pos))
|
|
668
|
+
|
|
669
|
+
def _close_current_tab(self):
|
|
670
|
+
"""Close the currently active tab."""
|
|
671
|
+
index = self.tab_widget.currentIndex()
|
|
672
|
+
if index >= 0:
|
|
673
|
+
self._close_tab(index)
|
|
674
|
+
|
|
534
675
|
def _close_tab(self, index: int):
|
|
535
|
-
"""Close a terminal tab."""
|
|
676
|
+
"""Close a terminal tab with confirmation if connected."""
|
|
536
677
|
tab = self.tab_widget.widget(index)
|
|
678
|
+
|
|
537
679
|
if isinstance(tab, TerminalTab):
|
|
680
|
+
# Check if session is active
|
|
681
|
+
if tab.is_connected():
|
|
682
|
+
reply = QMessageBox.question(
|
|
683
|
+
self,
|
|
684
|
+
"Close Tab",
|
|
685
|
+
f"'{tab.session.name}' has an active connection.\n\n"
|
|
686
|
+
"Disconnect and close this tab?",
|
|
687
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
688
|
+
QMessageBox.StandardButton.No
|
|
689
|
+
)
|
|
690
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
691
|
+
return
|
|
692
|
+
|
|
538
693
|
tab.disconnect()
|
|
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
|
+
|
|
539
711
|
self.tab_widget.removeTab(index)
|
|
540
712
|
|
|
713
|
+
def _close_other_tabs(self, keep_index: int):
|
|
714
|
+
"""Close all tabs except the specified one."""
|
|
715
|
+
active_count = self._get_active_session_count()
|
|
716
|
+
tab_to_keep = self.tab_widget.widget(keep_index)
|
|
717
|
+
keep_is_active = isinstance(tab_to_keep, (TerminalTab, LocalTerminalTab)) and tab_to_keep.is_connected()
|
|
718
|
+
other_active = active_count - (1 if keep_is_active else 0)
|
|
719
|
+
|
|
720
|
+
if other_active > 0:
|
|
721
|
+
reply = QMessageBox.question(
|
|
722
|
+
self,
|
|
723
|
+
"Close Other Tabs",
|
|
724
|
+
f"{other_active} other tab(s) have active connections.\n\n"
|
|
725
|
+
"Disconnect and close them?",
|
|
726
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
727
|
+
QMessageBox.StandardButton.No
|
|
728
|
+
)
|
|
729
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
730
|
+
return
|
|
731
|
+
|
|
732
|
+
# Close tabs from end to avoid index shifting issues
|
|
733
|
+
for i in range(self.tab_widget.count() - 1, -1, -1):
|
|
734
|
+
if i != keep_index:
|
|
735
|
+
tab = self.tab_widget.widget(i)
|
|
736
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)):
|
|
737
|
+
tab.disconnect()
|
|
738
|
+
self.tab_widget.removeTab(i)
|
|
739
|
+
|
|
740
|
+
def _close_tabs_to_right(self, index: int):
|
|
741
|
+
"""Close all tabs to the right of the specified index."""
|
|
742
|
+
tabs_to_close = self.tab_widget.count() - index - 1
|
|
743
|
+
if tabs_to_close <= 0:
|
|
744
|
+
return
|
|
745
|
+
|
|
746
|
+
# Count active sessions to the right
|
|
747
|
+
active_count = 0
|
|
748
|
+
for i in range(index + 1, self.tab_widget.count()):
|
|
749
|
+
tab = self.tab_widget.widget(i)
|
|
750
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)) and tab.is_connected():
|
|
751
|
+
active_count += 1
|
|
752
|
+
|
|
753
|
+
if active_count > 0:
|
|
754
|
+
reply = QMessageBox.question(
|
|
755
|
+
self,
|
|
756
|
+
"Close Tabs",
|
|
757
|
+
f"{active_count} tab(s) to the right have active connections.\n\n"
|
|
758
|
+
"Disconnect and close them?",
|
|
759
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
760
|
+
QMessageBox.StandardButton.No
|
|
761
|
+
)
|
|
762
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
763
|
+
return
|
|
764
|
+
|
|
765
|
+
# Close from end to avoid index shifting
|
|
766
|
+
for i in range(self.tab_widget.count() - 1, index, -1):
|
|
767
|
+
tab = self.tab_widget.widget(i)
|
|
768
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)):
|
|
769
|
+
tab.disconnect()
|
|
770
|
+
self.tab_widget.removeTab(i)
|
|
771
|
+
|
|
772
|
+
def _close_all_tabs(self):
|
|
773
|
+
"""Close all tabs with confirmation."""
|
|
774
|
+
if self.tab_widget.count() == 0:
|
|
775
|
+
return
|
|
776
|
+
|
|
777
|
+
active_count = self._get_active_session_count()
|
|
778
|
+
|
|
779
|
+
if active_count > 0:
|
|
780
|
+
reply = QMessageBox.question(
|
|
781
|
+
self,
|
|
782
|
+
"Close All Tabs",
|
|
783
|
+
f"{active_count} tab(s) have active connections.\n\n"
|
|
784
|
+
"Disconnect and close all tabs?",
|
|
785
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
786
|
+
QMessageBox.StandardButton.No
|
|
787
|
+
)
|
|
788
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
789
|
+
return
|
|
790
|
+
|
|
791
|
+
# Close all tabs
|
|
792
|
+
while self.tab_widget.count() > 0:
|
|
793
|
+
tab = self.tab_widget.widget(0)
|
|
794
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)):
|
|
795
|
+
tab.disconnect()
|
|
796
|
+
self.tab_widget.removeTab(0)
|
|
797
|
+
|
|
798
|
+
# -------------------------------------------------------------------------
|
|
799
|
+
# End Tab Management
|
|
800
|
+
# -------------------------------------------------------------------------
|
|
801
|
+
|
|
541
802
|
def closeEvent(self, event):
|
|
542
|
-
"""Clean up on close."""
|
|
803
|
+
"""Clean up on close with confirmation for active sessions."""
|
|
804
|
+
# Save window geometry first
|
|
543
805
|
if not self.isMaximized():
|
|
544
806
|
self.app_settings.window_width = self.width()
|
|
545
807
|
self.app_settings.window_height = self.height()
|
|
@@ -547,11 +809,28 @@ class MainWindow(QMainWindow):
|
|
|
547
809
|
self.app_settings.window_y = self.y()
|
|
548
810
|
self.app_settings.window_maximized = self.isMaximized()
|
|
549
811
|
|
|
812
|
+
# Check for active connections
|
|
813
|
+
active_count = self._get_active_session_count()
|
|
814
|
+
|
|
815
|
+
if active_count > 0:
|
|
816
|
+
reply = QMessageBox.question(
|
|
817
|
+
self,
|
|
818
|
+
"Quit nterm",
|
|
819
|
+
f"You have {active_count} active session(s).\n\n"
|
|
820
|
+
"Disconnect all and quit?",
|
|
821
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
822
|
+
QMessageBox.StandardButton.No
|
|
823
|
+
)
|
|
824
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
825
|
+
event.ignore()
|
|
826
|
+
return
|
|
827
|
+
|
|
550
828
|
save_settings()
|
|
829
|
+
|
|
551
830
|
# Disconnect all tabs
|
|
552
831
|
for i in range(self.tab_widget.count()):
|
|
553
832
|
tab = self.tab_widget.widget(i)
|
|
554
|
-
if isinstance(tab, TerminalTab):
|
|
833
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)):
|
|
555
834
|
tab.disconnect()
|
|
556
835
|
|
|
557
836
|
self.session_store.close()
|
|
@@ -582,7 +861,54 @@ class TerminalWindow(QMainWindow):
|
|
|
582
861
|
self.tab.connect()
|
|
583
862
|
|
|
584
863
|
def closeEvent(self, event):
|
|
585
|
-
"""Disconnect on close."""
|
|
864
|
+
"""Disconnect on close with confirmation."""
|
|
865
|
+
if self.tab.is_connected():
|
|
866
|
+
reply = QMessageBox.question(
|
|
867
|
+
self,
|
|
868
|
+
"Close Window",
|
|
869
|
+
f"'{self.tab.session.name}' has an active connection.\n\n"
|
|
870
|
+
"Disconnect and close?",
|
|
871
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
872
|
+
QMessageBox.StandardButton.No
|
|
873
|
+
)
|
|
874
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
875
|
+
event.ignore()
|
|
876
|
+
return
|
|
877
|
+
|
|
878
|
+
self.tab.disconnect()
|
|
879
|
+
event.accept()
|
|
880
|
+
|
|
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
|
+
|
|
586
912
|
self.tab.disconnect()
|
|
587
913
|
event.accept()
|
|
588
914
|
|
|
@@ -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
|
+
]
|