remotedesktop 0.6.0__tar.gz → 0.7.0__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-0.6.0 → remotedesktop-0.7.0}/PKG-INFO +1 -1
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/__init__.py +1 -1
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/client.py +1 -3
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/server.py +2 -1
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/sharing.py +11 -1
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/viewer.py +22 -8
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_sharing.py +17 -1
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_smoke.py +16 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/.github/workflows/ci.yml +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/.gitignore +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/CLAUDE.md +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/LICENSE +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/pyproject.toml +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/readme.md +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/run_claude.bat +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/run_client.bat +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/run_server.bat +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/scripts/make_coverage_badge.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/scripts/make_venv.bat +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/autostart.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/clipboard.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/config.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/db.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/discovery.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/frames.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/input_injection.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/inventory.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/logs.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/protocol.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/tls.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/src/remotedesktop/window_state.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/conftest.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_autostart.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_client_window.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_clipboard.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_config.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_discovery.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_frames.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_input.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_input_injection.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_inventory.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_logs.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_protocol.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_server_window.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_tls.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/tests/test_window_state.py +0 -0
- {remotedesktop-0.6.0 → remotedesktop-0.7.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remotedesktop
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Remote desktop client/server for Windows computers on the same LAN, with autodiscovery. Provides screen, keyboard, mouse, and clipboard sharing without RDP or Microsoft authentication.
|
|
5
5
|
Author-email: James Abel <j@abel.co>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -111,9 +111,7 @@ class ClientWindow(QMainWindow):
|
|
|
111
111
|
self.connection_log = QPlainTextEdit(self)
|
|
112
112
|
self.connection_log.setReadOnly(True)
|
|
113
113
|
self.connection_log.setMaximumBlockCount(1000)
|
|
114
|
-
|
|
115
|
-
log_dock.setWidget(self.connection_log)
|
|
116
|
-
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, log_dock)
|
|
114
|
+
tabs.addTab(self.connection_log, "Connection log")
|
|
117
115
|
|
|
118
116
|
self.discovery_panel.serverActivated.connect(self._on_server_activated)
|
|
119
117
|
self.discovery_panel.serversFound.connect(self._record_discovered)
|
|
@@ -64,7 +64,7 @@ class ServerWindow(QMainWindow):
|
|
|
64
64
|
status_layout = QVBoxLayout(status_tab)
|
|
65
65
|
status_layout.addWidget(self._summary)
|
|
66
66
|
status_layout.addWidget(self.autostart_checkbox, alignment=Qt.AlignmentFlag.AlignHCenter)
|
|
67
|
-
status_layout.
|
|
67
|
+
status_layout.addStretch(1)
|
|
68
68
|
|
|
69
69
|
# Tests inject a connection to a temp database; the app uses the default.
|
|
70
70
|
self._db = connection if connection is not None else db.connect(default_db_path())
|
|
@@ -75,6 +75,7 @@ class ServerWindow(QMainWindow):
|
|
|
75
75
|
InventoryTab(self.inventory, "Revoke access", self._revoke_client),
|
|
76
76
|
"Clients on LAN",
|
|
77
77
|
)
|
|
78
|
+
tabs.addTab(self.connection_log, "Connection log")
|
|
78
79
|
self.setCentralWidget(tabs)
|
|
79
80
|
|
|
80
81
|
if credentials is None:
|
|
@@ -22,6 +22,7 @@ from typing import cast
|
|
|
22
22
|
from PySide6.QtCore import QObject, QTimer, Signal
|
|
23
23
|
from PySide6.QtGui import QGuiApplication, QImage
|
|
24
24
|
from PySide6.QtNetwork import (
|
|
25
|
+
QAbstractSocket,
|
|
25
26
|
QHostAddress,
|
|
26
27
|
QSslCertificate,
|
|
27
28
|
QSslKey,
|
|
@@ -44,7 +45,10 @@ from remotedesktop import db, tls
|
|
|
44
45
|
|
|
45
46
|
_log = logging.getLogger("remotedesktop.sharing")
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
# 30 fps keeps key-to-glyph latency low (~17 ms average sampling delay).
|
|
49
|
+
# Affordable since inter-frame compression: an unchanged screen costs only
|
|
50
|
+
# a capture and a memory compare per tick — nothing is encoded or sent.
|
|
51
|
+
DEFAULT_FPS = 30
|
|
48
52
|
# Only for full frames to legacy (0.5.0) clients, which force-decode frames
|
|
49
53
|
# as JPEG. Delta-capable clients get lossless PNG keyframes and delta bands.
|
|
50
54
|
JPEG_QUALITY = 70
|
|
@@ -173,6 +177,9 @@ class ShareServer(QObject):
|
|
|
173
177
|
while self._server.hasPendingConnections():
|
|
174
178
|
# QSslServer hands out QSslSockets; the stubs say QTcpSocket.
|
|
175
179
|
sock = cast(QSslSocket, self._server.nextPendingConnection())
|
|
180
|
+
# TCP_NODELAY: input events and delta frames are small messages;
|
|
181
|
+
# Nagle + delayed ACK would add tens to hundreds of ms of latency.
|
|
182
|
+
sock.setSocketOption(QAbstractSocket.SocketOption.LowDelayOption, 1)
|
|
176
183
|
self.status.emit(
|
|
177
184
|
f"Incoming TLS connection from {_peer(sock)} "
|
|
178
185
|
f"(encrypted={sock.isEncrypted()}) — waiting for hello"
|
|
@@ -568,6 +575,9 @@ class ShareClient(QObject):
|
|
|
568
575
|
self._socket.ignoreSslErrors()
|
|
569
576
|
|
|
570
577
|
def _on_encrypted(self) -> None:
|
|
578
|
+
# TCP_NODELAY (settable only once connected): keystrokes are tiny
|
|
579
|
+
# messages; Nagle + delayed ACK would add tens to hundreds of ms.
|
|
580
|
+
self._socket.setSocketOption(QAbstractSocket.SocketOption.LowDelayOption, 1)
|
|
571
581
|
cert = self._socket.peerCertificate()
|
|
572
582
|
fingerprint = "" if cert.isNull() else tls.certificate_fingerprint(cert)
|
|
573
583
|
self._server_fingerprint = fingerprint
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Viewer widget that displays the remote desktop and forwards input to it."""
|
|
2
2
|
|
|
3
|
-
from PySide6.QtCore import QPointF, QRectF, Qt, Signal
|
|
3
|
+
from PySide6.QtCore import QPointF, QRectF, QSize, Qt, Signal
|
|
4
4
|
from PySide6.QtGui import (
|
|
5
5
|
QFocusEvent,
|
|
6
6
|
QImage,
|
|
@@ -32,7 +32,10 @@ class ViewerWidget(QWidget):
|
|
|
32
32
|
# The frame stays at the server's full resolution; what is painted is
|
|
33
33
|
# a SmoothTransformation-scaled copy (proper filtering, unlike the
|
|
34
34
|
# nearest-neighbor scaling of drawPixmap into a rect), cached until
|
|
35
|
-
# the frame or the display size changes.
|
|
35
|
+
# the frame or the display size changes. It is scaled to *device*
|
|
36
|
+
# pixels and stamped with the devicePixelRatio: sizing it in logical
|
|
37
|
+
# pixels would make Qt bilinear-upscale it again by the Windows
|
|
38
|
+
# display-scaling factor (125-150% on most monitors), blurring text.
|
|
36
39
|
self._scaled: QPixmap | None = None
|
|
37
40
|
self._message = "Not connected"
|
|
38
41
|
# Buttons/keys currently held, so releases can be forwarded even when
|
|
@@ -175,10 +178,21 @@ class ViewerWidget(QWidget):
|
|
|
175
178
|
painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, self._message)
|
|
176
179
|
return
|
|
177
180
|
rect = self._display_rect().toRect()
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
)
|
|
181
|
+
dpr = self.devicePixelRatioF()
|
|
182
|
+
target = QSize(round(rect.width() * dpr), round(rect.height() * dpr))
|
|
183
|
+
if (
|
|
184
|
+
self._scaled is None
|
|
185
|
+
or self._scaled.size() != target
|
|
186
|
+
or self._scaled.devicePixelRatio() != dpr
|
|
187
|
+
):
|
|
188
|
+
if self._frame.size() == target:
|
|
189
|
+
# Displayed at exactly 1:1 device pixels — no resample at all.
|
|
190
|
+
self._scaled = QPixmap(self._frame)
|
|
191
|
+
else:
|
|
192
|
+
self._scaled = self._frame.scaled(
|
|
193
|
+
target,
|
|
194
|
+
Qt.AspectRatioMode.IgnoreAspectRatio, # rect is already aspect-correct
|
|
195
|
+
Qt.TransformationMode.SmoothTransformation,
|
|
196
|
+
)
|
|
197
|
+
self._scaled.setDevicePixelRatio(dpr)
|
|
184
198
|
painter.drawPixmap(rect.topLeft(), self._scaled)
|
|
@@ -3,7 +3,7 @@ import time
|
|
|
3
3
|
|
|
4
4
|
from PySide6.QtCore import QEventLoop
|
|
5
5
|
from PySide6.QtGui import QColor, QImage, QPainter
|
|
6
|
-
from PySide6.QtNetwork import QSslSocket
|
|
6
|
+
from PySide6.QtNetwork import QAbstractSocket, QSslSocket
|
|
7
7
|
|
|
8
8
|
from remotedesktop import db, tls
|
|
9
9
|
from remotedesktop.config import KnownServers, PairedClients
|
|
@@ -399,6 +399,22 @@ def test_disconnected_client_paths_are_noops(qapp, tmp_path):
|
|
|
399
399
|
client.close()
|
|
400
400
|
|
|
401
401
|
|
|
402
|
+
def test_low_delay_is_set_on_both_ends(qapp, credentials, tmp_path):
|
|
403
|
+
server = make_server(credentials, tmp_path, approve=lambda *_: True)
|
|
404
|
+
client = make_client(tmp_path)
|
|
405
|
+
names = []
|
|
406
|
+
client.connected.connect(names.append)
|
|
407
|
+
client.connect_to("127.0.0.1", server.port)
|
|
408
|
+
try:
|
|
409
|
+
pump(qapp, lambda: names and server._streams)
|
|
410
|
+
option = QAbstractSocket.SocketOption.LowDelayOption
|
|
411
|
+
assert client._socket.socketOption(option) == 1
|
|
412
|
+
assert server._streams[0].socket.socketOption(option) == 1
|
|
413
|
+
finally:
|
|
414
|
+
client.close()
|
|
415
|
+
server.close()
|
|
416
|
+
|
|
417
|
+
|
|
402
418
|
def solid_image(color, *, width=64, height=128):
|
|
403
419
|
image = QImage(width, height, QImage.Format.Format_RGB32)
|
|
404
420
|
image.fill(QColor(color))
|
|
@@ -56,3 +56,19 @@ def test_viewer_widget_shows_and_clears_frames(qapp) -> None:
|
|
|
56
56
|
assert viewer.has_frame
|
|
57
57
|
viewer.clear("gone")
|
|
58
58
|
assert not viewer.has_frame
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_viewer_scales_frames_in_device_pixels(qapp) -> None:
|
|
62
|
+
from PySide6.QtGui import QImage
|
|
63
|
+
|
|
64
|
+
viewer = ViewerWidget()
|
|
65
|
+
viewer.resize(320, 240)
|
|
66
|
+
viewer.show_frame(QImage(640, 480, QImage.Format.Format_RGB32)) # same 4:3 aspect
|
|
67
|
+
viewer.grab() # forces a paint pass without showing a window
|
|
68
|
+
assert viewer._scaled is not None
|
|
69
|
+
dpr = viewer.devicePixelRatioF()
|
|
70
|
+
# Scaled to physical pixels and stamped with the ratio, so painting it
|
|
71
|
+
# never resamples a second time.
|
|
72
|
+
assert viewer._scaled.devicePixelRatio() == dpr
|
|
73
|
+
assert viewer._scaled.size().width() == round(320 * dpr)
|
|
74
|
+
assert viewer._scaled.size().height() == round(240 * dpr)
|
|
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
|