remotedesktop 0.0.1__tar.gz → 0.2.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.
Files changed (50) hide show
  1. remotedesktop-0.2.0/.github/workflows/ci.yml +48 -0
  2. {remotedesktop-0.0.1 → remotedesktop-0.2.0}/.gitignore +25 -24
  3. remotedesktop-0.2.0/CLAUDE.md +59 -0
  4. remotedesktop-0.2.0/PKG-INFO +113 -0
  5. {remotedesktop-0.0.1 → remotedesktop-0.2.0}/pyproject.toml +8 -0
  6. remotedesktop-0.2.0/readme.md +94 -0
  7. remotedesktop-0.2.0/run_client.bat +14 -0
  8. remotedesktop-0.2.0/run_server.bat +14 -0
  9. remotedesktop-0.2.0/scripts/make_coverage_badge.py +57 -0
  10. remotedesktop-0.2.0/scripts/make_venv.bat +13 -0
  11. {remotedesktop-0.0.1 → remotedesktop-0.2.0}/src/remotedesktop/__init__.py +3 -3
  12. remotedesktop-0.2.0/src/remotedesktop/client.py +245 -0
  13. remotedesktop-0.2.0/src/remotedesktop/clipboard.py +93 -0
  14. remotedesktop-0.2.0/src/remotedesktop/config.py +122 -0
  15. remotedesktop-0.2.0/src/remotedesktop/db.py +49 -0
  16. remotedesktop-0.2.0/src/remotedesktop/discovery.py +146 -0
  17. remotedesktop-0.2.0/src/remotedesktop/input_injection.py +123 -0
  18. remotedesktop-0.2.0/src/remotedesktop/inventory.py +218 -0
  19. remotedesktop-0.2.0/src/remotedesktop/protocol.py +75 -0
  20. remotedesktop-0.2.0/src/remotedesktop/server.py +174 -0
  21. remotedesktop-0.2.0/src/remotedesktop/sharing.py +544 -0
  22. remotedesktop-0.2.0/src/remotedesktop/tls.py +90 -0
  23. remotedesktop-0.2.0/src/remotedesktop/viewer.py +170 -0
  24. remotedesktop-0.2.0/src/remotedesktop/window_state.py +26 -0
  25. remotedesktop-0.2.0/tests/conftest.py +39 -0
  26. remotedesktop-0.2.0/tests/test_client_window.py +164 -0
  27. remotedesktop-0.2.0/tests/test_clipboard.py +178 -0
  28. remotedesktop-0.2.0/tests/test_config.py +58 -0
  29. remotedesktop-0.2.0/tests/test_discovery.py +129 -0
  30. remotedesktop-0.2.0/tests/test_input.py +235 -0
  31. remotedesktop-0.2.0/tests/test_input_injection.py +103 -0
  32. remotedesktop-0.2.0/tests/test_inventory.py +113 -0
  33. remotedesktop-0.2.0/tests/test_protocol.py +83 -0
  34. remotedesktop-0.2.0/tests/test_server_window.py +130 -0
  35. remotedesktop-0.2.0/tests/test_sharing.py +388 -0
  36. remotedesktop-0.2.0/tests/test_smoke.py +58 -0
  37. remotedesktop-0.2.0/tests/test_tls.py +19 -0
  38. remotedesktop-0.2.0/tests/test_window_state.py +28 -0
  39. remotedesktop-0.2.0/uv.lock +332 -0
  40. remotedesktop-0.0.1/CLAUDE.md +0 -38
  41. remotedesktop-0.0.1/PKG-INFO +0 -35
  42. remotedesktop-0.0.1/readme.md +0 -18
  43. remotedesktop-0.0.1/src/remotedesktop/client.py +0 -27
  44. remotedesktop-0.0.1/src/remotedesktop/server.py +0 -27
  45. remotedesktop-0.0.1/src/remotedesktop/viewer.py +0 -15
  46. remotedesktop-0.0.1/tests/conftest.py +0 -7
  47. remotedesktop-0.0.1/tests/test_smoke.py +0 -18
  48. remotedesktop-0.0.1/uv.lock +0 -142
  49. {remotedesktop-0.0.1 → remotedesktop-0.2.0}/LICENSE +0 -0
  50. {remotedesktop-0.0.1 → remotedesktop-0.2.0}/run_claude.bat +0 -0
