@dolusoft/command-console 0.0.0-stage → 1.0.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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dolusoft
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,115 @@
1
- # Temporary Holding Version
1
+ # @dolusoft/command-console
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A headless command console for Vue 3: `useCommandConsole` and the `CommandConsole` component of
4
+ [Command Console](https://github.com/dolusoft/command-console). A scrollback of entries above one input line with a
5
+ prompt: the user types a line, the console runs your command for it and shows what the command returns. Line editing,
6
+ recall, completion, cancellation, a context stack and the redaction of sensitive lines are the library's; the commands,
7
+ every text and all styling are yours. Ships no CSS; `vue` 3.5+ is its only peer.
8
+
9
+ ```sh
10
+ pnpm add @dolusoft/command-console
11
+ ```
12
+
13
+ The full guide (commands, returns and effects, completion, recall, keys, sensitive lines) is the
14
+ [repository README](https://github.com/dolusoft/command-console#readme).
15
+
16
+ ## The component
17
+
18
+ ```vue
19
+ <script setup lang="ts">
20
+ import { ref } from 'vue'
21
+ import {
22
+ CommandConsole,
23
+ type CommandDefinition,
24
+ type ConsoleEntry
25
+ } from '@dolusoft/command-console'
26
+
27
+ const commands: CommandDefinition[] = [
28
+ { name: 'echo', run: request => request.rest },
29
+ // From argument 1 on the line is secret: it is never kept, shown or emitted.
30
+ {
31
+ name: 'set',
32
+ sensitive: { fromArg: 1 },
33
+ run: (request, run) => save(request.args, run.signal)
34
+ },
35
+ { name: 'exit', run: () => ({ effects: [{ type: 'close' }] }) }
36
+ ]
37
+ const entries = ref<ConsoleEntry[]>([])
38
+ const open = ref(true)
39
+ </script>
40
+
41
+ <template>
42
+ <CommandConsole
43
+ v-if="open"
44
+ v-model:entries="entries"
45
+ :commands="commands"
46
+ prompt="app>"
47
+ autofocus
48
+ @close="open = false"
49
+ />
50
+ </template>
51
+ ```
52
+
53
+ - **Recall, completion and context** are always on. `entries`, `recall` and `context` are the console's own unless
54
+ bound with `v-model:*`; `initialState` gives the first value of an unbound slice.
55
+ - **Props:** `commands`, `prompt`, `initialState`, `createId`, `maxEntries`, `beforeRun`, `onError`, `formatError`,
56
+ `isSensitive`, `recallLimit`, `shouldRecord`, `labels`, `autofocus`, and `entries` / `recall` / `context`.
57
+ - **Events:** `update:entries`, `update:recall`, `update:context` (for `v-model`, each resolved against the value last
58
+ emitted, so two changes in one tick are both kept), `close` (a command's `close` effect) and `submit`
59
+ (`{ inputId, sensitive }`, never the line; for Enter and the exposed `run`).
60
+ - **Slots:** `block-text`, `block-json`, `block-table`, `block-list`, `block-message`, `block-custom` (`{ block, entry }`,
61
+ inside a `div.cc-block`), `prompt`, `busy`, `completion`. A slot replaces content, never the element that carries the
62
+ hooks.
63
+ - **Labels:** every text the component shows is in `labels`, over English defaults; `messages` merges one level deep
64
+ and a message may be a function of its parameters.
65
+ - **Expose:** `focus()`, `run(line)`, `cancel()`, `reset()`, `clear()`, `console` and `state` (unwrapped by the
66
+ template ref: `consoleRef.value.state.running`).
67
+
68
+ ## Styling
69
+
70
+ The component renders plain elements with `cc-*` classes and `data-*` attributes, listed in
71
+ [`contract/dom.ts`](https://github.com/dolusoft/command-console/blob/main/contract/dom.ts). Nothing has a size or a
72
+ style of its own: give `.cc-log` a height and `overflow-y: auto` and it scrolls, staying at the bottom while the user is
73
+ there.
74
+
75
+ ```css
76
+ .cc-log {
77
+ height: 20rem;
78
+ overflow-y: auto;
79
+ }
80
+ .cc-block {
81
+ margin: 0;
82
+ white-space: pre-wrap;
83
+ }
84
+ .cc-entry[data-tone='error'] {
85
+ color: crimson;
86
+ }
87
+ .cc-console[data-busy] .cc-input {
88
+ opacity: 0.6;
89
+ }
90
+ .cc-completion[aria-selected='true'] {
91
+ background: highlight;
92
+ }
93
+ ```
94
+
95
+ ## The composable
96
+
97
+ `useCommandConsole(options)` gives the same console without markup: `{ console, state }`, where `state` is a read-only
98
+ `ShallowRef` of the console's state. A value option (`commands`, `prompt`, `state`, `maxEntries`, `recallLimit`) takes
99
+ a value, a ref or a getter; a function option takes the function or a ref to it. `features` and `initialState` are read
100
+ once, and the console is disposed with the calling scope. The optional features come from
101
+ `@dolusoft/command-console-core`:
102
+
103
+ ```ts
104
+ import { useCommandConsole } from '@dolusoft/command-console'
105
+ import { consoleFeatures, recallFeature } from '@dolusoft/command-console-core'
106
+
107
+ const { console, state } = useCommandConsole({
108
+ features: consoleFeatures({ recallFeature }),
109
+ commands: () => commands.value
110
+ })
111
+ ```
112
+
113
+ ## License
114
+
115
+ MIT, see [LICENSE.md](./LICENSE.md).
@@ -0,0 +1,559 @@
1
+ import { Fragment, computed, createBlock, createElementBlock, createElementVNode, createTextVNode, defineComponent, h, nextTick, onBeforeUnmount, onMounted, onScopeDispose, openBlock, renderSlot, resolveDynamicComponent, shallowReadonly, shallowRef, toDisplayString, toRef, toValue, unref, useId, useSlots, useTemplateRef, watch } from "vue";
2
+ import { completionFeature, consoleFeatures, contextFeature, createConsole, recallFeature } from "@dolusoft/command-console-core";
3
+ // src/dom/focus.ts
4
+ var interactive = "a, button, input, select, textarea, [tabindex], [contenteditable]";
5
+ /**
6
+ * Whether a click on `target` inside `log` should focus the input: not when
7
+ * text is selected, not inside an interactive element.
8
+ */
9
+ var clickFocusesInput = (target, log) => {
10
+ const selection = log.ownerDocument.getSelection();
11
+ if (selection && !selection.isCollapsed && selection.toString() !== "") return false;
12
+ const found = (target instanceof Element ? target : target instanceof Node ? target.parentElement : null)?.closest(interactive);
13
+ return !(found && log.contains(found));
14
+ };
15
+ // src/dom/selection.ts
16
+ /** Whether the document has a non-empty selection inside `root`. */
17
+ var hasSelectionIn = (root) => {
18
+ const selection = root?.ownerDocument.getSelection();
19
+ if (!root || !selection || selection.isCollapsed) return false;
20
+ if (selection.toString() === "") return false;
21
+ const { anchorNode, focusNode } = selection;
22
+ return anchorNode !== null && root.contains(anchorNode) || focusNode !== null && root.contains(focusNode);
23
+ };
24
+ /** Where text is selected, as `KeyInput.selection` reports it. */
25
+ var selectionOf = (input, root) => {
26
+ if (input.selectionStart !== null && input.selectionStart !== input.selectionEnd) return "input";
27
+ return hasSelectionIn(root) ? "document" : "none";
28
+ };
29
+ // src/dom/keys.ts
30
+ /** The key press `event` on `input`, inside the console `root`. */
31
+ var keyInputOf = (event, input, root) => ({
32
+ key: event.key,
33
+ ctrlKey: event.ctrlKey,
34
+ shiftKey: event.shiftKey,
35
+ altKey: event.altKey,
36
+ metaKey: event.metaKey,
37
+ isComposing: event.isComposing || event.keyCode === 229,
38
+ selection: selectionOf(input, root)
39
+ });
40
+ // src/dom/scroll.ts
41
+ /** How close to the bottom still counts as at the bottom, in px. */
42
+ var slack = 2;
43
+ var useStickToBottom = (log, entries) => {
44
+ let stuck = true;
45
+ let observer = null;
46
+ let mounted = false;
47
+ const toBottom = () => {
48
+ const element = log.value;
49
+ if (mounted && element) element.scrollTop = element.scrollHeight;
50
+ };
51
+ onMounted(() => {
52
+ mounted = true;
53
+ if (typeof ResizeObserver !== "undefined" && entries.value) {
54
+ observer = new ResizeObserver(() => {
55
+ if (stuck) toBottom();
56
+ });
57
+ observer.observe(entries.value);
58
+ }
59
+ });
60
+ onBeforeUnmount(() => {
61
+ mounted = false;
62
+ observer?.disconnect();
63
+ observer = null;
64
+ });
65
+ return {
66
+ stick() {
67
+ stuck = true;
68
+ nextTick(toBottom);
69
+ },
70
+ onScroll() {
71
+ const element = log.value;
72
+ if (element) stuck = element.scrollHeight - element.scrollTop - element.clientHeight <= slack;
73
+ }
74
+ };
75
+ };
76
+ // src/labels.ts
77
+ /** The English defaults (README: Labels). */
78
+ var defaultLabels = {
79
+ input: "Command",
80
+ inputDescription: "Enter runs the command. Tab completes; on an empty line it leaves the console.",
81
+ log: "Console output",
82
+ redacted: "(input hidden)",
83
+ cancelledMarker: "^C",
84
+ completions: "Suggestions",
85
+ busy: "Running…",
86
+ messages: {
87
+ "command-not-found": (params) => params.name === null ? "command not found" : `${params.name}: command not found`,
88
+ "command-failed": "The command failed.",
89
+ "unterminated-quote": "Unterminated quote.",
90
+ interrupted: "Interrupted."
91
+ }
92
+ };
93
+ /**
94
+ * The defaults with `labels` over them; `messages` merges one level deep, so
95
+ * overriding one message keeps the others.
96
+ */
97
+ var resolveLabels = (labels) => ({
98
+ ...defaultLabels,
99
+ ...labels,
100
+ messages: {
101
+ ...defaultLabels.messages,
102
+ ...labels?.messages
103
+ }
104
+ });
105
+ /** The text of a message block, from `labels.messages`. */
106
+ var messageText = (labels, block) => {
107
+ const label = labels.messages[block.code];
108
+ if (typeof label === "string") return label;
109
+ return label(block.params ?? {});
110
+ };
111
+ // src/parts/completions-view.ts
112
+ /** The id of candidate `index` of the list `listId`. */
113
+ var optionId = (listId, index) => `${listId}-${index}`;
114
+ var renderCompletions = (session, listId, label, slots) => h("ul", {
115
+ class: "cc-completions",
116
+ role: "listbox",
117
+ id: listId,
118
+ "aria-label": label
119
+ }, session.candidates.map((candidate, index) => {
120
+ const active = index === session.active;
121
+ return h("li", {
122
+ key: `${index}:${candidate}`,
123
+ class: "cc-completion",
124
+ role: "option",
125
+ id: optionId(listId, index),
126
+ "aria-selected": active ? "true" : "false"
127
+ }, slots.completion ? slots.completion({
128
+ candidate,
129
+ index,
130
+ active
131
+ }) : candidate);
132
+ }));
133
+ // src/parts/block-view.ts
134
+ var json = (value) => JSON.stringify(value, null, 2);
135
+ /** A table cell: a missing key and `null` are empty. */
136
+ var cellText = (value) => value === void 0 || value === null ? "" : String(value);
137
+ var content = (block, labels) => {
138
+ const hooks = {
139
+ class: "cc-block",
140
+ "data-type": block.type
141
+ };
142
+ switch (block.type) {
143
+ case "text": return h("pre", hooks, block.text);
144
+ case "json": return h("pre", hooks, json(block.value));
145
+ case "table": return h("table", hooks, [h("thead", h("tr", block.columns.map((column) => h("th", { scope: "col" }, column.label)))), h("tbody", block.rows.map((row) => h("tr", block.columns.map((column) => h("td", cellText(row[column.key]))))))]);
146
+ case "list": return h("ul", hooks, block.items.map((item) => h("li", item)));
147
+ case "message": return h("div", hooks, messageText(labels, block));
148
+ case "custom": return h("pre", hooks, json(block));
149
+ }
150
+ };
151
+ /** Renders `block` of `entry`, through its slot when one is given. */
152
+ var renderBlock = (block, entry, labels, slots) => {
153
+ const slot = slots[`block-${block.type}`];
154
+ if (slot) return h("div", {
155
+ class: "cc-block",
156
+ "data-type": block.type
157
+ }, slot({
158
+ block,
159
+ entry
160
+ }));
161
+ return content(block, labels);
162
+ };
163
+ // src/parts/entry-view.ts
164
+ /** `span.cc-prompt` with the `prompt` slot inside, or the prompt text. */
165
+ var renderPrompt = (prompt, slots) => h("span", { class: "cc-prompt" }, slots.prompt ? slots.prompt({ prompt }) : prompt);
166
+ var renderEntry = (entry, labels, slots) => {
167
+ if (entry.kind === "input") return h("div", {
168
+ key: entry.id,
169
+ class: "cc-entry",
170
+ "data-kind": "input",
171
+ "data-status": entry.status,
172
+ "data-redacted": entry.redacted ? "" : void 0
173
+ }, [
174
+ renderPrompt(entry.prompt, slots),
175
+ h("span", { class: "cc-line" }, entry.redacted || entry.text === null ? labels.redacted : entry.text),
176
+ entry.status === "cancelled" ? h("span", { class: "cc-cancelled" }, labels.cancelledMarker) : null
177
+ ]);
178
+ return h("div", {
179
+ key: entry.id,
180
+ class: "cc-entry",
181
+ "data-kind": "output",
182
+ "data-tone": entry.tone,
183
+ "aria-live": entry.quiet ? "off" : void 0
184
+ }, entry.blocks.map((block) => renderBlock(block, entry, labels, slots)));
185
+ };
186
+ // src/use-command-console.ts
187
+ /** The options that are functions: a ref may hold one, a getter may not. */
188
+ var functionKeys = [
189
+ "onEntriesChange",
190
+ "onRecallChange",
191
+ "onContextChange",
192
+ "createId",
193
+ "beforeRun",
194
+ "onError",
195
+ "formatError",
196
+ "onClose",
197
+ "isSensitive",
198
+ "shouldRecord"
199
+ ];
200
+ /** The options that are values: a ref or a getter may give them. */
201
+ var valueKeys = [
202
+ "commands",
203
+ "prompt",
204
+ "state",
205
+ "maxEntries",
206
+ "recallLimit"
207
+ ];
208
+ /**
209
+ * Creates a console from `options` and keeps it in step with them.
210
+ *
211
+ * A value option (`commands`, `prompt`, `state`, `maxEntries`,
212
+ * `recallLimit`) may be a plain value, a ref or a getter. A function option
213
+ * (`beforeRun`, `onError`, …) may be the function or a ref that holds it: a
214
+ * bare function is the option itself, never a getter. `features` and
215
+ * `initialState` are read once.
216
+ *
217
+ * @example
218
+ * ```ts
219
+ * const { console, state } = useCommandConsole({
220
+ * features: consoleFeatures({ recallFeature }),
221
+ * commands: () => commands.value
222
+ * })
223
+ * ```
224
+ */
225
+ function useCommandConsole(options) {
226
+ const resolve = () => {
227
+ const resolved = {};
228
+ for (const key of valueKeys) resolved[key] = toValue(options[key]);
229
+ for (const key of functionKeys) resolved[key] = unref(options[key]);
230
+ return resolved;
231
+ };
232
+ const console = createConsole({
233
+ ...resolve(),
234
+ features: options.features,
235
+ initialState: toValue(options.initialState)
236
+ });
237
+ const state = shallowRef(console.getState());
238
+ const unsubscribe = console.subscribe((next) => {
239
+ state.value = next;
240
+ });
241
+ const stop = watch(resolve, (next) => {
242
+ console.setOptions((old) => ({
243
+ ...next,
244
+ features: old.features,
245
+ initialState: old.initialState
246
+ }));
247
+ });
248
+ onScopeDispose(() => {
249
+ stop();
250
+ unsubscribe();
251
+ console.dispose();
252
+ state.value = console.getState();
253
+ });
254
+ return {
255
+ console,
256
+ state: shallowReadonly(state)
257
+ };
258
+ }
259
+ // src/command-console.vue?vue&type=script&setup=true&lang.ts
260
+ var _hoisted_1 = ["data-busy", "data-context-depth"];
261
+ var _hoisted_2 = ["aria-label"];
262
+ var _hoisted_3 = {
263
+ ref: "entries",
264
+ class: "cc-entries"
265
+ };
266
+ var _hoisted_4 = {
267
+ class: "cc-status",
268
+ role: "status"
269
+ };
270
+ var _hoisted_5 = { class: "cc-input-line" };
271
+ var _hoisted_6 = [
272
+ "aria-expanded",
273
+ "aria-controls",
274
+ "aria-activedescendant",
275
+ "aria-label",
276
+ "aria-description",
277
+ "readonly"
278
+ ];
279
+ // src/command-console.vue
280
+ var command_console_default = /* @__PURE__ */ defineComponent({
281
+ name: "CommandConsole",
282
+ __name: "command-console",
283
+ props: {
284
+ commands: {},
285
+ prompt: { default: void 0 },
286
+ initialState: { default: void 0 },
287
+ createId: {
288
+ type: Function,
289
+ default: void 0
290
+ },
291
+ maxEntries: { default: void 0 },
292
+ beforeRun: {
293
+ type: Function,
294
+ default: void 0
295
+ },
296
+ onError: {
297
+ type: Function,
298
+ default: void 0
299
+ },
300
+ formatError: {
301
+ type: Function,
302
+ default: void 0
303
+ },
304
+ isSensitive: {
305
+ type: Function,
306
+ default: void 0
307
+ },
308
+ recallLimit: { default: void 0 },
309
+ shouldRecord: {
310
+ type: Function,
311
+ default: void 0
312
+ },
313
+ labels: { default: void 0 },
314
+ autofocus: {
315
+ type: Boolean,
316
+ default: false
317
+ },
318
+ entries: { default: void 0 },
319
+ recall: { default: void 0 },
320
+ context: { default: void 0 }
321
+ },
322
+ emits: [
323
+ "update:entries",
324
+ "update:recall",
325
+ "update:context",
326
+ "close",
327
+ "submit"
328
+ ],
329
+ setup(__props, { expose: __expose, emit: __emit }) {
330
+ const props = __props;
331
+ const emit = __emit;
332
+ const slots = useSlots();
333
+ const features = consoleFeatures({
334
+ recallFeature,
335
+ completionFeature,
336
+ contextFeature
337
+ });
338
+ const lastEmitted = {};
339
+ for (const slice of [
340
+ "entries",
341
+ "recall",
342
+ "context"
343
+ ]) watch(() => props[slice], () => {
344
+ delete lastEmitted[slice];
345
+ }, { flush: "sync" });
346
+ const report = (slice, send) => (updater) => {
347
+ const base = lastEmitted[slice] ?? props[slice] ?? [];
348
+ const value = typeof updater === "function" ? updater(base) : updater;
349
+ lastEmitted[slice] = value;
350
+ send(value);
351
+ };
352
+ /** The slices bound with `v-model`; the others are the console's. */
353
+ const controlled = () => {
354
+ const state = {};
355
+ if (props.entries !== void 0) state.entries = props.entries;
356
+ if (props.recall !== void 0) state.recall = props.recall;
357
+ if (props.context !== void 0) state.context = props.context;
358
+ return state;
359
+ };
360
+ const { console, state } = useCommandConsole({
361
+ features,
362
+ commands: () => props.commands,
363
+ prompt: () => props.prompt,
364
+ state: controlled,
365
+ maxEntries: () => props.maxEntries,
366
+ recallLimit: () => props.recallLimit,
367
+ initialState: props.initialState,
368
+ createId: toRef(props, "createId"),
369
+ beforeRun: toRef(props, "beforeRun"),
370
+ onError: toRef(props, "onError"),
371
+ formatError: toRef(props, "formatError"),
372
+ isSensitive: toRef(props, "isSensitive"),
373
+ shouldRecord: toRef(props, "shouldRecord"),
374
+ onEntriesChange: report("entries", (value) => emit("update:entries", value)),
375
+ onRecallChange: report("recall", (value) => emit("update:recall", value)),
376
+ onContextChange: report("context", (value) => emit("update:context", value)),
377
+ onClose: () => emit("close")
378
+ });
379
+ const labels = computed(() => resolveLabels(props.labels));
380
+ const listId = useId();
381
+ const running = computed(() => state.value.running);
382
+ const session = computed(() => {
383
+ const completion = state.value.completion;
384
+ return completion?.status === "open" ? completion : null;
385
+ });
386
+ const activeId = computed(() => session.value && session.value.active >= 0 ? optionId(listId, session.value.active) : void 0);
387
+ /** The prompt of the live line: the top context frame's, or the root's. */
388
+ const prompt = computed(() => {
389
+ state.value.context;
390
+ props.prompt;
391
+ return console.getPrompt();
392
+ });
393
+ /**
394
+ * The recalled line, for the status element (README: Recall): a screen reader
395
+ * does not announce a scripted value change in a focused field.
396
+ */
397
+ const recalled = computed(() => {
398
+ const { recall, recallCursor } = state.value;
399
+ return recallCursor !== null ? recall[recallCursor] ?? "" : "";
400
+ });
401
+ const entriesView = () => state.value.entries.map((entry) => renderEntry(entry, labels.value, slots));
402
+ const completionsView = () => session.value ? renderCompletions(session.value, listId, labels.value.completions, slots) : null;
403
+ const promptView = () => renderPrompt(prompt.value, slots);
404
+ const rootElement = useTemplateRef("root");
405
+ const logElement = useTemplateRef("log");
406
+ const entriesElement = useTemplateRef("entries");
407
+ const inputElement = useTemplateRef("input");
408
+ const scroll = useStickToBottom(logElement, entriesElement);
409
+ const focusInput = () => inputElement.value?.focus();
410
+ onMounted(() => {
411
+ if (props.autofocus) focusInput();
412
+ });
413
+ const onLogClick = (event) => {
414
+ if (logElement.value && clickFocusesInput(event.target, logElement.value)) focusInput();
415
+ };
416
+ let hadFocus = false;
417
+ watch(() => state.value.input.revision, () => {
418
+ const element = inputElement.value;
419
+ hadFocus = element !== null && element.ownerDocument.activeElement === element;
420
+ if (element) element.value = "";
421
+ }, { flush: "pre" });
422
+ /** The value and caret this component last reported (a user edit). */
423
+ let reported = {
424
+ value: "",
425
+ caret: 0
426
+ };
427
+ /**
428
+ * Writes the console's value into the element. A write by the console
429
+ * (completion, recall) also sets the caret it chose; the user's own edit is
430
+ * left alone, so a selection the user made stays.
431
+ */
432
+ const syncElement = (replaced) => {
433
+ const element = inputElement.value;
434
+ if (!element) return;
435
+ const { value, caret } = state.value.input;
436
+ if (element.value !== value) element.value = value;
437
+ if (replaced) {
438
+ if (hadFocus) element.focus();
439
+ hadFocus = false;
440
+ element.setSelectionRange(value.length, value.length);
441
+ return;
442
+ }
443
+ if (element.ownerDocument.activeElement !== element || value === reported.value && caret === reported.caret) return;
444
+ if (element.selectionStart !== caret || element.selectionEnd !== caret) element.setSelectionRange(caret, caret);
445
+ };
446
+ onMounted(() => {
447
+ syncElement(false);
448
+ });
449
+ watch(() => state.value.input, (next, previous) => {
450
+ syncElement(next.revision !== previous.revision);
451
+ }, { flush: "post" });
452
+ /** Reports the element's value and caret to the console (a user edit). */
453
+ const reportEdit = (element) => {
454
+ const caret = element.selectionStart ?? element.value.length;
455
+ reported = {
456
+ value: element.value,
457
+ caret
458
+ };
459
+ console.editInput(element.value, caret);
460
+ };
461
+ const onInput = (event) => {
462
+ reportEdit(event.currentTarget);
463
+ };
464
+ const onCaret = (event) => {
465
+ const element = event.currentTarget;
466
+ const caret = element.selectionStart ?? element.value.length;
467
+ if (element.value !== state.value.input.value) return;
468
+ if (caret !== state.value.input.caret) reportEdit(element);
469
+ };
470
+ /**
471
+ * Runs `action` and emits `submit` for the input entry it added, if any;
472
+ * keeps the log at the bottom after the user's own submit (C-58).
473
+ */
474
+ const submitting = (action) => {
475
+ const before = new Set(state.value.entries.map((entry) => entry.id));
476
+ const result = action();
477
+ const added = console.getState().entries.find((entry) => entry.kind === "input" && entry.status === "submitted" && !before.has(entry.id));
478
+ if (added) {
479
+ scroll.stick();
480
+ const submit = {
481
+ inputId: added.id,
482
+ sensitive: added.kind === "input" && added.redacted
483
+ };
484
+ emit("submit", submit);
485
+ }
486
+ return result;
487
+ };
488
+ const onKeydown = (event) => {
489
+ const element = event.currentTarget;
490
+ const key = keyInputOf(event, element, rootElement.value);
491
+ if (!key.isComposing) onCaret(event);
492
+ if (submitting(() => console.handleKey(key))) event.preventDefault();
493
+ };
494
+ __expose({
495
+ focus: () => {
496
+ focusInput();
497
+ },
498
+ run: (line) => submitting(() => console.run(line)),
499
+ cancel: () => {
500
+ console.cancel();
501
+ },
502
+ reset: () => {
503
+ console.reset();
504
+ },
505
+ clear: () => {
506
+ console.clearEntries();
507
+ },
508
+ console,
509
+ state
510
+ });
511
+ return (_ctx, _cache) => {
512
+ return openBlock(), createElementBlock("div", {
513
+ ref: "root",
514
+ class: "cc-console",
515
+ "data-busy": running.value ? "" : void 0,
516
+ "data-context-depth": unref(state).context.length
517
+ }, [
518
+ createElementVNode("div", {
519
+ ref: "log",
520
+ class: "cc-log",
521
+ role: "log",
522
+ "aria-label": labels.value.log,
523
+ onClick: onLogClick,
524
+ onScroll: _cache[0] || (_cache[0] = (...args) => unref(scroll).onScroll && unref(scroll).onScroll(...args))
525
+ }, [createElementVNode("div", _hoisted_3, [(openBlock(), createBlock(resolveDynamicComponent(entriesView)))], 512)], 40, _hoisted_2),
526
+ createElementVNode("div", _hoisted_4, [running.value ? renderSlot(_ctx.$slots, "busy", { running: running.value }, () => [createTextVNode(toDisplayString(labels.value.busy), 1)], void 0, 0) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(recalled.value), 1)], 64))]),
527
+ (openBlock(), createBlock(resolveDynamicComponent(completionsView))),
528
+ createElementVNode("div", _hoisted_5, [(openBlock(), createBlock(resolveDynamicComponent(promptView))), (openBlock(), createElementBlock("input", {
529
+ key: unref(state).input.revision,
530
+ ref: "input",
531
+ class: "cc-input",
532
+ type: "text",
533
+ role: "combobox",
534
+ "aria-autocomplete": "list",
535
+ "aria-expanded": session.value ? "true" : "false",
536
+ "aria-controls": session.value ? unref(listId) : void 0,
537
+ "aria-activedescendant": activeId.value,
538
+ "aria-label": labels.value.input,
539
+ "aria-description": labels.value.inputDescription,
540
+ autocomplete: "off",
541
+ autocorrect: "off",
542
+ autocapitalize: "off",
543
+ spellcheck: "false",
544
+ readonly: running.value !== null,
545
+ onInput,
546
+ onKeydown,
547
+ onKeyup: onCaret,
548
+ onClick: onCaret,
549
+ onSelectionchange: onCaret
550
+ }, null, 40, _hoisted_6))])
551
+ ], 8, _hoisted_1);
552
+ };
553
+ }
554
+ });
555
+ // src/index.ts
556
+ var src_default = command_console_default;
557
+ export { command_console_default as CommandConsole, src_default as default, useCommandConsole };
558
+
559
+ //# sourceMappingURL=command-console.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-console.js","names":["_useSlots","_openBlock","_createElementBlock","_unref","_createElementVNode","_createBlock","_resolveDynamicComponent","_renderSlot","_createTextVNode","_toDisplayString","_Fragment","_sfc_main","CommandConsole"],"sources":["../src/dom/focus.ts","../src/dom/selection.ts","../src/dom/keys.ts","../src/dom/scroll.ts","../src/labels.ts","../src/parts/completions-view.ts","../src/parts/block-view.ts","../src/parts/entry-view.ts","../src/use-command-console.ts","../src/command-console.vue","../src/command-console.vue","../src/index.ts"],"sourcesContent":["// A click in the log focuses the input (README: DOM and styling, B-33), unless\n// the user is selecting text or clicked something of its own.\n\nconst interactive =\n 'a, button, input, select, textarea, [tabindex], [contenteditable]'\n\n/**\n * Whether a click on `target` inside `log` should focus the input: not when\n * text is selected, not inside an interactive element.\n */\nexport const clickFocusesInput = (\n target: EventTarget | null,\n log: Element\n): boolean => {\n const selection = log.ownerDocument.getSelection()\n if (selection && !selection.isCollapsed && selection.toString() !== '') {\n return false\n }\n const element =\n target instanceof Element\n ? target\n : target instanceof Node\n ? target.parentElement\n : null\n const found = element?.closest(interactive)\n return !(found && log.contains(found))\n}\n","// Copy is never taken over (README: Keyboard, D-07): with text selected in\n// the input, or in the console's part of the document, Ctrl+C stays the\n// browser's.\nimport type { KeyInput } from '@dolusoft/command-console-core'\n\n/** Whether the document has a non-empty selection inside `root`. */\nconst hasSelectionIn = (root: Element | null): boolean => {\n const selection = root?.ownerDocument.getSelection()\n if (!root || !selection || selection.isCollapsed) {\n return false\n }\n if (selection.toString() === '') {\n return false\n }\n const { anchorNode, focusNode } = selection\n return (\n (anchorNode !== null && root.contains(anchorNode)) ||\n (focusNode !== null && root.contains(focusNode))\n )\n}\n\n/** Where text is selected, as `KeyInput.selection` reports it. */\nexport const selectionOf = (\n input: HTMLInputElement,\n root: Element | null\n): KeyInput['selection'] => {\n if (\n input.selectionStart !== null &&\n input.selectionStart !== input.selectionEnd\n ) {\n return 'input'\n }\n return hasSelectionIn(root) ? 'document' : 'none'\n}\n","// KeyboardEvent → KeyInput (core README: State): the adapter's whole share of\n// the key map. Core never sees a DOM event.\nimport type { KeyInput } from '@dolusoft/command-console-core'\n\nimport { selectionOf } from './selection'\n\n/** The key press `event` on `input`, inside the console `root`. */\nexport const keyInputOf = (\n event: KeyboardEvent,\n input: HTMLInputElement,\n root: Element | null\n): KeyInput => ({\n key: event.key,\n ctrlKey: event.ctrlKey,\n shiftKey: event.shiftKey,\n altKey: event.altKey,\n metaKey: event.metaKey,\n // 229: the key an IME holds while it composes (older engines do not set\n // `isComposing` on the keydown that starts a composition).\n isComposing: event.isComposing || event.keyCode === 229,\n selection: selectionOf(input, root)\n})\n","// Scroll (README: DOM and styling, Q-08, C-58): the log sticks to the bottom\n// while the user is within 2 px of it, and after the user's own submit; new\n// output does not move a view the user scrolled up. `scrollTop` is the only\n// style-related write of the package (P5).\nimport { nextTick, onBeforeUnmount, onMounted, type ShallowRef } from 'vue'\n\n/** How close to the bottom still counts as at the bottom, in px. */\nconst slack = 2\n\nexport interface StickToBottom {\n /** The user's own submit: stick again and show the bottom now. */\n stick(): void\n /** The log's `scroll` event. */\n onScroll(): void\n}\n\nexport const useStickToBottom = (\n log: Readonly<ShallowRef<HTMLElement | null>>,\n entries: Readonly<ShallowRef<HTMLElement | null>>\n): StickToBottom => {\n let stuck = true\n let observer: ResizeObserver | null = null\n let mounted = false\n\n const toBottom = () => {\n const element = log.value\n if (mounted && element) {\n element.scrollTop = element.scrollHeight\n }\n }\n\n onMounted(() => {\n mounted = true\n // One observer on the entries; a late callback after unmount does\n // nothing (D-04).\n if (typeof ResizeObserver !== 'undefined' && entries.value) {\n observer = new ResizeObserver(() => {\n if (stuck) {\n toBottom()\n }\n })\n observer.observe(entries.value)\n }\n })\n\n onBeforeUnmount(() => {\n mounted = false\n observer?.disconnect()\n observer = null\n })\n\n return {\n stick() {\n stuck = true\n void nextTick(toBottom)\n },\n onScroll() {\n const element = log.value\n if (element) {\n stuck =\n element.scrollHeight - element.scrollTop - element.clientHeight <=\n slack\n }\n }\n }\n}\n","// Every text the component renders comes from here (P7, C-47): English\n// defaults, merged one level deep with the `labels` prop.\nimport type { MessageBlock, MessageParams } from '@dolusoft/command-protocol'\n\nimport type { ConsoleLabels, ConsoleLabelsInput } from './contract'\n\n/** The English defaults (README: Labels). */\nconst defaultLabels: ConsoleLabels = {\n input: 'Command',\n inputDescription:\n 'Enter runs the command. Tab completes; on an empty line it leaves the console.',\n log: 'Console output',\n redacted: '(input hidden)',\n cancelledMarker: '^C',\n completions: 'Suggestions',\n busy: 'Running…',\n messages: {\n 'command-not-found': params =>\n params.name === null\n ? 'command not found'\n : `${params.name}: command not found`,\n 'command-failed': 'The command failed.',\n 'unterminated-quote': 'Unterminated quote.',\n interrupted: 'Interrupted.'\n }\n}\n\n/**\n * The defaults with `labels` over them; `messages` merges one level deep, so\n * overriding one message keeps the others.\n */\nexport const resolveLabels = (labels?: ConsoleLabelsInput): ConsoleLabels => ({\n ...defaultLabels,\n ...labels,\n messages: { ...defaultLabels.messages, ...labels?.messages }\n})\n\n/** The text of a message block, from `labels.messages`. */\nexport const messageText = (\n labels: ConsoleLabels,\n block: MessageBlock\n): string => {\n const label = labels.messages[block.code] as\n string | ((params: MessageParams[typeof block.code]) => string)\n if (typeof label === 'string') {\n return label\n }\n // A library-made block of a code with parameters always carries them;\n // `{}` keeps a consumer-made block without them from throwing.\n return label((block.params ?? {}) as never)\n}\n","// The completion list (README: DOM and styling, Completion): a named listbox,\n// one option per candidate, rendered only while a session is open. The ids tie\n// it to the input's `aria-controls` and `aria-activedescendant` (C-48).\nimport { h, type VNode, type VNodeArrayChildren } from 'vue'\n\nimport type { CompletionSession } from '@dolusoft/command-console-core'\n\nimport type { ConsoleSlots } from '../contract'\n\n/** The id of candidate `index` of the list `listId`. */\nexport const optionId = (listId: string, index: number): string =>\n `${listId}-${index}`\n\nexport const renderCompletions = (\n session: CompletionSession,\n listId: string,\n label: string,\n slots: Pick<ConsoleSlots, 'completion'>\n): VNode =>\n h(\n 'ul',\n {\n class: 'cc-completions',\n role: 'listbox',\n id: listId,\n 'aria-label': label\n },\n session.candidates.map((candidate, index) => {\n const active = index === session.active\n return h(\n 'li',\n {\n key: `${index}:${candidate}`,\n class: 'cc-completion',\n role: 'option',\n id: optionId(listId, index),\n 'aria-selected': active ? 'true' : 'false'\n },\n slots.completion\n ? (slots.completion({\n candidate,\n index,\n active\n }) as VNodeArrayChildren)\n : candidate\n )\n })\n )\n","// One output block (README: DOM and styling): `.cc-block` with its `data-type`,\n// in the element of its type, or a `div` around the consumer's `block-<type>`\n// slot (C-67). All text goes through Vue's escaping; there is no HTML (S-08).\nimport { h, type VNode, type VNodeArrayChildren } from 'vue'\n\nimport type {\n JsonPrimitive,\n OutputBlock,\n OutputEntry\n} from '@dolusoft/command-protocol'\n\nimport type { BlockSlotProps, ConsoleLabels, ConsoleSlots } from '../contract'\nimport { messageText } from '../labels'\n\n/** The `block-<type>` slots the component was given. */\nexport type BlockSlots = Pick<\n ConsoleSlots,\n | 'block-text'\n | 'block-json'\n | 'block-table'\n | 'block-list'\n | 'block-message'\n | 'block-custom'\n>\n\nconst json = (value: unknown) => JSON.stringify(value, null, 2)\n\n/** A table cell: a missing key and `null` are empty. */\nconst cellText = (value: JsonPrimitive | undefined): string =>\n value === undefined || value === null ? '' : String(value)\n\nconst content = (block: OutputBlock, labels: ConsoleLabels): VNode => {\n const hooks = { class: 'cc-block', 'data-type': block.type }\n switch (block.type) {\n case 'text':\n return h('pre', hooks, block.text)\n case 'json':\n return h('pre', hooks, json(block.value))\n case 'table':\n return h('table', hooks, [\n h(\n 'thead',\n h(\n 'tr',\n block.columns.map(column => h('th', { scope: 'col' }, column.label))\n )\n ),\n h(\n 'tbody',\n block.rows.map(row =>\n h(\n 'tr',\n block.columns.map(column => h('td', cellText(row[column.key])))\n )\n )\n )\n ])\n case 'list':\n return h(\n 'ul',\n hooks,\n block.items.map(item => h('li', item))\n )\n case 'message':\n return h('div', hooks, messageText(labels, block))\n case 'custom':\n return h('pre', hooks, json(block))\n }\n}\n\n/** Renders `block` of `entry`, through its slot when one is given. */\nexport const renderBlock = (\n block: OutputBlock,\n entry: OutputEntry,\n labels: ConsoleLabels,\n slots: BlockSlots\n): VNode => {\n const slot = slots[`block-${block.type}`] as\n ((props: BlockSlotProps) => unknown) | undefined\n if (slot) {\n return h(\n 'div',\n { class: 'cc-block', 'data-type': block.type },\n slot({ block, entry }) as VNodeArrayChildren\n )\n }\n return content(block, labels)\n}\n","// One entry of the log (README: DOM and styling): an ended input line (its\n// prompt, its text or `labels.redacted`, the cancelled marker) or the blocks of\n// an output.\nimport { h, type VNode, type VNodeArrayChildren } from 'vue'\n\nimport type { ConsoleEntry } from '@dolusoft/command-protocol'\n\nimport { renderBlock, type BlockSlots } from './block-view'\nimport type { ConsoleLabels, ConsoleSlots } from '../contract'\n\n/** The slots an entry may use. */\nexport type EntrySlots = BlockSlots & Pick<ConsoleSlots, 'prompt'>\n\n/** `span.cc-prompt` with the `prompt` slot inside, or the prompt text. */\nexport const renderPrompt = (\n prompt: string,\n slots: Pick<ConsoleSlots, 'prompt'>\n): VNode =>\n h(\n 'span',\n { class: 'cc-prompt' },\n slots.prompt ? (slots.prompt({ prompt }) as VNodeArrayChildren) : prompt\n )\n\nexport const renderEntry = (\n entry: ConsoleEntry,\n labels: ConsoleLabels,\n slots: EntrySlots\n): VNode => {\n if (entry.kind === 'input') {\n return h(\n 'div',\n {\n key: entry.id,\n class: 'cc-entry',\n 'data-kind': 'input',\n 'data-status': entry.status,\n 'data-redacted': entry.redacted ? '' : undefined\n },\n [\n renderPrompt(entry.prompt, slots),\n h(\n 'span',\n { class: 'cc-line' },\n entry.redacted || entry.text === null ? labels.redacted : entry.text\n ),\n entry.status === 'cancelled'\n ? h('span', { class: 'cc-cancelled' }, labels.cancelledMarker)\n : null\n ]\n )\n }\n return h(\n 'div',\n {\n key: entry.id,\n class: 'cc-entry',\n 'data-kind': 'output',\n 'data-tone': entry.tone,\n 'aria-live': entry.quiet ? 'off' : undefined\n },\n entry.blocks.map(block => renderBlock(block, entry, labels, slots))\n )\n}\n","// useCommandConsole (README: The composable): a console without markup. It\n// creates the console once, passes reactive option changes to `setOptions`,\n// mirrors every notification into a `shallowRef` and disposes the console with\n// its effect scope. It does no DOM work, so a consumer can draw a template of\n// its own. The Vue package keeps no console state of its own (P15).\nimport {\n onScopeDispose,\n shallowReadonly,\n shallowRef,\n toValue,\n unref,\n watch\n} from 'vue'\n\nimport {\n createConsole,\n type ConsoleOptions,\n type ConsoleState,\n type OptionalFeatures\n} from '@dolusoft/command-console-core'\n\nimport type {\n UseCommandConsoleOptions,\n UseCommandConsoleReturn\n} from './contract'\n\n/** The options that are functions: a ref may hold one, a getter may not. */\nconst functionKeys = [\n 'onEntriesChange',\n 'onRecallChange',\n 'onContextChange',\n 'createId',\n 'beforeRun',\n 'onError',\n 'formatError',\n 'onClose',\n 'isSensitive',\n 'shouldRecord'\n] as const\n\n/** The options that are values: a ref or a getter may give them. */\nconst valueKeys = [\n 'commands',\n 'prompt',\n 'state',\n 'maxEntries',\n 'recallLimit'\n] as const\n\n/**\n * Creates a console from `options` and keeps it in step with them.\n *\n * A value option (`commands`, `prompt`, `state`, `maxEntries`,\n * `recallLimit`) may be a plain value, a ref or a getter. A function option\n * (`beforeRun`, `onError`, …) may be the function or a ref that holds it: a\n * bare function is the option itself, never a getter. `features` and\n * `initialState` are read once.\n *\n * @example\n * ```ts\n * const { console, state } = useCommandConsole({\n * features: consoleFeatures({ recallFeature }),\n * commands: () => commands.value\n * })\n * ```\n */\nexport function useCommandConsole<\n // `{}` and not its constraint, as `createConsole` (ADR 0002).\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n T extends Partial<OptionalFeatures> = {}\n>(options: UseCommandConsoleOptions<T>): UseCommandConsoleReturn<T> {\n const resolve = (): ConsoleOptions<T> => {\n const resolved: Record<string, unknown> = {}\n for (const key of valueKeys) {\n resolved[key] = toValue(options[key])\n }\n for (const key of functionKeys) {\n resolved[key] = unref(options[key])\n }\n return resolved as unknown as ConsoleOptions<T>\n }\n\n const console = createConsole<T>({\n ...resolve(),\n features: options.features,\n initialState: toValue(options.initialState)\n })\n\n const state = shallowRef<ConsoleState>(console.getState())\n const unsubscribe = console.subscribe(next => {\n state.value = next\n })\n\n // Every reactive read of `resolve` is a dependency: a change of any option\n // hands the whole set to the console. The features stay those it was\n // created with.\n const stop = watch(resolve, next => {\n console.setOptions(old => ({\n ...next,\n features: old.features,\n initialState: old.initialState\n }))\n })\n\n onScopeDispose(() => {\n stop()\n unsubscribe()\n console.dispose()\n // The snapshot goes with the console: what it held (the live input\n // among it) is not kept.\n state.value = console.getState()\n })\n\n return {\n console: console,\n state: shallowReadonly(state)\n }\n}\n","<script setup lang=\"ts\">\n// <CommandConsole> (README: The component): the composable's console drawn as\n// the DOM of `contract/dom.ts` (README: DOM and styling). The component holds\n// no console state (P15): it renders the snapshot and forwards DOM events to\n// core actions. What it keeps is DOM-bound: element refs, the \"stuck to\n// bottom\" flag, the last value it emitted per controlled slice and the caret\n// it last reported.\nimport {\n computed,\n onMounted,\n toRef,\n useId,\n useTemplateRef,\n watch,\n type FunctionalComponent\n} from 'vue'\n\nimport {\n completionFeature,\n consoleFeatures,\n contextFeature,\n recallFeature,\n type OptionalFeatures,\n type Updater\n} from '@dolusoft/command-console-core'\nimport type { ConsoleEntry, ContextFrame } from '@dolusoft/command-protocol'\n\nimport type {\n ConsoleEmits,\n ConsoleProps,\n ConsoleSlots,\n ConsoleSubmit\n} from './contract'\nimport { clickFocusesInput } from './dom/focus'\nimport { keyInputOf } from './dom/keys'\nimport { useStickToBottom } from './dom/scroll'\nimport { resolveLabels } from './labels'\nimport { renderCompletions, optionId } from './parts/completions-view'\nimport { renderEntry, renderPrompt } from './parts/entry-view'\nimport { useCommandConsole } from './use-command-console'\n\ndefineOptions({ name: 'CommandConsole' })\n\nconst props = withDefaults(defineProps<ConsoleProps>(), {\n prompt: undefined,\n initialState: undefined,\n createId: undefined,\n maxEntries: undefined,\n beforeRun: undefined,\n onError: undefined,\n formatError: undefined,\n isSensitive: undefined,\n recallLimit: undefined,\n shouldRecord: undefined,\n labels: undefined,\n autofocus: false,\n entries: undefined,\n recall: undefined,\n context: undefined\n})\n\nconst emit = defineEmits<ConsoleEmits>()\n\nconst slots = defineSlots<ConsoleSlots>()\n\n// The component always uses every optional feature (README: The component).\nconst features: OptionalFeatures = consoleFeatures({\n recallFeature,\n completionFeature,\n contextFeature\n})\n\n// --- v-model (core README: Controlled slices, C-61) -------------------------\n// An updater is resolved against the value this component last emitted for\n// the slice, else the prop: a second action in the same tick builds on the\n// first one's value, not on a prop the parent has not re-rendered yet. A new\n// prop value clears it.\ntype Slice = 'entries' | 'recall' | 'context'\ninterface SliceValues {\n entries: ConsoleEntry[]\n recall: string[]\n context: ContextFrame[]\n}\nconst lastEmitted: Partial<SliceValues> = {}\nfor (const slice of ['entries', 'recall', 'context'] as const) {\n watch(\n () => props[slice],\n () => {\n delete lastEmitted[slice]\n },\n { flush: 'sync' }\n )\n}\nconst report =\n <K extends Slice>(slice: K, send: (value: SliceValues[K]) => void) =>\n (updater: Updater<SliceValues[K]>) => {\n const base = (lastEmitted[slice] ?? props[slice] ?? []) as SliceValues[K]\n const value = typeof updater === 'function' ? updater(base) : updater\n lastEmitted[slice] = value\n send(value)\n }\n\n/** The slices bound with `v-model`; the others are the console's. */\nconst controlled = () => {\n const state: Partial<SliceValues> = {}\n if (props.entries !== undefined) {\n state.entries = props.entries\n }\n if (props.recall !== undefined) {\n state.recall = props.recall\n }\n if (props.context !== undefined) {\n state.context = props.context\n }\n return state\n}\n\nconst { console, state } = useCommandConsole({\n features,\n commands: () => props.commands,\n prompt: () => props.prompt,\n state: controlled,\n maxEntries: () => props.maxEntries,\n recallLimit: () => props.recallLimit,\n initialState: props.initialState,\n createId: toRef(props, 'createId'),\n beforeRun: toRef(props, 'beforeRun'),\n onError: toRef(props, 'onError'),\n formatError: toRef(props, 'formatError'),\n isSensitive: toRef(props, 'isSensitive'),\n shouldRecord: toRef(props, 'shouldRecord'),\n onEntriesChange: report('entries', value => emit('update:entries', value)),\n onRecallChange: report('recall', value => emit('update:recall', value)),\n onContextChange: report('context', value => emit('update:context', value)),\n onClose: () => emit('close')\n})\n\n// --- What the template draws ------------------------------------------------\n\nconst labels = computed(() => resolveLabels(props.labels))\nconst listId = useId()\n\nconst running = computed(() => state.value.running)\nconst session = computed(() => {\n const completion = state.value.completion\n return completion?.status === 'open' ? completion : null\n})\nconst activeId = computed(() =>\n session.value && session.value.active >= 0\n ? optionId(listId, session.value.active)\n : undefined\n)\n/** The prompt of the live line: the top context frame's, or the root's. */\nconst prompt = computed(() => {\n void state.value.context\n void props.prompt\n return console.getPrompt()\n})\n/**\n * The recalled line, for the status element (README: Recall): a screen reader\n * does not announce a scripted value change in a focused field.\n */\nconst recalled = computed(() => {\n const { recall, recallCursor } = state.value\n return recallCursor !== null ? (recall[recallCursor] ?? '') : ''\n})\n\nconst entriesView: FunctionalComponent = () =>\n state.value.entries.map(entry => renderEntry(entry, labels.value, slots))\nconst completionsView: FunctionalComponent = () =>\n session.value\n ? renderCompletions(session.value, listId, labels.value.completions, slots)\n : null\nconst promptView: FunctionalComponent = () => renderPrompt(prompt.value, slots)\n\n// --- Elements ---------------------------------------------------------------\n\nconst rootElement = useTemplateRef<HTMLDivElement>('root')\nconst logElement = useTemplateRef<HTMLDivElement>('log')\nconst entriesElement = useTemplateRef<HTMLDivElement>('entries')\nconst inputElement = useTemplateRef<HTMLInputElement>('input')\n\nconst scroll = useStickToBottom(logElement, entriesElement)\n\nconst focusInput = () => inputElement.value?.focus()\n\nonMounted(() => {\n if (props.autofocus) {\n focusInput()\n }\n})\n\n// C-51: a click in the log focuses the input, unless text is selected or\n// the click was on something interactive.\nconst onLogClick = (event: MouseEvent) => {\n if (logElement.value && clickFocusesInput(event.target, logElement.value)) {\n focusInput()\n }\n}\n\n// --- The input element (README: DOM and styling, C-54) ----------------------\n// The element is keyed by `input.revision`. When it is replaced, the new\n// one gets focus only if the old one had it, and the caret goes to the end.\n// Its value is written as a DOM property here, never bound in the template:\n// Vue mirrors a bound `value` into the `value` attribute, which would put\n// the typed line into the DOM.\n\nlet hadFocus = false\nwatch(\n () => state.value.input.revision,\n () => {\n const element = inputElement.value\n hadFocus =\n element !== null && element.ownerDocument.activeElement === element\n // The element about to go may hold a sensitive line (C-36).\n if (element) {\n element.value = ''\n }\n },\n { flush: 'pre' }\n)\n\n/** The value and caret this component last reported (a user edit). */\nlet reported = { value: '', caret: 0 }\n\n/**\n * Writes the console's value into the element. A write by the console\n * (completion, recall) also sets the caret it chose; the user's own edit is\n * left alone, so a selection the user made stays.\n */\nconst syncElement = (replaced: boolean) => {\n const element = inputElement.value\n if (!element) {\n return\n }\n const { value, caret } = state.value.input\n if (element.value !== value) {\n element.value = value\n }\n if (replaced) {\n if (hadFocus) {\n element.focus()\n }\n hadFocus = false\n element.setSelectionRange(value.length, value.length)\n return\n }\n if (\n element.ownerDocument.activeElement !== element ||\n (value === reported.value && caret === reported.caret)\n ) {\n return\n }\n if (element.selectionStart !== caret || element.selectionEnd !== caret) {\n element.setSelectionRange(caret, caret)\n }\n}\nonMounted(() => {\n syncElement(false)\n})\nwatch(\n () => state.value.input,\n (next, previous) => {\n syncElement(next.revision !== previous.revision)\n },\n { flush: 'post' }\n)\n\n/** Reports the element's value and caret to the console (a user edit). */\nconst reportEdit = (element: HTMLInputElement) => {\n const caret = element.selectionStart ?? element.value.length\n reported = { value: element.value, caret }\n console.editInput(element.value, caret)\n}\n\nconst onInput = (event: Event) => {\n reportEdit(event.currentTarget as HTMLInputElement)\n}\n\n// A caret move the user made (click, arrow keys, selection).\nconst onCaret = (event: Event) => {\n const element = event.currentTarget as HTMLInputElement\n const caret = element.selectionStart ?? element.value.length\n if (element.value !== state.value.input.value) {\n return\n }\n if (caret !== state.value.input.caret) {\n reportEdit(element)\n }\n}\n\n// --- Submits (C-64) ---------------------------------------------------------\n\n/**\n * Runs `action` and emits `submit` for the input entry it added, if any;\n * keeps the log at the bottom after the user's own submit (C-58).\n */\nconst submitting = <T,>(action: () => T): T => {\n const before = new Set(state.value.entries.map(entry => entry.id))\n const result = action()\n const added = console\n .getState()\n .entries.find(\n entry =>\n entry.kind === 'input' &&\n entry.status === 'submitted' &&\n !before.has(entry.id)\n )\n if (added) {\n scroll.stick()\n const submit: ConsoleSubmit = {\n inputId: added.id,\n sensitive: added.kind === 'input' && added.redacted\n }\n emit('submit', submit)\n }\n return result\n}\n\nconst onKeydown = (event: KeyboardEvent) => {\n const element = event.currentTarget as HTMLInputElement\n const key = keyInputOf(event, element, rootElement.value)\n if (!key.isComposing) {\n // The caret may have moved without an event the console saw.\n onCaret(event)\n }\n const handled = submitting(() => console.handleKey(key))\n if (handled) {\n event.preventDefault()\n }\n}\n\n// --- Expose ------------------------------------------------------------------\n\nconst exposed = {\n focus: () => {\n focusInput()\n },\n run: (line: string) => submitting(() => console.run(line)),\n cancel: () => {\n console.cancel()\n },\n reset: () => {\n console.reset()\n },\n clear: () => {\n console.clearEntries()\n },\n console,\n state\n}\ndefineExpose(exposed)\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"cc-console\"\n :data-busy=\"running ? '' : undefined\"\n :data-context-depth=\"state.context.length\"\n >\n <div\n ref=\"log\"\n class=\"cc-log\"\n role=\"log\"\n :aria-label=\"labels.log\"\n @click=\"onLogClick\"\n @scroll=\"scroll.onScroll\"\n >\n <div ref=\"entries\" class=\"cc-entries\">\n <component :is=\"entriesView\" />\n </div>\n </div>\n <div class=\"cc-status\" role=\"status\">\n <template v-if=\"running\"\n ><slot name=\"busy\" :running=\"running\">{{ labels.busy }}</slot></template\n ><template v-else>{{ recalled }}</template>\n </div>\n <component :is=\"completionsView\" />\n <div class=\"cc-input-line\">\n <component :is=\"promptView\" />\n <input\n :key=\"state.input.revision\"\n ref=\"input\"\n class=\"cc-input\"\n type=\"text\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n :aria-expanded=\"session ? 'true' : 'false'\"\n :aria-controls=\"session ? listId : undefined\"\n :aria-activedescendant=\"activeId\"\n :aria-label=\"labels.input\"\n :aria-description=\"labels.inputDescription\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n :readonly=\"running !== null\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @keyup=\"onCaret\"\n @click=\"onCaret\"\n @selectionchange=\"onCaret\"\n />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\n// <CommandConsole> (README: The component): the composable's console drawn as\n// the DOM of `contract/dom.ts` (README: DOM and styling). The component holds\n// no console state (P15): it renders the snapshot and forwards DOM events to\n// core actions. What it keeps is DOM-bound: element refs, the \"stuck to\n// bottom\" flag, the last value it emitted per controlled slice and the caret\n// it last reported.\nimport {\n computed,\n onMounted,\n toRef,\n useId,\n useTemplateRef,\n watch,\n type FunctionalComponent\n} from 'vue'\n\nimport {\n completionFeature,\n consoleFeatures,\n contextFeature,\n recallFeature,\n type OptionalFeatures,\n type Updater\n} from '@dolusoft/command-console-core'\nimport type { ConsoleEntry, ContextFrame } from '@dolusoft/command-protocol'\n\nimport type {\n ConsoleEmits,\n ConsoleProps,\n ConsoleSlots,\n ConsoleSubmit\n} from './contract'\nimport { clickFocusesInput } from './dom/focus'\nimport { keyInputOf } from './dom/keys'\nimport { useStickToBottom } from './dom/scroll'\nimport { resolveLabels } from './labels'\nimport { renderCompletions, optionId } from './parts/completions-view'\nimport { renderEntry, renderPrompt } from './parts/entry-view'\nimport { useCommandConsole } from './use-command-console'\n\ndefineOptions({ name: 'CommandConsole' })\n\nconst props = withDefaults(defineProps<ConsoleProps>(), {\n prompt: undefined,\n initialState: undefined,\n createId: undefined,\n maxEntries: undefined,\n beforeRun: undefined,\n onError: undefined,\n formatError: undefined,\n isSensitive: undefined,\n recallLimit: undefined,\n shouldRecord: undefined,\n labels: undefined,\n autofocus: false,\n entries: undefined,\n recall: undefined,\n context: undefined\n})\n\nconst emit = defineEmits<ConsoleEmits>()\n\nconst slots = defineSlots<ConsoleSlots>()\n\n// The component always uses every optional feature (README: The component).\nconst features: OptionalFeatures = consoleFeatures({\n recallFeature,\n completionFeature,\n contextFeature\n})\n\n// --- v-model (core README: Controlled slices, C-61) -------------------------\n// An updater is resolved against the value this component last emitted for\n// the slice, else the prop: a second action in the same tick builds on the\n// first one's value, not on a prop the parent has not re-rendered yet. A new\n// prop value clears it.\ntype Slice = 'entries' | 'recall' | 'context'\ninterface SliceValues {\n entries: ConsoleEntry[]\n recall: string[]\n context: ContextFrame[]\n}\nconst lastEmitted: Partial<SliceValues> = {}\nfor (const slice of ['entries', 'recall', 'context'] as const) {\n watch(\n () => props[slice],\n () => {\n delete lastEmitted[slice]\n },\n { flush: 'sync' }\n )\n}\nconst report =\n <K extends Slice>(slice: K, send: (value: SliceValues[K]) => void) =>\n (updater: Updater<SliceValues[K]>) => {\n const base = (lastEmitted[slice] ?? props[slice] ?? []) as SliceValues[K]\n const value = typeof updater === 'function' ? updater(base) : updater\n lastEmitted[slice] = value\n send(value)\n }\n\n/** The slices bound with `v-model`; the others are the console's. */\nconst controlled = () => {\n const state: Partial<SliceValues> = {}\n if (props.entries !== undefined) {\n state.entries = props.entries\n }\n if (props.recall !== undefined) {\n state.recall = props.recall\n }\n if (props.context !== undefined) {\n state.context = props.context\n }\n return state\n}\n\nconst { console, state } = useCommandConsole({\n features,\n commands: () => props.commands,\n prompt: () => props.prompt,\n state: controlled,\n maxEntries: () => props.maxEntries,\n recallLimit: () => props.recallLimit,\n initialState: props.initialState,\n createId: toRef(props, 'createId'),\n beforeRun: toRef(props, 'beforeRun'),\n onError: toRef(props, 'onError'),\n formatError: toRef(props, 'formatError'),\n isSensitive: toRef(props, 'isSensitive'),\n shouldRecord: toRef(props, 'shouldRecord'),\n onEntriesChange: report('entries', value => emit('update:entries', value)),\n onRecallChange: report('recall', value => emit('update:recall', value)),\n onContextChange: report('context', value => emit('update:context', value)),\n onClose: () => emit('close')\n})\n\n// --- What the template draws ------------------------------------------------\n\nconst labels = computed(() => resolveLabels(props.labels))\nconst listId = useId()\n\nconst running = computed(() => state.value.running)\nconst session = computed(() => {\n const completion = state.value.completion\n return completion?.status === 'open' ? completion : null\n})\nconst activeId = computed(() =>\n session.value && session.value.active >= 0\n ? optionId(listId, session.value.active)\n : undefined\n)\n/** The prompt of the live line: the top context frame's, or the root's. */\nconst prompt = computed(() => {\n void state.value.context\n void props.prompt\n return console.getPrompt()\n})\n/**\n * The recalled line, for the status element (README: Recall): a screen reader\n * does not announce a scripted value change in a focused field.\n */\nconst recalled = computed(() => {\n const { recall, recallCursor } = state.value\n return recallCursor !== null ? (recall[recallCursor] ?? '') : ''\n})\n\nconst entriesView: FunctionalComponent = () =>\n state.value.entries.map(entry => renderEntry(entry, labels.value, slots))\nconst completionsView: FunctionalComponent = () =>\n session.value\n ? renderCompletions(session.value, listId, labels.value.completions, slots)\n : null\nconst promptView: FunctionalComponent = () => renderPrompt(prompt.value, slots)\n\n// --- Elements ---------------------------------------------------------------\n\nconst rootElement = useTemplateRef<HTMLDivElement>('root')\nconst logElement = useTemplateRef<HTMLDivElement>('log')\nconst entriesElement = useTemplateRef<HTMLDivElement>('entries')\nconst inputElement = useTemplateRef<HTMLInputElement>('input')\n\nconst scroll = useStickToBottom(logElement, entriesElement)\n\nconst focusInput = () => inputElement.value?.focus()\n\nonMounted(() => {\n if (props.autofocus) {\n focusInput()\n }\n})\n\n// C-51: a click in the log focuses the input, unless text is selected or\n// the click was on something interactive.\nconst onLogClick = (event: MouseEvent) => {\n if (logElement.value && clickFocusesInput(event.target, logElement.value)) {\n focusInput()\n }\n}\n\n// --- The input element (README: DOM and styling, C-54) ----------------------\n// The element is keyed by `input.revision`. When it is replaced, the new\n// one gets focus only if the old one had it, and the caret goes to the end.\n// Its value is written as a DOM property here, never bound in the template:\n// Vue mirrors a bound `value` into the `value` attribute, which would put\n// the typed line into the DOM.\n\nlet hadFocus = false\nwatch(\n () => state.value.input.revision,\n () => {\n const element = inputElement.value\n hadFocus =\n element !== null && element.ownerDocument.activeElement === element\n // The element about to go may hold a sensitive line (C-36).\n if (element) {\n element.value = ''\n }\n },\n { flush: 'pre' }\n)\n\n/** The value and caret this component last reported (a user edit). */\nlet reported = { value: '', caret: 0 }\n\n/**\n * Writes the console's value into the element. A write by the console\n * (completion, recall) also sets the caret it chose; the user's own edit is\n * left alone, so a selection the user made stays.\n */\nconst syncElement = (replaced: boolean) => {\n const element = inputElement.value\n if (!element) {\n return\n }\n const { value, caret } = state.value.input\n if (element.value !== value) {\n element.value = value\n }\n if (replaced) {\n if (hadFocus) {\n element.focus()\n }\n hadFocus = false\n element.setSelectionRange(value.length, value.length)\n return\n }\n if (\n element.ownerDocument.activeElement !== element ||\n (value === reported.value && caret === reported.caret)\n ) {\n return\n }\n if (element.selectionStart !== caret || element.selectionEnd !== caret) {\n element.setSelectionRange(caret, caret)\n }\n}\nonMounted(() => {\n syncElement(false)\n})\nwatch(\n () => state.value.input,\n (next, previous) => {\n syncElement(next.revision !== previous.revision)\n },\n { flush: 'post' }\n)\n\n/** Reports the element's value and caret to the console (a user edit). */\nconst reportEdit = (element: HTMLInputElement) => {\n const caret = element.selectionStart ?? element.value.length\n reported = { value: element.value, caret }\n console.editInput(element.value, caret)\n}\n\nconst onInput = (event: Event) => {\n reportEdit(event.currentTarget as HTMLInputElement)\n}\n\n// A caret move the user made (click, arrow keys, selection).\nconst onCaret = (event: Event) => {\n const element = event.currentTarget as HTMLInputElement\n const caret = element.selectionStart ?? element.value.length\n if (element.value !== state.value.input.value) {\n return\n }\n if (caret !== state.value.input.caret) {\n reportEdit(element)\n }\n}\n\n// --- Submits (C-64) ---------------------------------------------------------\n\n/**\n * Runs `action` and emits `submit` for the input entry it added, if any;\n * keeps the log at the bottom after the user's own submit (C-58).\n */\nconst submitting = <T,>(action: () => T): T => {\n const before = new Set(state.value.entries.map(entry => entry.id))\n const result = action()\n const added = console\n .getState()\n .entries.find(\n entry =>\n entry.kind === 'input' &&\n entry.status === 'submitted' &&\n !before.has(entry.id)\n )\n if (added) {\n scroll.stick()\n const submit: ConsoleSubmit = {\n inputId: added.id,\n sensitive: added.kind === 'input' && added.redacted\n }\n emit('submit', submit)\n }\n return result\n}\n\nconst onKeydown = (event: KeyboardEvent) => {\n const element = event.currentTarget as HTMLInputElement\n const key = keyInputOf(event, element, rootElement.value)\n if (!key.isComposing) {\n // The caret may have moved without an event the console saw.\n onCaret(event)\n }\n const handled = submitting(() => console.handleKey(key))\n if (handled) {\n event.preventDefault()\n }\n}\n\n// --- Expose ------------------------------------------------------------------\n\nconst exposed = {\n focus: () => {\n focusInput()\n },\n run: (line: string) => submitting(() => console.run(line)),\n cancel: () => {\n console.cancel()\n },\n reset: () => {\n console.reset()\n },\n clear: () => {\n console.clearEntries()\n },\n console,\n state\n}\ndefineExpose(exposed)\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"cc-console\"\n :data-busy=\"running ? '' : undefined\"\n :data-context-depth=\"state.context.length\"\n >\n <div\n ref=\"log\"\n class=\"cc-log\"\n role=\"log\"\n :aria-label=\"labels.log\"\n @click=\"onLogClick\"\n @scroll=\"scroll.onScroll\"\n >\n <div ref=\"entries\" class=\"cc-entries\">\n <component :is=\"entriesView\" />\n </div>\n </div>\n <div class=\"cc-status\" role=\"status\">\n <template v-if=\"running\"\n ><slot name=\"busy\" :running=\"running\">{{ labels.busy }}</slot></template\n ><template v-else>{{ recalled }}</template>\n </div>\n <component :is=\"completionsView\" />\n <div class=\"cc-input-line\">\n <component :is=\"promptView\" />\n <input\n :key=\"state.input.revision\"\n ref=\"input\"\n class=\"cc-input\"\n type=\"text\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n :aria-expanded=\"session ? 'true' : 'false'\"\n :aria-controls=\"session ? listId : undefined\"\n :aria-activedescendant=\"activeId\"\n :aria-label=\"labels.input\"\n :aria-description=\"labels.inputDescription\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n :readonly=\"running !== null\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @keyup=\"onCaret\"\n @click=\"onCaret\"\n @selectionchange=\"onCaret\"\n />\n </div>\n </div>\n</template>\n","// @dolusoft/command-console: a headless command console for Vue 3.\n// `useCommandConsole` is the console without markup; `CommandConsole` draws\n// it as the DOM of `contract/dom.ts`. No CSS (P5).\nimport CommandConsole from './command-console.vue'\n\nexport { CommandConsole }\nexport default CommandConsole\n\nexport { useCommandConsole } from './use-command-console'\n\nexport type * from './contract'\n"],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import { type OptionalFeatures } from '@dolusoft/command-console-core';
2
+ import type { ConsoleEntry, ContextFrame } from '@dolusoft/command-protocol';
3
+ import type { ConsoleProps, ConsoleSlots, ConsoleSubmit } from './contract.js';
4
+ type __VLS_Slots = ConsoleSlots;
5
+ declare const __VLS_base: import("vue").DefineComponent<ConsoleProps, {
6
+ focus: () => void;
7
+ run: (line: string) => Promise<boolean>;
8
+ cancel: () => void;
9
+ reset: () => void;
10
+ clear: () => void;
11
+ console: import("@dolusoft/command-console-core").Console<OptionalFeatures>;
12
+ state: Readonly<import("vue").ShallowRef<import("@dolusoft/command-console-core").ConsoleState>>;
13
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
+ close: () => any;
15
+ submit: (submit: ConsoleSubmit) => any;
16
+ "update:entries": (entries: ConsoleEntry[]) => any;
17
+ "update:recall": (recall: string[]) => any;
18
+ "update:context": (context: ContextFrame[]) => any;
19
+ }, string, import("vue").PublicProps, Readonly<ConsoleProps> & Readonly<{
20
+ onClose?: (() => any) | undefined;
21
+ onSubmit?: ((submit: ConsoleSubmit) => any) | undefined;
22
+ "onUpdate:entries"?: ((entries: ConsoleEntry[]) => any) | undefined;
23
+ "onUpdate:recall"?: ((recall: string[]) => any) | undefined;
24
+ "onUpdate:context"?: ((context: ContextFrame[]) => any) | undefined;
25
+ }>, {
26
+ prompt: string;
27
+ maxEntries: number;
28
+ recallLimit: number;
29
+ initialState: Partial<import("@dolusoft/command-console-core").ControllableState>;
30
+ createId: () => string;
31
+ beforeRun: (info: import("@dolusoft/command-console-core").RunInfo, signal: AbortSignal) => import("@dolusoft/command-console-core").BeforeRunResult | Promise<import("@dolusoft/command-console-core").BeforeRunResult>;
32
+ onError: (error: unknown, info: import("@dolusoft/command-console-core").RunInfo | null) => void;
33
+ formatError: (error: unknown, info: import("@dolusoft/command-console-core").RunInfo) => import("@dolusoft/command-protocol").CommandResponse;
34
+ isSensitive: (line: string) => boolean;
35
+ shouldRecord: (line: string) => boolean;
36
+ entries: ConsoleEntry[];
37
+ recall: string[];
38
+ context: ContextFrame[];
39
+ labels: import("./contract.js").ConsoleLabelsInput;
40
+ autofocus: boolean;
41
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
42
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
43
+ declare const _default: typeof __VLS_export;
44
+ export default _default;
45
+ type __VLS_WithSlots<T, S> = T & {
46
+ new (): {
47
+ $slots: S;
48
+ };
49
+ };
@@ -0,0 +1,183 @@
1
+ import type { MaybeRef, MaybeRefOrGetter, ShallowRef } from 'vue';
2
+ import type { BeforeRunResult, CommandDefinition, Console, ConsoleOptions, ConsoleState, ControllableState, OptionalFeatures, RunInfo, RunningCommand } from '@dolusoft/command-console-core';
3
+ import type { CommandResponse, ConsoleEntry, ContextFrame, CustomBlock, JsonBlock, ListBlock, MessageBlock, MessageCode, MessageParams, OutputBlock, OutputEntry, TableBlock, TextBlock } from '@dolusoft/command-protocol';
4
+ export type { BeforeRunResult, CommandDefinition, CommandRequest, CommandReturn, CompletionRequest, CompletionResult, CompletionSession, Console, ConsoleOptions, ConsoleState, ControllableState, InputState, KeyInput, OptionalFeatures, RunContext, RunInfo, RunningCommand, Updater } from '@dolusoft/command-console-core';
5
+ export type { CommandResponse, CommandSpec, ConsoleEffect, ConsoleEntry, ContextFrame, CustomBlock, InputEntry, JsonBlock, JsonPrimitive, JsonValue, ListBlock, MessageBlock, MessageCode, MessageParams, OutputBlock, OutputEntry, TableBlock, TableColumn, TextBlock } from '@dolusoft/command-protocol';
6
+ /** The text of one message code: a string, or a function of its parameters. */
7
+ export type MessageLabels = {
8
+ [C in MessageCode]: string | ((params: MessageParams[C]) => string);
9
+ };
10
+ /** Every text the component renders (C-47). */
11
+ export interface ConsoleLabels {
12
+ /** The input's accessible name. Default `'Command'`. */
13
+ input: string;
14
+ /**
15
+ * The input's description (`aria-description`): how Enter and Tab behave,
16
+ * and that Tab on an empty line leaves the console (C-59).
17
+ */
18
+ inputDescription: string;
19
+ /** The log's accessible name. Default `'Console output'`. */
20
+ log: string;
21
+ /** Shown instead of a sensitive line. Default `'(input hidden)'`. */
22
+ redacted: string;
23
+ /** Shown after a line ended with Ctrl+C. Default `'^C'`. */
24
+ cancelledMarker: string;
25
+ /** The completion list's accessible name. Default `'Suggestions'`. */
26
+ completions: string;
27
+ /** The status text while a command runs. Default `'Running…'`. */
28
+ busy: string;
29
+ /** The text of each library-made message block. */
30
+ messages: MessageLabels;
31
+ }
32
+ /**
33
+ * The `labels` prop: any part of `ConsoleLabels`. `messages` is merged one
34
+ * level deep, so overriding one message keeps the others.
35
+ */
36
+ export type ConsoleLabelsInput = Partial<Omit<ConsoleLabels, 'messages'>> & {
37
+ messages?: Partial<MessageLabels>;
38
+ };
39
+ /** The props of `<CommandConsole>`. */
40
+ export interface ConsoleProps {
41
+ /** The commands; validated on every change (C-56). */
42
+ commands: CommandDefinition[];
43
+ /** The root prompt. Default `'>'`. */
44
+ prompt?: string;
45
+ /** The first value of each uncontrolled slice; read once. */
46
+ initialState?: Partial<ControllableState>;
47
+ /** Makes entry and run ids. Default: a per-console counter. */
48
+ createId?: () => string;
49
+ /** Drops the oldest entries beyond this. Default unbounded. */
50
+ maxEntries?: number;
51
+ /** Called when a command is about to run (permission checks, C-14). */
52
+ beforeRun?: (info: RunInfo, signal: AbortSignal) => BeforeRunResult | Promise<BeforeRunResult>;
53
+ /**
54
+ * A command threw (`info` set) or a callback threw (`info` null). Receives
55
+ * the error exactly as it was thrown.
56
+ */
57
+ onError?: (error: unknown, info: RunInfo | null) => void;
58
+ /** Turns a command's error into its output. Default `command-failed`. */
59
+ formatError?: (error: unknown, info: RunInfo) => CommandResponse;
60
+ /** Marks more lines sensitive, OR-ed with the commands' `sensitive`. */
61
+ isSensitive?: (line: string) => boolean;
62
+ /** The most lines recall keeps. Default 100. */
63
+ recallLimit?: number;
64
+ /** Whether a submitted line is recorded for recall. Default every line. */
65
+ shouldRecord?: (line: string) => boolean;
66
+ /** Texts over the English defaults (C-47). */
67
+ labels?: ConsoleLabelsInput;
68
+ /** Focuses the input on mount (C-51). Default `false`. */
69
+ autofocus?: boolean;
70
+ /** The entries, for `v-model:entries`; controlled when bound (C-04). */
71
+ entries?: ConsoleEntry[];
72
+ /** The recall list, for `v-model:recall`; controlled when bound. */
73
+ recall?: string[];
74
+ /** The context frames, for `v-model:context`; controlled when bound. */
75
+ context?: ContextFrame[];
76
+ }
77
+ /** What the `submit` event carries: never the line (C-64). */
78
+ export interface ConsoleSubmit {
79
+ /** The id of the input entry the line ended as. */
80
+ inputId: string;
81
+ /** Whether the line was sensitive and its entry redacted. */
82
+ sensitive: boolean;
83
+ }
84
+ /** The events of `<CommandConsole>`. */
85
+ export interface ConsoleEmits {
86
+ /**
87
+ * The new entries of a bound `v-model:entries`, resolved against the last
88
+ * emitted value (C-61).
89
+ */
90
+ 'update:entries': [entries: ConsoleEntry[]];
91
+ /** The new recall list of a bound `v-model:recall`. */
92
+ 'update:recall': [recall: string[]];
93
+ /** The new context frames of a bound `v-model:context`. */
94
+ 'update:context': [context: ContextFrame[]];
95
+ /** A command returned the `close` effect (C-25). */
96
+ close: [];
97
+ /** A line was submitted with Enter or the exposed `run` (C-64). */
98
+ submit: [submit: ConsoleSubmit];
99
+ }
100
+ /** What a `block-<type>` slot receives. */
101
+ export interface BlockSlotProps<B extends OutputBlock = OutputBlock> {
102
+ block: B;
103
+ entry: OutputEntry;
104
+ }
105
+ /** What the `completion` slot receives. */
106
+ export interface CompletionSlotProps {
107
+ candidate: string;
108
+ index: number;
109
+ /** Whether this candidate is the selected one. */
110
+ active: boolean;
111
+ }
112
+ /** The slots of `<CommandConsole>` (C-67). */
113
+ export interface ConsoleSlots {
114
+ /** Replaces the rendering of a text block. */
115
+ 'block-text'?(props: BlockSlotProps<TextBlock>): unknown;
116
+ /** Replaces the rendering of a JSON block (for example to highlight it). */
117
+ 'block-json'?(props: BlockSlotProps<JsonBlock>): unknown;
118
+ /** Replaces the rendering of a table block. */
119
+ 'block-table'?(props: BlockSlotProps<TableBlock>): unknown;
120
+ /** Replaces the rendering of a list block. */
121
+ 'block-list'?(props: BlockSlotProps<ListBlock>): unknown;
122
+ /** Replaces the rendering of a message block. */
123
+ 'block-message'?(props: BlockSlotProps<MessageBlock>): unknown;
124
+ /** Renders a custom block; without it the block shows as JSON. */
125
+ 'block-custom'?(props: BlockSlotProps<CustomBlock>): unknown;
126
+ /** Inside each `span.cc-prompt`. */
127
+ prompt?(props: {
128
+ prompt: string;
129
+ }): unknown;
130
+ /** Inside `.cc-status` while a command runs. */
131
+ busy?(props: {
132
+ running: RunningCommand;
133
+ }): unknown;
134
+ /** Inside each `li.cc-completion`. */
135
+ completion?(props: CompletionSlotProps): unknown;
136
+ }
137
+ /** What a template ref to `<CommandConsole>` reaches. */
138
+ export interface ConsoleExpose {
139
+ /** Focuses the input. */
140
+ focus(): void;
141
+ /**
142
+ * Replaces the input with `line` and submits it, as the user would
143
+ * (C-63); keeps the log at the bottom.
144
+ */
145
+ run(line: string): Promise<boolean>;
146
+ /** Interrupts the running command, else cancels the line. */
147
+ cancel(): void;
148
+ /** Ends the run and returns every slice to its initial value. */
149
+ reset(): void;
150
+ /** Removes every entry. */
151
+ clear(): void;
152
+ /** The console, with every optional feature. */
153
+ console: Console<OptionalFeatures>;
154
+ /**
155
+ * The console's state (for example `running`, to key an overlay on). A
156
+ * template ref unwraps it: reading it in a computed or a template tracks
157
+ * it.
158
+ */
159
+ state: ConsoleState;
160
+ }
161
+ /** The value options of `useCommandConsole`: a value, a ref or a getter. */
162
+ type ValueOptions<T extends Partial<OptionalFeatures>> = {
163
+ [K in 'prompt' | 'state' | 'maxEntries' | 'recallLimit']?: MaybeRefOrGetter<ConsoleOptions<T>[K]>;
164
+ };
165
+ /** The function options of `useCommandConsole`: a function or a ref to one. */
166
+ type FunctionOptions<T extends Partial<OptionalFeatures>> = {
167
+ [K in 'onEntriesChange' | 'onRecallChange' | 'onContextChange' | 'createId' | 'beforeRun' | 'onError' | 'formatError' | 'onClose' | 'isSensitive' | 'shouldRecord']?: MaybeRef<ConsoleOptions<T>[K]>;
168
+ };
169
+ /** The options of `useCommandConsole`: the core options, reactive. */
170
+ export type UseCommandConsoleOptions<T extends Partial<OptionalFeatures> = {}> = ValueOptions<T> & FunctionOptions<T> & {
171
+ /** The optional features; fixed for the life of the console. */
172
+ features?: T;
173
+ commands: MaybeRefOrGetter<CommandDefinition[]>;
174
+ /** Read once, when the console is created. */
175
+ initialState?: MaybeRefOrGetter<Partial<ControllableState> | undefined>;
176
+ };
177
+ /** What `useCommandConsole` returns. */
178
+ export interface UseCommandConsoleReturn<T extends Partial<OptionalFeatures> = {}> {
179
+ /** The console; disposed with the effect scope. */
180
+ console: Console<T>;
181
+ /** The state after every action (C-68). */
182
+ state: Readonly<ShallowRef<ConsoleState>>;
183
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Whether a click on `target` inside `log` should focus the input: not when
3
+ * text is selected, not inside an interactive element.
4
+ */
5
+ export declare const clickFocusesInput: (target: EventTarget | null, log: Element) => boolean;
@@ -0,0 +1,3 @@
1
+ import type { KeyInput } from '@dolusoft/command-console-core';
2
+ /** The key press `event` on `input`, inside the console `root`. */
3
+ export declare const keyInputOf: (event: KeyboardEvent, input: HTMLInputElement, root: Element | null) => KeyInput;
@@ -0,0 +1,8 @@
1
+ import { type ShallowRef } from 'vue';
2
+ export interface StickToBottom {
3
+ /** The user's own submit: stick again and show the bottom now. */
4
+ stick(): void;
5
+ /** The log's `scroll` event. */
6
+ onScroll(): void;
7
+ }
8
+ export declare const useStickToBottom: (log: Readonly<ShallowRef<HTMLElement | null>>, entries: Readonly<ShallowRef<HTMLElement | null>>) => StickToBottom;
@@ -0,0 +1,3 @@
1
+ import type { KeyInput } from '@dolusoft/command-console-core';
2
+ /** Where text is selected, as `KeyInput.selection` reports it. */
3
+ export declare const selectionOf: (input: HTMLInputElement, root: Element | null) => KeyInput["selection"];
@@ -0,0 +1,5 @@
1
+ import CommandConsole from './command-console.vue.js';
2
+ export { CommandConsole };
3
+ export default CommandConsole;
4
+ export { useCommandConsole } from './use-command-console.js';
5
+ export type * from './contract.js';
@@ -0,0 +1,9 @@
1
+ import type { MessageBlock } from '@dolusoft/command-protocol';
2
+ import type { ConsoleLabels, ConsoleLabelsInput } from './contract.js';
3
+ /**
4
+ * The defaults with `labels` over them; `messages` merges one level deep, so
5
+ * overriding one message keeps the others.
6
+ */
7
+ export declare const resolveLabels: (labels?: ConsoleLabelsInput) => ConsoleLabels;
8
+ /** The text of a message block, from `labels.messages`. */
9
+ export declare const messageText: (labels: ConsoleLabels, block: MessageBlock) => string;
@@ -0,0 +1,7 @@
1
+ import { type VNode } from 'vue';
2
+ import type { OutputBlock, OutputEntry } from '@dolusoft/command-protocol';
3
+ import type { ConsoleLabels, ConsoleSlots } from '../contract.js';
4
+ /** The `block-<type>` slots the component was given. */
5
+ export type BlockSlots = Pick<ConsoleSlots, 'block-text' | 'block-json' | 'block-table' | 'block-list' | 'block-message' | 'block-custom'>;
6
+ /** Renders `block` of `entry`, through its slot when one is given. */
7
+ export declare const renderBlock: (block: OutputBlock, entry: OutputEntry, labels: ConsoleLabels, slots: BlockSlots) => VNode;
@@ -0,0 +1,6 @@
1
+ import { type VNode } from 'vue';
2
+ import type { CompletionSession } from '@dolusoft/command-console-core';
3
+ import type { ConsoleSlots } from '../contract.js';
4
+ /** The id of candidate `index` of the list `listId`. */
5
+ export declare const optionId: (listId: string, index: number) => string;
6
+ export declare const renderCompletions: (session: CompletionSession, listId: string, label: string, slots: Pick<ConsoleSlots, "completion">) => VNode;
@@ -0,0 +1,9 @@
1
+ import { type VNode } from 'vue';
2
+ import type { ConsoleEntry } from '@dolusoft/command-protocol';
3
+ import { type BlockSlots } from './block-view.js';
4
+ import type { ConsoleLabels, ConsoleSlots } from '../contract.js';
5
+ /** The slots an entry may use. */
6
+ export type EntrySlots = BlockSlots & Pick<ConsoleSlots, 'prompt'>;
7
+ /** `span.cc-prompt` with the `prompt` slot inside, or the prompt text. */
8
+ export declare const renderPrompt: (prompt: string, slots: Pick<ConsoleSlots, "prompt">) => VNode;
9
+ export declare const renderEntry: (entry: ConsoleEntry, labels: ConsoleLabels, slots: EntrySlots) => VNode;
@@ -0,0 +1,20 @@
1
+ import { type OptionalFeatures } from '@dolusoft/command-console-core';
2
+ import type { UseCommandConsoleOptions, UseCommandConsoleReturn } from './contract.js';
3
+ /**
4
+ * Creates a console from `options` and keeps it in step with them.
5
+ *
6
+ * A value option (`commands`, `prompt`, `state`, `maxEntries`,
7
+ * `recallLimit`) may be a plain value, a ref or a getter. A function option
8
+ * (`beforeRun`, `onError`, …) may be the function or a ref that holds it: a
9
+ * bare function is the option itself, never a getter. `features` and
10
+ * `initialState` are read once.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const { console, state } = useCommandConsole({
15
+ * features: consoleFeatures({ recallFeature }),
16
+ * commands: () => commands.value
17
+ * })
18
+ * ```
19
+ */
20
+ export declare function useCommandConsole<T extends Partial<OptionalFeatures> = {}>(options: UseCommandConsoleOptions<T>): UseCommandConsoleReturn<T>;
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@dolusoft/command-console",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "A headless command console for Vue 3: a scrollback above one input line, with recall, Tab completion, a context stack and redaction of sensitive lines. Ships no CSS.",
5
+ "private": false,
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "LICENSE.md"
11
+ ],
12
+ "sideEffects": false,
13
+ "main": "./dist/command-console.js",
14
+ "types": "./dist/types/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/types/index.d.ts",
18
+ "default": "./dist/command-console.js"
19
+ }
20
+ },
21
+ "engines": {
22
+ "node": ">=22.18"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/dolusoft/command-console.git",
27
+ "directory": "packages/command-console"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/dolusoft/command-console/issues"
31
+ },
32
+ "homepage": "https://github.com/dolusoft/command-console#readme",
33
+ "keywords": [
34
+ "vue",
35
+ "vue3",
36
+ "console",
37
+ "terminal",
38
+ "command-line",
39
+ "headless"
40
+ ],
41
+ "license": "MIT",
42
+ "dependencies": {
43
+ "@dolusoft/command-console-core": "1.0.0",
44
+ "@dolusoft/command-protocol": "1.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "vue": "^3.5.43"
48
+ },
49
+ "peerDependencies": {
50
+ "vue": "^3.5.0"
51
+ },
52
+ "scripts": {
53
+ "build": "vite build && vue-tsc -p tsconfig.build.json && node ../../scripts/emit-type-extensions.mjs dist/types"
54
+ }
6
55
  }