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
devicedeck/session.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Unified connection profile (WinSCP-style protocol + Moba-style session types)."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Optional, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConnectionKind(str, Enum):
|
|
9
|
+
ANDROID_ADB = "adb"
|
|
10
|
+
SSH_SFTP = "ssh" # SFTP files + ssh terminal
|
|
11
|
+
FTP = "ftp"
|
|
12
|
+
SERIAL = "serial"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SessionProfile:
|
|
17
|
+
kind: ConnectionKind
|
|
18
|
+
adb_serial: str = ""
|
|
19
|
+
# SSH / SFTP
|
|
20
|
+
ssh_host: str = ""
|
|
21
|
+
ssh_user: str = ""
|
|
22
|
+
ssh_port: int = 22
|
|
23
|
+
ssh_password: str = ""
|
|
24
|
+
# FTP
|
|
25
|
+
ftp_host: str = ""
|
|
26
|
+
ftp_port: int = 21
|
|
27
|
+
ftp_user: str = ""
|
|
28
|
+
ftp_password: str = ""
|
|
29
|
+
# Serial
|
|
30
|
+
serial_port: str = ""
|
|
31
|
+
serial_baud: str = "115200"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_user_at_host(text: str) -> Tuple[str, str]:
|
|
35
|
+
"""Split user@host into (user, host). If no @, returns ("", stripped host)."""
|
|
36
|
+
t = (text or "").strip()
|
|
37
|
+
if not t:
|
|
38
|
+
return "", ""
|
|
39
|
+
if "@" in t:
|
|
40
|
+
u, h = t.split("@", 1)
|
|
41
|
+
return u.strip(), h.strip()
|
|
42
|
+
return "", t
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def ssh_command_args(profile: SessionProfile) -> list:
|
|
46
|
+
"""Build `ssh` argv for interactive terminal (OpenSSH client)."""
|
|
47
|
+
host = (profile.ssh_host or "").strip()
|
|
48
|
+
user = (profile.ssh_user or "").strip()
|
|
49
|
+
if not host:
|
|
50
|
+
return ["ssh"]
|
|
51
|
+
target = f"{user}@{host}" if user else host
|
|
52
|
+
return ["ssh", "-p", str(int(profile.ssh_port or 22)), target]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# UI package
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Application window icon — multi-resolution for crisp taskbar and title bars."""
|
|
2
|
+
|
|
3
|
+
from PyQt5.QtCore import Qt
|
|
4
|
+
from PyQt5.QtGui import QColor, QIcon, QPainter, QPixmap
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _render_icon_pixmap(size: int) -> QPixmap:
|
|
8
|
+
"""Rounded tile with a simple device glyph, scaled to ``size``."""
|
|
9
|
+
s = max(16, int(size))
|
|
10
|
+
pm = QPixmap(s, s)
|
|
11
|
+
pm.fill(Qt.transparent)
|
|
12
|
+
p = QPainter(pm)
|
|
13
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
14
|
+
p.setRenderHint(QPainter.SmoothPixmapTransform, True)
|
|
15
|
+
p.setPen(Qt.NoPen)
|
|
16
|
+
|
|
17
|
+
m = max(1, s // 10)
|
|
18
|
+
r = max(2, s // 5)
|
|
19
|
+
p.setBrush(QColor("#2563eb"))
|
|
20
|
+
p.drawRoundedRect(m, m, s - 2 * m, s - 2 * m, r, r)
|
|
21
|
+
|
|
22
|
+
inner_l = s // 4
|
|
23
|
+
inner_t = s // 5
|
|
24
|
+
inner_w = s - 2 * inner_l
|
|
25
|
+
inner_h = int(s * 0.55)
|
|
26
|
+
p.setBrush(QColor("#e2e8f0"))
|
|
27
|
+
p.drawRoundedRect(inner_l, inner_t, inner_w, inner_h, max(1, s // 32), max(1, s // 32))
|
|
28
|
+
|
|
29
|
+
p.setBrush(QColor("#2563eb"))
|
|
30
|
+
ex = s // 2 - s // 10
|
|
31
|
+
ey = inner_t + inner_h - s // 12
|
|
32
|
+
ew = max(2, s // 5)
|
|
33
|
+
eh = max(2, s // 12)
|
|
34
|
+
p.drawEllipse(ex, ey, ew, eh)
|
|
35
|
+
|
|
36
|
+
p.end()
|
|
37
|
+
return pm
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_app_icon() -> QIcon:
|
|
41
|
+
"""Icons for taskbar, alt-tab, and window title — several sizes for HiDPI."""
|
|
42
|
+
icon = QIcon()
|
|
43
|
+
for size in (16, 20, 24, 32, 40, 48, 64, 96, 128, 256):
|
|
44
|
+
pm = _render_icon_pixmap(size)
|
|
45
|
+
icon.addPixmap(pm)
|
|
46
|
+
return icon
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from PyQt5.QtCore import Qt
|
|
2
|
+
from PyQt5.QtGui import QColor, QPainter, QPen
|
|
3
|
+
from PyQt5.QtWidgets import QComboBox
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ExpandAllComboBox(QComboBox):
|
|
7
|
+
"""Combobox that expands to show all options in one popup using native look."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, *args, **kwargs):
|
|
10
|
+
super().__init__(*args, **kwargs)
|
|
11
|
+
self.setSizeAdjustPolicy(QComboBox.AdjustToContents)
|
|
12
|
+
|
|
13
|
+
def showPopup(self) -> None:
|
|
14
|
+
rows = max(1, self.count())
|
|
15
|
+
self.setMaxVisibleItems(rows)
|
|
16
|
+
view = self.view()
|
|
17
|
+
# Use font metrics for predictable performance when opening popup.
|
|
18
|
+
row_h = max(self.fontMetrics().height() + 10, 24)
|
|
19
|
+
frame = view.frameWidth() * 2
|
|
20
|
+
popup_h = rows * row_h + frame + 2
|
|
21
|
+
fm = self.fontMetrics()
|
|
22
|
+
max_w = self.width()
|
|
23
|
+
for i in range(self.count()):
|
|
24
|
+
text_w = fm.horizontalAdvance(self.itemText(i)) + 56
|
|
25
|
+
if text_w > max_w:
|
|
26
|
+
max_w = text_w
|
|
27
|
+
view.setMinimumWidth(max_w)
|
|
28
|
+
view.setMinimumHeight(popup_h)
|
|
29
|
+
view.setMaximumHeight(popup_h)
|
|
30
|
+
super().showPopup()
|
|
31
|
+
|
|
32
|
+
def paintEvent(self, event) -> None:
|
|
33
|
+
super().paintEvent(event)
|
|
34
|
+
# Draw a high-contrast chevron so dropdown arrow stays visible in all themes.
|
|
35
|
+
p = QPainter(self)
|
|
36
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
37
|
+
c = self.palette().color(self.foregroundRole())
|
|
38
|
+
if not self.isEnabled():
|
|
39
|
+
c = QColor(c.red(), c.green(), c.blue(), 130)
|
|
40
|
+
p.setPen(QPen(c, 1.8, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
|
|
41
|
+
x = self.rect().right() - 12
|
|
42
|
+
y = self.rect().center().y()
|
|
43
|
+
p.drawLine(x - 4, y - 2, x, y + 2)
|
|
44
|
+
p.drawLine(x, y + 2, x + 4, y - 2)
|
|
45
|
+
p.end()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Shown once on a fresh install before ~/.devicedeck.json exists."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from PyQt5.QtCore import Qt
|
|
6
|
+
from PyQt5.QtWidgets import (
|
|
7
|
+
QApplication,
|
|
8
|
+
QDialog,
|
|
9
|
+
QDialogButtonBox,
|
|
10
|
+
QFileDialog,
|
|
11
|
+
QFormLayout,
|
|
12
|
+
QGroupBox,
|
|
13
|
+
QHBoxLayout,
|
|
14
|
+
QLabel,
|
|
15
|
+
QLineEdit,
|
|
16
|
+
QMessageBox,
|
|
17
|
+
QPushButton,
|
|
18
|
+
QRadioButton,
|
|
19
|
+
QVBoxLayout,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from ..config import AppConfig
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FirstRunDialog(QDialog):
|
|
26
|
+
def __init__(self, config: AppConfig, parent=None):
|
|
27
|
+
super().__init__(parent)
|
|
28
|
+
self._config = config
|
|
29
|
+
self.setWindowTitle("Welcome — set up DeviceDeck")
|
|
30
|
+
self.setModal(True)
|
|
31
|
+
app = QApplication.instance()
|
|
32
|
+
if app is not None:
|
|
33
|
+
self.setWindowIcon(app.windowIcon())
|
|
34
|
+
self.resize(520, 420)
|
|
35
|
+
self._build_ui()
|
|
36
|
+
|
|
37
|
+
def _build_ui(self) -> None:
|
|
38
|
+
root = QVBoxLayout(self)
|
|
39
|
+
intro = QLabel(
|
|
40
|
+
"<p><b>First run on this computer.</b></p>"
|
|
41
|
+
"<p>Choose a theme and optional tool paths. You can change everything later under "
|
|
42
|
+
"<b>File → Preferences</b>.</p>"
|
|
43
|
+
)
|
|
44
|
+
intro.setWordWrap(True)
|
|
45
|
+
intro.setOpenExternalLinks(False)
|
|
46
|
+
root.addWidget(intro)
|
|
47
|
+
|
|
48
|
+
theme_box = QGroupBox("Theme")
|
|
49
|
+
tb = QVBoxLayout(theme_box)
|
|
50
|
+
self._radio_light = QRadioButton("Light")
|
|
51
|
+
self._radio_dark = QRadioButton("Dark")
|
|
52
|
+
self._radio_light.setChecked(not bool(getattr(self._config, "dark_theme", False)))
|
|
53
|
+
self._radio_dark.setChecked(bool(getattr(self._config, "dark_theme", False)))
|
|
54
|
+
tb.addWidget(self._radio_light)
|
|
55
|
+
tb.addWidget(self._radio_dark)
|
|
56
|
+
root.addWidget(theme_box)
|
|
57
|
+
|
|
58
|
+
paths = QGroupBox("Tools (optional — leave empty to use PATH)")
|
|
59
|
+
pf = QFormLayout(paths)
|
|
60
|
+
row_adb = QHBoxLayout()
|
|
61
|
+
self.adb_edit = QLineEdit(self._config.adb_path)
|
|
62
|
+
self.adb_edit.setPlaceholderText("adb")
|
|
63
|
+
btn_adb = QPushButton("Browse…")
|
|
64
|
+
btn_adb.clicked.connect(self._browse_adb)
|
|
65
|
+
row_adb.addWidget(self.adb_edit, 1)
|
|
66
|
+
row_adb.addWidget(btn_adb)
|
|
67
|
+
pf.addRow("ADB:", row_adb)
|
|
68
|
+
|
|
69
|
+
row_sc = QHBoxLayout()
|
|
70
|
+
self.scrcpy_edit = QLineEdit(self._config.scrcpy_path)
|
|
71
|
+
self.scrcpy_edit.setPlaceholderText("scrcpy")
|
|
72
|
+
btn_sc = QPushButton("Browse…")
|
|
73
|
+
btn_sc.clicked.connect(self._browse_scrcpy)
|
|
74
|
+
row_sc.addWidget(self.scrcpy_edit, 1)
|
|
75
|
+
row_sc.addWidget(btn_sc)
|
|
76
|
+
pf.addRow("scrcpy:", row_sc)
|
|
77
|
+
root.addWidget(paths)
|
|
78
|
+
|
|
79
|
+
root.addStretch(1)
|
|
80
|
+
|
|
81
|
+
buttons = QDialogButtonBox(QDialogButtonBox.Ok)
|
|
82
|
+
buttons.accepted.connect(self._save)
|
|
83
|
+
ok = buttons.button(QDialogButtonBox.Ok)
|
|
84
|
+
if ok is not None:
|
|
85
|
+
ok.setText("Continue")
|
|
86
|
+
root.addWidget(buttons)
|
|
87
|
+
|
|
88
|
+
def _browse_adb(self) -> None:
|
|
89
|
+
path, _ = QFileDialog.getOpenFileName(self, "Select adb", str(Path.home()))
|
|
90
|
+
if path:
|
|
91
|
+
self.adb_edit.setText(path)
|
|
92
|
+
|
|
93
|
+
def _browse_scrcpy(self) -> None:
|
|
94
|
+
path, _ = QFileDialog.getOpenFileName(self, "Select scrcpy", str(Path.home()))
|
|
95
|
+
if path:
|
|
96
|
+
self.scrcpy_edit.setText(path)
|
|
97
|
+
|
|
98
|
+
def _save(self) -> None:
|
|
99
|
+
self._config.dark_theme = self._radio_dark.isChecked()
|
|
100
|
+
self._config.adb_path = self.adb_edit.text().strip()
|
|
101
|
+
self._config.scrcpy_path = self.scrcpy_edit.text().strip()
|
|
102
|
+
try:
|
|
103
|
+
self._config.save()
|
|
104
|
+
except OSError as exc:
|
|
105
|
+
QMessageBox.warning(self, "Could not save", str(exc))
|
|
106
|
+
return
|
|
107
|
+
self.accept()
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Icons for bookmarks and local shell buttons (clear, recognizable glyphs)."""
|
|
2
|
+
|
|
3
|
+
from PyQt5.QtCore import QPoint, Qt
|
|
4
|
+
from PyQt5.QtGui import QColor, QFont, QIcon, QPainter, QPixmap
|
|
5
|
+
from PyQt5.QtWidgets import QStyle, QWidget
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def icon_windows_cmd_console() -> QIcon:
|
|
9
|
+
"""Dark console tile with >_ — distinct from PowerShell."""
|
|
10
|
+
pm = QPixmap(22, 22)
|
|
11
|
+
pm.fill(Qt.transparent)
|
|
12
|
+
p = QPainter(pm)
|
|
13
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
14
|
+
p.setPen(Qt.NoPen)
|
|
15
|
+
p.setBrush(QColor("#1e293b"))
|
|
16
|
+
p.drawRoundedRect(2, 2, 18, 18, 4, 4)
|
|
17
|
+
p.setPen(QColor("#94a3b8"))
|
|
18
|
+
p.drawRoundedRect(2, 2, 18, 18, 4, 4)
|
|
19
|
+
p.setPen(QColor("#f1f5f9"))
|
|
20
|
+
p.setFont(QFont("Consolas", 8, QFont.Bold))
|
|
21
|
+
p.drawText(4, 15, ">_")
|
|
22
|
+
p.end()
|
|
23
|
+
return QIcon(pm)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def icon_windows_powershell() -> QIcon:
|
|
27
|
+
"""PowerShell blue tile with PS monogram."""
|
|
28
|
+
pm = QPixmap(22, 22)
|
|
29
|
+
pm.fill(Qt.transparent)
|
|
30
|
+
p = QPainter(pm)
|
|
31
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
32
|
+
p.setPen(Qt.NoPen)
|
|
33
|
+
p.setBrush(QColor("#012456"))
|
|
34
|
+
p.drawRoundedRect(2, 2, 18, 18, 4, 4)
|
|
35
|
+
p.setPen(QColor("#38bdf8"))
|
|
36
|
+
p.drawRoundedRect(2, 2, 18, 18, 4, 4)
|
|
37
|
+
p.setPen(QColor("#f8fafc"))
|
|
38
|
+
p.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
|
39
|
+
p.drawText(5, 14, "PS")
|
|
40
|
+
p.end()
|
|
41
|
+
return QIcon(pm)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def icon_media_play_green() -> QIcon:
|
|
45
|
+
pm = QPixmap(22, 22)
|
|
46
|
+
pm.fill(Qt.transparent)
|
|
47
|
+
p = QPainter(pm)
|
|
48
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
49
|
+
p.setPen(Qt.NoPen)
|
|
50
|
+
p.setBrush(QColor("#16a34a"))
|
|
51
|
+
p.drawRoundedRect(2, 2, 18, 18, 4, 4)
|
|
52
|
+
p.setBrush(QColor("#f8fafc"))
|
|
53
|
+
p.drawPolygon(QPoint(8, 6), QPoint(8, 16), QPoint(16, 11))
|
|
54
|
+
p.end()
|
|
55
|
+
return QIcon(pm)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def icon_media_stop_red() -> QIcon:
|
|
59
|
+
pm = QPixmap(22, 22)
|
|
60
|
+
pm.fill(Qt.transparent)
|
|
61
|
+
p = QPainter(pm)
|
|
62
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
63
|
+
p.setPen(Qt.NoPen)
|
|
64
|
+
p.setBrush(QColor("#dc2626"))
|
|
65
|
+
p.drawRoundedRect(2, 2, 18, 18, 4, 4)
|
|
66
|
+
p.setBrush(QColor("#f8fafc"))
|
|
67
|
+
p.drawRect(7, 7, 8, 8)
|
|
68
|
+
p.end()
|
|
69
|
+
return QIcon(pm)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def icon_nav_up() -> QIcon:
|
|
73
|
+
pm = QPixmap(22, 22)
|
|
74
|
+
pm.fill(Qt.transparent)
|
|
75
|
+
p = QPainter(pm)
|
|
76
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
77
|
+
p.setPen(Qt.NoPen)
|
|
78
|
+
p.setBrush(QColor("#1e293b"))
|
|
79
|
+
p.drawRoundedRect(2, 2, 18, 18, 4, 4)
|
|
80
|
+
p.setPen(QColor("#93c5fd"))
|
|
81
|
+
p.setBrush(QColor("#93c5fd"))
|
|
82
|
+
p.drawPolygon(QPoint(11, 6), QPoint(6, 12), QPoint(9, 12), QPoint(9, 16), QPoint(13, 16), QPoint(13, 12), QPoint(16, 12))
|
|
83
|
+
p.end()
|
|
84
|
+
return QIcon(pm)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def icon_home_folder() -> QIcon:
|
|
88
|
+
pm = QPixmap(22, 22)
|
|
89
|
+
pm.fill(Qt.transparent)
|
|
90
|
+
p = QPainter(pm)
|
|
91
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
92
|
+
p.setPen(Qt.NoPen)
|
|
93
|
+
p.setBrush(QColor("#1e293b"))
|
|
94
|
+
p.drawRoundedRect(2, 2, 18, 18, 4, 4)
|
|
95
|
+
p.setBrush(QColor("#f8fafc"))
|
|
96
|
+
p.drawPolygon(QPoint(4, 11), QPoint(11, 5), QPoint(18, 11))
|
|
97
|
+
p.drawRoundedRect(6, 10, 10, 7, 1, 1)
|
|
98
|
+
p.setBrush(QColor("#1e293b"))
|
|
99
|
+
p.drawRoundedRect(10, 12, 2, 5, 1, 1)
|
|
100
|
+
p.end()
|
|
101
|
+
return QIcon(pm)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def icon_root_drive() -> QIcon:
|
|
105
|
+
pm = QPixmap(22, 22)
|
|
106
|
+
pm.fill(Qt.transparent)
|
|
107
|
+
p = QPainter(pm)
|
|
108
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
109
|
+
p.setPen(Qt.NoPen)
|
|
110
|
+
p.setBrush(QColor("#334155"))
|
|
111
|
+
p.drawRoundedRect(3, 6, 16, 10, 3, 3)
|
|
112
|
+
p.setBrush(QColor("#22c55e"))
|
|
113
|
+
p.drawEllipse(15, 12, 3, 3)
|
|
114
|
+
p.end()
|
|
115
|
+
return QIcon(pm)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def bookmark_icon_for_kind(kind: str, widget: QWidget) -> QIcon:
|
|
119
|
+
"""Map bookmark kind to a platform-standard icon."""
|
|
120
|
+
st = widget.style()
|
|
121
|
+
k = (kind or "").lower()
|
|
122
|
+
if k == "ssh":
|
|
123
|
+
return st.standardIcon(QStyle.SP_DriveNetIcon)
|
|
124
|
+
if k == "adb":
|
|
125
|
+
return st.standardIcon(QStyle.SP_ComputerIcon)
|
|
126
|
+
if k == "local_cmd":
|
|
127
|
+
return icon_windows_cmd_console()
|
|
128
|
+
if k == "local_pwsh":
|
|
129
|
+
return icon_windows_powershell()
|
|
130
|
+
if k == "serial":
|
|
131
|
+
return st.standardIcon(QStyle.SP_MessageBoxInformation)
|
|
132
|
+
if k in ("sftp", "ftp"):
|
|
133
|
+
return st.standardIcon(QStyle.SP_FileDialogStart)
|
|
134
|
+
return st.standardIcon(QStyle.SP_FileIcon)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def bookmark_icon_from_entry(bm: dict, widget: QWidget) -> QIcon:
|
|
138
|
+
"""Optional per-bookmark override: bm['icon'] = 'ssh'|'adb'|..."""
|
|
139
|
+
if isinstance(bm, dict):
|
|
140
|
+
override = (bm.get("icon") or "").strip().lower()
|
|
141
|
+
if override:
|
|
142
|
+
return bookmark_icon_for_kind(override, widget)
|
|
143
|
+
return bookmark_icon_for_kind(str(bm.get("kind", "")), widget)
|
|
144
|
+
return bookmark_icon_for_kind("", widget)
|