-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ HTPC (Linux or Windows) │
│ │
│ ┌─────────────────┐ ┌──────────────────────────────────┐ │
│ │ QR Overlay │ │ FastAPI server :8765 │ │
│ │ GTK3 (Linux) │◀────│ WebSocket /ws │ │
│ │ tkinter (Win) │ └──────────────┬───────────────────┘ │
│ └─────────────────┘ │ │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌───────────────────┐ │
│ │ Linux backend │ │ Windows backend │ │
│ │ X11: pynput │ │ pynput media_* │ │
│ │ Wayland: evdev│ │ Win32 VK codes │ │
│ └──────┬───────┘ └────────┬──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ Xlib / /dev/uinput Win32 / HID │
└──────────────────────────────────────────────────────────────────┘
▲
│ WebSocket (LAN)
│
┌─────────────────────────────┴────────────────────────────────────┐
│ Phone — any browser │
│ │
│ Service Worker ──▶ (cache-first for UI assets) │
│ ws.js ──▶ glide-connect.jsx ──▶ glide-controller.jsx │
└───────────────────────────────────────────────────────────────────┘
- Detects platform (
sys.platform) and$XDG_SESSION_TYPE, initialises the right input backend - Starts uvicorn (FastAPI) in a daemon thread with its own asyncio event loop
- Blocks the main thread on
Gtk.main()(Linux) orroot.mainloop()(Windows tkinter) - Shuts down cleanly on SIGTERM / SIGINT
-
GET /qr.png— serves the QR code image -
GET /health— returns{"status": "ok", "clients": N} -
WS /ws— WebSocket endpoint; callsstate.client_connected()on open,state.client_disconnected()on close - Mounts
web/as a static file directory for the phone UI - Routes all incoming WebSocket messages to the input backend
WebSocket message types:
type |
Required fields | Action |
|---|---|---|
mouse_move |
dx, dy
|
Move cursor |
mouse_click |
button (left/right/middle) |
Click |
scroll |
dy |
Scroll |
key |
key (string) |
Press a named key |
text |
text |
Type a string |
launch |
app |
Launch an app by name |
A thread-safe AppState object passed to both the FastAPI app and the overlay. Maintains client_count and server_url. Subscribers (the overlay) are notified on every change via a simple callback list.
Linux: GTK3 window (gi.repository.Gtk) — uses GLib.idle_add() to schedule state callbacks safely from the WebSocket thread. Falls back to a terminal-only mode if GTK is unavailable.
Windows: tkinter window (stdlib) — uses root.after(0, ...) for thread-safety. Closing the window hides it (root.withdraw) rather than killing the server process.
Show policy (both platforms):
- Shows on startup if no device has ever connected
- Hides immediately when
client_count > 0 - On disconnect, starts an
IDLE_TIMEOUT_SECONDScountdown (default 7200 s / 2 h) - Timer cancels if a device reconnects before it fires
- Timer fires → shows popup only if
client_countis still 0
base.py — Abstract InputBackend class + shared launch_app() dispatcher that routes to _launch_app_linux() (xdg-open) or _launch_app_win() (os.startfile()).
linux.py — X11 (pynput) and Wayland (evdev/uinput) backends
- X11:
pynput.mouse.Controller+pynput.keyboard.Controllerwith XF86 keysyms - Wayland: virtual
UInputdevice; text viawtype→ydotool→wl-copy+ Ctrl+V
windows.py — Windows backend using pynput Win32 VK codes
-
Key.media_play_pause,Key.media_next,Key.media_volume_up, etc. -
press_key("sleep")callsrundll32.exe powrprof.dll,SetSuspendState 0,1,0 - seek keys mapped to Left/Right arrows (universal across VLC, Kodi, browsers)
__init__.py — Factory function
def get_backend() -> InputBackend:
if sys.platform == "win32":
return _init_windows()
session = os.environ.get("XDG_SESSION_TYPE", "x11").lower()
return _init_wayland() if session == "wayland" else _init_x11()Built with React 18 + Babel standalone (in-browser JSX transform). All dependencies served locally — no CDN, works fully offline on the LAN.
| File | Role |
|---|---|
index.html |
Entry point — loads React, Babel, ws.js, JSX; registers service worker |
manifest.json |
Web App Manifest — enables PWA install (name, icons, display: standalone) |
sw.js |
Service Worker — cache-first for 14 static assets, pass-through for WS/QR |
static/glide-tokens.css |
Design tokens: OLED dark (#07090c), teal accent (#00C896) |
static/glide-ui.jsx |
GIcon SVG icon set, GSlider component, GlideMark logo |
static/ws.js |
WebSocket manager — window.WS singleton, rAF-batched mouse moves |
static/glide-connect.jsx |
Connection flow UI (connecting → connected → controller) |
static/glide-controller.jsx |
Full controller: trackpad, scroll strip, drawer, keyboard sheet |
static/icons/ |
PWA icons: SVG, 192×512 PNG (maskable), Apple touch icon 180×180 |
-
Install: pre-caches all 14 static assets +
skipWaiting()(activates immediately) -
Activate: deletes stale caches from previous versions +
clients.claim() -
Fetch: cache-first for everything; pass-through for WS upgrades,
/qr.png,/health, and cross-origin requests
- Connects to
ws://<host>/wson page load - Reconnects automatically with exponential backoff on drop
- Mouse move events are batched with
requestAnimationFrame— at most one WS message per frame (~60/s) regardless of touch event rate
Linux:
Main thread: GTK main loop (Gtk.main)
└── GLib.idle_add callbacks (overlay state changes)
└── GLib.timeout_add_seconds (idle timer)
Daemon thread: asyncio event loop (uvicorn)
└── WebSocket handler (async)
└── Input backend calls (synchronous, brief)
Windows:
Main thread: tkinter main loop (root.mainloop)
└── root.after(0, ...) callbacks (overlay state changes)
└── root.after(ms, ...) (idle timer)
Daemon thread: asyncio event loop (uvicorn)
└── WebSocket handler (async)
└── Input backend calls (synchronous, brief)
All cross-thread communication goes through AppState (thread-safe with threading.Lock) and the platform-appropriate main-thread scheduler (GLib.idle_add or root.after).
The GitHub Actions release workflow (.github/workflows/release.yml) triggers on any v* tag push and runs 4 parallel build jobs:
| Job | Docker image | Output |
|---|---|---|
build-deb |
debian:bookworm-slim |
htpc-remote_VERSION_all.deb |
build-rpm |
fedora:latest |
htpc-remote-VERSION-1.noarch.rpm |
build-windows-zip |
GitHub runner (ubuntu) | htpc-remote-VERSION-windows.zip |
build-linux-tar |
GitHub runner (ubuntu) | htpc-remote-VERSION-linux.tar.gz |
All 4 artifacts are attached to the GitHub Release automatically.
See Building from Source for the full build process.
Getting started
Using Glide
Reference
Help