svg-terminal 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +384 -0
- package/dist/chunk-IVINEQLU.js +4603 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +524 -0
- package/dist/index.d.ts +733 -0
- package/dist/index.js +101 -0
- package/package.json +76 -0
|
@@ -0,0 +1,4603 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { z as z50 } from "zod";
|
|
3
|
+
|
|
4
|
+
// src/core/config.ts
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { z as z2 } from "zod";
|
|
7
|
+
|
|
8
|
+
// src/themes/dracula.ts
|
|
9
|
+
var dracula = {
|
|
10
|
+
name: "dracula",
|
|
11
|
+
colors: {
|
|
12
|
+
text: "#e4e4e4",
|
|
13
|
+
comment: "#6272a4",
|
|
14
|
+
background: "#0a0e27",
|
|
15
|
+
titleBarBackground: "#151b2e",
|
|
16
|
+
titleBarText: "#e0e6ed",
|
|
17
|
+
prompt: "#00ff9f",
|
|
18
|
+
cursor: "#00ff41",
|
|
19
|
+
red: "#ff5555",
|
|
20
|
+
green: "#50fa7b",
|
|
21
|
+
yellow: "#f1fa8c",
|
|
22
|
+
blue: "#729fcf",
|
|
23
|
+
magenta: "#ff79c6",
|
|
24
|
+
cyan: "#8be9fd",
|
|
25
|
+
white: "#ffffff",
|
|
26
|
+
orange: "#ffb86c",
|
|
27
|
+
purple: "#bd93f9",
|
|
28
|
+
pink: "#ff79c6",
|
|
29
|
+
brightRed: "#ff6e6e",
|
|
30
|
+
brightGreen: "#69ff94",
|
|
31
|
+
brightYellow: "#ffffa5",
|
|
32
|
+
brightBlue: "#d6acff",
|
|
33
|
+
brightMagenta: "#ff92df",
|
|
34
|
+
brightCyan: "#a4ffff",
|
|
35
|
+
brightWhite: "#ffffff",
|
|
36
|
+
brightBlack: "#6272a4"
|
|
37
|
+
},
|
|
38
|
+
buttons: {
|
|
39
|
+
close: "#ff5f57",
|
|
40
|
+
minimize: "#ffbd2e",
|
|
41
|
+
maximize: "#28ca42"
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/themes/nord.ts
|
|
46
|
+
var nord = {
|
|
47
|
+
name: "nord",
|
|
48
|
+
colors: {
|
|
49
|
+
text: "#d8dee9",
|
|
50
|
+
// Upstream Nord uses #4c566a (Polar Night 3) — visually invisible on the
|
|
51
|
+
// #2e3440 terminal background (~1.7:1). Lifted to a Polar Night / Snow Storm
|
|
52
|
+
// midpoint so [[fg:comment]] / [[dim]] text stays legible.
|
|
53
|
+
comment: "#7c8ba8",
|
|
54
|
+
background: "#2e3440",
|
|
55
|
+
titleBarBackground: "#3b4252",
|
|
56
|
+
titleBarText: "#d8dee9",
|
|
57
|
+
prompt: "#a3be8c",
|
|
58
|
+
cursor: "#88c0d0",
|
|
59
|
+
red: "#bf616a",
|
|
60
|
+
green: "#a3be8c",
|
|
61
|
+
yellow: "#ebcb8b",
|
|
62
|
+
blue: "#5e81ac",
|
|
63
|
+
magenta: "#b48ead",
|
|
64
|
+
cyan: "#88c0d0",
|
|
65
|
+
white: "#eceff4",
|
|
66
|
+
orange: "#d08770",
|
|
67
|
+
purple: "#b48ead",
|
|
68
|
+
pink: "#b48ead",
|
|
69
|
+
brightRed: "#bf616a",
|
|
70
|
+
brightGreen: "#a3be8c",
|
|
71
|
+
brightYellow: "#ebcb8b",
|
|
72
|
+
brightBlue: "#81a1c1",
|
|
73
|
+
brightMagenta: "#b48ead",
|
|
74
|
+
brightCyan: "#8fbcbb",
|
|
75
|
+
brightWhite: "#eceff4",
|
|
76
|
+
brightBlack: "#4c566a"
|
|
77
|
+
},
|
|
78
|
+
buttons: {
|
|
79
|
+
close: "#bf616a",
|
|
80
|
+
minimize: "#ebcb8b",
|
|
81
|
+
maximize: "#a3be8c"
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/themes/monokai.ts
|
|
86
|
+
var monokai = {
|
|
87
|
+
name: "monokai",
|
|
88
|
+
colors: {
|
|
89
|
+
text: "#f8f8f2",
|
|
90
|
+
comment: "#75715e",
|
|
91
|
+
background: "#272822",
|
|
92
|
+
titleBarBackground: "#1e1f1c",
|
|
93
|
+
// Quieter title text (canonical Monokai comment color) so the chrome
|
|
94
|
+
// reads differently from dracula at gallery-thumbnail size. Both themes
|
|
95
|
+
// ship with the same macOS chrome + similar warm-dark backgrounds; the
|
|
96
|
+
// title color is the cheapest place to differentiate.
|
|
97
|
+
titleBarText: "#75715e",
|
|
98
|
+
prompt: "#a6e22e",
|
|
99
|
+
cursor: "#f8f8f2",
|
|
100
|
+
red: "#f92672",
|
|
101
|
+
green: "#a6e22e",
|
|
102
|
+
yellow: "#e6db74",
|
|
103
|
+
blue: "#66d9ef",
|
|
104
|
+
magenta: "#ae81ff",
|
|
105
|
+
cyan: "#66d9ef",
|
|
106
|
+
white: "#f8f8f2",
|
|
107
|
+
orange: "#fd971f",
|
|
108
|
+
purple: "#ae81ff",
|
|
109
|
+
pink: "#f92672",
|
|
110
|
+
brightRed: "#f92672",
|
|
111
|
+
brightGreen: "#a6e22e",
|
|
112
|
+
brightYellow: "#e6db74",
|
|
113
|
+
brightBlue: "#66d9ef",
|
|
114
|
+
brightMagenta: "#ae81ff",
|
|
115
|
+
brightCyan: "#66d9ef",
|
|
116
|
+
brightWhite: "#f8f8f0",
|
|
117
|
+
brightBlack: "#75715e"
|
|
118
|
+
},
|
|
119
|
+
buttons: {
|
|
120
|
+
close: "#f92672",
|
|
121
|
+
minimize: "#e6db74",
|
|
122
|
+
maximize: "#a6e22e"
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// src/themes/amber.ts
|
|
127
|
+
var amber = {
|
|
128
|
+
name: "amber",
|
|
129
|
+
colors: {
|
|
130
|
+
text: "#ffb000",
|
|
131
|
+
comment: "#996600",
|
|
132
|
+
background: "#1a0f00",
|
|
133
|
+
titleBarBackground: "#2a1a00",
|
|
134
|
+
titleBarText: "#ffb000",
|
|
135
|
+
prompt: "#ffd700",
|
|
136
|
+
cursor: "#ffd700",
|
|
137
|
+
red: "#ff6600",
|
|
138
|
+
green: "#ffb000",
|
|
139
|
+
yellow: "#ffd700",
|
|
140
|
+
blue: "#cc8800",
|
|
141
|
+
magenta: "#ff8c00",
|
|
142
|
+
cyan: "#ffcc00",
|
|
143
|
+
white: "#ffe0a0",
|
|
144
|
+
orange: "#ff9900",
|
|
145
|
+
purple: "#cc7700",
|
|
146
|
+
pink: "#ff8c00",
|
|
147
|
+
brightRed: "#ff8800",
|
|
148
|
+
brightGreen: "#ffc000",
|
|
149
|
+
brightYellow: "#ffe000",
|
|
150
|
+
brightBlue: "#ddaa00",
|
|
151
|
+
brightMagenta: "#ffaa00",
|
|
152
|
+
brightCyan: "#ffdd00",
|
|
153
|
+
brightWhite: "#fff0c0",
|
|
154
|
+
brightBlack: "#664400"
|
|
155
|
+
},
|
|
156
|
+
// Canonical macOS triad (red/yellow/green) rather than amber-tinted dots.
|
|
157
|
+
// The monochrome version read as "decorative ornament" — viewers couldn't
|
|
158
|
+
// tell they were window controls. The triad is universal language.
|
|
159
|
+
buttons: {
|
|
160
|
+
close: "#ff5f57",
|
|
161
|
+
minimize: "#ffbd2e",
|
|
162
|
+
maximize: "#28ca42"
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// src/themes/green-phosphor.ts
|
|
167
|
+
var greenPhosphor = {
|
|
168
|
+
name: "green-phosphor",
|
|
169
|
+
colors: {
|
|
170
|
+
text: "#00ff41",
|
|
171
|
+
comment: "#008020",
|
|
172
|
+
background: "#001100",
|
|
173
|
+
titleBarBackground: "#002200",
|
|
174
|
+
titleBarText: "#00ff41",
|
|
175
|
+
prompt: "#33ff77",
|
|
176
|
+
cursor: "#33ff77",
|
|
177
|
+
red: "#00cc33",
|
|
178
|
+
green: "#00ff41",
|
|
179
|
+
yellow: "#66ff66",
|
|
180
|
+
blue: "#00cc66",
|
|
181
|
+
magenta: "#00ff88",
|
|
182
|
+
cyan: "#00ffaa",
|
|
183
|
+
white: "#aaffaa",
|
|
184
|
+
orange: "#33ff33",
|
|
185
|
+
purple: "#00dd55",
|
|
186
|
+
pink: "#00ff77",
|
|
187
|
+
brightRed: "#44ff44",
|
|
188
|
+
brightGreen: "#55ff77",
|
|
189
|
+
brightYellow: "#88ff88",
|
|
190
|
+
brightBlue: "#33dd88",
|
|
191
|
+
brightMagenta: "#44ffaa",
|
|
192
|
+
brightCyan: "#66ffcc",
|
|
193
|
+
brightWhite: "#ccffcc",
|
|
194
|
+
brightBlack: "#006600"
|
|
195
|
+
},
|
|
196
|
+
// Canonical macOS triad — same rationale as amber. Three slightly-
|
|
197
|
+
// different greens were unreadable as window controls.
|
|
198
|
+
buttons: {
|
|
199
|
+
close: "#ff5f57",
|
|
200
|
+
minimize: "#ffbd2e",
|
|
201
|
+
maximize: "#28ca42"
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// src/themes/cyberpunk.ts
|
|
206
|
+
var cyberpunk = {
|
|
207
|
+
name: "cyberpunk",
|
|
208
|
+
colors: {
|
|
209
|
+
text: "#e0e0ff",
|
|
210
|
+
comment: "#6060a0",
|
|
211
|
+
background: "#0a0014",
|
|
212
|
+
titleBarBackground: "#150028",
|
|
213
|
+
// Switched from canonical hot-pink #ff0080 to body text color. Saturated
|
|
214
|
+
// hot-pink on deep violet passes WCAG AA (~5.24:1) but visually buzzes
|
|
215
|
+
// (perceptual vibration on the saturated boundary). Body color reads as
|
|
216
|
+
// part of the chrome instead of a glow accent.
|
|
217
|
+
titleBarText: "#e0e0ff",
|
|
218
|
+
prompt: "#ff0080",
|
|
219
|
+
cursor: "#00ffff",
|
|
220
|
+
red: "#ff0055",
|
|
221
|
+
green: "#00ff88",
|
|
222
|
+
yellow: "#ffff00",
|
|
223
|
+
blue: "#0088ff",
|
|
224
|
+
magenta: "#ff00ff",
|
|
225
|
+
cyan: "#00ffff",
|
|
226
|
+
white: "#ffffff",
|
|
227
|
+
orange: "#ff8800",
|
|
228
|
+
purple: "#aa00ff",
|
|
229
|
+
pink: "#ff0080",
|
|
230
|
+
brightRed: "#ff3377",
|
|
231
|
+
brightGreen: "#33ffaa",
|
|
232
|
+
brightYellow: "#ffff55",
|
|
233
|
+
brightBlue: "#33aaff",
|
|
234
|
+
brightMagenta: "#ff55ff",
|
|
235
|
+
brightCyan: "#55ffff",
|
|
236
|
+
brightWhite: "#ffffff",
|
|
237
|
+
brightBlack: "#404060"
|
|
238
|
+
},
|
|
239
|
+
buttons: {
|
|
240
|
+
close: "#ff0055",
|
|
241
|
+
minimize: "#ffff00",
|
|
242
|
+
maximize: "#00ff88"
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// src/themes/solarized-dark.ts
|
|
247
|
+
var solarizedDark = {
|
|
248
|
+
name: "solarized-dark",
|
|
249
|
+
colors: {
|
|
250
|
+
text: "#839496",
|
|
251
|
+
// Solarized's canonical base01 (#586e75) fails WCAG AA on base03 by a
|
|
252
|
+
// wide margin (2.79:1; need 4.5). Lifted to a slightly brighter base01-ish
|
|
253
|
+
// value that clears AA while staying in the Solarized hue family.
|
|
254
|
+
comment: "#7d9499",
|
|
255
|
+
background: "#002b36",
|
|
256
|
+
titleBarBackground: "#073642",
|
|
257
|
+
titleBarText: "#93a1a1",
|
|
258
|
+
// Solarized's canonical blue (#268bd2) also fails AA on base03 (4.08:1).
|
|
259
|
+
// Lifted to a brighter Solarized-family blue.
|
|
260
|
+
prompt: "#4eb3e8",
|
|
261
|
+
cursor: "#2aa198",
|
|
262
|
+
red: "#dc322f",
|
|
263
|
+
green: "#859900",
|
|
264
|
+
yellow: "#b58900",
|
|
265
|
+
blue: "#268bd2",
|
|
266
|
+
magenta: "#d33682",
|
|
267
|
+
cyan: "#2aa198",
|
|
268
|
+
white: "#eee8d5",
|
|
269
|
+
orange: "#cb4b16",
|
|
270
|
+
purple: "#6c71c4",
|
|
271
|
+
pink: "#d33682",
|
|
272
|
+
brightRed: "#cb4b16",
|
|
273
|
+
brightGreen: "#859900",
|
|
274
|
+
brightYellow: "#b58900",
|
|
275
|
+
brightBlue: "#268bd2",
|
|
276
|
+
brightMagenta: "#6c71c4",
|
|
277
|
+
brightCyan: "#2aa198",
|
|
278
|
+
brightWhite: "#fdf6e3",
|
|
279
|
+
brightBlack: "#657b83"
|
|
280
|
+
},
|
|
281
|
+
buttons: {
|
|
282
|
+
close: "#dc322f",
|
|
283
|
+
minimize: "#b58900",
|
|
284
|
+
maximize: "#859900"
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// src/themes/win95.ts
|
|
289
|
+
var win95 = {
|
|
290
|
+
name: "win95",
|
|
291
|
+
colors: {
|
|
292
|
+
text: "#c0c0c0",
|
|
293
|
+
comment: "#808080",
|
|
294
|
+
background: "#000000",
|
|
295
|
+
titleBarBackground: "#000080",
|
|
296
|
+
titleBarText: "#ffffff",
|
|
297
|
+
prompt: "#c0c0c0",
|
|
298
|
+
cursor: "#ffffff",
|
|
299
|
+
red: "#ff0000",
|
|
300
|
+
green: "#00ff00",
|
|
301
|
+
yellow: "#ffff00",
|
|
302
|
+
blue: "#0000ff",
|
|
303
|
+
magenta: "#ff00ff",
|
|
304
|
+
cyan: "#00ffff",
|
|
305
|
+
white: "#ffffff",
|
|
306
|
+
orange: "#ff8800",
|
|
307
|
+
purple: "#8800ff",
|
|
308
|
+
pink: "#ff00ff",
|
|
309
|
+
brightRed: "#ff5555",
|
|
310
|
+
brightGreen: "#55ff55",
|
|
311
|
+
brightYellow: "#ffff55",
|
|
312
|
+
brightBlue: "#5555ff",
|
|
313
|
+
brightMagenta: "#ff55ff",
|
|
314
|
+
brightCyan: "#55ffff",
|
|
315
|
+
brightWhite: "#ffffff",
|
|
316
|
+
brightBlack: "#555555"
|
|
317
|
+
},
|
|
318
|
+
buttons: {
|
|
319
|
+
close: "#c0c0c0",
|
|
320
|
+
minimize: "#c0c0c0",
|
|
321
|
+
maximize: "#c0c0c0"
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// src/themes/catppuccin.ts
|
|
326
|
+
var catppuccin = {
|
|
327
|
+
name: "catppuccin",
|
|
328
|
+
colors: {
|
|
329
|
+
text: "#cdd6f4",
|
|
330
|
+
// Text
|
|
331
|
+
comment: "#9399b2",
|
|
332
|
+
// Overlay2 — lifted from canonical Surface2 (#585b70 = 3.0:1 on base) to clear AA
|
|
333
|
+
background: "#1e1e2e",
|
|
334
|
+
// Base
|
|
335
|
+
titleBarBackground: "#181825",
|
|
336
|
+
// Mantle
|
|
337
|
+
titleBarText: "#cdd6f4",
|
|
338
|
+
// Text
|
|
339
|
+
prompt: "#a6e3a1",
|
|
340
|
+
// Green
|
|
341
|
+
cursor: "#f5e0dc",
|
|
342
|
+
// Rosewater
|
|
343
|
+
red: "#f38ba8",
|
|
344
|
+
// Red
|
|
345
|
+
green: "#a6e3a1",
|
|
346
|
+
// Green
|
|
347
|
+
yellow: "#f9e2af",
|
|
348
|
+
// Yellow
|
|
349
|
+
blue: "#89b4fa",
|
|
350
|
+
// Blue
|
|
351
|
+
magenta: "#cba6f7",
|
|
352
|
+
// Mauve
|
|
353
|
+
cyan: "#94e2d5",
|
|
354
|
+
// Teal
|
|
355
|
+
white: "#bac2de",
|
|
356
|
+
// Subtext1
|
|
357
|
+
orange: "#fab387",
|
|
358
|
+
// Peach
|
|
359
|
+
purple: "#cba6f7",
|
|
360
|
+
// Mauve
|
|
361
|
+
pink: "#f5c2e7",
|
|
362
|
+
// Pink
|
|
363
|
+
brightRed: "#f38ba8",
|
|
364
|
+
brightGreen: "#a6e3a1",
|
|
365
|
+
brightYellow: "#f9e2af",
|
|
366
|
+
brightBlue: "#89b4fa",
|
|
367
|
+
brightMagenta: "#cba6f7",
|
|
368
|
+
brightCyan: "#94e2d5",
|
|
369
|
+
brightWhite: "#a6adc8",
|
|
370
|
+
// Subtext0
|
|
371
|
+
brightBlack: "#585b70"
|
|
372
|
+
// Surface2
|
|
373
|
+
},
|
|
374
|
+
buttons: {
|
|
375
|
+
close: "#f38ba8",
|
|
376
|
+
minimize: "#f9e2af",
|
|
377
|
+
maximize: "#a6e3a1"
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/themes/tokyo-night.ts
|
|
382
|
+
var tokyoNight = {
|
|
383
|
+
name: "tokyo-night",
|
|
384
|
+
colors: {
|
|
385
|
+
text: "#c0caf5",
|
|
386
|
+
comment: "#7986b7",
|
|
387
|
+
// Lifted from canonical #565f89 (~3.5:1) to clear AA on bg
|
|
388
|
+
background: "#1a1b26",
|
|
389
|
+
titleBarBackground: "#16161e",
|
|
390
|
+
titleBarText: "#c0caf5",
|
|
391
|
+
prompt: "#9ece6a",
|
|
392
|
+
// green
|
|
393
|
+
cursor: "#7aa2f7",
|
|
394
|
+
// blue
|
|
395
|
+
red: "#f7768e",
|
|
396
|
+
green: "#9ece6a",
|
|
397
|
+
yellow: "#e0af68",
|
|
398
|
+
blue: "#7aa2f7",
|
|
399
|
+
magenta: "#bb9af7",
|
|
400
|
+
cyan: "#7dcfff",
|
|
401
|
+
white: "#a9b1d6",
|
|
402
|
+
orange: "#ff9e64",
|
|
403
|
+
purple: "#bb9af7",
|
|
404
|
+
pink: "#f7768e",
|
|
405
|
+
brightRed: "#f7768e",
|
|
406
|
+
brightGreen: "#9ece6a",
|
|
407
|
+
brightYellow: "#e0af68",
|
|
408
|
+
brightBlue: "#7aa2f7",
|
|
409
|
+
brightMagenta: "#bb9af7",
|
|
410
|
+
brightCyan: "#7dcfff",
|
|
411
|
+
brightWhite: "#c0caf5",
|
|
412
|
+
brightBlack: "#414868"
|
|
413
|
+
},
|
|
414
|
+
buttons: {
|
|
415
|
+
close: "#f7768e",
|
|
416
|
+
minimize: "#e0af68",
|
|
417
|
+
maximize: "#9ece6a"
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// src/themes/gruvbox.ts
|
|
422
|
+
var gruvbox = {
|
|
423
|
+
name: "gruvbox",
|
|
424
|
+
colors: {
|
|
425
|
+
text: "#ebdbb2",
|
|
426
|
+
// fg
|
|
427
|
+
comment: "#a89984",
|
|
428
|
+
// gray (canonical) — passes AA (~4.9:1) on bg0
|
|
429
|
+
background: "#282828",
|
|
430
|
+
// bg0
|
|
431
|
+
titleBarBackground: "#1d2021",
|
|
432
|
+
// bg0_h (hard variant — slightly darker chrome)
|
|
433
|
+
titleBarText: "#ebdbb2",
|
|
434
|
+
prompt: "#b8bb26",
|
|
435
|
+
// bright_green
|
|
436
|
+
cursor: "#fabd2f",
|
|
437
|
+
// bright_yellow
|
|
438
|
+
red: "#cc241d",
|
|
439
|
+
// red
|
|
440
|
+
green: "#98971a",
|
|
441
|
+
// green
|
|
442
|
+
yellow: "#d79921",
|
|
443
|
+
// yellow
|
|
444
|
+
blue: "#458588",
|
|
445
|
+
// blue
|
|
446
|
+
magenta: "#b16286",
|
|
447
|
+
// purple
|
|
448
|
+
cyan: "#689d6a",
|
|
449
|
+
// aqua
|
|
450
|
+
white: "#a89984",
|
|
451
|
+
// gray
|
|
452
|
+
orange: "#d65d0e",
|
|
453
|
+
// orange
|
|
454
|
+
purple: "#b16286",
|
|
455
|
+
// purple
|
|
456
|
+
pink: "#fb4934",
|
|
457
|
+
// bright_red (closest to pink in the palette)
|
|
458
|
+
brightRed: "#fb4934",
|
|
459
|
+
brightGreen: "#b8bb26",
|
|
460
|
+
brightYellow: "#fabd2f",
|
|
461
|
+
brightBlue: "#83a598",
|
|
462
|
+
brightMagenta: "#d3869b",
|
|
463
|
+
brightCyan: "#8ec07c",
|
|
464
|
+
brightWhite: "#ebdbb2",
|
|
465
|
+
brightBlack: "#928374"
|
|
466
|
+
},
|
|
467
|
+
buttons: {
|
|
468
|
+
close: "#cc241d",
|
|
469
|
+
minimize: "#d79921",
|
|
470
|
+
maximize: "#98971a"
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
// src/themes/high-contrast.ts
|
|
475
|
+
var highContrast = {
|
|
476
|
+
name: "high-contrast",
|
|
477
|
+
colors: {
|
|
478
|
+
text: "#ffffff",
|
|
479
|
+
comment: "#aaaaaa",
|
|
480
|
+
// 9.1:1 — clears AAA at the dim end
|
|
481
|
+
background: "#000000",
|
|
482
|
+
titleBarBackground: "#1a1a1a",
|
|
483
|
+
titleBarText: "#ffffff",
|
|
484
|
+
prompt: "#00ff00",
|
|
485
|
+
// 15.3:1
|
|
486
|
+
cursor: "#ffff00",
|
|
487
|
+
red: "#ff5555",
|
|
488
|
+
green: "#00ff00",
|
|
489
|
+
yellow: "#ffff00",
|
|
490
|
+
blue: "#aaaaff",
|
|
491
|
+
magenta: "#ff00ff",
|
|
492
|
+
cyan: "#00ffff",
|
|
493
|
+
white: "#ffffff",
|
|
494
|
+
orange: "#ffaa00",
|
|
495
|
+
purple: "#cc88ff",
|
|
496
|
+
pink: "#ff99cc",
|
|
497
|
+
brightRed: "#ff8888",
|
|
498
|
+
brightGreen: "#88ff88",
|
|
499
|
+
brightYellow: "#ffff88",
|
|
500
|
+
brightBlue: "#ccccff",
|
|
501
|
+
brightMagenta: "#ff88ff",
|
|
502
|
+
brightCyan: "#88ffff",
|
|
503
|
+
brightWhite: "#ffffff",
|
|
504
|
+
brightBlack: "#888888"
|
|
505
|
+
// 5.9:1 — explicitly the dimmest legible value
|
|
506
|
+
},
|
|
507
|
+
buttons: {
|
|
508
|
+
close: "#ff5555",
|
|
509
|
+
minimize: "#ffff00",
|
|
510
|
+
maximize: "#00ff00"
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// src/themes/index.ts
|
|
515
|
+
var themes = {
|
|
516
|
+
dracula,
|
|
517
|
+
nord,
|
|
518
|
+
monokai,
|
|
519
|
+
amber,
|
|
520
|
+
"green-phosphor": greenPhosphor,
|
|
521
|
+
cyberpunk,
|
|
522
|
+
"solarized-dark": solarizedDark,
|
|
523
|
+
win95,
|
|
524
|
+
catppuccin,
|
|
525
|
+
"tokyo-night": tokyoNight,
|
|
526
|
+
gruvbox,
|
|
527
|
+
"high-contrast": highContrast
|
|
528
|
+
};
|
|
529
|
+
var customThemes = /* @__PURE__ */ new Map();
|
|
530
|
+
function registerTheme(theme) {
|
|
531
|
+
if (theme.name === "random") {
|
|
532
|
+
throw new Error(
|
|
533
|
+
`"random" is a reserved theme name (it triggers the daily-rotation behavior). Pick a different name for your custom theme.`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
customThemes.set(theme.name, theme);
|
|
537
|
+
}
|
|
538
|
+
function getTheme(name) {
|
|
539
|
+
return customThemes.get(name) ?? themes[name];
|
|
540
|
+
}
|
|
541
|
+
function listThemes() {
|
|
542
|
+
const names = new Set(customThemes.keys());
|
|
543
|
+
for (const name of Object.keys(themes)) names.add(name);
|
|
544
|
+
return Array.from(names);
|
|
545
|
+
}
|
|
546
|
+
var THEME_NAMES = Object.keys(themes);
|
|
547
|
+
function resolveTheme(nameOrTheme) {
|
|
548
|
+
if (typeof nameOrTheme === "object") {
|
|
549
|
+
return nameOrTheme;
|
|
550
|
+
}
|
|
551
|
+
if (nameOrTheme === "random") {
|
|
552
|
+
const names = listThemes();
|
|
553
|
+
const dayOfYear = Math.floor(
|
|
554
|
+
(Date.now() - new Date((/* @__PURE__ */ new Date()).getFullYear(), 0, 0).getTime()) / 864e5
|
|
555
|
+
);
|
|
556
|
+
const idx = dayOfYear % names.length;
|
|
557
|
+
const selected = names[idx];
|
|
558
|
+
if (process.env.SVG_TERMINAL_VERBOSE) {
|
|
559
|
+
console.error(`[svg-terminal] Theme rotation: day ${dayOfYear} \u2192 ${selected}`);
|
|
560
|
+
}
|
|
561
|
+
return getTheme(selected);
|
|
562
|
+
}
|
|
563
|
+
const theme = getTheme(nameOrTheme);
|
|
564
|
+
if (!theme) {
|
|
565
|
+
const available = listThemes().join(", ");
|
|
566
|
+
throw new Error(`Unknown theme "${nameOrTheme}". Available: ${available}, random`);
|
|
567
|
+
}
|
|
568
|
+
return theme;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/blocks/registry.ts
|
|
572
|
+
var registry = /* @__PURE__ */ new Map();
|
|
573
|
+
function registerBlock(block) {
|
|
574
|
+
registry.set(block.name, block);
|
|
575
|
+
}
|
|
576
|
+
function getBlock(name) {
|
|
577
|
+
return registry.get(name);
|
|
578
|
+
}
|
|
579
|
+
function listBlocks() {
|
|
580
|
+
return Array.from(registry.keys());
|
|
581
|
+
}
|
|
582
|
+
function registerBlocks(blocks) {
|
|
583
|
+
for (const block of blocks) {
|
|
584
|
+
registerBlock(block);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/core/defaults.ts
|
|
589
|
+
var DEFAULT_WINDOW = {
|
|
590
|
+
width: 1e3,
|
|
591
|
+
height: 560,
|
|
592
|
+
borderRadius: 12,
|
|
593
|
+
titleBarHeight: 40,
|
|
594
|
+
title: "user@terminal:~",
|
|
595
|
+
style: "macos",
|
|
596
|
+
autoHeight: false,
|
|
597
|
+
minHeight: 300,
|
|
598
|
+
maxHeight: 1200
|
|
599
|
+
};
|
|
600
|
+
var DEFAULT_TERMINAL = {
|
|
601
|
+
fontFamily: "JetBrains Mono, Fira Code, Ubuntu Mono, Consolas, Monaco, monospace",
|
|
602
|
+
fontSize: 14,
|
|
603
|
+
lineHeight: 1.8,
|
|
604
|
+
padding: 12,
|
|
605
|
+
paddingTop: 14,
|
|
606
|
+
prompt: "user@host:~$ "
|
|
607
|
+
};
|
|
608
|
+
var DEFAULT_EFFECTS = {
|
|
609
|
+
textGlow: false,
|
|
610
|
+
shadow: true,
|
|
611
|
+
scanlines: true,
|
|
612
|
+
vignette: false
|
|
613
|
+
};
|
|
614
|
+
var DEFAULT_ANIMATION = {
|
|
615
|
+
cursorBlinkCycle: 1e3,
|
|
616
|
+
charAppearDuration: 10,
|
|
617
|
+
outputLineStagger: 50,
|
|
618
|
+
commandOutputPause: 300,
|
|
619
|
+
scrollDelay: 10,
|
|
620
|
+
outputEndPause: 200,
|
|
621
|
+
defaultTypingDuration: 2e3,
|
|
622
|
+
defaultSequencePause: 1e3,
|
|
623
|
+
loop: true
|
|
624
|
+
};
|
|
625
|
+
var DEFAULT_ACCESSIBILITY = {
|
|
626
|
+
describe: true
|
|
627
|
+
};
|
|
628
|
+
var DEFAULT_CHROME = {
|
|
629
|
+
titleFontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
|
|
630
|
+
titleFontSize: 13,
|
|
631
|
+
buttonRadius: 6,
|
|
632
|
+
buttonSpacing: 20,
|
|
633
|
+
dimOpacity: 0.6,
|
|
634
|
+
buttonY: 16
|
|
635
|
+
};
|
|
636
|
+
var DEFAULT_CONFIG = {
|
|
637
|
+
window: DEFAULT_WINDOW,
|
|
638
|
+
text: DEFAULT_TERMINAL,
|
|
639
|
+
theme: dracula,
|
|
640
|
+
effects: DEFAULT_EFFECTS,
|
|
641
|
+
animation: DEFAULT_ANIMATION,
|
|
642
|
+
chrome: DEFAULT_CHROME,
|
|
643
|
+
accessibility: DEFAULT_ACCESSIBILITY,
|
|
644
|
+
maxDuration: 90,
|
|
645
|
+
scrollDuration: 100,
|
|
646
|
+
fetchTimeout: 1e4,
|
|
647
|
+
cacheTTL: 86400,
|
|
648
|
+
cachePath: ".svg-terminal-cache.json"
|
|
649
|
+
};
|
|
650
|
+
var CHAR_WIDTH_RATIO = 0.6;
|
|
651
|
+
function getMaxColumns(windowWidth, fontSize, padding) {
|
|
652
|
+
const contentWidth = windowWidth - padding * 2;
|
|
653
|
+
const charWidth = fontSize * CHAR_WIDTH_RATIO;
|
|
654
|
+
return Math.floor(contentWidth / charWidth);
|
|
655
|
+
}
|
|
656
|
+
function getDefaultBoxWidth(maxCols) {
|
|
657
|
+
return Math.min(maxCols - 4, 90);
|
|
658
|
+
}
|
|
659
|
+
var CURSOR_Y_OFFSET_RATIO = -0.85;
|
|
660
|
+
var SHADOW_PARAMS = { dy: 6, blur: 8, opacity: 0.55 };
|
|
661
|
+
var SCANLINE_PARAMS = { height: 2, opacity: 0.02 };
|
|
662
|
+
var SCROLL_ANIM_DURATION = 100;
|
|
663
|
+
var TYPING_PRESETS = {
|
|
664
|
+
instant: 400,
|
|
665
|
+
fast: 500,
|
|
666
|
+
quick: 800,
|
|
667
|
+
medium: 1200,
|
|
668
|
+
standard: 1400,
|
|
669
|
+
slow: 1800,
|
|
670
|
+
long: 2200
|
|
671
|
+
};
|
|
672
|
+
var PAUSE_PRESETS = {
|
|
673
|
+
minimal: 300,
|
|
674
|
+
short: 500,
|
|
675
|
+
quick: 600,
|
|
676
|
+
medium: 800,
|
|
677
|
+
standard: 1e3,
|
|
678
|
+
long: 1400,
|
|
679
|
+
dramatic: 1800,
|
|
680
|
+
showcase: 2800
|
|
681
|
+
};
|
|
682
|
+
function resolveTyping(value) {
|
|
683
|
+
if (typeof value === "number") return value;
|
|
684
|
+
if (typeof value === "string") return TYPING_PRESETS[value] ?? 1200;
|
|
685
|
+
return 1200;
|
|
686
|
+
}
|
|
687
|
+
function resolvePause(value) {
|
|
688
|
+
if (typeof value === "number") return value;
|
|
689
|
+
if (typeof value === "string") return PAUSE_PRESETS[value] ?? 1e3;
|
|
690
|
+
return 1e3;
|
|
691
|
+
}
|
|
692
|
+
function resolveBoxWidth(configWidth, context) {
|
|
693
|
+
if (configWidth !== void 0) return configWidth;
|
|
694
|
+
const { width } = context.config.window;
|
|
695
|
+
const { fontSize, padding } = context.config.text;
|
|
696
|
+
return getDefaultBoxWidth(getMaxColumns(width, fontSize, padding));
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/core/schema.ts
|
|
700
|
+
import { z } from "zod";
|
|
701
|
+
var HEX_COLOR_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
702
|
+
var THEME_COLOR_NAMES = /* @__PURE__ */ new Set([
|
|
703
|
+
"red",
|
|
704
|
+
"green",
|
|
705
|
+
"yellow",
|
|
706
|
+
"blue",
|
|
707
|
+
"magenta",
|
|
708
|
+
"cyan",
|
|
709
|
+
"white",
|
|
710
|
+
"orange",
|
|
711
|
+
"purple",
|
|
712
|
+
"pink",
|
|
713
|
+
"brightRed",
|
|
714
|
+
"brightGreen",
|
|
715
|
+
"brightYellow",
|
|
716
|
+
"brightBlue",
|
|
717
|
+
"brightMagenta",
|
|
718
|
+
"brightCyan",
|
|
719
|
+
"brightWhite",
|
|
720
|
+
"brightBlack",
|
|
721
|
+
"text",
|
|
722
|
+
"comment",
|
|
723
|
+
"background",
|
|
724
|
+
"prompt",
|
|
725
|
+
"cursor",
|
|
726
|
+
"titleBarBackground",
|
|
727
|
+
"titleBarText"
|
|
728
|
+
]);
|
|
729
|
+
var ColorRefSchema = z.string().refine(
|
|
730
|
+
(v) => HEX_COLOR_RE.test(v) || THEME_COLOR_NAMES.has(v),
|
|
731
|
+
{ message: 'color must be a hex string (#abc / #aabbcc) or a theme palette name (e.g. "cyan", "comment")' }
|
|
732
|
+
);
|
|
733
|
+
var FontFamilySchema = z.string().refine(
|
|
734
|
+
(v) => /^[A-Za-z0-9 ,'"\-_]+$/.test(v) && !/[<>;{}]/.test(v),
|
|
735
|
+
{ message: "fontFamily must contain only letters, digits, spaces, commas, quotes, hyphens, underscores" }
|
|
736
|
+
);
|
|
737
|
+
var BlockEntrySchema = z.object({
|
|
738
|
+
block: z.string().min(1, "Block name is required"),
|
|
739
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
740
|
+
command: z.string().optional(),
|
|
741
|
+
color: ColorRefSchema.optional(),
|
|
742
|
+
typing: z.string().optional(),
|
|
743
|
+
pause: z.string().optional()
|
|
744
|
+
});
|
|
745
|
+
var InlineThemeColorsSchema = z.object({
|
|
746
|
+
text: ColorRefSchema,
|
|
747
|
+
comment: ColorRefSchema,
|
|
748
|
+
background: ColorRefSchema,
|
|
749
|
+
titleBarBackground: ColorRefSchema,
|
|
750
|
+
titleBarText: ColorRefSchema,
|
|
751
|
+
prompt: ColorRefSchema,
|
|
752
|
+
cursor: ColorRefSchema,
|
|
753
|
+
red: ColorRefSchema,
|
|
754
|
+
green: ColorRefSchema,
|
|
755
|
+
yellow: ColorRefSchema,
|
|
756
|
+
blue: ColorRefSchema,
|
|
757
|
+
magenta: ColorRefSchema,
|
|
758
|
+
cyan: ColorRefSchema,
|
|
759
|
+
white: ColorRefSchema,
|
|
760
|
+
orange: ColorRefSchema,
|
|
761
|
+
purple: ColorRefSchema,
|
|
762
|
+
pink: ColorRefSchema,
|
|
763
|
+
brightRed: ColorRefSchema,
|
|
764
|
+
brightGreen: ColorRefSchema,
|
|
765
|
+
brightYellow: ColorRefSchema,
|
|
766
|
+
brightBlue: ColorRefSchema,
|
|
767
|
+
brightMagenta: ColorRefSchema,
|
|
768
|
+
brightCyan: ColorRefSchema,
|
|
769
|
+
brightWhite: ColorRefSchema,
|
|
770
|
+
brightBlack: ColorRefSchema
|
|
771
|
+
}).strict();
|
|
772
|
+
var InlineThemeSchema = z.object({
|
|
773
|
+
name: z.string().min(1),
|
|
774
|
+
colors: InlineThemeColorsSchema,
|
|
775
|
+
buttons: z.object({
|
|
776
|
+
close: ColorRefSchema,
|
|
777
|
+
minimize: ColorRefSchema,
|
|
778
|
+
maximize: ColorRefSchema
|
|
779
|
+
}).strict()
|
|
780
|
+
}).strict();
|
|
781
|
+
var WindowSchema = z.object({
|
|
782
|
+
width: z.number().positive("width must be positive").optional(),
|
|
783
|
+
height: z.number().positive("height must be positive").optional(),
|
|
784
|
+
borderRadius: z.number().min(0).optional(),
|
|
785
|
+
titleBarHeight: z.number().positive().optional(),
|
|
786
|
+
title: z.string().optional(),
|
|
787
|
+
style: z.enum(["macos", "win95", "floating", "minimal", "none"]).optional(),
|
|
788
|
+
autoHeight: z.boolean().optional(),
|
|
789
|
+
minHeight: z.number().positive().optional(),
|
|
790
|
+
maxHeight: z.number().positive().optional()
|
|
791
|
+
}).strict().optional();
|
|
792
|
+
var TerminalSchema = z.object({
|
|
793
|
+
fontFamily: FontFamilySchema.optional(),
|
|
794
|
+
fontSize: z.number().positive("fontSize must be positive").optional(),
|
|
795
|
+
lineHeight: z.number().positive("lineHeight must be positive").optional(),
|
|
796
|
+
padding: z.number().min(0).optional(),
|
|
797
|
+
paddingTop: z.number().min(0).optional(),
|
|
798
|
+
prompt: z.string().optional()
|
|
799
|
+
}).strict().optional();
|
|
800
|
+
var EffectsSchema = z.object({
|
|
801
|
+
textGlow: z.boolean().optional(),
|
|
802
|
+
shadow: z.boolean().optional(),
|
|
803
|
+
scanlines: z.boolean().optional(),
|
|
804
|
+
vignette: z.boolean().optional()
|
|
805
|
+
}).strict().optional();
|
|
806
|
+
var AnimationSchema = z.object({
|
|
807
|
+
cursorBlinkCycle: z.number().positive("cursorBlinkCycle must be positive").optional(),
|
|
808
|
+
charAppearDuration: z.number().positive("charAppearDuration must be positive").optional(),
|
|
809
|
+
outputLineStagger: z.number().min(0).optional(),
|
|
810
|
+
commandOutputPause: z.number().min(0).optional(),
|
|
811
|
+
scrollDelay: z.number().min(0).optional(),
|
|
812
|
+
outputEndPause: z.number().min(0).optional(),
|
|
813
|
+
defaultTypingDuration: z.number().positive().optional(),
|
|
814
|
+
defaultSequencePause: z.number().min(0).optional(),
|
|
815
|
+
loop: z.union([z.boolean(), z.number().int().positive()]).optional()
|
|
816
|
+
}).strict().optional();
|
|
817
|
+
var AccessibilitySchema = z.object({
|
|
818
|
+
describe: z.boolean().optional()
|
|
819
|
+
}).strict().optional();
|
|
820
|
+
var ChromeSchema = z.object({
|
|
821
|
+
titleFontFamily: FontFamilySchema.optional(),
|
|
822
|
+
titleFontSize: z.number().positive("titleFontSize must be positive").optional(),
|
|
823
|
+
buttonRadius: z.number().min(0).optional(),
|
|
824
|
+
buttonSpacing: z.number().positive().optional(),
|
|
825
|
+
dimOpacity: z.number().min(0).max(1, "dimOpacity must be between 0 and 1").optional(),
|
|
826
|
+
buttonY: z.number().min(0).optional()
|
|
827
|
+
}).strict().optional();
|
|
828
|
+
var UserConfigSchema = z.object({
|
|
829
|
+
theme: z.union([z.string(), InlineThemeSchema]).optional(),
|
|
830
|
+
window: WindowSchema,
|
|
831
|
+
terminal: TerminalSchema,
|
|
832
|
+
effects: EffectsSchema,
|
|
833
|
+
animation: AnimationSchema,
|
|
834
|
+
chrome: ChromeSchema,
|
|
835
|
+
accessibility: AccessibilitySchema,
|
|
836
|
+
blocks: z.array(BlockEntrySchema).min(1, "At least one block is required"),
|
|
837
|
+
variables: z.record(z.string(), z.unknown()).optional(),
|
|
838
|
+
maxDuration: z.number().positive().optional(),
|
|
839
|
+
scrollDuration: z.number().positive().optional(),
|
|
840
|
+
accessibilityLabel: z.string().optional(),
|
|
841
|
+
fetchTimeout: z.number().positive().optional(),
|
|
842
|
+
cacheTTL: z.number().int().min(0, "cacheTTL must be \u2265 0").optional(),
|
|
843
|
+
cachePath: z.string().min(1).optional()
|
|
844
|
+
}).strict();
|
|
845
|
+
function validateConfig(raw) {
|
|
846
|
+
return UserConfigSchema.parse(raw);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/core/errors.ts
|
|
850
|
+
var ConfigError = class extends Error {
|
|
851
|
+
/** Pre-formatted multi-line message ready for stderr. */
|
|
852
|
+
formatted;
|
|
853
|
+
constructor(formatted) {
|
|
854
|
+
super(formatted);
|
|
855
|
+
this.name = "ConfigError";
|
|
856
|
+
this.formatted = formatted;
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
var BlockConfigError = class extends Error {
|
|
860
|
+
formatted;
|
|
861
|
+
blockName;
|
|
862
|
+
entryIndex;
|
|
863
|
+
constructor(blockName, entryIndex, formatted) {
|
|
864
|
+
super(formatted);
|
|
865
|
+
this.name = "BlockConfigError";
|
|
866
|
+
this.formatted = formatted;
|
|
867
|
+
this.blockName = blockName;
|
|
868
|
+
this.entryIndex = entryIndex;
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
// src/core/config.ts
|
|
873
|
+
async function loadConfig(filePath) {
|
|
874
|
+
let raw;
|
|
875
|
+
try {
|
|
876
|
+
raw = readFileSync(filePath, "utf-8");
|
|
877
|
+
} catch (err) {
|
|
878
|
+
throw new ConfigError(`Cannot read config file: ${filePath}
|
|
879
|
+
${err.message}`);
|
|
880
|
+
}
|
|
881
|
+
const { default: yaml } = await import("js-yaml");
|
|
882
|
+
let parsed;
|
|
883
|
+
try {
|
|
884
|
+
parsed = yaml.load(raw);
|
|
885
|
+
} catch (err) {
|
|
886
|
+
const e = err;
|
|
887
|
+
const where = e.mark ? `:${e.mark.line + 1}:${e.mark.column + 1}` : "";
|
|
888
|
+
throw new ConfigError(`YAML parse error in ${filePath}${where}
|
|
889
|
+
${e.message}`);
|
|
890
|
+
}
|
|
891
|
+
let config;
|
|
892
|
+
try {
|
|
893
|
+
config = validateConfig(parsed);
|
|
894
|
+
} catch (err) {
|
|
895
|
+
if (err instanceof z2.ZodError) {
|
|
896
|
+
const issues = err.issues.map((i) => {
|
|
897
|
+
const path = i.path.length ? i.path.join(".") : "<root>";
|
|
898
|
+
return ` ${path}: ${i.message}`;
|
|
899
|
+
}).join("\n");
|
|
900
|
+
throw new ConfigError(`Invalid config in ${filePath}:
|
|
901
|
+
${issues}${hintForIssues(err)}`);
|
|
902
|
+
}
|
|
903
|
+
throw err;
|
|
904
|
+
}
|
|
905
|
+
validateNames(config, filePath);
|
|
906
|
+
return config;
|
|
907
|
+
}
|
|
908
|
+
function hintForIssues(err) {
|
|
909
|
+
const hints = [];
|
|
910
|
+
for (const issue of err.issues) {
|
|
911
|
+
if (issue.path[0] === "accessibility" && issue.path.length === 1) {
|
|
912
|
+
hints.push(" hint: to disable accessibility descriptions, use `accessibility: { describe: false }` (not a boolean).");
|
|
913
|
+
}
|
|
914
|
+
if (issue.code === "unrecognized_keys") {
|
|
915
|
+
const pathKey = issue.path.length === 0 ? "<root>" : issue.path.join(".");
|
|
916
|
+
const known = KNOWN_KEYS[pathKey];
|
|
917
|
+
const unknown = issue.keys ?? [];
|
|
918
|
+
if (known) {
|
|
919
|
+
for (const key of unknown) {
|
|
920
|
+
const suggestion = closestKey(key, known);
|
|
921
|
+
if (suggestion) {
|
|
922
|
+
const where = pathKey === "<root>" ? "" : ` at ${pathKey}`;
|
|
923
|
+
hints.push(` hint: unknown key "${key}"${where} \u2014 did you mean "${suggestion}"?`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return hints.length > 0 ? "\n" + hints.join("\n") : "";
|
|
930
|
+
}
|
|
931
|
+
var KNOWN_KEYS = {
|
|
932
|
+
"<root>": ["theme", "window", "terminal", "effects", "animation", "chrome", "accessibility", "blocks", "variables", "maxDuration", "scrollDuration", "accessibilityLabel", "fetchTimeout", "cacheTTL", "cachePath"],
|
|
933
|
+
window: ["width", "height", "borderRadius", "titleBarHeight", "title", "style", "autoHeight", "minHeight", "maxHeight"],
|
|
934
|
+
terminal: ["fontFamily", "fontSize", "lineHeight", "padding", "paddingTop", "prompt"],
|
|
935
|
+
effects: ["textGlow", "shadow", "scanlines", "vignette"],
|
|
936
|
+
animation: ["cursorBlinkCycle", "charAppearDuration", "outputLineStagger", "commandOutputPause", "scrollDelay", "outputEndPause", "defaultTypingDuration", "defaultSequencePause", "loop"],
|
|
937
|
+
chrome: ["titleFontFamily", "titleFontSize", "buttonRadius", "buttonSpacing", "dimOpacity", "buttonY"],
|
|
938
|
+
accessibility: ["describe"]
|
|
939
|
+
};
|
|
940
|
+
function closestKey(input, candidates) {
|
|
941
|
+
const maxDist = input.length <= 3 ? 1 : 2;
|
|
942
|
+
let best = null;
|
|
943
|
+
for (const c of candidates) {
|
|
944
|
+
const d = levenshtein(input.toLowerCase(), c.toLowerCase());
|
|
945
|
+
if (d <= maxDist && (best === null || d < best.dist)) {
|
|
946
|
+
best = { name: c, dist: d };
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return best?.name ?? null;
|
|
950
|
+
}
|
|
951
|
+
function levenshtein(a, b) {
|
|
952
|
+
if (a === b) return 0;
|
|
953
|
+
if (a.length === 0) return b.length;
|
|
954
|
+
if (b.length === 0) return a.length;
|
|
955
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
956
|
+
let curr = new Array(b.length + 1);
|
|
957
|
+
for (let i = 1; i <= a.length; i++) {
|
|
958
|
+
curr[0] = i;
|
|
959
|
+
for (let j = 1; j <= b.length; j++) {
|
|
960
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
961
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
962
|
+
}
|
|
963
|
+
[prev, curr] = [curr, prev];
|
|
964
|
+
}
|
|
965
|
+
return prev[b.length];
|
|
966
|
+
}
|
|
967
|
+
function validateNames(config, filePath) {
|
|
968
|
+
if (typeof config.theme === "string") {
|
|
969
|
+
if (config.theme !== "random" && !getTheme(config.theme)) {
|
|
970
|
+
const available = listThemes().join(", ");
|
|
971
|
+
throw new ConfigError(
|
|
972
|
+
`Unknown theme "${config.theme}" in ${filePath}
|
|
973
|
+
Available: ${available}, random`
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
} else if (config.theme && typeof config.theme === "object" && config.theme.name === "random") {
|
|
977
|
+
throw new ConfigError(
|
|
978
|
+
`Inline theme uses the reserved name "random" in ${filePath}
|
|
979
|
+
"random" triggers daily rotation; pick a different name for your inline theme.`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
for (let i = 0; i < config.blocks.length; i++) {
|
|
983
|
+
const entry = config.blocks[i];
|
|
984
|
+
if (!entry) continue;
|
|
985
|
+
if (!getBlock(entry.block)) {
|
|
986
|
+
throw new ConfigError(
|
|
987
|
+
`Unknown block "${entry.block}" at blocks[${i}] in ${filePath}
|
|
988
|
+
Run "svg-terminal blocks" to list available block types.`
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function mergeConfig(userConfig) {
|
|
994
|
+
const theme = resolveTheme(userConfig.theme ?? "dracula");
|
|
995
|
+
const isWin95 = theme.name === "win95";
|
|
996
|
+
const autoStyle = isWin95 && !userConfig.window?.style ? "win95" : void 0;
|
|
997
|
+
const autoTitleBarHeight = isWin95 && userConfig.window?.titleBarHeight === void 0 ? 22 : void 0;
|
|
998
|
+
const isCrt = theme.name === "amber" || theme.name === "green-phosphor" || theme.name === "cyberpunk";
|
|
999
|
+
const userVignetteSet = userConfig.effects?.vignette !== void 0;
|
|
1000
|
+
const autoEffects = isWin95 && !userConfig.effects ? { textGlow: false, scanlines: false, shadow: true } : isCrt && !userVignetteSet ? { vignette: true } : void 0;
|
|
1001
|
+
return {
|
|
1002
|
+
window: {
|
|
1003
|
+
...DEFAULT_WINDOW,
|
|
1004
|
+
...userConfig.window,
|
|
1005
|
+
...autoStyle ? { style: autoStyle } : {},
|
|
1006
|
+
...autoTitleBarHeight !== void 0 ? { titleBarHeight: autoTitleBarHeight } : {}
|
|
1007
|
+
},
|
|
1008
|
+
text: {
|
|
1009
|
+
...DEFAULT_TERMINAL,
|
|
1010
|
+
...userConfig.terminal
|
|
1011
|
+
},
|
|
1012
|
+
theme,
|
|
1013
|
+
effects: {
|
|
1014
|
+
...DEFAULT_EFFECTS,
|
|
1015
|
+
...autoEffects ?? {},
|
|
1016
|
+
...userConfig.effects
|
|
1017
|
+
},
|
|
1018
|
+
animation: {
|
|
1019
|
+
...DEFAULT_ANIMATION,
|
|
1020
|
+
...userConfig.animation
|
|
1021
|
+
},
|
|
1022
|
+
chrome: {
|
|
1023
|
+
...DEFAULT_CHROME,
|
|
1024
|
+
...userConfig.chrome
|
|
1025
|
+
},
|
|
1026
|
+
accessibility: {
|
|
1027
|
+
...DEFAULT_ACCESSIBILITY,
|
|
1028
|
+
...userConfig.accessibility
|
|
1029
|
+
},
|
|
1030
|
+
maxDuration: userConfig.maxDuration ?? DEFAULT_CONFIG.maxDuration,
|
|
1031
|
+
scrollDuration: userConfig.scrollDuration ?? DEFAULT_CONFIG.scrollDuration,
|
|
1032
|
+
fetchTimeout: userConfig.fetchTimeout ?? DEFAULT_CONFIG.fetchTimeout,
|
|
1033
|
+
cacheTTL: userConfig.cacheTTL ?? DEFAULT_CONFIG.cacheTTL,
|
|
1034
|
+
cachePath: userConfig.cachePath ?? DEFAULT_CONFIG.cachePath,
|
|
1035
|
+
// Optional aria-label override (#97). Unset means the auto-generated
|
|
1036
|
+
// command-summary / line-count label wins downstream.
|
|
1037
|
+
accessibilityLabel: userConfig.accessibilityLabel
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// src/core/effects.ts
|
|
1042
|
+
function generateDefs(effects, windowStyle) {
|
|
1043
|
+
const parts = [];
|
|
1044
|
+
if (effects.scanlines) {
|
|
1045
|
+
parts.push(`
|
|
1046
|
+
<pattern id="scanlines" patternUnits="userSpaceOnUse" width="1" height="${SCANLINE_PARAMS.height}">
|
|
1047
|
+
<rect width="1" height="1" fill="transparent"/>
|
|
1048
|
+
<rect y="1" width="1" height="1" fill="rgba(255,255,255,${SCANLINE_PARAMS.opacity})"/>
|
|
1049
|
+
</pattern>`);
|
|
1050
|
+
}
|
|
1051
|
+
if (windowStyle === "win95") {
|
|
1052
|
+
parts.push(`
|
|
1053
|
+
<linearGradient id="win95Caption" x1="0" y1="0" x2="1" y2="0">
|
|
1054
|
+
<stop offset="0" stop-color="#000080"/>
|
|
1055
|
+
<stop offset="1" stop-color="#1084d0"/>
|
|
1056
|
+
</linearGradient>`);
|
|
1057
|
+
}
|
|
1058
|
+
if (effects.vignette) {
|
|
1059
|
+
parts.push(`
|
|
1060
|
+
<radialGradient id="vignette" cx="50%" cy="50%" r="75%">
|
|
1061
|
+
<stop offset="0%" stop-color="black" stop-opacity="0"/>
|
|
1062
|
+
<stop offset="60%" stop-color="black" stop-opacity="0"/>
|
|
1063
|
+
<stop offset="100%" stop-color="black" stop-opacity="0.25"/>
|
|
1064
|
+
</radialGradient>`);
|
|
1065
|
+
}
|
|
1066
|
+
return parts.join("\n");
|
|
1067
|
+
}
|
|
1068
|
+
function generateFilters(effects, _glowColor) {
|
|
1069
|
+
const parts = [];
|
|
1070
|
+
if (effects.textGlow) {
|
|
1071
|
+
parts.push(`
|
|
1072
|
+
<filter id="textGlow" x="-10%" y="-10%" width="120%" height="120%">
|
|
1073
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation="0.6" result="core"/>
|
|
1074
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation="2" result="halo"/>
|
|
1075
|
+
<feMerge>
|
|
1076
|
+
<feMergeNode in="halo"/>
|
|
1077
|
+
<feMergeNode in="core"/>
|
|
1078
|
+
<feMergeNode in="SourceGraphic"/>
|
|
1079
|
+
</feMerge>
|
|
1080
|
+
</filter>`);
|
|
1081
|
+
}
|
|
1082
|
+
if (effects.shadow) {
|
|
1083
|
+
parts.push(`
|
|
1084
|
+
<filter id="shadow" x="-8%" y="-8%" width="116%" height="120%">
|
|
1085
|
+
<feGaussianBlur in="SourceAlpha" stdDeviation="${SHADOW_PARAMS.blur}"/>
|
|
1086
|
+
<feOffset dx="0" dy="${SHADOW_PARAMS.dy}" result="offsetblur"/>
|
|
1087
|
+
<feFlood flood-color="#000000" flood-opacity="${SHADOW_PARAMS.opacity}"/>
|
|
1088
|
+
<feComposite in2="offsetblur" operator="in"/>
|
|
1089
|
+
<feMerge>
|
|
1090
|
+
<feMergeNode/>
|
|
1091
|
+
<feMergeNode in="SourceGraphic"/>
|
|
1092
|
+
</feMerge>
|
|
1093
|
+
</filter>`);
|
|
1094
|
+
}
|
|
1095
|
+
return parts.join("\n");
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/core/markup-parser.ts
|
|
1099
|
+
function buildColorMap(colors) {
|
|
1100
|
+
return {
|
|
1101
|
+
// Standard terminal colors — black uses theme background
|
|
1102
|
+
black: colors.background,
|
|
1103
|
+
red: colors.red,
|
|
1104
|
+
green: colors.green,
|
|
1105
|
+
yellow: colors.yellow,
|
|
1106
|
+
blue: colors.blue,
|
|
1107
|
+
magenta: colors.magenta,
|
|
1108
|
+
cyan: colors.cyan,
|
|
1109
|
+
white: colors.white,
|
|
1110
|
+
// Bright variants
|
|
1111
|
+
bright_black: colors.brightBlack,
|
|
1112
|
+
bright_red: colors.brightRed,
|
|
1113
|
+
bright_green: colors.brightGreen,
|
|
1114
|
+
bright_yellow: colors.brightYellow,
|
|
1115
|
+
bright_blue: colors.brightBlue,
|
|
1116
|
+
bright_magenta: colors.brightMagenta,
|
|
1117
|
+
bright_cyan: colors.brightCyan,
|
|
1118
|
+
bright_white: colors.brightWhite,
|
|
1119
|
+
// Extended
|
|
1120
|
+
purple: colors.purple,
|
|
1121
|
+
pink: colors.pink,
|
|
1122
|
+
orange: colors.orange,
|
|
1123
|
+
comment: colors.comment,
|
|
1124
|
+
neon_green: colors.prompt,
|
|
1125
|
+
matrix_green: colors.cursor
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
var HEX_COLOR_RE2 = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
1129
|
+
function resolveColor(colorName, colorMap, fallback) {
|
|
1130
|
+
if (colorName.startsWith("#")) {
|
|
1131
|
+
return HEX_COLOR_RE2.test(colorName) ? colorName : fallback;
|
|
1132
|
+
}
|
|
1133
|
+
return colorMap[colorName.toLowerCase()] ?? fallback;
|
|
1134
|
+
}
|
|
1135
|
+
function parseMarkup(text, colorMap, fallbackColor) {
|
|
1136
|
+
if (!text) {
|
|
1137
|
+
return [{ text: "", fg: null, bg: null, bold: false, dim: false }];
|
|
1138
|
+
}
|
|
1139
|
+
if (!text.includes("[[")) {
|
|
1140
|
+
return [{ text, fg: null, bg: null, bold: false, dim: false }];
|
|
1141
|
+
}
|
|
1142
|
+
const spans = [];
|
|
1143
|
+
let pos = 0;
|
|
1144
|
+
let currentStyle = { fg: null, bg: null, bold: false, dim: false };
|
|
1145
|
+
const styleStack = [];
|
|
1146
|
+
while (pos < text.length) {
|
|
1147
|
+
const openStart = text.indexOf("[[", pos);
|
|
1148
|
+
if (openStart === -1) {
|
|
1149
|
+
const remaining = text.slice(pos);
|
|
1150
|
+
if (remaining) spans.push({ text: remaining, ...currentStyle });
|
|
1151
|
+
break;
|
|
1152
|
+
}
|
|
1153
|
+
if (openStart > pos) {
|
|
1154
|
+
spans.push({ text: text.slice(pos, openStart), ...currentStyle });
|
|
1155
|
+
}
|
|
1156
|
+
const openEnd = text.indexOf("]]", openStart);
|
|
1157
|
+
if (openEnd === -1) {
|
|
1158
|
+
spans.push({ text: text.slice(openStart), ...currentStyle });
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
const tag = text.slice(openStart + 2, openEnd);
|
|
1162
|
+
pos = openEnd + 2;
|
|
1163
|
+
if (tag.startsWith("/")) {
|
|
1164
|
+
if (styleStack.length > 0) {
|
|
1165
|
+
currentStyle = { ...styleStack.pop() };
|
|
1166
|
+
}
|
|
1167
|
+
} else {
|
|
1168
|
+
styleStack.push({ ...currentStyle });
|
|
1169
|
+
if (tag === "bold") {
|
|
1170
|
+
currentStyle.bold = true;
|
|
1171
|
+
} else if (tag === "dim") {
|
|
1172
|
+
currentStyle.dim = true;
|
|
1173
|
+
} else if (tag.startsWith("fg:")) {
|
|
1174
|
+
currentStyle.fg = resolveColor(tag.slice(3), colorMap, fallbackColor);
|
|
1175
|
+
} else if (tag.startsWith("bg:")) {
|
|
1176
|
+
currentStyle.bg = resolveColor(tag.slice(3), colorMap, fallbackColor);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return mergeSpans(spans);
|
|
1181
|
+
}
|
|
1182
|
+
function mergeSpans(spans) {
|
|
1183
|
+
if (spans.length <= 1) return spans;
|
|
1184
|
+
const merged = [];
|
|
1185
|
+
let current = { ...spans[0] };
|
|
1186
|
+
for (let i = 1; i < spans.length; i++) {
|
|
1187
|
+
const next = spans[i];
|
|
1188
|
+
if (current.fg === next.fg && current.bg === next.bg && current.bold === next.bold && current.dim === next.dim) {
|
|
1189
|
+
current.text += next.text;
|
|
1190
|
+
} else {
|
|
1191
|
+
if (current.text) merged.push(current);
|
|
1192
|
+
current = { ...next };
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (current.text) merged.push(current);
|
|
1196
|
+
return merged;
|
|
1197
|
+
}
|
|
1198
|
+
function hasMarkup(text) {
|
|
1199
|
+
return typeof text === "string" && text.includes("[[");
|
|
1200
|
+
}
|
|
1201
|
+
function stripMarkup(text) {
|
|
1202
|
+
if (!text) return "";
|
|
1203
|
+
return text.replace(/\[\[[^\]]{1,256}\]\]/g, "");
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// src/core/xml.ts
|
|
1207
|
+
function escapeXml(text) {
|
|
1208
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1209
|
+
}
|
|
1210
|
+
function roundCoord(value, decimals = 0) {
|
|
1211
|
+
if (decimals === 0) return Math.round(value);
|
|
1212
|
+
const factor = 10 ** decimals;
|
|
1213
|
+
return Math.round(value * factor) / factor;
|
|
1214
|
+
}
|
|
1215
|
+
function getTextWidth(text, fontSize) {
|
|
1216
|
+
return roundCoord(text.length * (fontSize * 0.6));
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/core/line-renderer.ts
|
|
1220
|
+
function setHold(attr, value, untilMs) {
|
|
1221
|
+
if (untilMs <= 0) return "";
|
|
1222
|
+
return `<set attributeName="${attr}" to="${value}" begin="0s" end="${untilMs}ms"/>`;
|
|
1223
|
+
}
|
|
1224
|
+
function fadeInStyle(startTimeMs) {
|
|
1225
|
+
return ` class="fade-in" style="animation-delay: ${startTimeMs}ms"`;
|
|
1226
|
+
}
|
|
1227
|
+
function roundLineY(n) {
|
|
1228
|
+
return +n.toFixed(1);
|
|
1229
|
+
}
|
|
1230
|
+
function generateStyledText(spans, defaultColor, dimOpacity) {
|
|
1231
|
+
return spans.map((span) => {
|
|
1232
|
+
const color = span.fg ?? defaultColor;
|
|
1233
|
+
const attrs = [`fill="${escapeXml(color)}"`];
|
|
1234
|
+
if (span.bold) attrs.push('font-weight="bold"');
|
|
1235
|
+
if (span.dim) attrs.push(`opacity="${dimOpacity}"`);
|
|
1236
|
+
return `<tspan ${attrs.join(" ")}>${escapeXml(span.text)}</tspan>`;
|
|
1237
|
+
}).join("");
|
|
1238
|
+
}
|
|
1239
|
+
function generateCursor(prompt, command, startTime, typingDuration, terminal, cursorColor, _cursorBlinkCycle, charAppearDuration) {
|
|
1240
|
+
const promptWidth = getTextWidth(prompt, terminal.fontSize);
|
|
1241
|
+
const charWidth = roundCoord(terminal.fontSize * CHAR_WIDTH_RATIO);
|
|
1242
|
+
const cursorY = roundCoord(terminal.fontSize * CURSOR_Y_OFFSET_RATIO);
|
|
1243
|
+
const typingEndTime = startTime + typingDuration;
|
|
1244
|
+
const moveAnim = command.length > 0 ? buildCursorWalk(command.length, promptWidth, charWidth, startTime, typingDuration) : "";
|
|
1245
|
+
return `
|
|
1246
|
+
<rect x="${promptWidth}" y="${cursorY}" width="${charWidth}" height="${terminal.fontSize}"
|
|
1247
|
+
fill="${escapeXml(cursorColor)}" opacity="0">
|
|
1248
|
+
<animate attributeName="opacity" from="0" to="1" begin="${startTime}ms" dur="${charAppearDuration}ms" fill="freeze"/>
|
|
1249
|
+
<animate attributeName="opacity" to="0" begin="${typingEndTime}ms" dur="${charAppearDuration}ms" fill="freeze"/>
|
|
1250
|
+
${moveAnim}
|
|
1251
|
+
</rect>`;
|
|
1252
|
+
}
|
|
1253
|
+
function buildCursorWalk(charCount, promptWidth, charWidth, startTime, typingDuration) {
|
|
1254
|
+
const values = [];
|
|
1255
|
+
const keyTimes = [];
|
|
1256
|
+
for (let i = 0; i <= charCount; i++) {
|
|
1257
|
+
const col = Math.max(0, i - 1);
|
|
1258
|
+
values.push(String(roundCoord(promptWidth + col * charWidth)));
|
|
1259
|
+
keyTimes.push((i / charCount).toFixed(4));
|
|
1260
|
+
}
|
|
1261
|
+
return `<animate attributeName="x" values="${values.join(";")}" keyTimes="${keyTimes.join(";")}" calcMode="discrete" begin="${startTime}ms" dur="${typingDuration}ms" fill="freeze"/>`;
|
|
1262
|
+
}
|
|
1263
|
+
function generateCommandLine(lineIndex, y, prompt, command, startTime, typingDuration, terminal, promptColor, cursorColor, cursorBlinkCycle, charAppearDuration) {
|
|
1264
|
+
const promptWidth = getTextWidth(prompt, terminal.fontSize);
|
|
1265
|
+
const charWidth = roundCoord(terminal.fontSize * CHAR_WIDTH_RATIO);
|
|
1266
|
+
const clipId = `cmdrev-${lineIndex}`;
|
|
1267
|
+
const revealClip = command.length > 0 ? buildRevealClip(clipId, promptWidth, charWidth, command.length, terminal.fontSize, startTime, typingDuration) : "";
|
|
1268
|
+
const cmdWidth = command.length * charWidth;
|
|
1269
|
+
return `
|
|
1270
|
+
<g transform="translate(0, ${y})">
|
|
1271
|
+
${revealClip}
|
|
1272
|
+
<text class="tt fade-in" style="animation-delay: ${startTime}ms" fill="${escapeXml(promptColor)}" textLength="${promptWidth}" lengthAdjust="spacingAndGlyphs">${escapeXml(prompt)}</text>
|
|
1273
|
+
<text class="tt" x="${promptWidth}" fill="${escapeXml(promptColor)}"${command.length > 0 ? ` textLength="${cmdWidth}" lengthAdjust="spacingAndGlyphs" clip-path="url(#${clipId})"` : ""}>${escapeXml(command)}</text>
|
|
1274
|
+
${generateCursor(prompt, command, startTime, typingDuration, terminal, cursorColor, cursorBlinkCycle, charAppearDuration)}
|
|
1275
|
+
</g>`;
|
|
1276
|
+
}
|
|
1277
|
+
function buildRevealClip(clipId, startX, charWidth, charCount, fontSize, startTime, typingDuration) {
|
|
1278
|
+
const values = [];
|
|
1279
|
+
const keyTimes = [];
|
|
1280
|
+
for (let i = 0; i <= charCount; i++) {
|
|
1281
|
+
values.push(String(roundCoord(i * charWidth)));
|
|
1282
|
+
keyTimes.push((i / charCount).toFixed(4));
|
|
1283
|
+
}
|
|
1284
|
+
const finalWidth = roundCoord(charCount * charWidth);
|
|
1285
|
+
return `<defs><clipPath id="${clipId}"><rect x="${startX}" y="${-fontSize}" width="${finalWidth}" height="${fontSize * 2}">${setHold("width", 0, startTime)}<animate attributeName="width" values="${values.join(";")}" keyTimes="${keyTimes.join(";")}" calcMode="discrete" begin="${startTime}ms" dur="${typingDuration}ms" fill="freeze"/></rect></clipPath></defs>`;
|
|
1286
|
+
}
|
|
1287
|
+
function generateAnimatedOutputLine(y, frames, color, startTime, colorMap, chrome, fps, loop) {
|
|
1288
|
+
const n = frames.length;
|
|
1289
|
+
const frameDurMs = 1e3 / fps;
|
|
1290
|
+
const cycleMs = Math.round(n * frameDurMs);
|
|
1291
|
+
const iter = loop ? "infinite" : "1";
|
|
1292
|
+
const textElements = frames.map((frame, i) => {
|
|
1293
|
+
const styled = hasMarkup(frame);
|
|
1294
|
+
const textContent = styled ? generateStyledText(parseMarkup(frame, colorMap, color), color, chrome.dimOpacity) : escapeXml(frame);
|
|
1295
|
+
const textFill = styled ? "" : ` fill="${escapeXml(color)}"`;
|
|
1296
|
+
const delayMs = Math.round(i * frameDurMs);
|
|
1297
|
+
const anim = `animation: frame-cycle-${n} ${cycleMs}ms linear ${delayMs}ms ${iter}`;
|
|
1298
|
+
return `<text class="tt frame-cycle-${n}"${textFill} opacity="${i === 0 ? "1" : "0"}" style="${anim}">${textContent}</text>`;
|
|
1299
|
+
}).join("");
|
|
1300
|
+
return `
|
|
1301
|
+
<g transform="translate(0, ${y})"${fadeInStyle(startTime)}>
|
|
1302
|
+
${textElements}
|
|
1303
|
+
</g>`;
|
|
1304
|
+
}
|
|
1305
|
+
function generateOutputLine(y, content, color, startTime, colorMap, chrome, pinWidth, fontSize) {
|
|
1306
|
+
const styled = hasMarkup(content);
|
|
1307
|
+
const textContent = styled ? generateStyledText(parseMarkup(content, colorMap, color), color, chrome.dimOpacity) : escapeXml(content);
|
|
1308
|
+
const textFill = styled ? "" : ` fill="${escapeXml(color)}"`;
|
|
1309
|
+
const pin = pinWidth && !styled && content.length > 0 ? ` textLength="${content.length * roundCoord(fontSize * CHAR_WIDTH_RATIO)}" lengthAdjust="spacingAndGlyphs"` : "";
|
|
1310
|
+
return `
|
|
1311
|
+
<g transform="translate(0, ${y})"${fadeInStyle(startTime)}>
|
|
1312
|
+
<text class="tt"${textFill}${pin}>
|
|
1313
|
+
${textContent}
|
|
1314
|
+
</text>
|
|
1315
|
+
</g>`;
|
|
1316
|
+
}
|
|
1317
|
+
function generateAllLines(frames, terminal, lineHeight, colors, chrome, animation) {
|
|
1318
|
+
const chromeConfig = chrome ?? DEFAULT_CHROME;
|
|
1319
|
+
const animConfig = animation ?? DEFAULT_ANIMATION;
|
|
1320
|
+
const colorMap = buildColorMap(colors);
|
|
1321
|
+
const processedLines = /* @__PURE__ */ new Map();
|
|
1322
|
+
for (const frame of frames) {
|
|
1323
|
+
if (frame.type === "add-command" && frame.lineIndex !== void 0) {
|
|
1324
|
+
const y = roundLineY(frame.lineIndex * lineHeight);
|
|
1325
|
+
processedLines.set(
|
|
1326
|
+
frame.lineIndex,
|
|
1327
|
+
generateCommandLine(
|
|
1328
|
+
frame.lineIndex,
|
|
1329
|
+
y,
|
|
1330
|
+
frame.prompt ?? terminal.prompt,
|
|
1331
|
+
frame.command ?? "",
|
|
1332
|
+
frame.time,
|
|
1333
|
+
frame.typingDuration ?? animConfig.defaultTypingDuration,
|
|
1334
|
+
terminal,
|
|
1335
|
+
colors.prompt,
|
|
1336
|
+
colors.cursor,
|
|
1337
|
+
animConfig.cursorBlinkCycle,
|
|
1338
|
+
animConfig.charAppearDuration
|
|
1339
|
+
)
|
|
1340
|
+
);
|
|
1341
|
+
} else if (frame.type === "add-output" && frame.lineIndex !== void 0) {
|
|
1342
|
+
const y = roundLineY(frame.lineIndex * lineHeight);
|
|
1343
|
+
if (frame.frames && frame.frames.length > 0) {
|
|
1344
|
+
processedLines.set(
|
|
1345
|
+
frame.lineIndex,
|
|
1346
|
+
generateAnimatedOutputLine(
|
|
1347
|
+
y,
|
|
1348
|
+
frame.frames,
|
|
1349
|
+
frame.color ?? colors.text,
|
|
1350
|
+
frame.time,
|
|
1351
|
+
colorMap,
|
|
1352
|
+
chromeConfig,
|
|
1353
|
+
frame.framesFps ?? 4,
|
|
1354
|
+
frame.framesLoop ?? true
|
|
1355
|
+
)
|
|
1356
|
+
);
|
|
1357
|
+
} else {
|
|
1358
|
+
processedLines.set(
|
|
1359
|
+
frame.lineIndex,
|
|
1360
|
+
generateOutputLine(
|
|
1361
|
+
y,
|
|
1362
|
+
frame.content ?? "",
|
|
1363
|
+
frame.color ?? colors.text,
|
|
1364
|
+
frame.time,
|
|
1365
|
+
colorMap,
|
|
1366
|
+
chromeConfig,
|
|
1367
|
+
frame.pinWidth ?? false,
|
|
1368
|
+
terminal.fontSize
|
|
1369
|
+
)
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return Array.from(processedLines.entries()).sort((a, b) => a[0] - b[0]).map(([, content]) => content).join("\n");
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// src/core/svg-generator.ts
|
|
1378
|
+
function countTotalLines(sequences) {
|
|
1379
|
+
let total = 0;
|
|
1380
|
+
for (const seq of sequences) {
|
|
1381
|
+
if (seq.type === "command") {
|
|
1382
|
+
total += 1;
|
|
1383
|
+
} else {
|
|
1384
|
+
total += seq.content.split("\n").length;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return total;
|
|
1388
|
+
}
|
|
1389
|
+
function calculateAutoHeight(sequences, window, terminal) {
|
|
1390
|
+
const lineHeight = terminal.fontSize * terminal.lineHeight;
|
|
1391
|
+
const totalLines = countTotalLines(sequences);
|
|
1392
|
+
const contentHeight = totalLines * lineHeight;
|
|
1393
|
+
const chromeHeight = window.titleBarHeight + terminal.paddingTop + terminal.padding * 2;
|
|
1394
|
+
const calculated = Math.ceil(contentHeight + chromeHeight);
|
|
1395
|
+
return Math.max(window.minHeight, Math.min(window.maxHeight, calculated));
|
|
1396
|
+
}
|
|
1397
|
+
function generateSvg(sequences, config) {
|
|
1398
|
+
const { text: terminal, theme, effects, animation, chrome } = config;
|
|
1399
|
+
const window = { ...config.window };
|
|
1400
|
+
if (window.autoHeight) {
|
|
1401
|
+
window.height = calculateAutoHeight(sequences, window, terminal);
|
|
1402
|
+
}
|
|
1403
|
+
const lineHeight = terminal.fontSize * terminal.lineHeight;
|
|
1404
|
+
const titleBarHeight = getTitleBarHeight(window);
|
|
1405
|
+
const topPadding = terminal.paddingTop;
|
|
1406
|
+
const viewportHeight = window.height - titleBarHeight - topPadding - terminal.padding;
|
|
1407
|
+
const maxVisibleLines = Math.floor(viewportHeight / lineHeight);
|
|
1408
|
+
const maxDurationMs = config.maxDuration * 1e3;
|
|
1409
|
+
const { frames, totalDuration } = createAnimationFrames(sequences, terminal, maxVisibleLines, config.scrollDuration, animation);
|
|
1410
|
+
if (totalDuration > maxDurationMs) {
|
|
1411
|
+
console.warn(`[svg-terminal] Animation duration (${(totalDuration / 1e3).toFixed(1)}s) exceeds maxDuration (${config.maxDuration}s)`);
|
|
1412
|
+
}
|
|
1413
|
+
const accessibilityLabel = config.accessibilityLabel ?? buildAccessibilityLabel(sequences);
|
|
1414
|
+
const showShadow = effects.shadow && window.style !== "none";
|
|
1415
|
+
const a11yChildren = renderAccessibilityChildren(accessibilityLabel, sequences, terminal.prompt, config.accessibility);
|
|
1416
|
+
const frameCounts = /* @__PURE__ */ new Set();
|
|
1417
|
+
for (const seq of sequences) {
|
|
1418
|
+
if (seq.type === "output" && seq.frames && seq.frames.length > 1) {
|
|
1419
|
+
frameCounts.add(seq.frames.length);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
const frameCycleKeyframes = [...frameCounts].sort((a, b) => a - b).map((n) => {
|
|
1423
|
+
const slot = (100 / n).toFixed(4);
|
|
1424
|
+
return ` @keyframes frame-cycle-${n} { 0%, ${slot}% { opacity: 1; } ${(parseFloat(slot) + 0.01).toFixed(4)}%, 100% { opacity: 0; } }`;
|
|
1425
|
+
}).join("\n");
|
|
1426
|
+
return `<svg width="${window.width}" height="${window.height}" viewBox="0 0 ${window.width} ${window.height}" xmlns="http://www.w3.org/2000/svg"
|
|
1427
|
+
role="img" aria-label="${escapeXml(accessibilityLabel)}">${a11yChildren}
|
|
1428
|
+
<style>
|
|
1429
|
+
.tt { font-family: ${escapeXml(terminal.fontFamily)}; font-size: ${terminal.fontSize}px; white-space: pre; }
|
|
1430
|
+
@keyframes scanlineScroll {
|
|
1431
|
+
from { transform: translateY(0); }
|
|
1432
|
+
to { transform: translateY(4px); }
|
|
1433
|
+
}
|
|
1434
|
+
.scanline-overlay { animation: scanlineScroll 1.2s linear infinite; }
|
|
1435
|
+
/* fadeIn drives all opacity fade-ins (prompt + output lines + animated frame
|
|
1436
|
+
wrappers). Lives in CSS rather than SMIL so the @media block below kills
|
|
1437
|
+
it under prefers-reduced-motion. fill-mode: backwards keeps the element
|
|
1438
|
+
at opacity 0 during the animation-delay window so it doesn't pop in
|
|
1439
|
+
before its scheduled time. */
|
|
1440
|
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
1441
|
+
.fade-in { animation: fadeIn 10ms linear backwards; }
|
|
1442
|
+
/* frame-cycle-N (closes #69): one rule per unique frame count across the
|
|
1443
|
+
animated blocks in this render. Each frame text element carries the
|
|
1444
|
+
class + an inline animation-delay = i*frameDur. The static opacity
|
|
1445
|
+
attribute (frame 0 = 1, others = 0) is the SMIL-stripped AND
|
|
1446
|
+
reduced-motion fallback \u2014 frame 0 visible, rest invisible. Under
|
|
1447
|
+
prefers-reduced-motion the @media block kills animation-duration so
|
|
1448
|
+
the underlying opacity attribute applies. */
|
|
1449
|
+
${frameCycleKeyframes}
|
|
1450
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1451
|
+
*, *::before, *::after {
|
|
1452
|
+
animation-duration: 0.01ms !important;
|
|
1453
|
+
animation-iteration-count: 1 !important;
|
|
1454
|
+
transition-duration: 0.01ms !important;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
</style>
|
|
1458
|
+
<defs>
|
|
1459
|
+
${generateDefs(effects, window.style)}
|
|
1460
|
+
${generateFilters(effects, theme.colors.cursor)}
|
|
1461
|
+
<clipPath id="terminalViewport">
|
|
1462
|
+
<rect x="0" y="${getTitleBarHeight(window)}" width="${window.width}" height="${window.height - getTitleBarHeight(window)}"/>
|
|
1463
|
+
</clipPath>
|
|
1464
|
+
</defs>
|
|
1465
|
+
|
|
1466
|
+
<g${showShadow ? ' filter="url(#shadow)"' : ""}>
|
|
1467
|
+
${renderWindow(window, theme)}
|
|
1468
|
+
${renderTitleBarForStyle(window, terminal, theme, chrome)}
|
|
1469
|
+
${renderTerminalContent(window, terminal, theme, effects, chrome, animation, frames, lineHeight)}
|
|
1470
|
+
${renderVignetteOverlay(effects, window)}
|
|
1471
|
+
${renderScanlineOverlay(effects, window)}
|
|
1472
|
+
</g>
|
|
1473
|
+
</svg>`;
|
|
1474
|
+
}
|
|
1475
|
+
function renderVignetteOverlay(effects, window) {
|
|
1476
|
+
if (!effects.vignette) return "";
|
|
1477
|
+
const titleBarHeight = getTitleBarHeight(window);
|
|
1478
|
+
const contentHeight = window.height - titleBarHeight;
|
|
1479
|
+
return `<rect x="0" y="${titleBarHeight}" width="${window.width}" height="${contentHeight}" fill="url(#vignette)" pointer-events="none"/>`;
|
|
1480
|
+
}
|
|
1481
|
+
function renderScanlineOverlay(effects, window, animated = true) {
|
|
1482
|
+
if (!effects.scanlines) return "";
|
|
1483
|
+
const titleBarHeight = getTitleBarHeight(window);
|
|
1484
|
+
const contentHeight = window.height - titleBarHeight;
|
|
1485
|
+
const classAttr = animated ? ' class="scanline-overlay"' : "";
|
|
1486
|
+
return `<rect x="0" y="${titleBarHeight}" width="${window.width}" height="${contentHeight}" fill="url(#scanlines)" pointer-events="none" opacity="0.5"${classAttr}/>`;
|
|
1487
|
+
}
|
|
1488
|
+
function buildAccessibilityLabel(sequences) {
|
|
1489
|
+
const commands = sequences.filter((s) => s.type === "command").map((s) => s.content).slice(0, 5);
|
|
1490
|
+
if (commands.length === 0) return "Animated terminal";
|
|
1491
|
+
return `Animated terminal showing: ${commands.join(", ")}`;
|
|
1492
|
+
}
|
|
1493
|
+
function renderAccessibilityChildren(label, sequences, prompt, a11y) {
|
|
1494
|
+
const title = `
|
|
1495
|
+
<title>${escapeXml(label)}</title>`;
|
|
1496
|
+
if (!a11y.describe) return title;
|
|
1497
|
+
const lines = [];
|
|
1498
|
+
for (const seq of sequences) {
|
|
1499
|
+
if (seq.type === "command") {
|
|
1500
|
+
lines.push(`${prompt}${seq.content}`);
|
|
1501
|
+
} else if (seq.content) {
|
|
1502
|
+
for (const line of seq.content.split("\n")) {
|
|
1503
|
+
const stripped = stripMarkup(line);
|
|
1504
|
+
if (isBoxDrawingOnly(stripped)) continue;
|
|
1505
|
+
lines.push(stripBoxFraming(stripped));
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (lines.length === 0) return title;
|
|
1510
|
+
return `${title}
|
|
1511
|
+
<desc>${escapeXml(lines.join("\n"))}</desc>`;
|
|
1512
|
+
}
|
|
1513
|
+
function isBoxDrawingOnly(line) {
|
|
1514
|
+
if (line.length === 0) return false;
|
|
1515
|
+
return /^[─-╿\s]+$/.test(line);
|
|
1516
|
+
}
|
|
1517
|
+
function stripBoxFraming(line) {
|
|
1518
|
+
return line.replace(/^[─-╿\s]+|[─-╿\s]+$/g, "");
|
|
1519
|
+
}
|
|
1520
|
+
function renderStaticAccessibilityChildren(label, lines, a11y) {
|
|
1521
|
+
const title = `
|
|
1522
|
+
<title>${escapeXml(label)}</title>`;
|
|
1523
|
+
if (!a11y.describe || lines.length === 0) return title;
|
|
1524
|
+
const stripped = lines.map(stripMarkup).filter((line) => !isBoxDrawingOnly(line)).map(stripBoxFraming).join("\n");
|
|
1525
|
+
if (stripped.length === 0) return title;
|
|
1526
|
+
return `${title}
|
|
1527
|
+
<desc>${escapeXml(stripped)}</desc>`;
|
|
1528
|
+
}
|
|
1529
|
+
function getTitleBarHeight(window) {
|
|
1530
|
+
if (window.style === "none" || window.style === "floating" || window.style === "minimal") {
|
|
1531
|
+
return 0;
|
|
1532
|
+
}
|
|
1533
|
+
return window.titleBarHeight;
|
|
1534
|
+
}
|
|
1535
|
+
function renderWin95TitleBar(window) {
|
|
1536
|
+
const h = window.titleBarHeight;
|
|
1537
|
+
const w = window.width;
|
|
1538
|
+
const silver = "#c0c0c0";
|
|
1539
|
+
const darkGray = "#808080";
|
|
1540
|
+
const white = "#ffffff";
|
|
1541
|
+
const black = "#000000";
|
|
1542
|
+
const btnW = Math.max(12, h - 6);
|
|
1543
|
+
const btnH = Math.round(btnW * 14 / 16);
|
|
1544
|
+
const btnY = (h - btnH) / 2;
|
|
1545
|
+
const btnGap = 2;
|
|
1546
|
+
const btnsTotal = btnW * 3 + btnGap * 2;
|
|
1547
|
+
const btnsX = w - btnsTotal - 4;
|
|
1548
|
+
const captionTextY = (h + 11) / 2;
|
|
1549
|
+
return `
|
|
1550
|
+
<!-- Win95 title bar -->
|
|
1551
|
+
<rect x="0" y="0" width="${w}" height="${h}" fill="${silver}"/>
|
|
1552
|
+
<!-- 3D raised border: top/left white, bottom/right dark -->
|
|
1553
|
+
<line x1="0" y1="0" x2="${w}" y2="0" stroke="${white}" stroke-width="2"/>
|
|
1554
|
+
<line x1="0" y1="0" x2="0" y2="${h}" stroke="${white}" stroke-width="2"/>
|
|
1555
|
+
<line x1="${w}" y1="0" x2="${w}" y2="${h}" stroke="${black}" stroke-width="1"/>
|
|
1556
|
+
<line x1="0" y1="${h}" x2="${w}" y2="${h}" stroke="${darkGray}" stroke-width="1"/>
|
|
1557
|
+
<!-- Blue caption (gradient defined in defs) -->
|
|
1558
|
+
<rect x="3" y="3" width="${w - 6 - btnsTotal - 6}" height="${h - 6}" fill="url(#win95Caption)"/>
|
|
1559
|
+
<!-- Caption text \u2014 sans-serif chrome font, not the terminal monospace -->
|
|
1560
|
+
<text x="8" y="${captionTextY}"
|
|
1561
|
+
font-family="Tahoma, 'MS Sans Serif', Geneva, sans-serif" font-size="11" font-weight="bold"
|
|
1562
|
+
fill="${white}">
|
|
1563
|
+
${escapeXml(window.title)}
|
|
1564
|
+
</text>
|
|
1565
|
+
<!-- Win95 buttons: minimize, maximize, close -->
|
|
1566
|
+
<g transform="translate(${btnsX}, ${btnY})">
|
|
1567
|
+
<rect x="0" y="0" width="${btnW}" height="${btnH}" fill="${silver}" stroke="${black}" stroke-width="1"/>
|
|
1568
|
+
<line x1="${btnW * 0.2}" y1="${btnH - 3}" x2="${btnW * 0.75}" y2="${btnH - 3}" stroke="${black}" stroke-width="1.5"/>
|
|
1569
|
+
<rect x="${btnW + btnGap}" y="0" width="${btnW}" height="${btnH}" fill="${silver}" stroke="${black}" stroke-width="1"/>
|
|
1570
|
+
<rect x="${btnW + btnGap + 3}" y="3" width="${btnW - 6}" height="${btnH - 6}" fill="none" stroke="${black}" stroke-width="1.2"/>
|
|
1571
|
+
<rect x="${(btnW + btnGap) * 2}" y="0" width="${btnW}" height="${btnH}" fill="${silver}" stroke="${black}" stroke-width="1"/>
|
|
1572
|
+
<line x1="${(btnW + btnGap) * 2 + 3}" y1="3" x2="${(btnW + btnGap) * 2 + btnW - 3}" y2="${btnH - 3}" stroke="${black}" stroke-width="1.2"/>
|
|
1573
|
+
<line x1="${(btnW + btnGap) * 2 + btnW - 3}" y1="3" x2="${(btnW + btnGap) * 2 + 3}" y2="${btnH - 3}" stroke="${black}" stroke-width="1.2"/>
|
|
1574
|
+
</g>`;
|
|
1575
|
+
}
|
|
1576
|
+
function renderTitleBarForStyle(window, terminal, theme, chrome) {
|
|
1577
|
+
if (window.style === "none" || window.style === "floating" || window.style === "minimal") {
|
|
1578
|
+
return "";
|
|
1579
|
+
}
|
|
1580
|
+
if (window.style === "win95") {
|
|
1581
|
+
return renderWin95TitleBar(window);
|
|
1582
|
+
}
|
|
1583
|
+
return renderTitleBar(window, terminal, theme, chrome);
|
|
1584
|
+
}
|
|
1585
|
+
function createAnimationFrames(sequences, terminal, maxVisibleLines, scrollDuration, anim) {
|
|
1586
|
+
let currentTime = 0;
|
|
1587
|
+
const frames = [];
|
|
1588
|
+
const buffer = [];
|
|
1589
|
+
let bufferStart = 0;
|
|
1590
|
+
for (const seq of sequences) {
|
|
1591
|
+
currentTime += seq.delay ?? 0;
|
|
1592
|
+
if (seq.type === "command") {
|
|
1593
|
+
buffer.push({ type: "command" });
|
|
1594
|
+
const typingDur = seq.typingDuration ?? anim.defaultTypingDuration;
|
|
1595
|
+
if (buffer.length - bufferStart > maxVisibleLines) {
|
|
1596
|
+
frames.push({
|
|
1597
|
+
time: currentTime,
|
|
1598
|
+
type: "scroll",
|
|
1599
|
+
scrollLines: 1,
|
|
1600
|
+
bufferStart: ++bufferStart
|
|
1601
|
+
});
|
|
1602
|
+
frames.push({
|
|
1603
|
+
time: currentTime + scrollDuration + anim.scrollDelay,
|
|
1604
|
+
type: "add-command",
|
|
1605
|
+
lineIndex: buffer.length - 1,
|
|
1606
|
+
prompt: seq.prompt ?? terminal.prompt,
|
|
1607
|
+
command: seq.content,
|
|
1608
|
+
typingDuration: typingDur
|
|
1609
|
+
});
|
|
1610
|
+
currentTime += scrollDuration + anim.scrollDelay;
|
|
1611
|
+
} else {
|
|
1612
|
+
frames.push({
|
|
1613
|
+
time: currentTime,
|
|
1614
|
+
type: "add-command",
|
|
1615
|
+
lineIndex: buffer.length - 1,
|
|
1616
|
+
prompt: seq.prompt ?? terminal.prompt,
|
|
1617
|
+
command: seq.content,
|
|
1618
|
+
typingDuration: typingDur
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
currentTime += typingDur;
|
|
1622
|
+
} else {
|
|
1623
|
+
if (!seq.content) {
|
|
1624
|
+
currentTime += seq.pause ?? anim.defaultSequencePause;
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
if (seq.frames && seq.frames.length > 0) {
|
|
1628
|
+
buffer.push({ type: "output" });
|
|
1629
|
+
frames.push({
|
|
1630
|
+
time: currentTime,
|
|
1631
|
+
type: "add-output",
|
|
1632
|
+
lineIndex: buffer.length - 1,
|
|
1633
|
+
content: seq.frames[0],
|
|
1634
|
+
// frame 0 acts as the static fallback
|
|
1635
|
+
color: seq.color,
|
|
1636
|
+
frames: seq.frames,
|
|
1637
|
+
framesFps: seq.framesFps,
|
|
1638
|
+
framesLoop: seq.framesLoop,
|
|
1639
|
+
pinWidth: seq.pinWidth
|
|
1640
|
+
});
|
|
1641
|
+
currentTime += anim.outputEndPause;
|
|
1642
|
+
} else {
|
|
1643
|
+
const lines = seq.content.split("\n");
|
|
1644
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1645
|
+
const baseTime = currentTime + i * anim.outputLineStagger;
|
|
1646
|
+
buffer.push({ type: "output" });
|
|
1647
|
+
if (buffer.length - bufferStart > maxVisibleLines) {
|
|
1648
|
+
frames.push({
|
|
1649
|
+
time: baseTime,
|
|
1650
|
+
type: "scroll",
|
|
1651
|
+
scrollLines: 1,
|
|
1652
|
+
bufferStart: ++bufferStart
|
|
1653
|
+
});
|
|
1654
|
+
frames.push({
|
|
1655
|
+
time: baseTime + scrollDuration + anim.scrollDelay,
|
|
1656
|
+
type: "add-output",
|
|
1657
|
+
lineIndex: buffer.length - 1,
|
|
1658
|
+
content: lines[i],
|
|
1659
|
+
color: seq.color,
|
|
1660
|
+
pinWidth: seq.pinWidth
|
|
1661
|
+
});
|
|
1662
|
+
} else {
|
|
1663
|
+
frames.push({
|
|
1664
|
+
time: baseTime,
|
|
1665
|
+
type: "add-output",
|
|
1666
|
+
lineIndex: buffer.length - 1,
|
|
1667
|
+
content: lines[i],
|
|
1668
|
+
color: seq.color,
|
|
1669
|
+
pinWidth: seq.pinWidth
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
currentTime += lines.length * anim.outputLineStagger + anim.outputEndPause;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
currentTime += seq.pause ?? anim.defaultSequencePause;
|
|
1677
|
+
}
|
|
1678
|
+
frames.push({ time: currentTime, type: "final" });
|
|
1679
|
+
return { frames, totalDuration: currentTime };
|
|
1680
|
+
}
|
|
1681
|
+
function renderWindow(window, theme) {
|
|
1682
|
+
const isSquare = window.style === "none" || window.style === "win95";
|
|
1683
|
+
const radius = isSquare ? 0 : window.borderRadius;
|
|
1684
|
+
const bg = `
|
|
1685
|
+
<rect x="0" y="0" width="${window.width}" height="${window.height}"
|
|
1686
|
+
rx="${radius}" ry="${radius}"
|
|
1687
|
+
fill="${escapeXml(theme.colors.background)}"/>`;
|
|
1688
|
+
if (window.style === "win95") {
|
|
1689
|
+
const w = window.width;
|
|
1690
|
+
const h = window.height;
|
|
1691
|
+
return `${bg}
|
|
1692
|
+
<line x1="0" y1="0" x2="${w}" y2="0" stroke="#ffffff" stroke-width="2"/>
|
|
1693
|
+
<line x1="0" y1="0" x2="0" y2="${h}" stroke="#ffffff" stroke-width="2"/>
|
|
1694
|
+
<line x1="${w - 1}" y1="0" x2="${w - 1}" y2="${h}" stroke="#000000" stroke-width="2"/>
|
|
1695
|
+
<line x1="0" y1="${h - 1}" x2="${w}" y2="${h - 1}" stroke="#000000" stroke-width="2"/>
|
|
1696
|
+
<line x1="1" y1="${h - 2}" x2="${w - 1}" y2="${h - 2}" stroke="#808080" stroke-width="1"/>
|
|
1697
|
+
<line x1="${w - 2}" y1="1" x2="${w - 2}" y2="${h - 1}" stroke="#808080" stroke-width="1"/>`;
|
|
1698
|
+
}
|
|
1699
|
+
return bg;
|
|
1700
|
+
}
|
|
1701
|
+
function renderTitleBar(window, terminal, theme, chrome) {
|
|
1702
|
+
const { buttonRadius: r, buttonSpacing: s, buttonY: y, titleFontSize, titleFontFamily } = chrome;
|
|
1703
|
+
return `
|
|
1704
|
+
<rect x="0" y="0" width="${window.width}" height="${window.titleBarHeight}"
|
|
1705
|
+
rx="${window.borderRadius}" ry="${window.borderRadius}"
|
|
1706
|
+
fill="${escapeXml(theme.colors.titleBarBackground)}"/>
|
|
1707
|
+
<rect x="0" y="${y}" width="${window.width}" height="${y}"
|
|
1708
|
+
fill="${escapeXml(theme.colors.titleBarBackground)}"/>
|
|
1709
|
+
<g id="window-controls">
|
|
1710
|
+
<circle cx="${terminal.padding + 2}" cy="${y}" r="${r}" fill="${escapeXml(theme.buttons.close)}" stroke="rgba(0,0,0,0.18)" stroke-width="0.5"/>
|
|
1711
|
+
<circle cx="${terminal.padding + 2 + s}" cy="${y}" r="${r}" fill="${escapeXml(theme.buttons.minimize)}" stroke="rgba(0,0,0,0.18)" stroke-width="0.5"/>
|
|
1712
|
+
<circle cx="${terminal.padding + 2 + s * 2}" cy="${y}" r="${r}" fill="${escapeXml(theme.buttons.maximize)}" stroke="rgba(0,0,0,0.18)" stroke-width="0.5"/>
|
|
1713
|
+
</g>
|
|
1714
|
+
<text x="${window.width / 2}" y="${(window.titleBarHeight + titleFontSize * 0.7) / 2}"
|
|
1715
|
+
font-family="${escapeXml(titleFontFamily)}" font-size="${titleFontSize}"
|
|
1716
|
+
fill="${escapeXml(theme.colors.titleBarText)}" text-anchor="middle">
|
|
1717
|
+
${escapeXml(window.title)}
|
|
1718
|
+
</text>`;
|
|
1719
|
+
}
|
|
1720
|
+
function renderTerminalContent(window, terminal, theme, effects, chrome, animation, frames, lineHeight) {
|
|
1721
|
+
const titleBarHeight = getTitleBarHeight(window);
|
|
1722
|
+
const contentY = titleBarHeight + terminal.paddingTop;
|
|
1723
|
+
const viewportHeight = window.height - titleBarHeight;
|
|
1724
|
+
const scrollAnimations = renderScrollAnimations(frames, terminal, window, lineHeight);
|
|
1725
|
+
const allLines = generateAllLines(frames, terminal, lineHeight, theme.colors, chrome, animation);
|
|
1726
|
+
const glow = effects.textGlow ? ' filter="url(#textGlow)"' : "";
|
|
1727
|
+
return `
|
|
1728
|
+
<rect x="0" y="${titleBarHeight}" width="${window.width}"
|
|
1729
|
+
height="${viewportHeight}" fill="${escapeXml(theme.colors.background)}"/>
|
|
1730
|
+
<g clip-path="url(#terminalViewport)">
|
|
1731
|
+
<g id="scrollContainer" transform="translate(${terminal.padding}, ${contentY})"${glow}>
|
|
1732
|
+
${scrollAnimations}
|
|
1733
|
+
${allLines}
|
|
1734
|
+
</g>
|
|
1735
|
+
</g>`;
|
|
1736
|
+
}
|
|
1737
|
+
function renderScrollAnimations(frames, terminal, window, lineHeight) {
|
|
1738
|
+
const scrollFrames = frames.filter((f) => f.type === "scroll");
|
|
1739
|
+
if (scrollFrames.length === 0) return "";
|
|
1740
|
+
const roundedLineHeight = roundCoord(lineHeight);
|
|
1741
|
+
const titleBarHeight = getTitleBarHeight(window);
|
|
1742
|
+
const scrollOriginY = titleBarHeight + terminal.paddingTop;
|
|
1743
|
+
const values = [`${terminal.padding} ${roundCoord(scrollOriginY)}`];
|
|
1744
|
+
const keyTimesMs = [0];
|
|
1745
|
+
let totalScroll = 0;
|
|
1746
|
+
for (const frame of scrollFrames) {
|
|
1747
|
+
const fromY = roundCoord(scrollOriginY - totalScroll);
|
|
1748
|
+
const scrollAmount = (frame.scrollLines ?? 1) * roundedLineHeight;
|
|
1749
|
+
totalScroll += scrollAmount;
|
|
1750
|
+
const toY = roundCoord(scrollOriginY - totalScroll);
|
|
1751
|
+
const startT = frame.time;
|
|
1752
|
+
const endT = frame.time + SCROLL_ANIM_DURATION;
|
|
1753
|
+
if (keyTimesMs[keyTimesMs.length - 1] < startT) {
|
|
1754
|
+
values.push(`${terminal.padding} ${fromY}`);
|
|
1755
|
+
keyTimesMs.push(startT);
|
|
1756
|
+
}
|
|
1757
|
+
values.push(`${terminal.padding} ${toY}`);
|
|
1758
|
+
keyTimesMs.push(endT);
|
|
1759
|
+
}
|
|
1760
|
+
const totalDur = keyTimesMs[keyTimesMs.length - 1];
|
|
1761
|
+
const keyTimes = keyTimesMs.map((t) => (t / totalDur).toFixed(4)).join(";");
|
|
1762
|
+
return `
|
|
1763
|
+
<animateTransform
|
|
1764
|
+
attributeName="transform" type="translate"
|
|
1765
|
+
values="${values.join(";")}" keyTimes="${keyTimes}"
|
|
1766
|
+
dur="${totalDur}ms" fill="freeze"/>`;
|
|
1767
|
+
}
|
|
1768
|
+
function renderStaticStyledText(text, colorMap, defaultColor, dimOpacity) {
|
|
1769
|
+
const spans = parseMarkup(text, colorMap, defaultColor);
|
|
1770
|
+
return spans.map((span) => {
|
|
1771
|
+
const color = span.fg ?? defaultColor;
|
|
1772
|
+
const attrs = [`fill="${escapeXml(color)}"`];
|
|
1773
|
+
if (span.bold) attrs.push('font-weight="bold"');
|
|
1774
|
+
if (span.dim) attrs.push(`opacity="${dimOpacity}"`);
|
|
1775
|
+
return `<tspan ${attrs.join(" ")}>${escapeXml(span.text)}</tspan>`;
|
|
1776
|
+
}).join("");
|
|
1777
|
+
}
|
|
1778
|
+
function generateStaticSvg(lines, config) {
|
|
1779
|
+
const { text: terminal, theme, effects, chrome } = config;
|
|
1780
|
+
const window = { ...config.window };
|
|
1781
|
+
if (window.autoHeight) {
|
|
1782
|
+
const lineHeight2 = terminal.fontSize * terminal.lineHeight;
|
|
1783
|
+
const contentHeight = lines.length * lineHeight2;
|
|
1784
|
+
const titleBarHeight2 = getTitleBarHeight(window);
|
|
1785
|
+
const chromeHeight = titleBarHeight2 + terminal.paddingTop + terminal.padding * 2;
|
|
1786
|
+
const calculated = Math.ceil(contentHeight + chromeHeight);
|
|
1787
|
+
window.height = Math.max(window.minHeight, Math.min(window.maxHeight, calculated));
|
|
1788
|
+
}
|
|
1789
|
+
const lineHeight = terminal.fontSize * terminal.lineHeight;
|
|
1790
|
+
const titleBarHeight = getTitleBarHeight(window);
|
|
1791
|
+
const contentY = titleBarHeight + terminal.paddingTop;
|
|
1792
|
+
const viewportHeight = window.height - titleBarHeight;
|
|
1793
|
+
const accessibilityLabel = config.accessibilityLabel ?? `Static terminal showing ${lines.length} lines`;
|
|
1794
|
+
const a11yChildren = renderStaticAccessibilityChildren(accessibilityLabel, lines, config.accessibility);
|
|
1795
|
+
const colorMap = buildColorMap(theme.colors);
|
|
1796
|
+
const glow = effects.textGlow ? ' filter="url(#textGlow)"' : "";
|
|
1797
|
+
const showShadow = effects.shadow && window.style !== "none";
|
|
1798
|
+
const lineElements = lines.map((line, i) => {
|
|
1799
|
+
const y = roundCoord(i * lineHeight);
|
|
1800
|
+
const hasMarkupTags = line.includes("[[");
|
|
1801
|
+
const textContent = hasMarkupTags ? renderStaticStyledText(line, colorMap, theme.colors.text, chrome.dimOpacity) : escapeXml(line);
|
|
1802
|
+
const fill = hasMarkupTags ? "" : ` fill="${escapeXml(theme.colors.text)}"`;
|
|
1803
|
+
return `
|
|
1804
|
+
<text class="tt" y="${y}"${fill}>
|
|
1805
|
+
${textContent}
|
|
1806
|
+
</text>`;
|
|
1807
|
+
}).join("");
|
|
1808
|
+
return `<svg width="${window.width}" height="${window.height}" viewBox="0 0 ${window.width} ${window.height}" xmlns="http://www.w3.org/2000/svg"
|
|
1809
|
+
role="img" aria-label="${escapeXml(accessibilityLabel)}">${a11yChildren}
|
|
1810
|
+
<style>.tt { font-family: ${escapeXml(terminal.fontFamily)}; font-size: ${terminal.fontSize}px; white-space: pre; }</style>
|
|
1811
|
+
<defs>
|
|
1812
|
+
${generateDefs(effects, window.style)}
|
|
1813
|
+
${generateFilters(effects, theme.colors.cursor)}
|
|
1814
|
+
<clipPath id="terminalViewport">
|
|
1815
|
+
<rect x="0" y="${titleBarHeight}" width="${window.width}" height="${viewportHeight}"/>
|
|
1816
|
+
</clipPath>
|
|
1817
|
+
</defs>
|
|
1818
|
+
|
|
1819
|
+
<g${showShadow ? ' filter="url(#shadow)"' : ""}>
|
|
1820
|
+
${renderWindow(window, theme)}
|
|
1821
|
+
${renderTitleBarForStyle(window, terminal, theme, chrome)}
|
|
1822
|
+
<rect x="0" y="${titleBarHeight}" width="${window.width}"
|
|
1823
|
+
height="${viewportHeight}" fill="${escapeXml(theme.colors.background)}"/>
|
|
1824
|
+
<g clip-path="url(#terminalViewport)">
|
|
1825
|
+
<g transform="translate(${terminal.padding}, ${contentY})"${glow}>
|
|
1826
|
+
${lineElements}
|
|
1827
|
+
</g>
|
|
1828
|
+
</g>
|
|
1829
|
+
${renderVignetteOverlay(effects, window)}
|
|
1830
|
+
${renderScanlineOverlay(effects, window, false)}
|
|
1831
|
+
</g>
|
|
1832
|
+
</svg>`;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// src/blocks/custom.ts
|
|
1836
|
+
import { z as z3 } from "zod";
|
|
1837
|
+
var customConfigSchema = z3.object({
|
|
1838
|
+
command: z3.string().optional(),
|
|
1839
|
+
lines: z3.array(z3.string()).optional(),
|
|
1840
|
+
color: z3.string().optional()
|
|
1841
|
+
}).strict();
|
|
1842
|
+
var customBlock = {
|
|
1843
|
+
name: "custom",
|
|
1844
|
+
description: "Display custom text with optional [[fg:color]] markup",
|
|
1845
|
+
configSchema: customConfigSchema,
|
|
1846
|
+
render(_context, config) {
|
|
1847
|
+
const command = config["command"] ?? 'echo "Hello, World!"';
|
|
1848
|
+
const lines = config["lines"] ?? ["Hello, World!"];
|
|
1849
|
+
const color = config["color"];
|
|
1850
|
+
return { command, lines, color };
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
|
|
1854
|
+
// src/blocks/neofetch.ts
|
|
1855
|
+
import { z as z4 } from "zod";
|
|
1856
|
+
var neofetchConfigSchema = z4.object({
|
|
1857
|
+
username: z4.string().optional(),
|
|
1858
|
+
hostname: z4.string().optional(),
|
|
1859
|
+
title: z4.string().optional(),
|
|
1860
|
+
os: z4.string().optional(),
|
|
1861
|
+
shell: z4.string().optional(),
|
|
1862
|
+
uptime: z4.string().optional(),
|
|
1863
|
+
role: z4.string().optional(),
|
|
1864
|
+
location: z4.string().optional(),
|
|
1865
|
+
languages: z4.string().optional(),
|
|
1866
|
+
editor: z4.string().optional(),
|
|
1867
|
+
command: z4.string().optional()
|
|
1868
|
+
}).strict();
|
|
1869
|
+
var neofetchBlock = {
|
|
1870
|
+
name: "neofetch",
|
|
1871
|
+
description: "Display system-info style output like neofetch",
|
|
1872
|
+
configSchema: neofetchConfigSchema,
|
|
1873
|
+
render(_context, config) {
|
|
1874
|
+
const username = config["username"] ?? "user";
|
|
1875
|
+
const hostname = config["hostname"] ?? "terminal";
|
|
1876
|
+
const title = config["title"] ?? `${username}@${hostname}`;
|
|
1877
|
+
const os = config["os"] ?? "TerminalOS v1.0";
|
|
1878
|
+
const shell = config["shell"] ?? "bash 5.2";
|
|
1879
|
+
const uptime = config["uptime"] ?? "a long time";
|
|
1880
|
+
const role = config["role"] ?? "Developer";
|
|
1881
|
+
const location = config["location"] ?? "localhost";
|
|
1882
|
+
const languages = config["languages"] ?? "TypeScript, Python, Go";
|
|
1883
|
+
const editor = config["editor"] ?? "neovim";
|
|
1884
|
+
const separator = "\u2500".repeat(title.length);
|
|
1885
|
+
const command = config["command"] ?? `neofetch --ascii_distro ${hostname}`;
|
|
1886
|
+
const lines = [
|
|
1887
|
+
`[[fg:cyan]]${title}[[/fg]]`,
|
|
1888
|
+
`[[fg:cyan]]${separator}[[/fg]]`,
|
|
1889
|
+
`[[fg:cyan]]OS[[/fg]]: ${os}`,
|
|
1890
|
+
`[[fg:cyan]]Shell[[/fg]]: ${shell}`,
|
|
1891
|
+
`[[fg:cyan]]Uptime[[/fg]]: ${uptime}`,
|
|
1892
|
+
`[[fg:cyan]]Role[[/fg]]: ${role}`,
|
|
1893
|
+
`[[fg:cyan]]Location[[/fg]]: ${location}`,
|
|
1894
|
+
`[[fg:cyan]]Languages[[/fg]]: ${languages}`,
|
|
1895
|
+
`[[fg:cyan]]Editor[[/fg]]: ${editor}`,
|
|
1896
|
+
"",
|
|
1897
|
+
// Color palette row
|
|
1898
|
+
"[[fg:red]]\u25CF[[/fg]] [[fg:green]]\u25CF[[/fg]] [[fg:yellow]]\u25CF[[/fg]] [[fg:blue]]\u25CF[[/fg]] [[fg:magenta]]\u25CF[[/fg]] [[fg:cyan]]\u25CF[[/fg]] [[fg:white]]\u25CF[[/fg]] [[fg:orange]]\u25CF[[/fg]]"
|
|
1899
|
+
];
|
|
1900
|
+
return { command, lines };
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
|
|
1904
|
+
// src/blocks/fortune.ts
|
|
1905
|
+
import { z as z5 } from "zod";
|
|
1906
|
+
|
|
1907
|
+
// src/core/box-generator.ts
|
|
1908
|
+
var BOX_OVERHEAD = 4;
|
|
1909
|
+
var BOX_STYLES = {
|
|
1910
|
+
double: {
|
|
1911
|
+
topLeft: "\u2554",
|
|
1912
|
+
topRight: "\u2557",
|
|
1913
|
+
bottomLeft: "\u255A",
|
|
1914
|
+
bottomRight: "\u255D",
|
|
1915
|
+
horizontal: "\u2550",
|
|
1916
|
+
vertical: "\u2551",
|
|
1917
|
+
separatorLeft: "\u2560",
|
|
1918
|
+
separatorRight: "\u2563"
|
|
1919
|
+
},
|
|
1920
|
+
rounded: {
|
|
1921
|
+
topLeft: "\u256D",
|
|
1922
|
+
topRight: "\u256E",
|
|
1923
|
+
bottomLeft: "\u2570",
|
|
1924
|
+
bottomRight: "\u256F",
|
|
1925
|
+
horizontal: "\u2500",
|
|
1926
|
+
vertical: "\u2502",
|
|
1927
|
+
separatorLeft: "\u251C",
|
|
1928
|
+
separatorRight: "\u2524"
|
|
1929
|
+
},
|
|
1930
|
+
single: {
|
|
1931
|
+
topLeft: "\u250C",
|
|
1932
|
+
topRight: "\u2510",
|
|
1933
|
+
bottomLeft: "\u2514",
|
|
1934
|
+
bottomRight: "\u2518",
|
|
1935
|
+
horizontal: "\u2500",
|
|
1936
|
+
vertical: "\u2502",
|
|
1937
|
+
separatorLeft: "\u251C",
|
|
1938
|
+
separatorRight: "\u2524"
|
|
1939
|
+
},
|
|
1940
|
+
heavy: {
|
|
1941
|
+
topLeft: "\u250F",
|
|
1942
|
+
topRight: "\u2513",
|
|
1943
|
+
bottomLeft: "\u2517",
|
|
1944
|
+
bottomRight: "\u251B",
|
|
1945
|
+
horizontal: "\u2501",
|
|
1946
|
+
vertical: "\u2503",
|
|
1947
|
+
separatorLeft: "\u2523",
|
|
1948
|
+
separatorRight: "\u252B"
|
|
1949
|
+
},
|
|
1950
|
+
dashed: {
|
|
1951
|
+
topLeft: "\u250C",
|
|
1952
|
+
topRight: "\u2510",
|
|
1953
|
+
bottomLeft: "\u2514",
|
|
1954
|
+
bottomRight: "\u2518",
|
|
1955
|
+
horizontal: "\u2504",
|
|
1956
|
+
vertical: "\u2506",
|
|
1957
|
+
separatorLeft: "\u251C",
|
|
1958
|
+
separatorRight: "\u2524"
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
function getDisplayWidth(str) {
|
|
1962
|
+
if (!str) return 0;
|
|
1963
|
+
const cleaned = stripMarkup(str).replace(/\x1b\[[0-9;]*m/g, "");
|
|
1964
|
+
let width = 0;
|
|
1965
|
+
for (const char of cleaned) {
|
|
1966
|
+
const code = char.codePointAt(0) ?? 0;
|
|
1967
|
+
if (code >= 127744 && code <= 129535 || code >= 9728 && code <= 9983 || code >= 9984 && code <= 10175 || code >= 128512 && code <= 128591 || code >= 128640 && code <= 128767) {
|
|
1968
|
+
width += 2;
|
|
1969
|
+
} else {
|
|
1970
|
+
width += 1;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
return width;
|
|
1974
|
+
}
|
|
1975
|
+
function padToWidth(str, targetWidth, padChar = " ") {
|
|
1976
|
+
const padding = Math.max(0, targetWidth - getDisplayWidth(str));
|
|
1977
|
+
return str + padChar.repeat(padding);
|
|
1978
|
+
}
|
|
1979
|
+
function truncateToWidth(str, maxWidth) {
|
|
1980
|
+
if (getDisplayWidth(str) <= maxWidth) return str;
|
|
1981
|
+
let width = 0;
|
|
1982
|
+
let result = "";
|
|
1983
|
+
for (const char of str) {
|
|
1984
|
+
const charWidth = getDisplayWidth(char);
|
|
1985
|
+
if (width + charWidth + 3 > maxWidth) return result + "...";
|
|
1986
|
+
result += char;
|
|
1987
|
+
width += charWidth;
|
|
1988
|
+
}
|
|
1989
|
+
return result;
|
|
1990
|
+
}
|
|
1991
|
+
function breakLongWord(word, maxWidth) {
|
|
1992
|
+
const chunks = [];
|
|
1993
|
+
let chunk = "";
|
|
1994
|
+
let chunkWidth = 0;
|
|
1995
|
+
for (const char of word) {
|
|
1996
|
+
const charWidth = getDisplayWidth(char);
|
|
1997
|
+
if (chunkWidth + charWidth > maxWidth && chunk !== "") {
|
|
1998
|
+
chunks.push(chunk);
|
|
1999
|
+
chunk = char;
|
|
2000
|
+
chunkWidth = charWidth;
|
|
2001
|
+
} else {
|
|
2002
|
+
chunk += char;
|
|
2003
|
+
chunkWidth += charWidth;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
if (chunk) chunks.push(chunk);
|
|
2007
|
+
return chunks;
|
|
2008
|
+
}
|
|
2009
|
+
function wrapText(text, maxWidth, indent = "") {
|
|
2010
|
+
if (!text || getDisplayWidth(text) <= maxWidth) return [text || ""];
|
|
2011
|
+
const words = text.split(/\s+/);
|
|
2012
|
+
const lines = [];
|
|
2013
|
+
let currentLine = "";
|
|
2014
|
+
for (const word of words) {
|
|
2015
|
+
const wordWidth = getDisplayWidth(word);
|
|
2016
|
+
if (wordWidth > maxWidth) {
|
|
2017
|
+
if (currentLine !== "") {
|
|
2018
|
+
lines.push(currentLine);
|
|
2019
|
+
currentLine = "";
|
|
2020
|
+
}
|
|
2021
|
+
const chunks = breakLongWord(word, maxWidth);
|
|
2022
|
+
for (const chunk of chunks) {
|
|
2023
|
+
if (lines.length === 0 && currentLine === "") {
|
|
2024
|
+
currentLine = chunk;
|
|
2025
|
+
} else {
|
|
2026
|
+
if (currentLine !== "") lines.push(currentLine);
|
|
2027
|
+
currentLine = indent + chunk;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
const lineWidth = getDisplayWidth(currentLine);
|
|
2033
|
+
if (currentLine === "") {
|
|
2034
|
+
currentLine = lines.length > 0 ? indent + word : word;
|
|
2035
|
+
} else if (lineWidth + 1 + wordWidth <= maxWidth) {
|
|
2036
|
+
currentLine += " " + word;
|
|
2037
|
+
} else {
|
|
2038
|
+
lines.push(currentLine);
|
|
2039
|
+
currentLine = indent + word;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
if (currentLine) lines.push(currentLine);
|
|
2043
|
+
return lines;
|
|
2044
|
+
}
|
|
2045
|
+
function createBox(config) {
|
|
2046
|
+
const {
|
|
2047
|
+
style = "double",
|
|
2048
|
+
width = 56,
|
|
2049
|
+
lines = [],
|
|
2050
|
+
separatorAfter = [],
|
|
2051
|
+
truncate = true,
|
|
2052
|
+
wrap: wrap2 = false
|
|
2053
|
+
} = config;
|
|
2054
|
+
const chars = BOX_STYLES[style];
|
|
2055
|
+
const innerWidth = width - BOX_OVERHEAD;
|
|
2056
|
+
const result = [];
|
|
2057
|
+
result.push(chars.topLeft + chars.horizontal.repeat(width - 2) + chars.topRight);
|
|
2058
|
+
const expanded = [];
|
|
2059
|
+
lines.forEach((line, index) => {
|
|
2060
|
+
const isSep = separatorAfter?.includes(index) ?? false;
|
|
2061
|
+
if (wrap2 && getDisplayWidth(line) > innerWidth) {
|
|
2062
|
+
const wrapped = wrapText(line, innerWidth);
|
|
2063
|
+
wrapped.forEach((wl, wi) => {
|
|
2064
|
+
expanded.push({ content: wl, separateAfter: wi === wrapped.length - 1 && isSep });
|
|
2065
|
+
});
|
|
2066
|
+
} else {
|
|
2067
|
+
let content = line;
|
|
2068
|
+
if (truncate && getDisplayWidth(content) > innerWidth) {
|
|
2069
|
+
content = truncateToWidth(content, innerWidth);
|
|
2070
|
+
}
|
|
2071
|
+
expanded.push({ content, separateAfter: isSep });
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
for (const { content, separateAfter } of expanded) {
|
|
2075
|
+
const padded = padToWidth(content, innerWidth);
|
|
2076
|
+
result.push(chars.vertical + " " + padded + " " + chars.vertical);
|
|
2077
|
+
if (separateAfter) {
|
|
2078
|
+
result.push(chars.separatorLeft + chars.horizontal.repeat(width - 2) + chars.separatorRight);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
result.push(chars.bottomLeft + chars.horizontal.repeat(width - 2) + chars.bottomRight);
|
|
2082
|
+
return result.join("\n");
|
|
2083
|
+
}
|
|
2084
|
+
function createDoubleBox(lines, width = 56, separatorAfter) {
|
|
2085
|
+
return createBox({ style: "double", width, lines, separatorAfter });
|
|
2086
|
+
}
|
|
2087
|
+
function createRoundedBox(lines, width = 48, separatorAfter) {
|
|
2088
|
+
return createBox({ style: "rounded", width, lines, separatorAfter });
|
|
2089
|
+
}
|
|
2090
|
+
function createTitledBox(opts) {
|
|
2091
|
+
const { title, subtitle, content = [], width = 56, style = "double" } = opts;
|
|
2092
|
+
const lines = ["", title];
|
|
2093
|
+
if (subtitle) lines.push(subtitle);
|
|
2094
|
+
const separatorIndex = lines.length - 1;
|
|
2095
|
+
lines.push(...content);
|
|
2096
|
+
return createBox({ style, width, lines, separatorAfter: [separatorIndex] });
|
|
2097
|
+
}
|
|
2098
|
+
function createAutoBox(config) {
|
|
2099
|
+
const { lines, style = "double", minWidth = 0, maxWidth = 120, separatorAfter } = config;
|
|
2100
|
+
let longestLine = 0;
|
|
2101
|
+
for (const line of lines) {
|
|
2102
|
+
const w = getDisplayWidth(line);
|
|
2103
|
+
if (w > longestLine) longestLine = w;
|
|
2104
|
+
}
|
|
2105
|
+
let boxWidth = longestLine + BOX_OVERHEAD;
|
|
2106
|
+
boxWidth = Math.max(boxWidth, minWidth);
|
|
2107
|
+
if (boxWidth > maxWidth) {
|
|
2108
|
+
return createBox({ style, width: maxWidth, lines, separatorAfter, wrap: true });
|
|
2109
|
+
}
|
|
2110
|
+
return createBox({ style, width: boxWidth, lines, separatorAfter });
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// src/blocks/fortune.ts
|
|
2114
|
+
var fortuneConfigSchema = z5.object({
|
|
2115
|
+
fortunes: z5.array(z5.string()).optional(),
|
|
2116
|
+
command: z5.string().optional(),
|
|
2117
|
+
width: z5.number().positive().optional(),
|
|
2118
|
+
color: z5.string().optional()
|
|
2119
|
+
}).strict();
|
|
2120
|
+
var DEFAULT_FORTUNES = [
|
|
2121
|
+
"The best code is no code at all.",
|
|
2122
|
+
"Talk is cheap. Show me the code. \u2014 Linus Torvalds",
|
|
2123
|
+
"First, solve the problem. Then, write the code.",
|
|
2124
|
+
"Premature optimization is the root of all evil. \u2014 Donald Knuth",
|
|
2125
|
+
"Walking on water and developing software from a specification are easy if both are frozen. \u2014 Edward Berard",
|
|
2126
|
+
"Simplicity is the soul of efficiency. \u2014 Austin Freeman",
|
|
2127
|
+
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand. \u2014 Martin Fowler",
|
|
2128
|
+
"Programs must be written for people to read, and only incidentally for machines to execute. \u2014 Harold Abelson",
|
|
2129
|
+
"It is not enough for code to work. \u2014 Robert C. Martin",
|
|
2130
|
+
"There are only two hard things in Computer Science: cache invalidation and naming things. \u2014 Phil Karlton",
|
|
2131
|
+
"The most important property of a program is whether it accomplishes the intention of its user. \u2014 C.A.R. Hoare",
|
|
2132
|
+
"Code is like humor. When you have to explain it, it is bad. \u2014 Cory House",
|
|
2133
|
+
"Make it work, make it right, make it fast. \u2014 Kent Beck",
|
|
2134
|
+
"Debugging is twice as hard as writing the code. \u2014 Brian Kernighan",
|
|
2135
|
+
"Everybody should learn to program a computer, because it teaches you how to think. \u2014 Steve Jobs",
|
|
2136
|
+
"A language that doesn't affect the way you think about programming is not worth knowing. \u2014 Alan Perlis",
|
|
2137
|
+
"Software is a great combination between artistry and engineering. \u2014 Bill Gates",
|
|
2138
|
+
"Programming isn't about what you know; it's about what you can figure out. \u2014 Chris Pine",
|
|
2139
|
+
"The function of good software is to make the complex appear to be simple. \u2014 Grady Booch",
|
|
2140
|
+
"Truth can only be found in one place: the code. \u2014 Robert C. Martin",
|
|
2141
|
+
"Sometimes it pays to stay in bed on Monday, rather than spending the rest of the week debugging Monday's code. \u2014 Christopher Thompson",
|
|
2142
|
+
"In order to be irreplaceable, one must always be different. \u2014 Coco Chanel",
|
|
2143
|
+
"When in doubt, use brute force. \u2014 Ken Thompson",
|
|
2144
|
+
"Controlling complexity is the essence of computer programming. \u2014 Brian Kernighan",
|
|
2145
|
+
"A good programmer is someone who always looks both ways before crossing a one-way street. \u2014 Doug Linder",
|
|
2146
|
+
"The computer was born to solve problems that did not exist before. \u2014 Bill Gates",
|
|
2147
|
+
"First, solve the problem. Then write the code. \u2014 John Johnson",
|
|
2148
|
+
"Programs are meant to be read by humans and only incidentally for computers to execute. \u2014 Donald Knuth"
|
|
2149
|
+
];
|
|
2150
|
+
var fortuneBlock = {
|
|
2151
|
+
name: "fortune",
|
|
2152
|
+
description: "Display a random fortune or quote in an ASCII box",
|
|
2153
|
+
configSchema: fortuneConfigSchema,
|
|
2154
|
+
render(context, config) {
|
|
2155
|
+
const fortunes = config["fortunes"] ?? DEFAULT_FORTUNES;
|
|
2156
|
+
const command = config["command"] ?? "fortune";
|
|
2157
|
+
const width = resolveBoxWidth(config["width"], context);
|
|
2158
|
+
const index = Math.floor(context.now.getTime() / 864e5) % fortunes.length;
|
|
2159
|
+
const fortune = fortunes[index] ?? fortunes[0] ?? "";
|
|
2160
|
+
const box = createRoundedBox(["", ` ${fortune}`, ""], width);
|
|
2161
|
+
const lines = box.split("\n");
|
|
2162
|
+
return { command, lines, color: config["color"] };
|
|
2163
|
+
}
|
|
2164
|
+
};
|
|
2165
|
+
|
|
2166
|
+
// src/blocks/motd.ts
|
|
2167
|
+
import { z as z7 } from "zod";
|
|
2168
|
+
|
|
2169
|
+
// src/blocks/weather.ts
|
|
2170
|
+
import { z as z6 } from "zod";
|
|
2171
|
+
|
|
2172
|
+
// src/core/http.ts
|
|
2173
|
+
var DEFAULT_FETCH_TIMEOUT = 1e4;
|
|
2174
|
+
function safeUrlForLog(url) {
|
|
2175
|
+
try {
|
|
2176
|
+
const u = new URL(url);
|
|
2177
|
+
return `${u.protocol}//${u.host}${u.pathname}`;
|
|
2178
|
+
} catch {
|
|
2179
|
+
return url.split("?")[0] ?? url;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
var MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
2183
|
+
async function readCappedText(response, url) {
|
|
2184
|
+
const contentLength = response.headers.get("content-length");
|
|
2185
|
+
if (contentLength && Number(contentLength) > MAX_RESPONSE_BYTES) {
|
|
2186
|
+
console.warn(`[svg-terminal] Response too large (${contentLength} bytes, cap ${MAX_RESPONSE_BYTES}) from ${safeUrlForLog(url)}`);
|
|
2187
|
+
return null;
|
|
2188
|
+
}
|
|
2189
|
+
const reader = response.body?.getReader();
|
|
2190
|
+
if (!reader) return response.text();
|
|
2191
|
+
const chunks = [];
|
|
2192
|
+
let total = 0;
|
|
2193
|
+
try {
|
|
2194
|
+
while (true) {
|
|
2195
|
+
const { done, value } = await reader.read();
|
|
2196
|
+
if (done) break;
|
|
2197
|
+
total += value.length;
|
|
2198
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
2199
|
+
await reader.cancel();
|
|
2200
|
+
console.warn(`[svg-terminal] Response exceeded ${MAX_RESPONSE_BYTES}-byte cap mid-stream from ${safeUrlForLog(url)}`);
|
|
2201
|
+
return null;
|
|
2202
|
+
}
|
|
2203
|
+
chunks.push(value);
|
|
2204
|
+
}
|
|
2205
|
+
} finally {
|
|
2206
|
+
reader.releaseLock();
|
|
2207
|
+
}
|
|
2208
|
+
return new TextDecoder().decode(Buffer.concat(chunks));
|
|
2209
|
+
}
|
|
2210
|
+
var USER_AGENT = `svg-terminal/${true ? "1.0.0" : "0.0.0-dev"}`;
|
|
2211
|
+
async function fetchWithTimeout(url, timeoutMs = DEFAULT_FETCH_TIMEOUT) {
|
|
2212
|
+
const controller = new AbortController();
|
|
2213
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2214
|
+
try {
|
|
2215
|
+
const response = await fetch(url, {
|
|
2216
|
+
signal: controller.signal,
|
|
2217
|
+
headers: { "User-Agent": USER_AGENT }
|
|
2218
|
+
});
|
|
2219
|
+
if (!response.ok) {
|
|
2220
|
+
console.warn(`[svg-terminal] HTTP ${response.status} from ${safeUrlForLog(url)}`);
|
|
2221
|
+
return null;
|
|
2222
|
+
}
|
|
2223
|
+
return response;
|
|
2224
|
+
} catch (error) {
|
|
2225
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2226
|
+
if (message.includes("abort")) {
|
|
2227
|
+
console.warn(`[svg-terminal] Timeout after ${timeoutMs}ms fetching ${safeUrlForLog(url)}`);
|
|
2228
|
+
} else {
|
|
2229
|
+
console.warn(`[svg-terminal] Fetch failed for ${safeUrlForLog(url)}: ${message}`);
|
|
2230
|
+
}
|
|
2231
|
+
return null;
|
|
2232
|
+
} finally {
|
|
2233
|
+
clearTimeout(timer);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
async function fetchJson(url, timeoutMs = DEFAULT_FETCH_TIMEOUT) {
|
|
2237
|
+
const response = await fetchWithTimeout(url, timeoutMs);
|
|
2238
|
+
if (!response) return null;
|
|
2239
|
+
const text = await readCappedText(response, url);
|
|
2240
|
+
if (text === null) return null;
|
|
2241
|
+
try {
|
|
2242
|
+
return JSON.parse(text);
|
|
2243
|
+
} catch {
|
|
2244
|
+
console.warn(`[svg-terminal] Invalid JSON from ${safeUrlForLog(url)}`);
|
|
2245
|
+
return null;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
async function fetchText(url, timeoutMs = DEFAULT_FETCH_TIMEOUT) {
|
|
2249
|
+
const response = await fetchWithTimeout(url, timeoutMs);
|
|
2250
|
+
if (!response) return null;
|
|
2251
|
+
try {
|
|
2252
|
+
return await readCappedText(response, url);
|
|
2253
|
+
} catch {
|
|
2254
|
+
console.warn(`[svg-terminal] Failed to read text from ${safeUrlForLog(url)}`);
|
|
2255
|
+
return null;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// src/core/cache.ts
|
|
2260
|
+
import { createHash, randomBytes } from "crypto";
|
|
2261
|
+
import { readFileSync as readFileSync2, realpathSync, renameSync, writeFileSync, unlinkSync } from "fs";
|
|
2262
|
+
import { dirname, isAbsolute, join, resolve, sep } from "path";
|
|
2263
|
+
var CACHE_VERSION = 1;
|
|
2264
|
+
function hashConfig(config) {
|
|
2265
|
+
const json = canonicalize(config);
|
|
2266
|
+
return createHash("sha256").update(json).digest("hex").slice(0, 16);
|
|
2267
|
+
}
|
|
2268
|
+
function canonicalize(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
2269
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
2270
|
+
if (ancestors.has(value)) {
|
|
2271
|
+
throw new Error("cache: config contains a circular reference (YAML anchor or programmatic cycle) \u2014 cannot hash");
|
|
2272
|
+
}
|
|
2273
|
+
ancestors.add(value);
|
|
2274
|
+
try {
|
|
2275
|
+
if (Array.isArray(value)) return `[${value.map((v) => canonicalize(v, ancestors)).join(",")}]`;
|
|
2276
|
+
const obj = value;
|
|
2277
|
+
const keys = Object.keys(obj).sort();
|
|
2278
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k], ancestors)}`).join(",")}}`;
|
|
2279
|
+
} finally {
|
|
2280
|
+
ancestors.delete(value);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
function resolveCachePath(configPath, cachePath) {
|
|
2284
|
+
if (isAbsolute(cachePath)) return resolve(cachePath);
|
|
2285
|
+
const configDirRaw = dirname(resolve(configPath));
|
|
2286
|
+
const resolvedRaw = resolve(configDirRaw, cachePath);
|
|
2287
|
+
const configDir = safeRealpath(configDirRaw);
|
|
2288
|
+
const resolvedCanonical = safeRealpath(dirname(resolvedRaw));
|
|
2289
|
+
const resolvedBasename = resolvedRaw.slice(dirname(resolvedRaw).length);
|
|
2290
|
+
const resolved = resolvedCanonical + resolvedBasename;
|
|
2291
|
+
if (resolved !== configDir && !resolved.startsWith(configDir + sep)) {
|
|
2292
|
+
throw new Error(`cachePath "${cachePath}" escapes the config directory`);
|
|
2293
|
+
}
|
|
2294
|
+
return resolved;
|
|
2295
|
+
}
|
|
2296
|
+
function safeRealpath(p) {
|
|
2297
|
+
try {
|
|
2298
|
+
return realpathSync(p);
|
|
2299
|
+
} catch {
|
|
2300
|
+
return p;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
function loadCacheFile(filePath) {
|
|
2304
|
+
let raw;
|
|
2305
|
+
try {
|
|
2306
|
+
raw = readFileSync2(filePath, "utf-8");
|
|
2307
|
+
} catch {
|
|
2308
|
+
return { version: CACHE_VERSION, entries: {} };
|
|
2309
|
+
}
|
|
2310
|
+
try {
|
|
2311
|
+
const parsed = JSON.parse(raw);
|
|
2312
|
+
if (parsed.version !== CACHE_VERSION || typeof parsed.entries !== "object" || parsed.entries === null) {
|
|
2313
|
+
console.warn(`[svg-terminal] cache: ignoring file at ${filePath} (incompatible schema)`);
|
|
2314
|
+
return { version: CACHE_VERSION, entries: {} };
|
|
2315
|
+
}
|
|
2316
|
+
return parsed;
|
|
2317
|
+
} catch {
|
|
2318
|
+
console.warn(`[svg-terminal] cache: ignoring corrupt JSON at ${filePath}`);
|
|
2319
|
+
return { version: CACHE_VERSION, entries: {} };
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
function persistCacheFile(filePath, data) {
|
|
2323
|
+
const dir = dirname(filePath);
|
|
2324
|
+
const tmp = join(dir, `.svg-terminal-cache.${randomBytes(6).toString("hex")}.tmp`);
|
|
2325
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2326
|
+
try {
|
|
2327
|
+
renameSync(tmp, filePath);
|
|
2328
|
+
} catch (err) {
|
|
2329
|
+
try {
|
|
2330
|
+
unlinkSync(tmp);
|
|
2331
|
+
} catch {
|
|
2332
|
+
}
|
|
2333
|
+
throw err;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
function makeUseCache(runtime, onEvent) {
|
|
2337
|
+
if (!Number.isFinite(runtime.ttl) || runtime.ttl < 0) {
|
|
2338
|
+
throw new Error(`CacheRuntime.ttl must be \u2265 0, got ${runtime.ttl}`);
|
|
2339
|
+
}
|
|
2340
|
+
return async (key, getter, opts) => {
|
|
2341
|
+
if (runtime.mode === "off") {
|
|
2342
|
+
onEvent?.("miss", key);
|
|
2343
|
+
return getter();
|
|
2344
|
+
}
|
|
2345
|
+
if (!runtime.data) runtime.data = loadCacheFile(runtime.filePath);
|
|
2346
|
+
const ttl = Math.max(0, opts?.ttl ?? runtime.ttl);
|
|
2347
|
+
const now = Date.now();
|
|
2348
|
+
const entry = runtime.data.entries[key];
|
|
2349
|
+
if (runtime.mode === "frozen") {
|
|
2350
|
+
if (entry) {
|
|
2351
|
+
onEvent?.("hit", key);
|
|
2352
|
+
return entry.payload;
|
|
2353
|
+
}
|
|
2354
|
+
throw new Error(`cache: frozen mode but no entry for "${key}" \u2014 re-run without --frozen-cache to populate`);
|
|
2355
|
+
}
|
|
2356
|
+
if (runtime.mode === "normal" && entry) {
|
|
2357
|
+
const ageMs = now - Date.parse(entry.fetchedAt);
|
|
2358
|
+
if (ageMs >= 0 && ageMs < ttl * 1e3) {
|
|
2359
|
+
onEvent?.("hit", key);
|
|
2360
|
+
return entry.payload;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
onEvent?.(runtime.mode === "refresh" ? "refreshed" : "miss", key);
|
|
2364
|
+
const payload = await getter();
|
|
2365
|
+
runtime.data.entries[key] = {
|
|
2366
|
+
fetchedAt: new Date(now).toISOString(),
|
|
2367
|
+
payload
|
|
2368
|
+
};
|
|
2369
|
+
runtime.dirty = true;
|
|
2370
|
+
return payload;
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
function flushCache(runtime) {
|
|
2374
|
+
if (runtime.mode === "off" || runtime.mode === "frozen" || !runtime.dirty || !runtime.data) return;
|
|
2375
|
+
pruneStaleEntries(runtime.data, runtime.ttl);
|
|
2376
|
+
persistCacheFile(runtime.filePath, runtime.data);
|
|
2377
|
+
}
|
|
2378
|
+
function pruneStaleEntries(data, ttl) {
|
|
2379
|
+
if (ttl <= 0) return;
|
|
2380
|
+
const cutoffMs = Date.now() - ttl * 1e3;
|
|
2381
|
+
for (const [key, entry] of Object.entries(data.entries)) {
|
|
2382
|
+
if (Date.parse(entry.fetchedAt) < cutoffMs) {
|
|
2383
|
+
delete data.entries[key];
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
function checkCache(input) {
|
|
2388
|
+
let data;
|
|
2389
|
+
try {
|
|
2390
|
+
data = JSON.parse(readFileSync2(input.filePath, "utf-8"));
|
|
2391
|
+
if (data.version !== CACHE_VERSION || typeof data.entries !== "object" || data.entries === null) {
|
|
2392
|
+
data = { version: CACHE_VERSION, entries: {} };
|
|
2393
|
+
}
|
|
2394
|
+
} catch {
|
|
2395
|
+
data = { version: CACHE_VERSION, entries: {} };
|
|
2396
|
+
}
|
|
2397
|
+
const now = Date.now();
|
|
2398
|
+
return input.entries.map(({ blockName, entryIndex, key }) => {
|
|
2399
|
+
const entry = data.entries[key];
|
|
2400
|
+
if (!entry) return { key, blockName, entryIndex, status: "MISSING" };
|
|
2401
|
+
const ageMs = now - Date.parse(entry.fetchedAt);
|
|
2402
|
+
const ageSeconds = Math.max(0, Math.round(ageMs / 1e3));
|
|
2403
|
+
if (ageMs >= 0 && ageMs < input.ttl * 1e3) {
|
|
2404
|
+
return { key, blockName, entryIndex, status: "OK", ageSeconds };
|
|
2405
|
+
}
|
|
2406
|
+
return { key, blockName, entryIndex, status: "STALE", ageSeconds };
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
// src/blocks/weather.ts
|
|
2411
|
+
var weatherSchema = z6.object({
|
|
2412
|
+
location: z6.string().optional(),
|
|
2413
|
+
units: z6.enum(["imperial", "metric", "both"]).optional(),
|
|
2414
|
+
compact: z6.boolean().optional(),
|
|
2415
|
+
width: z6.number().positive().optional(),
|
|
2416
|
+
command: z6.string().optional()
|
|
2417
|
+
}).strict();
|
|
2418
|
+
function encodeWttrLocation(location) {
|
|
2419
|
+
return encodeURIComponent(location).replace(/%20/g, "+");
|
|
2420
|
+
}
|
|
2421
|
+
function formatWeather(data, units, compact) {
|
|
2422
|
+
const current = data.current_condition[0];
|
|
2423
|
+
const area = data.nearest_area?.[0];
|
|
2424
|
+
if (!current) return ["Weather data unavailable"];
|
|
2425
|
+
const desc = current.weatherDesc?.[0]?.value ?? "Unknown";
|
|
2426
|
+
const locationName = area?.areaName?.[0]?.value ?? "Unknown";
|
|
2427
|
+
const region = area?.region?.[0]?.value;
|
|
2428
|
+
const locationStr = region ? `${locationName}, ${region}` : locationName;
|
|
2429
|
+
const tempF = `${current.temp_F}\xB0F`;
|
|
2430
|
+
const tempC = `${current.temp_C}\xB0C`;
|
|
2431
|
+
const feelsF = `${current.FeelsLikeF}\xB0F`;
|
|
2432
|
+
const feelsC = `${current.FeelsLikeC}\xB0C`;
|
|
2433
|
+
const temp = units === "imperial" ? tempF : units === "metric" ? tempC : `${tempF} / ${tempC}`;
|
|
2434
|
+
const feels = units === "imperial" ? feelsF : units === "metric" ? feelsC : `${feelsF} / ${feelsC}`;
|
|
2435
|
+
const wind = units === "metric" ? `${current.windspeedKmph} km/h ${current.winddir16Point}` : `${current.windspeedMiles} mph ${current.winddir16Point}`;
|
|
2436
|
+
if (compact) {
|
|
2437
|
+
return [
|
|
2438
|
+
`[[fg:cyan]]${locationStr}[[/fg]]`,
|
|
2439
|
+
`${desc} ${temp} Humidity: ${current.humidity}%`
|
|
2440
|
+
];
|
|
2441
|
+
}
|
|
2442
|
+
return [
|
|
2443
|
+
"",
|
|
2444
|
+
`[[fg:cyan]]WEATHER: ${locationStr}[[/fg]]`,
|
|
2445
|
+
`${desc} [[bold]]${temp}[[/bold]]`,
|
|
2446
|
+
`Humidity: ${current.humidity}% Wind: ${wind}`,
|
|
2447
|
+
`Feels Like: ${feels} UV: ${current.uvIndex}`,
|
|
2448
|
+
""
|
|
2449
|
+
];
|
|
2450
|
+
}
|
|
2451
|
+
var weatherBlock = {
|
|
2452
|
+
name: "weather",
|
|
2453
|
+
description: "Display current weather conditions from wttr.in",
|
|
2454
|
+
configSchema: weatherSchema,
|
|
2455
|
+
cacheable: true,
|
|
2456
|
+
async render(context, config) {
|
|
2457
|
+
const location = config["location"] ?? "";
|
|
2458
|
+
const units = config["units"] ?? "both";
|
|
2459
|
+
const compact = config["compact"] ?? false;
|
|
2460
|
+
const width = resolveBoxWidth(config["width"], context);
|
|
2461
|
+
const timeout = context.config.fetchTimeout;
|
|
2462
|
+
if (!location) {
|
|
2463
|
+
return {
|
|
2464
|
+
command: config["command"] ?? "curl wttr.in",
|
|
2465
|
+
lines: ["[[fg:yellow]]Weather: no location configured[[/fg]]"],
|
|
2466
|
+
typing: "fast",
|
|
2467
|
+
pause: "short",
|
|
2468
|
+
fallback: true
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
const url = `https://wttr.in/${encodeWttrLocation(location)}?format=j1`;
|
|
2472
|
+
const cacheKey = `weather:${hashConfig(config)}`;
|
|
2473
|
+
const data = context.useCache ? await context.useCache(cacheKey, () => fetchJson(url, timeout)) : await fetchJson(url, timeout);
|
|
2474
|
+
let lines;
|
|
2475
|
+
let isFallback = false;
|
|
2476
|
+
if (data?.current_condition?.length) {
|
|
2477
|
+
lines = formatWeather(data, units, compact);
|
|
2478
|
+
} else {
|
|
2479
|
+
lines = [
|
|
2480
|
+
"",
|
|
2481
|
+
"[[fg:yellow]]Weather data unavailable[[/fg]]",
|
|
2482
|
+
`Location: ${location}`,
|
|
2483
|
+
""
|
|
2484
|
+
];
|
|
2485
|
+
isFallback = true;
|
|
2486
|
+
}
|
|
2487
|
+
const box = compact ? void 0 : createDoubleBox(lines, width);
|
|
2488
|
+
return {
|
|
2489
|
+
command: config["command"] ?? `curl wttr.in/${location}`,
|
|
2490
|
+
lines: box ? box.split("\n") : lines,
|
|
2491
|
+
typing: "fast",
|
|
2492
|
+
pause: "medium",
|
|
2493
|
+
fallback: isFallback
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
async function fetchWeatherSummary(location, units, timeout) {
|
|
2498
|
+
if (!location) return null;
|
|
2499
|
+
const url = `https://wttr.in/${encodeWttrLocation(location)}?format=j1`;
|
|
2500
|
+
const data = await fetchJson(url, timeout);
|
|
2501
|
+
if (!data?.current_condition?.length) return null;
|
|
2502
|
+
const current = data.current_condition[0];
|
|
2503
|
+
const desc = current.weatherDesc?.[0]?.value ?? "";
|
|
2504
|
+
const temp = units === "metric" ? `${current.temp_C}\xB0C` : units === "imperial" ? `${current.temp_F}\xB0F` : `${current.temp_F}\xB0F/${current.temp_C}\xB0C`;
|
|
2505
|
+
return `${desc} ${temp} | Humidity: ${current.humidity}%`;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
// src/blocks/motd.ts
|
|
2509
|
+
var motdConfigSchema = z7.object({
|
|
2510
|
+
title: z7.string().optional(),
|
|
2511
|
+
subtitle: z7.string().optional(),
|
|
2512
|
+
width: z7.number().positive().optional(),
|
|
2513
|
+
command: z7.string().optional(),
|
|
2514
|
+
lines: z7.array(z7.string()).optional(),
|
|
2515
|
+
weather: z7.object({
|
|
2516
|
+
location: z7.string().optional(),
|
|
2517
|
+
units: z7.enum(["imperial", "metric", "both"]).optional()
|
|
2518
|
+
}).strict().optional()
|
|
2519
|
+
}).strict();
|
|
2520
|
+
var motdBlock = {
|
|
2521
|
+
name: "motd",
|
|
2522
|
+
description: "Display a welcome banner / message of the day",
|
|
2523
|
+
configSchema: motdConfigSchema,
|
|
2524
|
+
async render(context, config) {
|
|
2525
|
+
const title = config["title"] ?? "DEV TERMINAL";
|
|
2526
|
+
const subtitle = config["subtitle"] ?? "Powered by coffee & late-night debugging";
|
|
2527
|
+
const width = resolveBoxWidth(config["width"], context);
|
|
2528
|
+
const timeout = context.config.fetchTimeout;
|
|
2529
|
+
const date = context.now;
|
|
2530
|
+
const year = date.getFullYear();
|
|
2531
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
2532
|
+
const version = `v${year}.${month}`;
|
|
2533
|
+
const lines = [
|
|
2534
|
+
"",
|
|
2535
|
+
`${title} ${version}`,
|
|
2536
|
+
subtitle
|
|
2537
|
+
];
|
|
2538
|
+
const weatherConfig = config["weather"];
|
|
2539
|
+
if (weatherConfig) {
|
|
2540
|
+
const location = weatherConfig["location"] ?? "";
|
|
2541
|
+
const units = weatherConfig["units"] ?? "both";
|
|
2542
|
+
const weatherLine = await fetchWeatherSummary(location, units, timeout);
|
|
2543
|
+
if (weatherLine) {
|
|
2544
|
+
lines.push(`[[fg:cyan]]${weatherLine}[[/fg]]`);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
lines.push("");
|
|
2548
|
+
const extra = config["lines"];
|
|
2549
|
+
if (extra) {
|
|
2550
|
+
for (const line of extra) {
|
|
2551
|
+
lines.push(line);
|
|
2552
|
+
}
|
|
2553
|
+
lines.push("");
|
|
2554
|
+
}
|
|
2555
|
+
const box = createDoubleBox(lines, width);
|
|
2556
|
+
return {
|
|
2557
|
+
command: config["command"] ?? "cat /etc/motd",
|
|
2558
|
+
lines: box.split("\n"),
|
|
2559
|
+
typing: "fast",
|
|
2560
|
+
pause: "medium"
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
};
|
|
2564
|
+
|
|
2565
|
+
// src/blocks/dad-joke.ts
|
|
2566
|
+
import { z as z8 } from "zod";
|
|
2567
|
+
var jokeSchema = z8.object({
|
|
2568
|
+
q: z8.string(),
|
|
2569
|
+
a: z8.string(),
|
|
2570
|
+
category: z8.string().optional()
|
|
2571
|
+
}).strict();
|
|
2572
|
+
var dadJokeSchema = z8.object({
|
|
2573
|
+
jokes: z8.array(jokeSchema).optional(),
|
|
2574
|
+
width: z8.number().positive().optional(),
|
|
2575
|
+
command: z8.string().optional()
|
|
2576
|
+
}).strict();
|
|
2577
|
+
var dadJokeBlock = {
|
|
2578
|
+
name: "dad-joke",
|
|
2579
|
+
description: "Display a dad joke in a fancy ASCII box",
|
|
2580
|
+
configSchema: dadJokeSchema,
|
|
2581
|
+
render(context, config) {
|
|
2582
|
+
const jokes = config["jokes"] ?? [
|
|
2583
|
+
{ q: "Why do programmers prefer dark mode?", a: "Because light attracts bugs!", category: "classic" }
|
|
2584
|
+
];
|
|
2585
|
+
const width = resolveBoxWidth(config["width"], context);
|
|
2586
|
+
const dayOfYear = Math.floor(
|
|
2587
|
+
(context.now.getTime() - new Date(context.now.getFullYear(), 0, 0).getTime()) / 864e5
|
|
2588
|
+
);
|
|
2589
|
+
const joke = jokes[dayOfYear % jokes.length] ?? jokes[0];
|
|
2590
|
+
if (!joke) {
|
|
2591
|
+
return { command: "./dad-joke", lines: ["No jokes configured!"] };
|
|
2592
|
+
}
|
|
2593
|
+
const category = (joke.category ?? "classic").toUpperCase();
|
|
2594
|
+
const dateStr = context.now.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
2595
|
+
const lines = [
|
|
2596
|
+
"",
|
|
2597
|
+
`DAD JOKE OF THE DAY - ${dateStr}`,
|
|
2598
|
+
`Category: ${category}`,
|
|
2599
|
+
"",
|
|
2600
|
+
`Q: ${joke.q}`,
|
|
2601
|
+
"",
|
|
2602
|
+
`A: ${joke.a}`,
|
|
2603
|
+
""
|
|
2604
|
+
];
|
|
2605
|
+
const box = createDoubleBox(lines, width);
|
|
2606
|
+
return {
|
|
2607
|
+
command: config["command"] ?? "./dad-joke --random --format=fancy",
|
|
2608
|
+
lines: box.split("\n"),
|
|
2609
|
+
typing: "slow",
|
|
2610
|
+
pause: "long"
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
};
|
|
2614
|
+
|
|
2615
|
+
// src/blocks/htop.ts
|
|
2616
|
+
import { z as z9 } from "zod";
|
|
2617
|
+
var processSchema = z9.object({
|
|
2618
|
+
pid: z9.string(),
|
|
2619
|
+
user: z9.string(),
|
|
2620
|
+
cpu: z9.string(),
|
|
2621
|
+
mem: z9.string(),
|
|
2622
|
+
command: z9.string(),
|
|
2623
|
+
state: z9.string().optional()
|
|
2624
|
+
}).strict();
|
|
2625
|
+
var htopConfigSchema = z9.object({
|
|
2626
|
+
cpu: z9.number().min(0).max(100).optional(),
|
|
2627
|
+
mem: z9.number().min(0).max(100).optional(),
|
|
2628
|
+
processes: z9.array(processSchema).optional(),
|
|
2629
|
+
/** Total task count. Defaults to processes.length + 10 (some not in the displayed table). */
|
|
2630
|
+
tasks: z9.number().int().min(0).optional(),
|
|
2631
|
+
/** Load average shown next to memory bar. Defaults to "0.42 0.37 0.31". */
|
|
2632
|
+
load: z9.string().optional(),
|
|
2633
|
+
command: z9.string().optional()
|
|
2634
|
+
}).strict();
|
|
2635
|
+
var htopBlock = {
|
|
2636
|
+
name: "htop",
|
|
2637
|
+
description: "Display an htop-style process and resource monitor",
|
|
2638
|
+
configSchema: htopConfigSchema,
|
|
2639
|
+
render(_context, config) {
|
|
2640
|
+
const cpuPercent = config["cpu"] ?? 80.5;
|
|
2641
|
+
const memPercent = config["mem"] ?? 50;
|
|
2642
|
+
const processes = config["processes"] ?? [
|
|
2643
|
+
{ pid: "1337", user: "dev", cpu: "99.9", mem: "5.0", command: "coding --premium", state: "R" },
|
|
2644
|
+
{ pid: "2048", user: "dev", cpu: "42.0", mem: "3.7", command: "family --priority=max", state: "S" },
|
|
2645
|
+
{ pid: "4096", user: "dev", cpu: "15.2", mem: "2.1", command: "security-scanner", state: "S" }
|
|
2646
|
+
];
|
|
2647
|
+
const cpuBar = makeBar(cpuPercent);
|
|
2648
|
+
const memBar = makeBar(memPercent);
|
|
2649
|
+
const runningCount = processes.filter((p) => (p.state ?? "S") === "R").length;
|
|
2650
|
+
const totalTasks = config["tasks"] ?? processes.length + 10;
|
|
2651
|
+
const loadStr = config["load"] ?? "0.42 0.37 0.31";
|
|
2652
|
+
const lines = [
|
|
2653
|
+
` [[fg:cyan]]CPU[[/fg]][${colorBar(cpuBar, "green")} [[fg:white]]${cpuPercent.toFixed(1)}%[[/fg]]] [[fg:yellow]]Tasks:[[/fg]] ${totalTasks}, [[fg:green]]${runningCount} running[[/fg]]`,
|
|
2654
|
+
` [[fg:cyan]]Mem[[/fg]][${colorBar(memBar, "blue")} [[fg:white]]${memPercent.toFixed(1)}%[[/fg]]] [[fg:yellow]]Load:[[/fg]] ${loadStr}`,
|
|
2655
|
+
"",
|
|
2656
|
+
` [[fg:cyan]]PID[[/fg]] [[fg:cyan]]USER[[/fg]] [[fg:cyan]]S[[/fg]] [[fg:cyan]]CPU%[[/fg]] [[fg:cyan]]MEM%[[/fg]] [[fg:cyan]]Command[[/fg]]`
|
|
2657
|
+
];
|
|
2658
|
+
for (const proc of processes) {
|
|
2659
|
+
const state = proc.state ?? "S";
|
|
2660
|
+
const stateColor = state === "R" ? "green" : "yellow";
|
|
2661
|
+
lines.push(
|
|
2662
|
+
` [[fg:white]]${proc.pid.padEnd(6)}[[/fg]] [[fg:green]]${proc.user.padEnd(8)}[[/fg]] [[fg:${stateColor}]]${state}[[/fg]] ${proc.cpu.padStart(5)} ${proc.mem.padStart(5)} [[fg:white]]${proc.command}[[/fg]]`
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
return {
|
|
2666
|
+
command: config["command"] ?? "htop --sort-key=PERCENT_CPU",
|
|
2667
|
+
lines,
|
|
2668
|
+
typing: "slow",
|
|
2669
|
+
pause: "long"
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
};
|
|
2673
|
+
function makeBar(percent) {
|
|
2674
|
+
const filled = Math.round(percent / 5);
|
|
2675
|
+
const empty = 20 - filled;
|
|
2676
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
2677
|
+
}
|
|
2678
|
+
function colorBar(bar, color) {
|
|
2679
|
+
return `[[fg:${color}]]${bar}[[/fg]]`;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
// src/blocks/profile.ts
|
|
2683
|
+
import { z as z10 } from "zod";
|
|
2684
|
+
var profileConfigSchema = z10.object({
|
|
2685
|
+
name: z10.string().optional(),
|
|
2686
|
+
github: z10.string().optional(),
|
|
2687
|
+
web: z10.string().optional(),
|
|
2688
|
+
focus: z10.string().optional(),
|
|
2689
|
+
motto: z10.string().optional(),
|
|
2690
|
+
width: z10.number().positive().optional(),
|
|
2691
|
+
command: z10.string().optional()
|
|
2692
|
+
}).strict();
|
|
2693
|
+
var profileBlock = {
|
|
2694
|
+
name: "profile",
|
|
2695
|
+
description: "Display a developer profile info card",
|
|
2696
|
+
configSchema: profileConfigSchema,
|
|
2697
|
+
render(context, config) {
|
|
2698
|
+
const name = config["name"] ?? "Developer";
|
|
2699
|
+
const github = config["github"];
|
|
2700
|
+
const web = config["web"];
|
|
2701
|
+
const focus = config["focus"];
|
|
2702
|
+
const motto = config["motto"];
|
|
2703
|
+
const width = resolveBoxWidth(config["width"], context);
|
|
2704
|
+
const lines = ["", `\u{1F464} ${name.toUpperCase()}`, ""];
|
|
2705
|
+
if (github) lines.push(`GitHub: ${github}`);
|
|
2706
|
+
if (web) lines.push(`Web: ${web}`);
|
|
2707
|
+
if (focus) lines.push(`Focus: ${focus}`);
|
|
2708
|
+
if (motto) lines.push(`Motto: "${motto}"`);
|
|
2709
|
+
lines.push("");
|
|
2710
|
+
const box = createRoundedBox(lines, width);
|
|
2711
|
+
return {
|
|
2712
|
+
command: config["command"] ?? "cat /etc/profile",
|
|
2713
|
+
lines: box.split("\n"),
|
|
2714
|
+
typing: "medium",
|
|
2715
|
+
pause: "medium"
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
};
|
|
2719
|
+
|
|
2720
|
+
// src/blocks/goodbye.ts
|
|
2721
|
+
import { z as z11 } from "zod";
|
|
2722
|
+
var goodbyeSchema = z11.object({
|
|
2723
|
+
lines: z11.array(z11.string()).optional(),
|
|
2724
|
+
width: z11.number().positive().optional(),
|
|
2725
|
+
command: z11.string().optional()
|
|
2726
|
+
}).strict();
|
|
2727
|
+
var goodbyeBlock = {
|
|
2728
|
+
name: "goodbye",
|
|
2729
|
+
description: "Display a farewell message",
|
|
2730
|
+
configSchema: goodbyeSchema,
|
|
2731
|
+
render(context, config) {
|
|
2732
|
+
const lines = config["lines"] ?? [
|
|
2733
|
+
"",
|
|
2734
|
+
"Thanks for visiting!",
|
|
2735
|
+
"",
|
|
2736
|
+
"May your:",
|
|
2737
|
+
" - Code compile without warnings",
|
|
2738
|
+
" - Tests pass on first try",
|
|
2739
|
+
" - Bugs be easily reproducible",
|
|
2740
|
+
" - Coffee stay hot",
|
|
2741
|
+
" - Git conflicts be minimal",
|
|
2742
|
+
"",
|
|
2743
|
+
"See you in the commits!",
|
|
2744
|
+
""
|
|
2745
|
+
];
|
|
2746
|
+
const width = resolveBoxWidth(config["width"], context);
|
|
2747
|
+
const box = createDoubleBox(lines, width);
|
|
2748
|
+
return {
|
|
2749
|
+
command: config["command"] ?? "cat /etc/goodbye.txt",
|
|
2750
|
+
lines: box.split("\n"),
|
|
2751
|
+
typing: "medium",
|
|
2752
|
+
pause: "long"
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2755
|
+
};
|
|
2756
|
+
|
|
2757
|
+
// src/blocks/npm-install.ts
|
|
2758
|
+
import { z as z12 } from "zod";
|
|
2759
|
+
var npmInstallSchema = z12.object({
|
|
2760
|
+
package: z12.string().optional(),
|
|
2761
|
+
command: z12.string().optional()
|
|
2762
|
+
}).strict();
|
|
2763
|
+
var npmInstallBlock = {
|
|
2764
|
+
name: "npm-install",
|
|
2765
|
+
description: "Display a humorous npm install dependency tree",
|
|
2766
|
+
configSchema: npmInstallSchema,
|
|
2767
|
+
render(_context, config) {
|
|
2768
|
+
const pkg = config["package"] ?? "left-pad";
|
|
2769
|
+
const lines = [
|
|
2770
|
+
`added 847 packages in 42.0s`,
|
|
2771
|
+
"",
|
|
2772
|
+
"Dependencies resolved:",
|
|
2773
|
+
`\u251C\u2500\u2500 ${pkg}@1.0.0`,
|
|
2774
|
+
"\u2502 \u251C\u2500\u2500 is-string@1.0.0",
|
|
2775
|
+
"\u2502 \u2502 \u251C\u2500\u2500 is-object@1.0.0",
|
|
2776
|
+
"\u2502 \u2502 \u2502 \u251C\u2500\u2500 is-thing@1.0.0",
|
|
2777
|
+
"\u2502 \u2502 \u2502 \u2502 \u2514\u2500\u2500 is-anything@1.0.0",
|
|
2778
|
+
"\u2502 \u2502 \u2502 \u2502 \u2514\u2500\u2500 universe@\u221E",
|
|
2779
|
+
"\u2502 \u2514\u2500\u2500 string-utils@1.0.0",
|
|
2780
|
+
"\u2502 \u2514\u2500\u2500 ... 842 more packages",
|
|
2781
|
+
"\u2502",
|
|
2782
|
+
"[[fg:yellow]]\u26A0 3 vulnerabilities (1 moderate, 2 high)[[/fg]]",
|
|
2783
|
+
" Run `npm audit fix` to fix them",
|
|
2784
|
+
"",
|
|
2785
|
+
`Package size: 2.3 MB for a function that pads strings`,
|
|
2786
|
+
"Worth it? [[fg:red]]Absolutely not.[[/fg]] Did we do it anyway? [[fg:green]]Yes.[[/fg]]"
|
|
2787
|
+
];
|
|
2788
|
+
return {
|
|
2789
|
+
command: config["command"] ?? `npm install ${pkg}`,
|
|
2790
|
+
lines,
|
|
2791
|
+
typing: "medium",
|
|
2792
|
+
pause: "long"
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
};
|
|
2796
|
+
|
|
2797
|
+
// src/blocks/blog-post.ts
|
|
2798
|
+
import { z as z13 } from "zod";
|
|
2799
|
+
var blogPostSchema = z13.object({
|
|
2800
|
+
title: z13.string().optional(),
|
|
2801
|
+
url: z13.string().optional(),
|
|
2802
|
+
width: z13.number().positive().optional(),
|
|
2803
|
+
command: z13.string().optional()
|
|
2804
|
+
}).strict();
|
|
2805
|
+
var blogPostBlock = {
|
|
2806
|
+
name: "blog-post",
|
|
2807
|
+
description: "Display a blog post title in a box",
|
|
2808
|
+
configSchema: blogPostSchema,
|
|
2809
|
+
render(context, config) {
|
|
2810
|
+
const title = config["title"] ?? "My Latest Post";
|
|
2811
|
+
const url = config["url"] ?? "";
|
|
2812
|
+
const width = resolveBoxWidth(config["width"], context);
|
|
2813
|
+
const lines = ["", "\u{1F4DD} LATEST FROM THE BLOG", "", title];
|
|
2814
|
+
if (url) lines.push("", `\u{1F517} ${url}`);
|
|
2815
|
+
lines.push("");
|
|
2816
|
+
const box = createRoundedBox(lines, width);
|
|
2817
|
+
return {
|
|
2818
|
+
command: config["command"] ?? "curl -s blog/feed.xml | grep -m1 title",
|
|
2819
|
+
lines: box.split("\n"),
|
|
2820
|
+
typing: "slow",
|
|
2821
|
+
pause: "medium"
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
};
|
|
2825
|
+
|
|
2826
|
+
// src/blocks/national-day.ts
|
|
2827
|
+
import { z as z14 } from "zod";
|
|
2828
|
+
var dayEntrySchema = z14.object({
|
|
2829
|
+
name: z14.string(),
|
|
2830
|
+
desc: z14.string(),
|
|
2831
|
+
emoji: z14.string()
|
|
2832
|
+
}).strict();
|
|
2833
|
+
var nationalDaySchema = z14.object({
|
|
2834
|
+
days: z14.array(dayEntrySchema).optional(),
|
|
2835
|
+
width: z14.number().positive().optional(),
|
|
2836
|
+
command: z14.string().optional()
|
|
2837
|
+
}).strict();
|
|
2838
|
+
var nationalDayBlock = {
|
|
2839
|
+
name: "national-day",
|
|
2840
|
+
description: "Display a fun national day celebration",
|
|
2841
|
+
configSchema: nationalDaySchema,
|
|
2842
|
+
render(context, config) {
|
|
2843
|
+
const days = config["days"] ?? [];
|
|
2844
|
+
const width = resolveBoxWidth(config["width"], context);
|
|
2845
|
+
const dayOfYear = Math.floor(
|
|
2846
|
+
(context.now.getTime() - new Date(context.now.getFullYear(), 0, 0).getTime()) / 864e5
|
|
2847
|
+
);
|
|
2848
|
+
const day = days.length > 0 ? days[dayOfYear % days.length] : { name: "National Coding Day", desc: "Write some code!", emoji: "\u{1F4BB}" };
|
|
2849
|
+
if (!day) {
|
|
2850
|
+
return { command: "curl -s whatday.today/api | jq .today", lines: ["No days configured"] };
|
|
2851
|
+
}
|
|
2852
|
+
const nameMax = Math.max(10, width - 14);
|
|
2853
|
+
const descMax = Math.max(10, width - 8);
|
|
2854
|
+
const name = day.name.length > nameMax ? day.name.substring(0, nameMax - 3) + "..." : day.name;
|
|
2855
|
+
const desc = day.desc.length > descMax ? day.desc.substring(0, descMax - 3) + "..." : day.desc;
|
|
2856
|
+
const lines = ["", `${day.emoji} Today is ${name}`, ` "${desc}"`, ""];
|
|
2857
|
+
const box = createRoundedBox(lines, width);
|
|
2858
|
+
return {
|
|
2859
|
+
command: config["command"] ?? "curl -s whatday.today/api | jq .today",
|
|
2860
|
+
lines: box.split("\n"),
|
|
2861
|
+
typing: "medium",
|
|
2862
|
+
pause: "medium"
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
|
|
2867
|
+
// src/blocks/systemctl.ts
|
|
2868
|
+
import { z as z15 } from "zod";
|
|
2869
|
+
var systemctlSchema = z15.object({
|
|
2870
|
+
service: z15.string().optional(),
|
|
2871
|
+
description: z15.string().optional(),
|
|
2872
|
+
pid: z15.string().optional(),
|
|
2873
|
+
memory: z15.string().optional(),
|
|
2874
|
+
logs: z15.array(z15.string()).optional(),
|
|
2875
|
+
command: z15.string().optional()
|
|
2876
|
+
}).strict();
|
|
2877
|
+
var systemctlBlock = {
|
|
2878
|
+
name: "systemctl",
|
|
2879
|
+
description: "Display a systemd-style service status",
|
|
2880
|
+
configSchema: systemctlSchema,
|
|
2881
|
+
render(context, config) {
|
|
2882
|
+
const service = config["service"] ?? "dev-mode.service";
|
|
2883
|
+
const description = config["description"] ?? "Development Mode Service";
|
|
2884
|
+
const pid = config["pid"] ?? "1337";
|
|
2885
|
+
const memory = config["memory"] ?? "42.0M";
|
|
2886
|
+
const statusLines = config["logs"] ?? [
|
|
2887
|
+
"Service started successfully",
|
|
2888
|
+
"Maximum productivity achieved"
|
|
2889
|
+
];
|
|
2890
|
+
const timestamp = context.now.toISOString().slice(0, 19).replace("T", " ");
|
|
2891
|
+
const lines = [
|
|
2892
|
+
`\u25CF [[fg:cyan]]${service}[[/fg]] - ${description}`,
|
|
2893
|
+
` [[fg:purple]]Loaded:[[/fg]] loaded (/etc/systemd/${service}; [[fg:green]]enabled[[/fg]])`,
|
|
2894
|
+
` [[fg:purple]]Active:[[/fg]] [[fg:green]]active (running)[[/fg]] since boot`,
|
|
2895
|
+
` [[fg:purple]]Main PID:[[/fg]] ${pid} (${service.replace(".service", "")})`,
|
|
2896
|
+
` [[fg:purple]]Tasks:[[/fg]] \u221E`,
|
|
2897
|
+
` [[fg:purple]]Memory:[[/fg]] ${memory}`
|
|
2898
|
+
];
|
|
2899
|
+
for (const log of statusLines) {
|
|
2900
|
+
lines.push(`${timestamp} ${service}[${pid}]: [[fg:green]]\u2713[[/fg]] ${log}`);
|
|
2901
|
+
}
|
|
2902
|
+
return {
|
|
2903
|
+
command: config["command"] ?? `systemctl status ${service}`,
|
|
2904
|
+
lines,
|
|
2905
|
+
typing: "slow",
|
|
2906
|
+
pause: "long"
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
};
|
|
2910
|
+
|
|
2911
|
+
// src/blocks/github-stats.ts
|
|
2912
|
+
import { z as z16 } from "zod";
|
|
2913
|
+
var githubStatsSchema = z16.object({
|
|
2914
|
+
username: z16.string().optional(),
|
|
2915
|
+
width: z16.number().positive().optional(),
|
|
2916
|
+
command: z16.string().optional()
|
|
2917
|
+
}).strict();
|
|
2918
|
+
function formatCount(n) {
|
|
2919
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2920
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
2921
|
+
return String(n);
|
|
2922
|
+
}
|
|
2923
|
+
var githubStatsBlock = {
|
|
2924
|
+
name: "github-stats",
|
|
2925
|
+
description: "Display live GitHub user statistics",
|
|
2926
|
+
configSchema: githubStatsSchema,
|
|
2927
|
+
cacheable: true,
|
|
2928
|
+
async render(context, config) {
|
|
2929
|
+
const username = config["username"] ?? "";
|
|
2930
|
+
const width = resolveBoxWidth(config["width"], context);
|
|
2931
|
+
const timeout = context.config.fetchTimeout;
|
|
2932
|
+
if (!username) {
|
|
2933
|
+
return {
|
|
2934
|
+
command: "gh api users/???",
|
|
2935
|
+
lines: ["[[fg:yellow]]github-stats: no username configured[[/fg]]"],
|
|
2936
|
+
typing: "fast",
|
|
2937
|
+
pause: "short"
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
const url = `https://api.github.com/users/${encodeURIComponent(username)}`;
|
|
2941
|
+
const cacheKey = `github-stats:${hashConfig(config)}`;
|
|
2942
|
+
const data = context.useCache ? await context.useCache(cacheKey, () => fetchJson(url, timeout)) : await fetchJson(url, timeout);
|
|
2943
|
+
let lines;
|
|
2944
|
+
if (data) {
|
|
2945
|
+
const since = new Date(data.created_at);
|
|
2946
|
+
const sinceStr = `${since.toLocaleString("en-US", { month: "short" })} ${since.getFullYear()}`;
|
|
2947
|
+
lines = [
|
|
2948
|
+
"",
|
|
2949
|
+
`[[fg:cyan]]GITHUB: @${username}[[/fg]]`,
|
|
2950
|
+
`Repos: [[bold]]${formatCount(data.public_repos)}[[/bold]] Followers: [[bold]]${formatCount(data.followers)}[[/bold]]`,
|
|
2951
|
+
`Following: ${formatCount(data.following)} Gists: ${formatCount(data.public_gists)}`,
|
|
2952
|
+
`Member since: ${sinceStr}`,
|
|
2953
|
+
""
|
|
2954
|
+
];
|
|
2955
|
+
} else {
|
|
2956
|
+
lines = [
|
|
2957
|
+
"",
|
|
2958
|
+
`[[fg:yellow]]GitHub stats unavailable for @${username}[[/fg]]`,
|
|
2959
|
+
""
|
|
2960
|
+
];
|
|
2961
|
+
}
|
|
2962
|
+
const box = createDoubleBox(lines, width);
|
|
2963
|
+
return {
|
|
2964
|
+
command: config["command"] ?? `gh api users/${username} --jq '.public_repos,.followers'`,
|
|
2965
|
+
lines: box.split("\n"),
|
|
2966
|
+
typing: "fast",
|
|
2967
|
+
pause: "medium",
|
|
2968
|
+
fallback: !data
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
};
|
|
2972
|
+
|
|
2973
|
+
// src/blocks/github-languages.ts
|
|
2974
|
+
import { z as z17 } from "zod";
|
|
2975
|
+
var githubLanguagesSchema = z17.object({
|
|
2976
|
+
username: z17.string().optional(),
|
|
2977
|
+
top: z17.number().int().min(1).max(10).optional(),
|
|
2978
|
+
command: z17.string().optional(),
|
|
2979
|
+
barWidth: z17.number().int().min(5).max(40).optional(),
|
|
2980
|
+
fallback: z17.array(z17.object({
|
|
2981
|
+
name: z17.string(),
|
|
2982
|
+
percent: z17.number()
|
|
2983
|
+
})).optional()
|
|
2984
|
+
}).strict();
|
|
2985
|
+
var ROW_COLORS = ["cyan", "green", "yellow", "magenta", "blue"];
|
|
2986
|
+
var STATIC_FALLBACK = [
|
|
2987
|
+
{ name: "TypeScript", percent: 65 },
|
|
2988
|
+
{ name: "JavaScript", percent: 20 },
|
|
2989
|
+
{ name: "Rust", percent: 10 },
|
|
2990
|
+
{ name: "Go", percent: 5 }
|
|
2991
|
+
];
|
|
2992
|
+
function aggregate(repos, top) {
|
|
2993
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2994
|
+
for (const repo of repos) {
|
|
2995
|
+
if (!repo.language) continue;
|
|
2996
|
+
counts.set(repo.language, (counts.get(repo.language) ?? 0) + 1);
|
|
2997
|
+
}
|
|
2998
|
+
const total = Array.from(counts.values()).reduce((a, b) => a + b, 0);
|
|
2999
|
+
if (total === 0) return [];
|
|
3000
|
+
const sorted = Array.from(counts.entries()).sort((a, b) => b[1] - a[1]).slice(0, top).map(([name, count]) => ({
|
|
3001
|
+
name,
|
|
3002
|
+
percent: Math.round(count / total * 100)
|
|
3003
|
+
}));
|
|
3004
|
+
return sorted;
|
|
3005
|
+
}
|
|
3006
|
+
function renderSlices(slices, barWidth) {
|
|
3007
|
+
if (slices.length === 0) {
|
|
3008
|
+
return ["[[fg:yellow]]github-languages: no language data available[[/fg]]"];
|
|
3009
|
+
}
|
|
3010
|
+
const nameWidth = Math.max(...slices.map((s) => s.name.length));
|
|
3011
|
+
return slices.map((slice, i) => {
|
|
3012
|
+
const color = ROW_COLORS[i % ROW_COLORS.length];
|
|
3013
|
+
const filled = Math.round(slice.percent / 100 * barWidth);
|
|
3014
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
|
|
3015
|
+
const namePadded = slice.name.padEnd(nameWidth, " ");
|
|
3016
|
+
const pctPadded = `${slice.percent}%`.padStart(4, " ");
|
|
3017
|
+
return `[[fg:${color}]]${namePadded}[[/fg]] [[fg:${color}]]${bar}[[/fg]] ${pctPadded}`;
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
var githubLanguagesBlock = {
|
|
3021
|
+
name: "github-languages",
|
|
3022
|
+
description: "Top languages in a user's public repos, with percentage bars",
|
|
3023
|
+
configSchema: githubLanguagesSchema,
|
|
3024
|
+
cacheable: true,
|
|
3025
|
+
async render(context, config) {
|
|
3026
|
+
const username = config["username"] ?? "";
|
|
3027
|
+
const top = config["top"] ?? 5;
|
|
3028
|
+
const barWidth = config["barWidth"] ?? 20;
|
|
3029
|
+
const userFallback = config["fallback"];
|
|
3030
|
+
const timeout = context.config.fetchTimeout;
|
|
3031
|
+
const fallbackSlices = (userFallback && userFallback.length > 0 ? userFallback : STATIC_FALLBACK).slice(0, top);
|
|
3032
|
+
if (!username) {
|
|
3033
|
+
return {
|
|
3034
|
+
command: config["command"] ?? 'gh api users/???/repos --jq "[.[].language] | group_by(.) | ..."',
|
|
3035
|
+
lines: renderSlices(fallbackSlices, barWidth),
|
|
3036
|
+
typing: "fast",
|
|
3037
|
+
pause: "medium",
|
|
3038
|
+
fallback: true
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
const url = `https://api.github.com/users/${encodeURIComponent(username)}/repos?per_page=100`;
|
|
3042
|
+
const cacheKey = `github-languages:${hashConfig({ username, top })}`;
|
|
3043
|
+
const repos = context.useCache ? await context.useCache(cacheKey, () => fetchJson(url, timeout)) : await fetchJson(url, timeout);
|
|
3044
|
+
let slices;
|
|
3045
|
+
let usedFallback = false;
|
|
3046
|
+
if (repos && Array.isArray(repos)) {
|
|
3047
|
+
const aggregated = aggregate(repos, top);
|
|
3048
|
+
if (aggregated.length > 0) {
|
|
3049
|
+
slices = aggregated;
|
|
3050
|
+
} else {
|
|
3051
|
+
slices = fallbackSlices;
|
|
3052
|
+
usedFallback = true;
|
|
3053
|
+
}
|
|
3054
|
+
} else {
|
|
3055
|
+
slices = fallbackSlices;
|
|
3056
|
+
usedFallback = true;
|
|
3057
|
+
}
|
|
3058
|
+
return {
|
|
3059
|
+
command: config["command"] ?? `gh api users/${username}/repos --jq '[.[].language] | group_by(.) | map({k: .[0], n: length})'`,
|
|
3060
|
+
lines: renderSlices(slices, barWidth),
|
|
3061
|
+
typing: "fast",
|
|
3062
|
+
pause: "medium",
|
|
3063
|
+
fallback: usedFallback
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
};
|
|
3067
|
+
|
|
3068
|
+
// src/blocks/quote.ts
|
|
3069
|
+
import { z as z18 } from "zod";
|
|
3070
|
+
var quoteSchema = z18.object({
|
|
3071
|
+
fallback: z18.string().optional(),
|
|
3072
|
+
fallbackAuthor: z18.string().optional(),
|
|
3073
|
+
width: z18.number().positive().optional(),
|
|
3074
|
+
command: z18.string().optional()
|
|
3075
|
+
}).strict();
|
|
3076
|
+
var quoteBlock = {
|
|
3077
|
+
name: "quote",
|
|
3078
|
+
description: "Display a random inspirational quote",
|
|
3079
|
+
configSchema: quoteSchema,
|
|
3080
|
+
cacheable: true,
|
|
3081
|
+
async render(context, config) {
|
|
3082
|
+
const width = resolveBoxWidth(config["width"], context);
|
|
3083
|
+
const userFallback = config["fallback"];
|
|
3084
|
+
const userFallbackAuthor = config["fallbackAuthor"];
|
|
3085
|
+
const timeout = context.config.fetchTimeout;
|
|
3086
|
+
const url = "https://dummyjson.com/quotes/random";
|
|
3087
|
+
const cacheKey = `quote:${hashConfig(config)}`;
|
|
3088
|
+
const data = context.useCache ? await context.useCache(cacheKey, () => fetchJson(url, timeout)) : await fetchJson(url, timeout);
|
|
3089
|
+
let quote;
|
|
3090
|
+
let author;
|
|
3091
|
+
const isFallback = !data;
|
|
3092
|
+
if (data) {
|
|
3093
|
+
quote = data.quote;
|
|
3094
|
+
author = data.author;
|
|
3095
|
+
} else if (userFallback) {
|
|
3096
|
+
quote = userFallback;
|
|
3097
|
+
author = userFallbackAuthor ?? "";
|
|
3098
|
+
} else {
|
|
3099
|
+
const idx = Math.floor(context.now.getTime() / 864e5) % DEFAULT_FORTUNES.length;
|
|
3100
|
+
const entry = DEFAULT_FORTUNES[idx] ?? DEFAULT_FORTUNES[0];
|
|
3101
|
+
const m = /^(.*?)\s*—\s*(.+)$/.exec(entry);
|
|
3102
|
+
if (m) {
|
|
3103
|
+
quote = m[1];
|
|
3104
|
+
author = m[2];
|
|
3105
|
+
} else {
|
|
3106
|
+
quote = entry;
|
|
3107
|
+
author = "Unknown";
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
const maxLineWidth = width - 6;
|
|
3111
|
+
const words = quote.split(" ");
|
|
3112
|
+
const quoteLines = [];
|
|
3113
|
+
let currentLine = "";
|
|
3114
|
+
for (const word of words) {
|
|
3115
|
+
if (currentLine.length + word.length + 1 > maxLineWidth) {
|
|
3116
|
+
quoteLines.push(currentLine);
|
|
3117
|
+
currentLine = word;
|
|
3118
|
+
} else {
|
|
3119
|
+
currentLine = currentLine ? `${currentLine} ${word}` : word;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
if (currentLine) quoteLines.push(currentLine);
|
|
3123
|
+
const lines = [
|
|
3124
|
+
"",
|
|
3125
|
+
...quoteLines.map((l, i) => i === 0 ? `[[fg:green]]"${l}` : `[[fg:green]] ${l}`)
|
|
3126
|
+
];
|
|
3127
|
+
const lastIdx = lines.length - 1;
|
|
3128
|
+
lines[lastIdx] = `${lines[lastIdx]}"[[/fg]]`;
|
|
3129
|
+
lines.push(`[[fg:comment]] \u2014 ${author}[[/fg]]`);
|
|
3130
|
+
lines.push("");
|
|
3131
|
+
const box = createDoubleBox(lines, width);
|
|
3132
|
+
return {
|
|
3133
|
+
command: config["command"] ?? "fortune",
|
|
3134
|
+
lines: box.split("\n"),
|
|
3135
|
+
typing: "fast",
|
|
3136
|
+
pause: "long",
|
|
3137
|
+
fallback: isFallback
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
|
|
3142
|
+
// src/blocks/fun-fact.ts
|
|
3143
|
+
import { z as z19 } from "zod";
|
|
3144
|
+
var funFactSchema = z19.object({
|
|
3145
|
+
language: z19.string().optional(),
|
|
3146
|
+
fallback: z19.string().optional(),
|
|
3147
|
+
width: z19.number().positive().optional(),
|
|
3148
|
+
command: z19.string().optional()
|
|
3149
|
+
}).strict();
|
|
3150
|
+
var funFactBlock = {
|
|
3151
|
+
name: "fun-fact",
|
|
3152
|
+
description: "Display a random fun fact",
|
|
3153
|
+
configSchema: funFactSchema,
|
|
3154
|
+
cacheable: true,
|
|
3155
|
+
async render(context, config) {
|
|
3156
|
+
const width = resolveBoxWidth(config["width"], context);
|
|
3157
|
+
const language = config["language"] ?? "en";
|
|
3158
|
+
const fallback = config["fallback"] ?? "Honey never spoils. Archaeologists have found 3000-year-old honey that was still edible.";
|
|
3159
|
+
const timeout = context.config.fetchTimeout;
|
|
3160
|
+
const url = `https://uselessfacts.jsph.pl/api/v2/facts/random?language=${encodeURIComponent(language)}`;
|
|
3161
|
+
const cacheKey = `fun-fact:${hashConfig(config)}`;
|
|
3162
|
+
const data = context.useCache ? await context.useCache(cacheKey, () => fetchJson(url, timeout)) : await fetchJson(url, timeout);
|
|
3163
|
+
const factText = data?.text ?? fallback;
|
|
3164
|
+
const isFallback = !data?.text;
|
|
3165
|
+
const maxLineWidth = width - 6;
|
|
3166
|
+
const words = factText.split(" ");
|
|
3167
|
+
const factLines = [];
|
|
3168
|
+
let currentLine = "";
|
|
3169
|
+
for (const word of words) {
|
|
3170
|
+
if (currentLine.length + word.length + 1 > maxLineWidth) {
|
|
3171
|
+
factLines.push(currentLine);
|
|
3172
|
+
currentLine = word;
|
|
3173
|
+
} else {
|
|
3174
|
+
currentLine = currentLine ? `${currentLine} ${word}` : word;
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
if (currentLine) factLines.push(currentLine);
|
|
3178
|
+
const lines = [
|
|
3179
|
+
"",
|
|
3180
|
+
"[[fg:yellow]]DID YOU KNOW?[[/fg]]",
|
|
3181
|
+
"",
|
|
3182
|
+
...factLines,
|
|
3183
|
+
""
|
|
3184
|
+
];
|
|
3185
|
+
const box = createDoubleBox(lines, width);
|
|
3186
|
+
return {
|
|
3187
|
+
command: config["command"] ?? "curl -s uselessfacts.jsph.pl/api/v2/facts/random | jq .text",
|
|
3188
|
+
lines: box.split("\n"),
|
|
3189
|
+
typing: "fast",
|
|
3190
|
+
pause: "medium",
|
|
3191
|
+
fallback: isFallback
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
};
|
|
3195
|
+
|
|
3196
|
+
// src/blocks/vim-exit.ts
|
|
3197
|
+
import { z as z20 } from "zod";
|
|
3198
|
+
var vimExitSchema = z20.object({
|
|
3199
|
+
command: z20.string().optional()
|
|
3200
|
+
}).strict();
|
|
3201
|
+
var vimExitBlock = {
|
|
3202
|
+
name: "vim-exit",
|
|
3203
|
+
description: 'The classic "how do I exit vim?" message',
|
|
3204
|
+
configSchema: vimExitSchema,
|
|
3205
|
+
render(_context, config) {
|
|
3206
|
+
const command = config["command"] ?? "vim";
|
|
3207
|
+
return {
|
|
3208
|
+
command,
|
|
3209
|
+
lines: [
|
|
3210
|
+
"[[fg:comment]]~ VIM - Vi IMproved[[/fg]]",
|
|
3211
|
+
"[[fg:comment]]~ [[/fg]]",
|
|
3212
|
+
"[[fg:comment]]~ version 9.1.0 [[/fg]]",
|
|
3213
|
+
"[[fg:comment]]~ by Bram Moolenaar et al. [[/fg]]",
|
|
3214
|
+
"[[fg:comment]]~ [[/fg]]",
|
|
3215
|
+
"[[fg:yellow]]How do you exit this thing?[[/fg]]",
|
|
3216
|
+
"",
|
|
3217
|
+
" type [[fg:cyan]]:q[[/fg]] to quit",
|
|
3218
|
+
" type [[fg:cyan]]:q![[/fg]] to force-quit without saving",
|
|
3219
|
+
" type [[fg:cyan]]:wq[[/fg]] to save and quit",
|
|
3220
|
+
"",
|
|
3221
|
+
"[[dim]] (you were never in insert mode anyway)[[/dim]]"
|
|
3222
|
+
],
|
|
3223
|
+
pause: "medium"
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
};
|
|
3227
|
+
|
|
3228
|
+
// src/blocks/sudo-sandwich.ts
|
|
3229
|
+
import { z as z21 } from "zod";
|
|
3230
|
+
var sudoSandwichSchema = z21.object({
|
|
3231
|
+
user: z21.string().optional(),
|
|
3232
|
+
command: z21.string().optional()
|
|
3233
|
+
}).strict();
|
|
3234
|
+
var sudoSandwichBlock = {
|
|
3235
|
+
name: "sudo-sandwich",
|
|
3236
|
+
description: 'xkcd 149 "make me a sandwich" callback',
|
|
3237
|
+
configSchema: sudoSandwichSchema,
|
|
3238
|
+
render(_context, config) {
|
|
3239
|
+
const user = config["user"] ?? "dev";
|
|
3240
|
+
const command = config["command"] ?? "sudo make me a sandwich";
|
|
3241
|
+
return {
|
|
3242
|
+
command,
|
|
3243
|
+
lines: [
|
|
3244
|
+
`[sudo] password for ${user}:`,
|
|
3245
|
+
"",
|
|
3246
|
+
"[[fg:green]]OK.[[/fg]]",
|
|
3247
|
+
"",
|
|
3248
|
+
"[[dim]] (Reference: https://xkcd.com/149/)[[/dim]]"
|
|
3249
|
+
]
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
};
|
|
3253
|
+
|
|
3254
|
+
// src/blocks/rm-rf.ts
|
|
3255
|
+
import { z as z22 } from "zod";
|
|
3256
|
+
var rmRfSchema = z22.object({
|
|
3257
|
+
command: z22.string().optional()
|
|
3258
|
+
}).strict();
|
|
3259
|
+
var rmRfBlock = {
|
|
3260
|
+
name: "rm-rf",
|
|
3261
|
+
description: "Fake `rm -rf /` with dramatic narration",
|
|
3262
|
+
configSchema: rmRfSchema,
|
|
3263
|
+
render(_context, config) {
|
|
3264
|
+
const command = config["command"] ?? "sudo rm -rf / --no-preserve-root";
|
|
3265
|
+
return {
|
|
3266
|
+
command,
|
|
3267
|
+
lines: [
|
|
3268
|
+
"[[fg:red]]rm: WARNING: recursively removing root filesystem[[/fg]]",
|
|
3269
|
+
"",
|
|
3270
|
+
" removed /etc \u2713",
|
|
3271
|
+
" removed /usr/bin \u2713",
|
|
3272
|
+
" removed /home \u2713 [[dim]](you are still reading this how)[[/dim]]",
|
|
3273
|
+
" removed /var/log \u2713",
|
|
3274
|
+
" removed [[fg:yellow]]/dev/sanity[[/fg]] \u2713 [[dim]](no such file)[[/dim]]",
|
|
3275
|
+
"",
|
|
3276
|
+
"[[fg:red]]System integrity: 0%[[/fg]]",
|
|
3277
|
+
"[[dim]] Hint: update your r\xE9sum\xE9 before the next standup.[[/dim]]"
|
|
3278
|
+
],
|
|
3279
|
+
pause: "long"
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
};
|
|
3283
|
+
|
|
3284
|
+
// src/blocks/fork-bomb.ts
|
|
3285
|
+
import { z as z23 } from "zod";
|
|
3286
|
+
var forkBombSchema = z23.object({
|
|
3287
|
+
command: z23.string().optional()
|
|
3288
|
+
}).strict();
|
|
3289
|
+
var forkBombBlock = {
|
|
3290
|
+
name: "fork-bomb",
|
|
3291
|
+
description: "Mock fork-bomb warning with practical consequences",
|
|
3292
|
+
configSchema: forkBombSchema,
|
|
3293
|
+
render(_context, config) {
|
|
3294
|
+
const command = config["command"] ?? ":(){ :|:& };:";
|
|
3295
|
+
return {
|
|
3296
|
+
command,
|
|
3297
|
+
lines: [
|
|
3298
|
+
"[[fg:red]]bash: fork-bomb pattern detected[[/fg]]",
|
|
3299
|
+
"",
|
|
3300
|
+
"Running this will:",
|
|
3301
|
+
" \u2022 spawn processes until the kernel gives up",
|
|
3302
|
+
" \u2022 turn your laptop fan into a leaf blower",
|
|
3303
|
+
" \u2022 leave only [[fg:cyan]]REISUB[[/fg]] as escape",
|
|
3304
|
+
"",
|
|
3305
|
+
"[[fg:yellow]]Proceed? [y/N][[/fg]] [[dim]]_[[/dim]]",
|
|
3306
|
+
"",
|
|
3307
|
+
"[[dim]] (just kidding, nothing was actually executed)[[/dim]]"
|
|
3308
|
+
]
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
};
|
|
3312
|
+
|
|
3313
|
+
// src/blocks/kernel-panic.ts
|
|
3314
|
+
import { z as z24 } from "zod";
|
|
3315
|
+
var kernelPanicSchema = z24.object({
|
|
3316
|
+
command: z24.string().optional()
|
|
3317
|
+
}).strict();
|
|
3318
|
+
var kernelPanicBlock = {
|
|
3319
|
+
name: "kernel-panic",
|
|
3320
|
+
description: "A friendly kernel panic / BSOD spoof",
|
|
3321
|
+
configSchema: kernelPanicSchema,
|
|
3322
|
+
render(_context, config) {
|
|
3323
|
+
const command = config["command"] ?? "dmesg | tail";
|
|
3324
|
+
return {
|
|
3325
|
+
command,
|
|
3326
|
+
lines: [
|
|
3327
|
+
"[[fg:blue]][[bold]]:( [[/bold]][[/fg]]",
|
|
3328
|
+
"",
|
|
3329
|
+
"[[fg:white]]Your terminal ran into a problem and needs to think things over.[[/fg]]",
|
|
3330
|
+
"",
|
|
3331
|
+
" Stop code: [[fg:cyan]]CRITICAL_PROCESS_DIED[[/fg]]",
|
|
3332
|
+
" Process: [[fg:cyan]]hopes_and_dreams.exe[[/fg]]",
|
|
3333
|
+
" Started: Monday 09:01",
|
|
3334
|
+
" Crashed: Monday 09:02",
|
|
3335
|
+
"",
|
|
3336
|
+
"[[fg:white]]What you can try:[[/fg]]",
|
|
3337
|
+
" \u2022 Reduce coffee intake (not recommended)",
|
|
3338
|
+
" \u2022 Recompile with [[fg:cyan]]-O0[[/fg]] and pray",
|
|
3339
|
+
" \u2022 [[fg:green]]git commit --all[[/fg]] just in case",
|
|
3340
|
+
"",
|
|
3341
|
+
"[[dim]] collecting error info... 100%[[/dim]]"
|
|
3342
|
+
],
|
|
3343
|
+
pause: "long"
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
};
|
|
3347
|
+
|
|
3348
|
+
// src/blocks/segfault.ts
|
|
3349
|
+
import { z as z25 } from "zod";
|
|
3350
|
+
var segfaultSchema = z25.object({
|
|
3351
|
+
program: z25.string().optional(),
|
|
3352
|
+
command: z25.string().optional()
|
|
3353
|
+
}).strict();
|
|
3354
|
+
var segfaultBlock = {
|
|
3355
|
+
name: "segfault",
|
|
3356
|
+
description: "Fake segmentation fault with a corrupted backtrace",
|
|
3357
|
+
configSchema: segfaultSchema,
|
|
3358
|
+
render(_context, config) {
|
|
3359
|
+
const command = config["command"] ?? "./a.out";
|
|
3360
|
+
const program = config["program"] ?? "a.out";
|
|
3361
|
+
return {
|
|
3362
|
+
command,
|
|
3363
|
+
lines: [
|
|
3364
|
+
`[[fg:red]]Segmentation fault (core dumped)[[/fg]] [[dim]](${program})[[/dim]]`,
|
|
3365
|
+
"",
|
|
3366
|
+
" fault address: [[fg:cyan]]0xDEADBEEF[[/fg]]",
|
|
3367
|
+
" rip: [[fg:cyan]]0xCAFEBABE[[/fg]]",
|
|
3368
|
+
" rsp: [[fg:red]]CORRUPTED[[/fg]]",
|
|
3369
|
+
"",
|
|
3370
|
+
"[[fg:yellow]]backtrace:[[/fg]]",
|
|
3371
|
+
" #0 0x4141414141 in [[fg:cyan]]main[[/fg]] ()",
|
|
3372
|
+
" #1 0x4242424242 in [[fg:cyan]]definitely_not_a_bug[[/fg]] ()",
|
|
3373
|
+
" #2 0x??? in [[fg:red]]<corrupted>[[/fg]]",
|
|
3374
|
+
"",
|
|
3375
|
+
"[[dim]] Tip: it was a null pointer. it is always a null pointer.[[/dim]]"
|
|
3376
|
+
]
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
};
|
|
3380
|
+
|
|
3381
|
+
// src/blocks/whoami.ts
|
|
3382
|
+
import { z as z26 } from "zod";
|
|
3383
|
+
var whoamiSchema = z26.object({
|
|
3384
|
+
user: z26.string().optional(),
|
|
3385
|
+
/** UID, GID, and group memberships for the default 1-line `whoami -a` style. */
|
|
3386
|
+
uid: z26.number().int().nonnegative().optional(),
|
|
3387
|
+
gid: z26.number().int().nonnegative().optional(),
|
|
3388
|
+
groups: z26.array(z26.string()).optional(),
|
|
3389
|
+
/** Opt into the previous existential-bullets render (multi-line). */
|
|
3390
|
+
verbose: z26.boolean().optional(),
|
|
3391
|
+
/** Override the bullets shown in verbose mode. */
|
|
3392
|
+
bullets: z26.array(z26.string()).optional(),
|
|
3393
|
+
command: z26.string().optional()
|
|
3394
|
+
}).strict();
|
|
3395
|
+
var whoamiBlock = {
|
|
3396
|
+
name: "whoami",
|
|
3397
|
+
description: "Real `whoami -a` style (default) or existential bullets via `verbose: true`",
|
|
3398
|
+
configSchema: whoamiSchema,
|
|
3399
|
+
render(_context, config) {
|
|
3400
|
+
const user = config["user"] ?? "dev";
|
|
3401
|
+
const command = config["command"] ?? "whoami";
|
|
3402
|
+
const verbose = config["verbose"] ?? false;
|
|
3403
|
+
if (!verbose) {
|
|
3404
|
+
const uid = config["uid"] ?? 1e3;
|
|
3405
|
+
const gid = config["gid"] ?? 1e3;
|
|
3406
|
+
const groups = config["groups"] ?? [`${uid}(${user})`, "100(users)", "27(sudo)", "999(docker)"];
|
|
3407
|
+
return {
|
|
3408
|
+
command,
|
|
3409
|
+
lines: [
|
|
3410
|
+
`uid=${uid}([[fg:green]]${user}[[/fg]]) gid=${gid}(${user}) groups=${groups.join(",")}`
|
|
3411
|
+
]
|
|
3412
|
+
};
|
|
3413
|
+
}
|
|
3414
|
+
const bullets = config["bullets"] ?? [
|
|
3415
|
+
"a developer? [[fg:green]]sort of[[/fg]]",
|
|
3416
|
+
"a debugger? [[fg:green]]always[[/fg]]",
|
|
3417
|
+
"awake? [[fg:yellow]]debatable[[/fg]]",
|
|
3418
|
+
"productive? [[fg:red]]ask after coffee[[/fg]]"
|
|
3419
|
+
];
|
|
3420
|
+
return {
|
|
3421
|
+
command,
|
|
3422
|
+
lines: [
|
|
3423
|
+
user,
|
|
3424
|
+
"",
|
|
3425
|
+
"[[dim]]but who are you really?[[/dim]]",
|
|
3426
|
+
...bullets.map((b) => ` \u2022 ${b}`)
|
|
3427
|
+
]
|
|
3428
|
+
};
|
|
3429
|
+
}
|
|
3430
|
+
};
|
|
3431
|
+
|
|
3432
|
+
// src/blocks/last-login.ts
|
|
3433
|
+
import { z as z27 } from "zod";
|
|
3434
|
+
var loginEntrySchema = z27.object({
|
|
3435
|
+
user: z27.string().optional(),
|
|
3436
|
+
tty: z27.string().optional(),
|
|
3437
|
+
when: z27.string(),
|
|
3438
|
+
note: z27.string().optional()
|
|
3439
|
+
}).strict();
|
|
3440
|
+
var lastLoginSchema = z27.object({
|
|
3441
|
+
user: z27.string().optional(),
|
|
3442
|
+
entries: z27.array(loginEntrySchema).optional(),
|
|
3443
|
+
command: z27.string().optional()
|
|
3444
|
+
}).strict();
|
|
3445
|
+
var lastLoginBlock = {
|
|
3446
|
+
name: "last-login",
|
|
3447
|
+
description: "`last` output with embarrassing timestamps and parentheticals",
|
|
3448
|
+
configSchema: lastLoginSchema,
|
|
3449
|
+
render(_context, config) {
|
|
3450
|
+
const user = config["user"] ?? "dev";
|
|
3451
|
+
const command = config["command"] ?? "last -n 4";
|
|
3452
|
+
const entries = config["entries"] ?? [
|
|
3453
|
+
{ tty: "pts/0", when: "Tue 03:47 - still logged in", note: "debugging sleep.c" },
|
|
3454
|
+
{ tty: "pts/1", when: "Mon 23:12 - 23:13 (00:01)", note: "oops wrong button" },
|
|
3455
|
+
{ tty: "pts/0", when: "Mon 14:33 - 14:34 (00:01)", note: "left slack open" },
|
|
3456
|
+
{ tty: "pts/0", when: "Fri 09:00 - still logged in", note: "actually working?" }
|
|
3457
|
+
];
|
|
3458
|
+
const lines = entries.map((e) => {
|
|
3459
|
+
const u = (e.user ?? user).padEnd(8);
|
|
3460
|
+
const tty = (e.tty ?? "pts/0").padEnd(7);
|
|
3461
|
+
const note = e.note ? ` [[dim]](${e.note})[[/dim]]` : "";
|
|
3462
|
+
return `${u} ${tty} ${e.when}${note}`;
|
|
3463
|
+
});
|
|
3464
|
+
return { command, lines };
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
|
|
3468
|
+
// src/blocks/finger.ts
|
|
3469
|
+
import { z as z28 } from "zod";
|
|
3470
|
+
var fingerSchema = z28.object({
|
|
3471
|
+
user: z28.string().optional(),
|
|
3472
|
+
shell: z28.string().optional(),
|
|
3473
|
+
directory: z28.string().optional(),
|
|
3474
|
+
lastLogin: z28.string().optional(),
|
|
3475
|
+
mail: z28.number().int().min(0).optional(),
|
|
3476
|
+
plan: z28.array(z28.string()).optional(),
|
|
3477
|
+
command: z28.string().optional()
|
|
3478
|
+
}).strict();
|
|
3479
|
+
var fingerBlock = {
|
|
3480
|
+
name: "finger",
|
|
3481
|
+
description: "Faux finger(1) user info card",
|
|
3482
|
+
configSchema: fingerSchema,
|
|
3483
|
+
render(_context, config) {
|
|
3484
|
+
const user = config["user"] ?? "dev";
|
|
3485
|
+
const command = config["command"] ?? `finger ${user}`;
|
|
3486
|
+
const shell = config["shell"] ?? "/bin/bash (and vim, and screaming)";
|
|
3487
|
+
const directory = config["directory"] ?? `/home/${user}`;
|
|
3488
|
+
const lastLogin = config["lastLogin"] ?? "2 minutes ago, from keyboard";
|
|
3489
|
+
const mail = config["mail"] ?? 842;
|
|
3490
|
+
const plan = config["plan"] ?? [
|
|
3491
|
+
"too many tabs open",
|
|
3492
|
+
"coffee dependency: critical",
|
|
3493
|
+
"debugger of broken dreams"
|
|
3494
|
+
];
|
|
3495
|
+
return {
|
|
3496
|
+
command,
|
|
3497
|
+
lines: [
|
|
3498
|
+
`Login: ${user}`,
|
|
3499
|
+
`Directory: ${directory}`,
|
|
3500
|
+
`Shell: ${shell}`,
|
|
3501
|
+
`Last login: ${lastLogin}`,
|
|
3502
|
+
`New mail: [[fg:red]]${mail}[[/fg]] [[dim]](unread since 2019)[[/dim]]`,
|
|
3503
|
+
"",
|
|
3504
|
+
"Plan:",
|
|
3505
|
+
...plan.map((p) => ` \u2022 ${p}`)
|
|
3506
|
+
]
|
|
3507
|
+
};
|
|
3508
|
+
}
|
|
3509
|
+
};
|
|
3510
|
+
|
|
3511
|
+
// src/blocks/who.ts
|
|
3512
|
+
import { z as z29 } from "zod";
|
|
3513
|
+
var whoEntrySchema = z29.object({
|
|
3514
|
+
user: z29.string(),
|
|
3515
|
+
tty: z29.string().optional(),
|
|
3516
|
+
when: z29.string(),
|
|
3517
|
+
note: z29.string().optional()
|
|
3518
|
+
}).strict();
|
|
3519
|
+
var whoSchema = z29.object({
|
|
3520
|
+
user: z29.string().optional(),
|
|
3521
|
+
entries: z29.array(whoEntrySchema).optional(),
|
|
3522
|
+
command: z29.string().optional()
|
|
3523
|
+
}).strict();
|
|
3524
|
+
var whoBlock = {
|
|
3525
|
+
name: "who",
|
|
3526
|
+
description: "`who` output with ghost users (debugger, coffee, sanity)",
|
|
3527
|
+
configSchema: whoSchema,
|
|
3528
|
+
render(_context, config) {
|
|
3529
|
+
const user = config["user"] ?? "dev";
|
|
3530
|
+
const command = config["command"] ?? "who";
|
|
3531
|
+
const entries = config["entries"] ?? [
|
|
3532
|
+
{ user, tty: "pts/0", when: "today 09:14" },
|
|
3533
|
+
{ user: "debugger", tty: "pts/1", when: "today 09:14", note: "the ghost in the machine" },
|
|
3534
|
+
{ user: "coffee", tty: "pts/2", when: "today 03:47", note: "stimulant.exe is running" },
|
|
3535
|
+
{ user: "sanity", tty: "?", when: "last seen Friday", note: "checked out of reality" }
|
|
3536
|
+
];
|
|
3537
|
+
const lines = entries.map((e) => {
|
|
3538
|
+
const u = e.user.padEnd(9);
|
|
3539
|
+
const tty = (e.tty ?? "?").padEnd(7);
|
|
3540
|
+
const note = e.note ? ` [[dim]](${e.note})[[/dim]]` : "";
|
|
3541
|
+
return `${u} ${tty} ${e.when}${note}`;
|
|
3542
|
+
});
|
|
3543
|
+
return { command, lines };
|
|
3544
|
+
}
|
|
3545
|
+
};
|
|
3546
|
+
|
|
3547
|
+
// src/blocks/uptime.ts
|
|
3548
|
+
import { z as z30 } from "zod";
|
|
3549
|
+
var uptimeSchema = z30.object({
|
|
3550
|
+
days: z30.number().int().min(0).optional(),
|
|
3551
|
+
users: z30.number().int().min(0).optional(),
|
|
3552
|
+
load: z30.tuple([z30.number(), z30.number(), z30.number()]).optional(),
|
|
3553
|
+
lastIncident: z30.string().optional(),
|
|
3554
|
+
command: z30.string().optional()
|
|
3555
|
+
}).strict();
|
|
3556
|
+
var uptimeBlock = {
|
|
3557
|
+
name: "uptime",
|
|
3558
|
+
description: "Fake uptime with absurd numbers and SRE commentary",
|
|
3559
|
+
configSchema: uptimeSchema,
|
|
3560
|
+
render(context, config) {
|
|
3561
|
+
const days = config["days"] ?? 632;
|
|
3562
|
+
const users = config["users"] ?? 1;
|
|
3563
|
+
const load = config["load"] ?? [0.42, 0.37, 0.31];
|
|
3564
|
+
const lastIncident = config["lastIncident"] ?? "that one incident";
|
|
3565
|
+
const command = config["command"] ?? "uptime";
|
|
3566
|
+
const time = context.now.toLocaleTimeString("en-GB", { hour12: false });
|
|
3567
|
+
const loadStr = load.map((n) => n.toFixed(2)).join(", ");
|
|
3568
|
+
return {
|
|
3569
|
+
command,
|
|
3570
|
+
lines: [
|
|
3571
|
+
` ${time} up ${days} days, 3:14, ${users} user, load average: ${loadStr}`,
|
|
3572
|
+
"",
|
|
3573
|
+
`[[dim]](last reboot: ${lastIncident} \u2014 don't touch it)[[/dim]]`,
|
|
3574
|
+
"",
|
|
3575
|
+
"System health: [[fg:green]]STABLE[[/fg]]"
|
|
3576
|
+
]
|
|
3577
|
+
};
|
|
3578
|
+
}
|
|
3579
|
+
};
|
|
3580
|
+
|
|
3581
|
+
// src/blocks/matrix-rain.ts
|
|
3582
|
+
import { z as z31 } from "zod";
|
|
3583
|
+
var matrixRainSchema = z31.object({
|
|
3584
|
+
rows: z31.number().int().min(1).max(40).optional(),
|
|
3585
|
+
cols: z31.number().int().min(1).max(200).optional(),
|
|
3586
|
+
message: z31.string().optional(),
|
|
3587
|
+
command: z31.string().optional()
|
|
3588
|
+
}).strict();
|
|
3589
|
+
var KATAKANA = "\u30F2\u30A2\u30A4\u30A6\u30A8\u30AA\u30AB\u30AD\u30AF\u30B1\u30B3\u30B5\u30B7\u30B9\u30BB\u30BD\u30BF\u30C1\u30C4\u30C6\u30C8\u30CA\u30CB\u30CC\u30CD\u30CE\u30CF\u30D2\u30D5\u30D8\u30DB\u30DE\u30DF\u30E0\u30E1\u30E2\u30E4\u30E6\u30E8\u30E9\u30EA\u30EB\u30EC\u30ED\u30EF\u30F3";
|
|
3590
|
+
var DIGITS = "0123456789";
|
|
3591
|
+
var SYMBOLS = "*+-=<>:;[](){}/\\|";
|
|
3592
|
+
var POOL = (KATAKANA + DIGITS + SYMBOLS).split("");
|
|
3593
|
+
function mulberry32(seed) {
|
|
3594
|
+
let a = seed >>> 0;
|
|
3595
|
+
return () => {
|
|
3596
|
+
a = a + 1831565813 >>> 0;
|
|
3597
|
+
let t = a;
|
|
3598
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
3599
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
3600
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
var matrixRainBlock = {
|
|
3604
|
+
name: "matrix-rain",
|
|
3605
|
+
description: "Single-frame Matrix rain screen with ACCESS GRANTED footer",
|
|
3606
|
+
configSchema: matrixRainSchema,
|
|
3607
|
+
render(context, config) {
|
|
3608
|
+
const command = config["command"] ?? "./neo.sh";
|
|
3609
|
+
const rows = config["rows"] ?? 6;
|
|
3610
|
+
const cols = config["cols"] ?? 56;
|
|
3611
|
+
const message = config["message"] ?? "access granted.";
|
|
3612
|
+
const dayOfYear = Math.floor(context.now.getTime() / 864e5);
|
|
3613
|
+
const rand = mulberry32(dayOfYear);
|
|
3614
|
+
const lines = [];
|
|
3615
|
+
for (let r = 0; r < rows; r++) {
|
|
3616
|
+
let row = "";
|
|
3617
|
+
for (let c = 0; c < cols; c++) {
|
|
3618
|
+
row += POOL[Math.floor(rand() * POOL.length)] ?? " ";
|
|
3619
|
+
}
|
|
3620
|
+
lines.push(`[[fg:green]]${row}[[/fg]]`);
|
|
3621
|
+
}
|
|
3622
|
+
lines.push("");
|
|
3623
|
+
lines.push(`[[fg:green]][[bold]]> ${message}[[/bold]][[/fg]]`);
|
|
3624
|
+
return { command, lines, color: "green" };
|
|
3625
|
+
}
|
|
3626
|
+
};
|
|
3627
|
+
|
|
3628
|
+
// src/blocks/cowsay.ts
|
|
3629
|
+
import { z as z32 } from "zod";
|
|
3630
|
+
var cowsaySchema = z32.object({
|
|
3631
|
+
say: z32.string().optional(),
|
|
3632
|
+
width: z32.number().int().min(8).max(200).optional(),
|
|
3633
|
+
command: z32.string().optional()
|
|
3634
|
+
}).strict();
|
|
3635
|
+
function wrap(text, width) {
|
|
3636
|
+
const out = [];
|
|
3637
|
+
for (const para of text.split("\n")) {
|
|
3638
|
+
if (para.length === 0) {
|
|
3639
|
+
out.push("");
|
|
3640
|
+
continue;
|
|
3641
|
+
}
|
|
3642
|
+
let line = "";
|
|
3643
|
+
for (const word of para.split(/\s+/)) {
|
|
3644
|
+
if (word.length === 0) continue;
|
|
3645
|
+
if (word.length > width) {
|
|
3646
|
+
if (line) out.push(line);
|
|
3647
|
+
for (let i = 0; i < word.length; i += width) out.push(word.slice(i, i + width));
|
|
3648
|
+
line = "";
|
|
3649
|
+
continue;
|
|
3650
|
+
}
|
|
3651
|
+
const next = line ? `${line} ${word}` : word;
|
|
3652
|
+
if (next.length > width) {
|
|
3653
|
+
out.push(line);
|
|
3654
|
+
line = word;
|
|
3655
|
+
} else {
|
|
3656
|
+
line = next;
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
if (line) out.push(line);
|
|
3660
|
+
}
|
|
3661
|
+
return out;
|
|
3662
|
+
}
|
|
3663
|
+
function bubble(lines, inner) {
|
|
3664
|
+
if (lines.length === 0) return [];
|
|
3665
|
+
const out = [];
|
|
3666
|
+
out.push(" " + "_".repeat(inner + 2));
|
|
3667
|
+
if (lines.length === 1) {
|
|
3668
|
+
out.push(`< ${lines[0].padEnd(inner)} >`);
|
|
3669
|
+
} else {
|
|
3670
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3671
|
+
const left = i === 0 ? "/" : i === lines.length - 1 ? "\\" : "|";
|
|
3672
|
+
const right = i === 0 ? "\\" : i === lines.length - 1 ? "/" : "|";
|
|
3673
|
+
out.push(`${left} ${lines[i].padEnd(inner)} ${right}`);
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
out.push(" " + "-".repeat(inner + 2));
|
|
3677
|
+
return out;
|
|
3678
|
+
}
|
|
3679
|
+
var COW = [
|
|
3680
|
+
" \\ ^__^",
|
|
3681
|
+
" \\ (oo)\\_______",
|
|
3682
|
+
" (__)\\ )\\/\\",
|
|
3683
|
+
" ||----w |",
|
|
3684
|
+
" || ||"
|
|
3685
|
+
];
|
|
3686
|
+
var cowsayBlock = {
|
|
3687
|
+
name: "cowsay",
|
|
3688
|
+
description: "A cow says something. The cow is always right.",
|
|
3689
|
+
configSchema: cowsaySchema,
|
|
3690
|
+
render(_context, config) {
|
|
3691
|
+
const command = config["command"] ?? "cowsay";
|
|
3692
|
+
const say = config["say"] ?? "moo. I am a cow.";
|
|
3693
|
+
const width = Math.max(8, config["width"] ?? 40);
|
|
3694
|
+
const wrapped = wrap(say, width);
|
|
3695
|
+
const inner = Math.max(...wrapped.map((l) => l.length), 1);
|
|
3696
|
+
return {
|
|
3697
|
+
command,
|
|
3698
|
+
lines: [...bubble(wrapped, inner), ...COW]
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
};
|
|
3702
|
+
|
|
3703
|
+
// src/blocks/loading-spinner.ts
|
|
3704
|
+
import { z as z33 } from "zod";
|
|
3705
|
+
var BRAILLE_FRAMES = ["\u28F7", "\u28EF", "\u28DF", "\u287F", "\u28BF", "\u28FB", "\u28FD", "\u28FE"];
|
|
3706
|
+
var loadingSpinnerSchema = z33.object({
|
|
3707
|
+
label: z33.string().optional(),
|
|
3708
|
+
fps: z33.number().int().min(1).max(30).optional(),
|
|
3709
|
+
command: z33.string().optional(),
|
|
3710
|
+
color: z33.string().optional()
|
|
3711
|
+
}).strict();
|
|
3712
|
+
var loadingSpinnerBlock = {
|
|
3713
|
+
name: "loading-spinner",
|
|
3714
|
+
description: "A Braille spinner that cycles continuously",
|
|
3715
|
+
configSchema: loadingSpinnerSchema,
|
|
3716
|
+
render(_context, config) {
|
|
3717
|
+
const label = config["label"] ?? "loading";
|
|
3718
|
+
const fps = config["fps"] ?? 8;
|
|
3719
|
+
const command = config["command"] ?? "npm install";
|
|
3720
|
+
const color = config["color"] ?? "cyan";
|
|
3721
|
+
const frames = BRAILLE_FRAMES.map((g) => [`[[fg:${color}]]${g}[[/fg]] ${label}...`]);
|
|
3722
|
+
return {
|
|
3723
|
+
command,
|
|
3724
|
+
lines: frames[0],
|
|
3725
|
+
// static fallback (first frame)
|
|
3726
|
+
animation: { frames, fps }
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
};
|
|
3730
|
+
|
|
3731
|
+
// src/blocks/heartbeat.ts
|
|
3732
|
+
import { z as z34 } from "zod";
|
|
3733
|
+
var HEART_FRAMES = [" ", " \u2665 ", " \u2665 ", " \u2665 ", " "];
|
|
3734
|
+
var heartbeatSchema = z34.object({
|
|
3735
|
+
label: z34.string().optional(),
|
|
3736
|
+
color: z34.string().optional(),
|
|
3737
|
+
fps: z34.number().int().min(1).max(30).optional(),
|
|
3738
|
+
command: z34.string().optional()
|
|
3739
|
+
}).strict();
|
|
3740
|
+
var heartbeatBlock = {
|
|
3741
|
+
name: "heartbeat",
|
|
3742
|
+
description: "Pulsing heart for a project you love",
|
|
3743
|
+
configSchema: heartbeatSchema,
|
|
3744
|
+
render(_context, config) {
|
|
3745
|
+
const label = config["label"] ?? "with love";
|
|
3746
|
+
const color = config["color"] ?? "red";
|
|
3747
|
+
const fps = config["fps"] ?? 4;
|
|
3748
|
+
const command = config["command"] ?? "made-with --love";
|
|
3749
|
+
const frames = HEART_FRAMES.map((g) => [`[[fg:${color}]]${g}[[/fg]] ${label}`]);
|
|
3750
|
+
return {
|
|
3751
|
+
command,
|
|
3752
|
+
lines: frames[0],
|
|
3753
|
+
animation: { frames, fps }
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
};
|
|
3757
|
+
|
|
3758
|
+
// src/blocks/spinning-gear.ts
|
|
3759
|
+
import { z as z35 } from "zod";
|
|
3760
|
+
var GEAR_FRAMES = ["|", "/", "-", "\\"];
|
|
3761
|
+
var spinningGearSchema = z35.object({
|
|
3762
|
+
label: z35.string().optional(),
|
|
3763
|
+
color: z35.string().optional(),
|
|
3764
|
+
fps: z35.number().int().min(1).max(30).optional(),
|
|
3765
|
+
command: z35.string().optional()
|
|
3766
|
+
}).strict();
|
|
3767
|
+
var spinningGearBlock = {
|
|
3768
|
+
name: "spinning-gear",
|
|
3769
|
+
description: 'Rotating ASCII gear \u2014 the "machinery working" feel',
|
|
3770
|
+
configSchema: spinningGearSchema,
|
|
3771
|
+
render(_context, config) {
|
|
3772
|
+
const label = config["label"] ?? "processing";
|
|
3773
|
+
const color = config["color"] ?? "yellow";
|
|
3774
|
+
const fps = config["fps"] ?? 6;
|
|
3775
|
+
const command = config["command"] ?? "service status";
|
|
3776
|
+
const frames = GEAR_FRAMES.map((g) => [`[[fg:${color}]]${g}[[/fg]] ${label}`]);
|
|
3777
|
+
return {
|
|
3778
|
+
command,
|
|
3779
|
+
lines: frames[0],
|
|
3780
|
+
animation: { frames, fps }
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
};
|
|
3784
|
+
|
|
3785
|
+
// src/blocks/blinking-eyes.ts
|
|
3786
|
+
import { z as z36 } from "zod";
|
|
3787
|
+
var EYE_FRAMES = [
|
|
3788
|
+
"( \u25C9 \u25E1 \u25C9 )",
|
|
3789
|
+
"( \u25C9 \u25E1 \u25C9 )",
|
|
3790
|
+
"( \u25C9 \u25E1 \u25C9 )",
|
|
3791
|
+
"( \u25C9 \u25E1 \u25C9 )",
|
|
3792
|
+
"( - \u25E1 - )",
|
|
3793
|
+
"( \u25C9 \u25E1 \u25C9 )"
|
|
3794
|
+
];
|
|
3795
|
+
var blinkingEyesSchema = z36.object({
|
|
3796
|
+
label: z36.string().optional(),
|
|
3797
|
+
color: z36.string().optional(),
|
|
3798
|
+
fps: z36.number().int().min(1).max(30).optional(),
|
|
3799
|
+
command: z36.string().optional()
|
|
3800
|
+
}).strict();
|
|
3801
|
+
var blinkingEyesBlock = {
|
|
3802
|
+
name: "blinking-eyes",
|
|
3803
|
+
description: "Kaomoji eyes that blink \u2014 README mascot that feels alive",
|
|
3804
|
+
configSchema: blinkingEyesSchema,
|
|
3805
|
+
render(_context, config) {
|
|
3806
|
+
const label = config["label"] ?? "I'm watching";
|
|
3807
|
+
const color = config["color"] ?? "cyan";
|
|
3808
|
+
const fps = config["fps"] ?? 2;
|
|
3809
|
+
const command = config["command"] ?? "whoami";
|
|
3810
|
+
const frames = EYE_FRAMES.map((g) => [`[[fg:${color}]]${g}[[/fg]] ${label}`]);
|
|
3811
|
+
return {
|
|
3812
|
+
command,
|
|
3813
|
+
lines: frames[0],
|
|
3814
|
+
animation: { frames, fps }
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3817
|
+
};
|
|
3818
|
+
|
|
3819
|
+
// src/blocks/countdown.ts
|
|
3820
|
+
import { z as z37 } from "zod";
|
|
3821
|
+
var countdownSchema = z37.object({
|
|
3822
|
+
from: z37.number().int().min(1).max(60).optional(),
|
|
3823
|
+
go: z37.string().optional(),
|
|
3824
|
+
color: z37.string().optional(),
|
|
3825
|
+
fps: z37.number().int().min(1).max(10).optional(),
|
|
3826
|
+
command: z37.string().optional()
|
|
3827
|
+
}).strict();
|
|
3828
|
+
var countdownBlock = {
|
|
3829
|
+
name: "countdown",
|
|
3830
|
+
description: "T-minus N..0..go! launch stinger",
|
|
3831
|
+
configSchema: countdownSchema,
|
|
3832
|
+
render(_context, config) {
|
|
3833
|
+
const from = config["from"] ?? 5;
|
|
3834
|
+
const go = config["go"] ?? "\u{1F680} LIFT-OFF";
|
|
3835
|
+
const color = config["color"] ?? "yellow";
|
|
3836
|
+
const fps = config["fps"] ?? 1;
|
|
3837
|
+
const command = config["command"] ?? "./launch.sh";
|
|
3838
|
+
const frames = [];
|
|
3839
|
+
for (let n = from; n >= 1; n--) {
|
|
3840
|
+
frames.push([`[[fg:${color}]]T-${String(n).padStart(2)}...[[/fg]]`]);
|
|
3841
|
+
}
|
|
3842
|
+
frames.push([`[[fg:green]][[bold]]${go}[[/bold]][[/fg]]`]);
|
|
3843
|
+
return {
|
|
3844
|
+
command,
|
|
3845
|
+
lines: frames[0],
|
|
3846
|
+
animation: { frames, fps, loop: false }
|
|
3847
|
+
// play once, then freeze on GO
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
};
|
|
3851
|
+
|
|
3852
|
+
// src/blocks/sparkline.ts
|
|
3853
|
+
import { z as z38 } from "zod";
|
|
3854
|
+
var SPARK_GLYPHS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
3855
|
+
var sparklineSchema = z38.object({
|
|
3856
|
+
values: z38.array(z38.number()).min(1).optional(),
|
|
3857
|
+
label: z38.string().optional(),
|
|
3858
|
+
color: z38.string().optional(),
|
|
3859
|
+
min: z38.number().optional(),
|
|
3860
|
+
max: z38.number().optional(),
|
|
3861
|
+
command: z38.string().optional()
|
|
3862
|
+
}).strict();
|
|
3863
|
+
var sparklineBlock = {
|
|
3864
|
+
name: "sparkline",
|
|
3865
|
+
description: "ASCII sparkline (\u2581\u2582\u2584\u2587\u2586\u2585\u2583\u2582) for a metric trend",
|
|
3866
|
+
configSchema: sparklineSchema,
|
|
3867
|
+
render(_context, config) {
|
|
3868
|
+
const values = config["values"] ?? [3, 5, 2, 8, 6, 9, 7, 4, 6, 8];
|
|
3869
|
+
const label = config["label"] ?? "trend";
|
|
3870
|
+
const color = config["color"] ?? "green";
|
|
3871
|
+
const command = config["command"] ?? "metrics --tail";
|
|
3872
|
+
const min = config["min"] ?? Math.min(...values);
|
|
3873
|
+
const max = config["max"] ?? Math.max(...values);
|
|
3874
|
+
const span = max - min || 1;
|
|
3875
|
+
const glyphs = values.map((v) => {
|
|
3876
|
+
const idx = Math.min(SPARK_GLYPHS.length - 1, Math.max(0, Math.floor((v - min) / span * SPARK_GLYPHS.length)));
|
|
3877
|
+
return SPARK_GLYPHS[idx];
|
|
3878
|
+
}).join("");
|
|
3879
|
+
return {
|
|
3880
|
+
command,
|
|
3881
|
+
lines: [
|
|
3882
|
+
`${label}: [[fg:${color}]]${glyphs}[[/fg]] (min ${min}, max ${max}, n=${values.length})`
|
|
3883
|
+
]
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
3886
|
+
};
|
|
3887
|
+
|
|
3888
|
+
// src/blocks/bbs-login.ts
|
|
3889
|
+
import { z as z39 } from "zod";
|
|
3890
|
+
var bbsLoginSchema = z39.object({
|
|
3891
|
+
name: z39.string().optional(),
|
|
3892
|
+
baud: z39.number().int().optional(),
|
|
3893
|
+
motd: z39.string().optional(),
|
|
3894
|
+
command: z39.string().optional()
|
|
3895
|
+
}).strict();
|
|
3896
|
+
var bbsLoginBlock = {
|
|
3897
|
+
name: "bbs-login",
|
|
3898
|
+
description: "Retro 1980s BBS welcome banner \u2014 pairs with amber / green-phosphor",
|
|
3899
|
+
configSchema: bbsLoginSchema,
|
|
3900
|
+
render(_context, config) {
|
|
3901
|
+
const name = config["name"] ?? "TWA-BBS";
|
|
3902
|
+
const baud = config["baud"] ?? 9600;
|
|
3903
|
+
const motd = config["motd"] ?? "No new mail. 1 user online.";
|
|
3904
|
+
const command = config["command"] ?? "atdt 555-0123";
|
|
3905
|
+
return {
|
|
3906
|
+
command,
|
|
3907
|
+
lines: [
|
|
3908
|
+
"CONNECT 9600/ARQ/V.32",
|
|
3909
|
+
"",
|
|
3910
|
+
`[[fg:cyan]] _______ _ _ ____ ____ _____[[/fg]]`,
|
|
3911
|
+
`[[fg:cyan]] |__ __| | | | _ \\| _ \\/ ____|[[/fg]]`,
|
|
3912
|
+
`[[fg:cyan]] | | | | | | |_) | |_) | (___[[/fg]]`,
|
|
3913
|
+
`[[fg:cyan]] | | | |__| | __/| _ < \\___ \\[[/fg]]`,
|
|
3914
|
+
`[[fg:cyan]] |_| \\____/|_| |_| \\_\\____/[[/fg]]`,
|
|
3915
|
+
"",
|
|
3916
|
+
`[[fg:yellow]]Welcome to ${name}, ${baud} baud.[[/fg]]`,
|
|
3917
|
+
`[[dim]]${motd}[[/dim]]`,
|
|
3918
|
+
"",
|
|
3919
|
+
"[[fg:green]]login:[[/fg]] _"
|
|
3920
|
+
],
|
|
3921
|
+
typing: "slow",
|
|
3922
|
+
pause: "long"
|
|
3923
|
+
};
|
|
3924
|
+
}
|
|
3925
|
+
};
|
|
3926
|
+
|
|
3927
|
+
// src/blocks/build-badge.ts
|
|
3928
|
+
import { z as z40 } from "zod";
|
|
3929
|
+
var badgeEntrySchema = z40.object({
|
|
3930
|
+
label: z40.string(),
|
|
3931
|
+
status: z40.string(),
|
|
3932
|
+
value: z40.string().optional()
|
|
3933
|
+
}).strict();
|
|
3934
|
+
var buildBadgeSchema = z40.object({
|
|
3935
|
+
badges: z40.array(badgeEntrySchema).optional(),
|
|
3936
|
+
command: z40.string().optional()
|
|
3937
|
+
}).strict();
|
|
3938
|
+
var STATE_GLYPH = {
|
|
3939
|
+
ok: { glyph: "\u2713", color: "green" },
|
|
3940
|
+
warn: { glyph: "\u26A0", color: "yellow" },
|
|
3941
|
+
fail: { glyph: "\u2717", color: "red" }
|
|
3942
|
+
};
|
|
3943
|
+
var buildBadgeBlock = {
|
|
3944
|
+
name: "build-badge",
|
|
3945
|
+
description: "Terminal-style project status card (tests / lint / coverage)",
|
|
3946
|
+
configSchema: buildBadgeSchema,
|
|
3947
|
+
render(_context, config) {
|
|
3948
|
+
const badges = config["badges"] ?? [
|
|
3949
|
+
{ label: "tests", status: "ok", value: "243 passing" },
|
|
3950
|
+
{ label: "lint", status: "ok" },
|
|
3951
|
+
{ label: "coverage", status: "warn", value: "82%" },
|
|
3952
|
+
{ label: "build", status: "ok" }
|
|
3953
|
+
];
|
|
3954
|
+
const command = config["command"] ?? "npm run ci";
|
|
3955
|
+
const lines = badges.map((b) => {
|
|
3956
|
+
const meta = STATE_GLYPH[b.status] ?? { glyph: "\xB7", color: "comment" };
|
|
3957
|
+
const val = b.value ? ` ${b.value}` : "";
|
|
3958
|
+
return ` [[fg:${meta.color}]]${meta.glyph}[[/fg]] ${b.label.padEnd(10)}${val}`;
|
|
3959
|
+
});
|
|
3960
|
+
return {
|
|
3961
|
+
command,
|
|
3962
|
+
lines: ["", ...lines, ""]
|
|
3963
|
+
};
|
|
3964
|
+
}
|
|
3965
|
+
};
|
|
3966
|
+
|
|
3967
|
+
// src/blocks/license-card.ts
|
|
3968
|
+
import { z as z41 } from "zod";
|
|
3969
|
+
var licenseCardSchema = z41.object({
|
|
3970
|
+
license: z41.string().optional(),
|
|
3971
|
+
holder: z41.string().optional(),
|
|
3972
|
+
year: z41.number().int().min(1900).max(2100).optional(),
|
|
3973
|
+
url: z41.string().optional(),
|
|
3974
|
+
width: z41.number().positive().optional(),
|
|
3975
|
+
command: z41.string().optional()
|
|
3976
|
+
}).strict();
|
|
3977
|
+
var licenseCardBlock = {
|
|
3978
|
+
name: "license-card",
|
|
3979
|
+
description: "Boxed License / Copyright card \u2014 saves a README section",
|
|
3980
|
+
configSchema: licenseCardSchema,
|
|
3981
|
+
render(context, config) {
|
|
3982
|
+
const license = config["license"] ?? "MIT";
|
|
3983
|
+
const holder = config["holder"] ?? "You";
|
|
3984
|
+
const year = config["year"] ?? context.now.getFullYear();
|
|
3985
|
+
const url = config["url"] ?? "";
|
|
3986
|
+
const width = resolveBoxWidth(config["width"], context);
|
|
3987
|
+
const command = config["command"] ?? "cat LICENSE";
|
|
3988
|
+
const lines = [
|
|
3989
|
+
"",
|
|
3990
|
+
`[[fg:cyan]]License:[[/fg]] ${license}`,
|
|
3991
|
+
`[[fg:cyan]]Copyright:[[/fg]] \xA9 ${year} ${holder}`
|
|
3992
|
+
];
|
|
3993
|
+
if (url) lines.push(`[[fg:cyan]]Details:[[/fg]] ${url}`);
|
|
3994
|
+
lines.push("");
|
|
3995
|
+
const box = createRoundedBox(lines, width);
|
|
3996
|
+
return {
|
|
3997
|
+
command,
|
|
3998
|
+
lines: box.split("\n"),
|
|
3999
|
+
typing: "fast",
|
|
4000
|
+
pause: "medium"
|
|
4001
|
+
};
|
|
4002
|
+
}
|
|
4003
|
+
};
|
|
4004
|
+
|
|
4005
|
+
// src/blocks/ascii-clock.ts
|
|
4006
|
+
import { z as z42 } from "zod";
|
|
4007
|
+
var asciiClockSchema = z42.object({
|
|
4008
|
+
format: z42.enum(["12h", "24h"]).optional(),
|
|
4009
|
+
color: z42.string().optional(),
|
|
4010
|
+
label: z42.string().optional(),
|
|
4011
|
+
command: z42.string().optional()
|
|
4012
|
+
}).strict();
|
|
4013
|
+
var asciiClockBlock = {
|
|
4014
|
+
name: "ascii-clock",
|
|
4015
|
+
description: "HH:MM:SS clock with pulsing colon separators",
|
|
4016
|
+
configSchema: asciiClockSchema,
|
|
4017
|
+
render(context, config) {
|
|
4018
|
+
const format = config["format"] ?? "24h";
|
|
4019
|
+
const color = config["color"] ?? "cyan";
|
|
4020
|
+
const label = config["label"] ?? "";
|
|
4021
|
+
const command = config["command"] ?? "date +%H:%M:%S";
|
|
4022
|
+
const h24 = context.now.getHours();
|
|
4023
|
+
const m = String(context.now.getMinutes()).padStart(2, "0");
|
|
4024
|
+
const s = String(context.now.getSeconds()).padStart(2, "0");
|
|
4025
|
+
let hh;
|
|
4026
|
+
let suffix = "";
|
|
4027
|
+
if (format === "12h") {
|
|
4028
|
+
const h12 = h24 % 12 || 12;
|
|
4029
|
+
hh = String(h12).padStart(2, "0");
|
|
4030
|
+
suffix = h24 >= 12 ? " PM" : " AM";
|
|
4031
|
+
} else {
|
|
4032
|
+
hh = String(h24).padStart(2, "0");
|
|
4033
|
+
}
|
|
4034
|
+
const labelPart = label ? `${label} ` : "";
|
|
4035
|
+
const frameWithColons = [`${labelPart}[[fg:${color}]]${hh}:${m}:${s}${suffix}[[/fg]]`];
|
|
4036
|
+
const frameWithDots = [`${labelPart}[[fg:${color}]]${hh}\xB7${m}\xB7${s}${suffix}[[/fg]]`];
|
|
4037
|
+
return {
|
|
4038
|
+
command,
|
|
4039
|
+
lines: frameWithColons,
|
|
4040
|
+
animation: { frames: [frameWithColons, frameWithDots], fps: 1 }
|
|
4041
|
+
};
|
|
4042
|
+
}
|
|
4043
|
+
};
|
|
4044
|
+
|
|
4045
|
+
// src/blocks/progress-bar.ts
|
|
4046
|
+
import { z as z43 } from "zod";
|
|
4047
|
+
var progressBarSchema = z43.object({
|
|
4048
|
+
label: z43.string().optional(),
|
|
4049
|
+
width: z43.number().int().min(4).max(80).optional(),
|
|
4050
|
+
fps: z43.number().int().min(1).max(10).optional(),
|
|
4051
|
+
color: z43.string().optional(),
|
|
4052
|
+
command: z43.string().optional()
|
|
4053
|
+
}).strict();
|
|
4054
|
+
var progressBarBlock = {
|
|
4055
|
+
name: "progress-bar",
|
|
4056
|
+
description: "Fake build/install progress bar that fills 0% \u2192 100%",
|
|
4057
|
+
configSchema: progressBarSchema,
|
|
4058
|
+
render(_context, config) {
|
|
4059
|
+
const label = config["label"] ?? "building";
|
|
4060
|
+
const width = config["width"] ?? 20;
|
|
4061
|
+
const fps = config["fps"] ?? 4;
|
|
4062
|
+
const color = config["color"] ?? "green";
|
|
4063
|
+
const command = config["command"] ?? "./build.sh";
|
|
4064
|
+
const steps = 21;
|
|
4065
|
+
const frames = [];
|
|
4066
|
+
for (let i = 0; i < steps; i++) {
|
|
4067
|
+
const pct = Math.round(i / (steps - 1) * 100);
|
|
4068
|
+
const filled = Math.round(i / (steps - 1) * width);
|
|
4069
|
+
const empty = width - filled;
|
|
4070
|
+
const bar = `[[fg:${color}]]${"\u2588".repeat(filled)}[[/fg]]${"\u2591".repeat(empty)}`;
|
|
4071
|
+
frames.push([`${label} [${bar}] ${String(pct).padStart(3)}%`]);
|
|
4072
|
+
}
|
|
4073
|
+
return {
|
|
4074
|
+
command,
|
|
4075
|
+
lines: frames[0],
|
|
4076
|
+
animation: { frames, fps, loop: false }
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
4079
|
+
};
|
|
4080
|
+
|
|
4081
|
+
// src/blocks/bouncing-dot.ts
|
|
4082
|
+
import { z as z44 } from "zod";
|
|
4083
|
+
var bouncingDotSchema = z44.object({
|
|
4084
|
+
width: z44.number().int().min(8).max(80).optional(),
|
|
4085
|
+
glyph: z44.string().min(1).max(2).optional(),
|
|
4086
|
+
color: z44.string().optional(),
|
|
4087
|
+
fps: z44.number().int().min(1).max(30).optional(),
|
|
4088
|
+
command: z44.string().optional()
|
|
4089
|
+
}).strict();
|
|
4090
|
+
var bouncingDotBlock = {
|
|
4091
|
+
name: "bouncing-dot",
|
|
4092
|
+
description: "A single glyph bouncing left \u2194 right",
|
|
4093
|
+
configSchema: bouncingDotSchema,
|
|
4094
|
+
render(_context, config) {
|
|
4095
|
+
const width = config["width"] ?? 24;
|
|
4096
|
+
const glyph = config["glyph"] ?? "\u25CF";
|
|
4097
|
+
const color = config["color"] ?? "magenta";
|
|
4098
|
+
const fps = config["fps"] ?? 8;
|
|
4099
|
+
const command = config["command"] ?? "./bounce";
|
|
4100
|
+
const positions = [];
|
|
4101
|
+
for (let i = 0; i < width; i++) positions.push(i);
|
|
4102
|
+
for (let i = width - 2; i > 0; i--) positions.push(i);
|
|
4103
|
+
const frames = positions.map((pos) => {
|
|
4104
|
+
const left = " ".repeat(pos);
|
|
4105
|
+
const right = " ".repeat(width - pos - 1);
|
|
4106
|
+
return [`${left}[[fg:${color}]]${glyph}[[/fg]]${right}`];
|
|
4107
|
+
});
|
|
4108
|
+
return {
|
|
4109
|
+
command,
|
|
4110
|
+
lines: frames[0],
|
|
4111
|
+
animation: { frames, fps }
|
|
4112
|
+
};
|
|
4113
|
+
}
|
|
4114
|
+
};
|
|
4115
|
+
|
|
4116
|
+
// src/blocks/dice-roll.ts
|
|
4117
|
+
import { z as z45 } from "zod";
|
|
4118
|
+
var DIE_FACES = ["\u2680", "\u2681", "\u2682", "\u2683", "\u2684", "\u2685"];
|
|
4119
|
+
var diceRollSchema = z45.object({
|
|
4120
|
+
count: z45.number().int().min(1).max(8).optional(),
|
|
4121
|
+
result: z45.array(z45.number().int().min(1).max(6)).optional(),
|
|
4122
|
+
color: z45.string().optional(),
|
|
4123
|
+
fps: z45.number().int().min(1).max(30).optional(),
|
|
4124
|
+
command: z45.string().optional()
|
|
4125
|
+
}).strict();
|
|
4126
|
+
function mulberry322(seed) {
|
|
4127
|
+
let a = seed >>> 0;
|
|
4128
|
+
return () => {
|
|
4129
|
+
a = a + 1831565813 >>> 0;
|
|
4130
|
+
let t = a;
|
|
4131
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
4132
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
4133
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
4134
|
+
};
|
|
4135
|
+
}
|
|
4136
|
+
var diceRollBlock = {
|
|
4137
|
+
name: "dice-roll",
|
|
4138
|
+
description: "Roll N d6 dice with a tumble animation that lands on a result",
|
|
4139
|
+
configSchema: diceRollSchema,
|
|
4140
|
+
render(context, config) {
|
|
4141
|
+
const count = config["count"] ?? 3;
|
|
4142
|
+
const color = config["color"] ?? "yellow";
|
|
4143
|
+
const fps = config["fps"] ?? 6;
|
|
4144
|
+
const command = config["command"] ?? `roll ${count}d6`;
|
|
4145
|
+
const dayOfYear = Math.floor(context.now.getTime() / 864e5);
|
|
4146
|
+
const rand = mulberry322(dayOfYear);
|
|
4147
|
+
const result = config["result"] ?? Array.from({ length: count }, () => 1 + Math.floor(rand() * 6));
|
|
4148
|
+
const tumbleFrames = [];
|
|
4149
|
+
for (let i = 0; i < 6; i++) {
|
|
4150
|
+
const faces = Array.from({ length: count }, () => DIE_FACES[Math.floor(rand() * 6)]);
|
|
4151
|
+
tumbleFrames.push([`[[fg:${color}]]${faces.join(" ")}[[/fg]] tumbling\u2026`]);
|
|
4152
|
+
}
|
|
4153
|
+
const finalFaces = result.map((n) => DIE_FACES[Math.max(0, Math.min(5, n - 1))]).join(" ");
|
|
4154
|
+
const total = result.reduce((a, b) => a + b, 0);
|
|
4155
|
+
tumbleFrames.push([`[[fg:${color}]]${finalFaces}[[/fg]] \u2192 ${total}`]);
|
|
4156
|
+
return {
|
|
4157
|
+
command,
|
|
4158
|
+
lines: tumbleFrames[tumbleFrames.length - 1],
|
|
4159
|
+
// static fallback = final roll
|
|
4160
|
+
animation: { frames: tumbleFrames, fps, loop: false }
|
|
4161
|
+
};
|
|
4162
|
+
}
|
|
4163
|
+
};
|
|
4164
|
+
|
|
4165
|
+
// src/blocks/palette-swatch.ts
|
|
4166
|
+
import { z as z46 } from "zod";
|
|
4167
|
+
var COLOR_NAMES = [
|
|
4168
|
+
"red",
|
|
4169
|
+
"green",
|
|
4170
|
+
"yellow",
|
|
4171
|
+
"blue",
|
|
4172
|
+
"magenta",
|
|
4173
|
+
"cyan",
|
|
4174
|
+
"white",
|
|
4175
|
+
"orange",
|
|
4176
|
+
"bright_red",
|
|
4177
|
+
"bright_green",
|
|
4178
|
+
"bright_yellow",
|
|
4179
|
+
"bright_blue",
|
|
4180
|
+
"bright_magenta",
|
|
4181
|
+
"bright_cyan",
|
|
4182
|
+
"bright_white",
|
|
4183
|
+
"comment"
|
|
4184
|
+
];
|
|
4185
|
+
var paletteSwatchSchema = z46.object({
|
|
4186
|
+
glyph: z46.string().min(1).max(2).optional(),
|
|
4187
|
+
label: z46.string().optional(),
|
|
4188
|
+
command: z46.string().optional()
|
|
4189
|
+
}).strict();
|
|
4190
|
+
var paletteSwatchBlock = {
|
|
4191
|
+
name: "palette-swatch",
|
|
4192
|
+
description: "One-line render of the theme palette \u2014 useful for theme docs",
|
|
4193
|
+
configSchema: paletteSwatchSchema,
|
|
4194
|
+
render(_context, config) {
|
|
4195
|
+
const glyph = config["glyph"] ?? "\u2588";
|
|
4196
|
+
const label = config["label"] ?? "palette:";
|
|
4197
|
+
const command = config["command"] ?? "palette";
|
|
4198
|
+
const swatches = COLOR_NAMES.map((c) => `[[fg:${c}]]${glyph}[[/fg]]`).join(" ");
|
|
4199
|
+
return {
|
|
4200
|
+
command,
|
|
4201
|
+
lines: [`${label} ${swatches}`]
|
|
4202
|
+
};
|
|
4203
|
+
}
|
|
4204
|
+
};
|
|
4205
|
+
|
|
4206
|
+
// src/blocks/semver-bump.ts
|
|
4207
|
+
import { z as z47 } from "zod";
|
|
4208
|
+
var semverBumpSchema = z47.object({
|
|
4209
|
+
current: z47.string().regex(/^\d+\.\d+\.\d+$/, "current must be a semver triple like 1.2.3").optional(),
|
|
4210
|
+
highlight: z47.enum(["major", "minor", "patch", "none"]).optional(),
|
|
4211
|
+
command: z47.string().optional()
|
|
4212
|
+
}).strict();
|
|
4213
|
+
var semverBumpBlock = {
|
|
4214
|
+
name: "semver-bump",
|
|
4215
|
+
description: "Current semver + bump preview (major/minor/patch)",
|
|
4216
|
+
configSchema: semverBumpSchema,
|
|
4217
|
+
render(_context, config) {
|
|
4218
|
+
const current = config["current"] ?? "1.2.3";
|
|
4219
|
+
const highlight = config["highlight"] ?? "none";
|
|
4220
|
+
const command = config["command"] ?? "npm version --preview";
|
|
4221
|
+
const [maj, min, patch] = current.split(".").map(Number);
|
|
4222
|
+
const bumps = {
|
|
4223
|
+
major: `${maj + 1}.0.0`,
|
|
4224
|
+
minor: `${maj}.${min + 1}.0`,
|
|
4225
|
+
patch: `${maj}.${min}.${patch + 1}`
|
|
4226
|
+
};
|
|
4227
|
+
const tag = (kind) => {
|
|
4228
|
+
const color = kind === highlight ? "green" : "comment";
|
|
4229
|
+
const arrow = kind === highlight ? "\u2192" : " ";
|
|
4230
|
+
return ` [[fg:${color}]]${arrow} ${kind.padEnd(5)} ${bumps[kind]}[[/fg]]`;
|
|
4231
|
+
};
|
|
4232
|
+
return {
|
|
4233
|
+
command,
|
|
4234
|
+
lines: [
|
|
4235
|
+
`[[fg:cyan]]current[[/fg]] ${current}`,
|
|
4236
|
+
"",
|
|
4237
|
+
tag("major"),
|
|
4238
|
+
tag("minor"),
|
|
4239
|
+
tag("patch")
|
|
4240
|
+
]
|
|
4241
|
+
};
|
|
4242
|
+
}
|
|
4243
|
+
};
|
|
4244
|
+
|
|
4245
|
+
// src/blocks/ascii-calendar.ts
|
|
4246
|
+
import { z as z48 } from "zod";
|
|
4247
|
+
var MONTH_NAMES = [
|
|
4248
|
+
"January",
|
|
4249
|
+
"February",
|
|
4250
|
+
"March",
|
|
4251
|
+
"April",
|
|
4252
|
+
"May",
|
|
4253
|
+
"June",
|
|
4254
|
+
"July",
|
|
4255
|
+
"August",
|
|
4256
|
+
"September",
|
|
4257
|
+
"October",
|
|
4258
|
+
"November",
|
|
4259
|
+
"December"
|
|
4260
|
+
];
|
|
4261
|
+
var asciiCalendarSchema = z48.object({
|
|
4262
|
+
highlightColor: z48.string().optional(),
|
|
4263
|
+
weekStart: z48.enum(["sun", "mon"]).optional(),
|
|
4264
|
+
command: z48.string().optional()
|
|
4265
|
+
}).strict();
|
|
4266
|
+
var asciiCalendarBlock = {
|
|
4267
|
+
name: "ascii-calendar",
|
|
4268
|
+
description: "Current-month calendar grid with today highlighted",
|
|
4269
|
+
configSchema: asciiCalendarSchema,
|
|
4270
|
+
render(context, config) {
|
|
4271
|
+
const highlightColor = config["highlightColor"] ?? "green";
|
|
4272
|
+
const weekStart = config["weekStart"] ?? "sun";
|
|
4273
|
+
const command = config["command"] ?? "cal";
|
|
4274
|
+
const today = context.now;
|
|
4275
|
+
const year = today.getFullYear();
|
|
4276
|
+
const month = today.getMonth();
|
|
4277
|
+
const todayDate = today.getDate();
|
|
4278
|
+
const firstDow = new Date(year, month, 1).getDay();
|
|
4279
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
4280
|
+
const startOffset = weekStart === "mon" ? (firstDow + 6) % 7 : firstDow;
|
|
4281
|
+
const header = weekStart === "mon" ? "Mo Tu We Th Fr Sa Su" : "Su Mo Tu We Th Fr Sa";
|
|
4282
|
+
const title = `[[fg:cyan]]${MONTH_NAMES[month]} ${year}[[/fg]]`;
|
|
4283
|
+
const cells = [];
|
|
4284
|
+
for (let i = 0; i < startOffset; i++) cells.push(" ");
|
|
4285
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
4286
|
+
const cell = String(d).padStart(2, " ");
|
|
4287
|
+
cells.push(d === todayDate ? `[[fg:${highlightColor}]][[bold]]${cell}[[/bold]][[/fg]]` : cell);
|
|
4288
|
+
}
|
|
4289
|
+
while (cells.length % 7 !== 0) cells.push(" ");
|
|
4290
|
+
const rows = [];
|
|
4291
|
+
for (let i = 0; i < cells.length; i += 7) {
|
|
4292
|
+
rows.push(cells.slice(i, i + 7).join(" "));
|
|
4293
|
+
}
|
|
4294
|
+
return {
|
|
4295
|
+
command,
|
|
4296
|
+
lines: [title, header, ...rows],
|
|
4297
|
+
typing: "fast",
|
|
4298
|
+
pause: "medium"
|
|
4299
|
+
};
|
|
4300
|
+
}
|
|
4301
|
+
};
|
|
4302
|
+
|
|
4303
|
+
// src/blocks/toc.ts
|
|
4304
|
+
import { z as z49 } from "zod";
|
|
4305
|
+
var tocSchema = z49.object({
|
|
4306
|
+
title: z49.string().optional(),
|
|
4307
|
+
sections: z49.array(z49.string()).min(1).optional(),
|
|
4308
|
+
color: z49.string().optional(),
|
|
4309
|
+
command: z49.string().optional()
|
|
4310
|
+
}).strict();
|
|
4311
|
+
function slugify(title) {
|
|
4312
|
+
return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s/g, "-");
|
|
4313
|
+
}
|
|
4314
|
+
var tocBlock = {
|
|
4315
|
+
name: "toc",
|
|
4316
|
+
description: "Auto-generates a markdown anchor-link table of contents",
|
|
4317
|
+
configSchema: tocSchema,
|
|
4318
|
+
render(_context, config) {
|
|
4319
|
+
const title = config["title"] ?? "Table of Contents";
|
|
4320
|
+
const color = config["color"] ?? "cyan";
|
|
4321
|
+
const sections = config["sections"] ?? [
|
|
4322
|
+
"Introduction",
|
|
4323
|
+
"Getting Started",
|
|
4324
|
+
"Configuration",
|
|
4325
|
+
"Examples",
|
|
4326
|
+
"Contributing"
|
|
4327
|
+
];
|
|
4328
|
+
const command = config["command"] ?? "cat TOC.md";
|
|
4329
|
+
const lines = [`[[fg:${color}]][[bold]]${title}[[/bold]][[/fg]]`, ""];
|
|
4330
|
+
for (const s of sections) {
|
|
4331
|
+
lines.push(` \u2022 ${s} [[dim]]\u2192 #${slugify(s)}[[/dim]]`);
|
|
4332
|
+
}
|
|
4333
|
+
return {
|
|
4334
|
+
command,
|
|
4335
|
+
lines,
|
|
4336
|
+
typing: "fast",
|
|
4337
|
+
pause: "medium"
|
|
4338
|
+
};
|
|
4339
|
+
}
|
|
4340
|
+
};
|
|
4341
|
+
|
|
4342
|
+
// src/blocks/index.ts
|
|
4343
|
+
function registerBuiltinBlocks() {
|
|
4344
|
+
registerBlocks([
|
|
4345
|
+
customBlock,
|
|
4346
|
+
neofetchBlock,
|
|
4347
|
+
fortuneBlock,
|
|
4348
|
+
motdBlock,
|
|
4349
|
+
dadJokeBlock,
|
|
4350
|
+
htopBlock,
|
|
4351
|
+
profileBlock,
|
|
4352
|
+
goodbyeBlock,
|
|
4353
|
+
npmInstallBlock,
|
|
4354
|
+
blogPostBlock,
|
|
4355
|
+
nationalDayBlock,
|
|
4356
|
+
systemctlBlock,
|
|
4357
|
+
weatherBlock,
|
|
4358
|
+
githubStatsBlock,
|
|
4359
|
+
githubLanguagesBlock,
|
|
4360
|
+
quoteBlock,
|
|
4361
|
+
funFactBlock,
|
|
4362
|
+
vimExitBlock,
|
|
4363
|
+
sudoSandwichBlock,
|
|
4364
|
+
rmRfBlock,
|
|
4365
|
+
forkBombBlock,
|
|
4366
|
+
kernelPanicBlock,
|
|
4367
|
+
segfaultBlock,
|
|
4368
|
+
whoamiBlock,
|
|
4369
|
+
lastLoginBlock,
|
|
4370
|
+
fingerBlock,
|
|
4371
|
+
whoBlock,
|
|
4372
|
+
uptimeBlock,
|
|
4373
|
+
matrixRainBlock,
|
|
4374
|
+
cowsayBlock,
|
|
4375
|
+
loadingSpinnerBlock,
|
|
4376
|
+
heartbeatBlock,
|
|
4377
|
+
spinningGearBlock,
|
|
4378
|
+
blinkingEyesBlock,
|
|
4379
|
+
countdownBlock,
|
|
4380
|
+
sparklineBlock,
|
|
4381
|
+
bbsLoginBlock,
|
|
4382
|
+
buildBadgeBlock,
|
|
4383
|
+
licenseCardBlock,
|
|
4384
|
+
asciiClockBlock,
|
|
4385
|
+
progressBarBlock,
|
|
4386
|
+
bouncingDotBlock,
|
|
4387
|
+
diceRollBlock,
|
|
4388
|
+
paletteSwatchBlock,
|
|
4389
|
+
semverBumpBlock,
|
|
4390
|
+
asciiCalendarBlock,
|
|
4391
|
+
tocBlock
|
|
4392
|
+
]);
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4395
|
+
// src/index.ts
|
|
4396
|
+
registerBuiltinBlocks();
|
|
4397
|
+
var STRICT_BLOCK_CONFIG = false;
|
|
4398
|
+
function setStrictBlockConfig(enabled) {
|
|
4399
|
+
STRICT_BLOCK_CONFIG = enabled;
|
|
4400
|
+
}
|
|
4401
|
+
function validateBlockEntry(block, entry, index) {
|
|
4402
|
+
const cfg = entry.config ?? {};
|
|
4403
|
+
if (block.configSchema) {
|
|
4404
|
+
try {
|
|
4405
|
+
block.configSchema.parse(cfg);
|
|
4406
|
+
} catch (err) {
|
|
4407
|
+
if (err instanceof z50.ZodError) {
|
|
4408
|
+
const issues = err.issues.map((i) => {
|
|
4409
|
+
const path = i.path.length ? i.path.join(".") : "<root>";
|
|
4410
|
+
return ` ${path}: ${i.message}`;
|
|
4411
|
+
}).join("\n");
|
|
4412
|
+
throw new BlockConfigError(
|
|
4413
|
+
block.name,
|
|
4414
|
+
index,
|
|
4415
|
+
`Invalid config for block "${block.name}" at blocks[${index}]:
|
|
4416
|
+
${issues}`
|
|
4417
|
+
);
|
|
4418
|
+
}
|
|
4419
|
+
throw err;
|
|
4420
|
+
}
|
|
4421
|
+
return;
|
|
4422
|
+
}
|
|
4423
|
+
if (block.allowedKeys) {
|
|
4424
|
+
const allow = /* @__PURE__ */ new Set([
|
|
4425
|
+
...block.allowedKeys,
|
|
4426
|
+
// These four are universal entry-level keys handled by index.ts itself.
|
|
4427
|
+
"command",
|
|
4428
|
+
"color",
|
|
4429
|
+
"typing",
|
|
4430
|
+
"pause"
|
|
4431
|
+
]);
|
|
4432
|
+
const unknown = Object.keys(cfg).filter((k) => !allow.has(k));
|
|
4433
|
+
if (unknown.length === 0) return;
|
|
4434
|
+
const list = unknown.join(", ");
|
|
4435
|
+
const known = block.allowedKeys.join(", ");
|
|
4436
|
+
const msg = `Unknown config key(s) [${list}] for block "${block.name}" at blocks[${index}]
|
|
4437
|
+
Known keys: ${known}`;
|
|
4438
|
+
if (STRICT_BLOCK_CONFIG) {
|
|
4439
|
+
throw new BlockConfigError(block.name, index, msg);
|
|
4440
|
+
}
|
|
4441
|
+
console.warn(`[svg-terminal] warning: ${msg}`);
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
async function generate(userConfig, options = {}) {
|
|
4445
|
+
assertHasBlocks(userConfig);
|
|
4446
|
+
const config = mergeConfig(userConfig);
|
|
4447
|
+
const sequences = [];
|
|
4448
|
+
const { context, cacheRuntime } = buildContext(userConfig, config, options);
|
|
4449
|
+
for (let i = 0; i < userConfig.blocks.length; i++) {
|
|
4450
|
+
const entry = userConfig.blocks[i];
|
|
4451
|
+
const block = getBlock(entry.block);
|
|
4452
|
+
if (!block) {
|
|
4453
|
+
throw new Error(
|
|
4454
|
+
`Unknown block "${entry.block}". Register it with registerBlock() or use a built-in block.`
|
|
4455
|
+
);
|
|
4456
|
+
}
|
|
4457
|
+
validateBlockEntry(block, entry, i);
|
|
4458
|
+
const result = await block.render(context, entry.config ?? {});
|
|
4459
|
+
if (result.fallback) options.onCacheEvent?.("fallback", entry.block);
|
|
4460
|
+
sequences.push({
|
|
4461
|
+
type: "command",
|
|
4462
|
+
content: entry.command ?? result.command,
|
|
4463
|
+
typingDuration: resolveTyping(entry.typing ?? result.typing),
|
|
4464
|
+
pause: config.animation.commandOutputPause
|
|
4465
|
+
});
|
|
4466
|
+
sequences.push({
|
|
4467
|
+
type: "output",
|
|
4468
|
+
content: result.lines.join("\n"),
|
|
4469
|
+
color: entry.color ?? result.color,
|
|
4470
|
+
pause: resolvePause(entry.pause ?? result.pause),
|
|
4471
|
+
pinWidth: result.pinWidth,
|
|
4472
|
+
...result.animation ? {
|
|
4473
|
+
frames: result.animation.frames.map((f) => f.join("\n")),
|
|
4474
|
+
framesFps: Math.min(30, Math.max(1, result.animation.fps ?? 4)),
|
|
4475
|
+
framesLoop: result.animation.loop ?? true
|
|
4476
|
+
} : {}
|
|
4477
|
+
});
|
|
4478
|
+
}
|
|
4479
|
+
if (cacheRuntime) flushCache(cacheRuntime);
|
|
4480
|
+
return generateSvg(sequences, config);
|
|
4481
|
+
}
|
|
4482
|
+
function inspectCache(userConfig, configPath) {
|
|
4483
|
+
const merged = mergeConfig(userConfig);
|
|
4484
|
+
const filePath = resolveCachePath(configPath, merged.cachePath);
|
|
4485
|
+
const entries = [];
|
|
4486
|
+
for (let i = 0; i < userConfig.blocks.length; i++) {
|
|
4487
|
+
const entry = userConfig.blocks[i];
|
|
4488
|
+
const block = getBlock(entry.block);
|
|
4489
|
+
if (!block?.cacheable) continue;
|
|
4490
|
+
entries.push({
|
|
4491
|
+
blockName: entry.block,
|
|
4492
|
+
entryIndex: i,
|
|
4493
|
+
key: `${entry.block}:${hashConfig(entry.config ?? {})}`
|
|
4494
|
+
});
|
|
4495
|
+
}
|
|
4496
|
+
return { filePath, results: checkCache({ filePath, ttl: merged.cacheTTL, entries }) };
|
|
4497
|
+
}
|
|
4498
|
+
function buildContext(userConfig, config, options) {
|
|
4499
|
+
const cacheRuntime = makeCacheRuntime(config, options);
|
|
4500
|
+
const cacheEventForUseCache = options.onCacheEvent ? (evt, key) => options.onCacheEvent(evt, key) : void 0;
|
|
4501
|
+
const context = {
|
|
4502
|
+
now: options.now ?? /* @__PURE__ */ new Date(),
|
|
4503
|
+
config,
|
|
4504
|
+
variables: userConfig.variables ?? {},
|
|
4505
|
+
useCache: cacheRuntime ? makeUseCache(cacheRuntime, cacheEventForUseCache) : void 0
|
|
4506
|
+
};
|
|
4507
|
+
return { context, cacheRuntime };
|
|
4508
|
+
}
|
|
4509
|
+
function makeCacheRuntime(config, options) {
|
|
4510
|
+
const mode = options.cacheMode ?? "normal";
|
|
4511
|
+
if (mode === "off") {
|
|
4512
|
+
return { mode, filePath: "", ttl: config.cacheTTL, dirty: false };
|
|
4513
|
+
}
|
|
4514
|
+
if (!options.configPath) return void 0;
|
|
4515
|
+
let filePath;
|
|
4516
|
+
try {
|
|
4517
|
+
filePath = resolveCachePath(options.configPath, config.cachePath);
|
|
4518
|
+
} catch (err) {
|
|
4519
|
+
console.warn(`[svg-terminal] cache: disabled \u2014 ${err.message}`);
|
|
4520
|
+
return void 0;
|
|
4521
|
+
}
|
|
4522
|
+
return { mode, filePath, ttl: config.cacheTTL, dirty: false };
|
|
4523
|
+
}
|
|
4524
|
+
function assertHasBlocks(userConfig) {
|
|
4525
|
+
if (!Array.isArray(userConfig.blocks) || userConfig.blocks.length === 0) {
|
|
4526
|
+
throw new Error("generate() requires at least one block in userConfig.blocks");
|
|
4527
|
+
}
|
|
4528
|
+
}
|
|
4529
|
+
async function generateStatic(userConfig, options = {}) {
|
|
4530
|
+
assertHasBlocks(userConfig);
|
|
4531
|
+
const config = mergeConfig(userConfig);
|
|
4532
|
+
const allLines = [];
|
|
4533
|
+
const { context, cacheRuntime } = buildContext(userConfig, config, options);
|
|
4534
|
+
for (let i = 0; i < userConfig.blocks.length; i++) {
|
|
4535
|
+
const entry = userConfig.blocks[i];
|
|
4536
|
+
const block = getBlock(entry.block);
|
|
4537
|
+
if (!block) {
|
|
4538
|
+
throw new Error(
|
|
4539
|
+
`Unknown block "${entry.block}". Register it with registerBlock() or use a built-in block.`
|
|
4540
|
+
);
|
|
4541
|
+
}
|
|
4542
|
+
validateBlockEntry(block, entry, i);
|
|
4543
|
+
const result = await block.render(context, entry.config ?? {});
|
|
4544
|
+
if (result.fallback) options.onCacheEvent?.("fallback", entry.block);
|
|
4545
|
+
const prompt = config.text.prompt;
|
|
4546
|
+
allLines.push(`${prompt}${entry.command ?? result.command}`);
|
|
4547
|
+
allLines.push(...result.lines);
|
|
4548
|
+
}
|
|
4549
|
+
if (cacheRuntime) flushCache(cacheRuntime);
|
|
4550
|
+
return generateStaticSvg(allLines, config);
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4553
|
+
export {
|
|
4554
|
+
dracula,
|
|
4555
|
+
nord,
|
|
4556
|
+
monokai,
|
|
4557
|
+
amber,
|
|
4558
|
+
greenPhosphor,
|
|
4559
|
+
cyberpunk,
|
|
4560
|
+
solarizedDark,
|
|
4561
|
+
win95,
|
|
4562
|
+
catppuccin,
|
|
4563
|
+
tokyoNight,
|
|
4564
|
+
gruvbox,
|
|
4565
|
+
highContrast,
|
|
4566
|
+
themes,
|
|
4567
|
+
registerTheme,
|
|
4568
|
+
getTheme,
|
|
4569
|
+
listThemes,
|
|
4570
|
+
resolveTheme,
|
|
4571
|
+
registerBlock,
|
|
4572
|
+
getBlock,
|
|
4573
|
+
listBlocks,
|
|
4574
|
+
registerBlocks,
|
|
4575
|
+
TYPING_PRESETS,
|
|
4576
|
+
PAUSE_PRESETS,
|
|
4577
|
+
resolveTyping,
|
|
4578
|
+
resolvePause,
|
|
4579
|
+
ConfigError,
|
|
4580
|
+
BlockConfigError,
|
|
4581
|
+
loadConfig,
|
|
4582
|
+
mergeConfig,
|
|
4583
|
+
buildColorMap,
|
|
4584
|
+
parseMarkup,
|
|
4585
|
+
hasMarkup,
|
|
4586
|
+
stripMarkup,
|
|
4587
|
+
generateSvg,
|
|
4588
|
+
generateStaticSvg,
|
|
4589
|
+
createBox,
|
|
4590
|
+
createDoubleBox,
|
|
4591
|
+
createRoundedBox,
|
|
4592
|
+
createTitledBox,
|
|
4593
|
+
createAutoBox,
|
|
4594
|
+
fetchWithTimeout,
|
|
4595
|
+
fetchJson,
|
|
4596
|
+
fetchText,
|
|
4597
|
+
registerBuiltinBlocks,
|
|
4598
|
+
setStrictBlockConfig,
|
|
4599
|
+
generate,
|
|
4600
|
+
inspectCache,
|
|
4601
|
+
generateStatic
|
|
4602
|
+
};
|
|
4603
|
+
//# sourceMappingURL=chunk-IVINEQLU.js.map
|