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.
@@ -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()