ntermqt 0.1.1__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 CHANGED
@@ -22,6 +22,7 @@ from nterm.manager import (
22
22
  )
23
23
  from nterm.terminal.widget import TerminalWidget
24
24
  from nterm.session.ssh import SSHSession
25
+ from nterm.session.local_terminal import LocalTerminal
25
26
  from nterm.connection.profile import ConnectionProfile, AuthConfig
26
27
  from nterm.vault import CredentialManagerWidget
27
28
  from nterm.vault.resolver import CredentialResolver
@@ -225,6 +226,35 @@ class TerminalTab(QWidget):
225
226
  return True
226
227
 
227
228
 
229
+ class LocalTerminalTab(QWidget):
230
+ """A terminal tab for local processes (shell, IPython, etc.)."""
231
+
232
+ def __init__(self, name: str, session: LocalTerminal, parent=None):
233
+ super().__init__(parent)
234
+ self.name = name
235
+ self.local_session = session
236
+
237
+ layout = QVBoxLayout(self)
238
+ layout.setContentsMargins(0, 0, 0, 0)
239
+
240
+ self.terminal = TerminalWidget()
241
+ layout.addWidget(self.terminal)
242
+
243
+ self.terminal.attach_session(self.local_session)
244
+
245
+ def connect(self):
246
+ """Start the local process."""
247
+ self.local_session.connect()
248
+
249
+ def disconnect(self):
250
+ """Terminate the local process."""
251
+ self.local_session.disconnect()
252
+
253
+ def is_connected(self) -> bool:
254
+ """Check if the process is still running."""
255
+ return self.local_session.is_connected
256
+
257
+
228
258
  class MainWindow(QMainWindow):
