-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
┌─────────────────────────────────────────────────────────┐
│ HTPC — Pop!_OS / Debian │
│ │
│ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ GTK3 Overlay│ │ FastAPI server :7000 │ │
│ │ (QR popup) │◀────│ WebSocket /ws │ │
│ └──────────────┘ └──────────────┬───────────────┘ │
│ │ │
│ ┌────────────┴────────────┐ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────┐│
│ │ X11 backend│ │Wayland backend││
│ │ (pynput) │ │(evdev/uinput) ││
│ └──────┬──────┘ └──────┬───────┘│
│ │ │ │
│ ▼ ▼ │
│ Xlib / X11 /dev/uinput │
└─────────────────────────────────────────────────────────┘
▲
│ WebSocket (LAN)
│
┌─────────────────────────┴───────────────────────────────┐
│ Phone — any browser │
│ │
│ ws.js ──▶ glide-connect.jsx ──▶ glide-controller │
└──────────────────────────────────────────────────────────┘
- Detects
$XDG_SESSION_TYPEand 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()(GTK requirement) - 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 GTK overlay. Maintains client_count and server_url. Subscribers (the overlay) are notified on every change via a simple callback list.
- Runs on the GTK main thread — never blocked or written from another thread
- Uses
GLib.idle_add()to schedule state-change callbacks safely from the WebSocket thread -
Show policy:
- Shows on startup if no device has ever connected
- Hides immediately when
client_count > 0 - On disconnect, starts a
GLib.timeout_add_seconds(IDLE_TIMEOUT_SECONDS)timer - Timer cancels if a device reconnects before it fires
- Timer fires → show popup only if
client_countis still 0
base.py — Abstract InputBackend class defining the interface:
move_mouse(dx, dy)click(button)scroll(dy)type_text(text)press_key(key)-
launch_app(name)— shared implementation usingAPP_COMMANDS
x11.py — pynput backend
- Mouse via
pynput.mouse.Controller - Keyboard via
pynput.keyboard.Controllerwith XF86 keysyms
wayland.py — evdev/uinput backend
- Creates a virtual
UInputdevice on init with all required capabilities - Mouse as relative mouse events (
REL_X,REL_Y) - Keyboard as
KEY_*Linux keycodes - Text input:
wtype→ydotool→wl-copy+ Ctrl+V
__init__.py — Factory function
def get_backend() -> InputBackend:
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, then the JSX files |
static/glide-tokens.css |
Design tokens: OLED dark (#07090c), Pop!_OS teal accent |
static/glide-ui.jsx |
GIcon SVG icon set, GSlider component |
static/ws.js |
WebSocket manager — window.WS singleton with 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 |
- 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 - Exposes
window.WS.send(),window.WS.queueMove(),window.WS.queueScroll(),window.WS.onConnect(),window.WS.onDisconnect()
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)
All cross-thread communication goes through AppState (thread-safe with a threading.Lock) and GLib.idle_add() (posts work safely to the GTK thread).
The .deb is built inside a debian:bookworm-slim Docker container using dpkg-deb. This ensures reproducible builds regardless of the build host OS.
build-deb.sh
└── assembles the deb tree under /tmp/htpc-remote-pkg/
└── runs: docker run debian:bookworm-slim dpkg-deb --build ...
└── outputs: htpc-remote_<version>_all.deb
See Building from Source for the full build process.
Getting started
Using Glide
Reference
Help