claude-code-hub 1.34.0 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/keymap.js +1 -1
- package/lib/onboarding.js +59 -0
- package/lib/settings.js +8 -1
- package/lib/tray.js +37 -17
- package/package.json +5 -5
- package/packages/claude-hub-sdk/src/client.js +13 -0
- package/packages/claude-hub-sdk/src/keys.js +52 -9
- package/packages/claude-hub-sdk/src/stub.js +1 -0
- package/plugin/plugins/claude-code-hub/skills/hub-builder/references/keys.md +4 -2
- package/public/app.js +674 -21
- package/public/index.html +156 -2
- package/server.js +119 -3
package/lib/keymap.js
CHANGED
|
@@ -17,7 +17,7 @@ const PATTERN_ACTIONS = new Set(['hub.appByNumber']);
|
|
|
17
17
|
const MODS = ['ctrl', 'alt', 'shift', 'meta'];
|
|
18
18
|
|
|
19
19
|
// A combo name as the SDK's comboOf() gives it (protocol section 8), with one modifier at least.
|
|
20
|
-
const COMBO_RE = /^(?=(?:ctrl|alt|shift|meta)\+)(?:ctrl\+)?(?:alt\+)?(?:shift\+)?(?:meta\+)?(?:[a-
|
|
20
|
+
const COMBO_RE = /^(?=(?:ctrl|alt|shift|meta)\+)(?:ctrl\+)?(?:alt\+)?(?:shift\+)?(?:meta\+)?(?:[a-z0-9,.;'`/\\=\-[\]]|[A-Z][A-Za-z0-9]+)$/;
|
|
21
21
|
|
|
22
22
|
// "Ctrl+Alt+O" → "ctrl+alt+o", the form comboOf() gives. Null when the text cannot name a combo.
|
|
23
23
|
function normalizeCombo(text) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { pluginChecker } = require('./plugin-status');
|
|
6
|
+
|
|
7
|
+
const ONBOARDING = ['done', 'skipped'];
|
|
8
|
+
const SETTLED = new Set(['ok', 'disabled']);
|
|
9
|
+
|
|
10
|
+
function canonical(p) {
|
|
11
|
+
try {
|
|
12
|
+
return fs.realpathSync.native(p);
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Claude config dirs in the home folder that the hub does not list yet: a `.claude*` folder with a
|
|
19
|
+
// settings.json or a projects folder, so a cache or a backup folder is left out.
|
|
20
|
+
function findConfigDirs({ home, known, hubDir }) {
|
|
21
|
+
let entries;
|
|
22
|
+
try {
|
|
23
|
+
entries = fs.readdirSync(home, { withFileTypes: true });
|
|
24
|
+
} catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const skip = new Set([...known, hubDir].map((d) => canonical(d) ?? d));
|
|
28
|
+
return entries
|
|
29
|
+
.filter((e) => e.isDirectory() && e.name.startsWith('.claude'))
|
|
30
|
+
.map((e) => path.join(home, e.name))
|
|
31
|
+
.filter((d) => fs.existsSync(path.join(d, 'settings.json')) || fs.existsSync(path.join(d, 'projects')))
|
|
32
|
+
.map((d) => canonical(d))
|
|
33
|
+
.filter((d) => d && !skip.has(d))
|
|
34
|
+
.sort();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// `plugins` is [{id, name, bundled, install, args}]: args are the hub command-line arguments that install it.
|
|
38
|
+
function dirPlugins(configDir, plugins) {
|
|
39
|
+
const check = pluginChecker(configDir);
|
|
40
|
+
return plugins.map((p) => {
|
|
41
|
+
const { state, installed, bundled } = check(p);
|
|
42
|
+
return { id: p.id, name: p.name, state, installed, bundled };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pluginsToInstall(configDir, plugins) {
|
|
47
|
+
const check = pluginChecker(configDir);
|
|
48
|
+
return plugins.filter((p) => !SETTLED.has(check(p).state));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function setupStatus({ tray, configDirs, activeConfigDir, candidates, plugins }) {
|
|
52
|
+
const dirs = configDirs.map((d) => {
|
|
53
|
+
const list = dirPlugins(d, plugins);
|
|
54
|
+
return { path: d, active: d === activeConfigDir, plugins: list, pending: list.some((p) => !SETTLED.has(p.state)) };
|
|
55
|
+
});
|
|
56
|
+
return { tray, dirs, candidates, pluginsDone: dirs.every((d) => !d.pending) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { ONBOARDING, findConfigDirs, pluginsToInstall, setupStatus };
|
package/lib/settings.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { isObject, selectApps, validSettingValue } = require('./apps');
|
|
4
|
+
const { userKeys } = require('./keymap');
|
|
4
5
|
|
|
5
6
|
const CONFIG_DIR_SWITCH = ['palette', 'swap'];
|
|
6
7
|
const DEFAULT_POOL_SIZE = 3;
|
|
@@ -72,9 +73,15 @@ function withSettings(entry, schema, patch) {
|
|
|
72
73
|
// `saved` is config.json as on disk. Returns the new config, or {error}. Only the keys the
|
|
73
74
|
// patch names change; an app entry keeps its other fields (port, path, source). `schemas` maps an
|
|
74
75
|
// app id to the settings list of its manifest.
|
|
75
|
-
function applySettings(saved, patch, base, schemas = {}) {
|
|
76
|
+
function applySettings(saved, patch, base, schemas = {}, apps = []) {
|
|
76
77
|
if (!patch || typeof patch !== 'object') return { error: 'body must be an object' };
|
|
77
78
|
const next = { ...saved };
|
|
79
|
+
if (patch.keys !== undefined) {
|
|
80
|
+
const { keys, lines } = userKeys(patch.keys, apps);
|
|
81
|
+
if (lines.length) return { error: lines.join('; ') };
|
|
82
|
+
if (Object.keys(keys).length) next.keys = keys;
|
|
83
|
+
else delete next.keys;
|
|
84
|
+
}
|
|
78
85
|
if (patch.configDirSwitch !== undefined) {
|
|
79
86
|
if (!CONFIG_DIR_SWITCH.includes(patch.configDirSwitch))
|
|
80
87
|
return { error: `configDirSwitch must be one of ${CONFIG_DIR_SWITCH.join(', ')}` };
|
package/lib/tray.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { execFileSync, spawn } = require('node:child_process');
|
|
3
|
+
const { execFile, execFileSync, spawn } = require('node:child_process');
|
|
4
4
|
const crypto = require('node:crypto');
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const os = require('node:os');
|
|
7
7
|
const path = require('node:path');
|
|
8
|
+
const { promisify } = require('node:util');
|
|
8
9
|
const { version } = require('../package.json');
|
|
9
10
|
const { liveHub } = require('./hub-record');
|
|
10
11
|
|
|
11
12
|
const TRAY_SRC = path.join(__dirname, '..', 'tray');
|
|
12
13
|
const RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run';
|
|
13
14
|
const LAUNCH_AGENTS = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
15
|
+
// The hub page asks for the tray state, so the checks must not block the hub's event loop.
|
|
16
|
+
const run = promisify(execFile);
|
|
14
17
|
|
|
15
18
|
function dirHash(hubDir) {
|
|
16
19
|
return crypto.createHash('sha1').update(path.resolve(hubDir).toLowerCase()).digest('hex').slice(0, 8);
|
|
@@ -68,13 +71,13 @@ function windows(hubDir) {
|
|
|
68
71
|
return {
|
|
69
72
|
area: 'notification area',
|
|
70
73
|
prepare: () => ({ runValue: value }),
|
|
71
|
-
trayAlive(pid) {
|
|
72
|
-
const
|
|
73
|
-
return
|
|
74
|
+
async trayAlive(pid) {
|
|
75
|
+
const { stdout } = await run('tasklist', ['/fi', `PID eq ${pid}`, '/fo', 'csv', '/nh'], { windowsHide: true });
|
|
76
|
+
return stdout.toLowerCase().startsWith('"powershell.exe"');
|
|
74
77
|
},
|
|
75
|
-
autostartName() {
|
|
78
|
+
async autostartName() {
|
|
76
79
|
try {
|
|
77
|
-
|
|
80
|
+
await run('reg', ['query', RUN_KEY, '/v', value], { windowsHide: true });
|
|
78
81
|
return value;
|
|
79
82
|
} catch {
|
|
80
83
|
return null;
|
|
@@ -106,14 +109,14 @@ function macos(hubDir, launchAgentsDir) {
|
|
|
106
109
|
fs.writeFileSync(trayPlist, launchAgentPlist({ label, script: launcher }));
|
|
107
110
|
return { launchAgent: plist, path: process.env.PATH };
|
|
108
111
|
},
|
|
109
|
-
trayAlive(pid) {
|
|
112
|
+
async trayAlive(pid) {
|
|
110
113
|
try {
|
|
111
|
-
return
|
|
114
|
+
return (await run('ps', ['-p', String(pid), '-o', 'command='])).stdout.includes('hub-tray.js');
|
|
112
115
|
} catch {
|
|
113
116
|
return false;
|
|
114
117
|
}
|
|
115
118
|
},
|
|
116
|
-
autostartName: () => (fs.existsSync(plist) ? label : null),
|
|
119
|
+
autostartName: async () => (fs.existsSync(plist) ? label : null),
|
|
117
120
|
addAutostart() {
|
|
118
121
|
fs.mkdirSync(launchAgentsDir, { recursive: true });
|
|
119
122
|
fs.copyFileSync(trayPlist, plist);
|
|
@@ -144,12 +147,29 @@ function copyTray(plat, { hubDir, port, appId, defaultPort }) {
|
|
|
144
147
|
return dest;
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
async function
|
|
148
|
-
|
|
149
|
-
let trayPid = null;
|
|
150
|
+
async function trayPid(plat, hubDir) {
|
|
151
|
+
let pid = null;
|
|
150
152
|
try {
|
|
151
|
-
|
|
153
|
+
pid = Number.parseInt(fs.readFileSync(path.join(hubDir, 'tray.pid'), 'utf8'), 10);
|
|
152
154
|
} catch {}
|
|
155
|
+
return Number.isInteger(pid) && (await plat.trayAlive(pid)) ? pid : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function platformOf(platform, hubDir, launchAgentsDir) {
|
|
159
|
+
if (platform === 'darwin') return macos(hubDir, launchAgentsDir);
|
|
160
|
+
return platform === 'win32' ? windows(hubDir) : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// {supported, autostart, running} for the hub page's setup checklist.
|
|
164
|
+
async function trayState({ hubDir, platform = process.platform, launchAgentsDir = LAUNCH_AGENTS }) {
|
|
165
|
+
const plat = platformOf(platform, hubDir, launchAgentsDir);
|
|
166
|
+
if (!plat) return { supported: false, autostart: false, running: false };
|
|
167
|
+
const [autostart, pid] = await Promise.all([plat.autostartName(), trayPid(plat, hubDir)]);
|
|
168
|
+
return { supported: true, autostart: !!autostart, running: pid !== null };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function printStatus(plat, { hubDir, configDir }) {
|
|
172
|
+
const [autostart, pid] = await Promise.all([plat.autostartName(), trayPid(plat, hubDir)]);
|
|
153
173
|
let hub = null;
|
|
154
174
|
try {
|
|
155
175
|
hub = await liveHub(hubDir, fs.readFileSync(path.join(hubDir, 'token'), 'utf8').trim());
|
|
@@ -157,16 +177,16 @@ async function printStatus(plat, { hubDir, configDir }) {
|
|
|
157
177
|
console.log(` Hub dir: ${hubDir}`);
|
|
158
178
|
console.log(` Config dir: ${configDir}`);
|
|
159
179
|
console.log(` Autostart: ${autostart ? `on (${autostart})` : 'off'}`);
|
|
160
|
-
console.log(` Tray: ${
|
|
180
|
+
console.log(` Tray: ${pid !== null ? `running (pid ${pid})` : 'not running'}`);
|
|
161
181
|
console.log(` Hub: ${hub ? `running on port ${hub.port} (pid ${hub.pid})` : 'stopped'}`);
|
|
162
182
|
}
|
|
163
183
|
|
|
164
184
|
async function runTrayCommand({ flag, platform = process.platform, launchAgentsDir = LAUNCH_AGENTS, ...opts }) {
|
|
165
|
-
|
|
185
|
+
const plat = platformOf(platform, opts.hubDir, launchAgentsDir);
|
|
186
|
+
if (!plat) {
|
|
166
187
|
console.error(` ${flag} works only on Windows and macOS.`);
|
|
167
188
|
return false;
|
|
168
189
|
}
|
|
169
|
-
const plat = platform === 'darwin' ? macos(opts.hubDir, launchAgentsDir) : windows(opts.hubDir);
|
|
170
190
|
if (flag === '--tray-status') {
|
|
171
191
|
await printStatus(plat, opts);
|
|
172
192
|
return true;
|
|
@@ -185,4 +205,4 @@ async function runTrayCommand({ flag, platform = process.platform, launchAgentsD
|
|
|
185
205
|
return true;
|
|
186
206
|
}
|
|
187
207
|
|
|
188
|
-
module.exports = { runTrayCommand, runValue, launchAgentLabel, launchAgentPlist, appConfig };
|
|
208
|
+
module.exports = { runTrayCommand, trayState, runValue, launchAgentLabel, launchAgentPlist, appConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-hub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.0",
|
|
4
4
|
"description": "Unified hub for Claude Code tools — Marketplace + Kanban in one PWA",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://github.com/NikiforovAll/claude-code-hub#readme",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"claude-code-cost": "1.6.
|
|
50
|
-
"claude-code-kanban": "7.
|
|
51
|
-
"claude-code-marketplace": "1.7.
|
|
52
|
-
"claude-code-memory-explorer": "1.3.
|
|
49
|
+
"claude-code-cost": "1.6.2",
|
|
50
|
+
"claude-code-kanban": "7.32.0",
|
|
51
|
+
"claude-code-marketplace": "1.7.3",
|
|
52
|
+
"claude-code-memory-explorer": "1.3.6",
|
|
53
53
|
"express": "^4.21.0",
|
|
54
54
|
"open": "^10.1.0"
|
|
55
55
|
},
|
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
const tokenWaiters = new Set();
|
|
112
112
|
const themesFns = new Set();
|
|
113
113
|
const settingsFns = new Set();
|
|
114
|
+
const keysFns = new Set();
|
|
114
115
|
const queued = [];
|
|
115
116
|
// Topic → payload: the latest publish before welcome.
|
|
116
117
|
const outbox = new Map();
|
|
@@ -253,6 +254,12 @@
|
|
|
253
254
|
case 'hub:settings':
|
|
254
255
|
if (welcome) setSettings(m.settings);
|
|
255
256
|
return;
|
|
257
|
+
case 'hub:keys':
|
|
258
|
+
if (!welcome) return;
|
|
259
|
+
welcome.keys = comboMap(m.keys);
|
|
260
|
+
forward = strings(m.forward);
|
|
261
|
+
for (const fn of keysFns) fn();
|
|
262
|
+
return;
|
|
256
263
|
}
|
|
257
264
|
}
|
|
258
265
|
|
|
@@ -472,6 +479,12 @@
|
|
|
472
479
|
forwards,
|
|
473
480
|
// The combos forwards() matches, for a frame that tests keys with ClaudeHub.comboOf on its own.
|
|
474
481
|
forwardCombos: () => (forward ? [...forward] : []),
|
|
482
|
+
// Calls fn after the user changes the hub's keys, so a page that copied forwardCombos() or
|
|
483
|
+
// shows keyLabel() can read them again.
|
|
484
|
+
onKeys(fn) {
|
|
485
|
+
keysFns.add(fn);
|
|
486
|
+
return () => keysFns.delete(fn);
|
|
487
|
+
},
|
|
475
488
|
// The keys of a hub action for a help row, in this system's names. A list of actions shares one
|
|
476
489
|
// row: ['Ctrl', 'Alt', '←/→']. [] when no key runs them; null before welcome, standalone, or
|
|
477
490
|
// for an action the hub did not list.
|
|
@@ -9,19 +9,62 @@ var ClaudeHubKeys = (() => {
|
|
|
9
9
|
// Modifiers in ctrl, alt, shift, meta order, joined by '+' to the key. macOS composes
|
|
10
10
|
// Option+<key> into a character (Option+1 is '¡', Option+P is 'π') and holding Control does not
|
|
11
11
|
// undo it, so e.key alone cannot name these presses there. e.code is the physical key, which is
|
|
12
|
-
// wrong for non-US layouts, hence only as a fallback.
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
12
|
+
// wrong for non-US layouts, hence only as a fallback. A letter or punctuation key falls back to its
|
|
13
|
+
// US character only when e.key is not ASCII (a non-Latin layout, or Option on macOS), so AZERTY's
|
|
14
|
+
// ',' on the M key stays ','. The digit row always falls back, so AZERTY's '&' key is 1, as Windows
|
|
15
|
+
// and VS Code treat it. No fallback under AltGraph: Chromium sets it when AltGr types a character, and that
|
|
16
|
+
// press is text. Takes a real KeyboardEvent or a forwarded {key, code} payload; a payload without
|
|
17
|
+
// code degrades to key. A payload needs no AltGraph: the SDK forwards only a press whose key
|
|
18
|
+
// already names a bound combo.
|
|
19
|
+
// Self-contained on purpose, the table too: cck puts its source text into a sandboxed frame.
|
|
20
|
+
// codeOf() lists the same keys the other way round.
|
|
17
21
|
function comboOf(e) {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
22
|
+
const raw = typeof e.key === 'string' ? e.key : '';
|
|
23
|
+
const lower = raw.toLowerCase();
|
|
24
|
+
const code = e.getModifierState?.('AltGraph') ? '' : e.code || '';
|
|
25
|
+
const latin = !/[^\x20-\x7e]/.test(raw);
|
|
26
|
+
const digit = /^Digit([0-9])$/.exec(code)?.[1];
|
|
27
|
+
const us =
|
|
28
|
+
!latin &&
|
|
29
|
+
(/^Key([A-Z])$/.exec(code)?.[1].toLowerCase() ||
|
|
30
|
+
{
|
|
31
|
+
Minus: '-',
|
|
32
|
+
Equal: '=',
|
|
33
|
+
BracketLeft: '[',
|
|
34
|
+
BracketRight: ']',
|
|
35
|
+
Backslash: '\\',
|
|
36
|
+
Semicolon: ';',
|
|
37
|
+
Quote: "'",
|
|
38
|
+
Backquote: '`',
|
|
39
|
+
Comma: ',',
|
|
40
|
+
Period: '.',
|
|
41
|
+
Slash: '/',
|
|
42
|
+
}[code]);
|
|
43
|
+
const key = /^[a-z0-9]$/.test(lower) ? lower : digit || us || (e.key === ' ' ? 'Space' : e.key);
|
|
21
44
|
const mods = [e.ctrlKey && 'ctrl', e.altKey && 'alt', e.shiftKey && 'shift', e.metaKey && 'meta'];
|
|
22
45
|
return [...mods, key].filter(Boolean).join('+');
|
|
23
46
|
}
|
|
24
47
|
|
|
48
|
+
// The physical key (e.code) that types a combo's key on a US layout, or null for a named key.
|
|
49
|
+
function codeOf(key) {
|
|
50
|
+
if (/^[a-z]$/.test(key)) return `Key${key.toUpperCase()}`;
|
|
51
|
+
if (/^[0-9]$/.test(key)) return `Digit${key}`;
|
|
52
|
+
const codes = {
|
|
53
|
+
'-': 'Minus',
|
|
54
|
+
'=': 'Equal',
|
|
55
|
+
'[': 'BracketLeft',
|
|
56
|
+
']': 'BracketRight',
|
|
57
|
+
'\\': 'Backslash',
|
|
58
|
+
';': 'Semicolon',
|
|
59
|
+
"'": 'Quote',
|
|
60
|
+
'`': 'Backquote',
|
|
61
|
+
',': 'Comma',
|
|
62
|
+
'.': 'Period',
|
|
63
|
+
'/': 'Slash',
|
|
64
|
+
};
|
|
65
|
+
return codes[key] ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
25
68
|
// The keys of a combo as a help row shows them: ['Ctrl', 'Alt', 'P'], or ['⌃', '⌥', 'P'] on macOS.
|
|
26
69
|
// The {n} of a numbered combo reads 1…9.
|
|
27
70
|
function keyParts(combo, mac) {
|
|
@@ -46,7 +89,7 @@ var ClaudeHubKeys = (() => {
|
|
|
46
89
|
].filter(Boolean);
|
|
47
90
|
}
|
|
48
91
|
|
|
49
|
-
return { comboOf, keyParts, keySigs };
|
|
92
|
+
return { comboOf, codeOf, keyParts, keySigs };
|
|
50
93
|
})();
|
|
51
94
|
|
|
52
95
|
if (typeof module === 'object' && module.exports) module.exports = ClaudeHubKeys;
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
The hub's own keys (switch tools, open the palettes) live in the `keys` block of `<hub-dir>/config.json` (`--hub-dir`, else `CLAUDE_HUB_DIR`, else `~/.claude-hub`). Each tool's own keys are outside it. Page: `reference/shortcuts/#change-the-keys`.
|
|
4
4
|
|
|
5
|
+
The user can change them without you: hub settings (`Ctrl+,`), **Keyboard** page, **Customize…**. The window applies a change at once, with no restart. Point the user there first. Edit the file only when the user asks you to.
|
|
6
|
+
|
|
5
7
|
## Steps
|
|
6
8
|
|
|
7
9
|
1. Map each key the user asks for to an action in [Actions](#actions), and check its combo against [Rules](#rules). When a combo breaks a rule, tell the user why and offer one that passes.
|
|
8
10
|
2. Read `config.json` and merge the entries into its `keys` block. Every other field stays byte for byte: the hub owns `configDirs`, `activeConfigDir` and `apps`.
|
|
9
|
-
3. Restart the hub: it reads `keys` once, at startup. When the tray runs it, the user picks Quit in the tray menu and starts the hub again.
|
|
11
|
+
3. Restart the hub: it reads the file's `keys` once, at startup. When the tray runs it, the user picks Quit in the tray menu and starts the hub again.
|
|
10
12
|
4. Read the startup output, or `<hub-dir>/hub.log` when the tray started the hub. The hub logs each entry it drops as `<config.json path>: keys "<action>": … It keeps its default`. The reason is one of: not a combo, not a combo that ends in `+{n}`, `<combo> is kept by <app>`, `<combo> is already set for "<action>"`. An id that is not an action logs `keys "<id>" is not a hub action, ignored`.
|
|
11
13
|
5. Read `GET http://localhost:<port>/api/config?token=<token>` (the token is in `<hub-dir>/token`).
|
|
12
14
|
|
|
@@ -42,7 +44,7 @@ Ctrl+Alt+O opens the project palette, Ctrl+Alt+H and Ctrl+Alt+L move between too
|
|
|
42
44
|
|
|
43
45
|
## Rules
|
|
44
46
|
|
|
45
|
-
- **Form.** One or more of `ctrl`, `alt`, `shift`, `meta`, then one key, joined by `+`. The key is a letter, a digit
|
|
47
|
+
- **Form.** One or more of `ctrl`, `alt`, `shift`, `meta`, then one key, joined by `+`. The key is a letter, a digit, one of `` , . ; ' ` / \ = - [ ] ``, or a `KeyboardEvent.key` name (`ArrowUp`, `F2`, `PageDown`). On macOS `alt` is Option and `meta` is Command. Modifiers and letters take any case and order (`"Alt+Ctrl+O"` reads as `ctrl+alt+o`); a key name keeps its case (`PageDown`).
|
|
46
48
|
- **`{n}`.** `hub.appByNumber` ends in `+{n}`, and the hub binds it for 1 to 9. It is the only action with `{n}`.
|
|
47
49
|
- **`null`** leaves the action with no key.
|
|
48
50
|
- **Taking a default.** A combo that is another action's default moves to the new action, and the other action has no key until it gets one: `"hub.nextApp": "ctrl+alt+a"` leaves the app launcher with no key. Say so to the user.
|