From 54e4ffa8f05b76ae359aa6b4d910a4c219506dfb Mon Sep 17 00:00:00 2001 From: Anne Douwe Bouma Date: Tue, 13 Oct 2026 21:58:06 +0200 Subject: [PATCH 1/2] Allow remapping Home, behind a confirmation Home opens the app bar and is the usual way back into this app, so it was blocked as a mapping source. Let the user make it an exception: pressing it in the picker now asks for confirmation, and its action menu only offers pass or launch an app (no disable/replace/input/command), so it can never end up in an "acts as" loop that leaves it dead. Assigning the key happens before the confirmation, so the edit menu and hold action of an existing Home mapping keep working. Capture stays live on the warning screen so a stray Home press cannot throw the app bar over it. Cancel (or Back) restarts the picker. The guard stays in the UI, like the rest of EXCLUDED; a hand-written config can still be edited over the root shell to recover. --- README.md | 10 +++++++++- frontend/app.js | 47 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d539553..a2c5b39 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,15 @@ 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. +D-pad, OK, Back, Settings, Power, volume and channel. + +Home is the exception. It opens the app bar and is normally how you get back to +this app, so remapping it asks for confirmation first and only accepts two +actions: **launch an app** (map it to your own launcher and Home always returns +there) or **keep the normal press** (to attach a hold action). Everything else +is hidden for Home, and it can never act as another button — so it cannot end up +in a loop that leaves it doing nothing. If you do lock yourself out, a Home +mapping can be deleted by editing `config.json` over the root shell. ## Buttons that turn the TV on diff --git a/frontend/app.js b/frontend/app.js index 9ed3fc9..211084f 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -9,7 +9,9 @@ var KEYS = keys.KEYS; var NOISE = keys.NOISE_CODES; var VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : 'dev'; var COUNTDOWN_MS = 10000; +var CONFIRM_CAPTURE_MS = 60000; var BACK = 412; +var HOME = 773; // Keys the app itself needs while listening for a *target* button; they pass // through the daemon and are never taken as the result (they are in the list). var NAV_KEYS = [412, 28, 272, 103, 108, 105, 106]; @@ -28,8 +30,9 @@ var REPAIR_ROLE_COMMAND = "node -e '" + "' && ls-control scan-services"; // Buttons that can never be the source of a mapping, so the TV and this app -// always stay controllable. -var EXCLUDED = { 103: 1, 108: 1, 105: 1, 106: 1, 28: 1, 272: 1, 412: 1, 773: 1, 139: 1, 116: 1, 115: 1, 114: 1, 402: 1, 403: 1 }; +// always stay controllable. Home is the exception: it can be remapped, but only +// after the confirmation in the capture flow and only to pass or launch an app. +var EXCLUDED = { 103: 1, 108: 1, 105: 1, 106: 1, 28: 1, 272: 1, 412: 1, 139: 1, 116: 1, 115: 1, 114: 1, 402: 1, 403: 1 }; var EXCLUDED_LIST = Object.keys(EXCLUDED).map(function (k) { return parseInt(k, 10); }); var ICON = { @@ -262,16 +265,27 @@ function renderFlow() { body = renderCountdown(f); if (f.step === 'capture-target') footer = ''; break; + case 'confirm-home': + title = 'Remap Home?'; + lead = 'Home opens the app bar and is how you reach LG Input Mapper. If you remap it, you may not be able to open the app bar or this app again.'; + body = '
To keep the TV usable, Home can only launch an app or keep its normal press. Everything else stays unavailable for it.
'; + footer = '' + + ''; + break; case 'choose-action': var forHold = f.target === 'hold'; + // Home stays able to open the app bar or this app: it may only pass + // through or launch an app. No disable/replace/input/command, so it can + // never join a loop of "acts as" mappings either. + var isHome = f.key === HOME; title = forHold ? 'What should ' + keyName(f.key) + ' do when held?' : 'What should ' + keyName(f.key) + ' do?'; lead = forHold ? 'Hold for about half a second to trigger this instead of the normal tap action.' : 'Pick what happens when you press it.'; body = '
' + - item('act-disable', 'act', 'disable', ICON.block, 'Do nothing', 'Disable the button completely') + - item('act-replace', 'act', 'replace', ICON.swap, 'Act as another button', 'Play/Pause, Mute, Info, numbers, colours, or any button') + + (isHome ? '' : item('act-disable', 'act', 'disable', ICON.block, 'Do nothing', 'Disable the button completely')) + + (isHome ? '' : item('act-replace', 'act', 'replace', ICON.swap, 'Act as another button', 'Play/Pause, Mute, Info, numbers, colours, or any button')) + item('act-launch', 'act', 'launch', ICON.play, 'Launch an app', 'Open any installed app') + - item('act-input', 'act', 'input', ICON.hdmi, 'Switch to an input', 'HDMI, Live TV and other inputs on this TV') + - item('act-exec', 'act', 'exec', ICON.term, 'Run a command', 'Shell command as root, with presets') + + (isHome ? '' : item('act-input', 'act', 'input', ICON.hdmi, 'Switch to an input', 'HDMI, Live TV and other inputs on this TV')) + + (isHome ? '' : item('act-exec', 'act', 'exec', ICON.term, 'Run a command', 'Shell command as root, with presets')) + (forHold ? item('act-none', 'act', 'none', ICON.none, 'No hold action', 'Keep only the tap action') : item('act-pass', 'act', 'pass', ICON.hold, 'Keep the normal press', 'Leave the button as it is and only add a hold action')) + '
'; @@ -434,6 +448,14 @@ function handle(act, value, el) { } else editMenu(value); break; case 'pick-list': stopCountdown(); f.step = 'pick-key'; setFocus(null); render(); break; + case 'home-yes': + api.call('stopCapture', {}).then(null, function () {}); + f.step = 'choose-action'; setFocus(null); render(); + break; + case 'home-no': + api.call('stopCapture', {}).then(null, function () {}); + startFlow({ step: 'capture-source', target: 'action' }); + break; case 'act': chooseAction(value); break; case 'pick-key': applyAction({ type: 'replace', to: parseInt(value, 10) }); break; case 'pick-app': applyAction({ type: 'launch', app: value, title: appTitle(value) }); break; @@ -468,6 +490,7 @@ function goBack() { if (S.screen === 'flow') { var f = S.flow; stopCountdown(); + if (f.step === 'confirm-home') { api.call('stopCapture', {}).then(null, function () {}); return startFlow({ step: 'capture-source', target: 'action' }); } if (f.step === 'capture-source' || f.step === 'edit-menu' || f.step === 'summary' && !f.editing) return endFlow(); if (f.step === 'summary') return endFlow(); if (f.step === 'choose-action') { @@ -644,6 +667,7 @@ function startCountdown() { f.key = hit.code; var existing = S.config.mappings.filter(function (m) { return m.key === hit.code; })[0]; if (existing) { startFlow({ step: 'edit-menu', mapping: existing, key: existing.key, action: existing.action, hold: existing.hold, editing: true }); return; } + if (hit.code === HOME) { enterHomeConfirm(); return; } f.step = 'choose-action'; } else { applyAction({ type: 'replace', to: hit.code }); @@ -662,6 +686,17 @@ function stopCountdown() { api.call('stopCapture', {}).then(null, function () {}); } +// Remapping Home needs an explicit confirmation. Keep swallowing while the +// warning is up (D-pad/OK/Back stay live) so a second Home press cannot throw +// the app bar over this screen before anything has been saved. +function enterHomeConfirm() { + var f = S.flow; + f.step = 'confirm-home'; + setFocus('home-no'); + render(); + api.call('startCapture', { ms: CONFIRM_CAPTURE_MS, swallow: true, passthrough: NAV_KEYS }).then(null, function () {}); +} + // ----------------------------------------------------------------- monitor function monitorHold() { From b67f27fbd10126918c978f16ca00f39cc2013364 Mon Sep 17 00:00:00 2001 From: Anne Douwe Bouma Date: Tue, 13 Oct 2026 22:18:11 +0200 Subject: [PATCH 2/2] Home hold: open the stock launcher A Home remapped to a custom launcher loses LG's own app bar. Add an "Open the stock launcher" choice to Home's hold action menu that launches com.webos.app.home, so a long press still reaches it. It is stored as a plain launch action, so it stays within Home's pass-or-launch guard and the daemon and config validation are untouched. --- README.md | 8 +++++--- frontend/app.js | 6 ++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a2c5b39..88d0e92 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,11 @@ D-pad, OK, Back, Settings, Power, volume and channel. Home is the exception. It opens the app bar and is normally how you get back to this app, so remapping it asks for confirmation first and only accepts two actions: **launch an app** (map it to your own launcher and Home always returns -there) or **keep the normal press** (to attach a hold action). Everything else -is hidden for Home, and it can never act as another button — so it cannot end up -in a loop that leaves it doing nothing. If you do lock yourself out, a Home +there) or **keep the normal press** (to attach a hold action). The hold action +can also **open the stock launcher**, so a Home remapped to another app still +reaches the TV's own app bar on a long press. Everything else is hidden for +Home, and it can never act as another button — so it cannot end up in a loop +that leaves it doing nothing. If you do lock yourself out, a Home mapping can be deleted by editing `config.json` over the root shell. ## Buttons that turn the TV on diff --git a/frontend/app.js b/frontend/app.js index 211084f..d427c7d 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -12,6 +12,9 @@ var COUNTDOWN_MS = 10000; var CONFIRM_CAPTURE_MS = 60000; var BACK = 412; var HOME = 773; +// The TV's own launcher (the Home screen / app bar), so a remapped Home can +// still be held to get back to it. +var HOME_APP = 'com.webos.app.home'; // Keys the app itself needs while listening for a *target* button; they pass // through the daemon and are never taken as the result (they are in the list). var NAV_KEYS = [412, 28, 272, 103, 108, 105, 106]; @@ -45,6 +48,7 @@ var ICON = { term: '', none: '', hold: '', + home: '', check: '', edit: '', power: '', @@ -284,6 +288,7 @@ function renderFlow() { (isHome ? '' : item('act-disable', 'act', 'disable', ICON.block, 'Do nothing', 'Disable the button completely')) + (isHome ? '' : item('act-replace', 'act', 'replace', ICON.swap, 'Act as another button', 'Play/Pause, Mute, Info, numbers, colours, or any button')) + item('act-launch', 'act', 'launch', ICON.play, 'Launch an app', 'Open any installed app') + + (isHome && forHold ? item('act-home', 'act', 'home', ICON.home, 'Open the stock launcher', 'The TV\'s own Home screen and app bar') : '') + (isHome ? '' : item('act-input', 'act', 'input', ICON.hdmi, 'Switch to an input', 'HDMI, Live TV and other inputs on this TV')) + (isHome ? '' : item('act-exec', 'act', 'exec', ICON.term, 'Run a command', 'Shell command as root, with presets')) + (forHold ? item('act-none', 'act', 'none', ICON.none, 'No hold action', 'Keep only the tap action') @@ -560,6 +565,7 @@ function chooseAction(type) { var f = S.flow; if (type === 'none') { f.hold = null; f.step = 'summary'; setFocus('save'); return render(); } if (type === 'pass') { f.action = { type: 'pass' }; f.target = 'hold'; f.step = 'choose-action'; setFocus(null); return render(); } + if (type === 'home') return applyAction({ type: 'launch', app: HOME_APP, title: 'the stock launcher' }); if (type === 'disable') return applyAction({ type: 'disable' }); if (type === 'replace') { f.step = 'capture-target'; setFocus(null); render(); return startCountdown(); } if (type === 'input') {