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,236 @@
1
+ """Real-time device monitoring view with live gauges and graphs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import deque
6
+ from typing import Optional
7
+
8
+ from PySide6.QtCore import Qt, QTimer, Slot
9
+ from PySide6.QtWidgets import (
10
+ QComboBox,
11
+ QFrame,
12
+ QGridLayout,
13
+ QHBoxLayout,
14
+ QLabel,
15
+ QProgressBar,
16
+ QVBoxLayout,
17
+ QWidget,
18
+ )
19
+
20
+ from voidremote.services.monitor_service import DeviceSnapshot
21
+ from voidremote.ui.theme import Colors
22
+ from voidremote.ui.widgets.metric_gauge import MetricGauge
23
+
24
+
25
+ class SparklineWidget(QWidget):
26
+ """Minimal sparkline chart for a time-series metric."""
27
+
28
+ def __init__(self, color: str = Colors.ACCENT, parent: Optional[QWidget] = None) -> None:
29
+ super().__init__(parent)
30
+ self._color = color
31
+ self._data: deque[float] = deque(maxlen=60)
32
+ self.setMinimumHeight(60)
33
+
34
+ def push(self, value: float) -> None:
35
+ self._data.append(value)
36
+ self.update()
37
+
38
+ def paintEvent(self, event: object) -> None: # type: ignore[override]
39
+ if len(self._data) < 2:
40
+ return
41
+ from PySide6.QtGui import QPainter, QPen, QColor, QPainterPath
42
+ painter = QPainter(self)
43
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
44
+
45
+ w, h = self.width(), self.height()
46
+ data = list(self._data)
47
+ max_v = max(data) or 1.0
48
+ step = w / max(len(data) - 1, 1)
49
+
50
+ path = QPainterPath()
51
+ for i, v in enumerate(data):
52
+ x = i * step
53
+ y = h - (v / max_v) * (h - 4) - 2
54
+ if i == 0:
55
+ path.moveTo(x, y)
56
+ else:
57
+ path.lineTo(x, y)
58
+
59
+ pen = QPen(QColor(self._color))
60
+ pen.setWidth(2)
61
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
62
+ pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
63
+ painter.setPen(pen)
64
+ painter.drawPath(path)
65
+
66
+
67
+ class MetricCard(QFrame):
68
+ """Card showing a gauge + sparkline + current value label."""
69
+
70
+ def __init__(
71
+ self,
72
+ title: str,
73
+ color: str = Colors.ACCENT,
74
+ parent: Optional[QWidget] = None,
75
+ ) -> None:
76
+ super().__init__(parent)
77
+ self.setFrameShape(QFrame.Shape.StyledPanel)
78
+ self.setObjectName("DeviceCard")
79
+ self._color = color
80
+
81
+ layout = QVBoxLayout(self)
82
+ layout.setContentsMargins(16, 14, 16, 14)
83
+ layout.setSpacing(8)
84
+
85
+ title_label = QLabel(title)
86
+ title_label.setStyleSheet(
87
+ f"font-size: 13px; font-weight: 600; color: {Colors.TEXT_SECONDARY}; text-transform: uppercase; letter-spacing: 0.5px;"
88
+ )
89
+ layout.addWidget(title_label)
90
+
91
+ self._gauge = MetricGauge(label="", color=color)
92
+ self._gauge.setFixedHeight(110)
93
+ layout.addWidget(self._gauge, alignment=Qt.AlignmentFlag.AlignCenter)
94
+
95
+ self._spark = SparklineWidget(color=color)
96
+ layout.addWidget(self._spark)
97
+
98
+ self._value_label = QLabel("–")
99
+ self._value_label.setStyleSheet(
100
+ f"font-size: 12px; color: {Colors.TEXT_SECONDARY}; text-align: center;"
101
+ )
102
+ self._value_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
103
+ layout.addWidget(self._value_label)
104
+
105
+ def update_value(self, value: float, suffix: str = "%", detail: str = "") -> None:
106
+ self._gauge.set_value(value)
107
+ self._gauge.set_suffix(suffix)
108
+ self._spark.push(value)
109
+ self._value_label.setText(detail or f"{value:.1f}{suffix}")
110
+
111
+
112
+ class MonitorView(QWidget):
113
+ """
114
+ Live device monitoring dashboard.
115
+
116
+ Shows CPU, RAM, Battery, and Temperature in metric cards
117
+ with sparkline history graphs updated by MonitorService.
118
+ """
119
+
120
+ def __init__(self, controller: object, parent: Optional[QWidget] = None) -> None:
121
+ super().__init__(parent)
122
+ self._controller = controller
123
+ self._serial: Optional[str] = None
124
+ self._build_ui()
125
+
126
+ def _build_ui(self) -> None:
127
+ root = QVBoxLayout(self)
128
+ root.setContentsMargins(0, 0, 0, 0)
129
+ root.setSpacing(0)
130
+
131
+ # ── Header ──────────────────────────────────────
132
+ header = QHBoxLayout()
133
+ header.setContentsMargins(24, 16, 24, 12)
134
+ header.setSpacing(12)
135
+
136
+ title = QLabel("Monitor")
137
+ title.setStyleSheet(f"font-size: 22px; font-weight: 700; color: {Colors.TEXT_PRIMARY};")
138
+ header.addWidget(title)
139
+ header.addStretch()
140
+
141
+ header.addWidget(QLabel("Device:"))
142
+ self._device_combo = QComboBox()
143
+ self._device_combo.setMinimumWidth(220)
144
+ self._device_combo.currentTextChanged.connect(self._on_device_changed)
145
+ header.addWidget(self._device_combo)
146
+
147
+ header_w = QWidget()
148
+ header_w.setStyleSheet(
149
+ f"background: {Colors.BG_SURFACE}; border-bottom: 1px solid {Colors.BORDER};"
150
+ )
151
+ header_w.setLayout(header)
152
+ root.addWidget(header_w)
153
+
154
+ # ── Metric cards grid ────────────────────────────
155
+ content = QWidget()
156
+ content.setStyleSheet(f"background: {Colors.BG_BASE};")
157
+ grid = QGridLayout(content)
158
+ grid.setContentsMargins(24, 20, 24, 20)
159
+ grid.setSpacing(16)
160
+
161
+ self._cpu_card = MetricCard("CPU Usage", Colors.ACCENT)
162
+ self._ram_card = MetricCard("RAM Usage", Colors.INFO)
163
+ self._batt_card = MetricCard("Battery", Colors.SUCCESS)
164
+ self._temp_card = MetricCard("Temperature", Colors.WARNING)
165
+
166
+ grid.addWidget(self._cpu_card, 0, 0)
167
+ grid.addWidget(self._ram_card, 0, 1)
168
+ grid.addWidget(self._batt_card, 1, 0)
169
+ grid.addWidget(self._temp_card, 1, 1)
170
+
171
+ root.addWidget(content, stretch=1)
172
+
173
+ # ── Status bar ───────────────────────────────────
174
+ self._status_bar = QLabel("Select a device to begin monitoring")
175
+ self._status_bar.setStyleSheet(
176
+ f"color: {Colors.TEXT_MUTED}; padding: 8px 24px; "
177
+ f"background: {Colors.BG_SURFACE}; border-top: 1px solid {Colors.BORDER};"
178
+ )
179
+ root.addWidget(self._status_bar)
180
+
181
+ def set_devices(self, devices: list) -> None:
182
+ current = self._device_combo.currentData()
183
+ self._device_combo.blockSignals(True)
184
+ self._device_combo.clear()
185
+ for d in devices:
186
+ self._device_combo.addItem(f"{d.info.display_name} ({d.serial})", userData=d.serial)
187
+ idx = self._device_combo.findData(current)
188
+ if idx >= 0:
189
+ self._device_combo.setCurrentIndex(idx)
190
+ self._device_combo.blockSignals(False)
191
+
192
+ def _on_device_changed(self) -> None:
193
+ serial = self._device_combo.currentData()
194
+ if serial == self._serial:
195
+ return
196
+ if self._serial:
197
+ try:
198
+ self._controller.stop_monitoring(self._serial)
199
+ except Exception:
200
+ pass
201
+ self._serial = serial
202
+ if serial:
203
+ try:
204
+ self._controller.start_monitoring(serial, interval=2.0, callback=self._on_snapshot)
205
+ self._status_bar.setText(f"Monitoring {serial}")
206
+ except Exception as exc:
207
+ self._status_bar.setText(f"Error: {exc}")
208
+
209
+ def _on_snapshot(self, snapshot: DeviceSnapshot) -> None:
210
+ # This runs in a background thread; update UI via queued connection
211
+ from PySide6.QtCore import QMetaObject, Q_ARG
212
+ QMetaObject.invokeMethod(self, "_apply_snapshot", Qt.ConnectionType.QueuedConnection,
213
+ Q_ARG(object, snapshot))
214
+
215
+ @Slot(object)
216
+ def _apply_snapshot(self, snapshot: DeviceSnapshot) -> None:
217
+ self._cpu_card.update_value(snapshot.cpu_usage, "%", f"CPU: {snapshot.cpu_usage:.1f}%")
218
+ self._ram_card.update_value(
219
+ snapshot.ram_usage_percent, "%",
220
+ f"{snapshot.ram_used_mb:.0f} / {snapshot.ram_total_mb:.0f} MB"
221
+ )
222
+ self._batt_card.update_value(
223
+ float(snapshot.battery_level), "%",
224
+ f"{snapshot.battery_level}% {'⚡' if snapshot.battery_is_charging else ''}"
225
+ )
226
+ self._temp_card.update_value(
227
+ snapshot.battery_temperature, "°C",
228
+ f"{snapshot.battery_temperature:.1f}°C"
229
+ )
230
+
231
+ def cleanup(self) -> None:
232
+ if self._serial:
233
+ try:
234
+ self._controller.stop_monitoring(self._serial)
235
+ except Exception:
236
+ pass
@@ -0,0 +1,257 @@
1
+ """Settings panel view."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from PySide6.QtCore import Qt
8
+ from PySide6.QtWidgets import (
9
+ QCheckBox,
10
+ QComboBox,
11
+ QFormLayout,
12
+ QGroupBox,
13
+ QHBoxLayout,
14
+ QLabel,
15
+ QLineEdit,
16
+ QPushButton,
17
+ QScrollArea,
18
+ QSlider,
19
+ QSpinBox,
20
+ QVBoxLayout,
21
+ QWidget,
22
+ )
23
+
24
+ from voidremote.config.settings import AppSettings, get_settings, save_settings
25
+ from voidremote.ui.theme import Colors
26
+
27
+
28
+ class SettingsView(QWidget):
29
+ """Full settings panel with save/reset functionality."""
30
+
31
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
32
+ super().__init__(parent)
33
+ self._settings = get_settings()
34
+ self._build_ui()
35
+ self._load_values()
36
+
37
+ def _build_ui(self) -> None:
38
+ root = QVBoxLayout(self)
39
+ root.setContentsMargins(0, 0, 0, 0)
40
+ root.setSpacing(0)
41
+
42
+ # ── Header ──────────────────────────────────────
43
+ header = QHBoxLayout()
44
+ header.setContentsMargins(24, 16, 24, 12)
45
+
46
+ title = QLabel("Settings")
47
+ title.setStyleSheet(f"font-size: 22px; font-weight: 700; color: {Colors.TEXT_PRIMARY};")
48
+ header.addWidget(title)
49
+ header.addStretch()
50
+
51
+ btn_reset = QPushButton("Reset Defaults")
52
+ btn_reset.clicked.connect(self._reset_defaults)
53
+ header.addWidget(btn_reset)
54
+
55
+ btn_save = QPushButton("Save")
56
+ btn_save.setObjectName("accent")
57
+ btn_save.setFixedWidth(70)
58
+ btn_save.clicked.connect(self._save)
59
+ header.addWidget(btn_save)
60
+
61
+ header_w = QWidget()
62
+ header_w.setStyleSheet(f"background: {Colors.BG_SURFACE}; border-bottom: 1px solid {Colors.BORDER};")
63
+ header_w.setLayout(header)
64
+ root.addWidget(header_w)
65
+
66
+ # ── Scrollable content ───────────────────────────
67
+ scroll = QScrollArea()
68
+ scroll.setWidgetResizable(True)
69
+ scroll.setFrameShape(QScrollArea.Shape.NoFrame) # type: ignore[attr-defined]
70
+
71
+ content = QWidget()
72
+ content.setStyleSheet(f"background: {Colors.BG_BASE};")
73
+ content_layout = QVBoxLayout(content)
74
+ content_layout.setContentsMargins(32, 24, 32, 24)
75
+ content_layout.setSpacing(20)
76
+
77
+ content_layout.addWidget(self._build_adb_group())
78
+ content_layout.addWidget(self._build_ui_group())
79
+ content_layout.addWidget(self._build_mirror_group())
80
+ content_layout.addWidget(self._build_logging_group())
81
+ content_layout.addStretch()
82
+
83
+ scroll.setWidget(content)
84
+ root.addWidget(scroll, stretch=1)
85
+
86
+ self._status_label = QLabel()
87
+ self._status_label.setStyleSheet(
88
+ f"color: {Colors.SUCCESS}; padding: 6px 24px; "
89
+ f"background: {Colors.BG_SURFACE}; border-top: 1px solid {Colors.BORDER};"
90
+ )
91
+ root.addWidget(self._status_label)
92
+
93
+ def _build_adb_group(self) -> QGroupBox:
94
+ group = QGroupBox("ADB")
95
+ form = QFormLayout(group)
96
+ form.setSpacing(10)
97
+
98
+ self._adb_path = QLineEdit()
99
+ self._adb_path.setPlaceholderText("adb")
100
+ form.addRow("ADB Binary Path:", self._adb_path)
101
+
102
+ self._adb_port = QSpinBox()
103
+ self._adb_port.setRange(1, 65535)
104
+ form.addRow("Default Port:", self._adb_port)
105
+
106
+ self._adb_timeout = QSpinBox()
107
+ self._adb_timeout.setRange(1, 300)
108
+ self._adb_timeout.setSuffix(" s")
109
+ form.addRow("Command Timeout:", self._adb_timeout)
110
+
111
+ self._adb_retry = QSpinBox()
112
+ self._adb_retry.setRange(0, 10)
113
+ form.addRow("Retry Count:", self._adb_retry)
114
+
115
+ self._auto_start = QCheckBox("Auto-start ADB server")
116
+ form.addRow("", self._auto_start)
117
+
118
+ self._kill_on_exit = QCheckBox("Kill ADB server on exit")
119
+ form.addRow("", self._kill_on_exit)
120
+
121
+ return group
122
+
123
+ def _build_ui_group(self) -> QGroupBox:
124
+ group = QGroupBox("Interface")
125
+ form = QFormLayout(group)
126
+ form.setSpacing(10)
127
+
128
+ self._theme_combo = QComboBox()
129
+ self._theme_combo.addItems(["dark", "light", "auto"])
130
+ form.addRow("Theme:", self._theme_combo)
131
+
132
+ self._font_size = QSpinBox()
133
+ self._font_size.setRange(8, 24)
134
+ self._font_size.setSuffix(" px")
135
+ form.addRow("Font Size:", self._font_size)
136
+
137
+ self._animations = QCheckBox("Enable animations")
138
+ form.addRow("", self._animations)
139
+
140
+ self._notifications = QCheckBox("Enable notifications")
141
+ form.addRow("", self._notifications)
142
+
143
+ self._tray_icon = QCheckBox("Show system tray icon")
144
+ form.addRow("", self._tray_icon)
145
+
146
+ return group
147
+
148
+ def _build_mirror_group(self) -> QGroupBox:
149
+ group = QGroupBox("Screen Mirror")
150
+ form = QFormLayout(group)
151
+ form.setSpacing(10)
152
+
153
+ self._mirror_fps = QSpinBox()
154
+ self._mirror_fps.setRange(1, 120)
155
+ self._mirror_fps.setSuffix(" FPS")
156
+ form.addRow("Max FPS:", self._mirror_fps)
157
+
158
+ self._mirror_bitrate = QSpinBox()
159
+ self._mirror_bitrate.setRange(1, 64)
160
+ self._mirror_bitrate.setSuffix(" Mbps")
161
+ form.addRow("Bitrate:", self._mirror_bitrate)
162
+
163
+ self._mirror_width = QSpinBox()
164
+ self._mirror_width.setRange(320, 3840)
165
+ self._mirror_width.setSingleStep(16)
166
+ form.addRow("Max Width:", self._mirror_width)
167
+
168
+ self._stay_awake = QCheckBox("Keep device awake while mirroring")
169
+ form.addRow("", self._stay_awake)
170
+
171
+ self._show_touches = QCheckBox("Show touch points")
172
+ form.addRow("", self._show_touches)
173
+
174
+ return group
175
+
176
+ def _build_logging_group(self) -> QGroupBox:
177
+ group = QGroupBox("Logging")
178
+ form = QFormLayout(group)
179
+ form.setSpacing(10)
180
+
181
+ self._log_level = QComboBox()
182
+ self._log_level.addItems(["DEBUG", "INFO", "WARNING", "ERROR"])
183
+ form.addRow("Log Level:", self._log_level)
184
+
185
+ self._log_file_enabled = QCheckBox("Write log file")
186
+ form.addRow("", self._log_file_enabled)
187
+
188
+ self._log_max_size = QSpinBox()
189
+ self._log_max_size.setRange(1, 100)
190
+ self._log_max_size.setSuffix(" MB")
191
+ form.addRow("Max File Size:", self._log_max_size)
192
+
193
+ self._log_backup = QSpinBox()
194
+ self._log_backup.setRange(0, 20)
195
+ form.addRow("Backup Files:", self._log_backup)
196
+
197
+ return group
198
+
199
+ def _load_values(self) -> None:
200
+ s = self._settings
201
+ self._adb_path.setText(s.adb.path)
202
+ self._adb_port.setValue(s.adb.default_port)
203
+ self._adb_timeout.setValue(int(s.adb.command_timeout))
204
+ self._adb_retry.setValue(s.adb.retry_count)
205
+ self._auto_start.setChecked(s.adb.auto_start_server)
206
+ self._kill_on_exit.setChecked(s.adb.kill_server_on_exit)
207
+
208
+ self._theme_combo.setCurrentText(s.ui.theme)
209
+ self._font_size.setValue(s.ui.font_size)
210
+ self._animations.setChecked(s.ui.animations_enabled)
211
+ self._notifications.setChecked(s.ui.notifications_enabled)
212
+ self._tray_icon.setChecked(s.ui.tray_icon_enabled)
213
+
214
+ self._mirror_fps.setValue(s.mirror.max_fps)
215
+ self._mirror_bitrate.setValue(s.mirror.max_bitrate_mbps)
216
+ self._mirror_width.setValue(s.mirror.max_width)
217
+ self._stay_awake.setChecked(s.mirror.stay_awake)
218
+ self._show_touches.setChecked(s.mirror.show_touches)
219
+
220
+ self._log_level.setCurrentText(s.logging.level)
221
+ self._log_file_enabled.setChecked(s.logging.file_enabled)
222
+ self._log_max_size.setValue(s.logging.max_file_size_mb)
223
+ self._log_backup.setValue(s.logging.backup_count)
224
+
225
+ def _save(self) -> None:
226
+ s = self._settings
227
+ s.adb.path = self._adb_path.text().strip() or "adb"
228
+ s.adb.default_port = self._adb_port.value()
229
+ s.adb.command_timeout = float(self._adb_timeout.value())
230
+ s.adb.retry_count = self._adb_retry.value()
231
+ s.adb.auto_start_server = self._auto_start.isChecked()
232
+ s.adb.kill_server_on_exit = self._kill_on_exit.isChecked()
233
+
234
+ s.ui.theme = self._theme_combo.currentText()
235
+ s.ui.font_size = self._font_size.value()
236
+ s.ui.animations_enabled = self._animations.isChecked()
237
+ s.ui.notifications_enabled = self._notifications.isChecked()
238
+ s.ui.tray_icon_enabled = self._tray_icon.isChecked()
239
+
240
+ s.mirror.max_fps = self._mirror_fps.value()
241
+ s.mirror.max_bitrate_mbps = self._mirror_bitrate.value()
242
+ s.mirror.max_width = self._mirror_width.value()
243
+ s.mirror.stay_awake = self._stay_awake.isChecked()
244
+ s.mirror.show_touches = self._show_touches.isChecked()
245
+
246
+ s.logging.level = self._log_level.currentText()
247
+ s.logging.file_enabled = self._log_file_enabled.isChecked()
248
+ s.logging.max_file_size_mb = self._log_max_size.value()
249
+ s.logging.backup_count = self._log_backup.value()
250
+
251
+ s.save()
252
+ self._status_label.setText("✓ Settings saved")
253
+
254
+ def _reset_defaults(self) -> None:
255
+ self._settings.reset_to_defaults()
256
+ self._load_values()
257
+ self._status_label.setText("Settings reset to defaults")