xgen-dex-cli 1.6.0 → 1.7.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.
@@ -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,96 @@ 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
+ async function supportsKittyKeyboard(streams, timeoutMs = 200) {
531
+ if (process.env.DEX_NO_KITTY === "1") return false;
532
+ if (!streams.stdout.isTTY || !streams.isTTY) return false;
533
+ const wasRaw = streams.stdin.isRaw === true;
534
+ const wasPaused = !wasRaw;
535
+ streams.stdin.setRawMode?.(true);
536
+ streams.stdin.resume?.();
537
+ return new Promise((resolve) => {
538
+ let settled = false;
539
+ let seen = "";
540
+ const finish = (supported) => {
541
+ if (settled) return;
542
+ settled = true;
543
+ clearTimeout(timer);
544
+ streams.stdin.removeListener("data", onData);
545
+ if (!wasRaw) streams.stdin.setRawMode?.(false);
546
+ if (wasPaused) streams.stdin.pause?.();
547
+ resolve(supported);
548
+ };
549
+ const onData = (data) => {
550
+ seen += typeof data === "string" ? data : data.toString("utf8");
551
+ if (/\u001B\[\?\d*u/.test(seen)) finish(true);
552
+ };
553
+ const timer = setTimeout(() => finish(false), timeoutMs);
554
+ streams.stdin.on("data", onData);
555
+ streams.stdout.write(QUERY);
556
+ });
455
557
  }
456
558
 
457
559
  // src/tui/ime-text-input.tsx
@@ -499,7 +601,7 @@ function ImeTextInput(props) {
499
601
  const valueRef = useRef2(props.value);
500
602
  const cursorRef = useRef2(initialSegments.length);
501
603
  const pastingRef = useRef2(false);
502
- const composingRef = useRef2(EMPTY);
604
+ const typingRef = useRef2(IDLE);
503
605
  const moveCursor = (next, length) => {
504
606
  const resolved = clamp(next, 0, length);
505
607
  cursorRef.current = resolved;
@@ -511,8 +613,26 @@ function ImeTextInput(props) {
511
613
  moveCursor(nextCursor, segments.length);
512
614
  props.onChange(nextValue);
513
615
  };
616
+ const { stdin: stdin2, isRawModeSupported } = useStdin();
617
+ const onModeKeyRef = useRef2(void 0);
618
+ onModeKeyRef.current = () => props.onHangulModeChange?.(!props.hangulMode);
514
619
  useEffect2(() => {
515
- if (!props.focus || !props.hangulMode) composingRef.current = EMPTY;
620
+ if (!props.focus || !isRawModeSupported) return void 0;
621
+ const onData = (data) => {
622
+ const chunk = typeof data === "string" ? data : data.toString("utf8");
623
+ for (const event of parseKeyEvents(chunk)) {
624
+ typingRef.current = { ...typingRef.current, capsLock: event.capsLock };
625
+ if (event.eventType !== 1) continue;
626
+ if (event.name === "rightalt" || event.name === "capslock") onModeKeyRef.current?.();
627
+ }
628
+ };
629
+ stdin2?.on("data", onData);
630
+ return () => void stdin2?.off("data", onData);
631
+ }, [props.focus, isRawModeSupported, stdin2]);
632
+ useEffect2(() => {
633
+ if (!props.focus || !props.hangulMode) {
634
+ typingRef.current = { ...IDLE, capsLock: typingRef.current.capsLock };
635
+ }
516
636
  }, [props.focus, props.hangulMode]);
517
637
  useEffect2(() => {
518
638
  if (props.value === valueRef.current) return;
@@ -533,10 +653,9 @@ function ImeTextInput(props) {
533
653
  const current = graphemes(valueRef.current);
534
654
  const currentCursor = clamp(cursorRef.current, 0, current.length);
535
655
  const event = classifyInput(input, pastingRef.current);
536
- const composing = composingRef.current;
537
- const shown = display(composing);
656
+ const shown = display(typingRef.current.state);
538
657
  const settle = () => {
539
- composingRef.current = EMPTY;
658
+ typingRef.current = { ...IDLE, capsLock: typingRef.current.capsLock };
540
659
  };
541
660
  if (event.kind === "paste-start") {
542
661
  settle();
@@ -555,7 +674,7 @@ function ImeTextInput(props) {
555
674
  }
556
675
  return;
557
676
  }
558
- if (key.ctrl && (input === "`" || input === "l")) {
677
+ if (key.ctrl && (input === "`" || input === "l") || key.meta && input === " ") {
559
678
  settle();
560
679
  props.onHangulModeChange?.(!props.hangulMode);
561
680
  return;
@@ -586,10 +705,10 @@ function ImeTextInput(props) {
586
705
  return;
587
706
  }
588
707
  if (key.backspace || key.delete) {
589
- const stepped = back(composing);
708
+ const stepped = backspace(typingRef.current);
590
709
  if (stepped.handled) {
591
- composingRef.current = stepped.state;
592
- applyComposition(current, currentCursor, shown, "", display(stepped.state));
710
+ typingRef.current = stepped.session;
711
+ applyComposition(current, currentCursor, shown, "", stepped.composing);
593
712
  return;
594
713
  }
595
714
  if (currentCursor === 0) return;
@@ -603,30 +722,19 @@ function ImeTextInput(props) {
603
722
  }
604
723
  if (event.kind !== "text") return;
605
724
  if (props.hangulMode) {
606
- let state = composing;
725
+ let session = typingRef.current;
607
726
  let previous = shown;
608
727
  let segments = current;
609
728
  let cursor2 = currentCursor;
610
729
  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);
730
+ const result = typeKey(session, character);
731
+ session = result.session;
732
+ applyComposition(segments, cursor2, previous, result.commit, result.composing);
625
733
  segments = graphemes(valueRef.current);
626
734
  cursor2 = cursorRef.current;
627
- previous = next;
735
+ previous = result.composing;
628
736
  }
629
- composingRef.current = state;
737
+ typingRef.current = session;
630
738
  return;
631
739
  }
632
740
  settle();
@@ -662,8 +770,16 @@ function Header(props) {
662
770
  ] }) : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\uC124\uC815 \uD544\uC694" })
663
771
  ] });
664
772
  }
665
- function Footer({ text }) {
666
- return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: text }) });
773
+ function Footer({ text, mode }) {
774
+ return /* @__PURE__ */ jsxs2(Box2, { paddingX: 1, children: [
775
+ mode ? /* @__PURE__ */ jsxs2(Text2, { bold: true, color: mode === "\uD55C" ? "yellow" : "gray", children: [
776
+ "[",
777
+ mode,
778
+ "]",
779
+ " "
780
+ ] }) : null,
781
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: text })
782
+ ] });
667
783
  }
