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.
Files changed (77) hide show
  1. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/PKG-INFO +8 -2
  2. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/readme.md +7 -1
  3. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/__init__.py +1 -1
  4. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/logs.py +28 -1
  5. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/preferences.py +55 -11
  6. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_logs.py +22 -0
  7. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_preferences.py +25 -0
  8. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/.github/workflows/ci.yml +0 -0
  9. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/.github/workflows/release.yml +0 -0
  10. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/.gitignore +0 -0
  11. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/CLAUDE.md +0 -0
  12. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/LICENSE +0 -0
  13. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/docs/media/client-demo.gif +0 -0
  14. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/docs/media/server-demo.gif +0 -0
  15. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/icon/remotedesktop.ico +0 -0
  16. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/pyproject.toml +0 -0
  17. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/run.bat +0 -0
  18. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/run_claude.bat +0 -0
  19. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/scripts/make_coverage_badge.py +0 -0
  20. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/scripts/make_installer.bat +0 -0
  21. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/scripts/make_venv.bat +0 -0
  22. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/__main__.py +0 -0
  23. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/about.py +0 -0
  24. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/app.py +0 -0
  25. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/autostart.py +0 -0
  26. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/client.py +0 -0
  27. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/clipboard.py +0 -0
  28. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/compat.py +0 -0
  29. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/config.py +0 -0
  30. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/cursor_shape.py +0 -0
  31. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/db.py +0 -0
  32. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/discovery.py +0 -0
  33. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/dxgi.py +0 -0
  34. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/frames.py +0 -0
  35. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/icon.py +0 -0
  36. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/input_injection.py +0 -0
  37. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/inventory.py +0 -0
  38. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/modal_loop.py +0 -0
  39. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/performance.py +0 -0
  40. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/protocol.py +0 -0
  41. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/server.py +0 -0
  42. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/session_lock.py +0 -0
  43. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/sharing.py +0 -0
  44. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/single_instance.py +0 -0
  45. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/tls.py +0 -0
  46. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/viewer.py +0 -0
  47. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/visual_effects.py +0 -0
  48. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/src/remotedesktop/window_state.py +0 -0
  49. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/conftest.py +0 -0
  50. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_about.py +0 -0
  51. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_autostart.py +0 -0
  52. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_clipboard.py +0 -0
  53. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_compat.py +0 -0
  54. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_config.py +0 -0
  55. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_cursor_shape.py +0 -0
  56. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_discovery.py +0 -0
  57. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_dxgi.py +0 -0
  58. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_frames.py +0 -0
  59. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_icon.py +0 -0
  60. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_input.py +0 -0
  61. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_input_injection.py +0 -0
  62. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_inventory.py +0 -0
  63. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_main_window.py +0 -0
  64. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_modal_loop.py +0 -0
  65. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_performance.py +0 -0
  66. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_protocol.py +0 -0
  67. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_session_lock.py +0 -0
  68. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_sharing.py +0 -0
  69. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_sharing_tab.py +0 -0
  70. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_single_instance.py +0 -0
  71. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_smoke.py +0 -0
  72. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_tls.py +0 -0
  73. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_visual_effects.py +0 -0
  74. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tests/test_window_state.py +0 -0
  75. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tools/make_demo_gifs.py +0 -0
  76. {remotedesktop-1.9.1 → remotedesktop-1.9.3}/tools/make_icon.py +0 -0
  77. {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.1
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
@@ -1,3 +1,3 @@
1
1
  """Remote desktop client/server for Windows computers on the same LAN."""
2
2
 
3
- __version__ = "1.9.1"
3
+ __version__ = "1.9.3"
@@ -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. Your Windows settings "
143
- "come back when the last viewer disconnects; nothing is "
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). It can be "
197
- "clicked from a remote desktop session, so an update doesn't "
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