@simon_he/vue-tui 0.0.1 → 0.0.3

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/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  ```vue
14
14
  <script setup lang="ts">
15
15
  import { computed, ref } from 'vue'
16
- import { TerminalProvider, TBox, TInput, TText, useLayout } from 'vue-terminal'
16
+ import { TerminalProvider, TBox, TInput, TText, useLayout } from '@simon_he/vue-tui'
17
17
 
18
18
  const layout = useLayout()
19
19
  const cols = computed(() => layout.clipRect?.w ?? 80)
package/dist/index.cjs CHANGED
@@ -454,33 +454,6 @@ function createNodePathPickerProvider() {
454
454
  };
455
455
  }
456
456
 
457
- // src/cli/recording.ts
458
- async function loadFs() {
459
- return import("fs");
460
- }
461
- async function writeEventLog(path, events) {
462
- const { writeFileSync } = await loadFs();
463
- const lines = events.map((e) => JSON.stringify(e)).join("\n");
464
- writeFileSync(path, `${lines}${lines ? "\n" : ""}`, "utf8");
465
- }
466
- async function writeSnapshot(path, lines) {
467
- const { writeFileSync } = await loadFs();
468
- writeFileSync(path, `${lines.join("\n")}
469
- `, "utf8");
470
- }
471
- async function readEventLog(path) {
472
- const { readFileSync } = await loadFs();
473
- const raw = readFileSync(path, "utf8");
474
- const out = [];
475
- for (const line of raw.split(/\r?\n/g)) {
476
- const trimmed = line.trim();
477
- if (!trimmed)
478
- continue;
479
- out.push(JSON.parse(trimmed));
480
- }
481
- return out;
482
- }
483
-
484
457
  // src/utils/path.ts
485
458
  function normalizeSeparators(input) {
486
459
  return input.replace(/\\/g, "/");
@@ -652,43 +625,31 @@ function resolveUserPath(workspaceAbs, input) {
652
625
  return resolvePath(normalizePath(workspaceAbs), normalized);
653
626
  }
654
627
 
655
- // src/core/buffer/width.ts
656
- var fullWidthRanges = [
657
- [4352, 4447],
658
- [9001, 9002],
659
- [11904, 42191],
660
- [44032, 55203],
661
- [63744, 64255],
662
- [65040, 65049],
663
- [65072, 65135],
664
- [65280, 65376],
665
- [65504, 65510]
666
- ];
667
- function isFullWidthCodePoint(codePoint) {
668
- for (const [start, end] of fullWidthRanges) {
669
- if (codePoint >= start && codePoint <= end)
670
- return true;
671
- }
672
- return false;
628
+ // src/cli/recording.ts
629
+ async function loadFs() {
630
+ return import("fs");
673
631
  }
674
- function isEmojiLike(codePoint) {
675
- return codePoint >= 127744 && codePoint <= 129791 || codePoint >= 127462 && codePoint <= 127487;
632
+ async function writeEventLog(path, events) {
633
+ const { writeFileSync } = await loadFs();
634
+ const lines = events.map((e) => JSON.stringify(e)).join("\n");
635
+ writeFileSync(path, `${lines}${lines ? "\n" : ""}`, "utf8");
676
636
  }
677
- var extendedPictographicRe = null;
678
- try {
679
- extendedPictographicRe = new RegExp("\\p{Extended_Pictographic}", "u");
680
- } catch {
681
- extendedPictographicRe = null;
637
+ async function writeSnapshot(path, lines) {
638
+ const { writeFileSync } = await loadFs();
639
+ writeFileSync(path, `${lines.join("\n")}
640
+ `, "utf8");
682
641
  }
683
- function charCellWidth(text) {
684
- if (!text)
685
- return 1;
686
- if (extendedPictographicRe == null ? void 0 : extendedPictographicRe.test(text))
687
- return 2;
688
- const codePoint = text.codePointAt(0);
689
- if (codePoint == null)
690
- return 1;
691
- return isFullWidthCodePoint(codePoint) || isEmojiLike(codePoint) ? 2 : 1;
642
+ async function readEventLog(path) {
643
+ const { readFileSync } = await loadFs();
644
+ const raw = readFileSync(path, "utf8");
645
+ const out = [];
646
+ for (const line of raw.split(/\r?\n/g)) {
647
+ const trimmed = line.trim();
648
+ if (!trimmed)
649
+ continue;
650
+ out.push(JSON.parse(trimmed));
651
+ }
652
+ return out;
692
653
  }
693
654
 
694
655
  // src/core/ansi/sgr.ts
@@ -896,6 +857,45 @@ function parseAnsiSgr(input, baseStyle = {}) {
896
857
  return segments;
897
858
  }
898
859
 
860
+ // src/core/buffer/width.ts
861
+ var fullWidthRanges = [
862
+ [4352, 4447],
863
+ [9001, 9002],
864
+ [11904, 42191],
865
+ [44032, 55203],
866
+ [63744, 64255],
867
+ [65040, 65049],
868
+ [65072, 65135],
869
+ [65280, 65376],
870
+ [65504, 65510]
871
+ ];
872
+ function isFullWidthCodePoint(codePoint) {
873
+ for (const [start, end] of fullWidthRanges) {
874
+ if (codePoint >= start && codePoint <= end)
875
+ return true;
876
+ }
877
+ return false;
878
+ }
879
+ function isEmojiLike(codePoint) {
880
+ return codePoint >= 127744 && codePoint <= 129791 || codePoint >= 127462 && codePoint <= 127487;
881
+ }
882
+ var extendedPictographicRe = null;
883
+ try {
884
+ extendedPictographicRe = new RegExp("\\p{Extended_Pictographic}", "u");
885
+ } catch {
886
+ extendedPictographicRe = null;
887
+ }
888
+ function charCellWidth(text) {
889
+ if (!text)
890
+ return 1;
891
+ if (extendedPictographicRe == null ? void 0 : extendedPictographicRe.test(text))
892
+ return 2;
893
+ const codePoint = text.codePointAt(0);
894
+ if (codePoint == null)
895
+ return 1;
896
+ return isFullWidthCodePoint(codePoint) || isEmojiLike(codePoint) ? 2 : 1;
897
+ }
898
+
899
899
  // src/core/buffer/buffer.ts
900
900
  var DEFAULT_STYLE = Object.freeze({});
901
901
  function normalizeStyle(style) {
@@ -6770,6 +6770,30 @@ var TInput = (0, import_vue19.defineComponent)({
6770
6770
  return;
6771
6771
  }
6772
6772
  }
6773
+ const base64EncodeBytes = (bytes) => {
6774
+ const table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
6775
+ let out = "";
6776
+ for (let i = 0; i < bytes.length; i += 3) {
6777
+ const a = bytes[i] ?? 0;
6778
+ const b = bytes[i + 1] ?? 0;
6779
+ const c = bytes[i + 2] ?? 0;
6780
+ const triple = a << 16 | b << 8 | c;
6781
+ out += table[triple >> 18 & 63] ?? "";
6782
+ out += table[triple >> 12 & 63] ?? "";
6783
+ out += i + 1 < bytes.length ? table[triple >> 6 & 63] ?? "" : "=";
6784
+ out += i + 2 < bytes.length ? table[triple & 63] ?? "" : "=";
6785
+ }
6786
+ return out;
6787
+ };
6788
+ const base64EncodeText = (text) => {
6789
+ if (typeof TextEncoder !== "undefined")
6790
+ return base64EncodeBytes(new TextEncoder().encode(text));
6791
+ const bin = unescape(encodeURIComponent(text));
6792
+ const bytes = new Uint8Array(bin.length);
6793
+ for (let i = 0; i < bin.length; i++)
6794
+ bytes[i] = bin.charCodeAt(i);
6795
+ return base64EncodeBytes(bytes);
6796
+ };
6773
6797
  const copyText = async (text) => {
6774
6798
  var _a, _b, _c, _d, _e, _f;
6775
6799
  if (!text)
@@ -6785,7 +6809,7 @@ var TInput = (0, import_vue19.defineComponent)({
6785
6809
  const proc = globalThis.process;
6786
6810
  if (((_b = proc == null ? void 0 : proc.stdout) == null ? void 0 : _b.write) && ((_c = proc == null ? void 0 : proc.stdout) == null ? void 0 : _c.isTTY)) {
6787
6811
  try {
6788
- const base64 = typeof Buffer !== "undefined" ? Buffer.from(text, "utf-8").toString("base64") : btoa(unescape(encodeURIComponent(text)));
6812
+ const base64 = base64EncodeText(text);
6789
6813
  proc.stdout.write(`\x1B]52;c;${base64}\x07`);
6790
6814
  return true;
6791
6815
  } catch {
@@ -7985,13 +8009,15 @@ var TSelect = (0, import_vue23.defineComponent)({
7985
8009
  h: { type: Number, required: true },
7986
8010
  zIndex: { type: Number, default: 0 },
7987
8011
  options: { type: Array, required: true },
7988
- modelValue: { type: Number, default: 0 },
8012
+ modelValue: { type: [Number, Array], default: 0 },
8013
+ multiple: { type: Boolean, default: false },
8014
+ multipleEmit: { type: String, default: "value" },
7989
8015
  style: { type: Object, default: void 0 },
7990
8016
  highlightStyle: { type: Object, default: void 0 },
7991
8017
  autoFocus: { type: Boolean, default: false },
7992
8018
  closeOnBlur: { type: Boolean, default: false }
7993
8019
  },
7994
- emits: ["update:modelValue", "change", "close", "focus", "blur", "keydown"],
8020
+ emits: ["update:modelValue", "change", "confirm", "close", "focus", "blur", "keydown"],
7995
8021
  setup(props, { emit }) {
7996
8022
  const { terminal, scheduler, defaultStyle, events } = useTerminal();
7997
8023
  const layout = useLayout();
@@ -7999,7 +8025,17 @@ var TSelect = (0, import_vue23.defineComponent)({
7999
8025
  const parentEventZ = (0, import_vue23.inject)(EventZIndexContextKey, (0, import_vue23.computed)(() => 0));
8000
8026
  const eventZ = (0, import_vue23.computed)(() => (parentEventZ.value ?? 0) + (props.zIndex ?? 0));
8001
8027
  const focused = (0, import_vue23.ref)(false);
8002
- const active = (0, import_vue23.ref)(props.modelValue);
8028
+ const initialActive = (() => {
8029
+ const max = Math.max(0, props.options.length - 1);
8030
+ if (!props.multiple) {
8031
+ const idx = typeof props.modelValue === "number" ? props.modelValue : 0;
8032
+ return clamp6(idx, 0, max);
8033
+ }
8034
+ const selected = Array.isArray(props.modelValue) ? props.modelValue : [];
8035
+ const first = selected[0] ?? 0;
8036
+ return clamp6(first, 0, max);
8037
+ })();
8038
+ const active = (0, import_vue23.ref)(initialActive);
8003
8039
  const absRect = (0, import_vue23.computed)(() => {
8004
8040
  const raw = { x: props.x, y: props.y, w: props.w, h: props.h };
8005
8041
  const translated = translateRect(raw, layout.originX, layout.originY);
@@ -8008,7 +8044,13 @@ var TSelect = (0, import_vue23.defineComponent)({
8008
8044
  return intersectRect(translated, layout.clipRect) ?? { x: 0, y: 0, w: 0, h: 0 };
8009
8045
  });
8010
8046
  (0, import_vue23.watchEffect)(() => {
8011
- active.value = clamp6(props.modelValue, 0, Math.max(0, props.options.length - 1));
8047
+ const max = Math.max(0, props.options.length - 1);
8048
+ if (!props.multiple) {
8049
+ const idx = typeof props.modelValue === "number" ? props.modelValue : 0;
8050
+ active.value = clamp6(idx, 0, max);
8051
+ return;
8052
+ }
8053
+ active.value = clamp6(active.value, 0, max);
8012
8054
  });
8013
8055
  function getOptionLabel(opt) {
8014
8056
  return typeof opt === "string" ? opt : opt.label;
@@ -8016,30 +8058,93 @@ var TSelect = (0, import_vue23.defineComponent)({
8016
8058
  function getOptionDetail(opt) {
8017
8059
  return typeof opt === "string" ? void 0 : opt.detail;
8018
8060
  }
8019
- function commit(index) {
8061
+ function commitSingle(index) {
8020
8062
  const next = clamp6(index, 0, Math.max(0, props.options.length - 1));
8021
8063
  active.value = next;
8022
8064
  emit("update:modelValue", next);
8023
8065
  const opt = props.options[next];
8024
8066
  emit("change", opt ? getOptionLabel(opt) : null);
8025
8067
  }
8068
+ function getSelectedIndices() {
8069
+ if (!props.multiple)
8070
+ return [];
8071
+ const max = Math.max(0, props.options.length - 1);
8072
+ const raw = Array.isArray(props.modelValue) ? props.modelValue : [];
8073
+ const set = /* @__PURE__ */ new Set();
8074
+ for (const v of raw) {
8075
+ if (typeof v !== "number" || !Number.isFinite(v))
8076
+ continue;
8077
+ set.add(clamp6(Math.trunc(v), 0, max));
8078
+ }
8079
+ return [...set].sort((a, b) => a - b);
8080
+ }
8081
+ function makeMultiplePayload(indices) {
8082
+ const values = indices.map((i) => props.options[i]).filter(Boolean).map((opt) => getOptionLabel(opt));
8083
+ return { indices, values };
8084
+ }
8085
+ function emitMultiple(name, indices) {
8086
+ const payload = makeMultiplePayload(indices);
8087
+ if (props.multipleEmit === "index") {
8088
+ emit(name, payload.indices);
8089
+ return;
8090
+ }
8091
+ if (props.multipleEmit === "both") {
8092
+ emit(name, payload);
8093
+ return;
8094
+ }
8095
+ emit(name, payload.values);
8096
+ }
8097
+ function toggleMultiple(index) {
8098
+ const nextIndex = clamp6(index, 0, Math.max(0, props.options.length - 1));
8099
+ active.value = nextIndex;
8100
+ const set = new Set(getSelectedIndices());
8101
+ if (set.has(nextIndex))
8102
+ set.delete(nextIndex);
8103
+ else
8104
+ set.add(nextIndex);
8105
+ const indices = [...set].sort((a, b) => a - b);
8106
+ emit("update:modelValue", indices);
8107
+ emitMultiple("change", indices);
8108
+ }
8109
+ function confirmMultiple() {
8110
+ const indices = getSelectedIndices();
8111
+ emitMultiple("confirm", indices);
8112
+ }
8113
+ function commit(index) {
8114
+ if (props.multiple)
8115
+ toggleMultiple(index);
8116
+ else
8117
+ commitSingle(index);
8118
+ }
8026
8119
  function onKeydown(e) {
8027
8120
  emit("keydown", e);
8028
8121
  if (e.key === "ArrowUp") {
8029
8122
  e.preventDefault();
8030
8123
  active.value = clamp6(active.value - 1, 0, Math.max(0, props.options.length - 1));
8031
- emit("update:modelValue", active.value);
8124
+ if (!props.multiple)
8125
+ emit("update:modelValue", active.value);
8032
8126
  scheduler.invalidate();
8033
8127
  return;
8034
8128
  }
8035
8129
  if (e.key === "ArrowDown") {
8036
8130
  e.preventDefault();
8037
8131
  active.value = clamp6(active.value + 1, 0, Math.max(0, props.options.length - 1));
8038
- emit("update:modelValue", active.value);
8132
+ if (!props.multiple)
8133
+ emit("update:modelValue", active.value);
8039
8134
  scheduler.invalidate();
8040
8135
  return;
8041
8136
  }
8137
+ if (props.multiple && (e.code === "Space" || e.key === " " || e.key === "Spacebar")) {
8138
+ e.preventDefault();
8139
+ toggleMultiple(active.value);
8140
+ return;
8141
+ }
8042
8142
  if (e.key === "Enter") {
8143
+ if (props.multiple) {
8144
+ e.preventDefault();
8145
+ confirmMultiple();
8146
+ return;
8147
+ }
8043
8148
  commit(active.value);
8044
8149
  return;
8045
8150
  }
@@ -8093,7 +8198,7 @@ var TSelect = (0, import_vue23.defineComponent)({
8093
8198
  useRenderNode(() => ({
8094
8199
  zIndex: props.zIndex,
8095
8200
  rect: absRect.value,
8096
- deps: [visible.value, absRect.value, props.w, props.h, props.options, props.modelValue, props.style, props.highlightStyle, focused.value, active.value, defaultStyle.value],
8201
+ deps: [visible.value, absRect.value, props.w, props.h, props.options, props.modelValue, props.multiple, props.multipleEmit, props.style, props.highlightStyle, focused.value, active.value, defaultStyle.value],
8097
8202
  paint: () => {
8098
8203
  if (!visible.value)
8099
8204
  return;
@@ -8101,36 +8206,41 @@ var TSelect = (0, import_vue23.defineComponent)({
8101
8206
  if (r.w <= 0 || r.h <= 0)
8102
8207
  return;
8103
8208
  const base = props.style ?? defaultStyle.value;
8104
- const selectedBase = props.highlightStyle ?? { ...base, bg: "yellow", fg: "black" };
8209
+ const highlightBase = props.highlightStyle ?? { ...base, bg: "yellow", fg: "black" };
8105
8210
  const detailDimStyle = { ...base, dim: true };
8106
- const selectedDetailStyle = { ...selectedBase, dim: true };
8211
+ const highlightDetailStyle = { ...highlightBase, dim: true };
8212
+ const selectedSet = props.multiple ? new Set(getSelectedIndices()) : null;
8107
8213
  for (let i = 0; i < r.h; i++) {
8108
8214
  const opt = props.options[i];
8109
8215
  if (!opt) {
8110
8216
  terminal.write(" ".repeat(r.w), { x: r.x, y: r.y + i, style: base });
8111
8217
  continue;
8112
8218
  }
8113
- const isSelected = i === active.value;
8219
+ const isActiveRow = i === active.value;
8220
+ const isChecked = props.multiple ? selectedSet.has(i) : isActiveRow;
8221
+ const isHighlighted = props.multiple ? focused.value && isActiveRow : isActiveRow;
8222
+ const prefix = props.multiple ? isChecked ? "[x] " : "[ ] " : "";
8114
8223
  const label = sanitizeInlineText(getOptionLabel(opt));
8115
8224
  const detail = getOptionDetail(opt);
8116
8225
  const rawDetail = detail ? sanitizeInlineText(detail) : "";
8117
- const labelCells = textCellWidth3(label);
8226
+ const labelText = `${prefix}${label}`;
8227
+ const labelCells = textCellWidth3(labelText);
8118
8228
  const minGap = 2;
8119
8229
  const availableForDetail = r.w - labelCells - minGap;
8120
8230
  if (rawDetail && availableForDetail >= 4) {
8121
- const labelStyle = isSelected ? selectedBase : base;
8122
- terminal.write(label, { x: r.x, y: r.y + i, style: labelStyle });
8231
+ const labelStyle = isHighlighted ? highlightBase : base;
8232
+ terminal.write(labelText, { x: r.x, y: r.y + i, style: labelStyle });
8123
8233
  const detailCells = textCellWidth3(rawDetail);
8124
8234
  const actualDetailWidth = Math.min(detailCells, availableForDetail);
8125
8235
  const gapWidth = r.w - labelCells - actualDetailWidth;
8126
- const gapStyle = isSelected ? selectedBase : base;
8236
+ const gapStyle = isHighlighted ? highlightBase : base;
8127
8237
  terminal.write(" ".repeat(gapWidth), { x: r.x + labelCells, y: r.y + i, style: gapStyle });
8128
8238
  const detailText = sliceByCells(rawDetail, availableForDetail);
8129
- const dStyle = isSelected ? selectedDetailStyle : detailDimStyle;
8239
+ const dStyle = isHighlighted ? highlightDetailStyle : detailDimStyle;
8130
8240
  terminal.write(detailText, { x: r.x + labelCells + gapWidth, y: r.y + i, style: dStyle });
8131
8241
  } else {
8132
- const line = padEndByCells(sliceByCells(label, r.w), r.w);
8133
- const style = isSelected ? selectedBase : base;
8242
+ const line = padEndByCells(sliceByCells(labelText, r.w), r.w);
8243
+ const style = isHighlighted ? highlightBase : base;
8134
8244
  terminal.write(line, { x: r.x, y: r.y + i, style });
8135
8245
  }
8136
8246
  }
package/dist/index.d.ts CHANGED
@@ -77,19 +77,15 @@ type PathPickerProvider = Readonly<{
77
77
  }>;
78
78
  declare function createNodePathPickerProvider(): PathPickerProvider;
79
79
 
80
- //#endregion
81
- //#region src/cli/recording.d.ts
82
- declare function writeEventLog(path: string, events: readonly TerminalEventRecord[]): Promise<void>;
83
- declare function writeSnapshot(path: string, lines: readonly string[]): Promise<void>;
84
- declare function readEventLog(path: string): Promise<TerminalEventRecord[]>;
85
-
86
80
  //#endregion
87
81
  //#region src/cli/path-suggest.d.ts
88
82
  declare function resolveUserPath(workspaceAbs: string, input: string): string;
89
83
 
90
84
  //#endregion
91
- //#region src/core/buffer/width.d.ts
92
- declare function charCellWidth(text: string): 1 | 2;
85
+ //#region src/cli/recording.d.ts
86
+ declare function writeEventLog(path: string, events: readonly TerminalEventRecord[]): Promise<void>;
87
+ declare function writeSnapshot(path: string, lines: readonly string[]): Promise<void>;
88
+ declare function readEventLog(path: string): Promise<TerminalEventRecord[]>;
93
89
 
94
90
  //#endregion
95
91
  //#region src/core/types.d.ts
@@ -160,6 +156,10 @@ interface AnsiSegment {
160
156
  }
161
157
  declare function parseAnsiSgr(input: string, baseStyle?: Style): AnsiSegment[];
162
158
 
159
+ //#endregion
160
+ //#region src/core/buffer/width.d.ts
161
+ declare function charCellWidth(text: string): 1 | 2;
162
+
163
163
  //#endregion
164
164
  //#region src/core/terminal/create-terminal.d.ts
165
165
  declare function createTerminal(opts: {
package/dist/index.js CHANGED
@@ -417,32 +417,6 @@ function createNodePathPickerProvider() {
417
417
  };
418
418
  }
419
419
 
420
- //#endregion
421
- //#region src/cli/recording.ts
422
- async function loadFs() {
423
- return import("node:fs");
424
- }
425
- async function writeEventLog(path, events) {
426
- const { writeFileSync } = await loadFs();
427
- const lines = events.map((e) => JSON.stringify(e)).join("\n");
428
- writeFileSync(path, `${lines}${lines ? "\n" : ""}`, "utf8");
429
- }
430
- async function writeSnapshot(path, lines) {
431
- const { writeFileSync } = await loadFs();
432
- writeFileSync(path, `${lines.join("\n")}\n`, "utf8");
433
- }
434
- async function readEventLog(path) {
435
- const { readFileSync } = await loadFs();
436
- const raw = readFileSync(path, "utf8");
437
- const out = [];
438
- for (const line of raw.split(/\r?\n/g)) {
439
- const trimmed = line.trim();
440
- if (!trimmed) continue;
441
- out.push(JSON.parse(trimmed));
442
- }
443
- return out;
444
- }
445
-
446
420
  //#endregion
447
421
  //#region src/utils/path.ts
448
422
  function normalizeSeparators(input) {
@@ -617,37 +591,29 @@ function resolveUserPath(workspaceAbs, input) {
617
591
  }
618
592
 
619
593
  //#endregion
620
- //#region src/core/buffer/width.ts
621
- const fullWidthRanges = [
622
- [4352, 4447],
623
- [9001, 9002],
624
- [11904, 42191],
625
- [44032, 55203],
626
- [63744, 64255],
627
- [65040, 65049],
628
- [65072, 65135],
629
- [65280, 65376],
630
- [65504, 65510]
631
- ];
632
- function isFullWidthCodePoint(codePoint) {
633
- for (const [start, end] of fullWidthRanges) if (codePoint >= start && codePoint <= end) return true;
634
- return false;
594
+ //#region src/cli/recording.ts
595
+ async function loadFs() {
596
+ return import("node:fs");
635
597
  }
636
- function isEmojiLike(codePoint) {
637
- return codePoint >= 127744 && codePoint <= 129791 || codePoint >= 127462 && codePoint <= 127487;
598
+ async function writeEventLog(path, events) {
599
+ const { writeFileSync } = await loadFs();
600
+ const lines = events.map((e) => JSON.stringify(e)).join("\n");
601
+ writeFileSync(path, `${lines}${lines ? "\n" : ""}`, "utf8");
638
602
  }
639
- let extendedPictographicRe = null;
640
- try {
641
- extendedPictographicRe = new RegExp("\\p{Extended_Pictographic}", "u");
642
- } catch {
643
- extendedPictographicRe = null;
603
+ async function writeSnapshot(path, lines) {
604
+ const { writeFileSync } = await loadFs();
605
+ writeFileSync(path, `${lines.join("\n")}\n`, "utf8");
644
606
  }
645
- function charCellWidth(text) {
646
- if (!text) return 1;
647
- if (extendedPictographicRe === null || extendedPictographicRe === void 0 ? void 0 : extendedPictographicRe.test(text)) return 2;
648
- const codePoint = text.codePointAt(0);
649
- if (codePoint == null) return 1;
650
- return isFullWidthCodePoint(codePoint) || isEmojiLike(codePoint) ? 2 : 1;
607
+ async function readEventLog(path) {
608
+ const { readFileSync } = await loadFs();
609
+ const raw = readFileSync(path, "utf8");
610
+ const out = [];
611
+ for (const line of raw.split(/\r?\n/g)) {
612
+ const trimmed = line.trim();
613
+ if (!trimmed) continue;
614
+ out.push(JSON.parse(trimmed));
615
+ }
616
+ return out;
651
617
  }
652
618
 
653
619
  //#endregion
@@ -953,6 +919,40 @@ function parseAnsiSgr(input, baseStyle = {}) {
953
919
  return segments;
954
920
  }
955
921
 
922
+ //#endregion
923
+ //#region src/core/buffer/width.ts
924
+ const fullWidthRanges = [
925
+ [4352, 4447],
926
+ [9001, 9002],
927
+ [11904, 42191],
928
+ [44032, 55203],
929
+ [63744, 64255],
930
+ [65040, 65049],
931
+ [65072, 65135],
932
+ [65280, 65376],
933
+ [65504, 65510]
934
+ ];
935
+ function isFullWidthCodePoint(codePoint) {
936
+ for (const [start, end] of fullWidthRanges) if (codePoint >= start && codePoint <= end) return true;
937
+ return false;
938
+ }
939
+ function isEmojiLike(codePoint) {
940
+ return codePoint >= 127744 && codePoint <= 129791 || codePoint >= 127462 && codePoint <= 127487;
941
+ }
942
+ let extendedPictographicRe = null;
943
+ try {
944
+ extendedPictographicRe = new RegExp("\\p{Extended_Pictographic}", "u");
945
+ } catch {
946
+ extendedPictographicRe = null;
947
+ }
948
+ function charCellWidth(text) {
949
+ if (!text) return 1;
950
+ if (extendedPictographicRe === null || extendedPictographicRe === void 0 ? void 0 : extendedPictographicRe.test(text)) return 2;
951
+ const codePoint = text.codePointAt(0);
952
+ if (codePoint == null) return 1;
953
+ return isFullWidthCodePoint(codePoint) || isEmojiLike(codePoint) ? 2 : 1;
954
+ }
955
+
956
956
  //#endregion
957
957
  //#region src/core/buffer/buffer.ts
958
958
  const DEFAULT_STYLE = Object.freeze({});
@@ -7182,6 +7182,28 @@ const TInput = defineComponent({
7182
7182
  return;
7183
7183
  }
7184
7184
  }
7185
+ const base64EncodeBytes = (bytes) => {
7186
+ const table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7187
+ let out = "";
7188
+ for (let i = 0; i < bytes.length; i += 3) {
7189
+ const a = bytes[i] ?? 0;
7190
+ const b = bytes[i + 1] ?? 0;
7191
+ const c = bytes[i + 2] ?? 0;
7192
+ const triple = a << 16 | b << 8 | c;
7193
+ out += table[triple >> 18 & 63] ?? "";
7194
+ out += table[triple >> 12 & 63] ?? "";
7195
+ out += i + 1 < bytes.length ? table[triple >> 6 & 63] ?? "" : "=";
7196
+ out += i + 2 < bytes.length ? table[triple & 63] ?? "" : "=";
7197
+ }
7198
+ return out;
7199
+ };
7200
+ const base64EncodeText = (text) => {
7201
+ if (typeof TextEncoder !== "undefined") return base64EncodeBytes(new TextEncoder().encode(text));
7202
+ const bin = unescape(encodeURIComponent(text));
7203
+ const bytes = new Uint8Array(bin.length);
7204
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
7205
+ return base64EncodeBytes(bytes);
7206
+ };
7185
7207
  const copyText = async (text) => {
7186
7208
  var _nav$clipboard, _proc$stdout, _proc$stdout2, _doc$body;
7187
7209
  if (!text) return false;
@@ -7192,7 +7214,7 @@ const TInput = defineComponent({
7192
7214
  } catch {}
7193
7215
  const proc = globalThis.process;
7194
7216
  if ((proc === null || proc === void 0 || (_proc$stdout = proc.stdout) === null || _proc$stdout === void 0 ? void 0 : _proc$stdout.write) && (proc === null || proc === void 0 || (_proc$stdout2 = proc.stdout) === null || _proc$stdout2 === void 0 ? void 0 : _proc$stdout2.isTTY)) try {
7195
- const base64 = typeof Buffer !== "undefined" ? Buffer.from(text, "utf-8").toString("base64") : btoa(unescape(encodeURIComponent(text)));
7217
+ const base64 = base64EncodeText(text);
7196
7218
  proc.stdout.write(`\u001B]52;c;${base64}\u0007`);
7197
7219
  return true;
7198
7220
  } catch {}
@@ -8639,9 +8661,17 @@ const TSelect = defineComponent({
8639
8661
  required: true
8640
8662
  },
8641
8663
  modelValue: {
8642
- type: Number,
8664
+ type: [Number, Array],
8643
8665
  default: 0
8644
8666
  },
8667
+ multiple: {
8668
+ type: Boolean,
8669
+ default: false
8670
+ },
8671
+ multipleEmit: {
8672
+ type: String,
8673
+ default: "value"
8674
+ },
8645
8675
  style: {
8646
8676
  type: Object,
8647
8677
  default: void 0
@@ -8662,6 +8692,7 @@ const TSelect = defineComponent({
8662
8692
  emits: [
8663
8693
  "update:modelValue",
8664
8694
  "change",
8695
+ "confirm",
8665
8696
  "close",
8666
8697
  "focus",
8667
8698
  "blur",
@@ -8674,7 +8705,17 @@ const TSelect = defineComponent({
8674
8705
  const parentEventZ = inject(EventZIndexContextKey, computed(() => 0));
8675
8706
  const eventZ = computed(() => (parentEventZ.value ?? 0) + (props.zIndex ?? 0));
8676
8707
  const focused = ref(false);
8677
- const active = ref(props.modelValue);
8708
+ const initialActive = (() => {
8709
+ const max = Math.max(0, props.options.length - 1);
8710
+ if (!props.multiple) {
8711
+ const idx = typeof props.modelValue === "number" ? props.modelValue : 0;
8712
+ return clamp(idx, 0, max);
8713
+ }
8714
+ const selected = Array.isArray(props.modelValue) ? props.modelValue : [];
8715
+ const first = selected[0] ?? 0;
8716
+ return clamp(first, 0, max);
8717
+ })();
8718
+ const active = ref(initialActive);
8678
8719
  const absRect = computed(() => {
8679
8720
  const raw = {
8680
8721
  x: props.x,
@@ -8692,7 +8733,13 @@ const TSelect = defineComponent({
8692
8733
  };
8693
8734
  });
8694
8735
  watchEffect(() => {
8695
- active.value = clamp(props.modelValue, 0, Math.max(0, props.options.length - 1));
8736
+ const max = Math.max(0, props.options.length - 1);
8737
+ if (!props.multiple) {
8738
+ const idx = typeof props.modelValue === "number" ? props.modelValue : 0;
8739
+ active.value = clamp(idx, 0, max);
8740
+ return;
8741
+ }
8742
+ active.value = clamp(active.value, 0, max);
8696
8743
  });
8697
8744
  function getOptionLabel(opt) {
8698
8745
  return typeof opt === "string" ? opt : opt.label;
@@ -8700,30 +8747,88 @@ const TSelect = defineComponent({
8700
8747
  function getOptionDetail(opt) {
8701
8748
  return typeof opt === "string" ? void 0 : opt.detail;
8702
8749
  }
8703
- function commit(index) {
8750
+ function commitSingle(index) {
8704
8751
  const next = clamp(index, 0, Math.max(0, props.options.length - 1));
8705
8752
  active.value = next;
8706
8753
  emit("update:modelValue", next);
8707
8754
  const opt = props.options[next];
8708
8755
  emit("change", opt ? getOptionLabel(opt) : null);
8709
8756
  }
8757
+ function getSelectedIndices() {
8758
+ if (!props.multiple) return [];
8759
+ const max = Math.max(0, props.options.length - 1);
8760
+ const raw = Array.isArray(props.modelValue) ? props.modelValue : [];
8761
+ const set = new Set();
8762
+ for (const v of raw) {
8763
+ if (typeof v !== "number" || !Number.isFinite(v)) continue;
8764
+ set.add(clamp(Math.trunc(v), 0, max));
8765
+ }
8766
+ return [...set].sort((a, b) => a - b);
8767
+ }
8768
+ function makeMultiplePayload(indices) {
8769
+ const values = indices.map((i) => props.options[i]).filter(Boolean).map((opt) => getOptionLabel(opt));
8770
+ return {
8771
+ indices,
8772
+ values
8773
+ };
8774
+ }
8775
+ function emitMultiple(name, indices) {
8776
+ const payload = makeMultiplePayload(indices);
8777
+ if (props.multipleEmit === "index") {
8778
+ emit(name, payload.indices);
8779
+ return;
8780
+ }
8781
+ if (props.multipleEmit === "both") {
8782
+ emit(name, payload);
8783
+ return;
8784
+ }
8785
+ emit(name, payload.values);
8786
+ }
8787
+ function toggleMultiple(index) {
8788
+ const nextIndex = clamp(index, 0, Math.max(0, props.options.length - 1));
8789
+ active.value = nextIndex;
8790
+ const set = new Set(getSelectedIndices());
8791
+ if (set.has(nextIndex)) set.delete(nextIndex);
8792
+ else set.add(nextIndex);
8793
+ const indices = [...set].sort((a, b) => a - b);
8794
+ emit("update:modelValue", indices);
8795
+ emitMultiple("change", indices);
8796
+ }
8797
+ function confirmMultiple() {
8798
+ const indices = getSelectedIndices();
8799
+ emitMultiple("confirm", indices);
8800
+ }
8801
+ function commit(index) {
8802
+ if (props.multiple) toggleMultiple(index);
8803
+ else commitSingle(index);
8804
+ }
8710
8805
  function onKeydown(e) {
8711
8806
  emit("keydown", e);
8712
8807
  if (e.key === "ArrowUp") {
8713
8808
  e.preventDefault();
8714
8809
  active.value = clamp(active.value - 1, 0, Math.max(0, props.options.length - 1));
8715
- emit("update:modelValue", active.value);
8810
+ if (!props.multiple) emit("update:modelValue", active.value);
8716
8811
  scheduler.invalidate();
8717
8812
  return;
8718
8813
  }
8719
8814
  if (e.key === "ArrowDown") {
8720
8815
  e.preventDefault();
8721
8816
  active.value = clamp(active.value + 1, 0, Math.max(0, props.options.length - 1));
8722
- emit("update:modelValue", active.value);
8817
+ if (!props.multiple) emit("update:modelValue", active.value);
8723
8818
  scheduler.invalidate();
8724
8819
  return;
8725
8820
  }
8821
+ if (props.multiple && (e.code === "Space" || e.key === " " || e.key === "Spacebar")) {
8822
+ e.preventDefault();
8823
+ toggleMultiple(active.value);
8824
+ return;
8825
+ }
8726
8826
  if (e.key === "Enter") {
8827
+ if (props.multiple) {
8828
+ e.preventDefault();
8829
+ confirmMultiple();
8830
+ return;
8831
+ }
8727
8832
  commit(active.value);
8728
8833
  return;
8729
8834
  }
@@ -8777,6 +8882,8 @@ const TSelect = defineComponent({
8777
8882
  props.h,
8778
8883
  props.options,
8779
8884
  props.modelValue,
8885
+ props.multiple,
8886
+ props.multipleEmit,
8780
8887
  props.style,
8781
8888
  props.highlightStyle,
8782
8889
  focused.value,
@@ -8788,7 +8895,7 @@ const TSelect = defineComponent({
8788
8895
  const r = absRect.value;
8789
8896
  if (r.w <= 0 || r.h <= 0) return;
8790
8897
  const base = props.style ?? defaultStyle.value;
8791
- const selectedBase = props.highlightStyle ?? {
8898
+ const highlightBase = props.highlightStyle ?? {
8792
8899
  ...base,
8793
8900
  bg: "yellow",
8794
8901
  fg: "black"
@@ -8797,10 +8904,11 @@ const TSelect = defineComponent({
8797
8904
  ...base,
8798
8905
  dim: true
8799
8906
  };
8800
- const selectedDetailStyle = {
8801
- ...selectedBase,
8907
+ const highlightDetailStyle = {
8908
+ ...highlightBase,
8802
8909
  dim: true
8803
8910
  };
8911
+ const selectedSet = props.multiple ? new Set(getSelectedIndices()) : null;
8804
8912
  for (let i = 0; i < r.h; i++) {
8805
8913
  const opt = props.options[i];
8806
8914
  if (!opt) {
@@ -8811,16 +8919,20 @@ const TSelect = defineComponent({
8811
8919
  });
8812
8920
  continue;
8813
8921
  }
8814
- const isSelected = i === active.value;
8922
+ const isActiveRow = i === active.value;
8923
+ const isChecked = props.multiple ? selectedSet.has(i) : isActiveRow;
8924
+ const isHighlighted = props.multiple ? focused.value && isActiveRow : isActiveRow;
8925
+ const prefix = props.multiple ? isChecked ? "[x] " : "[ ] " : "";
8815
8926
  const label = sanitizeInlineText(getOptionLabel(opt));
8816
8927
  const detail = getOptionDetail(opt);
8817
8928
  const rawDetail = detail ? sanitizeInlineText(detail) : "";
8818
- const labelCells = textCellWidth$1(label);
8929
+ const labelText = `${prefix}${label}`;
8930
+ const labelCells = textCellWidth$1(labelText);
8819
8931
  const minGap = 2;
8820
8932
  const availableForDetail = r.w - labelCells - minGap;
8821
8933
  if (rawDetail && availableForDetail >= 4) {
8822
- const labelStyle = isSelected ? selectedBase : base;
8823
- terminal.write(label, {
8934
+ const labelStyle = isHighlighted ? highlightBase : base;
8935
+ terminal.write(labelText, {
8824
8936
  x: r.x,
8825
8937
  y: r.y + i,
8826
8938
  style: labelStyle
@@ -8828,22 +8940,22 @@ const TSelect = defineComponent({
8828
8940
  const detailCells = textCellWidth$1(rawDetail);
8829
8941
  const actualDetailWidth = Math.min(detailCells, availableForDetail);
8830
8942
  const gapWidth = r.w - labelCells - actualDetailWidth;
8831
- const gapStyle = isSelected ? selectedBase : base;
8943
+ const gapStyle = isHighlighted ? highlightBase : base;
8832
8944
  terminal.write(" ".repeat(gapWidth), {
8833
8945
  x: r.x + labelCells,
8834
8946
  y: r.y + i,
8835
8947
  style: gapStyle
8836
8948
  });
8837
8949
  const detailText = sliceByCells(rawDetail, availableForDetail);
8838
- const dStyle = isSelected ? selectedDetailStyle : detailDimStyle;
8950
+ const dStyle = isHighlighted ? highlightDetailStyle : detailDimStyle;
8839
8951
  terminal.write(detailText, {
8840
8952
  x: r.x + labelCells + gapWidth,
8841
8953
  y: r.y + i,
8842
8954
  style: dStyle
8843
8955
  });
8844
8956
  } else {
8845
- const line = padEndByCells(sliceByCells(label, r.w), r.w);
8846
- const style = isSelected ? selectedBase : base;
8957
+ const line = padEndByCells(sliceByCells(labelText, r.w), r.w);
8958
+ const style = isHighlighted ? highlightBase : base;
8847
8959
  terminal.write(line, {
8848
8960
  x: r.x,
8849
8961
  y: r.y + i,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@simon_he/vue-tui",
3
3
  "type": "module",
4
- "version": "0.0.1",
4
+ "version": "0.0.3",
5
5
  "description": "A Vue 3 terminal UI component library",
6
6
  "author": "Simon He",
7
7
  "license": "MIT",
@@ -77,6 +77,7 @@
77
77
  "build:examples": "pnpm run build:goatchain && pnpm -C examples/basic build && pnpm -C examples/goatchain build",
78
78
  "build:examples:terminal": "pnpm run build:goatchain && pnpm -C examples/basic build:terminal && pnpm -C examples/goatchain build:terminal",
79
79
  "run:basic:terminal": "pnpm run build && pnpm -C examples/basic terminal",
80
+ "run:basic:multi-select:terminal": "pnpm run build && pnpm -C examples/basic build:terminal:multi-select && pnpm -C examples/basic terminal:multi-select",
80
81
  "run:goatchain:terminal": "pnpm run build:goatchain && pnpm -C examples/goatchain terminal",
81
82
  "format": "prettier --cache --write .",
82
83
  "lint": "eslint . --cache",