668
784
  function Loading({ label = "\uBD88\uB7EC\uC624\uB294 \uC911..." }) {
669
785
  return /* @__PURE__ */ jsx2(Box2, { padding: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
@@ -1206,7 +1322,13 @@ function Dashboard(props) {
1206
1322
  }
1207
1323
  ),
1208
1324
  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" })
1325
+ /* @__PURE__ */ jsx6(
1326
+ Footer,
1327
+ {
1328
+ mode: hangulMode ? "\uD55C" : "EN",
1329
+ 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"
1330
+ }
1331
+ )
1210
1332
  ] });
1211
1333
  }
1212
1334
 
@@ -1633,25 +1755,26 @@ function RetryInput({ onRetry }) {
1633
1755
  }
1634
1756
 
1635
1757
  // 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`;
1758
+ var ESC2 = "\x1B";
1759
+ var ENTER_ALT_SCREEN = `${ESC2}[?1049h`;
1760
+ var LEAVE_ALT_SCREEN = `${ESC2}[?1049l`;
1761
+ var SHOW_CURSOR = `${ESC2}[?25h`;
1640
1762
  var DISABLE_REPORTS = [
1641
- `${ESC}[?1004l`,
1763
+ `${ESC2}[?1004l`,
1642
1764
  // 포커스 들어옴/나감
1643
- `${ESC}[?1000l`,
1765
+ `${ESC2}[?1000l`,
1644
1766
  // 마우스 클릭
1645
- `${ESC}[?1002l`,
1767
+ `${ESC2}[?1002l`,
1646
1768
  // 마우스 드래그
1647
- `${ESC}[?1003l`,
1769
+ `${ESC2}[?1003l`,
1648
1770
  // 마우스 이동 전부
1649
- `${ESC}[?1006l`
1771
+ `${ESC2}[?1006l`
1650
1772
  // SGR 확장 좌표
1651
1773
  ].join("");