229
259
  """
230
260
  Main application window with session tree and tabbed terminals.
@@ -274,7 +304,7 @@ class MainWindow(QMainWindow):
274
304
  # Apply multiline threshold to all open terminals
275
305
  for i in range(self.tab_widget.count()):
276
306
  tab = self.tab_widget.widget(i)
277
- if isinstance(tab, TerminalTab):
307
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)):
278
308
  tab.terminal.set_multiline_threshold(settings.multiline_paste_threshold)
279
309
 
280
310
 
@@ -401,6 +431,32 @@ class MainWindow(QMainWindow):
401
431
  action.triggered.connect(lambda checked, n=theme_name: self._apply_theme_by_name(n))
402
432
  theme_menu.addAction(action)
403
433
 
434
+ # Dev menu
435
+ dev_menu = menubar.addMenu("&Dev")
436
+
437
+ # IPython submenu
438
+ ipython_menu = dev_menu.addMenu("&IPython")
439
+
440
+ ipython_tab_action = QAction("Open in &Tab", self)
441
+ ipython_tab_action.setShortcut(QKeySequence("Ctrl+Shift+I"))
442
+ ipython_tab_action.triggered.connect(lambda: self._open_local("IPython", LocalTerminal.ipython(), "tab"))
443
+ ipython_menu.addAction(ipython_tab_action)
444
+
445
+ ipython_window_action = QAction("Open in &Window", self)
446
+ ipython_window_action.triggered.connect(lambda: self._open_local("IPython", LocalTerminal.ipython(), "window"))
447
+ ipython_menu.addAction(ipython_window_action)
448
+
449
+ # Shell submenu
450
+ shell_menu = dev_menu.addMenu("&Shell")
451
+
452
+ shell_tab_action = QAction("Open in &Tab", self)
453
+ shell_tab_action.triggered.connect(lambda: self._open_local("Shell", LocalTerminal(), "tab"))
454
+ shell_menu.addAction(shell_tab_action)
455
+
456
+ shell_window_action = QAction("Open in &Window", self)
457
+ shell_window_action.triggered.connect(lambda: self._open_local("Shell", LocalTerminal(), "window"))
458
+ shell_menu.addAction(shell_window_action)
459
+
404
460
  def _on_import_sessions(self):
405
461
  """Show import dialog."""
406
462
  dialog = ImportDialog(self.session_store, self)
@@ -445,7 +501,7 @@ class MainWindow(QMainWindow):
445
501
  # Update all open terminal tabs
446
502
  for i in range(self.tab_widget.count()):
447
503
  tab = self.tab_widget.widget(i)
448
- if isinstance(tab, TerminalTab):
504
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)):
449
505
  tab.terminal.set_theme(theme)
450
506
 
451
507
  def _apply_qt_theme(self, theme: Theme):
@@ -560,6 +616,23 @@ class MainWindow(QMainWindow):
560
616
  self._child_windows.append(window)
561
617
  window.destroyed.connect(lambda: self._child_windows.remove(window))
562
618
 
619
+ def _open_local(self, name: str, session: LocalTerminal, mode: str):
620
+ """Open a local terminal session (IPython, shell, etc.)."""
621
+ if mode == "tab":
622
+ tab = LocalTerminalTab(name, session)
623
+ tab.terminal.set_theme(self.current_theme)
624
+ idx = self.tab_widget.addTab(tab, name)
625
+ self.tab_widget.setCurrentIndex(idx)
626
+ self.tab_widget.setTabToolTip(idx, f"{name} (local)")
627
+ tab.connect()
628
+ else:
629
+ window = LocalTerminalWindow(name, session, self.current_theme)
630
+ window.show()
631
+ if not hasattr(self, '_child_windows'):
632
+ self._child_windows = []
633
+ self._child_windows.append(window)
634
+ window.destroyed.connect(lambda: self._child_windows.remove(window))
635
+
563
636
  # -------------------------------------------------------------------------
564
637
  # Tab Management (NEW)
565
638
  # -------------------------------------------------------------------------
@@ -569,7 +642,7 @@ class MainWindow(QMainWindow):
569
642
  count = 0
570
643
  for i in range(self.tab_widget.count()):
571
644
  tab = self.tab_widget.widget(i)
572
- if isinstance(tab, TerminalTab) and tab.is_connected():
645
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)) and tab.is_connected():
573
646
  count += 1
574
647
  return count
575
648
 
@@ -619,13 +692,29 @@ class MainWindow(QMainWindow):
619
692
 
620
693
  tab.disconnect()
621
694
 
695
+ elif isinstance(tab, LocalTerminalTab):
696
+ # Check if process is still running
697
+ if tab.is_connected():
698
+ reply = QMessageBox.question(
699
+ self,
700
+ "Close Tab",
701
+ f"'{tab.name}' is still running.\n\n"
702
+ "Terminate and close this tab?",
703
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
704
+ QMessageBox.StandardButton.No
705
+ )
706
+ if reply != QMessageBox.StandardButton.Yes:
707
+ return
708
+
709
+ tab.disconnect()
710
+
622
711
  self.tab_widget.removeTab(index)
623
712
 
624
713
  def _close_other_tabs(self, keep_index: int):
625
714
  """Close all tabs except the specified one."""
626
715
  active_count = self._get_active_session_count()
627
716
  tab_to_keep = self.tab_widget.widget(keep_index)
628
- keep_is_active = isinstance(tab_to_keep, TerminalTab) and tab_to_keep.is_connected()
717
+ keep_is_active = isinstance(tab_to_keep, (TerminalTab, LocalTerminalTab)) and tab_to_keep.is_connected()
629
718
  other_active = active_count - (1 if keep_is_active else 0)
630
719
 
631
720
  if other_active > 0:
@@ -644,7 +733,7 @@ class MainWindow(QMainWindow):
644
733
  for i in range(self.tab_widget.count() - 1, -1, -1):
645
734
  if i != keep_index:
646
735
  tab = self.tab_widget.widget(i)
647
- if isinstance(tab, TerminalTab):
736
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)):
648
737
  tab.disconnect()
649
738
  self.tab_widget.removeTab(i)
650
739
 
@@ -658,7 +747,7 @@ class MainWindow(QMainWindow):
658
747
  active_count = 0
659
748
  for i in range(index + 1, self.tab_widget.count()):
660
749
  tab = self.tab_widget.widget(i)
661
- if isinstance(tab, TerminalTab) and tab.is_connected():
750
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)) and tab.is_connected():
662
751
  active_count += 1
663
752
 
664
753
  if active_count > 0:
@@ -676,7 +765,7 @@ class MainWindow(QMainWindow):
676
765
  # Close from end to avoid index shifting
677
766
  for i in range(self.tab_widget.count() - 1, index, -1):
678
767
  tab = self.tab_widget.widget(i)
679
- if isinstance(tab, TerminalTab):
768
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)):
680
769
  tab.disconnect()
681
770
  self.tab_widget.removeTab(i)
682
771
 
@@ -702,7 +791,7 @@ class MainWindow(QMainWindow):
702
791
  # Close all tabs
703
792
  while self.tab_widget.count() > 0:
704
793
  tab = self.tab_widget.widget(0)
705
- if isinstance(tab, TerminalTab):
794
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)):
706
795
  tab.disconnect()
707
796
  self.tab_widget.removeTab(0)
708
797
 
@@ -741,7 +830,7 @@ class MainWindow(QMainWindow):
741
830
  # Disconnect all tabs
742
831
  for i in range(self.tab_widget.count()):
743
832
  tab = self.tab_widget.widget(i)
744
- if isinstance(tab, TerminalTab):
833
+ if isinstance(tab, (TerminalTab, LocalTerminalTab)):
745
834
  tab.disconnect()
746
835
 
747
836
  self.session_store.close()
@@ -790,6 +879,40 @@ class TerminalWindow(QMainWindow):
790
879
  event.accept()
791
880
 
792
881
 
882
+ class LocalTerminalWindow(QMainWindow):
883
+ """Standalone window for local terminal sessions."""
884
+
885
+ def __init__(self, name: str, session: LocalTerminal, theme: Theme):
886
+ super().__init__()
887
+ self.setWindowTitle(f"{name} - Local")
888
+ self.resize(1000, 700)
889
+
890
+ self.setStyleSheet(generate_stylesheet(theme))
891
+
892
+ self.tab = LocalTerminalTab(name, session)
893
+ self.tab.terminal.set_theme(theme)
894
+ self.setCentralWidget(self.tab)
895
+
896
+ self.tab.connect()
897
+
898
+ def closeEvent(self, event):
899
+ """Terminate on close with confirmation."""
900
+ if self.tab.is_connected():
901
+ reply = QMessageBox.question(
902
+ self,
903
+ "Close Window",
904
+ f"'{self.tab.name}' is still running.\n\nTerminate and close?",
905
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
906
+ QMessageBox.StandardButton.No
907
+ )
908
+ if reply != QMessageBox.StandardButton.Yes:
909
+ event.ignore()
910
+ return
911
+
912
+ self.tab.disconnect()
913
+ event.accept()
914
+
915
+
793
916
  def main():
794
917
  app = QApplication(sys.argv)
795
918
 
@@ -0,0 +1,43 @@
1
+ """
2
+ nterm.scripting - Scripting API for nterm
3
+
4
+ Provides programmatic access to nterm's device sessions and credential vault.
5
+ Usable from IPython, CLI, scripts, or as foundation for MCP tools.
6
+
7
+ Quick Start (IPython):
8
+ from nterm.scripting import api
9
+
10
+ api.devices() # List all saved devices
11
+ api.search("leaf") # Search devices
12
+ api.device("eng-leaf-1") # Get specific device
13
+
14
+ api.unlock("vault-password") # Unlock credential vault
15
+ api.credentials() # List credentials
16
+
17
+ api.help() # Show all commands
18
+
19
+ Quick Start (CLI):
20
+ nterm-cli devices
21
+ nterm-cli search leaf
22
+ nterm-cli credentials --unlock
23
+ """
24
+
25
+ from .api import (
26
+ NTermAPI,
27
+ DeviceInfo,
28
+ CredentialInfo,
29
+ get_api,
30
+ reset_api,
31
+ )
32
+
33
+ # Convenience: pre-instantiated API
34
+ api = get_api()
35
+
36
+ __all__ = [
37
+ "NTermAPI",
38
+ "DeviceInfo",
39
+ "CredentialInfo",
40
+ "get_api",
41
+ "reset_api",
42
+ "api",
43
+ ]