remotedesktop 0.20.1__tar.gz → 1.1.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-1.1.0/.github/workflows/release.yml +48 -0
- remotedesktop-1.1.0/CLAUDE.md +77 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/PKG-INFO +44 -34
- remotedesktop-1.1.0/docs/media/client-demo.gif +0 -0
- remotedesktop-1.1.0/docs/media/server-demo.gif +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/pyproject.toml +2 -3
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/readme.md +43 -33
- remotedesktop-0.20.1/run_client.bat → remotedesktop-1.1.0/run.bat +14 -14
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/__init__.py +1 -1
- remotedesktop-1.1.0/src/remotedesktop/__main__.py +6 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/about.py +6 -5
- remotedesktop-1.1.0/src/remotedesktop/app.py +660 -0
- remotedesktop-1.1.0/src/remotedesktop/autostart.py +96 -0
- remotedesktop-1.1.0/src/remotedesktop/client.py +110 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/icon.py +4 -6
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/inventory.py +54 -30
- remotedesktop-1.1.0/src/remotedesktop/preferences.py +76 -0
- remotedesktop-1.1.0/src/remotedesktop/server.py +362 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/sharing.py +4 -2
- remotedesktop-1.1.0/src/remotedesktop/single_instance.py +73 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/window_state.py +24 -26
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_about.py +7 -11
- remotedesktop-1.1.0/tests/test_autostart.py +85 -0
- remotedesktop-1.1.0/tests/test_icon.py +20 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_inventory.py +63 -9
- remotedesktop-1.1.0/tests/test_main_window.py +556 -0
- remotedesktop-1.1.0/tests/test_preferences.py +68 -0
- remotedesktop-1.1.0/tests/test_sharing_tab.py +409 -0
- remotedesktop-1.1.0/tests/test_single_instance.py +46 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_smoke.py +13 -16
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_window_state.py +28 -28
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tools/make_demo_gifs.py +36 -19
- remotedesktop-0.20.1/CLAUDE.md +0 -62
- remotedesktop-0.20.1/docs/media/client-demo.gif +0 -0
- remotedesktop-0.20.1/docs/media/server-demo.gif +0 -0
- remotedesktop-0.20.1/run_server.bat +0 -14
- remotedesktop-0.20.1/src/remotedesktop/autostart.py +0 -63
- remotedesktop-0.20.1/src/remotedesktop/client.py +0 -452
- remotedesktop-0.20.1/src/remotedesktop/preferences.py +0 -49
- remotedesktop-0.20.1/src/remotedesktop/server.py +0 -412
- remotedesktop-0.20.1/tests/test_autostart.py +0 -54
- remotedesktop-0.20.1/tests/test_client_window.py +0 -319
- remotedesktop-0.20.1/tests/test_icon.py +0 -19
- remotedesktop-0.20.1/tests/test_preferences.py +0 -31
- remotedesktop-0.20.1/tests/test_server_window.py +0 -389
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/.github/workflows/ci.yml +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/.gitignore +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/LICENSE +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/run_claude.bat +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/scripts/make_coverage_badge.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/scripts/make_venv.bat +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/clipboard.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/compat.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/config.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/db.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/discovery.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/dxgi.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/frames.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/input_injection.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/logs.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/modal_loop.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/performance.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/protocol.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/tls.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/src/remotedesktop/viewer.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/conftest.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_clipboard.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_compat.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_config.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_discovery.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_dxgi.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_frames.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_input.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_input_injection.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_logs.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_modal_loop.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_performance.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_protocol.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_sharing.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/tests/test_tls.py +0 -0
- {remotedesktop-0.20.1 → remotedesktop-1.1.0}/uv.lock +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# A merged version-bump PR changes src/remotedesktop/__init__.py — the only
|
|
4
|
+
# place the version lives — so a push to master touching it means a new
|
|
5
|
+
# version: tag it v<version> and publish a GitHub release with the built
|
|
6
|
+
# distributions attached. Idempotent: if the release already exists (rerun,
|
|
7
|
+
# or a touch that didn't change the version) it does nothing, so the manual
|
|
8
|
+
# workflow_dispatch trigger is always safe to run. PyPI publishing stays
|
|
9
|
+
# manual (`uv publish`), on explicit request only.
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
push:
|
|
13
|
+
branches: [master]
|
|
14
|
+
paths:
|
|
15
|
+
- src/remotedesktop/__init__.py
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
release:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
permissions:
|
|
22
|
+
contents: write # create the tag and the release
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: astral-sh/setup-uv@v6
|
|
26
|
+
- name: Read version
|
|
27
|
+
id: version
|
|
28
|
+
# __init__.py is docstring + __version__ only; exec-ing it avoids
|
|
29
|
+
# installing the package just to read the version.
|
|
30
|
+
run: |
|
|
31
|
+
version=$(python3 -c "exec(open('src/remotedesktop/__init__.py').read()); print(__version__)")
|
|
32
|
+
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
33
|
+
- name: Create release
|
|
34
|
+
env:
|
|
35
|
+
GH_TOKEN: ${{ github.token }}
|
|
36
|
+
run: |
|
|
37
|
+
tag="v${{ steps.version.outputs.version }}"
|
|
38
|
+
if gh release view "$tag" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
|
|
39
|
+
echo "Release $tag already exists — nothing to do."
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
uv build
|
|
43
|
+
gh release create "$tag" \
|
|
44
|
+
--repo "$GITHUB_REPOSITORY" \
|
|
45
|
+
--target "$GITHUB_SHA" \
|
|
46
|
+
--title "${{ steps.version.outputs.version }}" \
|
|
47
|
+
--generate-notes \
|
|
48
|
+
dist/*
|
|
@@ -0,0 +1,77 @@
|
|
|
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 remote desktop GUI application (PySide6) for Windows computers on the same LAN, per `readme.md`:
|
|
8
|
+
|
|
9
|
+
- **One app, both roles**: every instance can view other computers, and opts in to *sharing* its own screen via a checkbox on the Server tab (persisted; a viewing-only instance binds no ports and creates no TLS credentials). Only one instance runs per user session — a second launch activates the first (`single_instance.py`).
|
|
10
|
+
- **Autodiscovery and connection** of sharing computers on the LAN.
|
|
11
|
+
- **In scope:** desktop screen, keyboard, mouse, and clipboard.
|
|
12
|
+
- **Out of scope:** shared drives, devices, and multimedia (e.g., audio). Smooth playback of fast-changing full-screen content (video, games) is a non-goal: the screen transfer is lossless and optimized for mostly-static desktop work, not for video bandwidth.
|
|
13
|
+
- **Trust model:** on first connection, the user on the sharing side must explicitly permit the client. After that, the client may reconnect whenever sharing is on 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. (Pre-1.0 shipped as two separate apps; 1.0.0 unified them. The wire protocol did not change in that bump.)
|
|
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
|
+
The ruleset also **requires the `ruff`, `ty`, and `test` checks to pass**
|
|
23
|
+
(GitHub Actions, non-strict), so a merge with a red or missing check is
|
|
24
|
+
rejected by GitHub; if a CI job is added or renamed, update the ruleset's
|
|
25
|
+
required checks to match. CI publishes the coverage badge to the
|
|
26
|
+
unprotected `badges` branch (readme references it by raw URL), so no
|
|
27
|
+
workflow ever needs to write to `master`.
|
|
28
|
+
|
|
29
|
+
**Releases are tagged automatically**: merging a version-bump PR (a change
|
|
30
|
+
to `src/remotedesktop/__init__.py`, the only place the version lives)
|
|
31
|
+
triggers `.github/workflows/release.yml`, which tags `v<version>` and
|
|
32
|
+
creates a GitHub release with generated notes and the built sdist/wheel
|
|
33
|
+
attached. It is idempotent (skips if the release exists) and can be run
|
|
34
|
+
manually via workflow_dispatch. PyPI publishing stays manual (`uv publish`),
|
|
35
|
+
only on the user's explicit request.
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
Managed with `uv` (hatchling build backend, src layout):
|
|
40
|
+
|
|
41
|
+
- `uv sync` — create/update the venv with the project and dev dependencies
|
|
42
|
+
- `uv run pytest` — run all tests
|
|
43
|
+
- `uv run pytest tests/test_smoke.py::test_version` — run a single test
|
|
44
|
+
- `uv run remotedesktop` — launch the app. It's a `gui-script`, so it runs detached with no console output; use `uv run python -m remotedesktop` when you need stdout/tracebacks. `--minimized` starts hidden in the tray when sharing is on (what the autostart registration passes).
|
|
45
|
+
- `uv build` — build sdist and wheel into `dist/`
|
|
46
|
+
- `uv publish` — publish to PyPI
|
|
47
|
+
|
|
48
|
+
## Architecture
|
|
49
|
+
|
|
50
|
+
- One PySide6 GUI app: `src/remotedesktop/app.py` (`MainWindow` + `main()`, wired to the `remotedesktop` GUI script in `pyproject.toml`; `__main__.py` makes `python -m remotedesktop` work — the in-app Restart relaunches that). `client.py` holds the viewing-role widgets (`DiscoveryPanel`, `ServerSession`), `server.py` the sharing-role widgets (`SharingTab`, `ViewersTable`) — neither has a window or entry point of its own anymore.
|
|
51
|
+
- **The server opt-in** (`server.py` `SharingTab`) lives on the single "Server" tab, which groups everything server-related in QGroupBox sections: "Sharing this computer" (the `SharingTab`), "Servers on LAN" (client-role inventory, Forget), "Clients on LAN" (server-role inventory, Revoke). The opt-in is a checkbox persisted as the `server_enabled` setting. It owns the `ShareServer` + `DiscoveryResponder` lifecycle — both are created on enable (a fresh `ShareServer` per enable; `close()` leaves teardown state) and torn down on disable, so a viewing-only instance binds no ports and never creates TLS credentials (loaded lazily on first enable). The constructor only restores the checkbox; the host window calls `restore_sharing()` after wiring `statusMessage`/`peerEvent`/`sharingChanged`/`restartRequested`, so no startup status message is emitted before anyone listens. `serving` is checkbox AND actually listening. `revoke_client` works even while not sharing (direct `PairedClients.revoke`).
|
|
52
|
+
- **Tray + lifecycle** (`app.py`): while sharing, closing the window hides to a `QSystemTrayIcon` (menu: Show / Restart / Quit) and sharing continues; not sharing → close quits. `main()` sets `setQuitOnLastWindowClosed(False)`, so **every exit path must reach `QApplication.quit()`** (plain close, tray Quit, restart). `tray_available=` is injectable — tests always pass an explicit value and never call `isSystemTrayAvailable()`. `--minimized` (used by the autostart registration) starts hidden in the tray when sharing is on. `single_instance.py` (`SingleInstance`, QLocalServer-based) enforces one instance per user session; a second launch tells the first to `bring_to_front()` and exits. `autostart.py` registers `remotedesktop.exe --minimized` under HKCU Run value `"remotedesktop"` and migrates the pre-1.0 `"remotedesktop-server"` value at startup (`migrate_legacy`).
|
|
53
|
+
- The remote desktop view is a widget, `ViewerWidget` in `src/remotedesktop/viewer.py`. Screen display and keyboard/mouse/clipboard forwarding belong in this widget, not in the window. **Multiple simultaneous server connections**: each is a `ServerSession` in `client.py` (its own `ShareClient` + `ViewerWidget`), shown as a closable tab at the front of `MainWindow`'s central `QTabWidget`, titled with the server's reported hostname (fixed tabs get their close buttons stripped). Sessions are keyed by `host:port`; activating an already-known server reconnects in its existing tab, activating a connected one just focuses it. The window title lists the connected server names and a "— sharing" suffix while serving (`_update_window_title`), and the status bar follows the currently selected session tab (`status_text` per session). The `ClipboardSync`, inventories, and Connection log are shared across sessions; `client_performance.reset()` is only called when no *other* session is connected (resetting detaches every stream). Per-session status log lines are prefixed `[name]`.
|
|
54
|
+
- **Two `PerformanceMonitor`s, never one**: `MainWindow.client_performance` (viewing sessions) and `server_performance` (the SharingTab's viewers). `ShareServer.close()` resets its monitor, so sharing a monitor across roles would wipe the other role's graphs. The Performance tab has "Viewing"/"Sharing" sub-tabs (`MainWindow.performance_pages`); `PreferencesTab` takes the list of monitors and applies the history-window setting to all.
|
|
55
|
+
- **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. A sharing instance runs a `DiscoveryResponder` thread while sharing is enabled; the `DiscoveryPanel` (dock in `MainWindow`) calls the blocking `discover_servers()` on a worker thread and delivers results to the GUI via a queued signal. `DEFAULT_CONNECT_PORT` is 48654. An instance that shares and scans discovers itself — `DiscoveryPanel`'s `is_self=` callable labels that entry "(this computer)" (never hidden).
|
|
56
|
+
- 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.
|
|
57
|
+
- **Screen sharing** (`sharing.py` + `protocol.py` + `frames.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, full frames, or inter-frame deltas; malformed input aborts the socket). `ShareServer` owns one QTimer that grabs the primary screen at full resolution via `_capture()` (a seam tests override to drive deterministic frame content) and fans out to all clients, skipping clients whose send buffer is backlogged; the timer only runs while clients are connected. Capture is DXGI desktop duplication (`dxgi.py`, hand-rolled ctypes COM — ~10 ms per changed 4K frame, ~0 when idle, and "unchanged" is signalled by returning the same QImage object so the diff is skipped) with automatic fallback to `QScreen.grabWindow` (~96 ms) whenever duplication is unavailable or lost (secure desktop, RDP, display-mode change, CI); unit tests fake the duplication (`FakeDuplication` in `tests/test_dxgi.py`) and the real-hardware test skips where DXGI is unavailable. **Frame transfer is lossless and inter-frame compressed** (`frames.py`): consecutive captures are diffed in 64-row bands (memoryview compares), and only changed bands ship as PNG in a delta payload the client patches onto its previous frame; an unchanged screen sends nothing. Full PNG keyframes go to clients that just joined, just recovered from a send backlog, or requested one (a `{"type": "keyframe"}` message — sent when a delta fails to apply, so desync self-heals). The capability is negotiated via `"delta": true` in the client hello; clients without it (0.5.0) get full JPEG frames every tick because they force-decode frames as JPEG — never send them PNG. Each variant (legacy JPEG / keyframe PNG / delta) is encoded at most once per tick and shared across clients. `ShareClient` decodes/patches to QImage for the viewer, which displays it scaled with a cached `SmoothTransformation` copy (full resolution is always delivered; scaling happens client-side only).
|
|
58
|
+
- **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 app opens one connection and shares it. `MainWindow` takes a `connection=` keyword so tests inject a temp-file DB and never touch the real one (see `tests/test_main_window.py` / `test_sharing_tab.py`). Tests pass `db.connect(tmp_path/"x.db")` (or omit for in-memory), and use **separate DB files per simulated machine**.
|
|
59
|
+
- **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; `SharingTab` implements it as a modal stay-on-top QMessageBox. Tests inject stores backed by a temp DB and explicit identities so they never touch real data or prompt.
|
|
60
|
+
- **Status/debug logging is a feature**: `ShareServer`, `ShareClient`, `SharingTab`, and `DiscoveryPanel` emit human-readable `status`/`statusMessage` signals for every connection phase, shown in the window's 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. Everything the pane shows is also written to a rotating debug log file (`logs.py`; `logs.init_logging("remotedesktop")` is called only by `app.main()`, under `platformdirs.user_log_dir("remotedesktop")` — `%LOCALAPPDATA%\remotedesktop\Logs\remotedesktop.log` on Windows; pre-1.0 `client.log`/`server.log` files just age out). Lower-level detail (socket state changes, protocol aborts, frame-drop/backlog, discovery datagram drops) goes straight to `logging.getLogger("remotedesktop.*")` at debug/warning level — silent failure paths should log there even when a status message would be too noisy. Library code and tests never call `init_logging`; without a handler those loggers are no-ops.
|
|
61
|
+
- **Connection inventory** (`inventory.py`): the Server tab has two grouped tables ("Clients on LAN" for the sharing role, "Servers on LAN" for the viewing role), each backed by its own `ConnectionInventory` — one `PeerRecord` per peer with attempts count, current state, first/last seen. The sharing side is driven by `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), forwarded through `SharingTab.peerEvent`. The viewing side is driven by `MainWindow`'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.
|
|
62
|
+
- **Revoking access**: `InventoryTab` takes an optional `(action_label, action_callback)`; each row gets its own button labeled `action_label` (in the trailing stretch column) whose callback receives that row's peer key. "Clients on LAN" wires "Revoke" → `SharingTab.revoke_client(client_id)` (via `ShareServer.revoke_client` while sharing — removes the token via `PairedClients.revoke`, disconnects any live stream with a `denied: access revoked`, and `_drop` emits a "revoked" peer event; a `_revoked` set distinguishes it from a plain disconnect — or directly via `PairedClients.revoke` while not sharing). "Servers on LAN" wires "Forget" → `KnownServers.forget(key)` + disconnect + `ConnectionInventory.remove(key)`. **Both delete the peer's row from the table and the DB outright** (`MainWindow._record_server_peer` turns a "revoked" event into a `remove`); a rescan or new connection attempt records the peer afresh. After either, the next connection needs a fresh approval. The roles use *separate tables* (`server_peers` / `client_peers`, selected via the `table=` arg) so they don't commingle; `peers` is the default table used by tests. `ConnectionInventory` never lets a DB error break connectivity (load/save are wrapped).
|
|
63
|
+
- 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.
|
|
64
|
+
- **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.
|
|
65
|
+
- **Modal-loop pump** (`modal_loop.py`): a Windows title-bar drag (or click-and-hold) puts the window's thread into a native modal move/size loop where Qt's dispatcher stops — for a sharing instance this deadlocked when a *remote* click grabbed the window's own title bar (the mouse-up sat unread on the socket). `MainWindow.nativeEvent` feeds `ModalLoopPump`, which runs a native `SetTimer` from `WM_NCLBUTTONDOWN` (a press-and-hold that never moves blocks in DefWindowProc's click tracking without ever sending `WM_ENTERSIZEMOVE`) until `WM_CAPTURECHANGED`/`WM_EXITSIZEMOVE`, whose callback pumps Qt events (user input excluded). The caption buttons (min/max/close) are the exception — their tracking loop dispatches only mouse messages, so no timer can pump inside it; the pump instead performs those via its `caption_action` callback on press and consumes the message (nativeEvent must return True when `handle_native_event` does), so DefWindowProc's button tracking never starts. Tests inject a fake `timers` backend — never let them create real Win32 timers.
|
|
66
|
+
- **Log exchange**: either side can fetch the peer's debug log over the connection (`{"type": "log_request"}` → `{"type": "log", "text": ...}`, admitted streams only). `ShareServer`/`ShareClient` take a `log_provider=` callable (the app passes `logs.read_log_tail("remotedesktop")`, capped at `logs.TAIL_BYTES`); a missing provider/file answers with a placeholder rather than hanging the requester. The Connection log tab has both buttons — "Get server log" asks the server of the current session tab, "Get client log" asks the most recently admitted viewer; the reply opens a `logs.PeerLogDialog`.
|
|
67
|
+
- **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.
|
|
68
|
+
- **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.
|
|
69
|
+
- **Performance monitoring** (`performance.py` + `preferences.py`): `PerformanceMonitor` is an opt-in collaborator (`performance=` on `ShareServer`/`ShareClient`, the `clipboard=` pattern — plain sharing tests leave it off). While admitted streams are attached, a 1 s QTimer samples `MessageStream.bytes_sent/bytes_received` counters into rolling `MetricSeries` (bandwidth, aggregated across streams via per-stream baselines) and pings **every** attached stream (each ping id maps to its stream; a pong is honored only on the stream it was sent to). The graphs' RTT series follow only the *active* (most recently admitted) stream; per-stream send/recv/RTT snapshots come from `metrics_for(stream)` and feed the Server tab's `ViewersTable` (`server.py`; it takes the monitor and follows the current ShareServer via `set_share_server`, empty when None), which joins them with `ShareServer.viewers()` — identity fields (`user`/`host`/`os`) that the client hello reports (`_client_details()`; missing on old clients, shown as dashes). The ping is: `{"type": "ping", "id", "rtt"}` / `{"type": "pong", "id"}` — each side measures its own RTT and piggybacks it on its next ping so both ends graph both directions; peers without the feature ignore pings harmlessly (RTT graphs show "no data"). Ping/pong is admitted-streams-only on the server, like input/clipboard. The "Performance" tab (`PerformanceTab`, custom QPainter graphs, one per role sub-tab) schedules **no paint work while hidden** (gated on `isVisible()`, repaint on `showEvent`); the "Preferences" tab's history-window setting persists via the `Settings` store (`performance_window_seconds`, default 120). The monitor's timer runs only while streams are attached; all timing is `time.monotonic()` with an injectable `clock=` for tests — never monkeypatch `time.monotonic` (the test `pump()` helper depends on it).
|
|
70
|
+
- **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 GUI (and clipboard tests) enables it; **plain sharing/input tests must leave it off to avoid touching the real OS clipboard.** The app shares ONE `ClipboardSync` across both roles (one OS clipboard): a real local copy fans out to connected servers and own viewers; a peer-received payload records its signature before applying, so it never re-emits `changed` and cannot loop between the roles. 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.
|
|
71
|
+
- 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. **Versioning is strict semver** (`compat.py`, using the `semver` package): major = client/server compatibility contract, minor = backward-compatible features, patch = bug fixes — bump the major when the wire protocol or behavior breaks older peers. Both sides compare majors on every connection (hello/welcome carry `app_version`) and show a strong warning on mismatch (server status log + ⚠ in the viewers table's Version column; client log + non-modal dialog + status-bar marker) but never block the connection. Peers too old to report a version produce no warning.
|
|
72
|
+
- Widget tests need a `QApplication`; use the session-scoped `qapp` fixture in `tests/conftest.py`.
|
|
73
|
+
|
|
74
|
+
## Environment Notes
|
|
75
|
+
|
|
76
|
+
- Target platform is Windows; development happens on Windows 11. Requires Python >=3.14.
|
|
77
|
+
- **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.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remotedesktop
|
|
3
|
-
Version:
|
|
4
|
-
Summary: Remote desktop
|
|
3
|
+
Version: 1.1.0
|
|
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
|
|
7
7
|
Project-URL: PyPI, https://pypi.org/project/remotedesktop/
|
|
@@ -29,49 +29,55 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|

|
|
30
30
|

|
|
31
31
|
|
|
32
|
-
**Lossless, low-latency remote desktop for Windows computers on your LAN — pure Python, zero configuration.**
|
|
32
|
+
**Lossless, low-latency remote desktop for Windows computers on your LAN — one app, pure Python, zero configuration.**
|
|
33
33
|
|
|
34
|
-
Run the
|
|
35
|
-
on the
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
Run the same app on every computer. Tick *Share this computer's screen on
|
|
35
|
+
the LAN* on the ones you want to reach, and they appear automatically in
|
|
36
|
+
every other instance's *Servers* panel: the first connection is approved
|
|
37
|
+
with one click on the shared computer, and from then on it reconnects
|
|
38
|
+
instantly. View several computers at once — each in its own tab — while
|
|
39
|
+
optionally sharing your own screen at the same time; closing the window
|
|
40
|
+
while sharing keeps serving from the system tray. The screen stream is
|
|
38
41
|
pixel-exact at full resolution — built for documents, code, and terminals
|
|
39
42
|
rather than video — captured with DXGI desktop duplication and delta-compressed
|
|
40
43
|
so only the parts of the screen that changed are sent. No Windows RDP, no
|
|
41
|
-
Microsoft accounts, no cloud: just
|
|
44
|
+
Microsoft accounts, no cloud: just one app and your LAN.
|
|
42
45
|
|
|
43
46
|
## See it in action
|
|
44
47
|
|
|
45
|
-
###
|
|
48
|
+
### Viewing
|
|
46
49
|
|
|
47
|
-

|
|
48
51
|
|
|
49
|
-
The
|
|
52
|
+
The app discovers sharing computers on the LAN, connects, and streams each
|
|
50
53
|
desktop live in a tab named after that computer — click into the view and
|
|
51
54
|
your mouse and keyboard control the remote machine. Connect to several
|
|
52
|
-
|
|
55
|
+
computers at once and each gets its own tab; the window title lists every
|
|
53
56
|
connected computer. The *Performance* tab graphs bandwidth and round-trip
|
|
54
57
|
time with live statistics.
|
|
55
58
|
|
|
56
|
-
###
|
|
59
|
+
### Sharing
|
|
57
60
|
|
|
58
|
-

|
|
59
62
|
|
|
60
|
-
The
|
|
61
|
-
name, computer, OS) and how
|
|
62
|
-
|
|
63
|
+
The *Server* tab is where an instance opts in to being shared. It shows
|
|
64
|
+
every connected viewer — who they are (login name, computer, OS) and how
|
|
65
|
+
the connection is doing (bandwidth, round-trip time with
|
|
66
|
+
mean/min/max/p99/jitter over the recent window) — along with the history
|
|
67
|
+
of servers seen and clients paired, with one-click *Forget* / *Revoke*.
|
|
63
68
|
|
|
64
69
|
## Features
|
|
65
70
|
|
|
66
|
-
-
|
|
67
|
-
-
|
|
71
|
+
- 🧩 **One app, both roles** — every install can view other computers and share its own screen at the same time; sharing is an opt-in checkbox on the *Server* tab, and only one instance runs per computer (launching it again just raises the existing window).
|
|
72
|
+
- 🔍 **Autodiscovery** — sharing computers announce themselves over UDP; the app lists every one on the LAN, no addresses to type.
|
|
73
|
+
- 🗂️ **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.
|
|
68
74
|
- 🖥️ **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.
|
|
69
75
|
- ⌨️🖱️ **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.
|
|
70
76
|
- 📋 **Two-way clipboard** — text and images copied on either machine appear on the other.
|
|
71
77
|
- 🔒 **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.
|
|
72
78
|
- 📊 **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.
|
|
73
79
|
- 🔁 **Robust connections** — dead connections are detected and dropped within seconds, and approved clients reconnect automatically without ceremony.
|
|
74
|
-
- 🚀 **Hands-off operation** — optional start-at-login (per-user, no admin rights) and a *Restart
|
|
80
|
+
- 🚀 **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.
|
|
75
81
|
- 🗃️ **Persistent peer inventory** — both apps keep a SQLite-backed history of every peer seen on the LAN, with one-click *revoke access* / *forget server*.
|
|
76
82
|
|
|
77
83
|
In scope: screen, keyboard, mouse, and clipboard. Out of scope: shared
|
|
@@ -91,21 +97,24 @@ or, with [uv](https://docs.astral.sh/uv/):
|
|
|
91
97
|
uv tool install remotedesktop
|
|
92
98
|
```
|
|
93
99
|
|
|
94
|
-
Or run straight from a clone of this repository: double-click
|
|
95
|
-
|
|
96
|
-
viewing computer — each prepares the environment on first use and launches
|
|
97
|
-
the app.
|
|
100
|
+
Or run straight from a clone of this repository: double-click `run.bat` —
|
|
101
|
+
it prepares the environment on first use and launches the app.
|
|
98
102
|
|
|
99
103
|
## Quick start
|
|
100
104
|
|
|
101
|
-
1.
|
|
102
|
-
2. On the
|
|
103
|
-
|
|
104
|
-
3.
|
|
105
|
-
|
|
105
|
+
1. Run `remotedesktop` on both computers.
|
|
106
|
+
2. On the computer to share, open the *Server* tab and tick
|
|
107
|
+
*Share this computer's screen on the LAN*.
|
|
108
|
+
3. On the viewing computer, the shared computer appears in the *Servers*
|
|
109
|
+
panel — double-click it.
|
|
110
|
+
4. Approve the connection in the dialog that pops up on the shared
|
|
111
|
+
computer. That's it — future connections from that computer need no
|
|
112
|
+
approval.
|
|
106
113
|
|
|
107
|
-
From a terminal in a clone of this repo, the
|
|
108
|
-
`uv run remotedesktop
|
|
114
|
+
From a terminal in a clone of this repo, the app runs with
|
|
115
|
+
`uv run remotedesktop` (or `uv run python -m remotedesktop` for console
|
|
116
|
+
output). While sharing, closing the window keeps the app serving from the
|
|
117
|
+
system tray; quit from the tray menu.
|
|
109
118
|
|
|
110
119
|
## Security model
|
|
111
120
|
|
|
@@ -114,13 +123,14 @@ generates once and keeps. The trust model is tuned for a **trusted LAN**:
|
|
|
114
123
|
certificates are trusted on first use and a changed fingerprint is logged
|
|
115
124
|
rather than blocking the connection, favoring reliable reconnection over
|
|
116
125
|
strict certificate checking. Unapproved clients are limited to small
|
|
117
|
-
handshake messages until the
|
|
118
|
-
at any time from the
|
|
126
|
+
handshake messages until the user at the shared computer admits them;
|
|
127
|
+
access can be revoked at any time from the *Server* tab. There is
|
|
128
|
+
no dependency
|
|
119
129
|
on Windows RDP or any Microsoft-based authentication.
|
|
120
130
|
|
|
121
131
|
## How it works
|
|
122
132
|
|
|
123
|
-
All of this is pure Python — the
|
|
133
|
+
All of this is pure Python — the GUI is PySide6 (Qt), and the two places
|
|
124
134
|
that need to talk to Windows directly (screen capture and input injection)
|
|
125
135
|
call the Win32/COM APIs through `ctypes`, so there are no native extensions
|
|
126
136
|
to compile.
|
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "remotedesktop"
|
|
3
3
|
dynamic = ["version"]
|
|
4
|
-
description = "Remote desktop
|
|
4
|
+
description = "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
|
readme = "readme.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
authors = [{ name = "James Abel", email = "j@abel.co" }]
|
|
@@ -27,8 +27,7 @@ Repository = "https://github.com/jamesabel/remotedesktop"
|
|
|
27
27
|
PyPI = "https://pypi.org/project/remotedesktop/"
|
|
28
28
|
|
|
29
29
|
[project.gui-scripts]
|
|
30
|
-
remotedesktop
|
|
31
|
-
remotedesktop-server = "remotedesktop.server:main"
|
|
30
|
+
remotedesktop = "remotedesktop.app:main"
|
|
32
31
|
|
|
33
32
|
[dependency-groups]
|
|
34
33
|
dev = [
|
|
@@ -6,49 +6,55 @@
|
|
|
6
6
|

|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
**Lossless, low-latency remote desktop for Windows computers on your LAN — pure Python, zero configuration.**
|
|
10
|
-
|
|
11
|
-
Run the
|
|
12
|
-
on the
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
**Lossless, low-latency remote desktop for Windows computers on your LAN — one app, pure Python, zero configuration.**
|
|
10
|
+
|
|
11
|
+
Run the same app on every computer. Tick *Share this computer's screen on
|
|
12
|
+
the LAN* on the ones you want to reach, and they appear automatically in
|
|
13
|
+
every other instance's *Servers* panel: the first connection is approved
|
|
14
|
+
with one click on the shared computer, and from then on it reconnects
|
|
15
|
+
instantly. View several computers at once — each in its own tab — while
|
|
16
|
+
optionally sharing your own screen at the same time; closing the window
|
|
17
|
+
while sharing keeps serving from the system tray. The screen stream is
|
|
15
18
|
pixel-exact at full resolution — built for documents, code, and terminals
|
|
16
19
|
rather than video — captured with DXGI desktop duplication and delta-compressed
|
|
17
20
|
so only the parts of the screen that changed are sent. No Windows RDP, no
|
|
18
|
-
Microsoft accounts, no cloud: just
|
|
21
|
+
Microsoft accounts, no cloud: just one app and your LAN.
|
|
19
22
|
|
|
20
23
|
## See it in action
|
|
21
24
|
|
|
22
|
-
###
|
|
25
|
+
### Viewing
|
|
23
26
|
|
|
24
|
-

|
|
25
28
|
|
|
26
|
-
The
|
|
29
|
+
The app discovers sharing computers on the LAN, connects, and streams each
|
|
27
30
|
desktop live in a tab named after that computer — click into the view and
|
|
28
31
|
your mouse and keyboard control the remote machine. Connect to several
|
|
29
|
-
|
|
32
|
+
computers at once and each gets its own tab; the window title lists every
|
|
30
33
|
connected computer. The *Performance* tab graphs bandwidth and round-trip
|
|
31
34
|
time with live statistics.
|
|
32
35
|
|
|
33
|
-
###
|
|
36
|
+
### Sharing
|
|
34
37
|
|
|
35
|
-

|
|
36
39
|
|
|
37
|
-
The
|
|
38
|
-
name, computer, OS) and how
|
|
39
|
-
|
|
40
|
+
The *Server* tab is where an instance opts in to being shared. It shows
|
|
41
|
+
every connected viewer — who they are (login name, computer, OS) and how
|
|
42
|
+
the connection is doing (bandwidth, round-trip time with
|
|
43
|
+
mean/min/max/p99/jitter over the recent window) — along with the history
|
|
44
|
+
of servers seen and clients paired, with one-click *Forget* / *Revoke*.
|
|
40
45
|
|
|
41
46
|
## Features
|
|
42
47
|
|
|
43
|
-
-
|
|
44
|
-
-
|
|
48
|
+
- 🧩 **One app, both roles** — every install can view other computers and share its own screen at the same time; sharing is an opt-in checkbox on the *Server* tab, and only one instance runs per computer (launching it again just raises the existing window).
|
|
49
|
+
- 🔍 **Autodiscovery** — sharing computers announce themselves over UDP; the app lists every one on the LAN, no addresses to type.
|
|
50
|
+
- 🗂️ **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.
|
|
45
51
|
- 🖥️ **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.
|
|
46
52
|
- ⌨️🖱️ **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.
|
|
47
53
|
- 📋 **Two-way clipboard** — text and images copied on either machine appear on the other.
|
|
48
54
|
- 🔒 **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.
|
|
49
55
|
- 📊 **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.
|
|
50
56
|
- 🔁 **Robust connections** — dead connections are detected and dropped within seconds, and approved clients reconnect automatically without ceremony.
|
|
51
|
-
- 🚀 **Hands-off operation** — optional start-at-login (per-user, no admin rights) and a *Restart
|
|
57
|
+
- 🚀 **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.
|
|
52
58
|
- 🗃️ **Persistent peer inventory** — both apps keep a SQLite-backed history of every peer seen on the LAN, with one-click *revoke access* / *forget server*.
|
|
53
59
|
|
|
54
60
|
In scope: screen, keyboard, mouse, and clipboard. Out of scope: shared
|
|
@@ -68,21 +74,24 @@ or, with [uv](https://docs.astral.sh/uv/):
|
|
|
68
74
|
uv tool install remotedesktop
|
|
69
75
|
```
|
|
70
76
|
|
|
71
|
-
Or run straight from a clone of this repository: double-click
|
|
72
|
-
|
|
73
|
-
viewing computer — each prepares the environment on first use and launches
|
|
74
|
-
the app.
|
|
77
|
+
Or run straight from a clone of this repository: double-click `run.bat` —
|
|
78
|
+
it prepares the environment on first use and launches the app.
|
|
75
79
|
|
|
76
80
|
## Quick start
|
|
77
81
|
|
|
78
|
-
1.
|
|
79
|
-
2. On the
|
|
80
|
-
|
|
81
|
-
3.
|
|
82
|
-
|
|
82
|
+
1. Run `remotedesktop` on both computers.
|
|
83
|
+
2. On the computer to share, open the *Server* tab and tick
|
|
84
|
+
*Share this computer's screen on the LAN*.
|
|
85
|
+
3. On the viewing computer, the shared computer appears in the *Servers*
|
|
86
|
+
panel — double-click it.
|
|
87
|
+
4. Approve the connection in the dialog that pops up on the shared
|
|
88
|
+
computer. That's it — future connections from that computer need no
|
|
89
|
+
approval.
|
|
83
90
|
|
|
84
|
-
From a terminal in a clone of this repo, the
|
|
85
|
-
`uv run remotedesktop
|
|
91
|
+
From a terminal in a clone of this repo, the app runs with
|
|
92
|
+
`uv run remotedesktop` (or `uv run python -m remotedesktop` for console
|
|
93
|
+
output). While sharing, closing the window keeps the app serving from the
|
|
94
|
+
system tray; quit from the tray menu.
|
|
86
95
|
|
|
87
96
|
## Security model
|
|
88
97
|
|
|
@@ -91,13 +100,14 @@ generates once and keeps. The trust model is tuned for a **trusted LAN**:
|
|
|
91
100
|
certificates are trusted on first use and a changed fingerprint is logged
|
|
92
101
|
rather than blocking the connection, favoring reliable reconnection over
|
|
93
102
|
strict certificate checking. Unapproved clients are limited to small
|
|
94
|
-
handshake messages until the
|
|
95
|
-
at any time from the
|
|
103
|
+
handshake messages until the user at the shared computer admits them;
|
|
104
|
+
access can be revoked at any time from the *Server* tab. There is
|
|
105
|
+
no dependency
|
|
96
106
|
on Windows RDP or any Microsoft-based authentication.
|
|
97
107
|
|
|
98
108
|
## How it works
|
|
99
109
|
|
|
100
|
-
All of this is pure Python — the
|
|
110
|
+
All of this is pure Python — the GUI is PySide6 (Qt), and the two places
|
|
101
111
|
that need to talk to Windows directly (screen capture and input injection)
|
|
102
112
|
call the Win32/COM APIs through `ctypes`, so there are no native extensions
|
|
103
113
|
to compile.
|
|
@@ -1,14 +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
|
|
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.exe" %*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""The "About" tab
|
|
1
|
+
"""The "About" tab of the app window.
|
|
2
2
|
|
|
3
3
|
Shows the same metadata the PyPI page shows — summary, author, license,
|
|
4
4
|
Python requirement, links — read from the installed package's metadata via
|
|
@@ -17,9 +17,10 @@ _HOMEPAGE = "https://github.com/jamesabel/remotedesktop"
|
|
|
17
17
|
_PYPI = "https://pypi.org/project/remotedesktop/"
|
|
18
18
|
_FALLBACKS = {
|
|
19
19
|
"Summary": (
|
|
20
|
-
"Remote desktop
|
|
21
|
-
"
|
|
22
|
-
"
|
|
20
|
+
"Remote desktop app for Windows computers on the same LAN, with "
|
|
21
|
+
"autodiscovery. View other computers and optionally share this one — "
|
|
22
|
+
"screen, keyboard, mouse, and clipboard, without RDP or Microsoft "
|
|
23
|
+
"authentication."
|
|
23
24
|
),
|
|
24
25
|
"Author-email": "James Abel <j@abel.co>",
|
|
25
26
|
"License-Expression": "MIT",
|
|
@@ -64,7 +65,7 @@ class AboutTab(QWidget):
|
|
|
64
65
|
Qt.TextInteractionFlag.TextBrowserInteraction
|
|
65
66
|
)
|
|
66
67
|
logo = QLabel()
|
|
67
|
-
logo.setPixmap(icon.app_icon("
|
|
68
|
+
logo.setPixmap(icon.app_icon("app").pixmap(48, 48))
|
|
68
69
|
layout = QVBoxLayout(self)
|
|
69
70
|
layout.addWidget(logo)
|
|
70
71
|
layout.addWidget(body)
|