1652
- var ENABLE_BRACKETED_PASTE = `${ESC}[?2004h`;
1653
- var DISABLE_BRACKETED_PASTE = `${ESC}[?2004l`;
1654
- function createScreenGuard(stream) {
1774
+ var ENABLE_BRACKETED_PASTE = `${ESC2}[?2004h`;
1775
+ var DISABLE_BRACKETED_PASTE = `${ESC2}[?2004l`;
1776
+ var POP_KITTY_KEYBOARD = `${ESC2}[<u`;
1777
+ function createScreenGuard(stream, options = {}) {
1655
1778
  const alt = Boolean(stream.isTTY);
1656
1779
  let entered = false;
1657
1780
  let restored = false;
@@ -1664,7 +1787,11 @@ function createScreenGuard(stream) {
1664
1787
  restore() {
1665
1788
  if (restored) return;
1666
1789
  restored = true;
1667
- if (entered) stream.write(DISABLE_BRACKETED_PASTE + LEAVE_ALT_SCREEN);
1790
+ if (entered) {
1791
+ stream.write(
1792
+ (options.kittyKeyboard ? POP_KITTY_KEYBOARD : "") + DISABLE_BRACKETED_PASTE + LEAVE_ALT_SCREEN
1793
+ );
1794
+ }
1668
1795
  stream.write(SHOW_CURSOR);
1669
1796
  }
1670
1797
  };
@@ -1702,7 +1829,7 @@ async function writePreferences(preferences, env = process.env) {
1702
1829
  // src/tui/index.tsx
1703
1830
  import { jsx as jsx11 } from "react/jsx-runtime";
1704
1831
  async function runTui(engine) {
1705
- const screen = createScreenGuard(stdout);
1832
+ let screen = createScreenGuard(stdout);
1706
1833
  const restore = () => screen.restore();
1707
1834
  const onSignal = () => {
1708
1835
  restore();
@@ -1712,6 +1839,8 @@ async function runTui(engine) {
1712
1839
  process.once("SIGINT", onSignal);
1713
1840
  process.once("SIGTERM", onSignal);
1714
1841
  const preferences = await readPreferences();
1842
+ const kitty = await supportsKittyKeyboard({ stdin, stdout, isTTY: stdin.isTTY });
1843
+ screen = createScreenGuard(stdout, { kittyKeyboard: kitty });
1715
1844
  screen.enter();
1716
1845
  try {
1717
1846
  const instance = render(
@@ -1727,7 +1856,14 @@ async function runTui(engine) {
1727
1856
  ),
1728
1857
  {
1729
1858
  exitOnCtrlC: true,
1730
- patchConsole: true
1859
+ patchConsole: true,
1860
+ // 모든 키를 이스케이프로 받아야 수식 키 자체가 사건으로 온다.
1861
+ ...kitty ? {
1862
+ kittyKeyboard: {
1863
+ mode: "enabled",
1864
+ flags: ["disambiguateEscapeCodes", "reportAllKeysAsEscapeCodes"]
1865
+ }
1866
+ } : {}
1731
1867
  }
1732
1868
  );
1733
1869
  await instance.waitUntilExit();
@@ -1741,4 +1877,4 @@ async function runTui(engine) {
1741
1877
  export {
1742
1878
  runTui
1743
1879
  };
1744
- //# sourceMappingURL=tui-DTMX2JZ7.js.map
1880
+ //# sourceMappingURL=tui-QF5OJEFW.js.map