6.1 KiB
title, created, updated, type, namespace, tags, sources, confidence, related
| title | created | updated | type | namespace | tags | sources | confidence | related | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| HTPC — magic4pc (LG Magic Remote) | 2026-05-28 | 2026-05-29 | tech | family |
|
|
medium |
|
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/*/cmdlineforkwin_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.
- Find active window: (a) under cursor via
getmouselocation, or (b) largest bounding-box window (non-popup, highest ID) - Compute 16:9 letterbox viewport from window width:
lb_h = win_w * 9/16 - Compute offset:
off_y = -(lb_h - win_h) / 2 - 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
# 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
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
/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)
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:
/tmpis cleared → norun-state→freshStart=true→ launches configured app - Manual launch:
run-state=runningalready 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):
// 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