@@ -0,0 +1,48 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+
8
+ jobs:
9
+ ruff:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v6
14
+ - run: uvx ruff check .
15
+
16
+ ty:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: astral-sh/setup-uv@v6
21
+ # ty resolves imports from the project venv; pyproject pins the
22
+ # type-checking platform to win32 (the app's target).
23
+ - run: uv sync
24
+ - run: uvx ty check .
25
+
26
+ test:
27
+ # The app targets Windows: input injection, schannel TLS, and the GUI
28
+ # tests all assume it.
29
+ runs-on: windows-latest
30
+ permissions:
31
+ contents: write # to push the updated coverage badge
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - uses: astral-sh/setup-uv@v6
35
+ - run: uv sync
36
+ - run: uv run pytest --cov=remotedesktop --cov-report=term
37
+ - name: Publish coverage badge
38
+ # master is PR-only (repo ruleset), so the badge lives on its own
39
+ # branch: a single-file orphan commit, force-pushed each time.
40
+ if: github.ref == 'refs/heads/master' && github.event_name == 'push'
41
+ run: |
42
+ uv run python scripts/make_coverage_badge.py badges/coverage.svg
43
+ git config user.name "github-actions[bot]"
44
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
45
+ $blob = (git hash-object -w badges/coverage.svg).Trim()
46
+ $tree = ("100644 blob $blob`tcoverage.svg" | git mktree).Trim()
47
+ $commit = (git commit-tree $tree -m "Update coverage badge").Trim()
48
+ git push --force origin "${commit}:refs/heads/badges"
@@ -1,24 +1,25 @@
1
- # Python
2
- __pycache__/
3
- *.py[cod]
4
- *.egg-info/
5
- .eggs/
6
- build/
7
- dist/
8
-
9
- # Virtual environments
10
- .venv/
11
- venv/
12
-
13
- # Testing
14
- .pytest_cache/
15
- .coverage
16
- htmlcov/
17
-
18
- # Tooling caches
19
- .mypy_cache/
20
- .ruff_cache/
21
-
22
- # IDE
23
- .idea/
24
- .vscode/
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # Virtual environments
10
+ .venv/
11
+ venv/
12
+
13
+ # Testing
14
+ .pytest_cache/
15
+ .coverage
16
+ htmlcov/
17
+ badges/
18
+
19
+ # Tooling caches
20
+ .mypy_cache/
21
+ .ruff_cache/
22
+
23
+ # IDE
24
+ .idea/
25
+ .vscode/
@@ -0,0 +1,59 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What This Project Is
6
+
7
+ A Python client/server remote desktop GUI application (PySide6) for Windows computers on the same LAN, per `readme.md`:
8
+
9
+ - **Autodiscovery and connection** of servers on the LAN.
10
+ - **In scope:** desktop screen, keyboard, mouse, and clipboard.
11
+ - **Out of scope:** shared drives, devices, and multimedia (e.g., audio).
12
+ - **Two GUI apps:** a client and a server, each run on its respective computer.
13
+ - **Trust model:** on first connection, the user on the server side must explicitly permit the client. After that, the client may reconnect whenever the server is running without further approval.
14
+ - **Constraint:** does not use Windows RDP and does not rely on any Microsoft-based authentication.
15
+
16
+ LAN autodiscovery, screen sharing, keyboard/mouse forwarding, clipboard sync, and a TLS-encrypted transport with a token-authenticated handshake are all implemented — the readme feature set plus security hardening.
17
+
18
+ ## Workflow
19
+
20
+ **Never push directly to `master`** — a repository ruleset rejects it. Every
21
+ change goes through a pull request: branch → push → PR → CI green → merge.
22
+ CI publishes the coverage badge to the unprotected `badges` branch (readme
23
+ references it by raw URL), so no workflow ever needs to write to `master`.
24
+
25
+ ## Commands
26
+
27
+ Managed with `uv` (hatchling build backend, src layout):
28
+
29
+ - `uv sync` — create/update the venv with the project and dev dependencies
30
+ - `uv run pytest` — run all tests
31
+ - `uv run pytest tests/test_smoke.py::test_version` — run a single test
32
+ - `uv run remotedesktop-client` / `uv run remotedesktop-server` — launch the apps. These are `gui-scripts`, so they run detached with no console output; use `uv run python -m remotedesktop.client` (or `.server`) when you need stdout/tracebacks.
33
+ - `uv build` — build sdist and wheel into `dist/`
34
+ - `uv publish` — publish to PyPI
35
+
36
+ ## Architecture
37
+
38
+ - Both apps are PySide6 GUI applications. `src/remotedesktop/client.py` (`ClientWindow`) and `src/remotedesktop/server.py` (`ServerWindow`) hold the app entry points; `main()` in each is wired to the `remotedesktop-client` / `remotedesktop-server` GUI scripts in `pyproject.toml`.
39
+ - The client-side remote desktop view is a widget, `ViewerWidget` in `src/remotedesktop/viewer.py`, hosted as `ClientWindow`'s central widget. Screen display and keyboard/mouse/clipboard forwarding belong in this widget, not in the window.
40
+ - **Autodiscovery** (`src/remotedesktop/discovery.py`) is a stdlib-only UDP probe/response protocol, deliberately not mDNS: the client broadcasts a JSON probe to `DISCOVERY_PORT` (48653) and servers reply with `{name, port}`; datagrams with the wrong magic/version/type are dropped. The server runs a `DiscoveryResponder` thread while its window is open; the client's `DiscoveryPanel` (dock in `ClientWindow`) calls the blocking `discover_servers()` on a worker thread and delivers results to the GUI via a queued signal. `DEFAULT_CONNECT_PORT` (48654) is reserved for the future desktop connection.
41
+ - Discovery tests run over loopback with ephemeral ports (`bind_host`/`discovery_port`/`broadcast_hosts` parameters exist for this), so they never touch the real LAN or fixed ports.
42
+ - **Screen sharing** (`sharing.py` + `protocol.py`) runs entirely on the Qt event loop — no threads or locks. `MessageStream` frames messages over a QTcpSocket (4-byte length + kind byte; JSON control messages or JPEG frames; malformed input aborts the socket). `ShareServer` owns one QTimer that grabs the primary screen via `QScreen.grabWindow(0)`, JPEG-encodes once, and fans out to all clients, skipping clients whose send buffer is backlogged; the timer only runs while clients are connected. `ShareClient` decodes frames to QImage for the viewer.
43
+ - **All persistence is one SQLite database** (`db.py`): `db.connect(path)` opens the DB and ensures every table (`settings`, `paired_clients`, `known_servers`, and the inventory tables). The file lives at `config.default_db_path()` — `platformdirs.user_data_dir("remotedesktop")` (`%LOCALAPPDATA%\remotedesktop\remotedesktop.db` on Windows). **Use `platformdirs` for any new data location; don't hardcode `%APPDATA%`.** Every store (`Settings`, `PairedClients`, `KnownServers`, `ConnectionInventory`) takes a `sqlite3.Connection`; the GUIs open one connection per window and share it. Both window classes take a `connection=` keyword so tests inject a temp-file DB and never touch the real one (see `tests/test_client_window.py` / `test_server_window.py`). Tests pass `db.connect(tmp_path/"x.db")` (or omit for in-memory), and use **separate DB files for the server and client** since they model different machines.
44
+ - **Trust model plumbing** (`config.py`): the client has a stable UUID identity (`load_client_identity`, in the `settings` table) and the server maps each approved client-id to a token (`PairedClients`). `ShareServer` takes an `approve_client(client_id, name) -> bool` callback; `ServerWindow` implements it as a modal QMessageBox. Tests inject stores backed by a temp DB and explicit identities so they never touch real data or prompt.
45
+ - **Status/debug logging is a feature**: `ShareServer`, `ShareClient`, and `DiscoveryPanel` emit human-readable `status` signals for every connection phase, and both windows show them in a timestamped "Connection log" pane. When adding connection behavior, emit a status message for each new phase or failure path — there's a test asserting the server's phase messages.
46
+ - **Connection inventory** (`inventory.py`): both windows have a tab (server: "Clients on LAN"; client: "Servers on LAN") backed by a `ConnectionInventory` — one `PeerRecord` per peer with attempts count, current state, first/last seen. The server drives it from `ShareServer.peerEvent` (a structured `{key, event, name, address, detail}` signal emitted alongside the status strings at each phase — keep both in sync when adding phases). The client drives it from `ClientWindow`'s own handlers (discovered servers from `DiscoveryPanel.serversFound`, plus attempt/connected/denied/disconnected). Server peers key by client-id; client peers key by `host:port`. **Persisted in SQLite** and reloaded on startup, so it survives restarts.
47
+ - **Revoking access**: `InventoryTab` takes an optional `(action_label, action_callback)`; the callback gets the selected peer's key. The server's tab wires "Revoke access" → `ShareServer.revoke_client(client_id)` (removes the token via `PairedClients.revoke`, disconnects any live stream with a `denied: access revoked`, and `_drop` reports state "revoked"; a `_revoked` set distinguishes it from a plain disconnect). The client's tab wires "Forget server" → `KnownServers.forget(key)` + disconnect. After either, the next connection needs a fresh approval. Server and client each use a *separate table* (`server_peers` / `client_peers`, selected via the `table=` arg) so a machine running both apps doesn't commingle them; `peers` is the default table used by tests. `ConnectionInventory` never lets a DB error break connectivity (load/save are wrapped).
48
+ - Sharing tests drive real sockets on the GUI thread by pumping `qapp.processEvents()` until a condition holds (see `pump()` in `tests/test_sharing.py`). Use the `make_server`/`make_client` helpers there and the session-scoped `credentials` fixture (one generated cert reused across tests — generating per test is ~100ms each). Tests exercise the real pairing flow (auto-approve), so there is no "pre-approve" shortcut anymore.
49
+ - **Input forwarding**: `ViewerWidget` captures mouse/keyboard events and emits `inputEvent` dicts with coordinates normalized 0..1 over the *displayed* frame rect (letterboxing is reversed via `_display_rect()`; events outside the frame are dropped). Keys carry the client's `nativeVirtualKey()` — since both ends are Windows, the server injects that VK directly with no key-translation table. The client sends these as `{"type": "input", ...}`; `ShareServer` only injects input from streams that completed the hello/approval handshake.
50
+ - **Injection is isolated behind `InputInjector`** (`input_injection.py`, Windows `SendInput` via ctypes; inert stub off-Windows). ShareServer takes an `injector=` param so tests pass a recording fake — **never let tests construct a real `InputInjector`, or they will move the host's actual mouse/keyboard.** Normalized 0..1 coords map directly to SendInput's 0..65535 absolute range over the primary monitor.
51
+ - **Transport is TLS + a token handshake** (`tls.py`, `sharing.py`, `config.py`). The Qt SSL backend here is Windows **schannel** (no OpenSSL); it does complete server-side TLS with a `cryptography`-generated self-signed cert loaded from PEM (verified), but be wary of schannel-specific quirks if you change the config. The server persists a self-signed cert/key under the config dir (`tls.load_or_create_credentials`; `ShareServer(credentials=...)`, or ephemeral if omitted). The client connects with `connectToHostEncrypted`, ignores the expected self-signed cert errors (`PeerVerifyMode.VerifyNone`), and pins the cert fingerprint **softly** — a change is logged but does *not* block the connection (robustness over strict security, per the trusted-LAN intent). Authentication: on first connect the server user approves and the server issues a random token (`PairedClients.pair`, stored server-side by client-id; client stores it in `KnownServers` keyed by `host:port` with the fingerprint). On reconnect the client sends the token in its hello and is admitted with no prompt; a missing/invalid token just falls back to re-approval (never hard-fails). **Do not reintroduce hard cert-pinning or challenge-response** — the user explicitly wanted robust connections over maximum security.
52
+ - **Clipboard sync is bidirectional** (`clipboard.py` `ClipboardSync`): client copy → server and server copy → all clients, as `{"type": "clipboard", "text"/"image_png"}` messages. It's opt-in — `ShareServer`/`ShareClient` take a `clipboard=` param and do nothing with clipboard if it's None, so only the GUIs (and clipboard tests) enable it; **plain sharing/input tests must leave it off to avoid touching the real OS clipboard.** Echo/loop prevention is by *content signature*, not just a guard flag, because Windows fires `QClipboard.dataChanged` asynchronously (after a flag would be cleared); image signatures hash canonical RGBA pixels so a re-encoded PNG round trip doesn't loop. Server only accepts clipboard from streams past the approval handshake. Tests use a `FakeClipboard(QObject)` with a `changed` signal and recording `apply()` so two independent clipboards exist in one process.
53
+ - The package version lives only in `src/remotedesktop/__init__.py` (`__version__`); hatchling reads it from there (`[tool.hatch.version]`), so bump it in that one place.
54
+ - Widget tests need a `QApplication`; use the session-scoped `qapp` fixture in `tests/conftest.py`.
55
+
56
+ ## Environment Notes
57
+
58
+ - Target platform is Windows; development happens on Windows 11. Requires Python >=3.14.
59
+ - **Run the tests from PowerShell/cmd, not Git Bash.** Git Bash puts `C:\Program Files\Git\mingw64\bin` on PATH, so Qt loads its `openssl` TLS backend against Git's MinGW-built OpenSSL DLLs and crashes with an access violation on the first TLS connection. From PowerShell those DLLs aren't on PATH and Qt uses schannel, as intended.
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: remotedesktop
3
+ Version: 0.2.0
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
+ Author-email: James Abel <j@abel.co>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Environment :: Win32 (MS Windows)
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: System :: Networking
14
+ Requires-Python: >=3.14
15
+ Requires-Dist: cryptography>=49.0.0
16
+ Requires-Dist: platformdirs>=4.10.0
17
+ Requires-Dist: pyside6>=6.11.1
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Remote Desktop
21
+
22
+ [![CI](https://github.com/jamesabel/remotedesktop/actions/workflows/ci.yml/badge.svg)](https://github.com/jamesabel/remotedesktop/actions/workflows/ci.yml)
23
+ ![Coverage](https://raw.githubusercontent.com/jamesabel/remotedesktop/badges/coverage.svg)
24
+ [![PyPI](https://img.shields.io/pypi/v/remotedesktop)](https://pypi.org/project/remotedesktop/)
25
+ ![Python](https://img.shields.io/badge/python-3.14%2B-blue)
26
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green)
27
+
28
+ A Python client/server application that provides remote desktop for Windows
29
+ computers on the same LAN, with autodiscovery of servers.
30
+
31
+ Connections are made to the desktop screen, keyboard, mouse, and clipboard.
32
+ Other connections are not provided, such as shared drives, devices, or
33
+ multimedia (e.g., audio).
34
+
35
+ The client and server are PySide6 (Qt) GUI apps, each run on its respective
36
+ computer. The client requests connection to the server, and for the initial
37
+ connection the user on the server side must permit the connection. After
38
+ that, the client can connect to the server whenever the server is running,
39
+ without the server having to grant permission again.
40
+
41
+ This does not use Windows RDP nor rely on any Microsoft-based authentication.
42
+
43
+ ## Status
44
+
45
+ The core feature set works: autodiscovery, live screen viewing,
46
+ mouse/keyboard control, and two-way clipboard sync. A running server appears
47
+ in the client's *Servers* panel; opening it (after the server user approves
48
+ the first connection) shows the remote screen, clicking into the view
49
+ forwards mouse and keyboard input, and text/images copied on either side
50
+ appear on the other. Input forwarding is safe against interruptions: if the
51
+ viewer loses focus, a drag ends outside the view, or a client disconnects
52
+ mid-keystroke, anything still held down is released on the server — no stuck
53
+ keys or mouse buttons.
54
+
55
+ The connection is encrypted with TLS, and after the server user approves a
56
+ client once, that client reconnects automatically using a stored token.
57
+ Unapproved connections are limited to small handshake messages until the
58
+ server user admits them. The security model is tuned for a trusted LAN: the
59
+ server's certificate is self-signed and trusted on first use, favoring
60
+ reliable reconnection over strict certificate checking — if the server's
61
+ certificate ever changes, the client logs a warning and updates its stored
62
+ fingerprint rather than refusing to connect.
63
+
64
+ Both apps have a second tab listing every peer seen on the LAN — on the
65
+ server, the clients that have connected or attempted to; on the client, the
66
+ servers it has discovered or tried to reach — with each peer's current state,
67
+ number of attempts, and when it was first and last seen. From that tab the
68
+ server can revoke a client (disconnecting it and requiring approval to
69
+ reconnect) and the client can forget a server. This inventory, and all other
70
+ state (identity, pairings, settings), is stored in a SQLite database and
71
+ persists across restarts. Its location is chosen by `platformdirs` (on
72
+ Windows, `%LOCALAPPDATA%\remotedesktop`).
73
+
74
+ ## Requirements
75
+
76
+ - Windows
77
+ - Python 3.14+
78
+ - [uv](https://docs.astral.sh/uv/)
79
+
80
+ ## Running
81
+
82
+ From a clone of this repository, double-click `run_server.bat` on the
83
+ computer to be shared and `run_client.bat` on the viewing computer. Each
84
+ script prepares the environment (installing dependencies on first use) and
85
+ then launches the app, closing its console window once the app is running.
86
+
87
+ To run from a terminal instead:
88
+
89
+ ```
90
+ uv run remotedesktop-server
91
+ uv run remotedesktop-client
92
+ ```
93
+
94
+ The apps are also published on PyPI as
95
+ [`remotedesktop`](https://pypi.org/project/remotedesktop/).
96
+
97
+ ## How discovery works
98
+
99
+ The client broadcasts a small JSON probe over UDP (port 48653); each server
100
+ on the LAN replies with its hostname and connection port. Windows Firewall
101
+ must allow Python to receive inbound UDP on that port for a server to be
102
+ discoverable from other machines.
103
+
104
+ ## Development
105
+
106
+ ```
107
+ uv sync # set up the environment
108
+ uv run pytest # run the tests
109
+ ```
110
+
111
+ Run the tests from PowerShell or cmd, not Git Bash: Git Bash puts Git's
112
+ MinGW OpenSSL DLLs on `PATH`, which Qt's TLS backend loads and crashes on.
113
+ From PowerShell, Qt uses the Windows schannel backend as intended.
@@ -15,6 +15,8 @@ classifiers = [
15
15
  "Topic :: System :: Networking",
16
16
  ]
17
17
  dependencies = [
18
+ "cryptography>=49.0.0",
19
+ "platformdirs>=4.10.0",
18
20
  "pyside6>=6.11.1",
19
21
  ]
20
22
 
@@ -25,6 +27,7 @@ remotedesktop-server = "remotedesktop.server:main"
25
27
  [dependency-groups]
26
28
  dev = [
27
29
  "pytest>=8",
30
+ "pytest-cov>=7.1.0",
28
31
  ]
29
32
 
30
33
  [build-system]
@@ -36,3 +39,8 @@ path = "src/remotedesktop/__init__.py"
36
39
 
37
40
  [tool.pytest.ini_options]
38
41
  testpaths = ["tests"]
42
+
43
+ [tool.ty.environment]
44
+ # The app targets Windows (SendInput via ctypes.windll); type-check as such
45
+ # even when ty runs on a Linux CI runner.
46
+ python-platform = "win32"
@@ -0,0 +1,94 @@
1
+ # Remote Desktop
2
+
3
+ [![CI](https://github.com/jamesabel/remotedesktop/actions/workflows/ci.yml/badge.svg)](https://github.com/jamesabel/remotedesktop/actions/workflows/ci.yml)
4
+ ![Coverage](https://raw.githubusercontent.com/jamesabel/remotedesktop/badges/coverage.svg)
5
+ [![PyPI](https://img.shields.io/pypi/v/remotedesktop)](https://pypi.org/project/remotedesktop/)
6
+ ![Python](https://img.shields.io/badge/python-3.14%2B-blue)
7
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green)
8
+
9
+ A Python client/server application that provides remote desktop for Windows
10
+ computers on the same LAN, with autodiscovery of servers.
11
+
12
+ Connections are made to the desktop screen, keyboard, mouse, and clipboard.
13
+ Other connections are not provided, such as shared drives, devices, or
14
+ multimedia (e.g., audio).
15
+
16
+ The client and server are PySide6 (Qt) GUI apps, each run on its respective
17
+ computer. The client requests connection to the server, and for the initial
18
+ connection the user on the server side must permit the connection. After
19
+ that, the client can connect to the server whenever the server is running,
20
+ without the server having to grant permission again.
21
+
22
+ This does not use Windows RDP nor rely on any Microsoft-based authentication.
23
+
24
+ ## Status
25
+
26
+ The core feature set works: autodiscovery, live screen viewing,
27
+ mouse/keyboard control, and two-way clipboard sync. A running server appears
28
+ in the client's *Servers* panel; opening it (after the server user approves
29
+ the first connection) shows the remote screen, clicking into the view
30
+ forwards mouse and keyboard input, and text/images copied on either side
31
+ appear on the other. Input forwarding is safe against interruptions: if the
32
+ viewer loses focus, a drag ends outside the view, or a client disconnects
33
+ mid-keystroke, anything still held down is released on the server — no stuck
34
+ keys or mouse buttons.
35
+
36
+ The connection is encrypted with TLS, and after the server user approves a
37
+ client once, that client reconnects automatically using a stored token.
38
+ Unapproved connections are limited to small handshake messages until the
39
+ server user admits them. The security model is tuned for a trusted LAN: the
40
+ server's certificate is self-signed and trusted on first use, favoring
41
+ reliable reconnection over strict certificate checking — if the server's
42
+ certificate ever changes, the client logs a warning and updates its stored
43
+ fingerprint rather than refusing to connect.
44
+
45
+ Both apps have a second tab listing every peer seen on the LAN — on the
46
+ server, the clients that have connected or attempted to; on the client, the
47
+ servers it has discovered or tried to reach — with each peer's current state,
48
+ number of attempts, and when it was first and last seen. From that tab the
49
+ server can revoke a client (disconnecting it and requiring approval to
50
+ reconnect) and the client can forget a server. This inventory, and all other
51
+ state (identity, pairings, settings), is stored in a SQLite database and
52
+ persists across restarts. Its location is chosen by `platformdirs` (on
53
+ Windows, `%LOCALAPPDATA%\remotedesktop`).
54
+
55
+ ## Requirements
56
+
57
+ - Windows
58
+ - Python 3.14+
59
+ - [uv](https://docs.astral.sh/uv/)
60
+
61
+ ## Running
62
+
63
+ From a clone of this repository, double-click `run_server.bat` on the
64
+ computer to be shared and `run_client.bat` on the viewing computer. Each
65
+ script prepares the environment (installing dependencies on first use) and
66
+ then launches the app, closing its console window once the app is running.
67
+
68
+ To run from a terminal instead:
69
+
70
+ ```
71
+ uv run remotedesktop-server
72
+ uv run remotedesktop-client
73
+ ```
74
+
75
+ The apps are also published on PyPI as
76
+ [`remotedesktop`](https://pypi.org/project/remotedesktop/).
77
+
78
+ ## How discovery works
79
+
80
+ The client broadcasts a small JSON probe over UDP (port 48653); each server
81
+ on the LAN replies with its hostname and connection port. Windows Firewall
82
+ must allow Python to receive inbound UDP on that port for a server to be
83
+ discoverable from other machines.
84
+
85
+ ## Development
86
+
87
+ ```
88
+ uv sync # set up the environment
89
+ uv run pytest # run the tests
90
+ ```
91
+
92
+ Run the tests from PowerShell or cmd, not Git Bash: Git Bash puts Git's
93
+ MinGW OpenSSL DLLs on `PATH`, which Qt's TLS backend loads and crashes on.
94
+ From PowerShell, Qt uses the Windows schannel backend as intended.
@@ -0,0 +1,14 @@
1
+ @echo off
2
+ setlocal
3
+ cd /d "%~dp0"
4
+
5
+ rem Prepare the environment (installs/updates dependencies) before launching.
6
+ uv sync --quiet
7
+ if errorlevel 1 (
8
+ echo Failed to prepare the environment.
9
+ pause
10
+ exit /b 1
11
+ )
12
+
13
+ rem Launch the GUI detached so this console window can close immediately.
14
+ start "" ".venv\Scripts\remotedesktop-client.exe" %*
@@ -0,0 +1,14 @@
1
+ @echo off
2
+ setlocal
3
+ cd /d "%~dp0"
4
+
5
+ rem Prepare the environment (installs/updates dependencies) before launching.
6
+ uv sync --quiet
7
+ if errorlevel 1 (
8
+ echo Failed to prepare the environment.
9
+ pause
10
+ exit /b 1
11
+ )
12
+
13
+ rem Launch the GUI detached so this console window can close immediately.
14
+ start "" ".venv\Scripts\remotedesktop-server.exe" %*
@@ -0,0 +1,57 @@
1
+ """Generate a shields-style SVG coverage badge from the .coverage data file.
2
+
3
+ Reads the total percentage via `coverage report --format=total` (so run the
4
+ tests with `--cov` first) and writes the badge, by default to
5
+ badges/coverage.svg. CI runs this and commits the badge so the readme can
6
+ reference it without any external service (the repo is private).
7
+ """
8
+
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ _COLORS = [ # same thresholds the coverage-badge package used
14
+ (100, "#4c1"),
15
+ (90, "#97CA00"),
16
+ (75, "#a4a61d"),
17
+ (60, "#dfb317"),
18
+ (40, "#fe7d37"),
19
+ (0, "#e05d44"),
20
+ ]
21
+
22
+ _SVG = """<svg xmlns="http://www.w3.org/2000/svg" width="104" height="20" role="img" aria-label="coverage: {pct}%">
23
+ <linearGradient id="s" x2="0" y2="100%">
24
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
25
+ <stop offset="1" stop-opacity=".1"/>
26
+ </linearGradient>
27
+ <clipPath id="r"><rect width="104" height="20" rx="3" fill="#fff"/></clipPath>
28
+ <g clip-path="url(#r)">
29
+ <rect width="61" height="20" fill="#555"/>
30
+ <rect x="61" width="43" height="20" fill="{color}"/>
31
+ <rect width="104" height="20" fill="url(#s)"/>
32
+ </g>
33
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
34
+ <text x="31.5" y="14">coverage</text>
35
+ <text x="82.5" y="14">{pct}%</text>
36
+ </g>
37
+ </svg>
38
+ """
39
+
40
+
41
+ def main() -> None:
42
+ total = subprocess.run(
43
+ [sys.executable, "-m", "coverage", "report", "--format=total"],
44
+ check=True,
45
+ capture_output=True,
46
+ text=True,
47
+ ).stdout.strip()
48
+ pct = round(float(total))
49
+ color = next(c for threshold, c in _COLORS if pct >= threshold)
50
+ out = Path(sys.argv[1] if len(sys.argv) > 1 else "badges/coverage.svg")
51
+ out.parent.mkdir(parents=True, exist_ok=True)
52
+ out.write_text(_SVG.format(pct=pct, color=color), encoding="utf-8")
53
+ print(f"{out}: {pct}%")
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
@@ -0,0 +1,13 @@
1
+ @echo off
2
+ setlocal
3
+ cd /d "%~dp0.."
4
+
5
+ rem Create/update .venv with the project and dev dependencies.
6
+ uv sync
7
+ if errorlevel 1 (
8
+ echo Failed to create the environment.
9
+ pause
10
+ exit /b 1
11
+ )
12
+
13
+ echo .venv is ready.
@@ -1,3 +1,3 @@
1
- """Remote desktop client/server for Windows computers on the same LAN."""
2
-
3
- __version__ = "0.0.1"
1
+ """Remote desktop client/server for Windows computers on the same LAN."""
2
+
3
+ __version__ = "0.2.0"