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/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 { };