@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.
Files changed (2) hide show
  1. package/dist/index.mjs +71 -37
  2. 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
- this.data.value = max > 0 ? value.slice(0, max) : value;
76
- this.cursorPos = this.data.value.length;
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 cursorInDisplay = this.cursorPos;
90
- let before = display.slice(0, cursorInDisplay);
91
- let cursor = display[cursorInDisplay] || " ";
92
- let after = display.slice(cursorInDisplay + 1);
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 = display.slice(start, cursorInDisplay);
97
- cursor = display[cursorInDisplay] || " ";
98
- after = display.slice(cursorInDisplay + 1, start + maxVisible);
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
- this.data.value = this.data.value.slice(0, this.cursorPos - 1) + this.data.value.slice(this.cursorPos);
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
- this.data.value = this.data.value.slice(0, this.cursorPos) + this.data.value.slice(this.cursorPos + 1);
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 && this.data.value.length >= max) return true;
179
- this.data.value = this.data.value.slice(0, this.cursorPos) + char + this.data.value.slice(this.cursorPos);
180
- this.cursorPos++;
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
- this.data.selectedIndex = items.length - 1;
330
- if (items.length > maxVisible) this.data.scrollOffset = items.length - maxVisible;
331
- this.data.onSelect?.(items[this.data.selectedIndex], this.data.selectedIndex);
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 rest = text;
388
- while (rest.length > 0 && used < target) {
389
- if (rest.startsWith("\x1B")) {
390
- const m = CSI_RE.exec(rest);
391
- if (m && m[0].length > 0) {
392
- out += m[0];
393
- rest = rest.slice(m[0].length);
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 = rest.charAt(0);
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
- rest = rest.slice(1);
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.2",
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.4"
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",