diff --git a/README.md b/README.md
index d539553..88d0e92 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,17 @@ 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). 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 9ed3fc9..d427c7d 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -9,7 +9,12 @@ 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;
+// 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];
@@ -28,8 +33,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 = {
@@ -42,6 +48,7 @@ var ICON = {
term: '',
none: '',
hold: '',
+ home: '',
check: '',
edit: '',
power: '',
@@ -262,16 +269,28 @@ 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 && 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')
: 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 +453,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 +495,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') {
@@ -537,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') {
@@ -644,6 +673,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 +692,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() {