@fungi.computer/nacre 0.0.2 → 0.1.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/dist/command-host.d.ts +35 -6
- package/dist/command-host.js +67 -26
- package/dist/keys.d.ts +2 -2
- package/dist/keys.js +3 -3
- package/package.json +3 -3
package/dist/command-host.d.ts
CHANGED
|
@@ -58,11 +58,26 @@ export type AppCommandDeclaration = Readonly<{
|
|
|
58
58
|
}>>;
|
|
59
59
|
}>;
|
|
60
60
|
import type { AppHostHandlers, AppHostReservedChord } from "@fungi.computer/agar/host";
|
|
61
|
-
import { type WhistleExecutionInput, type WhistleExecutionOutcome, type WhistleContributionLease, type WhistleCommand, type WhistleMenuProjection, type WhistleRuntime, type WhistleSnapshot } from "@fungi.computer/whistle";
|
|
61
|
+
import { type WhistleExecutionInput, type WhistleExecutionOutcome, type WhistleContributionLease, type WhistleCommand, type WhistleCommandKey, type WhistleMenuProjection, type WhistleProjectionRow, type WhistleRuntime, type WhistleSnapshot } from "@fungi.computer/whistle";
|
|
62
62
|
/** Select the shell command-palette chord for a Mac or non-Mac keyboard. */
|
|
63
63
|
export declare function commandPaletteReservedChordForPlatform(mac: boolean): AppHostReservedChord;
|
|
64
64
|
/** Select the shell command-palette chord from the embedding browser user agent. */
|
|
65
65
|
export declare function commandPaletteReservedChordForUserAgent(userAgent: string): AppHostReservedChord;
|
|
66
|
+
/** One App command, named by its surface and its declared command id. */
|
|
67
|
+
export type AppCommandTarget = Readonly<{
|
|
68
|
+
/** The App surface, such as `installation:inkcap`. */
|
|
69
|
+
surfaceId: string;
|
|
70
|
+
/** The command id the App's Release declares. */
|
|
71
|
+
commandId: string;
|
|
72
|
+
}>;
|
|
73
|
+
/**
|
|
74
|
+
* The typed Whistle identity of a command a mounted App surface contributed:
|
|
75
|
+
* each mounted surface owns the Whistle namespace named by its surface id.
|
|
76
|
+
*
|
|
77
|
+
* @param target - The App surface and its declared command id.
|
|
78
|
+
* @returns The key every snapshot row of that command carries as `command`.
|
|
79
|
+
*/
|
|
80
|
+
export declare function appSurfaceCommandKey(target: AppCommandTarget): WhistleCommandKey;
|
|
66
81
|
/** Whistle source of Release-advertised rows for Apps not yet mounted. */
|
|
67
82
|
export declare const appDirectorySourceId = "app-directory";
|
|
68
83
|
/** The command side of one mounted App's host handlers. */
|
|
@@ -76,10 +91,19 @@ export type AppSurfaceCommandMount = Readonly<{
|
|
|
76
91
|
}>;
|
|
77
92
|
/** Commands owned by one pane, projected to globals plus its focus scope. */
|
|
78
93
|
export type PaneCommands = Readonly<{
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
/**
|
|
95
|
+
* The key of a command this pane contributed.
|
|
96
|
+
*
|
|
97
|
+
* @param id - The command's local id.
|
|
98
|
+
* @param group - The group it was contributed under, if any.
|
|
99
|
+
*/
|
|
100
|
+
command: (id: string, group?: string) => WhistleCommandKey;
|
|
101
|
+
/**
|
|
102
|
+
* Contribute or replace commands owned by this pane through the returned
|
|
103
|
+
* lease. A group gives one embedded source (such as a Parakeet contribution)
|
|
104
|
+
* its own namespace inside the pane, so its ids never collide with others.
|
|
105
|
+
*/
|
|
106
|
+
contribute: (commands: readonly WhistleCommand<never>[], group?: string) => WhistleContributionLease;
|
|
83
107
|
/** Run a command by id. */
|
|
84
108
|
execute: WhistleRuntime["execute"];
|
|
85
109
|
/** Read the commands and bindings currently available. */
|
|
@@ -110,6 +134,11 @@ export type AppSurfaceCommandHost = Readonly<{
|
|
|
110
134
|
appTitle: string;
|
|
111
135
|
command: AppCommandDeclaration;
|
|
112
136
|
}>[], run: (surfaceId: string, commandId: string) => Promise<void>) => () => void;
|
|
137
|
+
/**
|
|
138
|
+
* The App command a snapshot row runs: a mounted App's own row, or a
|
|
139
|
+
* directory row that opens its App first. Shell and pane rows have none.
|
|
140
|
+
*/
|
|
141
|
+
readonly appCommand: (row: Pick<WhistleProjectionRow, "command" | "sourceId">) => AppCommandTarget | undefined;
|
|
113
142
|
/** Read the menu rows belonging to the focused App. */
|
|
114
143
|
readonly menu: () => readonly WhistleMenuProjection[];
|
|
115
144
|
/**
|
|
@@ -118,7 +147,7 @@ export type AppSurfaceCommandHost = Readonly<{
|
|
|
118
147
|
*/
|
|
119
148
|
readonly active: () => WhistleSnapshot;
|
|
120
149
|
/** Execute an active row, an agent row, or an exact programmatic action. */
|
|
121
|
-
readonly execute: (
|
|
150
|
+
readonly execute: (command: WhistleCommandKey, input: WhistleExecutionInput) => Promise<WhistleExecutionOutcome>;
|
|
122
151
|
/** Read the complete renderer-neutral graph for host consumers. */
|
|
123
152
|
readonly snapshot: () => WhistleSnapshot;
|
|
124
153
|
/** Observe graph changes, including contribution replacement and disposal. */
|
package/dist/command-host.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { AppError } from "@fungi.computer/agar/errors";
|
|
2
|
-
import { createWhistle, scopeSnapshot, } from "@fungi.computer/whistle";
|
|
2
|
+
import { createWhistle, sameWhistleCommand, scopeSnapshot, } from "@fungi.computer/whistle";
|
|
3
3
|
const emptyMenu = Object.freeze([]);
|
|
4
|
+
/** The shell command an App's reserved command-palette chord runs. */
|
|
5
|
+
const commandPaletteCommand = Object.freeze({
|
|
6
|
+
namespace: "shell",
|
|
7
|
+
id: "show-command-palette",
|
|
8
|
+
});
|
|
4
9
|
const commandPaletteReservedChord = Object.freeze({
|
|
5
10
|
altKey: false,
|
|
6
11
|
ctrlKey: false,
|
|
@@ -21,6 +26,16 @@ export function commandPaletteReservedChordForPlatform(mac) {
|
|
|
21
26
|
export function commandPaletteReservedChordForUserAgent(userAgent) {
|
|
22
27
|
return commandPaletteReservedChordForPlatform(/Mac|iPhone|iPad/u.test(userAgent));
|
|
23
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The typed Whistle identity of a command a mounted App surface contributed:
|
|
31
|
+
* each mounted surface owns the Whistle namespace named by its surface id.
|
|
32
|
+
*
|
|
33
|
+
* @param target - The App surface and its declared command id.
|
|
34
|
+
* @returns The key every snapshot row of that command carries as `command`.
|
|
35
|
+
*/
|
|
36
|
+
export function appSurfaceCommandKey(target) {
|
|
37
|
+
return Object.freeze({ namespace: target.surfaceId, id: target.commandId });
|
|
38
|
+
}
|
|
24
39
|
/** Whistle source of Release-advertised rows for Apps not yet mounted. */
|
|
25
40
|
export const appDirectorySourceId = "app-directory";
|
|
26
41
|
/** Bindings an App may never claim: the leader chord crosses every iframe. */
|
|
@@ -154,7 +169,7 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
|
|
|
154
169
|
}
|
|
155
170
|
return {
|
|
156
171
|
sourceId: mount.id,
|
|
157
|
-
namespace:
|
|
172
|
+
namespace: mount.id,
|
|
158
173
|
scope: mount.id,
|
|
159
174
|
commands: commands.map((command) => {
|
|
160
175
|
// Agar carries a static availability fact; Whistle evaluates availability
|
|
@@ -231,7 +246,9 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
|
|
|
231
246
|
throw new Error("App reserved chord is unavailable");
|
|
232
247
|
if (input.repeat)
|
|
233
248
|
return;
|
|
234
|
-
const outcome = await execute(
|
|
249
|
+
const outcome = await execute(commandPaletteCommand, {
|
|
250
|
+
origin: "browser",
|
|
251
|
+
});
|
|
235
252
|
if (outcome.status !== "handled")
|
|
236
253
|
throw new Error("App reserved chord could not run");
|
|
237
254
|
};
|
|
@@ -247,11 +264,11 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
|
|
|
247
264
|
});
|
|
248
265
|
const agentCommands = Object.freeze({
|
|
249
266
|
snapshot: agentSnapshot,
|
|
250
|
-
execute: (
|
|
267
|
+
execute: (command, input) => {
|
|
251
268
|
if (input.origin !== "agent" ||
|
|
252
|
-
!agentSnapshot().agent.some((row) => row.
|
|
269
|
+
!agentSnapshot().agent.some((row) => sameWhistleCommand(row.command, command)))
|
|
253
270
|
return Promise.resolve({ status: "missing" });
|
|
254
|
-
return execute(
|
|
271
|
+
return execute(command, input);
|
|
255
272
|
},
|
|
256
273
|
});
|
|
257
274
|
return Object.freeze({
|
|
@@ -277,7 +294,9 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
|
|
|
277
294
|
throw new Error("Pane command mount ID must not be blank");
|
|
278
295
|
if (mounts.has(paneId) || panes.has(paneId))
|
|
279
296
|
throw new Error("Pane command mount ID is already active");
|
|
280
|
-
const namespace = `pane
|
|
297
|
+
const namespace = `pane:${paneId}`;
|
|
298
|
+
// A group is a namespace nested under the pane's own.
|
|
299
|
+
const groupNamespace = (group) => group === undefined ? namespace : `${namespace}/${group}`;
|
|
281
300
|
const ownedCommands = new Map();
|
|
282
301
|
const state = {
|
|
283
302
|
disposed: false,
|
|
@@ -317,16 +336,18 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
|
|
|
317
336
|
throw new Error("Pane commands cannot bind the reserved leader chord");
|
|
318
337
|
};
|
|
319
338
|
return Object.freeze({
|
|
320
|
-
|
|
321
|
-
contribute: (commands) => {
|
|
339
|
+
command: (id, group) => Object.freeze({ namespace: groupNamespace(group), id }),
|
|
340
|
+
contribute: (commands, group) => {
|
|
322
341
|
assertPaneLive();
|
|
323
342
|
checkBindings(commands);
|
|
343
|
+
const contributed = groupNamespace(group);
|
|
324
344
|
const underlying = whistle.contribute({
|
|
325
345
|
sourceId: `pane:${paneId}`,
|
|
326
|
-
namespace,
|
|
346
|
+
namespace: contributed,
|
|
327
347
|
scope: paneId,
|
|
328
348
|
commands,
|
|
329
349
|
});
|
|
350
|
+
const keys = (next) => next.map((command) => Object.freeze({ namespace: contributed, id: command.id }));
|
|
330
351
|
let active = true;
|
|
331
352
|
const lease = Object.freeze({
|
|
332
353
|
update: (replacement) => {
|
|
@@ -334,7 +355,7 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
|
|
|
334
355
|
return;
|
|
335
356
|
checkBindings(replacement);
|
|
336
357
|
underlying.update(replacement);
|
|
337
|
-
ownedCommands.set(lease,
|
|
358
|
+
ownedCommands.set(lease, keys(replacement));
|
|
338
359
|
},
|
|
339
360
|
dispose: () => {
|
|
340
361
|
if (!active)
|
|
@@ -346,10 +367,10 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
|
|
|
346
367
|
},
|
|
347
368
|
});
|
|
348
369
|
state.leases.add(lease);
|
|
349
|
-
ownedCommands.set(lease,
|
|
370
|
+
ownedCommands.set(lease, keys(commands));
|
|
350
371
|
return lease;
|
|
351
372
|
},
|
|
352
|
-
execute: (
|
|
373
|
+
execute: (command, input) => {
|
|
353
374
|
const snapshot = state.snapshot;
|
|
354
375
|
const rows = input.origin === "agent"
|
|
355
376
|
? [snapshot.agent]
|
|
@@ -364,16 +385,16 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
|
|
|
364
385
|
// A catalog action can be callable from an admitted row without a menu
|
|
365
386
|
// or palette projection. Its contribution still belongs to this pane.
|
|
366
387
|
const owned = input.origin !== "agent" &&
|
|
367
|
-
[...ownedCommands.values()].some((
|
|
388
|
+
[...ownedCommands.values()].some((keys) => keys.some((key) => sameWhistleCommand(key, command)));
|
|
368
389
|
const admitted = owned ||
|
|
369
|
-
rows.some((projection) => projection.some((row) => row.
|
|
390
|
+
rows.some((projection) => projection.some((row) => sameWhistleCommand(row.command, command)));
|
|
370
391
|
const human = input.origin !== "programmatic" && input.origin !== "agent";
|
|
371
392
|
if (state.disposed ||
|
|
372
393
|
disposed ||
|
|
373
394
|
!admitted ||
|
|
374
395
|
(human && focusedMount !== paneId))
|
|
375
396
|
return Promise.resolve({ status: "missing" });
|
|
376
|
-
return whistle.execute(
|
|
397
|
+
return whistle.execute(command, input);
|
|
377
398
|
},
|
|
378
399
|
snapshot: () => state.snapshot,
|
|
379
400
|
subscribe: (listener) => {
|
|
@@ -432,18 +453,24 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
|
|
|
432
453
|
shellContribution = undefined;
|
|
433
454
|
};
|
|
434
455
|
};
|
|
456
|
+
// Directory rows share one namespace; each row's local id is an opaque
|
|
457
|
+
// index whose App command target this map holds.
|
|
458
|
+
let directoryTargets = new Map();
|
|
435
459
|
const registerDirectoryCommands = (rows, run) => {
|
|
436
460
|
assertLive();
|
|
437
461
|
directoryContribution?.dispose();
|
|
462
|
+
const targets = new Map();
|
|
438
463
|
const contribution = whistle.contribute({
|
|
439
464
|
sourceId: appDirectorySourceId,
|
|
440
|
-
namespace:
|
|
441
|
-
commands: rows.flatMap(({ surfaceId, appTitle, command }) => {
|
|
465
|
+
namespace: appDirectorySourceId,
|
|
466
|
+
commands: rows.flatMap(({ surfaceId, appTitle, command }, index) => {
|
|
442
467
|
const palette = command.projections?.palette;
|
|
443
468
|
if (palette === undefined)
|
|
444
469
|
return [];
|
|
470
|
+
const id = `entry-${String(index)}`;
|
|
471
|
+
targets.set(id, Object.freeze({ surfaceId, commandId: command.id }));
|
|
445
472
|
const projected = {
|
|
446
|
-
id
|
|
473
|
+
id,
|
|
447
474
|
title: command.title,
|
|
448
475
|
category: `${appTitle} · ${command.category}`,
|
|
449
476
|
projections: {
|
|
@@ -457,32 +484,44 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
|
|
|
457
484
|
}),
|
|
458
485
|
});
|
|
459
486
|
directoryContribution = contribution;
|
|
487
|
+
directoryTargets = targets;
|
|
460
488
|
let active = true;
|
|
461
489
|
return () => {
|
|
462
490
|
if (!active)
|
|
463
491
|
return;
|
|
464
492
|
active = false;
|
|
465
493
|
contribution.dispose();
|
|
466
|
-
if (directoryContribution === contribution)
|
|
494
|
+
if (directoryContribution === contribution) {
|
|
467
495
|
directoryContribution = undefined;
|
|
496
|
+
directoryTargets = new Map();
|
|
497
|
+
}
|
|
468
498
|
};
|
|
469
499
|
};
|
|
500
|
+
const appCommand = (row) => {
|
|
501
|
+
if (row.sourceId === appDirectorySourceId)
|
|
502
|
+
return row.command.namespace === appDirectorySourceId
|
|
503
|
+
? directoryTargets.get(row.command.id)
|
|
504
|
+
: undefined;
|
|
505
|
+
return mounts.has(row.sourceId) && row.command.namespace === row.sourceId
|
|
506
|
+
? Object.freeze({ surfaceId: row.sourceId, commandId: row.command.id })
|
|
507
|
+
: undefined;
|
|
508
|
+
};
|
|
470
509
|
const menu = () => focusedMenu;
|
|
471
|
-
const isActive = (
|
|
510
|
+
const isActive = (command) => {
|
|
472
511
|
const { bindings, palette, slash, help, menu: rows } = activeSnapshot;
|
|
473
|
-
return [bindings, palette, slash, help, rows].some((projection) => projection.some((row) => row.
|
|
512
|
+
return [bindings, palette, slash, help, rows].some((projection) => projection.some((row) => sameWhistleCommand(row.command, command)));
|
|
474
513
|
};
|
|
475
|
-
const execute = async (
|
|
514
|
+
const execute = async (command, input) => {
|
|
476
515
|
if (input.origin === "agent" &&
|
|
477
516
|
!whistle
|
|
478
517
|
.snapshot()
|
|
479
|
-
.agent.some((
|
|
518
|
+
.agent.some((row) => sameWhistleCommand(row.command, command)))
|
|
480
519
|
return { status: "missing" };
|
|
481
520
|
if (input.origin !== "programmatic" &&
|
|
482
521
|
input.origin !== "agent" &&
|
|
483
|
-
!isActive(
|
|
522
|
+
!isActive(command))
|
|
484
523
|
return { status: "missing" };
|
|
485
|
-
return whistle.execute(
|
|
524
|
+
return whistle.execute(command, input);
|
|
486
525
|
};
|
|
487
526
|
const dispose = () => {
|
|
488
527
|
if (disposed)
|
|
@@ -501,6 +540,7 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
|
|
|
501
540
|
shellContribution = undefined;
|
|
502
541
|
directoryContribution?.dispose();
|
|
503
542
|
directoryContribution = undefined;
|
|
543
|
+
directoryTargets = new Map();
|
|
504
544
|
mounts.clear();
|
|
505
545
|
focusedMount = null;
|
|
506
546
|
focusedMenu = emptyMenu;
|
|
@@ -515,6 +555,7 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
|
|
|
515
555
|
focus,
|
|
516
556
|
registerShellCommands,
|
|
517
557
|
registerDirectoryCommands,
|
|
558
|
+
appCommand,
|
|
518
559
|
menu,
|
|
519
560
|
active: () => activeSnapshot,
|
|
520
561
|
execute,
|
package/dist/keys.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WhistleBindingProjection, WhistleExecutionInput, WhistleExecutionOutcome, WhistleSnapshot } from "@fungi.computer/whistle";
|
|
1
|
+
import type { WhistleBindingProjection, WhistleCommandKey, WhistleExecutionInput, WhistleExecutionOutcome, WhistleSnapshot } from "@fungi.computer/whistle";
|
|
2
2
|
/** One physical chord parsed from a Whistle binding such as `alt+left`. */
|
|
3
3
|
export type ShellChord = Readonly<{
|
|
4
4
|
/** The key's normalized name, such as left or escape. */
|
|
@@ -37,7 +37,7 @@ export type ShellKeyHost = Readonly<{
|
|
|
37
37
|
/** Observe command changes; call the returned disposer to stop listening. */
|
|
38
38
|
subscribe: (listener: () => void) => () => void;
|
|
39
39
|
/** Run a command through the host with the original input origin. */
|
|
40
|
-
execute: (
|
|
40
|
+
execute: (command: WhistleCommandKey, input: WhistleExecutionInput) => Promise<WhistleExecutionOutcome>;
|
|
41
41
|
}>;
|
|
42
42
|
/**
|
|
43
43
|
* Dispatch the active view's single-chord bindings from the shell document.
|
package/dist/keys.js
CHANGED
|
@@ -103,9 +103,9 @@ function isEditing(event) {
|
|
|
103
103
|
export function installShellKeys(root, host, mac) {
|
|
104
104
|
let chords = [];
|
|
105
105
|
const refresh = () => {
|
|
106
|
-
chords = host.active().bindings.flatMap(({ binding,
|
|
106
|
+
chords = host.active().bindings.flatMap(({ binding, command }) => {
|
|
107
107
|
const chord = parseChord(binding, mac);
|
|
108
|
-
return chord === null ? [] : [{
|
|
108
|
+
return chord === null ? [] : [{ command, chord }];
|
|
109
109
|
});
|
|
110
110
|
};
|
|
111
111
|
const onKeyDown = (event) => {
|
|
@@ -117,7 +117,7 @@ export function installShellKeys(root, host, mac) {
|
|
|
117
117
|
return;
|
|
118
118
|
event.preventDefault();
|
|
119
119
|
void host
|
|
120
|
-
.execute(match.
|
|
120
|
+
.execute(match.command, { origin: "keyboard" })
|
|
121
121
|
.catch((cause) => {
|
|
122
122
|
console.warn("Shell key command failed", cause);
|
|
123
123
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fungi.computer/nacre",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"author": "Levi <levi@technomancy.dev>",
|
|
5
5
|
"description": "The shell client for hosting Apps: workbench, command host, keys, commands, window manager and App directory rows",
|
|
6
6
|
"type": "module",
|
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
"LICENSE"
|
|
53
53
|
],
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@fungi.computer/agar": "0.
|
|
55
|
+
"@fungi.computer/agar": "0.5.0",
|
|
56
56
|
"@fungi.computer/ear-tag": "0.1.0",
|
|
57
57
|
"@fungi.computer/gills": "0.2.0",
|
|
58
58
|
"@fungi.computer/puffball": "0.2.0",
|
|
59
|
-
"@fungi.computer/whistle": "0.
|
|
59
|
+
"@fungi.computer/whistle": "0.3.0",
|
|
60
60
|
"effect": "4.0.2",
|
|
61
61
|
"zod": "4.4.3"
|
|
62
62
|
},
|