@crouton-kit/crouter 0.3.29 → 0.3.30
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/dist/build-root.d.ts +15 -7
- package/dist/build-root.js +48 -36
- package/dist/cli.js +20 -15
- package/dist/commands/chord.js +3 -24
- package/dist/core/__tests__/listing-completeness.test.js +2 -2
- package/dist/core/runtime/placement.d.ts +5 -5
- package/dist/core/runtime/placement.js +35 -24
- package/dist/core/runtime/tmux.d.ts +2 -2
- package/dist/core/runtime/tmux.js +13 -23
- package/package.json +1 -1
package/dist/build-root.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { RootDef } from './core/command.js';
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
2
|
+
/** Every shipped subtree name. Cheap (no module loading) — the front-door
|
|
3
|
+
* recursion guard and the dispatcher's first-token routing need only names. */
|
|
4
|
+
export declare const SUBTREE_NAMES: string[];
|
|
5
|
+
/** Build a root that contains only the subtree `first` dispatches into.
|
|
6
|
+
* Returns the FULL root when `first` is not a recognized subtree — bare `crtr`,
|
|
7
|
+
* `-h`/`--help`, `--version`, and any unknown leading token all need the
|
|
8
|
+
* complete tree (root -h lists every subtree; the unknown-path error names
|
|
9
|
+
* every valid child). This keeps every help/error surface byte-identical while
|
|
10
|
+
* the common leaf-dispatch path loads exactly one subtree. */
|
|
11
|
+
export declare function resolveRoot(first: string | undefined): Promise<RootDef>;
|
|
12
|
+
/** Assemble the full crtr command tree (all subtrees). Used for root -h, the
|
|
13
|
+
* unknown-path error, bare-root boot, and the listing-completeness test. Root
|
|
14
|
+
* owns only the tagline; every subtree declares its own root representation via
|
|
15
|
+
* its rootEntry. */
|
|
16
|
+
export declare function buildRoot(): Promise<RootDef>;
|
package/dist/build-root.js
CHANGED
|
@@ -1,38 +1,50 @@
|
|
|
1
1
|
import { defineRoot } from './core/command.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
2
|
+
const TAGLINE = 'crtr: agentic planning runtime.';
|
|
3
|
+
/** Lazy registry: subtree name → dynamic importer that builds its BranchDef.
|
|
4
|
+
* The whole point is that each `import()` only compiles that one subtree's
|
|
5
|
+
* module graph. The hot path (`crtr node focus …`, `crtr feed`, …) dispatches
|
|
6
|
+
* into a single leaf, so it must load ONE subtree — not the attach-TUI
|
|
7
|
+
* (babel/highlight.js), the web/vite command, and every other subtree it never
|
|
8
|
+
* touches. `crtr` cold-start is dominated by Node module loading; keeping the
|
|
9
|
+
* other 11 subtrees off the path is the biggest, lowest-risk win.
|
|
10
|
+
*
|
|
11
|
+
* Naming note: `push` and `feed` are two subtrees from one module — importing
|
|
12
|
+
* it once yields both registers. */
|
|
13
|
+
const SUBTREE_LOADERS = {
|
|
14
|
+
memory: async () => (await import('./commands/memory.js')).registerMemory(),
|
|
15
|
+
pkg: async () => (await import('./commands/pkg.js')).registerPkg(),
|
|
16
|
+
human: async () => (await import('./commands/human.js')).registerHuman(),
|
|
17
|
+
sys: async () => (await import('./commands/sys.js')).registerSys(),
|
|
18
|
+
node: async () => (await import('./commands/node.js')).registerNode(),
|
|
19
|
+
push: async () => (await import('./commands/push.js')).registerPush(),
|
|
20
|
+
feed: async () => (await import('./commands/push.js')).registerFeed(),
|
|
21
|
+
canvas: async () => (await import('./commands/canvas.js')).registerCanvas(),
|
|
22
|
+
view: async () => (await import('./commands/view.js')).registerView(),
|
|
23
|
+
attach: async () => (await import('./clients/attach/attach-cmd.js')).registerAttach(),
|
|
24
|
+
workspace: async () => (await import('./commands/workspace.js')).registerWorkspace(),
|
|
25
|
+
web: async () => (await import('./clients/web/web-cmd.js')).registerWeb(),
|
|
26
|
+
};
|
|
27
|
+
/** Every shipped subtree name. Cheap (no module loading) — the front-door
|
|
28
|
+
* recursion guard and the dispatcher's first-token routing need only names. */
|
|
29
|
+
export const SUBTREE_NAMES = Object.keys(SUBTREE_LOADERS);
|
|
30
|
+
/** Build a root that contains only the subtree `first` dispatches into.
|
|
31
|
+
* Returns the FULL root when `first` is not a recognized subtree — bare `crtr`,
|
|
32
|
+
* `-h`/`--help`, `--version`, and any unknown leading token all need the
|
|
33
|
+
* complete tree (root -h lists every subtree; the unknown-path error names
|
|
34
|
+
* every valid child). This keeps every help/error surface byte-identical while
|
|
35
|
+
* the common leaf-dispatch path loads exactly one subtree. */
|
|
36
|
+
export async function resolveRoot(first) {
|
|
37
|
+
const loader = first !== undefined ? SUBTREE_LOADERS[first] : undefined;
|
|
38
|
+
if (loader !== undefined) {
|
|
39
|
+
return defineRoot({ tagline: TAGLINE, globals: [], subtrees: [await loader()] });
|
|
40
|
+
}
|
|
41
|
+
return buildRoot();
|
|
42
|
+
}
|
|
43
|
+
/** Assemble the full crtr command tree (all subtrees). Used for root -h, the
|
|
44
|
+
* unknown-path error, bare-root boot, and the listing-completeness test. Root
|
|
45
|
+
* owns only the tagline; every subtree declares its own root representation via
|
|
46
|
+
* its rootEntry. */
|
|
47
|
+
export async function buildRoot() {
|
|
48
|
+
const subtrees = await Promise.all(SUBTREE_NAMES.map((n) => SUBTREE_LOADERS[n]()));
|
|
49
|
+
return defineRoot({ tagline: TAGLINE, globals: [], subtrees });
|
|
38
50
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runCli } from './core/command.js';
|
|
3
|
-
import {
|
|
3
|
+
import { resolveRoot } from './build-root.js';
|
|
4
4
|
import { maybeBootRoot } from './core/runtime/front-door.js';
|
|
5
5
|
import { maybeAutoUpdate } from './core/auto-update.js';
|
|
6
6
|
import { ensureOfficialMarketplace, ensureProjectScope } from './core/bootstrap.js';
|
|
7
7
|
import { provisionExports } from './core/skill-sync/export.js';
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
8
|
+
async function main() {
|
|
9
|
+
// Lazy command tree: load only the subtree this invocation dispatches into.
|
|
10
|
+
// resolveRoot returns the full tree only for help/version/bare/unknown, where
|
|
11
|
+
// every subtree is genuinely needed (root -h, the unknown-path error). The
|
|
12
|
+
// hot leaf-dispatch path loads one subtree, keeping the other 11 (and their
|
|
13
|
+
// heavy deps — the attach TUI, web/vite) off cold-start.
|
|
14
|
+
const root = await resolveRoot(process.argv[2]);
|
|
15
|
+
// The front door: bare `crtr` (or `crtr [dir] ["prompt"]`) boots a resident
|
|
16
|
+
// root node and execs pi in this terminal. Recognized subcommands fall through
|
|
17
|
+
// to the normal dispatcher. Must run before anything that assumes a subcommand.
|
|
18
|
+
if (maybeBootRoot(root, process.argv)) {
|
|
19
|
+
// bootRoot exec'd pi inline and exited; unreachable.
|
|
20
|
+
}
|
|
21
|
+
ensureOfficialMarketplace(process.argv);
|
|
22
|
+
provisionExports(root);
|
|
23
|
+
ensureProjectScope(process.argv);
|
|
24
|
+
maybeAutoUpdate(process.argv);
|
|
25
|
+
await runCli(root, process.argv);
|
|
17
26
|
}
|
|
18
|
-
|
|
19
|
-
provisionExports(root);
|
|
20
|
-
ensureProjectScope(process.argv);
|
|
21
|
-
maybeAutoUpdate(process.argv);
|
|
22
|
-
runCli(root, process.argv).catch((err) => {
|
|
27
|
+
main().catch((err) => {
|
|
23
28
|
const msg = err instanceof Error ? err.message : String(err);
|
|
24
29
|
process.stderr.write(`crtr: ${msg}\n`);
|
|
25
30
|
process.exit(1);
|
package/dist/commands/chord.js
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
// `crtr <argv>`. This keeps the menu static while the behaviour stays fully
|
|
9
9
|
// config-driven (no per-node menu rebuild).
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
// • a digit key 1..9 → focus report N (the Nth live report of the pane node)
|
|
11
|
+
// One special case bypasses the bind table:
|
|
13
12
|
// • a bind whose `run` is the sentinel `__graph__` → send-keys `/graph` into
|
|
14
13
|
// the pane (toggles the in-pi GRAPH modal); the menu emits this directly so
|
|
15
14
|
// the dispatcher only handles it defensively.
|
|
@@ -23,7 +22,7 @@ import { InputError } from '../core/io.js';
|
|
|
23
22
|
import { readConfig } from '../core/config.js';
|
|
24
23
|
import { sendKeysEnter } from '../core/runtime/tmux-chrome.js';
|
|
25
24
|
import { nodeInPane } from './node.js';
|
|
26
|
-
import { getNode, subscribersOf,
|
|
25
|
+
import { getNode, subscribersOf, view, fullName, } from '../core/canvas/index.js';
|
|
27
26
|
const pexec = promisify(execFile);
|
|
28
27
|
/** Template vars available to a `run` string. Single-valued vars interpolate
|
|
29
28
|
* in place (preserving spaces, e.g. a node name); `{subtree}` is multi-valued
|
|
@@ -76,7 +75,7 @@ export const chordLeaf = defineLeaf({
|
|
|
76
75
|
name: 'key',
|
|
77
76
|
type: 'string',
|
|
78
77
|
required: true,
|
|
79
|
-
constraint: 'The chord key pressed after alt+c (e.g.
|
|
78
|
+
constraint: 'The chord key pressed after alt+c (e.g. g or m).',
|
|
80
79
|
},
|
|
81
80
|
],
|
|
82
81
|
output: [
|
|
@@ -99,26 +98,6 @@ export const chordLeaf = defineLeaf({
|
|
|
99
98
|
next: 'Run from inside an agent\'s pane, or pass --pane <pane-id>.',
|
|
100
99
|
});
|
|
101
100
|
}
|
|
102
|
-
// Digit keys 1..9 → focus the Nth live report (generated, not a bind entry).
|
|
103
|
-
if (/^[1-9]$/.test(key)) {
|
|
104
|
-
const n = parseInt(key, 10);
|
|
105
|
-
const reports = subscriptionsOf(selfId)
|
|
106
|
-
.map((r) => r.node_id)
|
|
107
|
-
.filter((id) => {
|
|
108
|
-
const s = getNode(id)?.status;
|
|
109
|
-
return s === 'active' || s === 'idle';
|
|
110
|
-
});
|
|
111
|
-
const target = reports[n - 1];
|
|
112
|
-
if (target === undefined)
|
|
113
|
-
return { ran: false, key, node_id: selfId, action: 'noop' };
|
|
114
|
-
try {
|
|
115
|
-
await pexec('crtr', ['node', 'focus', target], { timeout: 15_000 });
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
/* best-effort */
|
|
119
|
-
}
|
|
120
|
-
return { ran: true, key, node_id: selfId, action: `node focus ${target}` };
|
|
121
|
-
}
|
|
122
101
|
const bind = readConfig('user').canvasNav.prefixBinds[key];
|
|
123
102
|
if (bind === undefined)
|
|
124
103
|
return { ran: false, key, node_id: selfId, action: 'noop' };
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
import { test } from 'node:test';
|
|
10
10
|
import assert from 'node:assert/strict';
|
|
11
11
|
import { buildRoot } from '../../build-root.js';
|
|
12
|
-
test('every non-hidden listing child declares description + whenToUse', () => {
|
|
13
|
-
const root = buildRoot();
|
|
12
|
+
test('every non-hidden listing child declares description + whenToUse', async () => {
|
|
13
|
+
const root = await buildRoot();
|
|
14
14
|
const missing = [];
|
|
15
15
|
const walk = (branch, path) => {
|
|
16
16
|
for (const child of branch.help.listing ?? []) {
|
|
@@ -23,11 +23,11 @@ export declare function listFocuses(): FocusRow[];
|
|
|
23
23
|
* targeting it. */
|
|
24
24
|
export declare function graphSurfaceTarget(nodeId: string): FocusRow | null;
|
|
25
25
|
/** Synchronously wait until a broker's view.sock accepts a connection. The spawn
|
|
26
|
-
* and focus flows are sync (the command layer calls them directly), so the
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* and focus flows are sync (the command layer calls them directly), so the async
|
|
27
|
+
* net poll runs in a worker THREAD while this thread blocks on `Atomics.wait` —
|
|
28
|
+
* in-process, no child Node cold-start. Success proves more than file existence:
|
|
29
|
+
* it is robust to a stale leftover socket the launching broker has not unlinked
|
|
30
|
+
* yet, because only an accepting listener yields a `connect`. */
|
|
31
31
|
export declare function waitForBrokerViewSocket(nodeId: string): boolean;
|
|
32
32
|
/** A reviver: ensure a node's broker ENGINE is alive (idempotent → reviveNode →
|
|
33
33
|
* headlessBrokerHost.launch). Injected so placement.ts need not import revive.ts
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// model-over-driver so the §5.1 import-lint ("only placement.ts /
|
|
24
24
|
// tmux-chrome.ts import tmux.ts") holds — every other module reaches the
|
|
25
25
|
// driver verbs through here.
|
|
26
|
-
import {
|
|
26
|
+
import { Worker } from 'node:worker_threads';
|
|
27
27
|
import { join } from 'node:path';
|
|
28
28
|
import { getNode, openFocusRow, closeFocusRow, getFocusByNode, getFocusByPane, getFocusById, listFocuses as listFocusRows, view, } from '../canvas/index.js';
|
|
29
29
|
import { paneExists, paneLocation, ensureSession, openNodeWindow, splitWindow, closePane, currentTmux, switchClient, selectWindow, getPaneOption, } from './tmux.js';
|
|
@@ -115,19 +115,17 @@ export function graphSurfaceTarget(nodeId) {
|
|
|
115
115
|
// ---------------------------------------------------------------------------
|
|
116
116
|
const BROKER_FOCUS_SOCKET_WAIT_MS = 30_000;
|
|
117
117
|
const BROKER_FOCUS_SOCKET_RETRY_MS = 100;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
* not unlinked yet, because only an accepting listener exits 0. */
|
|
124
|
-
export function waitForBrokerViewSocket(nodeId) {
|
|
125
|
-
const sockPath = join(nodeDir(nodeId), 'view.sock');
|
|
126
|
-
const probe = `
|
|
118
|
+
// The probe runs inside a worker thread (see waitForBrokerViewSocket). Async net
|
|
119
|
+
// events drive the same poll loop; the worker reports its verdict into a shared
|
|
120
|
+
// Int32 (0 = pending, 1 = accepted, 2 = gave up) and Atomics.notify wakes the
|
|
121
|
+
// blocked main thread. eval:true ⇒ CommonJS, so `require` and workerData apply.
|
|
122
|
+
const VIEW_SOCKET_PROBE_WORKER = `
|
|
127
123
|
const net = require('node:net');
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
const
|
|
124
|
+
const { workerData, parentPort } = require('node:worker_threads');
|
|
125
|
+
const { buffer, sockPath, waitMs, retryMs } = workerData;
|
|
126
|
+
const result = new Int32Array(buffer);
|
|
127
|
+
const deadline = Date.now() + waitMs;
|
|
128
|
+
const report = (code) => { Atomics.store(result, 0, code); Atomics.notify(result, 0); };
|
|
131
129
|
function attempt() {
|
|
132
130
|
let socket;
|
|
133
131
|
let settled = false;
|
|
@@ -135,9 +133,9 @@ function attempt() {
|
|
|
135
133
|
if (settled) return;
|
|
136
134
|
settled = true;
|
|
137
135
|
if (socket !== undefined) socket.destroy();
|
|
138
|
-
if (ok)
|
|
139
|
-
if (Date.now() >= deadline)
|
|
140
|
-
setTimeout(attempt,
|
|
136
|
+
if (ok) { report(1); return; }
|
|
137
|
+
if (Date.now() >= deadline) { report(2); return; }
|
|
138
|
+
setTimeout(attempt, retryMs);
|
|
141
139
|
};
|
|
142
140
|
try {
|
|
143
141
|
socket = net.createConnection(sockPath);
|
|
@@ -147,18 +145,31 @@ function attempt() {
|
|
|
147
145
|
}
|
|
148
146
|
socket.once('connect', () => finish(true));
|
|
149
147
|
socket.once('error', () => finish(false));
|
|
150
|
-
socket.setTimeout(
|
|
148
|
+
socket.setTimeout(retryMs, () => finish(false));
|
|
151
149
|
}
|
|
152
150
|
attempt();
|
|
151
|
+
void parentPort;
|
|
153
152
|
`;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
/** Synchronously wait until a broker's view.sock accepts a connection. The spawn
|
|
154
|
+
* and focus flows are sync (the command layer calls them directly), so the async
|
|
155
|
+
* net poll runs in a worker THREAD while this thread blocks on `Atomics.wait` —
|
|
156
|
+
* in-process, no child Node cold-start. Success proves more than file existence:
|
|
157
|
+
* it is robust to a stale leftover socket the launching broker has not unlinked
|
|
158
|
+
* yet, because only an accepting listener yields a `connect`. */
|
|
159
|
+
export function waitForBrokerViewSocket(nodeId) {
|
|
160
|
+
const sockPath = join(nodeDir(nodeId), 'view.sock');
|
|
161
|
+
const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
|
|
162
|
+
const result = new Int32Array(buffer);
|
|
163
|
+
const worker = new Worker(VIEW_SOCKET_PROBE_WORKER, {
|
|
164
|
+
eval: true,
|
|
165
|
+
workerData: { buffer, sockPath, waitMs: BROKER_FOCUS_SOCKET_WAIT_MS, retryMs: BROKER_FOCUS_SOCKET_RETRY_MS },
|
|
160
166
|
});
|
|
161
|
-
|
|
167
|
+
worker.unref();
|
|
168
|
+
// Block until the worker reports (or a hard timeout matching the old
|
|
169
|
+
// spawnSync). 'not-equal' covers the worker finishing before we wait.
|
|
170
|
+
Atomics.wait(result, 0, 0, BROKER_FOCUS_SOCKET_WAIT_MS + 1_000);
|
|
171
|
+
void worker.terminate();
|
|
172
|
+
return Atomics.load(result, 0) === 1;
|
|
162
173
|
}
|
|
163
174
|
function newFocusId() {
|
|
164
175
|
return `f-${newNodeId()}`;
|
|
@@ -178,8 +178,8 @@ export declare function switchClient(session: string): boolean;
|
|
|
178
178
|
* empty, same limitation as the menu's `/promote` item. Best-effort. */
|
|
179
179
|
export declare function sendKeysEnter(pane: string, text: string): boolean;
|
|
180
180
|
/** Bind Alt+C to the crouter action menu. Best-effort; false if tmux fails.
|
|
181
|
-
* The built-in items (promote/demote/detach/close
|
|
182
|
-
* chords (graph
|
|
181
|
+
* The built-in items (promote/resume/demote/detach/close) are static; the canvas-nav
|
|
182
|
+
* chords (default g→graph, m→manager + any custom prefixBind) are appended
|
|
183
183
|
* from `canvasNav.prefixBinds`, each routed through `crtr canvas chord` (or, for
|
|
184
184
|
* the `__graph__` sentinel, a `send-keys '/graph'`) so the menu stays static
|
|
185
185
|
* while behaviour is config-driven. */
|
|
@@ -336,8 +336,8 @@ export function sendKeysEnter(pane, text) {
|
|
|
336
336
|
* `prefixBind` may not claim these (the built-in item wins). */
|
|
337
337
|
const RESERVED_MENU_KEYS = new Set(['o', 'r', 'd', 'D', 'x']);
|
|
338
338
|
/** Bind Alt+C to the crouter action menu. Best-effort; false if tmux fails.
|
|
339
|
-
* The built-in items (promote/demote/detach/close
|
|
340
|
-
* chords (graph
|
|
339
|
+
* The built-in items (promote/resume/demote/detach/close) are static; the canvas-nav
|
|
340
|
+
* chords (default g→graph, m→manager + any custom prefixBind) are appended
|
|
341
341
|
* from `canvasNav.prefixBinds`, each routed through `crtr canvas chord` (or, for
|
|
342
342
|
* the `__graph__` sentinel, a `send-keys '/graph'`) so the menu stays static
|
|
343
343
|
* while behaviour is config-driven. */
|
|
@@ -363,7 +363,7 @@ export function installMenuBinding() {
|
|
|
363
363
|
// marks them canceled); revivable. Output discarded — the keypress just acts.
|
|
364
364
|
{ name: 'close agent + subtree', key: 'x', cmd: `run-shell "crtr node close --pane '#{pane_id}' >/dev/null 2>&1"` },
|
|
365
365
|
];
|
|
366
|
-
// Canvas-nav chords from config (default: g→graph, m→manager
|
|
366
|
+
// Canvas-nav chords from config (default: g→graph, m→manager). The
|
|
367
367
|
// `__graph__` sentinel toggles the in-pi GRAPH modal via send-keys; every
|
|
368
368
|
// other bind shells the chord dispatcher, which resolves the pane's node and
|
|
369
369
|
// interpolates the bind at popup time. Keys colliding with the built-ins are
|
|
@@ -382,26 +382,16 @@ export function installMenuBinding() {
|
|
|
382
382
|
: `run-shell "crtr canvas chord --pane '#{pane_id}' --key ${key} >/dev/null 2>&1"`;
|
|
383
383
|
items.push({ name, key, cmd });
|
|
384
384
|
}
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
// display-menu is modal, so the root `M-c` binding is shadowed while the menu
|
|
396
|
-
// is up; tmux instead assembles the re-pressed Esc+c into the meta key `M-c`
|
|
397
|
-
// and delivers it to the menu's own key handler. An unhandled key is ignored
|
|
398
|
-
// (the menu just stays open) and the trailing `c` never leaks to the editor
|
|
399
|
-
// (verified empirically in tmux 3.6b: no native ESC-cancel + c-leak occurs for
|
|
400
|
-
// an atomic keypress). We catch that `M-c` with a menu item keyed to it whose
|
|
401
|
-
// command is empty — selecting it runs nothing and closes the menu. The row
|
|
402
|
-
// doubles as the menu's self-documenting dismiss hint. Placed last so it reads
|
|
403
|
-
// as chrome below the actions; `M-c` collides with no mnemonic above.
|
|
404
|
-
items.push({ name: 'close menu', key: 'M-c', cmd: '' });
|
|
385
|
+
// Dismiss hint. A tmux display-menu always closes on its native cancel keys
|
|
386
|
+
// (Escape / q / C-c), encoding-independent. We do NOT try to catch a re-pressed
|
|
387
|
+
// Alt+C: under `extended-keys on` (common, and what pi negotiates) the second
|
|
388
|
+
// Alt+C reaches the overlay as a CSI-u key (`\033[99;3u`) that tmux's menu does
|
|
389
|
+
// NOT match against an `M-c` mnemonic item, so a "close menu" row keyed M-c
|
|
390
|
+
// never fires and the menu just sits open (verified in tmux 3.6b: legacy Esc+c
|
|
391
|
+
// closes, CSI-u does not). Instead, a disabled (`-` prefix → dim, unselectable)
|
|
392
|
+
// last row tells the user the close keys that always work. Placed last so it
|
|
393
|
+
// reads as chrome below the actions.
|
|
394
|
+
items.push({ name: '-esc / q to close', key: '', cmd: '' });
|
|
405
395
|
// tmux's -x sets the menu's LEFT edge. To sit the box INSIDE the pane's
|
|
406
396
|
// top-right corner, shift x left by the box width (longest line + tmux chrome:
|
|
407
397
|
// borders + padding + the right-aligned mnemonic-key column) via format math.
|