Skip to content

About

Remap the buttons on your LG Magic Remote, on the TV itself (rooted webOS)

Resources

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

LG Input Mapper

Remap, disable or repurpose the buttons on your LG Magic Remote, on a rooted webOS TV, using nothing but the remote:

  • Remap a button → press the button → choose what it should do:
    • do nothing, act as another button (Play/Pause, Mute, Info, numbers, colours, …), launch an app, switch to an input, or run a command (as root; with presets)
    • optionally a different action when held (long press), or a hold action only, leaving the normal press alone
  • Buttons that turn the TV on (Netflix, Prime Video, LG Channels, …) follow your mappings when pressed in standby too.
  • Key monitor to see the code of any button (buttons are held back while it is open).
  • Starts at boot via Homebrew Channel; changes apply instantly.

It is a from-scratch successor to Simon34545/lginputhook (LG Input Hook), built because that approach stopped working on webOS 24/25. Tested on a 2025 C5 (webOS 10.3) with an MR25GA remote; it should work on any rooted webOS 5+ TV on 32-bit or 64-bit ARM.

Requirements

Homebrew Channel with root (root status must show OK in its settings). Launch LG Input Mapper once after installing: it elevates its service, starts the remapper and enables the boot script.

Some buttons are deliberately not remappable so the TV stays usable: the D-pad, OK, Back, Home, Settings, Power, volume and channel.

Buttons that turn the TV on

Streaming buttons such as Netflix, Prime Video, Disney+ or LG Channels also turn the TV on from standby. Your mapping decides what happens then:

Mapped to Pressed while the TV is in standby
Launch an app, switch to an input The TV turns on straight into that app or input
Do nothing Nothing: the TV stays off
Act as another button, run a command The TV turns on like the power button, and the button or command follows once it is on (a couple of seconds)

These mappings also have an Allow from standby option, on by default. Turn it off and the button no longer turns the TV on, while it keeps doing its mapping when the TV is on.

Needs webOS 6 (2021) or newer; older TVs keep LG's behaviour for these buttons. Other buttons (Alexa, the colour buttons, …) cannot turn the TV on, and neither can streaming buttons LG does not use in your country: the app marks their mappings From standby: not supported.

Nothing is left behind: removing a mapping or stopping the remapper gives these buttons back their original behaviour, and uninstalling the app does that and also deletes everything it stored, your mappings included. If a firmware update removes root, LG's own servers restore the buttons the first time the TV starts up online.

Loops

Mappings that point at each other through Act as another button (Netflix acts as LG Channels, LG Channels acts as Netflix) form a loop. They stay in the list, shown in red, and those buttons do nothing, in standby too, until you delete or change one of them.

Install

Homebrew Channel

Homebrew Channel > Settings > Add repository, and enter:

https://raw.githubusercontent.com/fivefold3/webos-homebrew-repo/main/repo.json

Manual install

Grab the .ipk from the releases page and install it with webOS Dev Manager or ares-install. If you used LG Input Hook before, your mappings are imported on first launch.

How it works

The original LG Input Hook injected a PHP/frida hook into lginput2, which breaks whenever LG changes glibc. LG Input Mapper does not touch any LG process:

  1. lginputmapperd, a ~130 KB static C daemon, opens the remote's virtual input device (LGE M-RCU - Builtin [0], created by lginput2 via uinput) and takes exclusive control of it (EVIOCGRAB), so the TV no longer sees the raw events.
  2. Every event is looked up in your mappings and the result is written into a sibling device the compositor already reads (LGE M-RCU - Builtin [2]). surface-manager does not pick up input devices created after it starts, so a fresh uinput clone would go unread; injecting into an existing sibling is the trick magic_mapper found. A clone is still used as a fallback. Devices whose key set the output device cannot deliver (on the C5 that is the IR receiver LGE RCU) are left untouched rather than half-working.
  3. Long presses are detected in the daemon (the remote sends no repeat events), exec/launch actions are spawned detached, and the config file is watched with inotify so changes apply instantly.
  4. A button pressed in standby never reaches the input device: the TV's standby controller wakes it with a reason named after the button (netflix, lgchannels, …) and the boot manager (bootd) opens the app listed for that reason in the TV setting other.mapping_info, which it reads from /var/luna/preferences/other on every wake. The daemon rewrites only the entries of remapped buttons, through the settings service: launch mappings get their app, anything else isActive: false, which wakes the TV like the power button. For "do nothing" (and Allow from standby off) the button is also locked in the standby controller, the way LG blocks buttons it does not use in a country (micomservice/setCPHotKeyListLock; that call unlocks every button left out of its list, so it always gets LG's full list with only our locks changed). LG's own entries and locks are kept in standby.json and written back as soon as a mapping no longer needs the change. LG's server resets the table and lifts the locks after every reboot; the daemon notices (inotify) and applies its entries and locks again. For "act as" and command mappings the daemon follows tvpower's power state (a luna-send -i subscription) and, when the TV goes from standby to active, asks tvpower for the wake reason. (The clocks cannot tell: the C5 does suspend, but its CLOCK_BOOTTIME leaves the time asleep out.) For a while after being turned off the TV can stay in active standby (screen dark, system up); remote buttons then reach the daemon instead of the standby controller, so for a remapped button the daemon turns the TV on itself (tvpower/power/powerOn with the button's reason) and the wake goes on as above, or drops the press if the button does nothing in standby. If the app is uninstalled, the daemon keeps running from the deleted binary long enough to restore LG's entries and locks, remove its boot script, its config directory and its state directory, and exit; should it not get the chance, the boot script sends the prepared restore requests (standby-restore.json, standby-restore-micom.json) at the next boot, then removes the config directory and itself.

