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.
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.
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.
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.
Homebrew Channel > Settings > Add repository, and enter:
https://raw.githubusercontent.com/fivefold3/webos-homebrew-repo/main/repo.json
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.
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:
lginputmapperd, a ~130 KB static C daemon, opens the remote's virtual input device (LGE M-RCU - Builtin [0], created bylginput2via uinput) and takes exclusive control of it (EVIOCGRAB), so the TV no longer sees the raw events.- 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 receiverLGE RCU) are left untouched rather than half-working. - 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.
- 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 settingother.mapping_info, which it reads from/var/luna/preferences/otheron every wake. The daemon rewrites only the entries of remapped buttons, through the settings service: launch mappings get their app, anything elseisActive: 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 instandby.jsonand 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 (aluna-send -isubscription) and, when the TV goes from standby to active, asks tvpower for the wake reason. (The clocks cannot tell: the C5 does suspend, but itsCLOCK_BOOTTIMEleaves 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/powerOnwith 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 withzig ccas static musl binaries forarm(soft-float, as used by LG's 32-bit userland) andaarch64.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.
{
"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.
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.ipkscripts/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.
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
lginputmapperduses. 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
webostvjsnpm package. - core-js — Denis Pushkarev, MIT (polyfills in the app bundle).
MIT. See LICENSE.