voidremote 1.0.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.
- voidremote/__init__.py +31 -0
- voidremote/adb/__init__.py +28 -0
- voidremote/adb/client.py +385 -0
- voidremote/adb/device_parser.py +286 -0
- voidremote/cli/__init__.py +5 -0
- voidremote/cli/main.py +987 -0
- voidremote/config/__init__.py +37 -0
- voidremote/config/settings.py +203 -0
- voidremote/controllers/__init__.py +5 -0
- voidremote/controllers/app_controller.py +222 -0
- voidremote/core/__init__.py +1 -0
- voidremote/core/automation.py +184 -0
- voidremote/models/__init__.py +31 -0
- voidremote/models/device.py +263 -0
- voidremote/network/__init__.py +1 -0
- voidremote/network/discovery.py +116 -0
- voidremote/services/__init__.py +13 -0
- voidremote/services/device_service.py +340 -0
- voidremote/services/input_service.py +181 -0
- voidremote/services/monitor_service.py +198 -0
- voidremote/ui/__init__.py +1 -0
- voidremote/ui/app.py +70 -0
- voidremote/ui/dialogs/__init__.py +6 -0
- voidremote/ui/dialogs/connect_dialog.py +149 -0
- voidremote/ui/dialogs/pair_dialog.py +189 -0
- voidremote/ui/main_window.py +371 -0
- voidremote/ui/theme.py +529 -0
- voidremote/ui/views/__init__.py +15 -0
- voidremote/ui/views/dashboard_view.py +233 -0
- voidremote/ui/views/files_view.py +305 -0
- voidremote/ui/views/monitor_view.py +236 -0
- voidremote/ui/views/settings_view.py +257 -0
- voidremote/ui/views/shell_view.py +234 -0
- voidremote/ui/widgets/__init__.py +14 -0
- voidremote/ui/widgets/device_card.py +259 -0
- voidremote/ui/widgets/log_view.py +159 -0
- voidremote/ui/widgets/metric_gauge.py +90 -0
- voidremote/utils/__init__.py +28 -0
- voidremote/utils/logging.py +120 -0
- voidremote/utils/security.py +216 -0
- voidremote-1.0.0.dist-info/METADATA +578 -0
- voidremote-1.0.0.dist-info/RECORD +46 -0
- voidremote-1.0.0.dist-info/WHEEL +5 -0
- voidremote-1.0.0.dist-info/entry_points.txt +5 -0
- voidremote-1.0.0.dist-info/licenses/LICENSE +21 -0
- voidremote-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VoidRemote main application window.
|
|
3
|
+
|
|
4
|
+
Provides the shell: sidebar navigation, stacked views, toolbar,
|
|
5
|
+
status bar, notification system, and device-aware state management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from PySide6.QtCore import Qt, QSize, QTimer, Slot
|
|
14
|
+
from PySide6.QtGui import QAction, QIcon, QKeySequence
|
|
15
|
+
from PySide6.QtWidgets import (
|
|
16
|
+
QHBoxLayout,
|
|
17
|
+
QLabel,
|
|
18
|
+
QListWidget,
|
|
19
|
+
QListWidgetItem,
|
|
20
|
+
QMainWindow,
|
|
21
|
+
QSplitter,
|
|
22
|
+
QStackedWidget,
|
|
23
|
+
QStatusBar,
|
|
24
|
+
QToolBar,
|
|
25
|
+
QVBoxLayout,
|
|
26
|
+
QWidget,
|
|
27
|
+
QFrame,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from voidremote.ui.theme import Colors
|
|
31
|
+
from voidremote.ui.views.dashboard_view import DashboardView
|
|
32
|
+
from voidremote.ui.views.shell_view import ShellView
|
|
33
|
+
from voidremote.ui.views.monitor_view import MonitorView
|
|
34
|
+
from voidremote.ui.views.files_view import FilesView
|
|
35
|
+
from voidremote.ui.views.settings_view import SettingsView
|
|
36
|
+
from voidremote.ui.widgets.log_view import LogView
|
|
37
|
+
from voidremote.ui.dialogs.pair_dialog import PairDialog
|
|
38
|
+
from voidremote.ui.dialogs.connect_dialog import ConnectDialog
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
NAV_ITEMS = [
|
|
43
|
+
("π±", "Devices", "Dashboard β view and manage connected devices"),
|
|
44
|
+
("π₯οΈ", "Mirror", "Screen mirroring (launch from device card)"),
|
|
45
|
+
("π₯", "Shell", "ADB shell terminal"),
|
|
46
|
+
("π", "Monitor", "Real-time device performance monitoring"),
|
|
47
|
+
("π", "Files", "File manager β browse, upload, download"),
|
|
48
|
+
("βοΈ", "Settings", "Application settings"),
|
|
49
|
+
("π", "Logs", "Live application logs"),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SidebarItem(QListWidgetItem):
|
|
54
|
+
def __init__(self, icon: str, label: str, tooltip: str) -> None:
|
|
55
|
+
super().__init__(f" {icon} {label}")
|
|
56
|
+
self.setToolTip(tooltip)
|
|
57
|
+
self.setSizeHint(QSize(200, 46))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MainWindow(QMainWindow):
|
|
61
|
+
"""
|
|
62
|
+
Primary application window.
|
|
63
|
+
|
|
64
|
+
Layout:
|
|
65
|
+
βββββββββββ¬βββββββββββββββββββββββββββββββ
|
|
66
|
+
β Sidebar β Stacked view content β
|
|
67
|
+
β β β
|
|
68
|
+
βββββββββββ΄βββββββββββββββββββββββββββββββ
|
|
69
|
+
β Status bar β
|
|
70
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, controller: object) -> None:
|
|
74
|
+
super().__init__()
|
|
75
|
+
self._controller = controller
|
|
76
|
+
self._devices: list = []
|
|
77
|
+
self._build_window()
|
|
78
|
+
self._build_toolbar()
|
|
79
|
+
self._build_sidebar()
|
|
80
|
+
self._build_views()
|
|
81
|
+
self._build_statusbar()
|
|
82
|
+
self._connect_signals()
|
|
83
|
+
self._setup_shortcuts()
|
|
84
|
+
self._restore_geometry()
|
|
85
|
+
|
|
86
|
+
# Initial refresh after window is shown
|
|
87
|
+
QTimer.singleShot(500, self._initial_refresh)
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
# Window layout
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def _build_window(self) -> None:
|
|
94
|
+
self.setWindowTitle("VoidRemote β Wireless Android Controller")
|
|
95
|
+
self.setMinimumSize(900, 600)
|
|
96
|
+
self.resize(1280, 800)
|
|
97
|
+
|
|
98
|
+
central = QWidget()
|
|
99
|
+
central.setObjectName("MainWidget")
|
|
100
|
+
self.setCentralWidget(central)
|
|
101
|
+
|
|
102
|
+
self._root_layout = QHBoxLayout(central)
|
|
103
|
+
self._root_layout.setContentsMargins(0, 0, 0, 0)
|
|
104
|
+
self._root_layout.setSpacing(0)
|
|
105
|
+
|
|
106
|
+
def _build_toolbar(self) -> None:
|
|
107
|
+
toolbar = QToolBar("Main Toolbar")
|
|
108
|
+
toolbar.setMovable(False)
|
|
109
|
+
toolbar.setIconSize(QSize(20, 20))
|
|
110
|
+
toolbar.setFloatable(False)
|
|
111
|
+
|
|
112
|
+
spacer = QWidget()
|
|
113
|
+
spacer.setStyleSheet("background: transparent;")
|
|
114
|
+
|
|
115
|
+
self._toolbar_title = QLabel(" VoidRemote")
|
|
116
|
+
self._toolbar_title.setStyleSheet(
|
|
117
|
+
f"font-size: 15px; font-weight: 700; color: {Colors.ACCENT}; letter-spacing: 1px;"
|
|
118
|
+
)
|
|
119
|
+
toolbar.addWidget(self._toolbar_title)
|
|
120
|
+
toolbar.addSeparator()
|
|
121
|
+
|
|
122
|
+
act_pair = QAction("+ Pair", self)
|
|
123
|
+
act_pair.setToolTip("Pair new device via Wireless Debugging")
|
|
124
|
+
act_pair.triggered.connect(self._open_pair_dialog)
|
|
125
|
+
toolbar.addAction(act_pair)
|
|
126
|
+
|
|
127
|
+
act_connect = QAction("β‘ Connect", self)
|
|
128
|
+
act_connect.setToolTip("Connect to device by IP")
|
|
129
|
+
act_connect.triggered.connect(self._open_connect_dialog)
|
|
130
|
+
toolbar.addAction(act_connect)
|
|
131
|
+
|
|
132
|
+
act_refresh = QAction("β» Refresh", self)
|
|
133
|
+
act_refresh.setToolTip("Refresh device list (F5)")
|
|
134
|
+
act_refresh.triggered.connect(self._refresh_devices)
|
|
135
|
+
toolbar.addAction(act_refresh)
|
|
136
|
+
|
|
137
|
+
toolbar.addWidget(spacer)
|
|
138
|
+
|
|
139
|
+
self._device_count_label = QLabel("No devices")
|
|
140
|
+
self._device_count_label.setStyleSheet(f"color: {Colors.TEXT_MUTED}; padding: 0 12px;")
|
|
141
|
+
toolbar.addWidget(self._device_count_label)
|
|
142
|
+
|
|
143
|
+
self.addToolBar(toolbar)
|
|
144
|
+
|
|
145
|
+
def _build_sidebar(self) -> None:
|
|
146
|
+
self._sidebar = QListWidget()
|
|
147
|
+
self._sidebar.setObjectName("Sidebar")
|
|
148
|
+
self._sidebar.setFixedWidth(200)
|
|
149
|
+
self._sidebar.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
|
150
|
+
|
|
151
|
+
for icon, label, tooltip in NAV_ITEMS:
|
|
152
|
+
self._sidebar.addItem(SidebarItem(icon, label, tooltip))
|
|
153
|
+
|
|
154
|
+
self._sidebar.setCurrentRow(0)
|
|
155
|
+
self._sidebar.currentRowChanged.connect(self._on_nav_changed)
|
|
156
|
+
self._root_layout.addWidget(self._sidebar)
|
|
157
|
+
|
|
158
|
+
# Separator line
|
|
159
|
+
sep = QFrame()
|
|
160
|
+
sep.setFrameShape(QFrame.Shape.VLine)
|
|
161
|
+
sep.setStyleSheet(f"color: {Colors.BORDER};")
|
|
162
|
+
self._root_layout.addWidget(sep)
|
|
163
|
+
|
|
164
|
+
def _build_views(self) -> None:
|
|
165
|
+
self._stack = QStackedWidget()
|
|
166
|
+
self._root_layout.addWidget(self._stack, stretch=1)
|
|
167
|
+
|
|
168
|
+
self._dashboard = DashboardView(self._controller)
|
|
169
|
+
self._mirror_placeholder = self._make_placeholder(
|
|
170
|
+
"π₯οΈ", "Screen Mirror",
|
|
171
|
+
"Click 'Mirror' on a device card to launch screen mirroring."
|
|
172
|
+
)
|
|
173
|
+
self._shell = ShellView(self._controller)
|
|
174
|
+
self._monitor = MonitorView(self._controller)
|
|
175
|
+
self._files = FilesView(self._controller)
|
|
176
|
+
self._settings = SettingsView()
|
|
177
|
+
self._log_view = LogView()
|
|
178
|
+
|
|
179
|
+
for view in (
|
|
180
|
+
self._dashboard, self._mirror_placeholder,
|
|
181
|
+
self._shell, self._monitor,
|
|
182
|
+
self._files, self._settings, self._log_view,
|
|
183
|
+
):
|
|
184
|
+
self._stack.addWidget(view)
|
|
185
|
+
|
|
186
|
+
def _make_placeholder(self, icon: str, title: str, desc: str) -> QWidget:
|
|
187
|
+
w = QWidget()
|
|
188
|
+
w.setStyleSheet(f"background: {Colors.BG_BASE};")
|
|
189
|
+
lay = QVBoxLayout(w)
|
|
190
|
+
lay.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
191
|
+
lay.setSpacing(12)
|
|
192
|
+
|
|
193
|
+
icon_lbl = QLabel(icon)
|
|
194
|
+
icon_lbl.setStyleSheet("font-size: 52px;")
|
|
195
|
+
icon_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
196
|
+
lay.addWidget(icon_lbl)
|
|
197
|
+
|
|
198
|
+
title_lbl = QLabel(title)
|
|
199
|
+
title_lbl.setStyleSheet(
|
|
200
|
+
f"font-size: 20px; font-weight: 700; color: {Colors.TEXT_SECONDARY};"
|
|
201
|
+
)
|
|
202
|
+
title_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
203
|
+
lay.addWidget(title_lbl)
|
|
204
|
+
|
|
205
|
+
desc_lbl = QLabel(desc)
|
|
206
|
+
desc_lbl.setStyleSheet(f"color: {Colors.TEXT_MUTED}; font-size: 13px;")
|
|
207
|
+
desc_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
208
|
+
desc_lbl.setWordWrap(True)
|
|
209
|
+
lay.addWidget(desc_lbl)
|
|
210
|
+
|
|
211
|
+
return w
|
|
212
|
+
|
|
213
|
+
def _build_statusbar(self) -> None:
|
|
214
|
+
bar = QStatusBar()
|
|
215
|
+
bar.setStyleSheet(
|
|
216
|
+
f"QStatusBar {{ background: {Colors.BG_SURFACE}; border-top: 1px solid {Colors.BORDER}; }}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
self._status_label = QLabel("Ready")
|
|
220
|
+
self._status_label.setStyleSheet(f"color: {Colors.TEXT_SECONDARY}; font-size: 12px;")
|
|
221
|
+
bar.addWidget(self._status_label)
|
|
222
|
+
|
|
223
|
+
self._conn_dot = QLabel("β")
|
|
224
|
+
self._conn_dot.setStyleSheet(f"color: {Colors.TEXT_MUTED}; font-size: 10px;")
|
|
225
|
+
bar.addPermanentWidget(self._conn_dot)
|
|
226
|
+
|
|
227
|
+
self._adb_status = QLabel("ADB: initializingβ¦")
|
|
228
|
+
self._adb_status.setStyleSheet(f"color: {Colors.TEXT_MUTED}; font-size: 12px; padding-right: 8px;")
|
|
229
|
+
bar.addPermanentWidget(self._adb_status)
|
|
230
|
+
|
|
231
|
+
self.setStatusBar(bar)
|
|
232
|
+
|
|
233
|
+
# ------------------------------------------------------------------
|
|
234
|
+
# Signals & shortcuts
|
|
235
|
+
# ------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
def _connect_signals(self) -> None:
|
|
238
|
+
self._dashboard.pair_device.connect(self._open_pair_dialog)
|
|
239
|
+
self._dashboard.connect_device.connect(self._connect_device)
|
|
240
|
+
self._dashboard.disconnect_device.connect(self._disconnect_device)
|
|
241
|
+
self._dashboard.mirror_device.connect(self._mirror_device)
|
|
242
|
+
self._dashboard.show_device_info.connect(self._show_device_info)
|
|
243
|
+
|
|
244
|
+
def _setup_shortcuts(self) -> None:
|
|
245
|
+
from PySide6.QtGui import QShortcut
|
|
246
|
+
|
|
247
|
+
QShortcut(QKeySequence("F5"), self, self._refresh_devices)
|
|
248
|
+
QShortcut(QKeySequence("Ctrl+Shift+C"), self, self._open_connect_dialog)
|
|
249
|
+
QShortcut(QKeySequence("Ctrl+Shift+P"), self, self._open_pair_dialog)
|
|
250
|
+
QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
|
|
251
|
+
|
|
252
|
+
# ------------------------------------------------------------------
|
|
253
|
+
# Navigation
|
|
254
|
+
# ------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
@Slot(int)
|
|
257
|
+
def _on_nav_changed(self, row: int) -> None:
|
|
258
|
+
self._stack.setCurrentIndex(row)
|
|
259
|
+
if row == 0:
|
|
260
|
+
self._dashboard.refresh()
|
|
261
|
+
|
|
262
|
+
# ------------------------------------------------------------------
|
|
263
|
+
# Actions
|
|
264
|
+
# ------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
def _initial_refresh(self) -> None:
|
|
267
|
+
try:
|
|
268
|
+
self._controller.initialize()
|
|
269
|
+
self._adb_status.setText("ADB: connected")
|
|
270
|
+
self._adb_status.setStyleSheet(f"color: {Colors.SUCCESS}; font-size: 12px; padding-right: 8px;")
|
|
271
|
+
except Exception as exc:
|
|
272
|
+
self._adb_status.setText(f"ADB: {exc}")
|
|
273
|
+
self._adb_status.setStyleSheet(f"color: {Colors.ERROR}; font-size: 12px; padding-right: 8px;")
|
|
274
|
+
self._refresh_devices()
|
|
275
|
+
|
|
276
|
+
def _refresh_devices(self) -> None:
|
|
277
|
+
self._dashboard.refresh()
|
|
278
|
+
try:
|
|
279
|
+
self._devices = self._controller.list_devices(refresh=False)
|
|
280
|
+
count = len(self._devices)
|
|
281
|
+
self._device_count_label.setText(
|
|
282
|
+
f"{count} device{'s' if count != 1 else ''} connected"
|
|
283
|
+
)
|
|
284
|
+
self._conn_dot.setStyleSheet(
|
|
285
|
+
f"color: {Colors.SUCCESS if count else Colors.TEXT_MUTED}; font-size: 10px;"
|
|
286
|
+
)
|
|
287
|
+
self._shell.set_devices(self._devices)
|
|
288
|
+
self._monitor.set_devices(self._devices)
|
|
289
|
+
self._files.set_devices(self._devices)
|
|
290
|
+
except Exception as exc:
|
|
291
|
+
logger.warning("Refresh error: %s", exc)
|
|
292
|
+
|
|
293
|
+
def _open_pair_dialog(self) -> None:
|
|
294
|
+
dlg = PairDialog(self._controller, self)
|
|
295
|
+
dlg.paired.connect(self._on_paired)
|
|
296
|
+
dlg.exec()
|
|
297
|
+
|
|
298
|
+
def _open_connect_dialog(self) -> None:
|
|
299
|
+
dlg = ConnectDialog(self._controller, self)
|
|
300
|
+
dlg.connected.connect(self._on_connected)
|
|
301
|
+
dlg.exec()
|
|
302
|
+
|
|
303
|
+
@Slot(str, int)
|
|
304
|
+
def _on_paired(self, host: str, port: int) -> None:
|
|
305
|
+
self._set_status(f"Paired with {host}:{port} β connectingβ¦")
|
|
306
|
+
QTimer.singleShot(1000, lambda: self._connect_device(host, 5555))
|
|
307
|
+
|
|
308
|
+
@Slot(object)
|
|
309
|
+
def _on_connected(self, device: object) -> None:
|
|
310
|
+
self._set_status(f"Connected: {device.info.display_name}") # type: ignore[attr-defined]
|
|
311
|
+
self._refresh_devices()
|
|
312
|
+
|
|
313
|
+
@Slot(str, int)
|
|
314
|
+
def _connect_device(self, host: str, port: int) -> None:
|
|
315
|
+
try:
|
|
316
|
+
device = self._controller.connect_device(host, port)
|
|
317
|
+
self._set_status(f"Connected: {device.info.display_name}")
|
|
318
|
+
self._refresh_devices()
|
|
319
|
+
except Exception as exc:
|
|
320
|
+
self._set_status(f"Connection failed: {exc}", error=True)
|
|
321
|
+
|
|
322
|
+
@Slot(str)
|
|
323
|
+
def _disconnect_device(self, serial: str) -> None:
|
|
324
|
+
self._controller.disconnect_device(serial)
|
|
325
|
+
self._set_status(f"Disconnected: {serial}")
|
|
326
|
+
self._refresh_devices()
|
|
327
|
+
|
|
328
|
+
@Slot(str)
|
|
329
|
+
def _mirror_device(self, serial: str) -> None:
|
|
330
|
+
self._set_status(f"Screen mirror for {serial} β use 'voidremote mirror {serial}' in CLI")
|
|
331
|
+
|
|
332
|
+
@Slot(str)
|
|
333
|
+
def _show_device_info(self, serial: str) -> None:
|
|
334
|
+
device = self._controller.get_device(serial)
|
|
335
|
+
if device:
|
|
336
|
+
self._set_status(
|
|
337
|
+
f"{device.info.display_name} | Android {device.info.android_version} | {device.info.screen_resolution}"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def _set_status(self, msg: str, error: bool = False) -> None:
|
|
341
|
+
color = Colors.ERROR if error else Colors.TEXT_SECONDARY
|
|
342
|
+
self._status_label.setText(msg)
|
|
343
|
+
self._status_label.setStyleSheet(f"color: {color}; font-size: 12px;")
|
|
344
|
+
logger.info("Status: %s", msg)
|
|
345
|
+
|
|
346
|
+
# ------------------------------------------------------------------
|
|
347
|
+
# Geometry persistence
|
|
348
|
+
# ------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
def _restore_geometry(self) -> None:
|
|
351
|
+
from voidremote.config.settings import get_settings
|
|
352
|
+
s = get_settings().ui
|
|
353
|
+
self.resize(s.window_width, s.window_height)
|
|
354
|
+
if s.window_maximized:
|
|
355
|
+
self.showMaximized()
|
|
356
|
+
|
|
357
|
+
def closeEvent(self, event: object) -> None: # type: ignore[override]
|
|
358
|
+
from voidremote.config.settings import get_settings, save_settings
|
|
359
|
+
s = get_settings()
|
|
360
|
+
s.ui.window_width = self.width()
|
|
361
|
+
s.ui.window_height = self.height()
|
|
362
|
+
s.ui.window_maximized = self.isMaximized()
|
|
363
|
+
save_settings()
|
|
364
|
+
|
|
365
|
+
self._log_view.remove_handler()
|
|
366
|
+
self._monitor.cleanup()
|
|
367
|
+
try:
|
|
368
|
+
self._controller.shutdown()
|
|
369
|
+
except Exception:
|
|
370
|
+
pass
|
|
371
|
+
super().closeEvent(event) # type: ignore[call-arg]
|