xgen-dex-cli 1.6.0 → 1.7.1

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.
@@ -6,7 +6,7 @@ import {
6
6
 
7
7
  // src/tui/index.tsx
8
8
  import { render } from "ink";
9
- import { stdout } from "node:process";
9
+ import { stdin, stdout } from "node:process";
10
10
 
11
11
  // src/tui/app.tsx
12
12
  import { useCallback, useEffect as useEffect8, useState as useState11 } from "react";
@@ -189,7 +189,7 @@ function wrapToWidth(text, width) {
189
189
  }
190
190
  let line = "";
191
191
  let lineWidth = 0;
192
- const flush2 = () => {
192
+ const flush = () => {
193
193
  lines.push(line);
194
194
  line = "";
195
195
  lineWidth = 0;
@@ -197,7 +197,7 @@ function wrapToWidth(text, width) {
197
197
  for (const word of paragraph.match(/\s+|\S+/g) ?? []) {
198
198
  const wordWidth = stringWidth(word);
199
199
  if (lineWidth > 0 && lineWidth + wordWidth > width) {
200
- flush2();
200
+ flush();
201
201
  if (/^\s+$/.test(word)) continue;
202
202
  }
203
203
  if (wordWidth <= width) {
@@ -207,12 +207,12 @@ function wrapToWidth(text, width) {
207
207
  }
208
208
  for (const char of word) {
209
209
  const charWidth = stringWidth(char);
210
- if (lineWidth + charWidth > width) flush2();
210
+ if (lineWidth + charWidth > width) flush();
211
211
  line += char;
212
212
  lineWidth += charWidth;
213
213
  }
214
214
  }
215
- flush2();
215
+ flush();
216
216
  }
217
217
  return lines;
218
218
  }
@@ -288,7 +288,7 @@ import { Box as Box2, Text as Text2 } from "ink";
288
288
 
289
289
  // src/tui/ime-text-input.tsx
290
290
  import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
291
- import { Box, Text, useCursor, useInput } from "ink";
291
+ import { Box, Text, useCursor, useInput, useStdin } from "ink";
292
292
  import stringWidth2 from "string-width";
293
293
 
294
294
  // src/tui/terminal-input.ts
@@ -374,10 +374,24 @@ var SPLIT = Object.fromEntries(
374
374
  ])
375
375
  );
376
376
  var EMPTY = {};
377
- function jamoOf(key) {
378
- if (KEYS[key]) return KEYS[key];
377
+ var SHIFTED_KEYS = /* @__PURE__ */ new Set(["r", "e", "q", "t", "w", "o", "p"]);
378
+ function jamoOf(key, capsLock = false) {
379
+ const effective = capsLock ? swapCase(key) : key;
380
+ if (KEYS[effective]) return KEYS[effective];
381
+ const lower = effective.toLowerCase();
382
+ return lower === effective ? void 0 : KEYS[lower];
383
+ }
384
+ function swapCase(key) {
385
+ const lower = key.toLowerCase();
386
+ return key === lower ? key.toUpperCase() : lower;
387
+ }
388
+ function detectCapsLock(current, key) {
379
389
  const lower = key.toLowerCase();
380
- return lower === key ? void 0 : KEYS[lower];
390
+ const upper = key.toUpperCase();
391
+ if (lower === upper) return current;
392
+ if (!KEYS[lower]) return current;
393
+ if (SHIFTED_KEYS.has(lower)) return current;
394
+ return key === upper;
381
395
  }
382
396
  function isVowel(jamo) {
383
397
  return JUNG.includes(jamo) || jamo === "\u3157" || jamo === "\u315C" || jamo === "\u3161";
@@ -450,8 +464,105 @@ function back(state) {
450
464
  if (cho) return { state: EMPTY, handled: true };
451
465
  return { state: EMPTY, handled: false };
452
466
  }
453
- function flush(state) {
454
- return { commit: display(state), state: EMPTY };
467
+ var IDLE = { state: EMPTY, keys: [], capsLock: false };
468
+ function typeKey(session, key) {
469
+ const caps = detectCapsLock(session.capsLock, key);
470
+ let { state, keys } = session;
471
+ let commit = "";
472
+ if (caps !== session.capsLock && keys.length > 0) {
473
+ let rebuilt = EMPTY;
474
+ for (const previous of keys) {
475
+ const jamo2 = jamoOf(previous, caps);
476
+ if (!jamo2) continue;
477
+ const step = feed(rebuilt, jamo2);
478
+ commit += step.commit;
479
+ rebuilt = step.state;
480
+ }
481
+ state = rebuilt;
482
+ }
483
+ const jamo = jamoOf(key, caps);
484
+ if (!jamo) {
485
+ commit += display(state) + key;
486
+ return { session: { state: EMPTY, keys: [], capsLock: caps }, commit, composing: "" };
487
+ }
488
+ const result = feed(state, jamo);
489
+ commit += result.commit;
490
+ keys = result.commit ? [key] : [...keys, key];
491
+ return {
492
+ session: { state: result.state, keys, capsLock: caps },
493
+ commit,
494
+ composing: display(result.state)
495
+ };
496
+ }
497
+ function backspace(session) {
498
+ const stepped = back(session.state);
499
+ if (!stepped.handled) return { session, composing: "", handled: false };
500
+ return {
501
+ session: { ...session, state: stepped.state, keys: session.keys.slice(0, -1) },
502
+ composing: display(stepped.state),
503
+ handled: true
504
+ };
505
+ }
506
+
507
+ // src/tui/kitty.ts
508
+ var ESC = "\x1B";
509
+ var QUERY = `${ESC}[?u`;
510
+ var KEY_CODES = {
511
+ 57358: "capslock",
512
+ 57449: "rightalt"
513
+ };
514
+ var CAPS_LOCK_BIT = 64;
515
+ function parseKeyEvents(chunk) {
516
+ const events = [];
517
+ const pattern = /\u001B\[(\d+)(?::\d+)*(?:;(\d+)(?::(\d+))?)?(?:;[\d:]*)?u/g;
518
+ for (const match of chunk.matchAll(pattern)) {
519
+ const code = Number(match[1]);
520
+ const modifiers = match[2] ? Number(match[2]) - 1 : 0;
521
+ events.push({
522
+ name: KEY_CODES[code],
523
+ // eslint-disable-next-line no-bitwise
524
+ capsLock: (modifiers & CAPS_LOCK_BIT) !== 0,
525
+ eventType: match[3] ? Number(match[3]) : 1
526
+ });
527
+ }
528
+ return events;
529
+ }
530
+ function knownKittyTerminal(env = process.env) {
531
+ if (env.KITTY_WINDOW_ID) return true;
532
+ if (env.GHOSTTY_RESOURCES_DIR) return true;
533
+ const term = (env.TERM ?? "").toLowerCase();
534
+ if (term === "xterm-kitty" || term === "xterm-ghostty" || term.includes("foot")) return true;
535
+ const program = (env.TERM_PROGRAM ?? "").toLowerCase();
536
+ return program === "wezterm" || program === "ghostty" || program === "kitty";
537
+ }
538
+ async function supportsKittyKeyboard(streams, timeoutMs = 200, env = process.env) {
539
+ if (env.DEX_NO_KITTY === "1") return false;
540
+ if (!streams.stdout.isTTY || !streams.isTTY) return false;
541
+ if (knownKittyTerminal(env)) return true;
542
+ const wasRaw = streams.stdin.isRaw === true;
543
+ const wasPaused = !wasRaw;
544
+ streams.stdin.setRawMode?.(true);
545
+ streams.stdin.resume?.();
546
+ return new Promise((resolve) => {
547
+ let settled = false;
548
+ let seen = "";
549
+ const finish = (supported) => {
550
+ if (settled) return;
551
+ settled = true;
552
+ clearTimeout(timer);
553
+ streams.stdin.removeListener("data", onData);
554
+ if (!wasRaw) streams.stdin.setRawMode?.(false);
555
+ if (wasPaused) streams.stdin.pause?.();
556
+ resolve(supported);
557
+ };
558
+ const onData = (data) => {
559
+ seen += typeof data === "string" ? data : data.toString("utf8");
560
+ if (/\u001B\[\?\d*u/.test(seen)) finish(true);
561
+ };
562
+ const timer = setTimeout(() => finish(false), timeoutMs);
563
+ streams.stdin.on("data", onData);
564
+ streams.stdout.write(QUERY);
565
+ });
455
566
  }
456
567
 
457
568
  // src/tui/ime-text-input.tsx
@@ -487,6 +598,12 @@ function visibleInput(segments, cursor, maximumWidth) {
487
598
  cursorWidth: widthBeforeCursor
488
599
  };
489
600
  }
601
+ function isHangulToggle(input, key) {
602
+ if (key.ctrl && (input === "`" || input === " " || input === "l")) return true;
603
+ if (key.meta && input === " ") return true;
604
+ if (key.shift && input === " ") return true;
605
+ return false;
606
+ }
490
607
  function TerminalCursor({ x, y }) {
491
608
  const { setCursorPosition } = useCursor();
492
609
  setCursorPosition({ x, y });
@@ -499,7 +616,7 @@ function ImeTextInput(props) {
499
616
  const valueRef = useRef2(props.value);
500
617
  const cursorRef = useRef2(initialSegments.length);
501
618
  const pastingRef = useRef2(false);
502
- const composingRef = useRef2(EMPTY);
619
+ const typingRef = useRef2(IDLE);
503
620
  const moveCursor = (next, length) => {
504
621
  const resolved = clamp(next, 0, length);
505
622
  cursorRef.current = resolved;
@@ -511,8 +628,26 @@ function ImeTextInput(props) {
511
628
  moveCursor(nextCursor, segments.length);
512
629
  props.onChange(nextValue);
513
630
  };
631
+ const { stdin: stdin2, isRawModeSupported } = useStdin();
632
+ const onModeKeyRef = useRef2(void 0);
633
+ onModeKeyRef.current = () => props.onHangulModeChange?.(!props.hangulMode);
514
634
  useEffect2(() => {
515
- if (!props.focus || !props.hangulMode) composingRef.current = EMPTY;
635
+ if (!props.focus || !isRawModeSupported) return void 0;
636
+ const onData = (data) => {
637
+ const chunk = typeof data === "string" ? data : data.toString("utf8");
638
+ for (const event of parseKeyEvents(chunk)) {
639
+ typingRef.current = { ...typingRef.current, capsLock: event.capsLock };
640
+ if (event.eventType !== 1) continue;
641
+ if (event.name === "rightalt" || event.name === "capslock") onModeKeyRef.current?.();
642
+ }
643
+ };
644
+ stdin2?.on("data", onData);
645
+ return () => void stdin2?.off("data", onData);
646
+ }, [props.focus, isRawModeSupported, stdin2]);
647
+ useEffect2(() => {
648
+ if (!props.focus || !props.hangulMode) {
649
+ typingRef.current = { ...IDLE, capsLock: typingRef.current.capsLock };
650
+ }
516
651
  }, [props.focus, props.hangulMode]);
517
652
  useEffect2(() => {
518
653
  if (props.value === valueRef.current) return;
@@ -533,10 +668,9 @@ function ImeTextInput(props) {
533
668
  const current = graphemes(valueRef.current);
534
669
  const currentCursor = clamp(cursorRef.current, 0, current.length);
535
670
  const event = classifyInput(input, pastingRef.current);
536
- const composing = composingRef.current;
537
- const shown = display(composing);
671
+ const shown = display(typingRef.current.state);
538
672
  const settle = () => {
539
- composingRef.current = EMPTY;
673
+ typingRef.current = { ...IDLE, capsLock: typingRef.current.capsLock };
540
674
  };
541
675
  if (event.kind === "paste-start") {
542
676
  settle();
@@ -555,7 +689,7 @@ function ImeTextInput(props) {
555
689
  }
556
690
  return;
557
691
  }
558
- if (key.ctrl && (input === "`" || input === "l")) {
692
+ if (isHangulToggle(input, key)) {
559
693
  settle();
560
694
  props.onHangulModeChange?.(!props.hangulMode);
561
695
  return;
@@ -586,10 +720,10 @@ function ImeTextInput(props) {
586
720
  return;
587
721
  }
588
722
  if (key.backspace || key.delete) {
589
- const stepped = back(composing);
723
+ const stepped = backspace(typingRef.current);
590
724
  if (stepped.handled) {
591
- composingRef.current = stepped.state;
592
- applyComposition(current, currentCursor, shown, "", display(stepped.state));
725
+ typingRef.current = stepped.session;
726
+ applyComposition(current, currentCursor, shown, "", stepped.composing);
593
727
  return;
594
728
  }
595
729
  if (currentCursor === 0) return;
@@ -603,30 +737,19 @@ function ImeTextInput(props) {
603
737
  }
604
738
  if (event.kind !== "text") return;
605
739
  if (props.hangulMode) {
606
- let state = composing;
740
+ let session = typingRef.current;
607
741
  let previous = shown;
608
742
  let segments = current;
609
743
  let cursor2 = currentCursor;
610
744
  for (const character of event.text) {
611
- const jamo = jamoOf(character);
612
- if (!jamo) {
613
- const flushed = flush(state);
614
- state = flushed.state;
615
- applyComposition(segments, cursor2, previous, flushed.commit + character, "");
616
- segments = graphemes(valueRef.current);
617
- cursor2 = cursorRef.current;
618
- previous = "";
619
- continue;
620
- }
621
- const result = feed(state, jamo);
622
- state = result.state;
623
- const next = display(state);
624
- applyComposition(segments, cursor2, previous, result.commit, next);
745
+ const result = typeKey(session, character);
746
+ session = result.session;
747
+ applyComposition(segments, cursor2, previous, result.commit, result.composing);
625
748
  segments = graphemes(valueRef.current);
626
749
  cursor2 = cursorRef.current;
627
- previous = next;
750
+ previous = result.composing;
628
751
  }
629
- composingRef.current = state;
752
+ typingRef.current = session;
630
753
  return;
631
754
  }
632
755
  settle();
@@ -662,8 +785,16 @@ function Header(props) {
662
785
  ] }) : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\uC124\uC815 \uD544\uC694" })
663
786
  ] });
664
787
  }
665
- function Footer({ text }) {
666
- return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: text }) });
788
+ function Footer({ text, mode }) {
789
+ return /* @__PURE__ */ jsxs2(Box2, { paddingX: 1, children: [
790
+ mode ? /* @__PURE__ */ jsxs2(Text2, { bold: true, color: mode === "\uD55C" ? "yellow" : "gray", children: [
791
+ "[",
792
+ mode,
793
+ "]",
794
+ " "
795
+ ] }) : null,
796
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: text })
797
+ ] });
667
798
  }
668
799
  function Loading({ label = "\uBD88\uB7EC\uC624\uB294 \uC911..." }) {
669
800
  return /* @__PURE__ */ jsx2(Box2, { padding: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
@@ -1206,7 +1337,13 @@ function Dashboard(props) {
1206
1337
  }
1207
1338
  ),
1208
1339
  body,
1209
- /* @__PURE__ */ jsx6(Footer, { text: "Ctrl+Space \uD55C/\uC601 \xB7 Tab \uD328\uB110 \xB7 PgUp/PgDn \uC2A4\uD06C\uB864 \xB7 Ctrl+K \uBA85\uB839 \xB7 Ctrl+H \uAE30\uB85D \xB7 Ctrl+P \uD504\uB85C\uD544 \xB7 Esc \uCDE8\uC18C \xB7 Ctrl+Q \uC885\uB8CC" })
1340
+ /* @__PURE__ */ jsx6(
1341
+ Footer,
1342
+ {
1343
+ mode: hangulMode ? "\uD55C" : "EN",
1344
+ text: "Ctrl+Space \uD55C/\uC601 \xB7 Tab \uD328\uB110 \xB7 PgUp/PgDn \uC2A4\uD06C\uB864 \xB7 Ctrl+K \uBA85\uB839 \xB7 Ctrl+H \uAE30\uB85D \xB7 Ctrl+P \uD504\uB85C\uD544 \xB7 Esc \uCDE8\uC18C \xB7 Ctrl+Q \uC885\uB8CC"
1345
+ }
1346
+ )
1210
1347
  ] });
1211
1348
  }
1212
1349
 
@@ -1633,25 +1770,26 @@ function RetryInput({ onRetry }) {
1633
1770
  }
1634
1771
 
1635
1772
  // src/tui/screen.ts
1636
- var ESC = "\x1B";
1637
- var ENTER_ALT_SCREEN = `${ESC}[?1049h`;
1638
- var LEAVE_ALT_SCREEN = `${ESC}[?1049l`;
1639
- var SHOW_CURSOR = `${ESC}[?25h`;
1773
+ var ESC2 = "\x1B";
1774
+ var ENTER_ALT_SCREEN = `${ESC2}[?1049h`;
1775
+ var LEAVE_ALT_SCREEN = `${ESC2}[?1049l`;
1776
+ var SHOW_CURSOR = `${ESC2}[?25h`;
1640
1777
  var DISABLE_REPORTS = [
1641
- `${ESC}[?1004l`,
1778
+ `${ESC2}[?1004l`,
1642
1779
  // 포커스 들어옴/나감
1643
- `${ESC}[?1000l`,
1780
+ `${ESC2}[?1000l`,
1644
1781
  // 마우스 클릭
1645
- `${ESC}[?1002l`,
1782
+ `${ESC2}[?1002l`,
1646
1783
  // 마우스 드래그
1647
- `${ESC}[?1003l`,
1784
+ `${ESC2}[?1003l`,
1648
1785
  // 마우스 이동 전부
1649
- `${ESC}[?1006l`
1786
+ `${ESC2}[?1006l`
1650
1787
  // SGR 확장 좌표
1651
1788
  ].join("");
1652
- var ENABLE_BRACKETED_PASTE = `${ESC}[?2004h`;
1653
- var DISABLE_BRACKETED_PASTE = `${ESC}[?2004l`;
1654
- function createScreenGuard(stream) {
1789
+ var ENABLE_BRACKETED_PASTE = `${ESC2}[?2004h`;
1790
+ var DISABLE_BRACKETED_PASTE = `${ESC2}[?2004l`;
1791
+ var POP_KITTY_KEYBOARD = `${ESC2}[<u`;
1792
+ function createScreenGuard(stream, options = {}) {
1655
1793
  const alt = Boolean(stream.isTTY);
1656
1794
  let entered = false;
1657
1795
  let restored = false;
@@ -1664,7 +1802,11 @@ function createScreenGuard(stream) {
1664
1802
  restore() {
1665
1803
  if (restored) return;
1666
1804
  restored = true;
1667
- if (entered) stream.write(DISABLE_BRACKETED_PASTE + LEAVE_ALT_SCREEN);
1805
+ if (entered) {
1806
+ stream.write(
1807
+ (options.kittyKeyboard ? POP_KITTY_KEYBOARD : "") + DISABLE_BRACKETED_PASTE + LEAVE_ALT_SCREEN
1808
+ );
1809
+ }
1668
1810
  stream.write(SHOW_CURSOR);
1669
1811
  }
1670
1812
  };
@@ -1702,7 +1844,7 @@ async function writePreferences(preferences, env = process.env) {
1702
1844
  // src/tui/index.tsx
1703
1845
  import { jsx as jsx11 } from "react/jsx-runtime";
1704
1846
  async function runTui(engine) {
1705
- const screen = createScreenGuard(stdout);
1847
+ let screen = createScreenGuard(stdout);
1706
1848
  const restore = () => screen.restore();
1707
1849
  const onSignal = () => {
1708
1850
  restore();
@@ -1712,6 +1854,8 @@ async function runTui(engine) {
1712
1854
  process.once("SIGINT", onSignal);
1713
1855
  process.once("SIGTERM", onSignal);
1714
1856
  const preferences = await readPreferences();
1857
+ const kitty = await supportsKittyKeyboard({ stdin, stdout, isTTY: stdin.isTTY });
1858
+ screen = createScreenGuard(stdout, { kittyKeyboard: kitty });
1715
1859
  screen.enter();
1716
1860
  try {
1717
1861
  const instance = render(
@@ -1727,7 +1871,21 @@ async function runTui(engine) {
1727
1871
  ),
1728
1872
  {
1729
1873
  exitOnCtrlC: true,
1730
- patchConsole: true
1874
+ patchConsole: true,
1875
+ // 모든 키를 이스케이프로 받아야 한/영 키(오른쪽 Alt)와 Caps Lock 이 사건으로
1876
+ // 온다. 그런데 그렇게만 켜면 터미널이 **글쇠 코드만** 보내서 Shift+r 이
1877
+ // 소문자 `r` 로 도착한다 — 된소리도 영문 대문자도 사라진다. 그래서 글자까지
1878
+ // 함께 보내 달라고(reportAssociatedText) 요청한다.
1879
+ ...kitty ? {
1880
+ kittyKeyboard: {
1881
+ mode: "enabled",
1882
+ flags: [
1883
+ "disambiguateEscapeCodes",
1884
+ "reportAllKeysAsEscapeCodes",
1885
+ "reportAssociatedText"
1886
+ ]
1887
+ }
1888
+ } : {}
1731
1889
  }
1732
1890
  );
1733
1891
  await instance.waitUntilExit();
@@ -1741,4 +1899,4 @@ async function runTui(engine) {
1741
1899
  export {
1742
1900
  runTui
1743
1901
  };
1744
- //# sourceMappingURL=tui-DTMX2JZ7.js.map
1902
+ //# sourceMappingURL=tui-H3VM2MGV.js.map