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.
Files changed (46) hide show
  1. voidremote/__init__.py +31 -0
  2. voidremote/adb/__init__.py +28 -0
  3. voidremote/adb/client.py +385 -0
  4. voidremote/adb/device_parser.py +286 -0
  5. voidremote/cli/__init__.py +5 -0
  6. voidremote/cli/main.py +987 -0
  7. voidremote/config/__init__.py +37 -0
  8. voidremote/config/settings.py +203 -0
  9. voidremote/controllers/__init__.py +5 -0
  10. voidremote/controllers/app_controller.py +222 -0
  11. voidremote/core/__init__.py +1 -0
  12. voidremote/core/automation.py +184 -0
  13. voidremote/models/__init__.py +31 -0
  14. voidremote/models/device.py +263 -0
  15. voidremote/network/__init__.py +1 -0
  16. voidremote/network/discovery.py +116 -0
  17. voidremote/services/__init__.py +13 -0
  18. voidremote/services/device_service.py +340 -0
  19. voidremote/services/input_service.py +181 -0
  20. voidremote/services/monitor_service.py +198 -0
  21. voidremote/ui/__init__.py +1 -0
  22. voidremote/ui/app.py +70 -0
  23. voidremote/ui/dialogs/__init__.py +6 -0
  24. voidremote/ui/dialogs/connect_dialog.py +149 -0
  25. voidremote/ui/dialogs/pair_dialog.py +189 -0
  26. voidremote/ui/main_window.py +371 -0
  27. voidremote/ui/theme.py +529 -0
  28. voidremote/ui/views/__init__.py +15 -0
  29. voidremote/ui/views/dashboard_view.py +233 -0
  30. voidremote/ui/views/files_view.py +305 -0
  31. voidremote/ui/views/monitor_view.py +236 -0
  32. voidremote/ui/views/settings_view.py +257 -0
  33. voidremote/ui/views/shell_view.py +234 -0
  34. voidremote/ui/widgets/__init__.py +14 -0
  35. voidremote/ui/widgets/device_card.py +259 -0
  36. voidremote/ui/widgets/log_view.py +159 -0
  37. voidremote/ui/widgets/metric_gauge.py +90 -0
  38. voidremote/utils/__init__.py +28 -0
  39. voidremote/utils/logging.py +120 -0
  40. voidremote/utils/security.py +216 -0
  41. voidremote-1.0.0.dist-info/METADATA +578 -0
  42. voidremote-1.0.0.dist-info/RECORD +46 -0
  43. voidremote-1.0.0.dist-info/WHEEL +5 -0
  44. voidremote-1.0.0.dist-info/entry_points.txt +5 -0
  45. voidremote-1.0.0.dist-info/licenses/LICENSE +21 -0
  46. 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]