ntermqt 0.1.0__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/__init__.py +54 -0
- nterm/__main__.py +619 -0
- nterm/askpass/__init__.py +22 -0
- nterm/askpass/server.py +393 -0
- nterm/config.py +158 -0
- nterm/connection/__init__.py +17 -0
- nterm/connection/profile.py +296 -0
- nterm/manager/__init__.py +29 -0
- nterm/manager/connect_dialog.py +322 -0
- nterm/manager/editor.py +262 -0
- nterm/manager/io.py +678 -0
- nterm/manager/models.py +346 -0
- nterm/manager/settings.py +264 -0
- nterm/manager/tree.py +493 -0
- nterm/resources.py +48 -0
- nterm/session/__init__.py +60 -0
- nterm/session/askpass_ssh.py +399 -0
- nterm/session/base.py +110 -0
- nterm/session/interactive_ssh.py +522 -0
- nterm/session/pty_transport.py +571 -0
- nterm/session/ssh.py +610 -0
- nterm/terminal/__init__.py +11 -0
- nterm/terminal/bridge.py +83 -0
- nterm/terminal/resources/terminal.html +253 -0
- nterm/terminal/resources/terminal.js +414 -0
- nterm/terminal/resources/xterm-addon-fit.min.js +8 -0
- nterm/terminal/resources/xterm-addon-unicode11.min.js +8 -0
- nterm/terminal/resources/xterm-addon-web-links.min.js +8 -0
- nterm/terminal/resources/xterm.css +209 -0
- nterm/terminal/resources/xterm.min.js +8 -0
- nterm/terminal/widget.py +380 -0
- nterm/theme/__init__.py +10 -0
- nterm/theme/engine.py +456 -0
- nterm/theme/stylesheet.py +377 -0
- nterm/theme/themes/clean.yaml +0 -0
- nterm/theme/themes/default.yaml +36 -0
- nterm/theme/themes/dracula.yaml +36 -0
- nterm/theme/themes/gruvbox_dark.yaml +36 -0
- nterm/theme/themes/gruvbox_hybrid.yaml +38 -0
- nterm/theme/themes/gruvbox_light.yaml +36 -0
- nterm/vault/__init__.py +32 -0
- nterm/vault/credential_manager.py +163 -0
- nterm/vault/keychain.py +135 -0
- nterm/vault/manager_ui.py +962 -0
- nterm/vault/profile.py +219 -0
- nterm/vault/resolver.py +250 -0
- nterm/vault/store.py +642 -0
- ntermqt-0.1.0.dist-info/METADATA +327 -0
- ntermqt-0.1.0.dist-info/RECORD +52 -0
- ntermqt-0.1.0.dist-info/WHEEL +5 -0
- ntermqt-0.1.0.dist-info/entry_points.txt +5 -0
- ntermqt-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Example: Credential Manager with Theme Integration
|
|
4
|
+
|
|
5
|
+
Shows how to use the credential manager with nterm's theme system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from PyQt6.QtWidgets import (
|
|
12
|
+
QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
13
|
+
QHBoxLayout, QComboBox, QLabel, QPushButton,
|
|
14
|
+
)
|
|
15
|
+
from PyQt6.QtCore import Qt
|
|
16
|
+
|
|
17
|
+
# Add parent to path for imports
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
19
|
+
|
|
20
|
+
from nterm.vault import CredentialManagerWidget, CredentialResolver, ManagerTheme
|
|
21
|
+
from nterm.theme import Theme, ThemeEngine
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CredentialManagerWindow(QMainWindow):
|
|
25
|
+
"""Main window with theme-aware credential manager."""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.setWindowTitle("nTerm Credential Manager")
|
|
30
|
+
self.setMinimumSize(900, 700)
|
|
31
|
+
|
|
32
|
+
# Initialize theme engine
|
|
33
|
+
self.theme_engine = ThemeEngine()
|
|
34
|
+
self.theme_engine.load_themes()
|
|
35
|
+
|
|
36
|
+
# Initialize credential resolver (includes store)
|
|
37
|
+
self.resolver = CredentialResolver()
|
|
38
|
+
|
|
39
|
+
# Setup UI
|
|
40
|
+
self._setup_ui()
|
|
41
|
+
|
|
42
|
+
# Apply default theme
|
|
43
|
+
self._apply_theme(self.theme_engine.current)
|
|
44
|
+
|
|
45
|
+
# Try auto-unlock from keychain
|
|
46
|
+
self.manager.try_auto_unlock()
|
|
47
|
+
|
|
48
|
+
def _setup_ui(self):
|
|
49
|
+
central = QWidget()
|
|
50
|
+
self.setCentralWidget(central)
|
|
51
|
+
|
|
52
|
+
layout = QVBoxLayout(central)
|
|
53
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
54
|
+
layout.setSpacing(0)
|
|
55
|
+
|
|
56
|
+
# Theme selector bar
|
|
57
|
+
theme_bar = QWidget()
|
|
58
|
+
theme_bar.setObjectName("themeBar")
|
|
59
|
+
theme_layout = QHBoxLayout(theme_bar)
|
|
60
|
+
theme_layout.setContentsMargins(16, 8, 16, 8)
|
|
61
|
+
|
|
62
|
+
theme_layout.addWidget(QLabel("Theme:"))
|
|
63
|
+
|
|
64
|
+
self.theme_combo = QComboBox()
|
|
65
|
+
self.theme_combo.addItems(self.theme_engine.list_themes())
|
|
66
|
+
self.theme_combo.setCurrentText("default")
|
|
67
|
+
self.theme_combo.currentTextChanged.connect(self._on_theme_changed)
|
|
68
|
+
theme_layout.addWidget(self.theme_combo)
|
|
69
|
+
|
|
70
|
+
theme_layout.addStretch()
|
|
71
|
+
|
|
72
|
+
# Keychain management
|
|
73
|
+
from nterm.vault import KeychainIntegration
|
|
74
|
+
if KeychainIntegration.is_available():
|
|
75
|
+
clear_keychain_btn = QPushButton("Clear Keychain Password")
|
|
76
|
+
clear_keychain_btn.clicked.connect(self._clear_keychain)
|
|
77
|
+
theme_layout.addWidget(clear_keychain_btn)
|
|
78
|
+
|
|
79
|
+
layout.addWidget(theme_bar)
|
|
80
|
+
|
|
81
|
+
# Credential manager
|
|
82
|
+
self.manager = CredentialManagerWidget(store=self.resolver.store)
|
|
83
|
+
self.manager.credential_selected.connect(self._on_credential_selected)
|
|
84
|
+
self.manager.vault_unlocked.connect(self._on_vault_unlocked)
|
|
85
|
+
self.manager.vault_locked.connect(self._on_vault_locked)
|
|
86
|
+
layout.addWidget(self.manager)
|
|
87
|
+
|
|
88
|
+
def _apply_theme(self, theme: Theme):
|
|
89
|
+
"""Apply theme to entire window."""
|
|
90
|
+
self.theme_engine.current = theme
|
|
91
|
+
|
|
92
|
+
# Apply to credential manager
|
|
93
|
+
self.manager.set_theme(theme)
|
|
94
|
+
|
|
95
|
+
# Style the theme bar to match
|
|
96
|
+
self.setStyleSheet(f"""
|
|
97
|
+
QMainWindow {{
|
|
98
|
+
background-color: {theme.background_color};
|
|
99
|
+
}}
|
|
100
|
+
#themeBar {{
|
|
101
|
+
background-color: {theme.terminal_colors.get('black', '#313244')};
|
|
102
|
+
border-bottom: 1px solid {theme.border_color};
|
|
103
|
+
}}
|
|
104
|
+
#themeBar QLabel {{
|
|
105
|
+
color: {theme.foreground_color};
|
|
106
|
+
font-family: {theme.font_family};
|
|
107
|
+
}}
|
|
108
|
+
#themeBar QComboBox {{
|
|
109
|
+
background-color: {theme.background_color};
|
|
110
|
+
color: {theme.foreground_color};
|
|
111
|
+
border: 1px solid {theme.border_color};
|
|
112
|
+
border-radius: 4px;
|
|
113
|
+
padding: 4px 8px;
|
|
114
|
+
min-width: 120px;
|
|
115
|
+
}}
|
|
116
|
+
#themeBar QPushButton {{
|
|
117
|
+
background-color: {theme.background_color};
|
|
118
|
+
color: {theme.foreground_color};
|
|
119
|
+
border: 1px solid {theme.border_color};
|
|
120
|
+
border-radius: 4px;
|
|
121
|
+
padding: 4px 12px;
|
|
122
|
+
}}
|
|
123
|
+
#themeBar QPushButton:hover {{
|
|
124
|
+
border-color: {theme.accent_color};
|
|
125
|
+
}}
|
|
126
|
+
""")
|
|
127
|
+
|
|
128
|
+
def _on_theme_changed(self, theme_name: str):
|
|
129
|
+
theme = self.theme_engine.get_theme(theme_name)
|
|
130
|
+
if theme:
|
|
131
|
+
self._apply_theme(theme)
|
|
132
|
+
|
|
133
|
+
def _on_credential_selected(self, name: str):
|
|
134
|
+
print(f"Selected credential: {name}")
|
|
135
|
+
|
|
136
|
+
def _on_vault_unlocked(self):
|
|
137
|
+
print("Vault unlocked")
|
|
138
|
+
|
|
139
|
+
def _on_vault_locked(self):
|
|
140
|
+
print("Vault locked")
|
|
141
|
+
|
|
142
|
+
def _clear_keychain(self):
|
|
143
|
+
from nterm.vault import KeychainIntegration
|
|
144
|
+
if KeychainIntegration.clear_master_password():
|
|
145
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
146
|
+
QMessageBox.information(self, "Success", "Keychain password cleared")
|
|
147
|
+
else:
|
|
148
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
149
|
+
QMessageBox.warning(self, "Error", "Failed to clear keychain")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main():
|
|
153
|
+
app = QApplication(sys.argv)
|
|
154
|
+
app.setStyle("Fusion")
|
|
155
|
+
|
|
156
|
+
window = CredentialManagerWindow()
|
|
157
|
+
window.show()
|
|
158
|
+
|
|
159
|
+
sys.exit(app.exec())
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
main()
|
nterm/vault/keychain.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-platform keychain integration for master password storage.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- macOS: Keychain
|
|
6
|
+
- Windows: Credential Locker
|
|
7
|
+
- Linux: Secret Service (GNOME Keyring / KWallet)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Try to import keyring - it's optional
|
|
17
|
+
try:
|
|
18
|
+
import keyring
|
|
19
|
+
from keyring.errors import PasswordDeleteError, KeyringError
|
|
20
|
+
KEYRING_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
KEYRING_AVAILABLE = False
|
|
23
|
+
logger.debug("keyring not available - install with: pip install keyring")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class KeychainIntegration:
|
|
27
|
+
"""
|
|
28
|
+
Optional system keychain integration for master password caching.
|
|
29
|
+
|
|
30
|
+
This doesn't replace the vault's encryption - it just caches the
|
|
31
|
+
master password so users don't have to type it every session.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
SERVICE_NAME = "nterm-vault"
|
|
35
|
+
ACCOUNT_NAME = "master-password"
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def is_available(cls) -> bool:
|
|
39
|
+
"""Check if system keychain is available and functional."""
|
|
40
|
+
if not KEYRING_AVAILABLE:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
# Probe the keychain backend
|
|
45
|
+
backend = keyring.get_keyring()
|
|
46
|
+
# Check it's not the fail backend
|
|
47
|
+
backend_name = type(backend).__name__
|
|
48
|
+
if "Fail" in backend_name or "Null" in backend_name:
|
|
49
|
+
logger.debug(f"Keyring backend not usable: {backend_name}")
|
|
50
|
+
return False
|
|
51
|
+
logger.debug(f"Keyring backend: {backend_name}")
|
|
52
|
+
return True
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.debug(f"Keyring probe failed: {e}")
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def get_backend_name(cls) -> Optional[str]:
|
|
59
|
+
"""Get the name of the active keyring backend."""
|
|
60
|
+
if not KEYRING_AVAILABLE:
|
|
61
|
+
return None
|
|
62
|
+
try:
|
|
63
|
+
return type(keyring.get_keyring()).__name__
|
|
64
|
+
except Exception:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def store_master_password(cls, password: str) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Store master password in system keychain.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
password: The master vault password
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if stored successfully
|
|
77
|
+
"""
|
|
78
|
+
if not cls.is_available():
|
|
79
|
+
logger.warning("Keychain not available")
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
keyring.set_password(cls.SERVICE_NAME, cls.ACCOUNT_NAME, password)
|
|
84
|
+
logger.info("Master password stored in system keychain")
|
|
85
|
+
return True
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f"Failed to store password in keychain: {e}")
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def get_master_password(cls) -> Optional[str]:
|
|
92
|
+
"""
|
|
93
|
+
Retrieve master password from system keychain.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Password if found, None otherwise
|
|
97
|
+
"""
|
|
98
|
+
if not KEYRING_AVAILABLE:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
password = keyring.get_password(cls.SERVICE_NAME, cls.ACCOUNT_NAME)
|
|
103
|
+
if password:
|
|
104
|
+
logger.debug("Retrieved master password from keychain")
|
|
105
|
+
return password
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.debug(f"Failed to get password from keychain: {e}")
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def clear_master_password(cls) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
Remove master password from system keychain.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if removed (or wasn't present)
|
|
117
|
+
"""
|
|
118
|
+
if not KEYRING_AVAILABLE:
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
keyring.delete_password(cls.SERVICE_NAME, cls.ACCOUNT_NAME)
|
|
123
|
+
logger.info("Master password removed from system keychain")
|
|
124
|
+
return True
|
|
125
|
+
except PasswordDeleteError:
|
|
126
|
+
# Password wasn't stored - that's fine
|
|
127
|
+
return True
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Failed to remove password from keychain: {e}")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def has_stored_password(cls) -> bool:
|
|
134
|
+
"""Check if a password is stored without retrieving it."""
|
|
135
|
+
return cls.get_master_password() is not None
|