@dolusoft/command-console 0.0.0-stage → 1.0.1
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 +21 -0
- package/README.md +114 -2
- package/dist/command-console.js +560 -0
- package/dist/command-console.js.map +1 -0
- package/dist/types/command-console.vue.d.ts +49 -0
- package/dist/types/contract.d.ts +183 -0
- package/dist/types/dom/focus.d.ts +6 -0
- package/dist/types/dom/keys.d.ts +3 -0
- package/dist/types/dom/scroll.d.ts +8 -0
- package/dist/types/dom/selection.d.ts +3 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/labels.d.ts +9 -0
- package/dist/types/parts/block-view.d.ts +7 -0
- package/dist/types/parts/completions-view.d.ts +6 -0
- package/dist/types/parts/entry-view.d.ts +9 -0
- package/dist/types/use-command-console.d.ts +20 -0
- package/package.json +52 -3
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
|
-
#
|
|
1
|
+
# @dolusoft/command-console
|
|
2
2
|
|
|
3
|
-
|
|
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,560 @@
|
|
|
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 `root` should focus the input: not when
|
|
7
|
+
* text is selected, not inside an interactive element (the input included:
|
|
8
|
+
* a click on it is the browser's).
|
|
9
|
+
*/
|
|
10
|
+
var clickFocusesInput = (target, root) => {
|
|
11
|
+
const selection = root.ownerDocument.getSelection();
|
|
12
|
+
if (selection && !selection.isCollapsed && selection.toString() !== "") return false;
|
|
13
|
+
const found = (target instanceof Element ? target : target instanceof Node ? target.parentElement : null)?.closest(interactive);
|
|
14
|
+
return !(found && root.contains(found));
|
|
15
|
+
};
|
|
16
|
+
// src/dom/selection.ts
|
|
17
|
+
/** Whether the document has a non-empty selection inside `root`. */
|
|
18
|
+
var hasSelectionIn = (root) => {
|
|
19
|
+
const selection = root?.ownerDocument.getSelection();
|
|
20
|
+
if (!root || !selection || selection.isCollapsed) return false;
|
|
21
|
+
if (selection.toString() === "") return false;
|
|
22
|
+
const { anchorNode, focusNode } = selection;
|
|
23
|
+
return anchorNode !== null && root.contains(anchorNode) || focusNode !== null && root.contains(focusNode);
|
|
24
|
+
};
|
|
25
|
+
/** Where text is selected, as `KeyInput.selection` reports it. */
|
|
26
|
+
var selectionOf = (input, root) => {
|
|
27
|
+
if (input.selectionStart !== null && input.selectionStart !== input.selectionEnd) return "input";
|
|
28
|
+
return hasSelectionIn(root) ? "document" : "none";
|
|
29
|
+
};
|
|
30
|
+
// src/dom/keys.ts
|
|
31
|
+
/** The key press `event` on `input`, inside the console `root`. */
|
|
32
|
+
var keyInputOf = (event, input, root) => ({
|
|
33
|
+
key: event.key,
|
|
34
|
+
ctrlKey: event.ctrlKey,
|
|
35
|
+
shiftKey: event.shiftKey,
|
|
36
|
+
altKey: event.altKey,
|
|
37
|
+
metaKey: event.metaKey,
|
|
38
|
+
isComposing: event.isComposing || event.keyCode === 229,
|
|
39
|
+
selection: selectionOf(input, root)
|
|
40
|
+
});
|
|
41
|
+
// src/dom/scroll.ts
|
|
42
|
+
/** How close to the bottom still counts as at the bottom, in px. */
|
|
43
|
+
var slack = 2;
|
|
44
|
+
var useStickToBottom = (log, entries) => {
|
|
45
|
+
let stuck = true;
|
|
46
|
+
let observer = null;
|
|
47
|
+
let mounted = false;
|
|
48
|
+
const toBottom = () => {
|
|
49
|
+
const element = log.value;
|
|
50
|
+
if (mounted && element) element.scrollTop = element.scrollHeight;
|
|
51
|
+
};
|
|
52
|
+
onMounted(() => {
|
|
53
|
+
mounted = true;
|
|
54
|
+
if (typeof ResizeObserver !== "undefined" && entries.value) {
|
|
55
|
+
observer = new ResizeObserver(() => {
|
|
56
|
+
if (stuck) toBottom();
|
|
57
|
+
});
|
|
58
|
+
observer.observe(entries.value);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
onBeforeUnmount(() => {
|
|
62
|
+
mounted = false;
|
|
63
|
+
observer?.disconnect();
|
|
64
|
+
observer = null;
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
stick() {
|
|
68
|
+
stuck = true;
|
|
69
|
+
nextTick(toBottom);
|
|
70
|
+
},
|
|
71
|
+
onScroll() {
|
|
72
|
+
const element = log.value;
|
|
73
|
+
if (element) stuck = element.scrollHeight - element.scrollTop - element.clientHeight <= slack;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
// src/labels.ts
|
|
78
|
+
/** The English defaults (README: Labels). */
|
|
79
|
+
var defaultLabels = {
|
|
80
|
+
input: "Command",
|
|
81
|
+
inputDescription: "Enter runs the command. Tab completes; on an empty line it leaves the console.",
|
|
82
|
+
log: "Console output",
|
|
83
|
+
redacted: "(input hidden)",
|
|
84
|
+
cancelledMarker: "^C",
|
|
85
|
+
completions: "Suggestions",
|
|
86
|
+
busy: "Running…",
|
|
87
|
+
messages: {
|
|
88
|
+
"command-not-found": (params) => params.name === null ? "command not found" : `${params.name}: command not found`,
|
|
89
|
+
"command-failed": "The command failed.",
|
|
90
|
+
"unterminated-quote": "Unterminated quote.",
|
|
91
|
+
interrupted: "Interrupted."
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* The defaults with `labels` over them; `messages` merges one level deep, so
|
|
96
|
+
* overriding one message keeps the others.
|
|
97
|
+
*/
|
|
98
|
+
var resolveLabels = (labels) => ({
|
|
99
|
+
...defaultLabels,
|
|
100
|
+
...labels,
|
|
101
|
+
messages: {
|
|
102
|
+
...defaultLabels.messages,
|
|
103
|
+
...labels?.messages
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
/** The text of a message block, from `labels.messages`. */
|
|
107
|
+
var messageText = (labels, block) => {
|
|
108
|
+
const label = labels.messages[block.code];
|
|
109
|
+
if (typeof label === "string") return label;
|
|
110
|
+
return label(block.params ?? {});
|
|
111
|
+
};
|
|
112
|
+
// src/parts/completions-view.ts
|
|
113
|
+
/** The id of candidate `index` of the list `listId`. */
|
|
114
|
+
var optionId = (listId, index) => `${listId}-${index}`;
|
|
115
|
+
var renderCompletions = (session, listId, label, slots) => h("ul", {
|
|
116
|
+
class: "cc-completions",
|
|
117
|
+
role: "listbox",
|
|
118
|
+
id: listId,
|
|
119
|
+
"aria-label": label
|
|
120
|
+
}, session.candidates.map((candidate, index) => {
|
|
121
|
+
const active = index === session.active;
|
|
122
|
+
return h("li", {
|
|
123
|
+
key: `${index}:${candidate}`,
|
|
124
|
+
class: "cc-completion",
|
|
125
|
+
role: "option",
|
|
126
|
+
id: optionId(listId, index),
|
|
127
|
+
"aria-selected": active ? "true" : "false"
|
|
128
|
+
}, slots.completion ? slots.completion({
|
|
129
|
+
candidate,
|
|
130
|
+
index,
|
|
131
|
+
active
|
|
132
|
+
}) : candidate);
|
|
133
|
+
}));
|
|
134
|
+
// src/parts/block-view.ts
|
|
135
|
+
var json = (value) => JSON.stringify(value, null, 2);
|
|
136
|
+
/** A table cell: a missing key and `null` are empty. */
|
|
137
|
+
var cellText = (value) => value === void 0 || value === null ? "" : String(value);
|
|
138
|
+
var content = (block, labels) => {
|
|
139
|
+
const hooks = {
|
|
140
|
+
class: "cc-block",
|
|
141
|
+
"data-type": block.type
|
|
142
|
+
};
|
|
143
|
+
switch (block.type) {
|
|
144
|
+
case "text": return h("pre", hooks, block.text);
|
|
145
|
+
case "json": return h("pre", hooks, json(block.value));
|
|
146
|
+
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]))))))]);
|
|
147
|
+
case "list": return h("ul", hooks, block.items.map((item) => h("li", item)));
|
|
148
|
+
case "message": return h("div", hooks, messageText(labels, block));
|
|
149
|
+
case "custom": return h("pre", hooks, json(block));
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
/** Renders `block` of `entry`, through its slot when one is given. */
|
|
153
|
+
var renderBlock = (block, entry, labels, slots) => {
|
|
154
|
+
const slot = slots[`block-${block.type}`];
|
|
155
|
+
if (slot) return h("div", {
|
|
156
|
+
class: "cc-block",
|
|
157
|
+
"data-type": block.type
|
|
158
|
+
}, slot({
|
|
159
|
+
block,
|
|
160
|
+
entry
|
|
161
|
+
}));
|
|
162
|
+
return content(block, labels);
|
|
163
|
+
};
|
|
164
|
+
// src/parts/entry-view.ts
|
|
165
|
+
/** `span.cc-prompt` with the `prompt` slot inside, or the prompt text. */
|
|
166
|
+
var renderPrompt = (prompt, slots) => h("span", { class: "cc-prompt" }, slots.prompt ? slots.prompt({ prompt }) : prompt);
|
|
167
|
+
var renderEntry = (entry, labels, slots) => {
|
|
168
|
+
if (entry.kind === "input") return h("div", {
|
|
169
|
+
key: entry.id,
|
|
170
|
+
class: "cc-entry",
|
|
171
|
+
"data-kind": "input",
|
|
172
|
+
"data-status": entry.status,
|
|
173
|
+
"data-redacted": entry.redacted ? "" : void 0
|
|
174
|
+
}, [
|
|
175
|
+
renderPrompt(entry.prompt, slots),
|
|
176
|
+
h("span", { class: "cc-line" }, entry.redacted || entry.text === null ? labels.redacted : entry.text),
|
|
177
|
+
entry.status === "cancelled" ? h("span", { class: "cc-cancelled" }, labels.cancelledMarker) : null
|
|
178
|
+
]);
|
|
179
|
+
return h("div", {
|
|
180
|
+
key: entry.id,
|
|
181
|
+
class: "cc-entry",
|
|
182
|
+
"data-kind": "output",
|
|
183
|
+
"data-tone": entry.tone,
|
|
184
|
+
"aria-live": entry.quiet ? "off" : void 0
|
|
185
|
+
}, entry.blocks.map((block) => renderBlock(block, entry, labels, slots)));
|
|
186
|
+
};
|
|
187
|
+
// src/use-command-console.ts
|
|
188
|
+
/** The options that are functions: a ref may hold one, a getter may not. */
|
|
189
|
+
var functionKeys = [
|
|
190
|
+
"onEntriesChange",
|
|
191
|
+
"onRecallChange",
|
|
192
|
+
"onContextChange",
|
|
193
|
+
"createId",
|
|
194
|
+
"beforeRun",
|
|
195
|
+
"onError",
|
|
196
|
+
"formatError",
|
|
197
|
+
"onClose",
|
|
198
|
+
"isSensitive",
|
|
199
|
+
"shouldRecord"
|
|
200
|
+
];
|
|
201
|
+
/** The options that are values: a ref or a getter may give them. */
|
|
202
|
+
var valueKeys = [
|
|
203
|
+
"commands",
|
|
204
|
+
"prompt",
|
|
205
|
+
"state",
|
|
206
|
+
"maxEntries",
|
|
207
|
+
"recallLimit"
|
|
208
|
+
];
|
|
209
|
+
/**
|
|
210
|
+
* Creates a console from `options` and keeps it in step with them.
|
|
211
|
+
*
|
|
212
|
+
* A value option (`commands`, `prompt`, `state`, `maxEntries`,
|
|
213
|
+
* `recallLimit`) may be a plain value, a ref or a getter. A function option
|
|
214
|
+
* (`beforeRun`, `onError`, …) may be the function or a ref that holds it: a
|
|
215
|
+
* bare function is the option itself, never a getter. `features` and
|
|
216
|
+
* `initialState` are read once.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```ts
|
|
220
|
+
* const { console, state } = useCommandConsole({
|
|
221
|
+
* features: consoleFeatures({ recallFeature }),
|
|
222
|
+
* commands: () => commands.value
|
|
223
|
+
* })
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
function useCommandConsole(options) {
|
|
227
|
+
const resolve = () => {
|
|
228
|
+
const resolved = {};
|
|
229
|
+
for (const key of valueKeys) resolved[key] = toValue(options[key]);
|
|
230
|
+
for (const key of functionKeys) resolved[key] = unref(options[key]);
|
|
231
|
+
return resolved;
|
|
232
|
+
};
|
|
233
|
+
const console = createConsole({
|
|
234
|
+
...resolve(),
|
|
235
|
+
features: options.features,
|
|
236
|
+
initialState: toValue(options.initialState)
|
|
237
|
+
});
|
|
238
|
+
const state = shallowRef(console.getState());
|
|
239
|
+
const unsubscribe = console.subscribe((next) => {
|
|
240
|
+
state.value = next;
|
|
241
|
+
});
|
|
242
|
+
const stop = watch(resolve, (next) => {
|
|
243
|
+
console.setOptions((old) => ({
|
|
244
|
+
...next,
|
|
245
|
+
features: old.features,
|
|
246
|
+
initialState: old.initialState
|
|
247
|
+
}));
|
|
248
|
+
});
|
|
249
|
+
onScopeDispose(() => {
|
|
250
|
+
stop();
|
|
251
|
+
unsubscribe();
|
|
252
|
+
console.dispose();
|
|
253
|
+
state.value = console.getState();
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
console,
|
|
257
|
+
state: shallowReadonly(state)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
// src/command-console.vue?vue&type=script&setup=true&lang.ts
|
|
261
|
+
var _hoisted_1 = ["data-busy", "data-context-depth"];
|
|
262
|
+
var _hoisted_2 = ["aria-label"];
|
|
263
|
+
var _hoisted_3 = {
|
|
264
|
+
ref: "entries",
|
|
265
|
+
class: "cc-entries"
|
|
266
|
+
};
|
|
267
|
+
var _hoisted_4 = {
|
|
268
|
+
class: "cc-status",
|
|
269
|
+
role: "status"
|
|
270
|
+
};
|
|
271
|
+
var _hoisted_5 = { class: "cc-input-line" };
|
|
272
|
+
var _hoisted_6 = [
|
|
273
|
+
"aria-expanded",
|
|
274
|
+
"aria-controls",
|
|
275
|
+
"aria-activedescendant",
|
|
276
|
+
"aria-label",
|
|
277
|
+
"aria-description",
|
|
278
|
+
"readonly"
|
|
279
|
+
];
|
|
280
|
+
// src/command-console.vue
|
|
281
|
+
var command_console_default = /* @__PURE__ */ defineComponent({
|
|
282
|
+
name: "CommandConsole",
|
|
283
|
+
__name: "command-console",
|
|
284
|
+
props: {
|
|
285
|
+
commands: {},
|
|
286
|
+
prompt: { default: void 0 },
|
|
287
|
+
initialState: { default: void 0 },
|
|
288
|
+
createId: {
|
|
289
|
+
type: Function,
|
|
290
|
+
default: void 0
|
|
291
|
+
},
|
|
292
|
+
maxEntries: { default: void 0 },
|
|
293
|
+
beforeRun: {
|
|
294
|
+
type: Function,
|
|
295
|
+
default: void 0
|
|
296
|
+
},
|
|
297
|
+
onError: {
|
|
298
|
+
type: Function,
|
|
299
|
+
default: void 0
|
|
300
|
+
},
|
|
301
|
+
formatError: {
|
|
302
|
+
type: Function,
|
|
303
|
+
default: void 0
|
|
304
|
+
},
|
|
305
|
+
isSensitive: {
|
|
306
|
+
type: Function,
|
|
307
|
+
default: void 0
|
|
308
|
+
},
|
|
309
|
+
recallLimit: { default: void 0 },
|
|
310
|
+
shouldRecord: {
|
|
311
|
+
type: Function,
|
|
312
|
+
default: void 0
|
|
313
|
+
},
|
|
314
|
+
labels: { default: void 0 },
|
|
315
|
+
autofocus: {
|
|
316
|
+
type: Boolean,
|
|
317
|
+
default: false
|
|
318
|
+
},
|
|
319
|
+
entries: { default: void 0 },
|
|
320
|
+
recall: { default: void 0 },
|
|
321
|
+
context: { default: void 0 }
|
|
322
|
+
},
|
|
323
|
+
emits: [
|
|
324
|
+
"update:entries",
|
|
325
|
+
"update:recall",
|
|
326
|
+
"update:context",
|
|
327
|
+
"close",
|
|
328
|
+
"submit"
|
|
329
|
+
],
|
|
330
|
+
setup(__props, { expose: __expose, emit: __emit }) {
|
|
331
|
+
const props = __props;
|
|
332
|
+
const emit = __emit;
|
|
333
|
+
const slots = useSlots();
|
|
334
|
+
const features = consoleFeatures({
|
|
335
|
+
recallFeature,
|
|
336
|
+
completionFeature,
|
|
337
|
+
contextFeature
|
|
338
|
+
});
|
|
339
|
+
const lastEmitted = {};
|
|
340
|
+
for (const slice of [
|
|
341
|
+
"entries",
|
|
342
|
+
"recall",
|
|
343
|
+
"context"
|
|
344
|
+
]) watch(() => props[slice], () => {
|
|
345
|
+
delete lastEmitted[slice];
|
|
346
|
+
}, { flush: "sync" });
|
|
347
|
+
const report = (slice, send) => (updater) => {
|
|
348
|
+
const base = lastEmitted[slice] ?? props[slice] ?? [];
|
|
349
|
+
const value = typeof updater === "function" ? updater(base) : updater;
|
|
350
|
+
lastEmitted[slice] = value;
|
|
351
|
+
send(value);
|
|
352
|
+
};
|
|
353
|
+
/** The slices bound with `v-model`; the others are the console's. */
|
|
354
|
+
const controlled = () => {
|
|
355
|
+
const state = {};
|
|
356
|
+
if (props.entries !== void 0) state.entries = props.entries;
|
|
357
|
+
if (props.recall !== void 0) state.recall = props.recall;
|
|
358
|
+
if (props.context !== void 0) state.context = props.context;
|
|
359
|
+
return state;
|
|
360
|
+
};
|
|
361
|
+
const { console, state } = useCommandConsole({
|
|
362
|
+
features,
|
|
363
|
+
commands: () => props.commands,
|
|
364
|
+
prompt: () => props.prompt,
|
|
365
|
+
state: controlled,
|
|
366
|
+
maxEntries: () => props.maxEntries,
|
|
367
|
+
recallLimit: () => props.recallLimit,
|
|
368
|
+
initialState: props.initialState,
|
|
369
|
+
createId: toRef(props, "createId"),
|
|
370
|
+
beforeRun: toRef(props, "beforeRun"),
|
|
371
|
+
onError: toRef(props, "onError"),
|
|
372
|
+
formatError: toRef(props, "formatError"),
|
|
373
|
+
isSensitive: toRef(props, "isSensitive"),
|
|
374
|
+
shouldRecord: toRef(props, "shouldRecord"),
|
|
375
|
+
onEntriesChange: report("entries", (value) => emit("update:entries", value)),
|
|
376
|
+
onRecallChange: report("recall", (value) => emit("update:recall", value)),
|
|
377
|
+
onContextChange: report("context", (value) => emit("update:context", value)),
|
|
378
|
+
onClose: () => emit("close")
|
|
379
|
+
});
|
|
380
|
+
const labels = computed(() => resolveLabels(props.labels));
|
|
381
|
+
const listId = useId();
|
|
382
|
+
const running = computed(() => state.value.running);
|
|
383
|
+
const session = computed(() => {
|
|
384
|
+
const completion = state.value.completion;
|
|
385
|
+
return completion?.status === "open" ? completion : null;
|
|
386
|
+
});
|
|
387
|
+
const activeId = computed(() => session.value && session.value.active >= 0 ? optionId(listId, session.value.active) : void 0);
|
|
388
|
+
/** The prompt of the live line: the top context frame's, or the root's. */
|
|
389
|
+
const prompt = computed(() => {
|
|
390
|
+
state.value.context;
|
|
391
|
+
props.prompt;
|
|
392
|
+
return console.getPrompt();
|
|
393
|
+
});
|
|
394
|
+
/**
|
|
395
|
+
* The recalled line, for the status element (README: Recall): a screen reader
|
|
396
|
+
* does not announce a scripted value change in a focused field.
|
|
397
|
+
*/
|
|
398
|
+
const recalled = computed(() => {
|
|
399
|
+
const { recall, recallCursor } = state.value;
|
|
400
|
+
return recallCursor !== null ? recall[recallCursor] ?? "" : "";
|
|
401
|
+
});
|
|
402
|
+
const entriesView = () => state.value.entries.map((entry) => renderEntry(entry, labels.value, slots));
|
|
403
|
+
const completionsView = () => session.value ? renderCompletions(session.value, listId, labels.value.completions, slots) : null;
|
|
404
|
+
const promptView = () => renderPrompt(prompt.value, slots);
|
|
405
|
+
const rootElement = useTemplateRef("root");
|
|
406
|
+
const logElement = useTemplateRef("log");
|
|
407
|
+
const entriesElement = useTemplateRef("entries");
|
|
408
|
+
const inputElement = useTemplateRef("input");
|
|
409
|
+
const scroll = useStickToBottom(logElement, entriesElement);
|
|
410
|
+
const focusInput = () => inputElement.value?.focus();
|
|
411
|
+
onMounted(() => {
|
|
412
|
+
if (props.autofocus) focusInput();
|
|
413
|
+
});
|
|
414
|
+
const onRootClick = (event) => {
|
|
415
|
+
if (rootElement.value && clickFocusesInput(event.target, rootElement.value)) focusInput();
|
|
416
|
+
};
|
|
417
|
+
let hadFocus = false;
|
|
418
|
+
watch(() => state.value.input.revision, () => {
|
|
419
|
+
const element = inputElement.value;
|
|
420
|
+
hadFocus = element !== null && element.ownerDocument.activeElement === element;
|
|
421
|
+
if (element) element.value = "";
|
|
422
|
+
}, { flush: "pre" });
|
|
423
|
+
/** The value and caret this component last reported (a user edit). */
|
|
424
|
+
let reported = {
|
|
425
|
+
value: "",
|
|
426
|
+
caret: 0
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* Writes the console's value into the element. A write by the console
|
|
430
|
+
* (completion, recall) also sets the caret it chose; the user's own edit is
|
|
431
|
+
* left alone, so a selection the user made stays.
|
|
432
|
+
*/
|
|
433
|
+
const syncElement = (replaced) => {
|
|
434
|
+
const element = inputElement.value;
|
|
435
|
+
if (!element) return;
|
|
436
|
+
const { value, caret } = state.value.input;
|
|
437
|
+
if (element.value !== value) element.value = value;
|
|
438
|
+
if (replaced) {
|
|
439
|
+
if (hadFocus) element.focus();
|
|
440
|
+
hadFocus = false;
|
|
441
|
+
element.setSelectionRange(value.length, value.length);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (element.ownerDocument.activeElement !== element || value === reported.value && caret === reported.caret) return;
|
|
445
|
+
if (element.selectionStart !== caret || element.selectionEnd !== caret) element.setSelectionRange(caret, caret);
|
|
446
|
+
};
|
|
447
|
+
onMounted(() => {
|
|
448
|
+
syncElement(false);
|
|
449
|
+
});
|
|
450
|
+
watch(() => state.value.input, (next, previous) => {
|
|
451
|
+
syncElement(next.revision !== previous.revision);
|
|
452
|
+
}, { flush: "post" });
|
|
453
|
+
/** Reports the element's value and caret to the console (a user edit). */
|
|
454
|
+
const reportEdit = (element) => {
|
|
455
|
+
const caret = element.selectionStart ?? element.value.length;
|
|
456
|
+
reported = {
|
|
457
|
+
value: element.value,
|
|
458
|
+
caret
|
|
459
|
+
};
|
|
460
|
+
console.editInput(element.value, caret);
|
|
461
|
+
};
|
|
462
|
+
const onInput = (event) => {
|
|
463
|
+
reportEdit(event.currentTarget);
|
|
464
|
+
};
|
|
465
|
+
const onCaret = (event) => {
|
|
466
|
+
const element = event.currentTarget;
|
|
467
|
+
const caret = element.selectionStart ?? element.value.length;
|
|
468
|
+
if (element.value !== state.value.input.value) return;
|
|
469
|
+
if (caret !== state.value.input.caret) reportEdit(element);
|
|
470
|
+
};
|
|
471
|
+
/**
|
|
472
|
+
* Runs `action` and emits `submit` for the input entry it added, if any;
|
|
473
|
+
* keeps the log at the bottom after the user's own submit (C-58).
|
|
474
|
+
*/
|
|
475
|
+
const submitting = (action) => {
|
|
476
|
+
const before = new Set(state.value.entries.map((entry) => entry.id));
|
|
477
|
+
const result = action();
|
|
478
|
+
const added = console.getState().entries.find((entry) => entry.kind === "input" && entry.status === "submitted" && !before.has(entry.id));
|
|
479
|
+
if (added) {
|
|
480
|
+
scroll.stick();
|
|
481
|
+
const submit = {
|
|
482
|
+
inputId: added.id,
|
|
483
|
+
sensitive: added.kind === "input" && added.redacted
|
|
484
|
+
};
|
|
485
|
+
emit("submit", submit);
|
|
486
|
+
}
|
|
487
|
+
return result;
|
|
488
|
+
};
|
|
489
|
+
const onKeydown = (event) => {
|
|
490
|
+
const element = event.currentTarget;
|
|
491
|
+
const key = keyInputOf(event, element, rootElement.value);
|
|
492
|
+
if (!key.isComposing) onCaret(event);
|
|
493
|
+
if (submitting(() => console.handleKey(key))) event.preventDefault();
|
|
494
|
+
};
|
|
495
|
+
__expose({
|
|
496
|
+
focus: () => {
|
|
497
|
+
focusInput();
|
|
498
|
+
},
|
|
499
|
+
run: (line) => submitting(() => console.run(line)),
|
|
500
|
+
cancel: () => {
|
|
501
|
+
console.cancel();
|
|
502
|
+
},
|
|
503
|
+
reset: () => {
|
|
504
|
+
console.reset();
|
|
505
|
+
},
|
|
506
|
+
clear: () => {
|
|
507
|
+
console.clearEntries();
|
|
508
|
+
},
|
|
509
|
+
console,
|
|
510
|
+
state
|
|
511
|
+
});
|
|
512
|
+
return (_ctx, _cache) => {
|
|
513
|
+
return openBlock(), createElementBlock("div", {
|
|
514
|
+
ref: "root",
|
|
515
|
+
class: "cc-console",
|
|
516
|
+
"data-busy": running.value ? "" : void 0,
|
|
517
|
+
"data-context-depth": unref(state).context.length,
|
|
518
|
+
onClick: onRootClick
|
|
519
|
+
}, [
|
|
520
|
+
createElementVNode("div", {
|
|
521
|
+
ref: "log",
|
|
522
|
+
class: "cc-log",
|
|
523
|
+
role: "log",
|
|
524
|
+
"aria-label": labels.value.log,
|
|
525
|
+
onScroll: _cache[0] || (_cache[0] = (...args) => unref(scroll).onScroll && unref(scroll).onScroll(...args))
|
|
526
|
+
}, [createElementVNode("div", _hoisted_3, [(openBlock(), createBlock(resolveDynamicComponent(entriesView)))], 512)], 40, _hoisted_2),
|
|
527
|
+
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))]),
|
|
528
|
+
(openBlock(), createBlock(resolveDynamicComponent(completionsView))),
|
|
529
|
+
createElementVNode("div", _hoisted_5, [(openBlock(), createBlock(resolveDynamicComponent(promptView))), (openBlock(), createElementBlock("input", {
|
|
530
|
+
key: unref(state).input.revision,
|
|
531
|
+
ref: "input",
|
|
532
|
+
class: "cc-input",
|
|
533
|
+
type: "text",
|
|
534
|
+
role: "combobox",
|
|
535
|
+
"aria-autocomplete": "list",
|
|
536
|
+
"aria-expanded": session.value ? "true" : "false",
|
|
537
|
+
"aria-controls": session.value ? unref(listId) : void 0,
|
|
538
|
+
"aria-activedescendant": activeId.value,
|
|
539
|
+
"aria-label": labels.value.input,
|
|
540
|
+
"aria-description": labels.value.inputDescription,
|
|
541
|
+
autocomplete: "off",
|
|
542
|
+
autocorrect: "off",
|
|
543
|
+
autocapitalize: "off",
|
|
544
|
+
spellcheck: "false",
|
|
545
|
+
readonly: running.value !== null,
|
|
546
|
+
onInput,
|
|
547
|
+
onKeydown,
|
|
548
|
+
onKeyup: onCaret,
|
|
549
|
+
onClick: onCaret,
|
|
550
|
+
onSelectionchange: onCaret
|
|
551
|
+
}, null, 40, _hoisted_6))])
|
|
552
|
+
], 8, _hoisted_1);
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
// src/index.ts
|
|
557
|
+
var src_default = command_console_default;
|
|
558
|
+
export { command_console_default as CommandConsole, src_default as default, useCommandConsole };
|
|
559
|
+
|
|
560
|
+
//# 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 anywhere in the console focuses the input (README: DOM and styling,\n// C-51), unless 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 `root` should focus the input: not when\n * text is selected, not inside an interactive element (the input included:\n * a click on it is the browser's).\n */\nexport const clickFocusesInput = (\n target: EventTarget | null,\n root: Element\n): boolean => {\n const selection = root.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 && root.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 anywhere in the console focuses the input, unless text is\n// selected or the click was on something interactive.\nconst onRootClick = (event: MouseEvent) => {\n if (rootElement.value && clickFocusesInput(event.target, rootElement.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 @click=\"onRootClick\"\n >\n <div\n ref=\"log\"\n class=\"cc-log\"\n role=\"log\"\n :aria-label=\"labels.log\"\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 anywhere in the console focuses the input, unless text is\n// selected or the click was on something interactive.\nconst onRootClick = (event: MouseEvent) => {\n if (rootElement.value && clickFocusesInput(event.target, rootElement.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 @click=\"onRootClick\"\n >\n <div\n ref=\"log\"\n class=\"cc-log\"\n role=\"log\"\n :aria-label=\"labels.log\"\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,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a click on `target` inside `root` should focus the input: not when
|
|
3
|
+
* text is selected, not inside an interactive element (the input included:
|
|
4
|
+
* a click on it is the browser's).
|
|
5
|
+
*/
|
|
6
|
+
export declare const clickFocusesInput: (target: EventTarget | null, root: Element) => boolean;
|
|
@@ -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,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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
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.1",
|
|
44
|
+
"@dolusoft/command-protocol": "1.0.1"
|
|
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
|
}
|