@dui-toolkit/plugin-tui 0.1.1-next.2 → 0.1.1-next.4
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 +71 -37
- package/package.json +5 -2
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { stripAnsi, truncateByCells, visibleLength } from "@bdocs/dui";
|
|
1
|
+
import { splitGraphemes, stripAnsi, truncateByCells, visibleLength } from "@bdocs/dui";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
//#region src/widget.ts
|
|
4
4
|
var BaseWidget = class {
|
|
@@ -72,8 +72,11 @@ var TextInput = class extends BaseWidget {
|
|
|
72
72
|
/** Set the text value programmatically. */
|
|
73
73
|
setValue(value) {
|
|
74
74
|
const max = this.data.maxLength;
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
if (max > 0) {
|
|
76
|
+
const g = splitGraphemes(value);
|
|
77
|
+
this.data.value = g.slice(0, max).join("");
|
|
78
|
+
} else this.data.value = value;
|
|
79
|
+
this.cursorPos = splitGraphemes(this.data.value).length;
|
|
77
80
|
this.data.onChange?.(this.data.value);
|
|
78
81
|
}
|
|
79
82
|
/** Get the current text value. */
|
|
@@ -86,16 +89,18 @@ var TextInput = class extends BaseWidget {
|
|
|
86
89
|
const value = this.data.mask ? this.data.mask.repeat(this.data.value.length) : this.data.value;
|
|
87
90
|
const display = value || this.data.placeholder;
|
|
88
91
|
const isEmpty = !value;
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
let
|
|
92
|
-
let
|
|
92
|
+
const graphemes = splitGraphemes(display);
|
|
93
|
+
const cursorInDisplay = Math.min(this.cursorPos, graphemes.length);
|
|
94
|
+
let before = graphemes.slice(0, cursorInDisplay).join("");
|
|
95
|
+
let cursor = graphemes[cursorInDisplay] || " ";
|
|
96
|
+
let after = graphemes.slice(cursorInDisplay + 1).join("");
|
|
97
|
+
graphemes.slice(cursorInDisplay + 1);
|
|
93
98
|
const maxVisible = width - 4;
|
|
94
99
|
if (visibleLength(display) > maxVisible) {
|
|
95
100
|
const start = Math.max(0, cursorInDisplay - Math.floor(maxVisible / 2));
|
|
96
|
-
before =
|
|
97
|
-
cursor =
|
|
98
|
-
after =
|
|
101
|
+
before = graphemes.slice(start, cursorInDisplay).join("");
|
|
102
|
+
cursor = graphemes[cursorInDisplay] || " ";
|
|
103
|
+
after = graphemes.slice(cursorInDisplay + 1, start + maxVisible).join("");
|
|
99
104
|
}
|
|
100
105
|
const lines = [];
|
|
101
106
|
lines.push(`┌${"─".repeat(width - 2)}┐`);
|
|
@@ -138,14 +143,18 @@ var TextInput = class extends BaseWidget {
|
|
|
138
143
|
return true;
|
|
139
144
|
case "Backspace":
|
|
140
145
|
if (this.cursorPos > 0) {
|
|
141
|
-
|
|
142
|
-
this.cursorPos
|
|
146
|
+
const g = splitGraphemes(this.data.value);
|
|
147
|
+
const pos = Math.min(this.cursorPos, g.length);
|
|
148
|
+
this.data.value = g.slice(0, pos - 1).join("") + g.slice(pos).join("");
|
|
149
|
+
this.cursorPos = pos - 1;
|
|
143
150
|
this.data.onChange?.(this.data.value);
|
|
144
151
|
}
|
|
145
152
|
return true;
|
|
146
153
|
case "Delete":
|
|
147
154
|
if (this.cursorPos < this.data.value.length) {
|
|
148
|
-
|
|
155
|
+
const g = splitGraphemes(this.data.value);
|
|
156
|
+
const pos = Math.min(this.cursorPos, g.length);
|
|
157
|
+
this.data.value = g.slice(0, pos).join("") + g.slice(pos + 1).join("");
|
|
149
158
|
this.data.onChange?.(this.data.value);
|
|
150
159
|
}
|
|
151
160
|
return true;
|
|
@@ -175,9 +184,13 @@ var TextInput = class extends BaseWidget {
|
|
|
175
184
|
}
|
|
176
185
|
if (char && char.length === 1 && char >= " ") {
|
|
177
186
|
const max = this.data.maxLength;
|
|
178
|
-
if (max > 0
|
|
179
|
-
|
|
180
|
-
|
|
187
|
+
if (max > 0) {
|
|
188
|
+
if (splitGraphemes(this.data.value).length >= max) return true;
|
|
189
|
+
}
|
|
190
|
+
const g = splitGraphemes(this.data.value);
|
|
191
|
+
const pos = Math.min(this.cursorPos, g.length);
|
|
192
|
+
this.data.value = g.slice(0, pos).join("") + char + g.slice(pos).join("");
|
|
193
|
+
this.cursorPos = pos + 1;
|
|
181
194
|
this.data.onChange?.(this.data.value);
|
|
182
195
|
return true;
|
|
183
196
|
}
|
|
@@ -280,7 +293,7 @@ var SelectList = class extends BaseWidget {
|
|
|
280
293
|
if (!this.focused) return false;
|
|
281
294
|
const { key, char, ctrl } = event;
|
|
282
295
|
const items = this.getFilteredItems();
|
|
283
|
-
const maxVisible = Math.max(1, this.viewportHeight - 2);
|
|
296
|
+
const maxVisible = Math.max(1, this.viewportHeight - (this.data.filterActive ? 4 : 2));
|
|
284
297
|
if (this.data.filterActive) {
|
|
285
298
|
if (key === "Escape") {
|
|
286
299
|
this.data.filterActive = false;
|
|
@@ -307,28 +320,30 @@ var SelectList = class extends BaseWidget {
|
|
|
307
320
|
}
|
|
308
321
|
switch (key) {
|
|
309
322
|
case "ArrowUp":
|
|
310
|
-
if (this.data.selectedIndex > 0) {
|
|
323
|
+
if (items.length > 0 && this.data.selectedIndex > 0) {
|
|
311
324
|
this.data.selectedIndex--;
|
|
312
325
|
if (this.data.selectedIndex < this.data.scrollOffset) this.data.scrollOffset = this.data.selectedIndex;
|
|
313
326
|
}
|
|
314
|
-
this.data.onSelect?.(items[this.data.selectedIndex], this.data.selectedIndex);
|
|
327
|
+
if (items[this.data.selectedIndex]) this.data.onSelect?.(items[this.data.selectedIndex], this.data.selectedIndex);
|
|
315
328
|
return true;
|
|
316
329
|
case "ArrowDown":
|
|
317
|
-
if (this.data.selectedIndex < items.length - 1) {
|
|
330
|
+
if (items.length > 0 && this.data.selectedIndex < items.length - 1) {
|
|
318
331
|
this.data.selectedIndex++;
|
|
319
332
|
if (this.data.selectedIndex >= this.data.scrollOffset + maxVisible) this.data.scrollOffset = this.data.selectedIndex - maxVisible + 1;
|
|
320
333
|
}
|
|
321
|
-
this.data.onSelect?.(items[this.data.selectedIndex], this.data.selectedIndex);
|
|
334
|
+
if (items[this.data.selectedIndex]) this.data.onSelect?.(items[this.data.selectedIndex], this.data.selectedIndex);
|
|
322
335
|
return true;
|
|
323
336
|
case "Home":
|
|
324
337
|
this.data.selectedIndex = 0;
|
|
325
338
|
this.data.scrollOffset = 0;
|
|
326
|
-
this.data.onSelect?.(items[0], 0);
|
|
339
|
+
if (items[0]) this.data.onSelect?.(items[0], 0);
|
|
327
340
|
return true;
|
|
328
341
|
case "End":
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
342
|
+
if (items.length > 0) {
|
|
343
|
+
this.data.selectedIndex = items.length - 1;
|
|
344
|
+
if (items.length > maxVisible) this.data.scrollOffset = items.length - maxVisible;
|
|
345
|
+
this.data.onSelect?.(items[this.data.selectedIndex], this.data.selectedIndex);
|
|
346
|
+
}
|
|
332
347
|
return true;
|
|
333
348
|
case "Enter": {
|
|
334
349
|
const sel = items[this.data.selectedIndex];
|
|
@@ -368,7 +383,28 @@ var SelectList = class extends BaseWidget {
|
|
|
368
383
|
* corrupts terminal state) or wide glyphs (CJK / emoji). These helpers stay
|
|
369
384
|
* cell-aware and never split an escape sequence.
|
|
370
385
|
*/
|
|
371
|
-
const CSI_RE = /\x1b\[[0-9;:<=>?]*[
|
|
386
|
+
const CSI_RE = /\x1b\[[0-9;:<=>?]*[ -\/]*[@-~]/;
|
|
387
|
+
/**
|
|
388
|
+
* Skip past a single escape sequence starting at `rest[0]`.
|
|
389
|
+
* Returns the length of the escape sequence, or 0 if `rest` does not start
|
|
390
|
+
* with a recognised escape.
|
|
391
|
+
*/
|
|
392
|
+
function escapeLength(rest) {
|
|
393
|
+
if (!rest.startsWith("\x1B")) return 0;
|
|
394
|
+
const csi = CSI_RE.exec(rest);
|
|
395
|
+
if (csi && csi[0].length > 0) return csi[0].length;
|
|
396
|
+
if (rest.length >= 2) {
|
|
397
|
+
if (rest[1] === "]") {
|
|
398
|
+
for (let i = 2; i < rest.length; i++) {
|
|
399
|
+
if (rest[i] === "\x07") return i + 1;
|
|
400
|
+
if (rest[i] === "\x1B" && rest[i + 1] === "\\") return i + 2;
|
|
401
|
+
}
|
|
402
|
+
return rest.length;
|
|
403
|
+
}
|
|
404
|
+
return 2;
|
|
405
|
+
}
|
|
406
|
+
return 1;
|
|
407
|
+
}
|
|
372
408
|
/**
|
|
373
409
|
* Truncate an ANSI-wrapped string to `maxCells` terminal cells.
|
|
374
410
|
*
|
|
@@ -384,22 +420,20 @@ function truncateAnsi(text, maxCells) {
|
|
|
384
420
|
const target = Math.max(0, maxCells - 1);
|
|
385
421
|
let out = "";
|
|
386
422
|
let used = 0;
|
|
387
|
-
let
|
|
388
|
-
while (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
423
|
+
let pos = 0;
|
|
424
|
+
while (pos < text.length && used < target) {
|
|
425
|
+
const escLen = escapeLength(text.slice(pos));
|
|
426
|
+
if (escLen > 0) {
|
|
427
|
+
out += text.slice(pos, pos + escLen);
|
|
428
|
+
pos += escLen;
|
|
429
|
+
continue;
|
|
396
430
|
}
|
|
397
|
-
const ch =
|
|
431
|
+
const ch = text.charAt(pos);
|
|
398
432
|
const w = visibleLength(ch);
|
|
399
433
|
if (used + w > target) break;
|
|
400
434
|
out += ch;
|
|
401
435
|
used += w;
|
|
402
|
-
|
|
436
|
+
pos++;
|
|
403
437
|
}
|
|
404
438
|
return out + "…";
|
|
405
439
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dui-toolkit/plugin-tui",
|
|
3
|
-
"version": "0.1.1-next.
|
|
3
|
+
"version": "0.1.1-next.4",
|
|
4
4
|
"description": "TUI widget toolkit plugin for @bdocs/dui — text inputs, select lists, modals, status bars, and more.",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.mts",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"type": "module",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@bdocs/dui": "0.7.0-next.
|
|
31
|
+
"@bdocs/dui": "0.7.0-next.6"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^22.0.0",
|
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
"typescript": "^5.9.3",
|
|
37
37
|
"vitest": "^3.0.0"
|
|
38
38
|
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@bdocs/dui": "0.7.0-next.6"
|
|
41
|
+
},
|
|
39
42
|
"scripts": {
|
|
40
43
|
"build": "tsdown --config-loader unrun",
|
|
41
44
|
"dev": "tsdown --watch --config-loader unrun",
|