@valyrianjs/terminal 0.1.0 → 0.1.2
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/README.md +105 -55
- package/dist/ansi.d.ts +20 -4
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +171 -47
- package/dist/ansi.js.map +1 -1
- package/dist/editor-state.d.ts +22 -0
- package/dist/editor-state.d.ts.map +1 -0
- package/dist/editor-state.js +110 -0
- package/dist/editor-state.js.map +1 -0
- package/dist/events.d.ts +1 -4
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +15 -38
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/keymap.d.ts +7 -0
- package/dist/keymap.d.ts.map +1 -0
- package/dist/keymap.js +133 -0
- package/dist/keymap.js.map +1 -0
- package/dist/layout.d.ts +10 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +97 -7
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts +1 -0
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +24 -1
- package/dist/mouse.js.map +1 -1
- package/dist/output-writer.d.ts +9 -0
- package/dist/output-writer.d.ts.map +1 -0
- package/dist/output-writer.js +79 -0
- package/dist/output-writer.js.map +1 -0
- package/dist/paste.d.ts +7 -0
- package/dist/paste.d.ts.map +1 -0
- package/dist/paste.js +18 -0
- package/dist/paste.js.map +1 -0
- package/dist/primitives.d.ts +8 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +9 -1
- package/dist/primitives.js.map +1 -1
- package/dist/render.d.ts +8 -3
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +840 -67
- package/dist/render.js.map +1 -1
- package/dist/runtime.d.ts +29 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +215 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +24 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +729 -199
- package/dist/session.js.map +1 -1
- package/dist/stream-log.d.ts +40 -0
- package/dist/stream-log.d.ts.map +1 -0
- package/dist/stream-log.js +73 -0
- package/dist/stream-log.js.map +1 -0
- package/dist/text.d.ts +3 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +19 -0
- package/dist/text.js.map +1 -0
- package/dist/theme.d.ts +7 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +254 -0
- package/dist/theme.js.map +1 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +42 -1
- package/dist/tree.js.map +1 -1
- package/dist/types.d.ts +183 -18
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +302 -136
- package/docs/assets/quick-note.svg +13 -0
- package/docs/cookbook.md +297 -202
- package/docs/core-concepts.md +143 -55
- package/docs/getting-started.md +209 -90
- package/docs/interaction-model.md +95 -61
- package/docs/primitive-gallery.md +365 -0
- package/docs/session-runtime.md +132 -363
- package/docs/valyrian-modules.md +3196 -0
- package/llms-full.txt +5357 -0
- package/package.json +21 -8
- package/src/ansi.ts +269 -0
- package/src/clipboard.ts +76 -0
- package/src/editor-state.ts +162 -0
- package/src/events.ts +163 -0
- package/src/index.ts +92 -0
- package/src/keymap.ts +151 -0
- package/src/layout.ts +282 -0
- package/src/mouse.ts +68 -0
- package/src/output-writer.ts +93 -0
- package/src/paste.ts +23 -0
- package/src/primitives.ts +52 -0
- package/src/render.ts +1107 -0
- package/src/runtime.ts +273 -0
- package/src/scheduler.ts +33 -0
- package/src/session.ts +1260 -0
- package/src/stream-log.ts +96 -0
- package/src/text.ts +20 -0
- package/src/theme.ts +263 -0
- package/src/tree.ts +169 -0
- package/src/types.ts +523 -0
- package/tsconfig.json +4 -7
- package/docs/local-demo.md +0 -28
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export type StreamLogEntryType = "assistant" | "log";
|
|
2
|
+
export type StreamLogEntryStatus = "streaming" | "complete" | "cancelled" | "error";
|
|
3
|
+
|
|
4
|
+
export type StreamLogEntry = {
|
|
5
|
+
id: string;
|
|
6
|
+
type: StreamLogEntryType;
|
|
7
|
+
content: string;
|
|
8
|
+
status: StreamLogEntryStatus;
|
|
9
|
+
error?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type StreamLogSnapshot = {
|
|
13
|
+
activeId?: string;
|
|
14
|
+
entries: StreamLogEntry[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type AppendEntry = {
|
|
18
|
+
id: string;
|
|
19
|
+
type: StreamLogEntryType;
|
|
20
|
+
content: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function createStreamLog() {
|
|
24
|
+
const entries: StreamLogEntry[] = [];
|
|
25
|
+
let activeId: string | undefined;
|
|
26
|
+
|
|
27
|
+
function findEntry(id: string) {
|
|
28
|
+
return entries.find((entry) => entry.id === id);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
append(value: AppendEntry) {
|
|
33
|
+
const entry: StreamLogEntry = { ...value, status: "complete" };
|
|
34
|
+
entries.push(entry);
|
|
35
|
+
return { ...entry };
|
|
36
|
+
},
|
|
37
|
+
update(id: string, content: string) {
|
|
38
|
+
if (activeId && activeId !== id) {
|
|
39
|
+
const activeEntry = findEntry(activeId);
|
|
40
|
+
if (activeEntry) {
|
|
41
|
+
activeEntry.status = "cancelled";
|
|
42
|
+
delete activeEntry.error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let entry = findEntry(id);
|
|
46
|
+
if (!entry) {
|
|
47
|
+
entry = { id, type: "assistant", content: "", status: "streaming" };
|
|
48
|
+
entries.push(entry);
|
|
49
|
+
}
|
|
50
|
+
entry.content = content;
|
|
51
|
+
entry.status = "streaming";
|
|
52
|
+
delete entry.error;
|
|
53
|
+
activeId = id;
|
|
54
|
+
return { ...entry };
|
|
55
|
+
},
|
|
56
|
+
complete(id: string) {
|
|
57
|
+
const entry = findEntry(id);
|
|
58
|
+
if (entry) {
|
|
59
|
+
entry.status = "complete";
|
|
60
|
+
delete entry.error;
|
|
61
|
+
}
|
|
62
|
+
if (activeId === id) {
|
|
63
|
+
activeId = undefined;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
cancel() {
|
|
67
|
+
if (!activeId) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const entry = findEntry(activeId);
|
|
71
|
+
if (entry) {
|
|
72
|
+
entry.status = "cancelled";
|
|
73
|
+
delete entry.error;
|
|
74
|
+
}
|
|
75
|
+
activeId = undefined;
|
|
76
|
+
},
|
|
77
|
+
error(id: string, message: string) {
|
|
78
|
+
let entry = findEntry(id);
|
|
79
|
+
if (!entry) {
|
|
80
|
+
entry = { id, type: "assistant", content: "", status: "error" };
|
|
81
|
+
entries.push(entry);
|
|
82
|
+
}
|
|
83
|
+
entry.status = "error";
|
|
84
|
+
entry.error = message;
|
|
85
|
+
if (activeId === id) {
|
|
86
|
+
activeId = undefined;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
snapshot(): StreamLogSnapshot {
|
|
90
|
+
return {
|
|
91
|
+
activeId,
|
|
92
|
+
entries: entries.map((entry) => ({ ...entry }))
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/text.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const STRING_TERMINAL_CONTROL = /(?:\u001b(?:P|X|\^|_)|[\u0090\u0098\u009e\u009f])[\s\S]*?(?:\u001b\\|\u009c|$)/g;
|
|
2
|
+
const OSC_TERMINAL_CONTROL = /(?:\u001b\]|\u009d)[\s\S]*?(?:\u0007|\u001b\\|\u009c|$)/g;
|
|
3
|
+
const CSI_TERMINAL_CONTROL = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
4
|
+
const C1_CSI_TERMINAL_CONTROL = /\u009b[0-?]*[ -/]*[@-~]/g;
|
|
5
|
+
const ESC_TERMINAL_CONTROL = /\u001b[ -/]*[0-~]/g;
|
|
6
|
+
const C1_TERMINAL_CONTROL = /[\u0080-\u009f]/g;
|
|
7
|
+
const C0_TERMINAL_CONTROL = /[\u0000-\u0009\u000b-\u001f\u007f]/g;
|
|
8
|
+
|
|
9
|
+
export function stripTerminalControls(value: unknown) {
|
|
10
|
+
return String(value)
|
|
11
|
+
.replace(STRING_TERMINAL_CONTROL, "")
|
|
12
|
+
.replace(OSC_TERMINAL_CONTROL, "")
|
|
13
|
+
.replace(CSI_TERMINAL_CONTROL, "")
|
|
14
|
+
.replace(C1_CSI_TERMINAL_CONTROL, "")
|
|
15
|
+
.replace(ESC_TERMINAL_CONTROL, "")
|
|
16
|
+
.replace(C1_TERMINAL_CONTROL, "")
|
|
17
|
+
.replace(C0_TERMINAL_CONTROL, "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const plainText = stripTerminalControls;
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import type { TerminalSemanticStyleKind, TerminalStyleDefinition, TerminalStyleToken, TerminalStyleTree, TerminalStyleValue, TerminalTheme } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const STYLE_RECIPE_KEYS = new Set(["color", "background", "border", "padding", "plainPrefix", "plainSuffix"]);
|
|
4
|
+
|
|
5
|
+
const reverseVideoToken: TerminalStyleToken = {
|
|
6
|
+
ansiOpen: "\u001b[7m",
|
|
7
|
+
ansiClose: "\u001b[27m"
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const defaultTerminalTheme: TerminalTheme = {
|
|
11
|
+
styles: {
|
|
12
|
+
state: {
|
|
13
|
+
focus: { color: "#ffffff" },
|
|
14
|
+
hover: { color: "#dddddd" },
|
|
15
|
+
disabled: { color: "#777777" },
|
|
16
|
+
loading: { color: "#88c0d0", background: "#1f2328" },
|
|
17
|
+
pressed: { background: "#333333" },
|
|
18
|
+
checked: { color: "#a3be8c" },
|
|
19
|
+
unchecked: { color: "#d8dee9" },
|
|
20
|
+
indeterminate: { color: "#ebcb8b" },
|
|
21
|
+
selected: { background: "#2e3440" },
|
|
22
|
+
current: { background: "#3b4252" },
|
|
23
|
+
expanded: { color: "#b48ead" },
|
|
24
|
+
collapsed: { color: "#81a1c1" },
|
|
25
|
+
invalid: { color: "#bf616a" },
|
|
26
|
+
valid: { color: "#a3be8c" },
|
|
27
|
+
readonly: { color: "#d8dee9" },
|
|
28
|
+
placeholder: { color: "#777777" },
|
|
29
|
+
selection: { background: "#434c5e" },
|
|
30
|
+
editing: { color: "#88c0d0" },
|
|
31
|
+
submitted: { color: "#a3be8c" },
|
|
32
|
+
empty: { color: "#777777" },
|
|
33
|
+
error: { color: "#bf616a" },
|
|
34
|
+
warning: { color: "#ebcb8b" },
|
|
35
|
+
success: { color: "#a3be8c" },
|
|
36
|
+
muted: { color: "#777777" },
|
|
37
|
+
dragging: { color: "#b48ead" },
|
|
38
|
+
dropTarget: { background: "#243b53" },
|
|
39
|
+
capturing: { color: "#d08770" }
|
|
40
|
+
},
|
|
41
|
+
button: {
|
|
42
|
+
base: { color: "#d8dee9", background: "#1f2328", padding: { left: 2, right: 2 } },
|
|
43
|
+
focus: { color: "#ffffff", background: "#315f9e" },
|
|
44
|
+
hover: { color: "#ffffff", background: "#2b3137" },
|
|
45
|
+
pressed: { color: "#ffffff", background: "#3b4252" },
|
|
46
|
+
disabled: { color: "#777777", background: "#1f2328" },
|
|
47
|
+
warning: {
|
|
48
|
+
color: "#ebcb8b",
|
|
49
|
+
background: "#2e2600",
|
|
50
|
+
border: { left: true, right: true, style: "solid", color: "#ebcb8b" },
|
|
51
|
+
padding: { left: 1, right: 1 },
|
|
52
|
+
focus: { color: "#ffffff", border: { left: true, right: true, style: "double", color: "#ffffff" } },
|
|
53
|
+
hover: { color: "#ffffff" },
|
|
54
|
+
pressed: { background: "#4a3600" }
|
|
55
|
+
},
|
|
56
|
+
loading: { color: "#88c0d0", background: "#1f2328" },
|
|
57
|
+
success: { color: "#a3be8c" },
|
|
58
|
+
error: { color: "#bf616a" },
|
|
59
|
+
checked: { color: "#a3be8c" },
|
|
60
|
+
unchecked: { color: "#d8dee9" },
|
|
61
|
+
indeterminate: { color: "#ebcb8b" }
|
|
62
|
+
},
|
|
63
|
+
input: {
|
|
64
|
+
base: { color: "#d8dee9", background: "#0d1117", padding: { left: 2, right: 0 } },
|
|
65
|
+
focus: { color: "#ffffff", background: "#161b22" },
|
|
66
|
+
disabled: { color: "#777777" },
|
|
67
|
+
invalid: { color: "#bf616a" },
|
|
68
|
+
valid: { color: "#a3be8c" },
|
|
69
|
+
readonly: { color: "#777777" },
|
|
70
|
+
placeholder: { color: "#777777" },
|
|
71
|
+
selection: { background: "#434c5e" },
|
|
72
|
+
editing: { color: "#88c0d0" },
|
|
73
|
+
submitted: { color: "#a3be8c" }
|
|
74
|
+
},
|
|
75
|
+
editor: { base: { color: "#d8dee9" }, focus: { color: "#ffffff" }, selection: { background: "#434c5e" }, editing: { color: "#88c0d0" } },
|
|
76
|
+
surface: {
|
|
77
|
+
card: { background: "#111111", padding: 1 },
|
|
78
|
+
empty: { color: "#777777" }, loading: { color: "#88c0d0" }, error: { color: "#bf616a" }, warning: { color: "#ebcb8b" }, success: { color: "#a3be8c" }, muted: { color: "#777777" }, dragging: { color: "#b48ead" }, dropTarget: { background: "#243b53" }, capturing: { color: "#d08770" }, focus: { color: "#ffffff" }, hover: { color: "#dddddd" }
|
|
79
|
+
},
|
|
80
|
+
list: { base: { color: "#d8dee9" }, selected: { color: "#ffffff", background: "#2e3440" }, current: { color: "#ffffff", background: "#3b4252" }, hover: { color: "#ffffff", background: "#2b3137" }, empty: { color: "#777777" }, expanded: { color: "#b48ead" }, collapsed: { color: "#81a1c1" } },
|
|
81
|
+
scroll: { base: { color: "#d8dee9" }, hover: { color: "#dddddd" } },
|
|
82
|
+
log: { base: { color: "#d8dee9" }, empty: { color: "#777777" }, error: { color: "#bf616a" }, warning: { color: "#ebcb8b" }, success: { color: "#a3be8c" }, muted: { color: "#777777" } },
|
|
83
|
+
overlay: { base: { background: "#111111" }, dragging: { color: "#b48ead" }, dropTarget: { background: "#243b53" }, capturing: { color: "#d08770" } },
|
|
84
|
+
pane: {
|
|
85
|
+
header: { background: "#3a3a3a", color: "#ffffff" },
|
|
86
|
+
transcript: { background: "#303030", color: "#dddddd" },
|
|
87
|
+
tools: { background: "#252525", color: "#cccccc" },
|
|
88
|
+
prompt: { background: "#333333", color: "#ffffff" }
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
spans: {
|
|
92
|
+
selection: {
|
|
93
|
+
...reverseVideoToken,
|
|
94
|
+
plainPrefix: "[",
|
|
95
|
+
plainSuffix: "]"
|
|
96
|
+
},
|
|
97
|
+
"input.selection": {
|
|
98
|
+
plainPrefix: "[",
|
|
99
|
+
plainSuffix: "]"
|
|
100
|
+
},
|
|
101
|
+
"list.current": {
|
|
102
|
+
plainPrefix: "> "
|
|
103
|
+
},
|
|
104
|
+
focus: {
|
|
105
|
+
background: "#1f2328"
|
|
106
|
+
},
|
|
107
|
+
"current-row": reverseVideoToken,
|
|
108
|
+
hover: {
|
|
109
|
+
ansiOpen: "\u001b[4m",
|
|
110
|
+
ansiClose: "\u001b[24m"
|
|
111
|
+
},
|
|
112
|
+
highlight: {
|
|
113
|
+
ansiOpen: "\u001b[1m",
|
|
114
|
+
ansiClose: "\u001b[22m"
|
|
115
|
+
},
|
|
116
|
+
"pane.header": {
|
|
117
|
+
ansiOpen: "\u001b[48;5;236m\u001b[38;5;255m",
|
|
118
|
+
ansiClose: "\u001b[49m\u001b[39m"
|
|
119
|
+
},
|
|
120
|
+
"pane.transcript": {
|
|
121
|
+
ansiOpen: "\u001b[48;5;235m\u001b[38;5;252m",
|
|
122
|
+
ansiClose: "\u001b[49m\u001b[39m"
|
|
123
|
+
},
|
|
124
|
+
"pane.tools": {
|
|
125
|
+
ansiOpen: "\u001b[48;5;234m\u001b[38;5;250m",
|
|
126
|
+
ansiClose: "\u001b[49m\u001b[39m"
|
|
127
|
+
},
|
|
128
|
+
"pane.prompt": {
|
|
129
|
+
ansiOpen: "\u001b[48;5;237m\u001b[38;5;255m",
|
|
130
|
+
ansiClose: "\u001b[49m\u001b[39m"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const highContrastTerminalTheme: TerminalTheme = {
|
|
136
|
+
spans: {
|
|
137
|
+
...defaultTerminalTheme.spans,
|
|
138
|
+
focus: {
|
|
139
|
+
...reverseVideoToken,
|
|
140
|
+
plainPrefix: ">",
|
|
141
|
+
plainSuffix: "<"
|
|
142
|
+
},
|
|
143
|
+
error: {
|
|
144
|
+
ansiOpen: "\u001b[1;31m",
|
|
145
|
+
ansiClose: "\u001b[22;39m",
|
|
146
|
+
plainPrefix: "!"
|
|
147
|
+
},
|
|
148
|
+
warning: {
|
|
149
|
+
ansiOpen: "\u001b[1;33m",
|
|
150
|
+
ansiClose: "\u001b[22;39m",
|
|
151
|
+
plainPrefix: "!"
|
|
152
|
+
},
|
|
153
|
+
success: {
|
|
154
|
+
ansiOpen: "\u001b[1;32m",
|
|
155
|
+
ansiClose: "\u001b[22;39m",
|
|
156
|
+
plainPrefix: "+"
|
|
157
|
+
},
|
|
158
|
+
muted: {
|
|
159
|
+
ansiOpen: "\u001b[2m",
|
|
160
|
+
ansiClose: "\u001b[22m"
|
|
161
|
+
},
|
|
162
|
+
disabled: {
|
|
163
|
+
ansiOpen: "\u001b[2m",
|
|
164
|
+
ansiClose: "\u001b[22m",
|
|
165
|
+
plainPrefix: "(",
|
|
166
|
+
plainSuffix: ")"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export function mergeTerminalTheme(theme?: TerminalTheme): TerminalTheme {
|
|
172
|
+
const styles = mergeStyleTrees(defaultTerminalTheme.styles, theme?.styles);
|
|
173
|
+
validateStyleTree(styles);
|
|
174
|
+
return {
|
|
175
|
+
styles,
|
|
176
|
+
spans: {
|
|
177
|
+
...defaultTerminalTheme.spans,
|
|
178
|
+
...theme?.spans
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
184
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function cloneStyleTree(tree: TerminalStyleTree | undefined): TerminalStyleTree | undefined {
|
|
188
|
+
if (!tree) return undefined;
|
|
189
|
+
const next: TerminalStyleTree = {};
|
|
190
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
191
|
+
next[key] = isPlainObject(value) ? cloneStyleTree(value as TerminalStyleTree) : value as any;
|
|
192
|
+
}
|
|
193
|
+
return next;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function mergeStyleTrees(base: TerminalStyleTree | undefined, override: TerminalStyleTree | undefined): TerminalStyleTree | undefined {
|
|
197
|
+
const next = cloneStyleTree(base) || {};
|
|
198
|
+
if (!override) return next;
|
|
199
|
+
for (const [key, value] of Object.entries(override)) {
|
|
200
|
+
if (STYLE_RECIPE_KEYS.has(key)) {
|
|
201
|
+
next[key] = isPlainObject(value) ? cloneStyleTree(value as TerminalStyleTree) : value as any;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const existing = next[key];
|
|
205
|
+
next[key] = isPlainObject(existing) && isPlainObject(value)
|
|
206
|
+
? mergeStyleTrees(existing as TerminalStyleTree, value as TerminalStyleTree)
|
|
207
|
+
: isPlainObject(value) ? cloneStyleTree(value as TerminalStyleTree) : value as any;
|
|
208
|
+
}
|
|
209
|
+
return next;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function validateStyleTree(tree: TerminalStyleTree | undefined, path = "styles") {
|
|
213
|
+
if (!tree) return;
|
|
214
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
215
|
+
if (STYLE_RECIPE_KEYS.has(key)) continue;
|
|
216
|
+
if (typeof value === "undefined") continue;
|
|
217
|
+
if (!isPlainObject(value)) {
|
|
218
|
+
throw new RangeError(`Unknown terminal style recipe key: ${path}.${key}`);
|
|
219
|
+
}
|
|
220
|
+
validateStyleTree(value as TerminalStyleTree, `${path}.${key}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function styleRecipeFromNode(node: TerminalStyleTree): TerminalStyleDefinition | undefined {
|
|
225
|
+
const recipe: TerminalStyleDefinition = {};
|
|
226
|
+
for (const key of STYLE_RECIPE_KEYS) {
|
|
227
|
+
if (Object.prototype.hasOwnProperty.call(node, key)) {
|
|
228
|
+
(recipe as any)[key] = isPlainObject(node[key]) ? { ...(node[key] as object) } : node[key];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return Object.keys(recipe).length ? recipe : undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function resolveTerminalStyle(value: TerminalStyleValue | undefined, theme?: TerminalTheme): TerminalStyleDefinition | undefined {
|
|
235
|
+
if (typeof value === "undefined") return undefined;
|
|
236
|
+
if (typeof value !== "string") {
|
|
237
|
+
return { ...value, border: isPlainObject(value.border) ? { ...value.border } : value.border, padding: isPlainObject(value.padding) ? { ...value.padding } : value.padding };
|
|
238
|
+
}
|
|
239
|
+
const merged = mergeTerminalTheme(theme);
|
|
240
|
+
const parts = value.split(".").filter(Boolean);
|
|
241
|
+
let node: unknown = merged.styles;
|
|
242
|
+
for (const part of parts) {
|
|
243
|
+
if (!isPlainObject(node) || !(part in node)) {
|
|
244
|
+
throw new RangeError(`Unknown terminal style token: ${value}`);
|
|
245
|
+
}
|
|
246
|
+
node = (node as TerminalStyleTree)[part];
|
|
247
|
+
}
|
|
248
|
+
if (!isPlainObject(node)) {
|
|
249
|
+
throw new RangeError(`Unknown terminal style token: ${value}`);
|
|
250
|
+
}
|
|
251
|
+
const recipe = styleRecipeFromNode(node as TerminalStyleTree);
|
|
252
|
+
if (!recipe) {
|
|
253
|
+
throw new RangeError(`Unknown terminal style token: ${value}`);
|
|
254
|
+
}
|
|
255
|
+
return recipe;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function resolveTerminalStyleToken(
|
|
259
|
+
kind: TerminalSemanticStyleKind | (string & {}),
|
|
260
|
+
theme?: TerminalTheme
|
|
261
|
+
): TerminalStyleToken | undefined {
|
|
262
|
+
return mergeTerminalTheme(theme).spans?.[kind];
|
|
263
|
+
}
|
package/src/tree.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { TerminalFocusNode, TerminalNode, TerminalPrimitiveTag, VnodeLike } from "./types.js";
|
|
2
|
+
|
|
3
|
+
function isVnodeLike(value: any): value is VnodeLike {
|
|
4
|
+
return Boolean(value && typeof value === "object" && "tag" in value && "children" in value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isPojoComponent(value: any): value is { view: Function } {
|
|
8
|
+
return Boolean(value && typeof value === "object" && typeof value.view === "function");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeChildren(children: any[]): TerminalNode[] {
|
|
12
|
+
const out: TerminalNode[] = [];
|
|
13
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
14
|
+
out.push(...resolveToTerminalNodes(children[i]));
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveComponent(component: any, props: Record<string, any>, children: any[]) {
|
|
20
|
+
if (isPojoComponent(component)) {
|
|
21
|
+
return component.view.call(component, props, children);
|
|
22
|
+
}
|
|
23
|
+
return component(props, children);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveToTerminalNodes(input: any): TerminalNode[] {
|
|
27
|
+
if (input == null || input === false || input === true) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(input)) {
|
|
32
|
+
return input.flatMap(resolveToTerminalNodes);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof input === "string" || typeof input === "number") {
|
|
36
|
+
return [{ type: "text", value: String(input) }];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isVnodeLike(input)) {
|
|
40
|
+
if (input.props && "v-for" in (input.props || {})) {
|
|
41
|
+
const set = (input.props || {})["v-for"];
|
|
42
|
+
const callback = input.children?.[0];
|
|
43
|
+
|
|
44
|
+
if (typeof callback === "function" && Array.isArray(set)) {
|
|
45
|
+
const out: TerminalNode[] = [];
|
|
46
|
+
for (let i = 0; i < set.length; i += 1) {
|
|
47
|
+
out.push(...resolveToTerminalNodes(callback(set[i], i)));
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof input.tag === "function" || isPojoComponent(input.tag)) {
|
|
54
|
+
return resolveToTerminalNodes(resolveComponent(input.tag, input.props || {}, input.children || []));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
type: "element",
|
|
60
|
+
tag: input.tag as TerminalPrimitiveTag,
|
|
61
|
+
props: (input.props || {}) as Record<string, any>,
|
|
62
|
+
children: normalizeChildren(input.children || [])
|
|
63
|
+
}
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return [{ type: "text", value: String(input) }];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveRoot(input: any): TerminalNode[] {
|
|
71
|
+
if (typeof input === "function" && !isVnodeLike(input)) {
|
|
72
|
+
return resolveToTerminalNodes(input({}, []));
|
|
73
|
+
}
|
|
74
|
+
return resolveToTerminalNodes(input);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function textContent(node: TerminalNode): string {
|
|
78
|
+
if (node.type === "text") {
|
|
79
|
+
return node.value;
|
|
80
|
+
}
|
|
81
|
+
return node.children.map(textContent).join("");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isFocusable(node: TerminalNode): node is TerminalFocusNode {
|
|
85
|
+
return node.type === "element" && (node.tag === "terminal-input" || node.tag === "terminal-editor" || node.tag === "terminal-button" || node.tag === "terminal-list" || node.tag === "terminal-scroll" || Boolean(node.props.focusable));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function collectFocusableNodes(nodes: TerminalNode[], out: TerminalFocusNode[]) {
|
|
89
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
90
|
+
const node = nodes[i];
|
|
91
|
+
if (isFocusable(node)) {
|
|
92
|
+
out.push(node);
|
|
93
|
+
}
|
|
94
|
+
if (node.type === "element") {
|
|
95
|
+
collectFocusableNodes(node.children, out);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function collectDirectOverlayFocusableNodes(nodes: TerminalNode[], out: TerminalFocusNode[]) {
|
|
101
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
102
|
+
const node = nodes[i];
|
|
103
|
+
if (node.type !== "element") {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const overlayChildren = node.children.filter((child) => child.type === "element" && child.tag === "terminal-overlay" && child.props.trapFocus !== false);
|
|
108
|
+
if (overlayChildren.length) {
|
|
109
|
+
collectFocusableNodes(overlayChildren, out);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
collectDirectOverlayFocusableNodes(node.children, out);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function collectActiveFocusScopeFocusableNodes(nodes: TerminalNode[], focusedId: string | null, out: TerminalFocusNode[]): boolean {
|
|
118
|
+
if (!focusedId) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
123
|
+
const node = nodes[i];
|
|
124
|
+
if (node.type !== "element") {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (node.tag === "terminal-focus-scope" && node.props.active !== false) {
|
|
129
|
+
const scoped: TerminalFocusNode[] = [];
|
|
130
|
+
collectFocusableNodes(node.children, scoped);
|
|
131
|
+
if (scoped.some((focusable) => focusable.props.id === focusedId)) {
|
|
132
|
+
if (collectActiveFocusScopeFocusableNodes(node.children, focusedId, out)) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
out.push(...scoped);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (collectActiveFocusScopeFocusableNodes(node.children, focusedId, out)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function findFocusableById(nodes: TerminalNode[], id: string): TerminalFocusNode | null {
|
|
149
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
150
|
+
const node = nodes[i];
|
|
151
|
+
if (isFocusable(node) && node.props.id === id) {
|
|
152
|
+
return node;
|
|
153
|
+
}
|
|
154
|
+
if (node.type === "element") {
|
|
155
|
+
const found = findFocusableById(node.children, id);
|
|
156
|
+
if (found) {
|
|
157
|
+
return found;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function findFocused(nodes: TerminalNode[], focusedId: string | null) {
|
|
165
|
+
if (!focusedId) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return findFocusableById(nodes, focusedId);
|
|
169
|
+
}
|