harunire 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2122 -0
- package/package.json +43 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region src/ui/ansi.ts
|
|
6
|
+
const ansi = {
|
|
7
|
+
moveTo: (x, y) => `\x1b[${y + 1};${x + 1}H`,
|
|
8
|
+
reset: "\x1B[0m",
|
|
9
|
+
fg: (c) => c === "default" ? "\x1B[39m" : `\x1b[38;2;${c[0]};${c[1]};${c[2]}m`,
|
|
10
|
+
bg: (c) => c === "default" ? "\x1B[49m" : `\x1b[48;2;${c[0]};${c[1]};${c[2]}m`
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/ui/ascii.ts
|
|
15
|
+
function ascii(str) {
|
|
16
|
+
return str;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/ui/calc-height.ts
|
|
21
|
+
function calcHeight(widget) {
|
|
22
|
+
if (widget.type === "text") return 1;
|
|
23
|
+
if (widget.type === "row") return Math.max(1, ...widget.children.map(calcHeight));
|
|
24
|
+
if (widget.type === "col") {
|
|
25
|
+
const gap = widget.gap ?? 0;
|
|
26
|
+
const childrenHeight = widget.children.reduce((sum, c) => sum + calcHeight(c), 0);
|
|
27
|
+
const totalGap = gap * Math.max(0, widget.children.length - 1);
|
|
28
|
+
return widget.height ?? childrenHeight + totalGap;
|
|
29
|
+
}
|
|
30
|
+
if (widget.type === "grid") {
|
|
31
|
+
const numRows = widget.rows.length;
|
|
32
|
+
const totalGapY = (widget.gapY ?? 0) * Math.max(0, numRows - 1);
|
|
33
|
+
return numRows * widget.cellHeight + totalGapY;
|
|
34
|
+
}
|
|
35
|
+
if (widget.type === "canvas") return widget.height;
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/ui/display-width.ts
|
|
41
|
+
/**
|
|
42
|
+
* Calculate display width of a string, handling CJK characters and ANSI escape sequences
|
|
43
|
+
*/
|
|
44
|
+
function isFullWidth$1(char) {
|
|
45
|
+
const code = char.codePointAt(0);
|
|
46
|
+
if (code === void 0) return false;
|
|
47
|
+
if (code >= 19968 && code <= 40959) return true;
|
|
48
|
+
if (code >= 13312 && code <= 19903) return true;
|
|
49
|
+
if (code >= 12352 && code <= 12447) return true;
|
|
50
|
+
if (code >= 12448 && code <= 12543) return true;
|
|
51
|
+
if (code >= 65280 && code <= 65376) return true;
|
|
52
|
+
if (code >= 65381 && code <= 65439) return false;
|
|
53
|
+
if (code >= 12288 && code <= 12351) return true;
|
|
54
|
+
if (code >= 44032 && code <= 55215) return true;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
function displayWidth$1(str) {
|
|
58
|
+
const withoutAnsi = str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
59
|
+
let width = 0;
|
|
60
|
+
for (const char of withoutAnsi) width += isFullWidth$1(char) ? 2 : 1;
|
|
61
|
+
return width;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/ui/calc-width.ts
|
|
66
|
+
/**
|
|
67
|
+
* Calculate the natural width of a widget
|
|
68
|
+
*/
|
|
69
|
+
function calcWidth(widget) {
|
|
70
|
+
if (widget.type === "text") {
|
|
71
|
+
const px = widget.style.px ?? 0;
|
|
72
|
+
const pl = widget.style.pl ?? px;
|
|
73
|
+
const pr = widget.style.pr ?? px;
|
|
74
|
+
return pl + displayWidth$1(widget.content) + pr;
|
|
75
|
+
}
|
|
76
|
+
if (widget.type === "row") {
|
|
77
|
+
const px = widget.px ?? 0;
|
|
78
|
+
const pl = widget.pl ?? px;
|
|
79
|
+
const pr = widget.pr ?? px;
|
|
80
|
+
const gap = widget.gap ?? 0;
|
|
81
|
+
const childrenWidth = widget.children.reduce((sum, c) => sum + calcWidth(c), 0);
|
|
82
|
+
const totalGap = gap * Math.max(0, widget.children.length - 1);
|
|
83
|
+
return widget.width ?? pl + childrenWidth + totalGap + pr;
|
|
84
|
+
}
|
|
85
|
+
if (widget.type === "col") {
|
|
86
|
+
const px = widget.px ?? 0;
|
|
87
|
+
const pl = widget.pl ?? px;
|
|
88
|
+
const pr = widget.pr ?? px;
|
|
89
|
+
const childMaxWidth = Math.max(0, ...widget.children.map(calcWidth));
|
|
90
|
+
return widget.width ?? pl + childMaxWidth + pr;
|
|
91
|
+
}
|
|
92
|
+
if (widget.type === "grid") {
|
|
93
|
+
const numCols = Math.max(0, ...widget.rows.map((r) => r.length));
|
|
94
|
+
const totalGapX = (widget.gapX ?? 0) * Math.max(0, numCols - 1);
|
|
95
|
+
return numCols * widget.cellWidth + totalGapX;
|
|
96
|
+
}
|
|
97
|
+
if (widget.type === "canvas") return widget.width;
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/ui/errors.ts
|
|
103
|
+
/**
|
|
104
|
+
* Custom error classes for open-tui
|
|
105
|
+
*/
|
|
106
|
+
var HasciiUiError = class extends Error {
|
|
107
|
+
constructor(message) {
|
|
108
|
+
super(message);
|
|
109
|
+
this.name = "HasciiUiError";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var HasciiUiInvalidColorError = class extends HasciiUiError {
|
|
113
|
+
input;
|
|
114
|
+
constructor(input) {
|
|
115
|
+
super(`Invalid color: ${JSON.stringify(input)}`);
|
|
116
|
+
this.name = "HasciiUiInvalidColorError";
|
|
117
|
+
this.input = input;
|
|
118
|
+
Object.freeze(this);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var HasciiUiOverflowError = class extends HasciiUiError {
|
|
122
|
+
content;
|
|
123
|
+
maxWidth;
|
|
124
|
+
actualWidth;
|
|
125
|
+
constructor(content, maxWidth, actualWidth) {
|
|
126
|
+
super(`Content overflow: "${content}" requires ${actualWidth} chars but only ${maxWidth} available`);
|
|
127
|
+
this.name = "HasciiUiOverflowError";
|
|
128
|
+
this.content = content;
|
|
129
|
+
this.maxWidth = maxWidth;
|
|
130
|
+
this.actualWidth = actualWidth;
|
|
131
|
+
Object.freeze(this);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/ui/color.ts
|
|
137
|
+
/**
|
|
138
|
+
* Color normalization utilities
|
|
139
|
+
*/
|
|
140
|
+
const NAMED_COLORS = {
|
|
141
|
+
black: [
|
|
142
|
+
0,
|
|
143
|
+
0,
|
|
144
|
+
0
|
|
145
|
+
],
|
|
146
|
+
white: [
|
|
147
|
+
255,
|
|
148
|
+
255,
|
|
149
|
+
255
|
|
150
|
+
],
|
|
151
|
+
red: [
|
|
152
|
+
255,
|
|
153
|
+
0,
|
|
154
|
+
0
|
|
155
|
+
],
|
|
156
|
+
green: [
|
|
157
|
+
0,
|
|
158
|
+
128,
|
|
159
|
+
0
|
|
160
|
+
],
|
|
161
|
+
blue: [
|
|
162
|
+
0,
|
|
163
|
+
0,
|
|
164
|
+
255
|
|
165
|
+
],
|
|
166
|
+
yellow: [
|
|
167
|
+
255,
|
|
168
|
+
255,
|
|
169
|
+
0
|
|
170
|
+
],
|
|
171
|
+
cyan: [
|
|
172
|
+
0,
|
|
173
|
+
255,
|
|
174
|
+
255
|
|
175
|
+
],
|
|
176
|
+
magenta: [
|
|
177
|
+
255,
|
|
178
|
+
0,
|
|
179
|
+
255
|
|
180
|
+
],
|
|
181
|
+
gray: [
|
|
182
|
+
128,
|
|
183
|
+
128,
|
|
184
|
+
128
|
|
185
|
+
],
|
|
186
|
+
grey: [
|
|
187
|
+
128,
|
|
188
|
+
128,
|
|
189
|
+
128
|
|
190
|
+
],
|
|
191
|
+
darkgray: [
|
|
192
|
+
64,
|
|
193
|
+
64,
|
|
194
|
+
64
|
|
195
|
+
],
|
|
196
|
+
darkgrey: [
|
|
197
|
+
64,
|
|
198
|
+
64,
|
|
199
|
+
64
|
|
200
|
+
],
|
|
201
|
+
lightgray: [
|
|
202
|
+
192,
|
|
203
|
+
192,
|
|
204
|
+
192
|
|
205
|
+
],
|
|
206
|
+
lightgrey: [
|
|
207
|
+
192,
|
|
208
|
+
192,
|
|
209
|
+
192
|
|
210
|
+
],
|
|
211
|
+
orange: [
|
|
212
|
+
255,
|
|
213
|
+
165,
|
|
214
|
+
0
|
|
215
|
+
],
|
|
216
|
+
pink: [
|
|
217
|
+
255,
|
|
218
|
+
192,
|
|
219
|
+
203
|
|
220
|
+
],
|
|
221
|
+
purple: [
|
|
222
|
+
128,
|
|
223
|
+
0,
|
|
224
|
+
128
|
|
225
|
+
],
|
|
226
|
+
brown: [
|
|
227
|
+
139,
|
|
228
|
+
69,
|
|
229
|
+
19
|
|
230
|
+
],
|
|
231
|
+
lime: [
|
|
232
|
+
0,
|
|
233
|
+
255,
|
|
234
|
+
0
|
|
235
|
+
],
|
|
236
|
+
navy: [
|
|
237
|
+
0,
|
|
238
|
+
0,
|
|
239
|
+
128
|
|
240
|
+
],
|
|
241
|
+
teal: [
|
|
242
|
+
0,
|
|
243
|
+
128,
|
|
244
|
+
128
|
|
245
|
+
],
|
|
246
|
+
olive: [
|
|
247
|
+
128,
|
|
248
|
+
128,
|
|
249
|
+
0
|
|
250
|
+
],
|
|
251
|
+
maroon: [
|
|
252
|
+
128,
|
|
253
|
+
0,
|
|
254
|
+
0
|
|
255
|
+
],
|
|
256
|
+
aqua: [
|
|
257
|
+
0,
|
|
258
|
+
255,
|
|
259
|
+
255
|
|
260
|
+
],
|
|
261
|
+
silver: [
|
|
262
|
+
192,
|
|
263
|
+
192,
|
|
264
|
+
192
|
|
265
|
+
],
|
|
266
|
+
gold: [
|
|
267
|
+
255,
|
|
268
|
+
215,
|
|
269
|
+
0
|
|
270
|
+
]
|
|
271
|
+
};
|
|
272
|
+
const HEX_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
273
|
+
function parseHex(hex) {
|
|
274
|
+
const match = HEX_PATTERN.exec(hex);
|
|
275
|
+
if (!match || !match[1]) throw new HasciiUiInvalidColorError(hex);
|
|
276
|
+
let hexValue = match[1];
|
|
277
|
+
if (hexValue.length === 3) hexValue = hexValue.split("").map((c) => c + c).join("");
|
|
278
|
+
return [
|
|
279
|
+
Number.parseInt(hexValue.slice(0, 2), 16),
|
|
280
|
+
Number.parseInt(hexValue.slice(2, 4), 16),
|
|
281
|
+
Number.parseInt(hexValue.slice(4, 6), 16)
|
|
282
|
+
];
|
|
283
|
+
}
|
|
284
|
+
function isRGB(value) {
|
|
285
|
+
if (!Array.isArray(value) || value.length !== 3) return false;
|
|
286
|
+
return value.every((n) => typeof n === "number" && n >= 0 && n <= 255 && Number.isInteger(n));
|
|
287
|
+
}
|
|
288
|
+
function normalizeColor(input) {
|
|
289
|
+
if (input === "default") return "default";
|
|
290
|
+
if (isRGB(input)) return input;
|
|
291
|
+
if (typeof input === "string") {
|
|
292
|
+
if (input.startsWith("#")) return parseHex(input);
|
|
293
|
+
const named = NAMED_COLORS[input.toLowerCase()];
|
|
294
|
+
if (named) return named;
|
|
295
|
+
}
|
|
296
|
+
throw new HasciiUiInvalidColorError(input);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/ui/is-options.ts
|
|
301
|
+
function isOptions(x) {
|
|
302
|
+
if (typeof x !== "object" || x === null) return false;
|
|
303
|
+
if ("type" in x) return false;
|
|
304
|
+
return "justify" in x || "items" in x || "width" in x || "height" in x || "gap" in x || "px" in x || "pl" in x || "pr" in x || "bg" in x;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/ui/text.ts
|
|
309
|
+
/**
|
|
310
|
+
* Create a text widget
|
|
311
|
+
*/
|
|
312
|
+
function normalizeStyle(input) {
|
|
313
|
+
return {
|
|
314
|
+
px: input.px,
|
|
315
|
+
pl: input.pl,
|
|
316
|
+
pr: input.pr,
|
|
317
|
+
fg: input.fg !== void 0 ? normalizeColor(input.fg) : void 0,
|
|
318
|
+
bg: input.bg !== void 0 ? normalizeColor(input.bg) : void 0
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function text(styleOrContent, content) {
|
|
322
|
+
if (typeof styleOrContent === "string") return {
|
|
323
|
+
type: "text",
|
|
324
|
+
content: ascii(styleOrContent),
|
|
325
|
+
style: {}
|
|
326
|
+
};
|
|
327
|
+
return {
|
|
328
|
+
type: "text",
|
|
329
|
+
content: ascii(content ?? ""),
|
|
330
|
+
style: normalizeStyle(styleOrContent)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/ui/col.ts
|
|
336
|
+
/**
|
|
337
|
+
* Create a col widget (vertical layout)
|
|
338
|
+
*/
|
|
339
|
+
function col(optionsOrChild, ...rest) {
|
|
340
|
+
if (optionsOrChild === void 0) return {
|
|
341
|
+
type: "col",
|
|
342
|
+
children: []
|
|
343
|
+
};
|
|
344
|
+
if (isOptions(optionsOrChild)) return {
|
|
345
|
+
type: "col",
|
|
346
|
+
children: rest.map((c) => typeof c === "string" ? text(c) : c),
|
|
347
|
+
justify: optionsOrChild.justify,
|
|
348
|
+
items: optionsOrChild.items,
|
|
349
|
+
width: optionsOrChild.width,
|
|
350
|
+
height: optionsOrChild.height,
|
|
351
|
+
gap: optionsOrChild.gap,
|
|
352
|
+
px: optionsOrChild.px,
|
|
353
|
+
pl: optionsOrChild.pl,
|
|
354
|
+
pr: optionsOrChild.pr,
|
|
355
|
+
bg: optionsOrChild.bg !== void 0 ? normalizeColor(optionsOrChild.bg) : void 0
|
|
356
|
+
};
|
|
357
|
+
return {
|
|
358
|
+
type: "col",
|
|
359
|
+
children: [optionsOrChild, ...rest].map((c) => typeof c === "string" ? text(c) : c)
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/ui/palette.ts
|
|
365
|
+
const PALETTE = Object.freeze([
|
|
366
|
+
[
|
|
367
|
+
0,
|
|
368
|
+
0,
|
|
369
|
+
0
|
|
370
|
+
],
|
|
371
|
+
[
|
|
372
|
+
128,
|
|
373
|
+
0,
|
|
374
|
+
0
|
|
375
|
+
],
|
|
376
|
+
[
|
|
377
|
+
0,
|
|
378
|
+
128,
|
|
379
|
+
0
|
|
380
|
+
],
|
|
381
|
+
[
|
|
382
|
+
128,
|
|
383
|
+
128,
|
|
384
|
+
0
|
|
385
|
+
],
|
|
386
|
+
[
|
|
387
|
+
0,
|
|
388
|
+
0,
|
|
389
|
+
128
|
|
390
|
+
],
|
|
391
|
+
[
|
|
392
|
+
128,
|
|
393
|
+
0,
|
|
394
|
+
128
|
|
395
|
+
],
|
|
396
|
+
[
|
|
397
|
+
0,
|
|
398
|
+
128,
|
|
399
|
+
128
|
|
400
|
+
],
|
|
401
|
+
[
|
|
402
|
+
192,
|
|
403
|
+
192,
|
|
404
|
+
192
|
|
405
|
+
],
|
|
406
|
+
[
|
|
407
|
+
128,
|
|
408
|
+
128,
|
|
409
|
+
128
|
|
410
|
+
],
|
|
411
|
+
[
|
|
412
|
+
255,
|
|
413
|
+
0,
|
|
414
|
+
0
|
|
415
|
+
],
|
|
416
|
+
[
|
|
417
|
+
0,
|
|
418
|
+
255,
|
|
419
|
+
0
|
|
420
|
+
],
|
|
421
|
+
[
|
|
422
|
+
255,
|
|
423
|
+
255,
|
|
424
|
+
0
|
|
425
|
+
],
|
|
426
|
+
[
|
|
427
|
+
0,
|
|
428
|
+
0,
|
|
429
|
+
255
|
|
430
|
+
],
|
|
431
|
+
[
|
|
432
|
+
255,
|
|
433
|
+
0,
|
|
434
|
+
255
|
|
435
|
+
],
|
|
436
|
+
[
|
|
437
|
+
0,
|
|
438
|
+
255,
|
|
439
|
+
255
|
|
440
|
+
],
|
|
441
|
+
[
|
|
442
|
+
255,
|
|
443
|
+
255,
|
|
444
|
+
255
|
|
445
|
+
]
|
|
446
|
+
]);
|
|
447
|
+
function paletteToRgb(index) {
|
|
448
|
+
return PALETTE[index % PALETTE.length] ?? [
|
|
449
|
+
0,
|
|
450
|
+
0,
|
|
451
|
+
0
|
|
452
|
+
];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/ui/render-widget.ts
|
|
457
|
+
/**
|
|
458
|
+
* Internal recursive widget renderer
|
|
459
|
+
*/
|
|
460
|
+
function renderWidget(widget, x, y, maxWidth, maxHeight, ctx) {
|
|
461
|
+
if (maxWidth <= 0 || maxHeight <= 0) return ctx;
|
|
462
|
+
if (widget.type === "text") {
|
|
463
|
+
const px = widget.style.px ?? 0;
|
|
464
|
+
const pl = widget.style.pl ?? px;
|
|
465
|
+
const pr = widget.style.pr ?? px;
|
|
466
|
+
const availableForPadding = Math.min(pl + pr, maxWidth);
|
|
467
|
+
const actualPl = Math.min(pl, availableForPadding);
|
|
468
|
+
const actualPr = Math.min(pr, Math.max(0, availableForPadding - actualPl));
|
|
469
|
+
const availableForContent = Math.max(0, maxWidth - actualPl - actualPr);
|
|
470
|
+
const contentLength = displayWidth$1(widget.content);
|
|
471
|
+
if (contentLength > availableForContent) throw new HasciiUiOverflowError(widget.content, availableForContent, contentLength);
|
|
472
|
+
if (widget.style.bg) ctx.output += ansi.bg(widget.style.bg);
|
|
473
|
+
if (widget.style.fg) ctx.output += ansi.fg(widget.style.fg);
|
|
474
|
+
ctx.output += ansi.moveTo(x, y);
|
|
475
|
+
ctx.output += " ".repeat(actualPl) + widget.content + " ".repeat(actualPr);
|
|
476
|
+
return ctx;
|
|
477
|
+
}
|
|
478
|
+
if (widget.type === "row") {
|
|
479
|
+
const rowWidth = Math.min(widget.width ?? calcWidth(widget), maxWidth);
|
|
480
|
+
const rowHeight = Math.min(calcHeight(widget), maxHeight);
|
|
481
|
+
if (widget.bg) {
|
|
482
|
+
ctx.output += ansi.bg(widget.bg);
|
|
483
|
+
const emptyLine = " ".repeat(rowWidth);
|
|
484
|
+
for (let row$1 = 0; row$1 < rowHeight; row$1++) ctx.output += ansi.moveTo(x, y + row$1) + emptyLine;
|
|
485
|
+
}
|
|
486
|
+
const px = widget.px ?? 0;
|
|
487
|
+
const pl = widget.pl ?? px;
|
|
488
|
+
const pr = widget.pr ?? px;
|
|
489
|
+
const contentWidth = rowWidth - pl - pr;
|
|
490
|
+
const gap = widget.gap ?? 0;
|
|
491
|
+
const childrenWidth = widget.children.reduce((sum, c) => sum + calcWidth(c), 0);
|
|
492
|
+
const totalWidth = childrenWidth + gap * Math.max(0, widget.children.length - 1);
|
|
493
|
+
let startX = x + pl;
|
|
494
|
+
let actualGap = gap;
|
|
495
|
+
if (widget.justify === "center") startX = x + pl + Math.floor((contentWidth - totalWidth) / 2);
|
|
496
|
+
else if (widget.justify === "end") startX = x + pl + (contentWidth - totalWidth);
|
|
497
|
+
else if (widget.justify === "space-between" && widget.children.length > 1) {
|
|
498
|
+
const extraSpace = contentWidth - childrenWidth;
|
|
499
|
+
actualGap = Math.floor(extraSpace / (widget.children.length - 1));
|
|
500
|
+
}
|
|
501
|
+
let cx = startX;
|
|
502
|
+
let remainingWidth = contentWidth;
|
|
503
|
+
for (let i = 0; i < widget.children.length; i++) {
|
|
504
|
+
const child = widget.children[i];
|
|
505
|
+
if (child) {
|
|
506
|
+
const childNaturalWidth = calcWidth(child);
|
|
507
|
+
const childNaturalHeight = calcHeight(child);
|
|
508
|
+
const childMaxWidth = Math.min(childNaturalWidth, remainingWidth);
|
|
509
|
+
let cy = y;
|
|
510
|
+
if (widget.items === "center") cy = y + Math.floor((rowHeight - childNaturalHeight) / 2);
|
|
511
|
+
else if (widget.items === "end") cy = y + (rowHeight - childNaturalHeight);
|
|
512
|
+
ctx = renderWidget(child, cx, cy, childMaxWidth, rowHeight, ctx);
|
|
513
|
+
cx += childMaxWidth;
|
|
514
|
+
remainingWidth -= childMaxWidth;
|
|
515
|
+
}
|
|
516
|
+
if (i < widget.children.length - 1) {
|
|
517
|
+
cx += actualGap;
|
|
518
|
+
remainingWidth -= actualGap;
|
|
519
|
+
}
|
|
520
|
+
if (remainingWidth <= 0) break;
|
|
521
|
+
}
|
|
522
|
+
return ctx;
|
|
523
|
+
}
|
|
524
|
+
if (widget.type === "col") {
|
|
525
|
+
const colWidth = Math.min(widget.width ?? calcWidth(widget), maxWidth);
|
|
526
|
+
const colHeight = Math.min(widget.height ?? calcHeight(widget), maxHeight);
|
|
527
|
+
if (widget.bg) {
|
|
528
|
+
ctx.output += ansi.bg(widget.bg);
|
|
529
|
+
const emptyLine = " ".repeat(colWidth);
|
|
530
|
+
for (let row$1 = 0; row$1 < colHeight; row$1++) ctx.output += ansi.moveTo(x, y + row$1) + emptyLine;
|
|
531
|
+
}
|
|
532
|
+
const px = widget.px ?? 0;
|
|
533
|
+
const pl = widget.pl ?? px;
|
|
534
|
+
const pr = widget.pr ?? px;
|
|
535
|
+
const contentWidth = colWidth - pl - pr;
|
|
536
|
+
const gap = widget.gap ?? 0;
|
|
537
|
+
const childrenHeight = widget.children.reduce((sum, c) => sum + calcHeight(c), 0);
|
|
538
|
+
const totalHeight = childrenHeight + gap * Math.max(0, widget.children.length - 1);
|
|
539
|
+
let startY = y;
|
|
540
|
+
let actualGap = gap;
|
|
541
|
+
if (widget.justify === "space-between" && widget.children.length > 1) {
|
|
542
|
+
const extraSpace = colHeight - childrenHeight;
|
|
543
|
+
actualGap = Math.floor(extraSpace / (widget.children.length - 1));
|
|
544
|
+
} else if (widget.justify === "center" && totalHeight < colHeight) startY = y + Math.floor((colHeight - totalHeight) / 2);
|
|
545
|
+
else if (widget.justify === "end" && totalHeight < colHeight) startY = y + (colHeight - totalHeight);
|
|
546
|
+
let cy = startY;
|
|
547
|
+
let remainingHeight = colHeight;
|
|
548
|
+
for (let i = 0; i < widget.children.length; i++) {
|
|
549
|
+
const child = widget.children[i];
|
|
550
|
+
if (child) {
|
|
551
|
+
const childNaturalHeight = calcHeight(child);
|
|
552
|
+
const childMaxHeight = Math.min(childNaturalHeight, remainingHeight);
|
|
553
|
+
const childWidth = calcWidth(child);
|
|
554
|
+
let cx = x + pl;
|
|
555
|
+
if (widget.items === "center") cx = x + pl + Math.floor((contentWidth - childWidth) / 2);
|
|
556
|
+
else if (widget.items === "end") cx = x + pl + (contentWidth - childWidth);
|
|
557
|
+
ctx = renderWidget(child, cx, cy, contentWidth, childMaxHeight, ctx);
|
|
558
|
+
cy += childMaxHeight;
|
|
559
|
+
remainingHeight -= childMaxHeight;
|
|
560
|
+
}
|
|
561
|
+
if (i < widget.children.length - 1) {
|
|
562
|
+
cy += actualGap;
|
|
563
|
+
remainingHeight -= actualGap;
|
|
564
|
+
}
|
|
565
|
+
if (remainingHeight <= 0) break;
|
|
566
|
+
}
|
|
567
|
+
return ctx;
|
|
568
|
+
}
|
|
569
|
+
if (widget.type === "grid") {
|
|
570
|
+
const gridWidth = calcWidth(widget);
|
|
571
|
+
const gridHeight = calcHeight(widget);
|
|
572
|
+
const gapX = widget.gapX ?? 0;
|
|
573
|
+
const gapY = widget.gapY ?? 0;
|
|
574
|
+
if (widget.bg) {
|
|
575
|
+
ctx.output += ansi.bg(widget.bg);
|
|
576
|
+
const emptyLine = " ".repeat(gridWidth);
|
|
577
|
+
for (let row$1 = 0; row$1 < gridHeight; row$1++) ctx.output += ansi.moveTo(x, y + row$1) + emptyLine;
|
|
578
|
+
}
|
|
579
|
+
for (let rowIdx = 0; rowIdx < widget.rows.length; rowIdx++) {
|
|
580
|
+
const row$1 = widget.rows[rowIdx];
|
|
581
|
+
if (!row$1) continue;
|
|
582
|
+
const cellY = y + rowIdx * (widget.cellHeight + gapY);
|
|
583
|
+
for (let colIdx = 0; colIdx < row$1.length; colIdx++) {
|
|
584
|
+
const cell = row$1[colIdx];
|
|
585
|
+
if (!cell) continue;
|
|
586
|
+
ctx = renderWidget(cell, x + colIdx * (widget.cellWidth + gapX), cellY, widget.cellWidth, widget.cellHeight, ctx);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return ctx;
|
|
590
|
+
}
|
|
591
|
+
if (widget.type === "canvas") {
|
|
592
|
+
const canvasWidth = Math.min(widget.width, maxWidth);
|
|
593
|
+
const canvasHeight = Math.min(widget.height, maxHeight);
|
|
594
|
+
for (let row$1 = 0; row$1 < canvasHeight; row$1++) {
|
|
595
|
+
const bufferRow = widget.buffer[row$1];
|
|
596
|
+
if (!bufferRow) continue;
|
|
597
|
+
ctx.output += ansi.moveTo(x, y + row$1);
|
|
598
|
+
for (let col$1 = 0; col$1 < canvasWidth; col$1++) {
|
|
599
|
+
const cell = bufferRow[col$1];
|
|
600
|
+
if (!cell) {
|
|
601
|
+
ctx.output += " ";
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const fgRgb = paletteToRgb(cell.fg);
|
|
605
|
+
ctx.output += ansi.fg(fgRgb);
|
|
606
|
+
if (cell.bg !== null) {
|
|
607
|
+
const bgRgb = paletteToRgb(cell.bg);
|
|
608
|
+
ctx.output += ansi.bg(bgRgb);
|
|
609
|
+
}
|
|
610
|
+
ctx.output += cell.char;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return ctx;
|
|
614
|
+
}
|
|
615
|
+
return ctx;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
//#endregion
|
|
619
|
+
//#region src/ui/render.ts
|
|
620
|
+
/**
|
|
621
|
+
* Render a widget tree to ANSI string
|
|
622
|
+
*/
|
|
623
|
+
function render(widget, screenX = 0, screenY = 0, maxWidth = 9999, maxHeight = 9999) {
|
|
624
|
+
return renderWidget(widget, screenX, screenY, maxWidth, maxHeight, { output: "" }).output + ansi.reset;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region src/ui/row.ts
|
|
629
|
+
/**
|
|
630
|
+
* Create a row widget (horizontal layout)
|
|
631
|
+
*/
|
|
632
|
+
function row(optionsOrChild, ...rest) {
|
|
633
|
+
if (optionsOrChild === void 0) return {
|
|
634
|
+
type: "row",
|
|
635
|
+
children: []
|
|
636
|
+
};
|
|
637
|
+
if (isOptions(optionsOrChild)) return {
|
|
638
|
+
type: "row",
|
|
639
|
+
children: rest.map((c) => typeof c === "string" ? text(c) : c),
|
|
640
|
+
justify: optionsOrChild.justify,
|
|
641
|
+
items: optionsOrChild.items,
|
|
642
|
+
width: optionsOrChild.width,
|
|
643
|
+
gap: optionsOrChild.gap,
|
|
644
|
+
px: optionsOrChild.px,
|
|
645
|
+
pl: optionsOrChild.pl,
|
|
646
|
+
pr: optionsOrChild.pr,
|
|
647
|
+
bg: optionsOrChild.bg !== void 0 ? normalizeColor(optionsOrChild.bg) : void 0
|
|
648
|
+
};
|
|
649
|
+
return {
|
|
650
|
+
type: "row",
|
|
651
|
+
children: [optionsOrChild, ...rest].map((c) => typeof c === "string" ? text(c) : c)
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/editor/file-tree.ts
|
|
657
|
+
function buildFileTree(dirPath, depth = 0) {
|
|
658
|
+
const name = basename(dirPath) || dirPath;
|
|
659
|
+
const isDirectory = statSync(dirPath).isDirectory();
|
|
660
|
+
const children = [];
|
|
661
|
+
if (isDirectory) {
|
|
662
|
+
const entries = readdirSync(dirPath);
|
|
663
|
+
for (const entry of entries) {
|
|
664
|
+
if (entry.startsWith(".")) continue;
|
|
665
|
+
const childPath = join(dirPath, entry);
|
|
666
|
+
children.push(buildFileTree(childPath, depth + 1));
|
|
667
|
+
}
|
|
668
|
+
children.sort((a, b) => {
|
|
669
|
+
if (a.isDirectory === b.isDirectory) return a.name.localeCompare(b.name);
|
|
670
|
+
return a.isDirectory ? -1 : 1;
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
name,
|
|
675
|
+
path: dirPath,
|
|
676
|
+
isDirectory,
|
|
677
|
+
children,
|
|
678
|
+
expanded: depth === 0,
|
|
679
|
+
depth
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function flattenTree(node, list = [], skipRoot = false) {
|
|
683
|
+
if (!skipRoot) list.push(node);
|
|
684
|
+
if (node.isDirectory && node.expanded) for (const child of node.children) flattenTree(child, list);
|
|
685
|
+
return list;
|
|
686
|
+
}
|
|
687
|
+
function readFile(path) {
|
|
688
|
+
try {
|
|
689
|
+
return readFileSync(path, "utf-8").split("\n");
|
|
690
|
+
} catch {
|
|
691
|
+
return ["(Cannot read file)"];
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
//#endregion
|
|
696
|
+
//#region src/editor/model.ts
|
|
697
|
+
function initModel(targetPath$1, terminalWidth, terminalHeight) {
|
|
698
|
+
const root = buildFileTree(targetPath$1);
|
|
699
|
+
return {
|
|
700
|
+
root,
|
|
701
|
+
flatList: flattenTree(root, [], true),
|
|
702
|
+
scrollOffset: 0,
|
|
703
|
+
selectedIndex: 0,
|
|
704
|
+
terminalHeight,
|
|
705
|
+
terminalWidth,
|
|
706
|
+
openedFile: null,
|
|
707
|
+
fileContent: [],
|
|
708
|
+
fileScrollOffset: 0,
|
|
709
|
+
fileHorizontalOffset: 0,
|
|
710
|
+
focus: "tree",
|
|
711
|
+
cursor: {
|
|
712
|
+
line: 0,
|
|
713
|
+
col: 0
|
|
714
|
+
},
|
|
715
|
+
modified: false,
|
|
716
|
+
treeVisible: true,
|
|
717
|
+
mouseEnabled: true
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
//#endregion
|
|
722
|
+
//#region src/editor/constants.ts
|
|
723
|
+
const INDENT_SIZE = 2;
|
|
724
|
+
const EDITOR_PADDING = 0;
|
|
725
|
+
const SCROLLBAR_WIDTH = 0;
|
|
726
|
+
|
|
727
|
+
//#endregion
|
|
728
|
+
//#region src/editor/text-utils.ts
|
|
729
|
+
function isFullWidth(char) {
|
|
730
|
+
const code = char.codePointAt(0);
|
|
731
|
+
if (code === void 0) return false;
|
|
732
|
+
if (code >= 19968 && code <= 40959) return true;
|
|
733
|
+
if (code >= 13312 && code <= 19903) return true;
|
|
734
|
+
if (code >= 12352 && code <= 12447) return true;
|
|
735
|
+
if (code >= 12448 && code <= 12543) return true;
|
|
736
|
+
if (code >= 65280 && code <= 65376) return true;
|
|
737
|
+
if (code >= 65381 && code <= 65439) return false;
|
|
738
|
+
if (code >= 12288 && code <= 12351) return true;
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
function isPrintableAscii(char) {
|
|
742
|
+
const code = char.charCodeAt(0);
|
|
743
|
+
return code >= 32 && code <= 126;
|
|
744
|
+
}
|
|
745
|
+
function isAllowedChar(char) {
|
|
746
|
+
return isPrintableAscii(char) || isFullWidth(char);
|
|
747
|
+
}
|
|
748
|
+
function sanitize(str) {
|
|
749
|
+
let result = "";
|
|
750
|
+
for (const char of str) if (char.codePointAt(0) === 9) result += " ";
|
|
751
|
+
else if (isAllowedChar(char)) result += char;
|
|
752
|
+
else result += "?";
|
|
753
|
+
return result;
|
|
754
|
+
}
|
|
755
|
+
function displayWidth(str) {
|
|
756
|
+
let width = 0;
|
|
757
|
+
for (const char of str) width += isFullWidth(char) ? 2 : 1;
|
|
758
|
+
return width;
|
|
759
|
+
}
|
|
760
|
+
function truncateByWidth(str, maxWidth) {
|
|
761
|
+
let width = 0;
|
|
762
|
+
let result = "";
|
|
763
|
+
for (const char of str) {
|
|
764
|
+
const charWidth = isFullWidth(char) ? 2 : 1;
|
|
765
|
+
if (width + charWidth > maxWidth) break;
|
|
766
|
+
result += char;
|
|
767
|
+
width += charWidth;
|
|
768
|
+
}
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
function sliceByWidth(str, startWidth, maxWidth) {
|
|
772
|
+
let currentWidth = 0;
|
|
773
|
+
let result = "";
|
|
774
|
+
let started = false;
|
|
775
|
+
for (const char of str) {
|
|
776
|
+
const charWidth = isFullWidth(char) ? 2 : 1;
|
|
777
|
+
if (!started) {
|
|
778
|
+
if (currentWidth + charWidth > startWidth) {
|
|
779
|
+
started = true;
|
|
780
|
+
if (currentWidth < startWidth) {
|
|
781
|
+
result += " ";
|
|
782
|
+
currentWidth = startWidth + 1;
|
|
783
|
+
} else {
|
|
784
|
+
result += char;
|
|
785
|
+
currentWidth += charWidth;
|
|
786
|
+
}
|
|
787
|
+
} else currentWidth += charWidth;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (displayWidth(result) + charWidth > maxWidth) break;
|
|
791
|
+
result += char;
|
|
792
|
+
}
|
|
793
|
+
return result;
|
|
794
|
+
}
|
|
795
|
+
function padEndByWidth(str, targetWidth) {
|
|
796
|
+
const currentWidth = displayWidth(str);
|
|
797
|
+
if (currentWidth >= targetWidth) return str;
|
|
798
|
+
return str + " ".repeat(targetWidth - currentWidth);
|
|
799
|
+
}
|
|
800
|
+
function charIndexToSanitizedWidth(str, charIndex) {
|
|
801
|
+
let width = 0;
|
|
802
|
+
let i = 0;
|
|
803
|
+
for (const char of str) {
|
|
804
|
+
if (i >= charIndex) break;
|
|
805
|
+
if (char.codePointAt(0) === 9) width += 2;
|
|
806
|
+
else if (isFullWidth(char)) width += 2;
|
|
807
|
+
else width += 1;
|
|
808
|
+
i++;
|
|
809
|
+
}
|
|
810
|
+
return width;
|
|
811
|
+
}
|
|
812
|
+
function maxDisplayWidth(lines) {
|
|
813
|
+
let max = 0;
|
|
814
|
+
for (const line of lines) {
|
|
815
|
+
const w = displayWidth(line);
|
|
816
|
+
if (w > max) max = w;
|
|
817
|
+
}
|
|
818
|
+
return max;
|
|
819
|
+
}
|
|
820
|
+
function splitAtWidth(str, targetWidth) {
|
|
821
|
+
let width = 0;
|
|
822
|
+
let beforeEnd = 0;
|
|
823
|
+
let charEnd = 0;
|
|
824
|
+
const chars = [...str];
|
|
825
|
+
for (let i = 0; i < chars.length; i++) {
|
|
826
|
+
const char = chars[i];
|
|
827
|
+
const charWidth = isFullWidth(char) ? 2 : 1;
|
|
828
|
+
if (width >= targetWidth) {
|
|
829
|
+
beforeEnd = i;
|
|
830
|
+
charEnd = i + 1;
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
if (width + charWidth > targetWidth) {
|
|
834
|
+
beforeEnd = i;
|
|
835
|
+
charEnd = i + 1;
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
width += charWidth;
|
|
839
|
+
beforeEnd = i + 1;
|
|
840
|
+
charEnd = i + 1;
|
|
841
|
+
}
|
|
842
|
+
if (beforeEnd >= chars.length) return {
|
|
843
|
+
before: str,
|
|
844
|
+
char: " ",
|
|
845
|
+
after: ""
|
|
846
|
+
};
|
|
847
|
+
return {
|
|
848
|
+
before: chars.slice(0, beforeEnd).join(""),
|
|
849
|
+
char: chars[beforeEnd] || " ",
|
|
850
|
+
after: chars.slice(charEnd).join("")
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region src/editor/update.ts
|
|
856
|
+
function update(model$1, msg) {
|
|
857
|
+
if (msg.type === "NOOP") return model$1;
|
|
858
|
+
if (msg.type === "RESIZE") return ensureVisible({
|
|
859
|
+
...model$1,
|
|
860
|
+
terminalHeight: msg.height,
|
|
861
|
+
terminalWidth: msg.width
|
|
862
|
+
});
|
|
863
|
+
if (msg.type === "MOVE") {
|
|
864
|
+
const newIndex = Math.max(0, Math.min(model$1.flatList.length - 1, model$1.selectedIndex + msg.delta));
|
|
865
|
+
return ensureVisible({
|
|
866
|
+
...model$1,
|
|
867
|
+
selectedIndex: newIndex
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
if (msg.type === "SCROLL") {
|
|
871
|
+
const contentHeight = model$1.terminalHeight - 2;
|
|
872
|
+
const maxScroll = Math.max(0, model$1.flatList.length - contentHeight);
|
|
873
|
+
const newOffset = Math.max(0, Math.min(maxScroll, model$1.scrollOffset + msg.delta));
|
|
874
|
+
return {
|
|
875
|
+
...model$1,
|
|
876
|
+
scrollOffset: newOffset
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
if (msg.type === "SCROLL_TO") {
|
|
880
|
+
const contentHeight = model$1.terminalHeight - 2;
|
|
881
|
+
const maxScroll = Math.max(0, model$1.flatList.length - contentHeight);
|
|
882
|
+
const newOffset = Math.max(0, Math.min(maxScroll, msg.position));
|
|
883
|
+
return {
|
|
884
|
+
...model$1,
|
|
885
|
+
scrollOffset: newOffset
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
if (msg.type === "SCROLL_FILE") {
|
|
889
|
+
const contentHeight = model$1.terminalHeight - 2;
|
|
890
|
+
const maxScroll = Math.max(0, model$1.fileContent.length - contentHeight);
|
|
891
|
+
const newOffset = Math.max(0, Math.min(maxScroll, model$1.fileScrollOffset + msg.delta));
|
|
892
|
+
return {
|
|
893
|
+
...model$1,
|
|
894
|
+
fileScrollOffset: newOffset
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
if (msg.type === "SCROLL_FILE_TO") {
|
|
898
|
+
const contentHeight = model$1.terminalHeight - 2;
|
|
899
|
+
const maxScroll = Math.max(0, model$1.fileContent.length - contentHeight);
|
|
900
|
+
const newOffset = Math.max(0, Math.min(maxScroll, msg.position));
|
|
901
|
+
return {
|
|
902
|
+
...model$1,
|
|
903
|
+
fileScrollOffset: newOffset
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
if (msg.type === "SCROLL_FILE_H") {
|
|
907
|
+
const maxWidth = maxDisplayWidth(model$1.fileContent) + EDITOR_PADDING * 2;
|
|
908
|
+
const editorWidth = model$1.terminalWidth - SCROLLBAR_WIDTH;
|
|
909
|
+
const maxScroll = Math.max(0, maxWidth - editorWidth);
|
|
910
|
+
const newOffset = Math.max(0, Math.min(maxScroll, model$1.fileHorizontalOffset + msg.delta));
|
|
911
|
+
return {
|
|
912
|
+
...model$1,
|
|
913
|
+
fileHorizontalOffset: newOffset
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
if (msg.type === "SCROLL_FILE_H_TO") {
|
|
917
|
+
const maxWidth = maxDisplayWidth(model$1.fileContent) + EDITOR_PADDING * 2;
|
|
918
|
+
const editorWidth = model$1.terminalWidth - SCROLLBAR_WIDTH;
|
|
919
|
+
const maxScroll = Math.max(0, maxWidth - editorWidth);
|
|
920
|
+
const newOffset = Math.max(0, Math.min(maxScroll, msg.position));
|
|
921
|
+
return {
|
|
922
|
+
...model$1,
|
|
923
|
+
fileHorizontalOffset: newOffset
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
if (msg.type === "SELECT") {
|
|
927
|
+
if (msg.index < 0 || msg.index >= model$1.flatList.length) return model$1;
|
|
928
|
+
return {
|
|
929
|
+
...model$1,
|
|
930
|
+
selectedIndex: msg.index
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
if (msg.type === "OPEN_FILE") {
|
|
934
|
+
const content = readFile(msg.path);
|
|
935
|
+
return {
|
|
936
|
+
...model$1,
|
|
937
|
+
openedFile: msg.path,
|
|
938
|
+
fileContent: content,
|
|
939
|
+
fileScrollOffset: 0,
|
|
940
|
+
fileHorizontalOffset: 0,
|
|
941
|
+
cursor: {
|
|
942
|
+
line: 0,
|
|
943
|
+
col: 0
|
|
944
|
+
},
|
|
945
|
+
modified: false
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
if (msg.type === "TOGGLE") {
|
|
949
|
+
const node = model$1.flatList[msg.index];
|
|
950
|
+
if (!node) return model$1;
|
|
951
|
+
if (!node.isDirectory) {
|
|
952
|
+
const content = readFile(node.path);
|
|
953
|
+
return {
|
|
954
|
+
...model$1,
|
|
955
|
+
openedFile: node.path,
|
|
956
|
+
fileContent: content,
|
|
957
|
+
fileScrollOffset: 0,
|
|
958
|
+
fileHorizontalOffset: 0,
|
|
959
|
+
cursor: {
|
|
960
|
+
line: 0,
|
|
961
|
+
col: 0
|
|
962
|
+
},
|
|
963
|
+
modified: false
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
node.expanded = !node.expanded;
|
|
967
|
+
const newFlatList = flattenTree(model$1.root, [], true);
|
|
968
|
+
const newSelectedIndex = Math.min(model$1.selectedIndex, newFlatList.length - 1);
|
|
969
|
+
return {
|
|
970
|
+
...model$1,
|
|
971
|
+
flatList: newFlatList,
|
|
972
|
+
selectedIndex: newSelectedIndex
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
if (msg.type === "FOCUS_TOGGLE") {
|
|
976
|
+
const newFocus = model$1.focus === "tree" ? "editor" : "tree";
|
|
977
|
+
return {
|
|
978
|
+
...model$1,
|
|
979
|
+
focus: newFocus
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
if (msg.type === "TOGGLE_TREE") {
|
|
983
|
+
if (model$1.treeVisible) return {
|
|
984
|
+
...model$1,
|
|
985
|
+
treeVisible: false,
|
|
986
|
+
focus: "editor"
|
|
987
|
+
};
|
|
988
|
+
return {
|
|
989
|
+
...model$1,
|
|
990
|
+
treeVisible: true,
|
|
991
|
+
focus: "tree"
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
if (msg.type === "CURSOR_MOVE") {
|
|
995
|
+
if (!model$1.openedFile || model$1.fileContent.length === 0) return model$1;
|
|
996
|
+
const maxLine = Math.max(0, model$1.fileContent.length - 1);
|
|
997
|
+
const newLine = Math.max(0, Math.min(maxLine, model$1.cursor.line + msg.deltaLine));
|
|
998
|
+
const lineContent = model$1.fileContent[newLine] || "";
|
|
999
|
+
const maxCol = Math.max(0, [...lineContent].length - 1);
|
|
1000
|
+
const newCol = Math.max(0, Math.min(maxCol, model$1.cursor.col + msg.deltaCol));
|
|
1001
|
+
return ensureCursorVisible({
|
|
1002
|
+
...model$1,
|
|
1003
|
+
cursor: {
|
|
1004
|
+
line: newLine,
|
|
1005
|
+
col: newCol
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
if (msg.type === "INSERT_CHAR") {
|
|
1010
|
+
if (!model$1.openedFile) return model$1;
|
|
1011
|
+
const newContent = [...model$1.fileContent];
|
|
1012
|
+
const chars = [...newContent[model$1.cursor.line] || ""];
|
|
1013
|
+
const safeChars = [...msg.char].filter(isAllowedChar);
|
|
1014
|
+
if (safeChars.length === 0) return model$1;
|
|
1015
|
+
chars.splice(model$1.cursor.col, 0, ...safeChars);
|
|
1016
|
+
newContent[model$1.cursor.line] = chars.join("");
|
|
1017
|
+
return ensureCursorVisible({
|
|
1018
|
+
...model$1,
|
|
1019
|
+
fileContent: newContent,
|
|
1020
|
+
cursor: {
|
|
1021
|
+
line: model$1.cursor.line,
|
|
1022
|
+
col: model$1.cursor.col + safeChars.length
|
|
1023
|
+
},
|
|
1024
|
+
modified: true
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
if (msg.type === "DELETE_CHAR") {
|
|
1028
|
+
if (!model$1.openedFile) return model$1;
|
|
1029
|
+
const newContent = [...model$1.fileContent];
|
|
1030
|
+
const line = newContent[model$1.cursor.line] || "";
|
|
1031
|
+
const chars = [...line];
|
|
1032
|
+
if (model$1.cursor.col < chars.length) {
|
|
1033
|
+
chars.splice(model$1.cursor.col, 1);
|
|
1034
|
+
newContent[model$1.cursor.line] = chars.join("");
|
|
1035
|
+
return {
|
|
1036
|
+
...model$1,
|
|
1037
|
+
fileContent: newContent,
|
|
1038
|
+
modified: true
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
if (model$1.cursor.line < newContent.length - 1) {
|
|
1042
|
+
const nextLine = newContent[model$1.cursor.line + 1] || "";
|
|
1043
|
+
newContent[model$1.cursor.line] = line + nextLine;
|
|
1044
|
+
newContent.splice(model$1.cursor.line + 1, 1);
|
|
1045
|
+
return {
|
|
1046
|
+
...model$1,
|
|
1047
|
+
fileContent: newContent,
|
|
1048
|
+
modified: true
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
return model$1;
|
|
1052
|
+
}
|
|
1053
|
+
if (msg.type === "DELETE_CHAR_BEFORE") {
|
|
1054
|
+
if (!model$1.openedFile) return model$1;
|
|
1055
|
+
if (model$1.cursor.col > 0) {
|
|
1056
|
+
const newContent = [...model$1.fileContent];
|
|
1057
|
+
const chars = [...newContent[model$1.cursor.line] || ""];
|
|
1058
|
+
chars.splice(model$1.cursor.col - 1, 1);
|
|
1059
|
+
newContent[model$1.cursor.line] = chars.join("");
|
|
1060
|
+
return ensureCursorVisible({
|
|
1061
|
+
...model$1,
|
|
1062
|
+
fileContent: newContent,
|
|
1063
|
+
cursor: {
|
|
1064
|
+
line: model$1.cursor.line,
|
|
1065
|
+
col: model$1.cursor.col - 1
|
|
1066
|
+
},
|
|
1067
|
+
modified: true
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
if (model$1.cursor.line > 0) {
|
|
1071
|
+
const newContent = [...model$1.fileContent];
|
|
1072
|
+
const prevLine = newContent[model$1.cursor.line - 1] || "";
|
|
1073
|
+
const currentLine = newContent[model$1.cursor.line] || "";
|
|
1074
|
+
const newCol = [...prevLine].length;
|
|
1075
|
+
newContent[model$1.cursor.line - 1] = prevLine + currentLine;
|
|
1076
|
+
newContent.splice(model$1.cursor.line, 1);
|
|
1077
|
+
return ensureCursorVisible({
|
|
1078
|
+
...model$1,
|
|
1079
|
+
fileContent: newContent,
|
|
1080
|
+
cursor: {
|
|
1081
|
+
line: model$1.cursor.line - 1,
|
|
1082
|
+
col: newCol
|
|
1083
|
+
},
|
|
1084
|
+
modified: true
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
return model$1;
|
|
1088
|
+
}
|
|
1089
|
+
if (msg.type === "INSERT_NEWLINE") {
|
|
1090
|
+
if (!model$1.openedFile) return model$1;
|
|
1091
|
+
const newContent = [...model$1.fileContent];
|
|
1092
|
+
const chars = [...newContent[model$1.cursor.line] || ""];
|
|
1093
|
+
const beforeCursor = chars.slice(0, model$1.cursor.col).join("");
|
|
1094
|
+
const afterCursor = chars.slice(model$1.cursor.col).join("");
|
|
1095
|
+
newContent[model$1.cursor.line] = beforeCursor;
|
|
1096
|
+
newContent.splice(model$1.cursor.line + 1, 0, afterCursor);
|
|
1097
|
+
return ensureCursorVisible({
|
|
1098
|
+
...model$1,
|
|
1099
|
+
fileContent: newContent,
|
|
1100
|
+
cursor: {
|
|
1101
|
+
line: model$1.cursor.line + 1,
|
|
1102
|
+
col: 0
|
|
1103
|
+
},
|
|
1104
|
+
modified: true
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
if (msg.type === "SAVE") {
|
|
1108
|
+
if (!model$1.openedFile || !model$1.modified) return model$1;
|
|
1109
|
+
try {
|
|
1110
|
+
writeFileSync(model$1.openedFile, model$1.fileContent.join("\n"));
|
|
1111
|
+
return {
|
|
1112
|
+
...model$1,
|
|
1113
|
+
modified: false
|
|
1114
|
+
};
|
|
1115
|
+
} catch {
|
|
1116
|
+
return model$1;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (msg.type === "CLICK") return handleClick(model$1, msg.row, msg.col);
|
|
1120
|
+
if (msg.type === "TOGGLE_MOUSE") return {
|
|
1121
|
+
...model$1,
|
|
1122
|
+
mouseEnabled: !model$1.mouseEnabled
|
|
1123
|
+
};
|
|
1124
|
+
return model$1;
|
|
1125
|
+
}
|
|
1126
|
+
function ensureVisible(model$1) {
|
|
1127
|
+
const contentHeight = model$1.terminalHeight - 2;
|
|
1128
|
+
let newOffset = model$1.scrollOffset;
|
|
1129
|
+
if (model$1.selectedIndex < model$1.scrollOffset) newOffset = model$1.selectedIndex;
|
|
1130
|
+
if (model$1.selectedIndex >= model$1.scrollOffset + contentHeight) newOffset = model$1.selectedIndex - contentHeight + 1;
|
|
1131
|
+
const maxFileWidth = maxDisplayWidth(model$1.fileContent) + EDITOR_PADDING * 2;
|
|
1132
|
+
const editorWidth = model$1.terminalWidth - SCROLLBAR_WIDTH;
|
|
1133
|
+
const maxHorizontalOffset = Math.max(0, maxFileWidth - editorWidth);
|
|
1134
|
+
const newHorizontalOffset = Math.min(model$1.fileHorizontalOffset, maxHorizontalOffset);
|
|
1135
|
+
const maxFileScrollOffset = Math.max(0, model$1.fileContent.length - contentHeight);
|
|
1136
|
+
const newFileScrollOffset = Math.min(model$1.fileScrollOffset, maxFileScrollOffset);
|
|
1137
|
+
return {
|
|
1138
|
+
...model$1,
|
|
1139
|
+
scrollOffset: newOffset,
|
|
1140
|
+
fileHorizontalOffset: newHorizontalOffset,
|
|
1141
|
+
fileScrollOffset: newFileScrollOffset
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
function ensureCursorVisible(model$1) {
|
|
1145
|
+
const contentHeight = model$1.terminalHeight - 2;
|
|
1146
|
+
const editorContentWidth = model$1.terminalWidth - SCROLLBAR_WIDTH;
|
|
1147
|
+
let newScrollOffset = model$1.fileScrollOffset;
|
|
1148
|
+
let newHorizontalOffset = model$1.fileHorizontalOffset;
|
|
1149
|
+
if (model$1.cursor.line < model$1.fileScrollOffset) newScrollOffset = model$1.cursor.line;
|
|
1150
|
+
if (model$1.cursor.line >= model$1.fileScrollOffset + contentHeight) newScrollOffset = model$1.cursor.line - contentHeight + 1;
|
|
1151
|
+
const cursorWidth = EDITOR_PADDING + charIndexToSanitizedWidth(model$1.fileContent[model$1.cursor.line] || "", model$1.cursor.col);
|
|
1152
|
+
if (cursorWidth < newHorizontalOffset) newHorizontalOffset = Math.max(0, cursorWidth - EDITOR_PADDING);
|
|
1153
|
+
if (cursorWidth >= newHorizontalOffset + editorContentWidth) newHorizontalOffset = cursorWidth - editorContentWidth + 1;
|
|
1154
|
+
return {
|
|
1155
|
+
...model$1,
|
|
1156
|
+
fileScrollOffset: newScrollOffset,
|
|
1157
|
+
fileHorizontalOffset: newHorizontalOffset
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
function handleClick(model$1, row$1, col$1) {
|
|
1161
|
+
const treeWidth = model$1.treeVisible ? Math.floor(model$1.terminalWidth / 2) : 0;
|
|
1162
|
+
const editorWidth = model$1.terminalWidth - treeWidth;
|
|
1163
|
+
const contentHeight = model$1.terminalHeight - 2;
|
|
1164
|
+
if (row$1 === model$1.terminalHeight - 1 && col$1 > treeWidth) {
|
|
1165
|
+
const maxFileWidth = maxDisplayWidth(model$1.fileContent) + EDITOR_PADDING * 2;
|
|
1166
|
+
const editorContentWidth = editorWidth - SCROLLBAR_WIDTH;
|
|
1167
|
+
const scrollbarStart = treeWidth + 1;
|
|
1168
|
+
const scrollbarEnd = scrollbarStart + editorContentWidth;
|
|
1169
|
+
if (maxFileWidth > editorContentWidth && col$1 >= scrollbarStart && col$1 < scrollbarEnd) {
|
|
1170
|
+
const clickPosInScrollbar = col$1 - scrollbarStart;
|
|
1171
|
+
const maxOffset = maxFileWidth - editorContentWidth;
|
|
1172
|
+
const newOffset = Math.floor(clickPosInScrollbar / editorContentWidth * maxOffset);
|
|
1173
|
+
return {
|
|
1174
|
+
...model$1,
|
|
1175
|
+
fileHorizontalOffset: Math.max(0, Math.min(maxOffset, newOffset)),
|
|
1176
|
+
focus: "editor"
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
return {
|
|
1180
|
+
...model$1,
|
|
1181
|
+
focus: "editor"
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
if (model$1.treeVisible && col$1 <= treeWidth) {
|
|
1185
|
+
if (row$1 < 2 || row$1 > contentHeight + 1) return {
|
|
1186
|
+
...model$1,
|
|
1187
|
+
focus: "tree"
|
|
1188
|
+
};
|
|
1189
|
+
const index = model$1.scrollOffset + row$1 - 2;
|
|
1190
|
+
if (index >= model$1.flatList.length) return {
|
|
1191
|
+
...model$1,
|
|
1192
|
+
focus: "tree"
|
|
1193
|
+
};
|
|
1194
|
+
const node = model$1.flatList[index];
|
|
1195
|
+
if (node && node.isDirectory) {
|
|
1196
|
+
node.expanded = !node.expanded;
|
|
1197
|
+
const newFlatList = flattenTree(model$1.root, [], true);
|
|
1198
|
+
return {
|
|
1199
|
+
...model$1,
|
|
1200
|
+
flatList: newFlatList,
|
|
1201
|
+
selectedIndex: index,
|
|
1202
|
+
focus: "tree"
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
if (node && !node.isDirectory) {
|
|
1206
|
+
const content = readFile(node.path);
|
|
1207
|
+
return {
|
|
1208
|
+
...model$1,
|
|
1209
|
+
selectedIndex: index,
|
|
1210
|
+
openedFile: node.path,
|
|
1211
|
+
fileContent: content,
|
|
1212
|
+
fileScrollOffset: 0,
|
|
1213
|
+
fileHorizontalOffset: 0,
|
|
1214
|
+
cursor: {
|
|
1215
|
+
line: 0,
|
|
1216
|
+
col: 0
|
|
1217
|
+
},
|
|
1218
|
+
modified: false,
|
|
1219
|
+
focus: "tree"
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
return {
|
|
1223
|
+
...model$1,
|
|
1224
|
+
selectedIndex: index,
|
|
1225
|
+
focus: "tree"
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
if (col$1 === model$1.terminalWidth && row$1 >= 2 && row$1 <= contentHeight + 1) {
|
|
1229
|
+
const clickPosInScrollbar = row$1 - 2;
|
|
1230
|
+
const maxScroll = Math.max(0, model$1.fileContent.length - contentHeight);
|
|
1231
|
+
if (maxScroll > 0) {
|
|
1232
|
+
const newOffset = Math.floor(clickPosInScrollbar / contentHeight * model$1.fileContent.length);
|
|
1233
|
+
return {
|
|
1234
|
+
...model$1,
|
|
1235
|
+
fileScrollOffset: Math.max(0, Math.min(maxScroll, newOffset)),
|
|
1236
|
+
focus: "editor"
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
return {
|
|
1240
|
+
...model$1,
|
|
1241
|
+
focus: "editor"
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
if (row$1 >= 2 && row$1 <= contentHeight + 1) {
|
|
1245
|
+
const clickedLine = model$1.fileScrollOffset + row$1 - 2;
|
|
1246
|
+
if (clickedLine < model$1.fileContent.length) {
|
|
1247
|
+
const clickColInText = col$1 - 1 + model$1.fileHorizontalOffset - EDITOR_PADDING;
|
|
1248
|
+
if (clickColInText < 0) return {
|
|
1249
|
+
...model$1,
|
|
1250
|
+
cursor: {
|
|
1251
|
+
line: clickedLine,
|
|
1252
|
+
col: 0
|
|
1253
|
+
},
|
|
1254
|
+
focus: "editor"
|
|
1255
|
+
};
|
|
1256
|
+
const line = model$1.fileContent[clickedLine] || "";
|
|
1257
|
+
const lineWidth = displayWidth(sanitize(line));
|
|
1258
|
+
const clampedClickCol = Math.min(clickColInText, Math.max(0, lineWidth - 1));
|
|
1259
|
+
let charIndex = 0;
|
|
1260
|
+
let width = 0;
|
|
1261
|
+
for (const char of line) {
|
|
1262
|
+
let charWidth;
|
|
1263
|
+
if (char.codePointAt(0) === 9) charWidth = 2;
|
|
1264
|
+
else if (isFullWidth(char)) charWidth = 2;
|
|
1265
|
+
else charWidth = 1;
|
|
1266
|
+
if (width + charWidth > clampedClickCol) break;
|
|
1267
|
+
width += charWidth;
|
|
1268
|
+
charIndex++;
|
|
1269
|
+
}
|
|
1270
|
+
const maxCharIndex = Math.max(0, [...line].length - 1);
|
|
1271
|
+
charIndex = Math.min(charIndex, maxCharIndex);
|
|
1272
|
+
return {
|
|
1273
|
+
...model$1,
|
|
1274
|
+
cursor: {
|
|
1275
|
+
line: clickedLine,
|
|
1276
|
+
col: charIndex
|
|
1277
|
+
},
|
|
1278
|
+
focus: "editor"
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
...model$1,
|
|
1284
|
+
focus: "editor"
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
//#endregion
|
|
1289
|
+
//#region src/editor/calc-scrollbar.ts
|
|
1290
|
+
function calcScrollbar(props) {
|
|
1291
|
+
if (props.totalItems <= props.visibleItems) return {
|
|
1292
|
+
start: 0,
|
|
1293
|
+
end: 0
|
|
1294
|
+
};
|
|
1295
|
+
const thumbSize = Math.max(1, Math.floor(props.visibleItems / props.totalItems * props.height));
|
|
1296
|
+
const maxOffset = props.totalItems - props.visibleItems;
|
|
1297
|
+
const thumbPosition = Math.floor(props.offset / maxOffset * (props.height - thumbSize));
|
|
1298
|
+
return {
|
|
1299
|
+
start: thumbPosition,
|
|
1300
|
+
end: thumbPosition + thumbSize
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
//#endregion
|
|
1305
|
+
//#region src/editor/calc-horizontal-scrollbar.ts
|
|
1306
|
+
function calcHorizontalScrollbar(props) {
|
|
1307
|
+
if (props.maxWidth <= props.visibleWidth) return {
|
|
1308
|
+
start: 0,
|
|
1309
|
+
end: 0
|
|
1310
|
+
};
|
|
1311
|
+
const thumbSize = Math.max(2, Math.floor(props.visibleWidth / props.maxWidth * props.width));
|
|
1312
|
+
const maxOffset = props.maxWidth - props.visibleWidth;
|
|
1313
|
+
const thumbPosition = Math.floor(props.offset / maxOffset * (props.width - thumbSize));
|
|
1314
|
+
return {
|
|
1315
|
+
start: thumbPosition,
|
|
1316
|
+
end: thumbPosition + thumbSize
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
//#endregion
|
|
1321
|
+
//#region src/editor/view-header.ts
|
|
1322
|
+
function viewHeader(props) {
|
|
1323
|
+
const theme$1 = props.theme;
|
|
1324
|
+
const bg = theme$1.headerBgFocus;
|
|
1325
|
+
let content;
|
|
1326
|
+
if (props.focus === "tree") content = ` ${props.rootPath}`;
|
|
1327
|
+
else if (props.openedFile) {
|
|
1328
|
+
const modifiedMark = props.modified ? " [+]" : "";
|
|
1329
|
+
content = ` ${basename(props.openedFile)}${modifiedMark}`;
|
|
1330
|
+
} else content = " (No file)";
|
|
1331
|
+
const headerText = padEndByWidth(truncateByWidth(sanitize(content), props.width), props.width);
|
|
1332
|
+
return text({
|
|
1333
|
+
bg,
|
|
1334
|
+
fg: theme$1.headerFg
|
|
1335
|
+
}, headerText);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
//#endregion
|
|
1339
|
+
//#region src/editor/view-footer.ts
|
|
1340
|
+
function viewFooter(props) {
|
|
1341
|
+
const theme$1 = props.theme;
|
|
1342
|
+
const bg = theme$1.headerBg;
|
|
1343
|
+
let leftContent;
|
|
1344
|
+
if (props.focus === "tree") leftContent = " [Tab] Editor [Ctrl+C] Quit";
|
|
1345
|
+
else leftContent = ` Ln ${props.cursorLine + 1}, Col ${props.cursorCol + 1} [Tab] Tree [Ctrl+S] Save`;
|
|
1346
|
+
const rightContent = `[Esc] ${props.mouseEnabled ? "MOUSE" : "KEY"} `;
|
|
1347
|
+
const leftWidth = displayWidth(sanitize(leftContent));
|
|
1348
|
+
const rightWidth = displayWidth(rightContent);
|
|
1349
|
+
const paddingWidth = Math.max(0, props.width - leftWidth - rightWidth);
|
|
1350
|
+
const padding = " ".repeat(paddingWidth);
|
|
1351
|
+
const footerText = truncateByWidth(sanitize(leftContent) + padding + rightContent, props.width);
|
|
1352
|
+
return text({
|
|
1353
|
+
bg,
|
|
1354
|
+
fg: theme$1.headerFg
|
|
1355
|
+
}, footerText);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
//#endregion
|
|
1359
|
+
//#region src/editor/theme.ts
|
|
1360
|
+
const DIR_INDICATOR = {
|
|
1361
|
+
TRIANGLE_FILLED: {
|
|
1362
|
+
collapsed: "▶",
|
|
1363
|
+
expanded: "▼"
|
|
1364
|
+
},
|
|
1365
|
+
TRIANGLE_OUTLINE: {
|
|
1366
|
+
collapsed: "▷",
|
|
1367
|
+
expanded: "▽"
|
|
1368
|
+
},
|
|
1369
|
+
ARROW: {
|
|
1370
|
+
collapsed: ">",
|
|
1371
|
+
expanded: "v"
|
|
1372
|
+
},
|
|
1373
|
+
PLUS_MINUS: {
|
|
1374
|
+
collapsed: "+",
|
|
1375
|
+
expanded: "-"
|
|
1376
|
+
},
|
|
1377
|
+
CHEVRON: {
|
|
1378
|
+
collapsed: "›",
|
|
1379
|
+
expanded: "⌄"
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
function getDirIndicator(theme$1) {
|
|
1383
|
+
if (typeof theme$1.dirIndicator === "string") return DIR_INDICATOR[theme$1.dirIndicator];
|
|
1384
|
+
return theme$1.dirIndicator;
|
|
1385
|
+
}
|
|
1386
|
+
const defaultTheme = {
|
|
1387
|
+
dirIndicator: "PLUS_MINUS",
|
|
1388
|
+
treeBg: "default",
|
|
1389
|
+
treeBgIndent: [
|
|
1390
|
+
20,
|
|
1391
|
+
30,
|
|
1392
|
+
50
|
|
1393
|
+
],
|
|
1394
|
+
treeBgPrefix: [
|
|
1395
|
+
30,
|
|
1396
|
+
45,
|
|
1397
|
+
70
|
|
1398
|
+
],
|
|
1399
|
+
treeBgName: [
|
|
1400
|
+
40,
|
|
1401
|
+
55,
|
|
1402
|
+
85
|
|
1403
|
+
],
|
|
1404
|
+
treeBgSelected: [
|
|
1405
|
+
50,
|
|
1406
|
+
70,
|
|
1407
|
+
110
|
|
1408
|
+
],
|
|
1409
|
+
treeFg: [
|
|
1410
|
+
150,
|
|
1411
|
+
170,
|
|
1412
|
+
190
|
|
1413
|
+
],
|
|
1414
|
+
treeFgDir: [
|
|
1415
|
+
120,
|
|
1416
|
+
180,
|
|
1417
|
+
240
|
|
1418
|
+
],
|
|
1419
|
+
treeFgPrefix: [
|
|
1420
|
+
90,
|
|
1421
|
+
150,
|
|
1422
|
+
220
|
|
1423
|
+
],
|
|
1424
|
+
editorBg: "default",
|
|
1425
|
+
editorFg: [
|
|
1426
|
+
180,
|
|
1427
|
+
200,
|
|
1428
|
+
220
|
|
1429
|
+
],
|
|
1430
|
+
editorFgDimmed: [
|
|
1431
|
+
40,
|
|
1432
|
+
50,
|
|
1433
|
+
70
|
|
1434
|
+
],
|
|
1435
|
+
editorBgCurrentLine: "default",
|
|
1436
|
+
editorFgLineNumber: [
|
|
1437
|
+
60,
|
|
1438
|
+
80,
|
|
1439
|
+
120
|
|
1440
|
+
],
|
|
1441
|
+
cursorBg: [
|
|
1442
|
+
100,
|
|
1443
|
+
150,
|
|
1444
|
+
220
|
|
1445
|
+
],
|
|
1446
|
+
cursorFg: [
|
|
1447
|
+
10,
|
|
1448
|
+
15,
|
|
1449
|
+
25
|
|
1450
|
+
],
|
|
1451
|
+
headerBg: [
|
|
1452
|
+
15,
|
|
1453
|
+
20,
|
|
1454
|
+
35
|
|
1455
|
+
],
|
|
1456
|
+
headerBgFocus: [
|
|
1457
|
+
30,
|
|
1458
|
+
50,
|
|
1459
|
+
90
|
|
1460
|
+
],
|
|
1461
|
+
headerFg: [
|
|
1462
|
+
150,
|
|
1463
|
+
180,
|
|
1464
|
+
210
|
|
1465
|
+
],
|
|
1466
|
+
scrollbarTrack: "default",
|
|
1467
|
+
scrollbarThumb: [
|
|
1468
|
+
50,
|
|
1469
|
+
80,
|
|
1470
|
+
130
|
|
1471
|
+
],
|
|
1472
|
+
hScrollbarTrack: "default",
|
|
1473
|
+
hScrollbarThumb: [
|
|
1474
|
+
50,
|
|
1475
|
+
80,
|
|
1476
|
+
130
|
|
1477
|
+
]
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
//#endregion
|
|
1481
|
+
//#region src/editor/view-tree-line.ts
|
|
1482
|
+
function viewTreeLine(props) {
|
|
1483
|
+
const theme$1 = props.theme;
|
|
1484
|
+
if (!props.node) return {
|
|
1485
|
+
widget: text({}, ""),
|
|
1486
|
+
width: 0
|
|
1487
|
+
};
|
|
1488
|
+
const indentWidth = (props.node.depth - 1) * INDENT_SIZE;
|
|
1489
|
+
const indent = " ".repeat(indentWidth);
|
|
1490
|
+
const dirIndicator = getDirIndicator(theme$1);
|
|
1491
|
+
let prefix;
|
|
1492
|
+
if (props.node.isDirectory) prefix = props.node.expanded ? dirIndicator.expanded : dirIndicator.collapsed;
|
|
1493
|
+
else prefix = "-";
|
|
1494
|
+
const prefixWithSpace = " " + prefix + " ";
|
|
1495
|
+
const prefixWidth = displayWidth(prefixWithSpace);
|
|
1496
|
+
const truncatedName = truncateByWidth(sanitize(props.node.name) + " ", props.maxWidth - indentWidth - prefixWidth);
|
|
1497
|
+
const nameWidth = displayWidth(truncatedName);
|
|
1498
|
+
const indentBg = props.isSelected ? theme$1.treeBgSelected : theme$1.treeBgIndent;
|
|
1499
|
+
const prefixBg = props.isSelected ? theme$1.treeBgSelected : theme$1.treeBgPrefix;
|
|
1500
|
+
const nameBg = props.isSelected ? theme$1.treeBgSelected : theme$1.treeBgName;
|
|
1501
|
+
const prefixFg = theme$1.treeFgPrefix;
|
|
1502
|
+
const nameFg = props.node.isDirectory ? theme$1.treeFgDir : theme$1.treeFg;
|
|
1503
|
+
const totalWidth = indentWidth + prefixWidth + nameWidth;
|
|
1504
|
+
if (indentWidth === 0) return {
|
|
1505
|
+
widget: row(text({
|
|
1506
|
+
bg: prefixBg,
|
|
1507
|
+
fg: prefixFg
|
|
1508
|
+
}, prefixWithSpace), text({
|
|
1509
|
+
bg: nameBg,
|
|
1510
|
+
fg: nameFg
|
|
1511
|
+
}, truncatedName)),
|
|
1512
|
+
width: totalWidth
|
|
1513
|
+
};
|
|
1514
|
+
return {
|
|
1515
|
+
widget: row(text({ bg: indentBg }, indent), text({
|
|
1516
|
+
bg: prefixBg,
|
|
1517
|
+
fg: prefixFg
|
|
1518
|
+
}, prefixWithSpace), text({
|
|
1519
|
+
bg: nameBg,
|
|
1520
|
+
fg: nameFg
|
|
1521
|
+
}, truncatedName)),
|
|
1522
|
+
width: totalWidth
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
//#endregion
|
|
1527
|
+
//#region src/editor/view-editor-line.ts
|
|
1528
|
+
function viewEditorLine(props) {
|
|
1529
|
+
const theme$1 = props.theme;
|
|
1530
|
+
const contentPadding = " ".repeat(EDITOR_PADDING);
|
|
1531
|
+
const lineBg = props.isCurrentLine ? theme$1.editorBgCurrentLine : theme$1.editorBg;
|
|
1532
|
+
const lineFg = props.dimmed ? theme$1.editorFgDimmed : theme$1.editorFg;
|
|
1533
|
+
const showVScrollbar = !props.dimmed && props.isScrollbarThumb;
|
|
1534
|
+
const vScrollbarBg = showVScrollbar ? theme$1.scrollbarThumb : lineBg;
|
|
1535
|
+
const hScrollbar = props.hScrollbar;
|
|
1536
|
+
const isHScrollbarLine = hScrollbar !== void 0 && !props.dimmed;
|
|
1537
|
+
if (props.line === void 0) {
|
|
1538
|
+
const paddedVisible = padEndByWidth(sliceByWidth(" ".repeat(props.contentWidth), props.visibleStart, props.visibleWidth), props.visibleWidth);
|
|
1539
|
+
if (isHScrollbarLine) return renderHScrollbarLine(paddedVisible, hScrollbar, props.visibleWidth, lineFg, showVScrollbar, theme$1, -1);
|
|
1540
|
+
const splitLast = splitAtWidth(paddedVisible, props.visibleWidth - 1);
|
|
1541
|
+
return row(text({
|
|
1542
|
+
bg: lineBg,
|
|
1543
|
+
fg: lineFg
|
|
1544
|
+
}, splitLast.before), text({
|
|
1545
|
+
bg: vScrollbarBg,
|
|
1546
|
+
fg: lineFg
|
|
1547
|
+
}, splitLast.char + splitLast.after));
|
|
1548
|
+
}
|
|
1549
|
+
const paddedVisibleText = padEndByWidth(sliceByWidth(padEndByWidth(sliceByWidth(contentPadding + padEndByWidth(sanitize(props.line), props.maxLineWidth) + contentPadding, props.horizontalOffset, props.contentWidth), props.contentWidth), props.visibleStart, props.visibleWidth), props.visibleWidth);
|
|
1550
|
+
if (isHScrollbarLine) {
|
|
1551
|
+
let cursorPosInVisible = -1;
|
|
1552
|
+
if (props.showCursor && props.isCurrentLine) cursorPosInVisible = EDITOR_PADDING + charIndexToSanitizedWidth(props.line, props.cursor.col) - props.horizontalOffset - props.visibleStart;
|
|
1553
|
+
return renderHScrollbarLine(paddedVisibleText, hScrollbar, props.visibleWidth, lineFg, showVScrollbar, theme$1, cursorPosInVisible);
|
|
1554
|
+
}
|
|
1555
|
+
const scrollbarPos = props.visibleWidth - 1;
|
|
1556
|
+
const splitScrollbar = splitAtWidth(paddedVisibleText, scrollbarPos);
|
|
1557
|
+
if (props.showCursor && props.isCurrentLine) {
|
|
1558
|
+
const cursorPosInVisible = EDITOR_PADDING + charIndexToSanitizedWidth(props.line, props.cursor.col) - props.horizontalOffset - props.visibleStart;
|
|
1559
|
+
if (cursorPosInVisible >= 0 && cursorPosInVisible < props.visibleWidth) {
|
|
1560
|
+
if (cursorPosInVisible === scrollbarPos) return row(text({
|
|
1561
|
+
bg: lineBg,
|
|
1562
|
+
fg: lineFg
|
|
1563
|
+
}, splitScrollbar.before), text({
|
|
1564
|
+
bg: theme$1.cursorBg,
|
|
1565
|
+
fg: theme$1.cursorFg
|
|
1566
|
+
}, splitScrollbar.char + splitScrollbar.after));
|
|
1567
|
+
const splitCursor = splitAtWidth(splitScrollbar.before, cursorPosInVisible);
|
|
1568
|
+
return row(text({
|
|
1569
|
+
bg: lineBg,
|
|
1570
|
+
fg: lineFg
|
|
1571
|
+
}, splitCursor.before), text({
|
|
1572
|
+
bg: theme$1.cursorBg,
|
|
1573
|
+
fg: theme$1.cursorFg
|
|
1574
|
+
}, splitCursor.char), text({
|
|
1575
|
+
bg: lineBg,
|
|
1576
|
+
fg: lineFg
|
|
1577
|
+
}, splitCursor.after), text({
|
|
1578
|
+
bg: vScrollbarBg,
|
|
1579
|
+
fg: lineFg
|
|
1580
|
+
}, splitScrollbar.char + splitScrollbar.after));
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
return row(text({
|
|
1584
|
+
bg: lineBg,
|
|
1585
|
+
fg: lineFg
|
|
1586
|
+
}, splitScrollbar.before), text({
|
|
1587
|
+
bg: vScrollbarBg,
|
|
1588
|
+
fg: lineFg
|
|
1589
|
+
}, splitScrollbar.char + splitScrollbar.after));
|
|
1590
|
+
}
|
|
1591
|
+
function renderHScrollbarLine(content, hScrollbar, visibleWidth, fg, showVScrollbar, theme$1, cursorPos) {
|
|
1592
|
+
const vScrollbarPos = visibleWidth - 1;
|
|
1593
|
+
const splitVScrollbar = splitAtWidth(content, vScrollbarPos);
|
|
1594
|
+
const mainContent = splitVScrollbar.before;
|
|
1595
|
+
const vScrollbarChar = splitVScrollbar.char + splitVScrollbar.after;
|
|
1596
|
+
const thumbStart = Math.max(hScrollbar.start, 0);
|
|
1597
|
+
const thumbEnd = Math.min(hScrollbar.end, vScrollbarPos);
|
|
1598
|
+
const vScrollbarBg = showVScrollbar ? theme$1.scrollbarThumb : theme$1.hScrollbarTrack;
|
|
1599
|
+
const cursorOnVScrollbar = cursorPos === vScrollbarPos;
|
|
1600
|
+
const cursorInMain = cursorPos >= 0 && cursorPos < visibleWidth && cursorPos < vScrollbarPos;
|
|
1601
|
+
if (thumbEnd <= thumbStart) {
|
|
1602
|
+
if (cursorOnVScrollbar) return row(text({
|
|
1603
|
+
bg: theme$1.hScrollbarTrack,
|
|
1604
|
+
fg
|
|
1605
|
+
}, mainContent), text({
|
|
1606
|
+
bg: theme$1.cursorBg,
|
|
1607
|
+
fg: theme$1.cursorFg
|
|
1608
|
+
}, vScrollbarChar));
|
|
1609
|
+
if (cursorInMain) {
|
|
1610
|
+
const splitCursor = splitAtWidth(mainContent, cursorPos);
|
|
1611
|
+
return row(text({
|
|
1612
|
+
bg: theme$1.hScrollbarTrack,
|
|
1613
|
+
fg
|
|
1614
|
+
}, splitCursor.before), text({
|
|
1615
|
+
bg: theme$1.cursorBg,
|
|
1616
|
+
fg: theme$1.cursorFg
|
|
1617
|
+
}, splitCursor.char), text({
|
|
1618
|
+
bg: theme$1.hScrollbarTrack,
|
|
1619
|
+
fg
|
|
1620
|
+
}, splitCursor.after), text({
|
|
1621
|
+
bg: vScrollbarBg,
|
|
1622
|
+
fg
|
|
1623
|
+
}, vScrollbarChar));
|
|
1624
|
+
}
|
|
1625
|
+
return row(text({
|
|
1626
|
+
bg: theme$1.hScrollbarTrack,
|
|
1627
|
+
fg
|
|
1628
|
+
}, mainContent), text({
|
|
1629
|
+
bg: vScrollbarBg,
|
|
1630
|
+
fg
|
|
1631
|
+
}, vScrollbarChar));
|
|
1632
|
+
}
|
|
1633
|
+
const segments = [];
|
|
1634
|
+
if (cursorInMain) {
|
|
1635
|
+
const splitCursor = splitAtWidth(mainContent, cursorPos);
|
|
1636
|
+
if (cursorPos > 0) if (cursorPos <= thumbStart) segments.push(text({
|
|
1637
|
+
bg: theme$1.hScrollbarTrack,
|
|
1638
|
+
fg
|
|
1639
|
+
}, splitCursor.before));
|
|
1640
|
+
else if (cursorPos >= thumbEnd) if (thumbStart > 0) {
|
|
1641
|
+
const splitThumbStart = splitAtWidth(splitCursor.before, thumbStart);
|
|
1642
|
+
segments.push(text({
|
|
1643
|
+
bg: theme$1.hScrollbarTrack,
|
|
1644
|
+
fg
|
|
1645
|
+
}, splitThumbStart.before));
|
|
1646
|
+
const thumbPart = splitThumbStart.char + splitThumbStart.after;
|
|
1647
|
+
if (thumbEnd < cursorPos) {
|
|
1648
|
+
const splitThumbEnd = splitAtWidth(thumbPart, thumbEnd - thumbStart);
|
|
1649
|
+
segments.push(text({
|
|
1650
|
+
bg: theme$1.hScrollbarThumb,
|
|
1651
|
+
fg
|
|
1652
|
+
}, splitThumbEnd.before + splitThumbEnd.char));
|
|
1653
|
+
segments.push(text({
|
|
1654
|
+
bg: theme$1.hScrollbarTrack,
|
|
1655
|
+
fg
|
|
1656
|
+
}, splitThumbEnd.after));
|
|
1657
|
+
} else segments.push(text({
|
|
1658
|
+
bg: theme$1.hScrollbarThumb,
|
|
1659
|
+
fg
|
|
1660
|
+
}, thumbPart));
|
|
1661
|
+
} else {
|
|
1662
|
+
const splitThumbEnd = splitAtWidth(splitCursor.before, thumbEnd);
|
|
1663
|
+
segments.push(text({
|
|
1664
|
+
bg: theme$1.hScrollbarThumb,
|
|
1665
|
+
fg
|
|
1666
|
+
}, splitThumbEnd.before + splitThumbEnd.char));
|
|
1667
|
+
segments.push(text({
|
|
1668
|
+
bg: theme$1.hScrollbarTrack,
|
|
1669
|
+
fg
|
|
1670
|
+
}, splitThumbEnd.after));
|
|
1671
|
+
}
|
|
1672
|
+
else if (thumbStart > 0) {
|
|
1673
|
+
const splitThumbStart = splitAtWidth(splitCursor.before, thumbStart);
|
|
1674
|
+
segments.push(text({
|
|
1675
|
+
bg: theme$1.hScrollbarTrack,
|
|
1676
|
+
fg
|
|
1677
|
+
}, splitThumbStart.before));
|
|
1678
|
+
segments.push(text({
|
|
1679
|
+
bg: theme$1.hScrollbarThumb,
|
|
1680
|
+
fg
|
|
1681
|
+
}, splitThumbStart.char + splitThumbStart.after));
|
|
1682
|
+
} else segments.push(text({
|
|
1683
|
+
bg: theme$1.hScrollbarThumb,
|
|
1684
|
+
fg
|
|
1685
|
+
}, splitCursor.before));
|
|
1686
|
+
segments.push(text({
|
|
1687
|
+
bg: theme$1.cursorBg,
|
|
1688
|
+
fg: theme$1.cursorFg
|
|
1689
|
+
}, splitCursor.char));
|
|
1690
|
+
const afterCursorPos = cursorPos + 1;
|
|
1691
|
+
if (afterCursorPos < vScrollbarPos) {
|
|
1692
|
+
const afterContent = splitCursor.after;
|
|
1693
|
+
if (afterCursorPos >= thumbEnd) segments.push(text({
|
|
1694
|
+
bg: theme$1.hScrollbarTrack,
|
|
1695
|
+
fg
|
|
1696
|
+
}, afterContent));
|
|
1697
|
+
else if (afterCursorPos < thumbStart) {
|
|
1698
|
+
const splitToThumb = splitAtWidth(afterContent, thumbStart - afterCursorPos);
|
|
1699
|
+
segments.push(text({
|
|
1700
|
+
bg: theme$1.hScrollbarTrack,
|
|
1701
|
+
fg
|
|
1702
|
+
}, splitToThumb.before + splitToThumb.char));
|
|
1703
|
+
const afterThumbStart = splitToThumb.after;
|
|
1704
|
+
const thumbLen = thumbEnd - thumbStart - 1;
|
|
1705
|
+
if (thumbLen > 0) {
|
|
1706
|
+
const splitThumbEnd = splitAtWidth(afterThumbStart, thumbLen);
|
|
1707
|
+
segments.push(text({
|
|
1708
|
+
bg: theme$1.hScrollbarThumb,
|
|
1709
|
+
fg
|
|
1710
|
+
}, splitThumbEnd.before + splitThumbEnd.char));
|
|
1711
|
+
segments.push(text({
|
|
1712
|
+
bg: theme$1.hScrollbarTrack,
|
|
1713
|
+
fg
|
|
1714
|
+
}, splitThumbEnd.after));
|
|
1715
|
+
} else segments.push(text({
|
|
1716
|
+
bg: theme$1.hScrollbarTrack,
|
|
1717
|
+
fg
|
|
1718
|
+
}, afterThumbStart));
|
|
1719
|
+
} else {
|
|
1720
|
+
const remainingThumb = thumbEnd - afterCursorPos;
|
|
1721
|
+
if (remainingThumb > 0) {
|
|
1722
|
+
const splitThumbEnd = splitAtWidth(afterContent, remainingThumb);
|
|
1723
|
+
segments.push(text({
|
|
1724
|
+
bg: theme$1.hScrollbarThumb,
|
|
1725
|
+
fg
|
|
1726
|
+
}, splitThumbEnd.before + splitThumbEnd.char));
|
|
1727
|
+
segments.push(text({
|
|
1728
|
+
bg: theme$1.hScrollbarTrack,
|
|
1729
|
+
fg
|
|
1730
|
+
}, splitThumbEnd.after));
|
|
1731
|
+
} else segments.push(text({
|
|
1732
|
+
bg: theme$1.hScrollbarTrack,
|
|
1733
|
+
fg
|
|
1734
|
+
}, afterContent));
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
} else {
|
|
1738
|
+
const splitBeforeThumb = splitAtWidth(mainContent, thumbStart);
|
|
1739
|
+
const beforeThumbContent = splitBeforeThumb.before;
|
|
1740
|
+
const afterBeforeThumb = splitBeforeThumb.char + splitBeforeThumb.after;
|
|
1741
|
+
const thumbLength = thumbEnd - thumbStart;
|
|
1742
|
+
const splitThumb = splitAtWidth(afterBeforeThumb, thumbLength);
|
|
1743
|
+
const thumbContent = splitThumb.before + (thumbLength > splitThumb.before.length ? splitThumb.char : "");
|
|
1744
|
+
const afterThumbContent = thumbLength > splitThumb.before.length ? splitThumb.after : splitThumb.char + splitThumb.after;
|
|
1745
|
+
segments.push(text({
|
|
1746
|
+
bg: theme$1.hScrollbarTrack,
|
|
1747
|
+
fg
|
|
1748
|
+
}, beforeThumbContent));
|
|
1749
|
+
segments.push(text({
|
|
1750
|
+
bg: theme$1.hScrollbarThumb,
|
|
1751
|
+
fg
|
|
1752
|
+
}, thumbContent));
|
|
1753
|
+
segments.push(text({
|
|
1754
|
+
bg: theme$1.hScrollbarTrack,
|
|
1755
|
+
fg
|
|
1756
|
+
}, afterThumbContent));
|
|
1757
|
+
}
|
|
1758
|
+
if (cursorOnVScrollbar) segments.push(text({
|
|
1759
|
+
bg: theme$1.cursorBg,
|
|
1760
|
+
fg: theme$1.cursorFg
|
|
1761
|
+
}, vScrollbarChar));
|
|
1762
|
+
else segments.push(text({
|
|
1763
|
+
bg: vScrollbarBg,
|
|
1764
|
+
fg
|
|
1765
|
+
}, vScrollbarChar));
|
|
1766
|
+
return row(...segments);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
//#endregion
|
|
1770
|
+
//#region src/editor/view.ts
|
|
1771
|
+
function view(props) {
|
|
1772
|
+
const model$1 = props.model;
|
|
1773
|
+
const theme$1 = props.theme;
|
|
1774
|
+
const totalWidth = Math.max(10, model$1.terminalWidth);
|
|
1775
|
+
const height = Math.max(5, model$1.terminalHeight);
|
|
1776
|
+
const contentHeight = Math.max(1, height - 2);
|
|
1777
|
+
const maxTreeWidth = model$1.treeVisible ? Math.floor(totalWidth / 2) : 0;
|
|
1778
|
+
const editorContentWidth = Math.max(1, totalWidth - SCROLLBAR_WIDTH);
|
|
1779
|
+
const visibleItems = model$1.flatList.slice(model$1.scrollOffset, model$1.scrollOffset + contentHeight);
|
|
1780
|
+
const visibleLines = model$1.fileContent.slice(model$1.fileScrollOffset, model$1.fileScrollOffset + contentHeight);
|
|
1781
|
+
const fileScrollbar = calcScrollbar({
|
|
1782
|
+
totalItems: model$1.fileContent.length,
|
|
1783
|
+
visibleItems: contentHeight,
|
|
1784
|
+
offset: model$1.fileScrollOffset,
|
|
1785
|
+
height: contentHeight
|
|
1786
|
+
});
|
|
1787
|
+
const maxLineWidth = maxDisplayWidth(model$1.fileContent);
|
|
1788
|
+
const hScrollbar = calcHorizontalScrollbar({
|
|
1789
|
+
maxWidth: maxLineWidth + EDITOR_PADDING * 2,
|
|
1790
|
+
visibleWidth: totalWidth - maxTreeWidth,
|
|
1791
|
+
offset: model$1.fileHorizontalOffset,
|
|
1792
|
+
width: totalWidth - maxTreeWidth
|
|
1793
|
+
});
|
|
1794
|
+
const contentRows = [];
|
|
1795
|
+
for (let i = 0; i < contentHeight; i++) {
|
|
1796
|
+
const node = visibleItems[i];
|
|
1797
|
+
const line = visibleLines[i];
|
|
1798
|
+
const lineIndex = model$1.fileScrollOffset + i;
|
|
1799
|
+
const isLastLine = i === contentHeight - 1;
|
|
1800
|
+
if (model$1.treeVisible) {
|
|
1801
|
+
const treeResult = viewTreeLine({
|
|
1802
|
+
node,
|
|
1803
|
+
isSelected: model$1.scrollOffset + i === model$1.selectedIndex,
|
|
1804
|
+
maxWidth: maxTreeWidth,
|
|
1805
|
+
theme: theme$1
|
|
1806
|
+
});
|
|
1807
|
+
const editorVisibleStart = treeResult.width;
|
|
1808
|
+
const editorVisibleWidth = totalWidth - treeResult.width - SCROLLBAR_WIDTH;
|
|
1809
|
+
const editorLine = viewEditorLine({
|
|
1810
|
+
line,
|
|
1811
|
+
lineIndex,
|
|
1812
|
+
contentWidth: editorContentWidth,
|
|
1813
|
+
horizontalOffset: model$1.fileHorizontalOffset,
|
|
1814
|
+
maxLineWidth,
|
|
1815
|
+
isScrollbarThumb: i >= fileScrollbar.start && i < fileScrollbar.end,
|
|
1816
|
+
cursor: model$1.cursor,
|
|
1817
|
+
showCursor: model$1.focus === "editor" && model$1.openedFile !== null,
|
|
1818
|
+
isCurrentLine: lineIndex === model$1.cursor.line,
|
|
1819
|
+
theme: theme$1,
|
|
1820
|
+
visibleStart: editorVisibleStart,
|
|
1821
|
+
visibleWidth: editorVisibleWidth,
|
|
1822
|
+
dimmed: model$1.focus === "tree",
|
|
1823
|
+
hScrollbar: isLastLine ? hScrollbar : void 0
|
|
1824
|
+
});
|
|
1825
|
+
contentRows.push(row(treeResult.widget, editorLine));
|
|
1826
|
+
} else {
|
|
1827
|
+
const editorLine = viewEditorLine({
|
|
1828
|
+
line,
|
|
1829
|
+
lineIndex,
|
|
1830
|
+
contentWidth: editorContentWidth,
|
|
1831
|
+
horizontalOffset: model$1.fileHorizontalOffset,
|
|
1832
|
+
maxLineWidth,
|
|
1833
|
+
isScrollbarThumb: i >= fileScrollbar.start && i < fileScrollbar.end,
|
|
1834
|
+
cursor: model$1.cursor,
|
|
1835
|
+
showCursor: model$1.focus === "editor" && model$1.openedFile !== null,
|
|
1836
|
+
isCurrentLine: lineIndex === model$1.cursor.line,
|
|
1837
|
+
theme: theme$1,
|
|
1838
|
+
visibleStart: 0,
|
|
1839
|
+
visibleWidth: totalWidth - SCROLLBAR_WIDTH,
|
|
1840
|
+
dimmed: model$1.focus === "tree",
|
|
1841
|
+
hScrollbar: isLastLine ? hScrollbar : void 0
|
|
1842
|
+
});
|
|
1843
|
+
contentRows.push(editorLine);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
const header = viewHeader({
|
|
1847
|
+
rootPath: model$1.root.path,
|
|
1848
|
+
openedFile: model$1.openedFile,
|
|
1849
|
+
width: totalWidth,
|
|
1850
|
+
focus: model$1.focus,
|
|
1851
|
+
modified: model$1.modified,
|
|
1852
|
+
theme: theme$1
|
|
1853
|
+
});
|
|
1854
|
+
const footer = viewFooter({
|
|
1855
|
+
width: totalWidth,
|
|
1856
|
+
focus: model$1.focus,
|
|
1857
|
+
cursorLine: model$1.cursor.line,
|
|
1858
|
+
cursorCol: model$1.cursor.col,
|
|
1859
|
+
mouseEnabled: model$1.mouseEnabled,
|
|
1860
|
+
theme: theme$1
|
|
1861
|
+
});
|
|
1862
|
+
return col(header, ...contentRows, footer);
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
//#endregion
|
|
1866
|
+
//#region src/editor/parse-input.ts
|
|
1867
|
+
function parseTreeInput(str, model$1) {
|
|
1868
|
+
if (str === "\x1B[A" || str === "k") return {
|
|
1869
|
+
type: "MOVE",
|
|
1870
|
+
delta: -1
|
|
1871
|
+
};
|
|
1872
|
+
if (str === "\x1B[B" || str === "j") return {
|
|
1873
|
+
type: "MOVE",
|
|
1874
|
+
delta: 1
|
|
1875
|
+
};
|
|
1876
|
+
if (str === "\x1B[C" || str === "l" || str === "\r" || str === "\n") return {
|
|
1877
|
+
type: "TOGGLE",
|
|
1878
|
+
index: model$1.selectedIndex
|
|
1879
|
+
};
|
|
1880
|
+
if (str === "\x1B[D" || str === "h") {
|
|
1881
|
+
const node = model$1.flatList[model$1.selectedIndex];
|
|
1882
|
+
if (node && node.isDirectory && node.expanded) return {
|
|
1883
|
+
type: "TOGGLE",
|
|
1884
|
+
index: model$1.selectedIndex
|
|
1885
|
+
};
|
|
1886
|
+
return { type: "NOOP" };
|
|
1887
|
+
}
|
|
1888
|
+
if (str === "\x1B[5~") return {
|
|
1889
|
+
type: "MOVE",
|
|
1890
|
+
delta: -20
|
|
1891
|
+
};
|
|
1892
|
+
if (str === "\x1B[6~") return {
|
|
1893
|
+
type: "MOVE",
|
|
1894
|
+
delta: 20
|
|
1895
|
+
};
|
|
1896
|
+
return { type: "NOOP" };
|
|
1897
|
+
}
|
|
1898
|
+
function parseEditorInput(str) {
|
|
1899
|
+
if (str === "\x1B[A") return {
|
|
1900
|
+
type: "CURSOR_MOVE",
|
|
1901
|
+
deltaLine: -1,
|
|
1902
|
+
deltaCol: 0
|
|
1903
|
+
};
|
|
1904
|
+
if (str === "\x1B[B") return {
|
|
1905
|
+
type: "CURSOR_MOVE",
|
|
1906
|
+
deltaLine: 1,
|
|
1907
|
+
deltaCol: 0
|
|
1908
|
+
};
|
|
1909
|
+
if (str === "\x1B[C") return {
|
|
1910
|
+
type: "CURSOR_MOVE",
|
|
1911
|
+
deltaLine: 0,
|
|
1912
|
+
deltaCol: 1
|
|
1913
|
+
};
|
|
1914
|
+
if (str === "\x1B[D") return {
|
|
1915
|
+
type: "CURSOR_MOVE",
|
|
1916
|
+
deltaLine: 0,
|
|
1917
|
+
deltaCol: -1
|
|
1918
|
+
};
|
|
1919
|
+
if (str === "\x1B[5~") return {
|
|
1920
|
+
type: "SCROLL_FILE",
|
|
1921
|
+
delta: -20
|
|
1922
|
+
};
|
|
1923
|
+
if (str === "\x1B[6~") return {
|
|
1924
|
+
type: "SCROLL_FILE",
|
|
1925
|
+
delta: 20
|
|
1926
|
+
};
|
|
1927
|
+
if (str === "" || str === "\b") return { type: "DELETE_CHAR_BEFORE" };
|
|
1928
|
+
if (str === "\x1B[3~") return { type: "DELETE_CHAR" };
|
|
1929
|
+
if (str === "\r" || str === "\n") return { type: "INSERT_NEWLINE" };
|
|
1930
|
+
if (str === "") return { type: "SAVE" };
|
|
1931
|
+
if (str.length > 0 && !str.startsWith("\x1B")) return {
|
|
1932
|
+
type: "INSERT_CHAR",
|
|
1933
|
+
char: str
|
|
1934
|
+
};
|
|
1935
|
+
return { type: "NOOP" };
|
|
1936
|
+
}
|
|
1937
|
+
function parseMouseInput(str, model$1) {
|
|
1938
|
+
const mouseMatch = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
|
1939
|
+
if (!mouseMatch || !mouseMatch[1] || !mouseMatch[2] || !mouseMatch[3] || !mouseMatch[4]) return { type: "NOOP" };
|
|
1940
|
+
const button = parseInt(mouseMatch[1], 10);
|
|
1941
|
+
const mouseCol = parseInt(mouseMatch[2], 10);
|
|
1942
|
+
const mouseRow = parseInt(mouseMatch[3], 10);
|
|
1943
|
+
const isPress = mouseMatch[4] === "M";
|
|
1944
|
+
const treeWidth = model$1.treeVisible ? Math.floor(model$1.terminalWidth / 2) : 0;
|
|
1945
|
+
if (button === 0 && isPress) return {
|
|
1946
|
+
type: "CLICK",
|
|
1947
|
+
row: mouseRow,
|
|
1948
|
+
col: mouseCol
|
|
1949
|
+
};
|
|
1950
|
+
if (button === 64) {
|
|
1951
|
+
if (model$1.treeVisible && mouseCol <= treeWidth) return {
|
|
1952
|
+
type: "SCROLL",
|
|
1953
|
+
delta: -3
|
|
1954
|
+
};
|
|
1955
|
+
return {
|
|
1956
|
+
type: "SCROLL_FILE",
|
|
1957
|
+
delta: -3
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
if (button === 65) {
|
|
1961
|
+
if (model$1.treeVisible && mouseCol <= treeWidth) return {
|
|
1962
|
+
type: "SCROLL",
|
|
1963
|
+
delta: 3
|
|
1964
|
+
};
|
|
1965
|
+
return {
|
|
1966
|
+
type: "SCROLL_FILE",
|
|
1967
|
+
delta: 3
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
if (button === 68) return {
|
|
1971
|
+
type: "SCROLL_FILE_H",
|
|
1972
|
+
delta: -8
|
|
1973
|
+
};
|
|
1974
|
+
if (button === 69) return {
|
|
1975
|
+
type: "SCROLL_FILE_H",
|
|
1976
|
+
delta: 8
|
|
1977
|
+
};
|
|
1978
|
+
return { type: "NOOP" };
|
|
1979
|
+
}
|
|
1980
|
+
function parseInput(data, model$1) {
|
|
1981
|
+
const str = data.toString();
|
|
1982
|
+
if (str === "q" || str === "") return { type: "NOOP" };
|
|
1983
|
+
if (str === " ") return { type: "TOGGLE_TREE" };
|
|
1984
|
+
if (str === "\x1B") return { type: "TOGGLE_MOUSE" };
|
|
1985
|
+
if (model$1.focus === "tree") {
|
|
1986
|
+
const msg = parseTreeInput(str, model$1);
|
|
1987
|
+
if (msg.type !== "NOOP") return msg;
|
|
1988
|
+
}
|
|
1989
|
+
if (model$1.focus === "editor" && model$1.openedFile) {
|
|
1990
|
+
const msg = parseEditorInput(str);
|
|
1991
|
+
if (msg.type !== "NOOP") return msg;
|
|
1992
|
+
}
|
|
1993
|
+
const mouseMsg = parseMouseInput(str, model$1);
|
|
1994
|
+
if (mouseMsg.type !== "NOOP") return mouseMsg;
|
|
1995
|
+
return { type: "NOOP" };
|
|
1996
|
+
}
|
|
1997
|
+
function shouldQuit(data) {
|
|
1998
|
+
const str = data.toString();
|
|
1999
|
+
return str === "q" || str === "";
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
//#endregion
|
|
2003
|
+
//#region src/editor/terminal.ts
|
|
2004
|
+
function enterAltScreen() {
|
|
2005
|
+
return "\x1B[?1049h";
|
|
2006
|
+
}
|
|
2007
|
+
function exitAltScreen() {
|
|
2008
|
+
return "\x1B[?1049l";
|
|
2009
|
+
}
|
|
2010
|
+
function hideCursor() {
|
|
2011
|
+
return "\x1B[?25l";
|
|
2012
|
+
}
|
|
2013
|
+
function showCursor() {
|
|
2014
|
+
return "\x1B[?25h";
|
|
2015
|
+
}
|
|
2016
|
+
function enableMouse() {
|
|
2017
|
+
return "\x1B[?1000h\x1B[?1006h";
|
|
2018
|
+
}
|
|
2019
|
+
function disableMouse() {
|
|
2020
|
+
return "\x1B[?1000l\x1B[?1006l";
|
|
2021
|
+
}
|
|
2022
|
+
function clearScreen() {
|
|
2023
|
+
return "\x1B[2J\x1B[H";
|
|
2024
|
+
}
|
|
2025
|
+
function moveCursor(x, y) {
|
|
2026
|
+
return `\x1b[${y};${x}H`;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
//#endregion
|
|
2030
|
+
//#region src/index.ts
|
|
2031
|
+
function cleanup() {
|
|
2032
|
+
process.stdout.write(showCursor() + disableMouse() + exitAltScreen());
|
|
2033
|
+
process.stdin.setRawMode(false);
|
|
2034
|
+
}
|
|
2035
|
+
function getCursorScreenPosition(model$1) {
|
|
2036
|
+
const contentHeight = model$1.terminalHeight - 2;
|
|
2037
|
+
if (model$1.focus === "tree" && model$1.treeVisible) {
|
|
2038
|
+
if (model$1.selectedIndex < model$1.scrollOffset || model$1.selectedIndex >= model$1.scrollOffset + contentHeight) return null;
|
|
2039
|
+
return {
|
|
2040
|
+
x: 1,
|
|
2041
|
+
y: 2 + (model$1.selectedIndex - model$1.scrollOffset)
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
if (model$1.focus === "editor" && model$1.openedFile) {
|
|
2045
|
+
if (model$1.cursor.line < model$1.fileScrollOffset || model$1.cursor.line >= model$1.fileScrollOffset + contentHeight) return null;
|
|
2046
|
+
const screenRow = 2 + (model$1.cursor.line - model$1.fileScrollOffset);
|
|
2047
|
+
const cursorPosInView = EDITOR_PADDING + charIndexToSanitizedWidth(model$1.fileContent[model$1.cursor.line] || "", model$1.cursor.col) - model$1.fileHorizontalOffset;
|
|
2048
|
+
const editorWidth = model$1.terminalWidth;
|
|
2049
|
+
if (cursorPosInView < 0 || cursorPosInView >= editorWidth) return null;
|
|
2050
|
+
return {
|
|
2051
|
+
x: 1 + cursorPosInView,
|
|
2052
|
+
y: screenRow
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
function handleError(err) {
|
|
2058
|
+
writeFileSync("/tmp/editor-error.log", err instanceof Error ? `${err.message}\n${err.stack}` : String(err));
|
|
2059
|
+
cleanup();
|
|
2060
|
+
console.error("Error logged to /tmp/editor-error.log");
|
|
2061
|
+
process.exit(1);
|
|
2062
|
+
}
|
|
2063
|
+
const targetPath = process.argv[2] || process.cwd();
|
|
2064
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2065
|
+
console.error("This program requires an interactive terminal.");
|
|
2066
|
+
process.exit(1);
|
|
2067
|
+
}
|
|
2068
|
+
process.on("uncaughtException", handleError);
|
|
2069
|
+
let model = initModel(targetPath, process.stdout.columns || 80, process.stdout.rows || 24);
|
|
2070
|
+
const theme = defaultTheme;
|
|
2071
|
+
process.stdout.write(enterAltScreen() + enableMouse());
|
|
2072
|
+
function renderWithCursor() {
|
|
2073
|
+
let output = render(view({
|
|
2074
|
+
model,
|
|
2075
|
+
theme
|
|
2076
|
+
}), 0, 0, model.terminalWidth, model.terminalHeight);
|
|
2077
|
+
const cursorPos = getCursorScreenPosition(model);
|
|
2078
|
+
if (cursorPos) output += showCursor() + moveCursor(cursorPos.x, cursorPos.y);
|
|
2079
|
+
else output += hideCursor();
|
|
2080
|
+
return output;
|
|
2081
|
+
}
|
|
2082
|
+
process.stdout.write(renderWithCursor());
|
|
2083
|
+
process.stdout.on("resize", () => {
|
|
2084
|
+
try {
|
|
2085
|
+
const msg = {
|
|
2086
|
+
type: "RESIZE",
|
|
2087
|
+
height: process.stdout.rows || 24,
|
|
2088
|
+
width: process.stdout.columns || 80
|
|
2089
|
+
};
|
|
2090
|
+
model = update(model, msg);
|
|
2091
|
+
process.stdout.write(clearScreen() + renderWithCursor());
|
|
2092
|
+
} catch (err) {
|
|
2093
|
+
handleError(err);
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
process.stdin.setRawMode(true);
|
|
2097
|
+
process.stdin.resume();
|
|
2098
|
+
process.stdin.on("data", (data) => {
|
|
2099
|
+
try {
|
|
2100
|
+
if (shouldQuit(data)) {
|
|
2101
|
+
cleanup();
|
|
2102
|
+
process.exit(0);
|
|
2103
|
+
}
|
|
2104
|
+
const prevMouseEnabled = model.mouseEnabled;
|
|
2105
|
+
const msg = parseInput(data, model);
|
|
2106
|
+
model = update(model, msg);
|
|
2107
|
+
let output = "";
|
|
2108
|
+
if (model.mouseEnabled !== prevMouseEnabled) output += model.mouseEnabled ? enableMouse() : disableMouse();
|
|
2109
|
+
output += renderWithCursor();
|
|
2110
|
+
process.stdout.write(output);
|
|
2111
|
+
} catch (err) {
|
|
2112
|
+
handleError(err);
|
|
2113
|
+
}
|
|
2114
|
+
});
|
|
2115
|
+
process.on("exit", cleanup);
|
|
2116
|
+
process.on("SIGINT", () => {
|
|
2117
|
+
cleanup();
|
|
2118
|
+
process.exit(0);
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
//#endregion
|
|
2122
|
+
export { };
|