remotedesktop 1.9.1__tar.gz → 1.9.3__tar.gz
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.
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/PKG-INFO +8 -2
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/readme.md +7 -1
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/__init__.py +1 -1
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/logs.py +28 -1
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/preferences.py +55 -11
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_logs.py +22 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_preferences.py +25 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/.github/workflows/ci.yml +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/.github/workflows/release.yml +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/.gitignore +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/CLAUDE.md +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/LICENSE +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/docs/media/client-demo.gif +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/docs/media/server-demo.gif +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/icon/remotedesktop.ico +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/pyproject.toml +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/run.bat +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/run_claude.bat +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/scripts/make_coverage_badge.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/scripts/make_installer.bat +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/scripts/make_venv.bat +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/__main__.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/about.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/app.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/autostart.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/client.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/clipboard.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/compat.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/config.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/cursor_shape.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/db.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/discovery.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/dxgi.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/frames.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/icon.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/input_injection.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/inventory.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/modal_loop.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/performance.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/protocol.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/server.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/session_lock.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/sharing.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/single_instance.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/tls.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/viewer.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/visual_effects.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/window_state.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/conftest.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_about.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_autostart.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_clipboard.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_compat.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_config.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_cursor_shape.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_discovery.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_dxgi.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_frames.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_icon.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_input.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_input_injection.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_inventory.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_main_window.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_modal_loop.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_performance.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_protocol.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_session_lock.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_sharing.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_sharing_tab.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_single_instance.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_smoke.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_tls.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_visual_effects.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_window_state.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tools/make_demo_gifs.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tools/make_icon.py +0 -0
- {remotedesktop-1.9.1 → remotedesktop-1.9.3}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remotedesktop
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.3
|
|
4
4
|
Summary: Remote desktop app for Windows computers on the same LAN, with autodiscovery. View other computers and optionally share this one — screen, keyboard, mouse, and clipboard, without RDP or Microsoft authentication.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jamesabel/remotedesktop
|
|
6
6
|
Project-URL: Repository, https://github.com/jamesabel/remotedesktop
|
|
@@ -74,11 +74,14 @@ one-click *Forget* / *Revoke*, and the live connection log.
|
|
|
74
74
|
- 🔍 **Autodiscovery** — sharing computers announce themselves over UDP; the app lists every one on the LAN at startup, no addresses to type (press *Refresh* or F5 to rescan).
|
|
75
75
|
- 🗂️ **Multiple computers at once** — view and control several computers simultaneously, each in its own tab named for that computer; the window title shows who you're connected to, even minimized.
|
|
76
76
|
- 🖥️ **Lossless screen sharing** — pixel-exact at full resolution, DXGI desktop-duplication capture (~10 ms per 4K frame), and inter-frame delta compression: an unchanged screen sends nothing.
|
|
77
|
+
- ⚡ **Fewer frames to send while sharing** — while at least one viewer is connected, Windows animations, menu fades, and shadows are turned off on the shared computer so the stream snaps instead of smearing; everything is restored the moment the last viewer leaves, and nothing is permanently changed (a recommended, default-on preference).
|
|
77
78
|
- ⌨️🖱️ **Full input control** — mouse, wheel, and keyboard forwarding that is safe against interruptions: anything still held down is released on the server if the viewer loses focus or disconnects, so no stuck keys. Prefer eyes-only? Choose *Shared, view only* and sharing becomes view-only, switchable live.
|
|
79
|
+
- 🖲️ **Cursor shape mirroring** — the pointer is never burned into the captured frame; instead your own cursor takes the remote cursor's shape (an I-beam over text, resize arrows on a window edge), so it is always crisp and lag-free.
|
|
78
80
|
- 🖼️ **View your way** — each connection scales to fit or shows the remote screen at 1:1 pixels with panning, and F11 goes full screen. While you type into a remote session every key is forwarded — F11 is the one key that stays local.
|
|
79
81
|
- 📋 **Two-way clipboard** — text and images copied on either machine appear on the other; a Preferences toggle turns syncing off entirely.
|
|
80
82
|
- 📸 **Screen captures** — grab the remote screen at its full resolution and copy it to the clipboard or save it as a PNG, from the *Screen capture* panel buttons or the File menu.
|
|
81
83
|
- 🔒 **TLS + approve-once pairing** — every connection is encrypted; the server user approves a new client once, after which it reconnects with a stored token and no prompt.
|
|
84
|
+
- 🔐 **Honest lock-screen behavior** — a locked server tells viewers so with a clear on-screen notice instead of a frozen frame, and streaming resumes by itself once someone signs in at the machine (see [The Windows lock screen](#the-windows-lock-screen)).
|
|
82
85
|
- 📊 **Built-in performance monitoring** — live bandwidth and round-trip-time graphs with window statistics (mean/min/max/p99/jitter), plus a per-viewer table on the server.
|
|
83
86
|
- 🔁 **Robust connections** — dead connections are detected within seconds, and dropped sessions reconnect automatically with backoff; a server restart heals by itself, no clicks needed. Connections that were open when the app closed are restored on the next start.
|
|
84
87
|
- 🚀 **Hands-off operation** — optional start-at-login (per-user, no admin rights), close-to-tray while sharing (the screen stays available with the window closed), and a *Restart app* button usable from the remote session itself, so you can update the software without visiting the machine.
|
|
@@ -178,7 +181,10 @@ an idle screen costs essentially nothing. When duplication is unavailable
|
|
|
178
181
|
or gets lost — the secure desktop (UAC/logon screen), an RDP session, a
|
|
179
182
|
display-mode change — the server transparently falls back to Qt's
|
|
180
183
|
`QScreen.grabWindow` (~96 ms per frame) and keeps retrying duplication in
|
|
181
|
-
the background.
|
|
184
|
+
the background. The captured frame never contains the mouse pointer;
|
|
185
|
+
instead the server reports the current cursor's *shape* whenever it
|
|
186
|
+
changes, and each client mirrors it on its own local cursor — which is why
|
|
187
|
+
the pointer you see is always sharp and moves with zero latency.
|
|
182
188
|
|
|
183
189
|
**Screen transfer.** Frames are captured at up to 30 fps and compared with
|
|
184
190
|
the previous capture in 64-row bands; only the bands that changed are
|
|
@@ -50,11 +50,14 @@ one-click *Forget* / *Revoke*, and the live connection log.
|
|
|
50
50
|
- 🔍 **Autodiscovery** — sharing computers announce themselves over UDP; the app lists every one on the LAN at startup, no addresses to type (press *Refresh* or F5 to rescan).
|
|
51
51
|
- 🗂️ **Multiple computers at once** — view and control several computers simultaneously, each in its own tab named for that computer; the window title shows who you're connected to, even minimized.
|
|
52
52
|
- 🖥️ **Lossless screen sharing** — pixel-exact at full resolution, DXGI desktop-duplication capture (~10 ms per 4K frame), and inter-frame delta compression: an unchanged screen sends nothing.
|
|
53
|
+
- ⚡ **Fewer frames to send while sharing** — while at least one viewer is connected, Windows animations, menu fades, and shadows are turned off on the shared computer so the stream snaps instead of smearing; everything is restored the moment the last viewer leaves, and nothing is permanently changed (a recommended, default-on preference).
|
|
53
54
|
- ⌨️🖱️ **Full input control** — mouse, wheel, and keyboard forwarding that is safe against interruptions: anything still held down is released on the server if the viewer loses focus or disconnects, so no stuck keys. Prefer eyes-only? Choose *Shared, view only* and sharing becomes view-only, switchable live.
|
|
55
|
+
- 🖲️ **Cursor shape mirroring** — the pointer is never burned into the captured frame; instead your own cursor takes the remote cursor's shape (an I-beam over text, resize arrows on a window edge), so it is always crisp and lag-free.
|
|
54
56
|
- 🖼️ **View your way** — each connection scales to fit or shows the remote screen at 1:1 pixels with panning, and F11 goes full screen. While you type into a remote session every key is forwarded — F11 is the one key that stays local.
|
|
55
57
|
- 📋 **Two-way clipboard** — text and images copied on either machine appear on the other; a Preferences toggle turns syncing off entirely.
|
|
56
58
|
- 📸 **Screen captures** — grab the remote screen at its full resolution and copy it to the clipboard or save it as a PNG, from the *Screen capture* panel buttons or the File menu.
|
|
57
59
|
- 🔒 **TLS + approve-once pairing** — every connection is encrypted; the server user approves a new client once, after which it reconnects with a stored token and no prompt.
|
|
60
|
+
- 🔐 **Honest lock-screen behavior** — a locked server tells viewers so with a clear on-screen notice instead of a frozen frame, and streaming resumes by itself once someone signs in at the machine (see [The Windows lock screen](#the-windows-lock-screen)).
|
|
58
61
|
- 📊 **Built-in performance monitoring** — live bandwidth and round-trip-time graphs with window statistics (mean/min/max/p99/jitter), plus a per-viewer table on the server.
|
|
59
62
|
- 🔁 **Robust connections** — dead connections are detected within seconds, and dropped sessions reconnect automatically with backoff; a server restart heals by itself, no clicks needed. Connections that were open when the app closed are restored on the next start.
|
|
60
63
|
- 🚀 **Hands-off operation** — optional start-at-login (per-user, no admin rights), close-to-tray while sharing (the screen stays available with the window closed), and a *Restart app* button usable from the remote session itself, so you can update the software without visiting the machine.
|
|
@@ -154,7 +157,10 @@ an idle screen costs essentially nothing. When duplication is unavailable
|
|
|
154
157
|
or gets lost — the secure desktop (UAC/logon screen), an RDP session, a
|
|
155
158
|
display-mode change — the server transparently falls back to Qt's
|
|
156
159
|
`QScreen.grabWindow` (~96 ms per frame) and keeps retrying duplication in
|
|
157
|
-
the background.
|
|
160
|
+
the background. The captured frame never contains the mouse pointer;
|
|
161
|
+
instead the server reports the current cursor's *shape* whenever it
|
|
162
|
+
changes, and each client mirrors it on its own local cursor — which is why
|
|
163
|
+
the pointer you see is always sharp and moves with zero latency.
|
|
158
164
|
|
|
159
165
|
**Screen transfer.** Frames are captured at up to 30 fps and compared with
|
|
160
166
|
the previous capture in 64-row bands; only the bands that changed are
|
|
@@ -16,7 +16,7 @@ import logging.handlers
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
18
|
import platformdirs
|
|
19
|
-
from PySide6.QtCore import Qt
|
|
19
|
+
from PySide6.QtCore import QMessageLogContext, Qt, QtMsgType, qInstallMessageHandler
|
|
20
20
|
from PySide6.QtWidgets import QDialog, QPlainTextEdit, QVBoxLayout, QWidget
|
|
21
21
|
|
|
22
22
|
# Cap on what a peer gets when it asks for our log: the most recent entries
|
|
@@ -70,12 +70,39 @@ class PeerLogDialog(QDialog):
|
|
|
70
70
|
layout.addWidget(view)
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
_QT_LEVELS = {
|
|
74
|
+
QtMsgType.QtDebugMsg: logging.DEBUG,
|
|
75
|
+
QtMsgType.QtInfoMsg: logging.INFO,
|
|
76
|
+
QtMsgType.QtWarningMsg: logging.WARNING,
|
|
77
|
+
QtMsgType.QtCriticalMsg: logging.ERROR,
|
|
78
|
+
QtMsgType.QtFatalMsg: logging.CRITICAL,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _qt_message_handler(
|
|
83
|
+
mode: QtMsgType, context: QMessageLogContext, message: str
|
|
84
|
+
) -> None:
|
|
85
|
+
# Category ("qt.qpa.mime", ...) is how Qt names the subsystem; file/line
|
|
86
|
+
# are only populated in debug builds of Qt, so they aren't included.
|
|
87
|
+
category = context.category
|
|
88
|
+
if category and category != "default":
|
|
89
|
+
message = f"{category}: {message}"
|
|
90
|
+
logging.getLogger("remotedesktop.qt").log(
|
|
91
|
+
_QT_LEVELS.get(mode, logging.WARNING), message
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
73
95
|
def init_logging(app_name: str, *, directory: Path | None = None) -> Path:
|
|
74
96
|
"""Write "remotedesktop" logger output to <log dir>/<app_name>.log.
|
|
75
97
|
|
|
98
|
+
Also routes Qt's own log output (e.g. "qt.qpa.mime: Retrying to obtain
|
|
99
|
+
clipboard.") into the same file: Qt's default message handler writes to
|
|
100
|
+
stderr, which a GUI app must never do.
|
|
101
|
+
|
|
76
102
|
Returns the log file path. `directory` exists for tests; the apps use
|
|
77
103
|
the platformdirs log dir.
|
|
78
104
|
"""
|
|
105
|
+
qInstallMessageHandler(_qt_message_handler)
|
|
79
106
|
log_dir = directory if directory is not None else default_log_dir()
|
|
80
107
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
81
108
|
path = log_path(app_name, directory=directory)
|
|
@@ -118,6 +118,10 @@ class PreferencesTab(QWidget):
|
|
|
118
118
|
)
|
|
119
119
|
self._autostart = autostart if autostart is not None else Autostart()
|
|
120
120
|
self.history_minutes = QSpinBox()
|
|
121
|
+
self.history_minutes.setToolTip(
|
|
122
|
+
"How much recent history the Performance tab's bandwidth and\n"
|
|
123
|
+
"round-trip-time graphs (and their statistics) cover."
|
|
124
|
+
)
|
|
121
125
|
self.history_minutes.setRange(1, 30)
|
|
122
126
|
self.history_minutes.setSuffix(" min")
|
|
123
127
|
self.history_minutes.setValue(
|
|
@@ -125,23 +129,35 @@ class PreferencesTab(QWidget):
|
|
|
125
129
|
)
|
|
126
130
|
self.history_minutes.valueChanged.connect(self._on_history_changed)
|
|
127
131
|
self.autostart_checkbox = QCheckBox("Start Remote Desktop when I log in to Windows")
|
|
132
|
+
self.autostart_checkbox.setToolTip(
|
|
133
|
+
"Registers this app in your Windows startup (per-user, no admin\n"
|
|
134
|
+
"rights) so it launches at login — minimized to the tray while\n"
|
|
135
|
+
"sharing is on, so sharing resumes after a reboot without\n"
|
|
136
|
+
"anyone having to launch the app."
|
|
137
|
+
)
|
|
128
138
|
self.autostart_checkbox.setChecked(self._autostart.is_enabled())
|
|
129
139
|
self.autostart_checkbox.setEnabled(self._autostart.available)
|
|
130
140
|
self.autostart_checkbox.toggled.connect(self._on_autostart_toggled)
|
|
131
141
|
self.clipboard_checkbox = QCheckBox("Sync clipboard with connected computers")
|
|
142
|
+
self.clipboard_checkbox.setToolTip(
|
|
143
|
+
"Text and images copied on this computer appear on connected\n"
|
|
144
|
+
"computers, and theirs appear here.\n"
|
|
145
|
+
"Turn off to keep this computer's clipboard private — nothing\n"
|
|
146
|
+
"is sent or applied in either direction."
|
|
147
|
+
)
|
|
132
148
|
self.clipboard_checkbox.setChecked(load_clipboard_sync_enabled(settings))
|
|
133
149
|
self.clipboard_checkbox.toggled.connect(self._on_clipboard_toggled)
|
|
134
150
|
self.reduce_effects_checkbox = QCheckBox(
|
|
135
|
-
"Reduce Windows visual effects while sharing this screen"
|
|
151
|
+
"Reduce Windows visual effects while sharing this screen (recommended)"
|
|
136
152
|
)
|
|
137
153
|
self.reduce_effects_checkbox.setToolTip(
|
|
138
|
-
"Makes the remote view feel more responsive: window animations
|
|
139
|
-
"menu fades, and shadows each stream as a burst of screen
|
|
140
|
-
"updates, so turning them off while viewers are connected lets
|
|
141
|
-
"the remote screen snap instead of smearing — and saves
|
|
142
|
-
"bandwidth. Submenus also open faster
|
|
143
|
-
"come back when the last viewer
|
|
144
|
-
"permanently changed."
|
|
154
|
+
"Makes the remote view feel more responsive: window animations,\n"
|
|
155
|
+
"menu fades, and shadows each stream as a burst of screen\n"
|
|
156
|
+
"updates, so turning them off while viewers are connected lets\n"
|
|
157
|
+
"the remote screen snap instead of smearing — and saves\n"
|
|
158
|
+
"bandwidth. Submenus also open faster.\n"
|
|
159
|
+
"Your Windows settings come back when the last viewer\n"
|
|
160
|
+
"disconnects; nothing is permanently changed."
|
|
145
161
|
)
|
|
146
162
|
self.reduce_effects_checkbox.setChecked(load_reduce_effects_enabled(settings))
|
|
147
163
|
self.reduce_effects_checkbox.toggled.connect(self._on_reduce_effects_toggled)
|
|
@@ -149,14 +165,30 @@ class PreferencesTab(QWidget):
|
|
|
149
165
|
self.sharing_off_radio = QRadioButton(
|
|
150
166
|
"Not shared — no one can see this computer's screen"
|
|
151
167
|
)
|
|
168
|
+
self.sharing_off_radio.setToolTip(
|
|
169
|
+
"This computer neither listens for connections nor announces\n"
|
|
170
|
+
"itself on the LAN. Existing viewers are disconnected."
|
|
171
|
+
)
|
|
152
172
|
self.sharing_view_radio = QRadioButton(
|
|
153
173
|
"Shared, view only — clients can watch this computer's screen "
|
|
154
174
|
"but not control it"
|
|
155
175
|
)
|
|
176
|
+
self.sharing_view_radio.setToolTip(
|
|
177
|
+
"Approved clients see this screen live, but their keyboard and\n"
|
|
178
|
+
"mouse input is ignored.\n"
|
|
179
|
+
"Switching between view only and full control applies instantly\n"
|
|
180
|
+
"without disconnecting viewers."
|
|
181
|
+
)
|
|
156
182
|
self.sharing_control_radio = QRadioButton(
|
|
157
183
|
"Shared, full control — clients can watch this computer's screen "
|
|
158
184
|
"and control it with their keyboard and mouse"
|
|
159
185
|
)
|
|
186
|
+
self.sharing_control_radio.setToolTip(
|
|
187
|
+
"Approved clients see this screen live and can type and click\n"
|
|
188
|
+
"as if sitting at this computer.\n"
|
|
189
|
+
"Each new client needs a one-time approval on this computer\n"
|
|
190
|
+
"before it can connect."
|
|
191
|
+
)
|
|
160
192
|
self._mode_radios = {
|
|
161
193
|
SHARING_MODE_OFF: self.sharing_off_radio,
|
|
162
194
|
SHARING_MODE_VIEW: self.sharing_view_radio,
|
|
@@ -174,8 +206,14 @@ class PreferencesTab(QWidget):
|
|
|
174
206
|
sharing_layout.addWidget(radio)
|
|
175
207
|
# Theme: an explicit light/dark override, or follow the OS setting.
|
|
176
208
|
self.theme_system_radio = QRadioButton("Follow the Windows light/dark setting")
|
|
209
|
+
self.theme_system_radio.setToolTip(
|
|
210
|
+
"Match Windows: the app switches automatically when the\n"
|
|
211
|
+
"Windows light/dark mode changes."
|
|
212
|
+
)
|
|
177
213
|
self.theme_light_radio = QRadioButton("Light")
|
|
214
|
+
self.theme_light_radio.setToolTip("Always use the light theme.")
|
|
178
215
|
self.theme_dark_radio = QRadioButton("Dark")
|
|
216
|
+
self.theme_dark_radio.setToolTip("Always use the dark theme.")
|
|
179
217
|
self._theme_radios = {
|
|
180
218
|
THEME_SYSTEM: self.theme_system_radio,
|
|
181
219
|
THEME_LIGHT: self.theme_light_radio,
|
|
@@ -193,14 +231,20 @@ class PreferencesTab(QWidget):
|
|
|
193
231
|
theme_layout.addWidget(radio)
|
|
194
232
|
self.restart_button = QPushButton("Restart app")
|
|
195
233
|
self.restart_button.setToolTip(
|
|
196
|
-
"Relaunch this app (e.g. after updating the software)
|
|
197
|
-
"clicked from a remote desktop session, so an update
|
|
198
|
-
"require visiting this computer."
|
|
234
|
+
"Relaunch this app (e.g. after updating the software).\n"
|
|
235
|
+
"It can be clicked from a remote desktop session, so an update\n"
|
|
236
|
+
"doesn't require visiting this computer."
|
|
199
237
|
)
|
|
200
238
|
self.viewer_checkbox = QCheckBox(
|
|
201
239
|
"Act as a client — discover servers on this LAN and view or "
|
|
202
240
|
"control their screens"
|
|
203
241
|
)
|
|
242
|
+
self.viewer_checkbox.setToolTip(
|
|
243
|
+
"Shows the client-side UI: the server panel, session tabs, and\n"
|
|
244
|
+
"server history.\n"
|
|
245
|
+
"Turn off on a computer that only shares its screen — it will\n"
|
|
246
|
+
"not connect to (or scan for) other computers at all."
|
|
247
|
+
)
|
|
204
248
|
self.viewer_checkbox.setChecked(load_viewer_enabled(settings))
|
|
205
249
|
self.viewer_checkbox.toggled.connect(self._on_viewer_toggled)
|
|
206
250
|
layout = QFormLayout(self)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
+
from PySide6.QtCore import qInstallMessageHandler, qWarning
|
|
4
|
+
|
|
3
5
|
from remotedesktop import logs
|
|
4
6
|
|
|
5
7
|
|
|
@@ -23,6 +25,26 @@ def test_init_logging_writes_formatted_records(tmp_path):
|
|
|
23
25
|
handler.close()
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
def test_init_logging_routes_qt_messages_to_the_log_file(tmp_path):
|
|
29
|
+
# Qt's default message handler writes to stderr — a GUI app must never
|
|
30
|
+
# do that, so init_logging redirects Qt's output into the log file.
|
|
31
|
+
path = logs.init_logging("client", directory=tmp_path)
|
|
32
|
+
root = logging.getLogger("remotedesktop")
|
|
33
|
+
try:
|
|
34
|
+
qWarning("qt grumbled about something")
|
|
35
|
+
for handler in root.handlers:
|
|
36
|
+
handler.flush()
|
|
37
|
+
text = path.read_text(encoding="utf-8")
|
|
38
|
+
assert "qt grumbled about something" in text
|
|
39
|
+
assert "WARNING" in text
|
|
40
|
+
assert "remotedesktop.qt" in text
|
|
41
|
+
finally:
|
|
42
|
+
qInstallMessageHandler(None) # back to Qt's default handler
|
|
43
|
+
for handler in list(root.handlers):
|
|
44
|
+
root.removeHandler(handler)
|
|
45
|
+
handler.close()
|
|
46
|
+
|
|
47
|
+
|
|
26
48
|
def test_read_log_tail_returns_recent_content(tmp_path):
|
|
27
49
|
(tmp_path / "server.log").write_text("early line\nrecent line\n", encoding="utf-8")
|
|
28
50
|
text = logs.read_log_tail("server", directory=tmp_path)
|
|
@@ -50,6 +50,31 @@ def test_history_change_applies_to_all_monitors(qapp, tmp_path):
|
|
|
50
50
|
assert all(m.window_seconds == 180.0 for m in monitors)
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
def test_every_preference_control_has_a_multiline_tooltip(qapp, tmp_path):
|
|
54
|
+
settings = Settings(db.connect(tmp_path / "prefs.db"))
|
|
55
|
+
tab = PreferencesTab(settings, PerformanceMonitor(), autostart=make_autostart())
|
|
56
|
+
assert tab.reduce_effects_checkbox.text().endswith("(recommended)")
|
|
57
|
+
for control in (
|
|
58
|
+
tab.viewer_checkbox,
|
|
59
|
+
tab.sharing_off_radio,
|
|
60
|
+
tab.sharing_view_radio,
|
|
61
|
+
tab.sharing_control_radio,
|
|
62
|
+
tab.theme_system_radio,
|
|
63
|
+
tab.theme_light_radio,
|
|
64
|
+
tab.theme_dark_radio,
|
|
65
|
+
tab.history_minutes,
|
|
66
|
+
tab.clipboard_checkbox,
|
|
67
|
+
tab.reduce_effects_checkbox,
|
|
68
|
+
tab.autostart_checkbox,
|
|
69
|
+
tab.restart_button,
|
|
70
|
+
):
|
|
71
|
+
tip = control.toolTip()
|
|
72
|
+
assert tip, f"{control.text() if hasattr(control, 'text') else control} has no tooltip"
|
|
73
|
+
# Long tooltips are broken into lines by hand — one endless line is
|
|
74
|
+
# hard to read. Short one-liners (the theme overrides) are fine.
|
|
75
|
+
assert "\n" in tip or len(tip) < 80
|
|
76
|
+
|
|
77
|
+
|
|
53
78
|
@pytest.mark.skipif(sys.platform != "win32", reason="Windows registry")
|
|
54
79
|
def test_autostart_checkbox_toggles_registration(qapp, tmp_path):
|
|
55
80
|
autostart = make_autostart()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|