Files
obsidian-vault/wiki/tech/htpc-magic4pc.md
T

6.1 KiB
Raw Blame History

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
htpc
how-to
pitfalls
family/how-to/htpc-magic4pc.md
family/how-to/magic4pc-webos.md
medium
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 01920 × 01080 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

# 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: /tmp is cleared → no run-statefreshStart=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):

// 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