adbsshdeck 0.1.1__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.
- adbsshdeck-0.1.1.dist-info/LICENSE +21 -0
- adbsshdeck-0.1.1.dist-info/METADATA +136 -0
- adbsshdeck-0.1.1.dist-info/RECORD +29 -0
- adbsshdeck-0.1.1.dist-info/WHEEL +5 -0
- adbsshdeck-0.1.1.dist-info/entry_points.txt +2 -0
- adbsshdeck-0.1.1.dist-info/top_level.txt +1 -0
- devicedeck/__init__.py +4 -0
- devicedeck/__main__.py +4 -0
- devicedeck/app.py +45 -0
- devicedeck/config.py +130 -0
- devicedeck/services/__init__.py +1 -0
- devicedeck/services/adb_devices.py +74 -0
- devicedeck/services/commands.py +96 -0
- devicedeck/services/remote_clients.py +84 -0
- devicedeck/session.py +52 -0
- devicedeck/ui/__init__.py +1 -0
- devicedeck/ui/app_icon.py +46 -0
- devicedeck/ui/combo_utils.py +45 -0
- devicedeck/ui/first_run_dialog.py +107 -0
- devicedeck/ui/icon_utils.py +144 -0
- devicedeck/ui/main_window.py +691 -0
- devicedeck/ui/preferences_dialog.py +122 -0
- devicedeck/ui/session_login_dialog.py +795 -0
- devicedeck/ui/styles.py +1021 -0
- devicedeck/ui/tabs/__init__.py +1 -0
- devicedeck/ui/tabs/file_explorer_tab.py +3680 -0
- devicedeck/ui/tabs/scrcpy_tab.py +770 -0
- devicedeck/ui/tabs/terminal_tab.py +1192 -0
- devicedeck/ui/win_scrcpy_hotkey.py +76 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
"""WinSCP-style Login: form + saved sessions list (bookmarks). Terminal and Explorer share the same storage."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .. import APP_TITLE
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from PyQt5.QtCore import QSize, Qt
|
|
9
|
+
from PyQt5.QtGui import QShowEvent
|
|
10
|
+
from PyQt5.QtWidgets import (
|
|
11
|
+
QAbstractItemView,
|
|
12
|
+
QCheckBox,
|
|
13
|
+
QDialog,
|
|
14
|
+
QDialogButtonBox,
|
|
15
|
+
QFormLayout,
|
|
16
|
+
QGroupBox,
|
|
17
|
+
QHBoxLayout,
|
|
18
|
+
QInputDialog,
|
|
19
|
+
QLabel,
|
|
20
|
+
QLineEdit,
|
|
21
|
+
QListWidget,
|
|
22
|
+
QListWidgetItem,
|
|
23
|
+
QMessageBox,
|
|
24
|
+
QPushButton,
|
|
25
|
+
QVBoxLayout,
|
|
26
|
+
QWidget,
|
|
27
|
+
QStyle,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Self-contained so the Login window stays readable regardless of the main window theme.
|
|
31
|
+
_LOGIN_DIALOG_STYLESHEET = """
|
|
32
|
+
QDialog#SessionLoginDialog {
|
|
33
|
+
background-color: #ffffff;
|
|
34
|
+
}
|
|
35
|
+
QDialog#SessionLoginDialog QLabel {
|
|
36
|
+
color: #0f172a;
|
|
37
|
+
background-color: transparent;
|
|
38
|
+
}
|
|
39
|
+
QDialog#SessionLoginDialog QLabel#LoginDialogIntro {
|
|
40
|
+
color: #1e293b;
|
|
41
|
+
font-size: 13px;
|
|
42
|
+
}
|
|
43
|
+
QDialog#SessionLoginDialog QGroupBox {
|
|
44
|
+
color: #0f172a;
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
border: 1px solid #cbd5e1;
|
|
47
|
+
border-radius: 6px;
|
|
48
|
+
margin-top: 10px;
|
|
49
|
+
padding-top: 12px;
|
|
50
|
+
background-color: #f8fafc;
|
|
51
|
+
}
|
|
52
|
+
QDialog#SessionLoginDialog QGroupBox::title {
|
|
53
|
+
subcontrol-origin: margin;
|
|
54
|
+
left: 10px;
|
|
55
|
+
padding: 0 6px;
|
|
56
|
+
}
|
|
57
|
+
QDialog#SessionLoginDialog QLineEdit, QDialog#SessionLoginDialog QComboBox {
|
|
58
|
+
background-color: #ffffff;
|
|
59
|
+
color: #0f172a;
|
|
60
|
+
border: 1px solid #94a3b8;
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
padding: 6px 8px;
|
|
63
|
+
min-height: 20px;
|
|
64
|
+
}
|
|
65
|
+
QDialog#SessionLoginDialog QComboBox QAbstractItemView {
|
|
66
|
+
margin: 0px;
|
|
67
|
+
padding: 0px;
|
|
68
|
+
}
|
|
69
|
+
QDialog#SessionLoginDialog QComboBox QAbstractItemView::viewport {
|
|
70
|
+
background-color: #ffffff;
|
|
71
|
+
margin: 0px;
|
|
72
|
+
padding: 0px;
|
|
73
|
+
}
|
|
74
|
+
QDialog#SessionLoginDialog QComboBox QAbstractItemView::item {
|
|
75
|
+
min-height: 22px;
|
|
76
|
+
padding: 2px 6px;
|
|
77
|
+
}
|
|
78
|
+
QDialog#SessionLoginDialog QListWidget#SessionBookmarkList {
|
|
79
|
+
background-color: #f8fafc;
|
|
80
|
+
color: #0f172a;
|
|
81
|
+
border: 1px solid #cbd5e1;
|
|
82
|
+
border-radius: 4px;
|
|
83
|
+
padding: 4px;
|
|
84
|
+
font-size: 13px;
|
|
85
|
+
}
|
|
86
|
+
QDialog#SessionLoginDialog QListWidget#SessionBookmarkList::item {
|
|
87
|
+
padding: 6px;
|
|
88
|
+
color: #0f172a;
|
|
89
|
+
}
|
|
90
|
+
QDialog#SessionLoginDialog QListWidget#SessionBookmarkList::item:selected {
|
|
91
|
+
background-color: #dbeafe;
|
|
92
|
+
color: #0f172a;
|
|
93
|
+
}
|
|
94
|
+
QDialog#SessionLoginDialog QPushButton {
|
|
95
|
+
background-color: #f1f5f9;
|
|
96
|
+
color: #0f172a;
|
|
97
|
+
border: 1px solid #cbd5e1;
|
|
98
|
+
border-radius: 4px;
|
|
99
|
+
padding: 6px 12px;
|
|
100
|
+
}
|
|
101
|
+
QDialog#SessionLoginDialog QPushButton:hover {
|
|
102
|
+
background-color: #e2e8f0;
|
|
103
|
+
}
|
|
104
|
+
QDialog#SessionLoginDialog QDialogButtonBox QPushButton {
|
|
105
|
+
min-width: 72px;
|
|
106
|
+
}
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
_LOGIN_DIALOG_DARK_STYLESHEET = """
|
|
110
|
+
QDialog#SessionLoginDialog {
|
|
111
|
+
background-color: #0f172a;
|
|
112
|
+
color: #f8fafc;
|
|
113
|
+
}
|
|
114
|
+
QDialog#SessionLoginDialog QLabel {
|
|
115
|
+
color: #e2e8f0;
|
|
116
|
+
background-color: transparent;
|
|
117
|
+
}
|
|
118
|
+
QDialog#SessionLoginDialog QLabel#LoginDialogIntro {
|
|
119
|
+
color: #cbd5e1;
|
|
120
|
+
}
|
|
121
|
+
QDialog#SessionLoginDialog QGroupBox {
|
|
122
|
+
color: #f8fafc;
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
border: 1px solid #334155;
|
|
125
|
+
border-radius: 6px;
|
|
126
|
+
margin-top: 10px;
|
|
127
|
+
padding-top: 12px;
|
|
128
|
+
background-color: #111827;
|
|
129
|
+
}
|
|
130
|
+
QDialog#SessionLoginDialog QGroupBox::title {
|
|
131
|
+
subcontrol-origin: margin;
|
|
132
|
+
left: 10px;
|
|
133
|
+
padding: 0 6px;
|
|
134
|
+
color: #cbd5e1;
|
|
135
|
+
}
|
|
136
|
+
QDialog#SessionLoginDialog QLineEdit, QDialog#SessionLoginDialog QComboBox {
|
|
137
|
+
background-color: #0b1220;
|
|
138
|
+
color: #f8fafc;
|
|
139
|
+
border: 1px solid #334155;
|
|
140
|
+
border-radius: 4px;
|
|
141
|
+
padding: 6px 8px;
|
|
142
|
+
min-height: 20px;
|
|
143
|
+
}
|
|
144
|
+
QDialog#SessionLoginDialog QComboBox QAbstractItemView {
|
|
145
|
+
background-color: #0b1220;
|
|
146
|
+
color: #f8fafc;
|
|
147
|
+
border: 1px solid #334155;
|
|
148
|
+
selection-background-color: #1d4ed8;
|
|
149
|
+
selection-color: #ffffff;
|
|
150
|
+
margin: 0px;
|
|
151
|
+
padding: 0px;
|
|
152
|
+
}
|
|
153
|
+
QDialog#SessionLoginDialog QComboBox QAbstractItemView::viewport {
|
|
154
|
+
background-color: #0b1220;
|
|
155
|
+
margin: 0px;
|
|
156
|
+
padding: 0px;
|
|
157
|
+
}
|
|
158
|
+
QDialog#SessionLoginDialog QComboBox QAbstractItemView::item {
|
|
159
|
+
min-height: 22px;
|
|
160
|
+
padding: 2px 6px;
|
|
161
|
+
}
|
|
162
|
+
QDialog#SessionLoginDialog QListWidget#SessionBookmarkList {
|
|
163
|
+
background-color: #0b1220;
|
|
164
|
+
color: #f8fafc;
|
|
165
|
+
border: 1px solid #334155;
|
|
166
|
+
border-radius: 4px;
|
|
167
|
+
padding: 4px;
|
|
168
|
+
font-size: 13px;
|
|
169
|
+
}
|
|
170
|
+
QDialog#SessionLoginDialog QListWidget#SessionBookmarkList::item {
|
|
171
|
+
padding: 6px;
|
|
172
|
+
color: #f8fafc;
|
|
173
|
+
}
|
|
174
|
+
QDialog#SessionLoginDialog QListWidget#SessionBookmarkList::item:selected {
|
|
175
|
+
background-color: #1d4ed8;
|
|
176
|
+
color: #ffffff;
|
|
177
|
+
}
|
|
178
|
+
QDialog#SessionLoginDialog QPushButton {
|
|
179
|
+
background-color: #1e293b;
|
|
180
|
+
color: #f8fafc;
|
|
181
|
+
border: 1px solid #334155;
|
|
182
|
+
border-radius: 4px;
|
|
183
|
+
padding: 6px 12px;
|
|
184
|
+
}
|
|
185
|
+
QDialog#SessionLoginDialog QPushButton:hover {
|
|
186
|
+
background-color: #334155;
|
|
187
|
+
}
|
|
188
|
+
QDialog#SessionLoginDialog QDialogButtonBox QPushButton {
|
|
189
|
+
min-width: 72px;
|
|
190
|
+
}
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
from ..config import AppConfig
|
|
194
|
+
from .icon_utils import bookmark_icon_from_entry
|
|
195
|
+
from ..session import parse_user_at_host
|
|
196
|
+
from ..services.adb_devices import list_adb_devices
|
|
197
|
+
from ..services.commands import run_adb
|
|
198
|
+
from ..services.remote_clients import connect_ftp, connect_sftp
|
|
199
|
+
from .combo_utils import ExpandAllComboBox
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
from serial.tools import list_ports
|
|
203
|
+
except Exception:
|
|
204
|
+
list_ports = None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass
|
|
208
|
+
class SessionLoginOutcome:
|
|
209
|
+
kind: str = "" # adb | sftp | ftp
|
|
210
|
+
adb_serial: str = ""
|
|
211
|
+
adb_display_label: str = ""
|
|
212
|
+
sftp_transport: Any = None
|
|
213
|
+
sftp_client: Any = None
|
|
214
|
+
sftp_host: str = ""
|
|
215
|
+
sftp_user: str = ""
|
|
216
|
+
sftp_port: int = 22
|
|
217
|
+
sftp_password: str = ""
|
|
218
|
+
ftp_client: Any = None
|
|
219
|
+
ftp_host: str = ""
|
|
220
|
+
ftp_port: int = 21
|
|
221
|
+
ftp_user: str = ""
|
|
222
|
+
ftp_password: str = ""
|
|
223
|
+
serial_port: str = ""
|
|
224
|
+
serial_baud: str = "115200"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _bookmark_allowed_for_dialog(for_terminal: bool, kind: str) -> bool:
|
|
228
|
+
if for_terminal:
|
|
229
|
+
return kind in ("ssh", "adb", "serial", "local_cmd", "local_pwsh")
|
|
230
|
+
return kind in ("sftp", "ftp", "adb")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class SessionLoginDialog(QDialog):
|
|
234
|
+
def __init__(
|
|
235
|
+
self,
|
|
236
|
+
get_adb_path: Callable[[], str],
|
|
237
|
+
default_ssh_host: str,
|
|
238
|
+
preferred_adb_serial: str,
|
|
239
|
+
parent: Optional[QWidget] = None,
|
|
240
|
+
*,
|
|
241
|
+
for_terminal: bool = False,
|
|
242
|
+
initial_protocol: Optional[str] = None,
|
|
243
|
+
config: Optional[AppConfig] = None,
|
|
244
|
+
on_bookmarks_changed: Optional[Callable[[], None]] = None,
|
|
245
|
+
):
|
|
246
|
+
super().__init__(parent)
|
|
247
|
+
self._get_adb_path = get_adb_path
|
|
248
|
+
self._for_terminal = for_terminal
|
|
249
|
+
self._initial_protocol = initial_protocol
|
|
250
|
+
self._config = config
|
|
251
|
+
self._on_bookmarks_changed = on_bookmarks_changed
|
|
252
|
+
self._outcome: Optional[SessionLoginOutcome] = None
|
|
253
|
+
self._preferred_adb_serial = (preferred_adb_serial or "").strip()
|
|
254
|
+
self.setObjectName("SessionLoginDialog")
|
|
255
|
+
self.setWindowTitle(f"Connect — {APP_TITLE}")
|
|
256
|
+
self.setModal(True)
|
|
257
|
+
self.resize(720, 420)
|
|
258
|
+
self._build_ui(default_ssh_host, preferred_adb_serial)
|
|
259
|
+
dark = bool(getattr(self._config, "dark_theme", False))
|
|
260
|
+
self.setStyleSheet(_LOGIN_DIALOG_DARK_STYLESHEET if dark else _LOGIN_DIALOG_STYLESHEET)
|
|
261
|
+
|
|
262
|
+
def outcome(self) -> Optional[SessionLoginOutcome]:
|
|
263
|
+
return self._outcome
|
|
264
|
+
|
|
265
|
+
def showEvent(self, event: QShowEvent) -> None:
|
|
266
|
+
super().showEvent(event)
|
|
267
|
+
if hasattr(self, "device_combo") and getattr(self, "adb_box", None) and self.adb_box.isVisible():
|
|
268
|
+
cur_data = self.device_combo.currentData()
|
|
269
|
+
pref = str(cur_data).strip() if cur_data is not None else ""
|
|
270
|
+
self._fill_device_combo(pref or self._preferred_adb_serial)
|
|
271
|
+
|
|
272
|
+
def _refresh_devices_clicked(self) -> None:
|
|
273
|
+
cur_data = self.device_combo.currentData()
|
|
274
|
+
pref = str(cur_data).strip() if cur_data is not None else ""
|
|
275
|
+
self._fill_device_combo(pref or self._preferred_adb_serial)
|
|
276
|
+
|
|
277
|
+
def _fill_device_combo(self, preferred_serial: str) -> None:
|
|
278
|
+
self.device_combo.clear()
|
|
279
|
+
pairs = list_adb_devices(self._get_adb_path())
|
|
280
|
+
if pairs:
|
|
281
|
+
for serial, display in pairs:
|
|
282
|
+
self.device_combo.addItem(display, serial)
|
|
283
|
+
if preferred_serial and serial == preferred_serial:
|
|
284
|
+
self.device_combo.setCurrentIndex(self.device_combo.count() - 1)
|
|
285
|
+
else:
|
|
286
|
+
code, _, _ = run_adb(self._get_adb_path(), ["devices"])
|
|
287
|
+
if code != 0:
|
|
288
|
+
self.device_combo.addItem("ADB not found — check Preferences")
|
|
289
|
+
else:
|
|
290
|
+
self.device_combo.addItem("No device — connect USB / enable ADB")
|
|
291
|
+
|
|
292
|
+
def _bookmark_list_data(self) -> List[Dict[str, Any]]:
|
|
293
|
+
if not self._config:
|
|
294
|
+
return []
|
|
295
|
+
return [
|
|
296
|
+
bm
|
|
297
|
+
for bm in self._config.session_bookmarks
|
|
298
|
+
if isinstance(bm, dict) and _bookmark_allowed_for_dialog(self._for_terminal, bm.get("kind", ""))
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
def _refresh_bookmark_list(self) -> None:
|
|
302
|
+
if not hasattr(self, "_bookmark_list"):
|
|
303
|
+
return
|
|
304
|
+
self._bookmark_list.clear()
|
|
305
|
+
for bm in self._bookmark_list_data():
|
|
306
|
+
it = QListWidgetItem(bm.get("name") or "Untitled")
|
|
307
|
+
it.setIcon(bookmark_icon_from_entry(bm, self))
|
|
308
|
+
it.setData(Qt.UserRole, bm)
|
|
309
|
+
self._bookmark_list.addItem(it)
|
|
310
|
+
self._bookmark_list.clearSelection()
|
|
311
|
+
|
|
312
|
+
def _load_bookmark_into_form(self, _item: Optional[QListWidgetItem] = None) -> None:
|
|
313
|
+
"""Apply the selected bookmark to the form (does not connect)."""
|
|
314
|
+
it = self._bookmark_list.currentItem()
|
|
315
|
+
if not it:
|
|
316
|
+
QMessageBox.information(self, "Bookmarks", "Select a saved session in the list first.")
|
|
317
|
+
return
|
|
318
|
+
bm = it.data(Qt.UserRole)
|
|
319
|
+
if not isinstance(bm, dict):
|
|
320
|
+
return
|
|
321
|
+
self._apply_bookmark_to_fields(bm)
|
|
322
|
+
|
|
323
|
+
def _on_bookmark_double_clicked(self, item: QListWidgetItem) -> None:
|
|
324
|
+
bm = item.data(Qt.UserRole)
|
|
325
|
+
if not isinstance(bm, dict):
|
|
326
|
+
return
|
|
327
|
+
self._bookmark_list.setCurrentItem(item)
|
|
328
|
+
self._connect_from_bookmark(bm)
|
|
329
|
+
|
|
330
|
+
def _connect_from_bookmark(self, bm: Dict[str, Any]) -> None:
|
|
331
|
+
"""Double-click: connect immediately using bookmark data (same as Login, without re-saving bookmark)."""
|
|
332
|
+
if self._for_terminal:
|
|
333
|
+
k = bm.get("kind")
|
|
334
|
+
if k == "local_cmd":
|
|
335
|
+
self._outcome = SessionLoginOutcome(kind="local_cmd")
|
|
336
|
+
self.accept()
|
|
337
|
+
return
|
|
338
|
+
if k == "local_pwsh":
|
|
339
|
+
self._outcome = SessionLoginOutcome(kind="local_pwsh")
|
|
340
|
+
self.accept()
|
|
341
|
+
return
|
|
342
|
+
self._apply_bookmark_to_fields(bm)
|
|
343
|
+
if not self._for_terminal:
|
|
344
|
+
k = bm.get("kind")
|
|
345
|
+
if k in ("sftp", "ftp") and not self.password_edit.text().strip():
|
|
346
|
+
pwd, ok = QInputDialog.getText(
|
|
347
|
+
self,
|
|
348
|
+
"Password",
|
|
349
|
+
"Password for this connection (not stored in saved bookmarks):",
|
|
350
|
+
QLineEdit.Password,
|
|
351
|
+
)
|
|
352
|
+
if not ok:
|
|
353
|
+
return
|
|
354
|
+
self.password_edit.setText(pwd)
|
|
355
|
+
self._try_login(skip_bookmark_save=True)
|
|
356
|
+
|
|
357
|
+
def _apply_bookmark_to_fields(self, bm: Dict[str, Any]) -> None:
|
|
358
|
+
k = bm.get("kind")
|
|
359
|
+
if k == "adb":
|
|
360
|
+
self.protocol_combo.setCurrentText("Android (ADB)")
|
|
361
|
+
serial = (bm.get("adb_serial") or "").strip()
|
|
362
|
+
for i in range(self.device_combo.count()):
|
|
363
|
+
if self.device_combo.itemData(i) == serial:
|
|
364
|
+
self.device_combo.setCurrentIndex(i)
|
|
365
|
+
break
|
|
366
|
+
else:
|
|
367
|
+
if serial:
|
|
368
|
+
self.device_combo.insertItem(0, bm.get("adb_label") or serial, serial)
|
|
369
|
+
self.device_combo.setCurrentIndex(0)
|
|
370
|
+
elif k == "ssh" and self._for_terminal:
|
|
371
|
+
self.protocol_combo.setCurrentText("SFTP")
|
|
372
|
+
self.host_edit.setText(bm.get("ssh_host", ""))
|
|
373
|
+
try:
|
|
374
|
+
p = int(bm.get("ssh_port") or 22)
|
|
375
|
+
except (TypeError, ValueError):
|
|
376
|
+
p = 22
|
|
377
|
+
self.port_edit.setText(str(p))
|
|
378
|
+
self.user_edit.setText(bm.get("ssh_user", ""))
|
|
379
|
+
self.password_edit.clear()
|
|
380
|
+
elif k == "sftp" and not self._for_terminal:
|
|
381
|
+
self.protocol_combo.setCurrentText("SFTP")
|
|
382
|
+
self.host_edit.setText(bm.get("sftp_host", bm.get("ssh_host", "")))
|
|
383
|
+
try:
|
|
384
|
+
p = int(bm.get("sftp_port", bm.get("ssh_port")) or 22)
|
|
385
|
+
except (TypeError, ValueError):
|
|
386
|
+
p = 22
|
|
387
|
+
self.port_edit.setText(str(p))
|
|
388
|
+
self.user_edit.setText(bm.get("sftp_user", bm.get("ssh_user", "")))
|
|
389
|
+
self.password_edit.clear()
|
|
390
|
+
elif k == "ftp" and not self._for_terminal:
|
|
391
|
+
self.protocol_combo.setCurrentText("FTP")
|
|
392
|
+
self.host_edit.setText(bm.get("ftp_host", ""))
|
|
393
|
+
try:
|
|
394
|
+
p = int(bm.get("ftp_port") or 21)
|
|
395
|
+
except (TypeError, ValueError):
|
|
396
|
+
p = 21
|
|
397
|
+
self.port_edit.setText(str(p))
|
|
398
|
+
self.user_edit.setText(bm.get("ftp_user", ""))
|
|
399
|
+
self.password_edit.clear()
|
|
400
|
+
elif k == "serial" and self._for_terminal:
|
|
401
|
+
self.protocol_combo.setCurrentText("Serial")
|
|
402
|
+
serial_port = (bm.get("serial_port") or "").strip() or "COM3"
|
|
403
|
+
idx = self.serial_port_combo.findText(serial_port)
|
|
404
|
+
if idx < 0:
|
|
405
|
+
self.serial_port_combo.addItem(serial_port)
|
|
406
|
+
idx = self.serial_port_combo.findText(serial_port)
|
|
407
|
+
if idx >= 0:
|
|
408
|
+
self.serial_port_combo.setCurrentIndex(idx)
|
|
409
|
+
baud = (bm.get("serial_baud") or "").strip() or "115200"
|
|
410
|
+
idxb = self.serial_baud_combo.findText(baud)
|
|
411
|
+
if idxb >= 0:
|
|
412
|
+
self.serial_baud_combo.setCurrentIndex(idxb)
|
|
413
|
+
|
|
414
|
+
def _remove_selected_bookmark(self) -> None:
|
|
415
|
+
if not self._config:
|
|
416
|
+
return
|
|
417
|
+
selected = [it for it in self._bookmark_list.selectedItems() if isinstance(it.data(Qt.UserRole), dict)]
|
|
418
|
+
if not selected:
|
|
419
|
+
return
|
|
420
|
+
names = {str(it.data(Qt.UserRole).get("name", "")).strip() for it in selected}
|
|
421
|
+
names.discard("")
|
|
422
|
+
if not names:
|
|
423
|
+
return
|
|
424
|
+
self._config.session_bookmarks = [
|
|
425
|
+
x
|
|
426
|
+
for x in self._config.session_bookmarks
|
|
427
|
+
if not (isinstance(x, dict) and str(x.get("name", "")).strip() in names)
|
|
428
|
+
]
|
|
429
|
+
self._config.save()
|
|
430
|
+
self._refresh_bookmark_list()
|
|
431
|
+
if self._on_bookmarks_changed:
|
|
432
|
+
self._on_bookmarks_changed()
|
|
433
|
+
|
|
434
|
+
def _upsert_bookmark(self, bm: Dict[str, Any]) -> None:
|
|
435
|
+
if not self._config:
|
|
436
|
+
return
|
|
437
|
+
name = (bm.get("name") or "").strip()
|
|
438
|
+
if not name:
|
|
439
|
+
return
|
|
440
|
+
lst = self._config.session_bookmarks
|
|
441
|
+
replaced = False
|
|
442
|
+
for i, x in enumerate(lst):
|
|
443
|
+
if isinstance(x, dict) and x.get("name") == name:
|
|
444
|
+
lst[i] = bm
|
|
445
|
+
replaced = True
|
|
446
|
+
break
|
|
447
|
+
if not replaced:
|
|
448
|
+
lst.append(bm)
|
|
449
|
+
self._config.save()
|
|
450
|
+
self._refresh_bookmark_list()
|
|
451
|
+
if self._on_bookmarks_changed:
|
|
452
|
+
self._on_bookmarks_changed()
|
|
453
|
+
|
|
454
|
+
def _maybe_save_bookmark(
|
|
455
|
+
self,
|
|
456
|
+
kind: str,
|
|
457
|
+
payload: Dict[str, Any],
|
|
458
|
+
) -> None:
|
|
459
|
+
if not self._config:
|
|
460
|
+
return
|
|
461
|
+
if not getattr(self, "save_bookmark_cb", None) or not self.save_bookmark_cb.isChecked():
|
|
462
|
+
return
|
|
463
|
+
name = self.bookmark_name_edit.text().strip()
|
|
464
|
+
if not name and kind == "adb":
|
|
465
|
+
serial = str(payload.get("adb_serial") or "").strip()
|
|
466
|
+
if serial:
|
|
467
|
+
name = f"ADB {serial}"
|
|
468
|
+
if not name:
|
|
469
|
+
return
|
|
470
|
+
bm: Dict[str, Any] = {"name": name, "kind": kind}
|
|
471
|
+
bm.update(payload)
|
|
472
|
+
icon_key = self.bookmark_icon_combo.currentData()
|
|
473
|
+
if icon_key:
|
|
474
|
+
bm["icon"] = str(icon_key)
|
|
475
|
+
else:
|
|
476
|
+
bm.pop("icon", None)
|
|
477
|
+
self._upsert_bookmark(bm)
|
|
478
|
+
|
|
479
|
+
def _build_ui(self, default_ssh_host: str, preferred_adb_serial: str) -> None:
|
|
480
|
+
root = QVBoxLayout(self)
|
|
481
|
+
|
|
482
|
+
outer = QHBoxLayout()
|
|
483
|
+
if self._config is not None:
|
|
484
|
+
left = QVBoxLayout()
|
|
485
|
+
lbl = QLabel("Saved sessions")
|
|
486
|
+
lbl.setObjectName("MobaSidebarHeading")
|
|
487
|
+
left.addWidget(lbl)
|
|
488
|
+
self._bookmark_list = QListWidget()
|
|
489
|
+
self._bookmark_list.setObjectName("SessionBookmarkList")
|
|
490
|
+
self._bookmark_list.setIconSize(QSize(20, 20))
|
|
491
|
+
self._bookmark_list.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
492
|
+
self._refresh_bookmark_list()
|
|
493
|
+
self._bookmark_list.clearSelection()
|
|
494
|
+
self._bookmark_list.itemDoubleClicked.connect(self._on_bookmark_double_clicked)
|
|
495
|
+
left.addWidget(self._bookmark_list, 1)
|
|
496
|
+
load_btn = QPushButton("Load into form")
|
|
497
|
+
load_btn.setIcon(self.style().standardIcon(QStyle.SP_DialogOpenButton))
|
|
498
|
+
load_btn.setToolTip(
|
|
499
|
+
"Copy the selected bookmark into the fields on the right. Double-click a bookmark to connect immediately."
|
|
500
|
+
)
|
|
501
|
+
load_btn.clicked.connect(self._load_bookmark_into_form)
|
|
502
|
+
left.addWidget(load_btn)
|
|
503
|
+
rm = QPushButton("Remove selected")
|
|
504
|
+
rm.setIcon(self.style().standardIcon(QStyle.SP_TrashIcon))
|
|
505
|
+
rm.clicked.connect(self._remove_selected_bookmark)
|
|
506
|
+
left.addWidget(rm)
|
|
507
|
+
outer.addLayout(left)
|
|
508
|
+
|
|
509
|
+
form_host = QVBoxLayout()
|
|
510
|
+
if self._for_terminal:
|
|
511
|
+
intro = QLabel(
|
|
512
|
+
"Choose SFTP, Android (ADB), or Serial. Enter connection details, or pick a bookmark on the left. "
|
|
513
|
+
"Double-click a bookmark to connect; use Load into form to edit fields first."
|
|
514
|
+
)
|
|
515
|
+
intro.setObjectName("LoginDialogIntro")
|
|
516
|
+
intro.setWordWrap(True)
|
|
517
|
+
form_host.addWidget(intro)
|
|
518
|
+
else:
|
|
519
|
+
intro2 = QLabel(
|
|
520
|
+
"Connect to SFTP, FTP, or Android (ADB). "
|
|
521
|
+
"Double-click a bookmark to connect; use Load into form to copy details into the fields first."
|
|
522
|
+
)
|
|
523
|
+
intro2.setObjectName("LoginDialogIntro")
|
|
524
|
+
intro2.setWordWrap(True)
|
|
525
|
+
form_host.addWidget(intro2)
|
|
526
|
+
|
|
527
|
+
self.protocol_combo = ExpandAllComboBox()
|
|
528
|
+
self.protocol_combo.setMaxVisibleItems(12)
|
|
529
|
+
if self._for_terminal:
|
|
530
|
+
self.protocol_combo.addItems(["SFTP", "Android (ADB)", "Serial"])
|
|
531
|
+
else:
|
|
532
|
+
self.protocol_combo.addItems(["SFTP", "FTP", "Android (ADB)"])
|
|
533
|
+
self.protocol_combo.currentIndexChanged.connect(self._on_protocol_changed)
|
|
534
|
+
form_host.addWidget(self.protocol_combo)
|
|
535
|
+
|
|
536
|
+
self.network_box = QGroupBox("Session")
|
|
537
|
+
net_form = QFormLayout(self.network_box)
|
|
538
|
+
du, dh = parse_user_at_host(default_ssh_host)
|
|
539
|
+
self.host_edit = QLineEdit(dh)
|
|
540
|
+
self.host_edit.setPlaceholderText("Host name")
|
|
541
|
+
self.port_edit = QLineEdit("22")
|
|
542
|
+
self.port_edit.setMaximumWidth(72)
|
|
543
|
+
self.user_edit = QLineEdit(du)
|
|
544
|
+
self.user_edit.setPlaceholderText("User name")
|
|
545
|
+
self.password_edit = QLineEdit()
|
|
546
|
+
self.password_edit.setEchoMode(QLineEdit.Password)
|
|
547
|
+
self.password_edit.setPlaceholderText("Password (optional)")
|
|
548
|
+
net_form.addRow("Host name:", self.host_edit)
|
|
549
|
+
net_form.addRow("Port number:", self.port_edit)
|
|
550
|
+
net_form.addRow("User name:", self.user_edit)
|
|
551
|
+
net_form.addRow("Password:", self.password_edit)
|
|
552
|
+
form_host.addWidget(self.network_box)
|
|
553
|
+
|
|
554
|
+
self.adb_box = QGroupBox("Android device")
|
|
555
|
+
adb_l = QVBoxLayout(self.adb_box)
|
|
556
|
+
self.device_combo = ExpandAllComboBox()
|
|
557
|
+
self.device_combo.setMaxVisibleItems(20)
|
|
558
|
+
self.device_combo.setMinimumWidth(320)
|
|
559
|
+
self._fill_device_combo(preferred_adb_serial)
|
|
560
|
+
adb_row = QHBoxLayout()
|
|
561
|
+
adb_row.addWidget(self.device_combo, 1)
|
|
562
|
+
ref_dev = QPushButton("Refresh devices")
|
|
563
|
+
ref_dev.setToolTip("Run adb again (use after plugging in USB or authorizing debugging).")
|
|
564
|
+
ref_dev.clicked.connect(self._refresh_devices_clicked)
|
|
565
|
+
adb_row.addWidget(ref_dev)
|
|
566
|
+
adb_l.addLayout(adb_row)
|
|
567
|
+
adb_hint = QLabel(
|
|
568
|
+
"Device name comes from adb (model). Each tab can use a different device."
|
|
569
|
+
if not self._for_terminal
|
|
570
|
+
else "Device list shows name · serial. Login starts adb shell only."
|
|
571
|
+
)
|
|
572
|
+
adb_hint.setWordWrap(True)
|
|
573
|
+
adb_l.addWidget(adb_hint)
|
|
574
|
+
form_host.addWidget(self.adb_box)
|
|
575
|
+
|
|
576
|
+
self.serial_box = QGroupBox("Serial terminal")
|
|
577
|
+
serial_form = QFormLayout(self.serial_box)
|
|
578
|
+
self.serial_port_combo = ExpandAllComboBox()
|
|
579
|
+
self.serial_port_combo.setEditable(True)
|
|
580
|
+
ports = []
|
|
581
|
+
if list_ports is not None:
|
|
582
|
+
try:
|
|
583
|
+
ports = sorted([p.device for p in list_ports.comports()])
|
|
584
|
+
except Exception:
|
|
585
|
+
ports = []
|
|
586
|
+
if ports:
|
|
587
|
+
self.serial_port_combo.addItems(ports)
|
|
588
|
+
else:
|
|
589
|
+
self.serial_port_combo.addItem("COM3")
|
|
590
|
+
default_port = (self._config.default_serial_port if self._config else "") or "COM3"
|
|
591
|
+
idxp = self.serial_port_combo.findText(default_port)
|
|
592
|
+
if idxp < 0:
|
|
593
|
+
self.serial_port_combo.addItem(default_port)
|
|
594
|
+
idxp = self.serial_port_combo.findText(default_port)
|
|
595
|
+
self.serial_port_combo.setCurrentIndex(max(0, idxp))
|
|
596
|
+
|
|
597
|
+
self.serial_baud_combo = ExpandAllComboBox()
|
|
598
|
+
self.serial_baud_combo.setEditable(True)
|
|
599
|
+
self.serial_baud_combo.addItems(["9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600"])
|
|
600
|
+
default_baud = (self._config.default_serial_baud if self._config else "") or "115200"
|
|
601
|
+
idxb = self.serial_baud_combo.findText(default_baud)
|
|
602
|
+
if idxb < 0:
|
|
603
|
+
self.serial_baud_combo.addItem(default_baud)
|
|
604
|
+
idxb = self.serial_baud_combo.findText(default_baud)
|
|
605
|
+
self.serial_baud_combo.setCurrentIndex(max(0, idxb))
|
|
606
|
+
|
|
607
|
+
serial_form.addRow("COM port:", self.serial_port_combo)
|
|
608
|
+
serial_form.addRow("Baud rate:", self.serial_baud_combo)
|
|
609
|
+
form_host.addWidget(self.serial_box)
|
|
610
|
+
|
|
611
|
+
self.save_bookmark_cb = QCheckBox("Save this connection as bookmark")
|
|
612
|
+
self.save_bookmark_cb.setChecked(False)
|
|
613
|
+
form_host.addWidget(self.save_bookmark_cb)
|
|
614
|
+
self.bookmark_name_edit = QLineEdit()
|
|
615
|
+
self.bookmark_name_edit.setPlaceholderText("Bookmark name")
|
|
616
|
+
form_host.addWidget(self.bookmark_name_edit)
|
|
617
|
+
self._bookmark_icon_lbl = QLabel("Bookmark icon:")
|
|
618
|
+
form_host.addWidget(self._bookmark_icon_lbl)
|
|
619
|
+
self.bookmark_icon_combo = ExpandAllComboBox()
|
|
620
|
+
self.bookmark_icon_combo.setMaxVisibleItems(12)
|
|
621
|
+
self.bookmark_icon_combo.addItem("Auto (from session type)", "")
|
|
622
|
+
self.bookmark_icon_combo.addItem("SSH / network", "ssh")
|
|
623
|
+
self.bookmark_icon_combo.addItem("ADB / device", "adb")
|
|
624
|
+
self.bookmark_icon_combo.addItem("Command Prompt", "local_cmd")
|
|
625
|
+
self.bookmark_icon_combo.addItem("PowerShell", "local_pwsh")
|
|
626
|
+
self.bookmark_icon_combo.addItem("Serial", "serial")
|
|
627
|
+
self.bookmark_icon_combo.addItem("SFTP / files", "sftp")
|
|
628
|
+
self.bookmark_icon_combo.addItem("FTP", "ftp")
|
|
629
|
+
form_host.addWidget(self.bookmark_icon_combo)
|
|
630
|
+
self.save_bookmark_cb.toggled.connect(self.bookmark_name_edit.setEnabled)
|
|
631
|
+
self.save_bookmark_cb.toggled.connect(self.bookmark_icon_combo.setEnabled)
|
|
632
|
+
self.save_bookmark_cb.toggled.connect(self._bookmark_icon_lbl.setEnabled)
|
|
633
|
+
self.bookmark_name_edit.setEnabled(False)
|
|
634
|
+
self.bookmark_icon_combo.setEnabled(False)
|
|
635
|
+
self._bookmark_icon_lbl.setEnabled(False)
|
|
636
|
+
|
|
637
|
+
outer.addLayout(form_host, 1)
|
|
638
|
+
root.addLayout(outer)
|
|
639
|
+
|
|
640
|
+
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
641
|
+
buttons.accepted.connect(self._try_login)
|
|
642
|
+
buttons.rejected.connect(self.reject)
|
|
643
|
+
self._login_btn = buttons.button(QDialogButtonBox.Ok)
|
|
644
|
+
self._login_btn.setText("Login")
|
|
645
|
+
self._login_btn.setIcon(self.style().standardIcon(QStyle.SP_DialogApplyButton))
|
|
646
|
+
cancel_btn = buttons.button(QDialogButtonBox.Cancel)
|
|
647
|
+
if cancel_btn is not None:
|
|
648
|
+
cancel_btn.setIcon(self.style().standardIcon(QStyle.SP_DialogCancelButton))
|
|
649
|
+
root.addWidget(buttons)
|
|
650
|
+
|
|
651
|
+
self._on_protocol_changed(0)
|
|
652
|
+
if self._initial_protocol:
|
|
653
|
+
idx = self.protocol_combo.findText(self._initial_protocol)
|
|
654
|
+
if idx >= 0:
|
|
655
|
+
self.protocol_combo.setCurrentIndex(idx)
|
|
656
|
+
|
|
657
|
+
def _on_protocol_changed(self, _index: int) -> None:
|
|
658
|
+
proto = self.protocol_combo.currentText()
|
|
659
|
+
is_adb = proto == "Android (ADB)"
|
|
660
|
+
is_serial = proto == "Serial"
|
|
661
|
+
is_ftp = proto == "FTP"
|
|
662
|
+
self.network_box.setVisible((not is_adb) and (not is_serial))
|
|
663
|
+
self.adb_box.setVisible(is_adb)
|
|
664
|
+
self.serial_box.setVisible(is_serial)
|
|
665
|
+
if (not is_adb) and (not is_serial):
|
|
666
|
+
self.port_edit.setText("21" if is_ftp else "22")
|
|
667
|
+
|
|
668
|
+
def _serial_from_device_selection(self) -> str:
|
|
669
|
+
data = self.device_combo.currentData()
|
|
670
|
+
if data is not None:
|
|
671
|
+
return str(data).strip()
|
|
672
|
+
t = self.device_combo.currentText().strip()
|
|
673
|
+
return t.split()[0] if t else ""
|
|
674
|
+
|
|
675
|
+
def _try_login(self, skip_bookmark_save: bool = False) -> None:
|
|
676
|
+
proto = self.protocol_combo.currentText()
|
|
677
|
+
if proto == "Android (ADB)":
|
|
678
|
+
text = self.device_combo.currentText().strip()
|
|
679
|
+
if not text or "No device" in text or "not found" in text.lower() or "ADB not found" in text:
|
|
680
|
+
QMessageBox.warning(self, "Login", "Select a device, or fix ADB (USB / Preferences).")
|
|
681
|
+
return
|
|
682
|
+
serial = self._serial_from_device_selection()
|
|
683
|
+
if not serial:
|
|
684
|
+
QMessageBox.warning(self, "Login", "Could not read device serial.")
|
|
685
|
+
return
|
|
686
|
+
self._outcome = SessionLoginOutcome(
|
|
687
|
+
kind="adb",
|
|
688
|
+
adb_serial=serial,
|
|
689
|
+
adb_display_label=text,
|
|
690
|
+
)
|
|
691
|
+
if not skip_bookmark_save:
|
|
692
|
+
self._maybe_save_bookmark(
|
|
693
|
+
"adb",
|
|
694
|
+
{"adb_serial": serial, "adb_label": text},
|
|
695
|
+
)
|
|
696
|
+
self.accept()
|
|
697
|
+
return
|
|
698
|
+
|
|
699
|
+
if proto == "Serial":
|
|
700
|
+
port = (self.serial_port_combo.currentText() or "").strip() or "COM3"
|
|
701
|
+
baud = (self.serial_baud_combo.currentText() or "").strip() or "115200"
|
|
702
|
+
self._outcome = SessionLoginOutcome(
|
|
703
|
+
kind="serial",
|
|
704
|
+
serial_port=port,
|
|
705
|
+
serial_baud=baud,
|
|
706
|
+
)
|
|
707
|
+
if not skip_bookmark_save:
|
|
708
|
+
self._maybe_save_bookmark(
|
|
709
|
+
"serial",
|
|
710
|
+
{"serial_port": port, "serial_baud": baud},
|
|
711
|
+
)
|
|
712
|
+
self.accept()
|
|
713
|
+
return
|
|
714
|
+
|
|
715
|
+
host = self.host_edit.text().strip()
|
|
716
|
+
if not host:
|
|
717
|
+
QMessageBox.warning(self, "Login", "Enter host name.")
|
|
718
|
+
return
|
|
719
|
+
try:
|
|
720
|
+
port = int(self.port_edit.text().strip() or ("21" if proto == "FTP" else "22"))
|
|
721
|
+
except ValueError:
|
|
722
|
+
port = 21 if proto == "FTP" else 22
|
|
723
|
+
user = self.user_edit.text().strip()
|
|
724
|
+
password = self.password_edit.text()
|
|
725
|
+
|
|
726
|
+
if proto == "SFTP":
|
|
727
|
+
if self._for_terminal:
|
|
728
|
+
self._outcome = SessionLoginOutcome(
|
|
729
|
+
kind="sftp",
|
|
730
|
+
sftp_transport=None,
|
|
731
|
+
sftp_client=None,
|
|
732
|
+
sftp_host=host,
|
|
733
|
+
sftp_user=user,
|
|
734
|
+
sftp_port=port,
|
|
735
|
+
sftp_password=password,
|
|
736
|
+
)
|
|
737
|
+
if not skip_bookmark_save:
|
|
738
|
+
self._maybe_save_bookmark(
|
|
739
|
+
"ssh",
|
|
740
|
+
{
|
|
741
|
+
"ssh_host": host,
|
|
742
|
+
"ssh_user": user,
|
|
743
|
+
"ssh_port": port,
|
|
744
|
+
},
|
|
745
|
+
)
|
|
746
|
+
self.accept()
|
|
747
|
+
return
|
|
748
|
+
t, sftp, err = connect_sftp(host, port, user, password)
|
|
749
|
+
if err or sftp is None:
|
|
750
|
+
QMessageBox.warning(self, "SFTP", err or "Connection failed.")
|
|
751
|
+
return
|
|
752
|
+
self._outcome = SessionLoginOutcome(
|
|
753
|
+
kind="sftp",
|
|
754
|
+
sftp_transport=t,
|
|
755
|
+
sftp_client=sftp,
|
|
756
|
+
sftp_host=host,
|
|
757
|
+
sftp_user=user,
|
|
758
|
+
sftp_port=port,
|
|
759
|
+
sftp_password=password,
|
|
760
|
+
)
|
|
761
|
+
if not skip_bookmark_save:
|
|
762
|
+
self._maybe_save_bookmark(
|
|
763
|
+
"sftp",
|
|
764
|
+
{
|
|
765
|
+
"sftp_host": host,
|
|
766
|
+
"sftp_user": user,
|
|
767
|
+
"sftp_port": port,
|
|
768
|
+
},
|
|
769
|
+
)
|
|
770
|
+
self.accept()
|
|
771
|
+
return
|
|
772
|
+
|
|
773
|
+
if proto == "FTP":
|
|
774
|
+
ftp, err = connect_ftp(host, port, user, password)
|
|
775
|
+
if err or ftp is None:
|
|
776
|
+
QMessageBox.warning(self, "FTP", err or "Connection failed.")
|
|
777
|
+
return
|
|
778
|
+
self._outcome = SessionLoginOutcome(
|
|
779
|
+
kind="ftp",
|
|
780
|
+
ftp_client=ftp,
|
|
781
|
+
ftp_host=host,
|
|
782
|
+
ftp_port=port,
|
|
783
|
+
ftp_user=user,
|
|
784
|
+
ftp_password=password,
|
|
785
|
+
)
|
|
786
|
+
if not skip_bookmark_save:
|
|
787
|
+
self._maybe_save_bookmark(
|
|
788
|
+
"ftp",
|
|
789
|
+
{
|
|
790
|
+
"ftp_host": host,
|
|
791
|
+
"ftp_port": port,
|
|
792
|
+
"ftp_user": user,
|
|
793
|
+
},
|
|
794
|
+
)
|
|
795
|
+
self.accept()
|