Layout:

  • daemon/ – lginputmapperd.c (+ vendored cJSON). Built with zig cc as static musl binaries for arm (soft-float, as used by LG's 32-bit userland) and aarch64.
  • service/ – Node service (com.lginputmapper.app.service): keeps the daemon running, installs the boot script, reads/writes the config, serves status and the live key log over Luna.
  • frontend/ – the TV app: vanilla JS with D-pad spatial navigation.
  • shared/keys.js – known button codes (verified on an MR25GA).

Runtime files: config /home/root/.config/lginputmapper/config.json (plus standby.json and the standby-restore*.json requests next to it while the TV's hotkey table or key locks are changed), state /tmp/lginputmapperd/{status.json,daemon.log,service.log}, boot script /var/lib/webosbrew/init.d/lginputmapper. Button presses are only written down (events.jsonl) while the key picker or the key monitor is open, and the file is deleted when it closes.

Security: the service runs as root and stores exec commands the daemon runs as root, so it only answers calls from the LG Input Mapper app itself. Homebrew Channel's elevation opens the service's Luna role to every caller; the service locks it back down to the app after each launch (and restores it by itself if that ever keeps the app out). The installed files are made writable by their owner only, and the config and state directories are root-only.

Config format

{
  "version": 2,
  "holdMs": 500,
  "devices": { "include": ["LGE *"], "exclude": [] },
  "output": { "mode": "auto", "device": "LGE M-RCU - Builtin [2]" },
  "mappings": [
    { "key": 1037, "label": "Netflix", "action": { "type": "launch", "app": "org.jellyfin.webos" } },
    { "key": 1086, "action": { "type": "disable" } },
    { "key": 1042, "action": { "type": "replace", "to": 164 },
      "hold": { "type": "exec", "command": "luna-send -n 1 luna://com.webos.service.tvpower/power/turnOffScreen '{\"standbyMode\":\"active\"}'" } }
  ]
}

Action types: pass, disable, replace (to), launch (app, optional params), exec (command, optional onRelease). Commands get LGINPUTMAPPER_KEY and LGINPUTMAPPER_VALUE in their environment, and LGINPUTMAPPER_WAKE=1 when they run because the button woke the TV. "standby": false on a mapping is Allow from standby turned off.

Building

Needs Node ≥ 18 and zig (for cross-compiling the daemon).

npm install
npm run build      # daemon (both arches) + webpack bundles into dist/
npm run package    # dist/com.lginputmapper.app_<version>_all.ipk

scripts/deploy.sh <ssh-host> copies the ipk over SSH, installs it through the TV's install service and launches the app. Useful on the TV: lginputmapperd --list prints all input devices and their key bitmaps; lginputmapperd --dry-run logs events without grabbing anything.

Credits

This project stands on other people's work:

  • Simon34545 — lginputhook (LG Input Hook), the original remote remapper for rooted webOS TVs and the project this one succeeds. AkshayRao27 documented the button IDs of several LG remotes in that repository, which seeded the key table in shared/keys.js.
  • Andrew Fraley — magic_mapper showed that grabbing the Magic Remote's input device and re-injecting events into a sibling device works; that is the mechanism lginputmapperd uses. Its button table (C5 additions by DanielPower) contributed codes as well.
  • Prior art on the hook-based approach LG Input Hook used, none of whose code is used here: Informatic's inputhook gist, smx-smx's ezinject and sundermann's inputhookpp.
  • webOS Homebrew Channel (webosbrew) provides the root access, service elevation and boot hook this app relies on.

Third-party code shipped in the package:

  • cJSON — Dave Gamble and contributors, MIT (daemon/vendor/cJSON.LICENSE).
  • webOSTV.js — LG Electronics, Apache-2.0, bundled from the webostvjs npm package.
  • core-js — Denis Pushkarev, MIT (polyfills in the app bundle).

License

MIT. See LICENSE.

About

Remap the buttons on your LG Magic Remote, on the TV itself (rooted webOS)

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages