--- title: HTPC — magic4pc (LG Magic Remote) created: '2026-05-28' updated: '2026-05-29' type: tech namespace: family tags: [htpc, how-to, pitfalls] sources: - family/how-to/htpc-magic4pc.md - family/how-to/magic4pc-webos.md confidence: medium related: - "[[htpc-bazzite-proton]]" - "[[tech/htpc-steam-emulators]]" - "[[tech/tv-luna-send]]" --- # HTPC — magic4pc (LG Magic Remote) Go-client for using the LG Magic Remote (webOS TV app) as HTPC mouse/keyboard input. Fork: `mallexxx/magic4pc_altclient` branch `bazzite-linux`. LG TV: `192.168.1.75`, HTPC: `192.168.1.86`. ## Architecture - **TV server**: webOS app on LG TV → sends UDP to fixed source port **9106** - **HTPC client**: `/usr/local/bin/magic4pc`, UDP listener - **xdotool**: single persistent process via stdin pipe (mouse + keyboard) - **ydotool**: gamescope-specific keys (Ctrl+1, Ctrl+2) via `/run/user/1000/.ydotool_socket` Session detection: - `isGamescopeSession()` — scans `/proc/*/cmdline` for `kwin_wayland`; if absent → gamescope (Steam Big Picture mode) - `activeDisplay()` — KDE uses Xwayland display from kwin args; gamescope uses `:1` (game) if visible windows present, else `:0` (Steam BP) ## Button Mapping | Button | Keycode | Gamescope | KDE Desktop | |--------|---------|-----------|-------------| | Red | 403 | Ctrl+1 (Steam menu) | Super | | Green | 404 | Escape | Escape | | Yellow | 405 | Ctrl+2 (Steam QAM) | Middle click | | Blue | 406 | Right click | Right click | | Back | 461 | Mouse X1 | Mouse X1 | | D-pad | 37-40 | Arrow keys | Arrow keys | | Ch+/Ch- | 33/34 | PageUp/Down | PageUp/Down | | Play/Stop/Pause | 415/413/19 | XF86Audio* | XF86Audio* | | Touch (swipe) | — | mousemove (scaled) | mousemove | | Tap | — | Left click | Left click | ## Mouse Coordinate Mapping TV sends coordinates in 0–1920 × 0–1080 space. **In gamescope:** uses letterbox algorithm. 1. Find active window: (a) under cursor via `getmouselocation`, or (b) largest bounding-box window (non-popup, highest ID) 2. Compute 16:9 letterbox viewport from window width: `lb_h = win_w * 9/16` 3. Compute offset: `off_y = -(lb_h - win_h) / 2` 4. Map: `x11_x = tv_x * scale + off_x`; clip negatives to 0 **In KDE:** `getdisplaygeometry` → scale = display / 1920×1080. ## Key Pitfalls | Problem | Fix | |---------|-----| | TV "service error" / won't reconnect | Fixed source UDP port 9106 | | TV stuck "waiting for client" | Server keepalive timeout 10s | | KDE XTest prompt on every click | Single persistent xdotool process | | KDE XTest prompt after xdotool restart | `XwaylandEisNoPrompt=true` in kwinrc | | Wrong mouse coordinates | Dynamic scale via `xdotool getdisplaygeometry` | | Binary lost after `rpm-ostree upgrade` | Rebuild and redeploy | ## Deploy ```bash # Build on Mac cd ~/Developer/magic4pc_altclient GOOS=linux GOARCH=amd64 go build -o magic4pc . # Copy + restart on HTPC scp magic4pc htpc:/home/bazzite/magic4pc/magic4pc echo bazzite | sudo -S systemctl stop magic4pc.service echo bazzite | sudo -S cp /home/bazzite/magic4pc/magic4pc \ /usr/local/bin/magic4pc echo bazzite | sudo -S systemctl start magic4pc.service ``` Sudoers rule (no-password systemctl): ``` bazzite ALL=(ALL) NOPASSWD: /usr/bin/systemctl ``` ## WebOS TV App — Build & Deploy The TV-side is a React/Enact WebOS app + Node.js service bundled into an IPK. Sources: `~/Developer/magic4pc/webos/`. ### Build ```bash cd ~/Developer/magic4pc/webos NODE_OPTIONS=--openssl-legacy-provider npm run build ``` **Pitfall:** `--openssl-legacy-provider` is required — the old webpack is incompatible with Node.js 25+. ### Package IPK ```bash /Users/admin/webOS_TV_SDK/CLI/bin/ares-package dist/ service/ --outdir . ``` **Pitfall:** both `dist/` and `service/` arguments are required — omitting `service/` excludes the Node.js service from the IPK. **Pitfall:** `npm run package` = `ares-package -n` (unsigned) — TV rejects unsigned packages. Use the SDK command directly. ### Deploy (deploy.sh) ```bash cd ~/Developer/magic4pc/webos && ./deploy.sh ``` Script flow: build → package → scp → close → remove → install → launch. Uses [[tech/tv-luna-send]] wrapper for close/remove/launch. For install (subscribe mode) — direct `ssh+script` with polling on `"state":"installed"`. **Pitfall:** `ares-install` / `ares-launch` are unreliable — they don't wait for completion. Use `luna-send dev/install` via `script` wrapper. ### Version in UI Displayed as `1.1.0 (YYYY-MM-DD HH:MM)` — injected by webpack at build time via `process.env.BUILD_DATE`. ## Auto-launch on Boot/Wake Magic4pc launches a configured app when the TV boots or wakes. | File | Location | Purpose | |------|----------|---------| | `magic4pc-settings` | PERSISTENT_DIR | Selected app ID | | `magic4pc-last-app` | PERSISTENT_DIR | Last foreground app | | `magic4pc-run-state` | `/tmp/` | `running` after first launch | `PERSISTENT_DIR = /media/developer/apps/usr/palm/services/me.wouterdek.magic4pc.service` Logic: - **Boot/wake:** `/tmp` is cleared → no `run-state` → `freshStart=true` → launches configured app - **Manual launch:** `run-state=running` already present → skips auto-launch The `init.d` script on TV (`/var/lib/webosbrew/init.d/magic4pc`) removes `run-state` on suspend so wake triggers fresh launch. Always edit in repo (`tv-scripts/init.d-magic4pc.sh`), never directly on TV. ## WebOS Back Key + System Keyboard When the system keyboard is open, **Back #1** is consumed by the OS (closes keyboard) — the `keydown` DOM event and `onButtonDown` are **not** fired. **Back #2** arrives normally. Fix used in `MainPanel.js` (`_kbWasOpen` flag): ```js // Input.onActivate: this._kbWasOpen = true; // onButtonDown on Back: if (this.state.wolMacActive || this._kbWasOpen) { this._kbWasOpen = false; return; // suppress panel close } ``` Approaches that don't work: `Popup.noAutoDismiss`, timeout heuristic, polling `document.activeElement` (keyboard is a system overlay, focus never moves to INPUT). ## See Also - [[htpc-bazzite-proton]] — Proton game compatibility on this HTPC - [[tech/htpc-steam-emulators]] — Steam, emulators, gamepad config - [[tech/tv-luna-send]] — luna-send SSH wrapper for WebOS TV commands