xgen-dex-cli 1.5.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.
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  DexError,
3
+ dataDirectory,
3
4
  publicError
4
5
  } from "./chunk-ULMEXJOR.js";
5
6
 
6
7
  // src/tui/index.tsx
7
8
  import { render } from "ink";
8
- import { stdout } from "node:process";
9
+ import { stdin, stdout } from "node:process";
9
10
 
10
11
  // src/tui/app.tsx
11
12
  import { useCallback, useEffect as useEffect8, useState as useState11 } from "react";
@@ -287,7 +288,7 @@ import { Box as Box2, Text as Text2 } from "ink";
287
288
 
288
289
  // src/tui/ime-text-input.tsx
289
290
  import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
290
- import { Box, Text, useCursor, useInput } from "ink";
291
+ import { Box, Text, useCursor, useInput, useStdin } from "ink";
291
292
  import stringWidth2 from "string-width";
292
293
 
293
294
  // src/tui/terminal-input.ts
@@ -304,6 +305,257 @@ function classifyInput(input, pasting = false) {
304
305
  return text ? { kind: "text", text } : { kind: "ignore" };
305
306
  }
306
307
 
308
+ // src/tui/hangul.ts
309
+ var CHO = "\u3131\u3132\u3134\u3137\u3138\u3139\u3141\u3142\u3143\u3145\u3146\u3147\u3148\u3149\u314A\u314B\u314C\u314D\u314E";
310
+ var JUNG = "\u314F\u3150\u3151\u3152\u3153\u3154\u3155\u3156\u3157\u3158\u3159\u315A\u315B\u315C\u315D\u315E\u315F\u3160\u3161\u3162\u3163";
311
+ var JONG = " \u3131\u3132\u3133\u3134\u3135\u3136\u3137\u3139\u313A\u313B\u313C\u313D\u313E\u313F\u3140\u3141\u3142\u3144\u3145\u3146\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
312
+ var SYLLABLE_BASE = 44032;
313
+ var KEYS = {
314
+ r: "\u3131",
315
+ R: "\u3132",
316
+ s: "\u3134",
317
+ e: "\u3137",
318
+ E: "\u3138",
319
+ f: "\u3139",
320
+ a: "\u3141",
321
+ q: "\u3142",
322
+ Q: "\u3143",
323
+ t: "\u3145",
324
+ T: "\u3146",
325
+ d: "\u3147",
326
+ w: "\u3148",
327
+ W: "\u3149",
328
+ c: "\u314A",
329
+ z: "\u314B",
330
+ x: "\u314C",
331
+ v: "\u314D",
332
+ g: "\u314E",
333
+ k: "\u314F",
334
+ o: "\u3150",
335
+ i: "\u3151",
336
+ O: "\u3152",
337
+ j: "\u3153",
338
+ p: "\u3154",
339
+ u: "\u3155",
340
+ P: "\u3156",
341
+ h: "\u3157",
342
+ y: "\u315B",
343
+ n: "\u315C",
344
+ b: "\u3160",
345
+ m: "\u3161",
346
+ l: "\u3163"
347
+ };
348
+ var VOWEL_PAIRS = {
349
+ "\u3157\u314F": "\u3158",
350
+ "\u3157\u3150": "\u3159",
351
+ "\u3157\u3163": "\u315A",
352
+ "\u315C\u3153": "\u315D",
353
+ "\u315C\u3154": "\u315E",
354
+ "\u315C\u3163": "\u315F",
355
+ "\u3161\u3163": "\u3162"
356
+ };
357
+ var FINAL_PAIRS = {
358
+ "\u3131\u3145": "\u3133",
359
+ "\u3134\u3148": "\u3135",
360
+ "\u3134\u314E": "\u3136",
361
+ "\u3139\u3131": "\u313A",
362
+ "\u3139\u3141": "\u313B",
363
+ "\u3139\u3142": "\u313C",
364
+ "\u3139\u3145": "\u313D",
365
+ "\u3139\u314C": "\u313E",
366
+ "\u3139\u314D": "\u313F",
367
+ "\u3139\u314E": "\u3140",
368
+ "\u3142\u3145": "\u3144"
369
+ };
370
+ var SPLIT = Object.fromEntries(
371
+ [...Object.entries(VOWEL_PAIRS), ...Object.entries(FINAL_PAIRS)].map(([pair, joined]) => [
372
+ joined,
373
+ [pair[0], pair[1]]
374
+ ])
375
+ );
376
+ var EMPTY = {};
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) {
389
+ const lower = key.toLowerCase();
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;
395
+ }
396
+ function isVowel(jamo) {
397
+ return JUNG.includes(jamo) || jamo === "\u3157" || jamo === "\u315C" || jamo === "\u3161";
398
+ }
399
+ function canBeFinal(jamo) {
400
+ return JONG.indexOf(jamo) > 0;
401
+ }
402
+ function display(state) {
403
+ const { cho, jung, jong } = state;
404
+ if (cho && jung) {
405
+ const l = CHO.indexOf(cho);
406
+ const v = JUNG.indexOf(jung);
407
+ const t = jong ? JONG.indexOf(jong) : 0;
408
+ if (l >= 0 && v >= 0 && t >= 0) {
409
+ return String.fromCharCode(SYLLABLE_BASE + (l * 21 + v) * 28 + t);
410
+ }
411
+ }
412
+ return cho ?? jung ?? "";
413
+ }
414
+ function feed(state, jamo) {
415
+ const { cho, jung, jong } = state;
416
+ if (isVowel(jamo)) {
417
+ if (cho && jung && jong) {
418
+ const parts = SPLIT[jong];
419
+ const moved = parts ? parts[1] : jong;
420
+ const kept = parts ? parts[0] : void 0;
421
+ return {
422
+ commit: display({ cho, jung, jong: kept }),
423
+ state: { cho: moved, jung: jamo }
424
+ };
425
+ }
426
+ if (cho && jung) {
427
+ const merged = VOWEL_PAIRS[jung + jamo];
428
+ if (merged) return { commit: "", state: { cho, jung: merged } };
429
+ return { commit: display(state), state: { jung: jamo } };
430
+ }
431
+ if (cho) return { commit: "", state: { cho, jung: jamo } };
432
+ if (jung) {
433
+ const merged = VOWEL_PAIRS[jung + jamo];
434
+ if (merged) return { commit: "", state: { jung: merged } };
435
+ return { commit: jung, state: { jung: jamo } };
436
+ }
437
+ return { commit: "", state: { jung: jamo } };
438
+ }
439
+ if (cho && jung) {
440
+ if (!jong) {
441
+ if (canBeFinal(jamo)) return { commit: "", state: { cho, jung, jong: jamo } };
442
+ return { commit: display(state), state: { cho: jamo } };
443
+ }
444
+ const merged = FINAL_PAIRS[jong + jamo];
445
+ if (merged) return { commit: "", state: { cho, jung, jong: merged } };
446
+ return { commit: display(state), state: { cho: jamo } };
447
+ }
448
+ if (cho) {
449
+ return { commit: cho, state: { cho: jamo } };
450
+ }
451
+ if (jung) return { commit: jung, state: { cho: jamo } };
452
+ return { commit: "", state: { cho: jamo } };
453
+ }
454
+ function back(state) {
455
+ const { cho, jung, jong } = state;
456
+ if (jong) {
457
+ const parts = SPLIT[jong];
458
+ return { state: { cho, jung, jong: parts ? parts[0] : void 0 }, handled: true };
459
+ }
460
+ if (jung) {
461
+ const parts = SPLIT[jung];
462
+ return { state: { cho, jung: parts ? parts[0] : void 0 }, handled: true };
463
+ }
464
+ if (cho) return { state: EMPTY, handled: true };
465
+ return { state: EMPTY, handled: false };
466
+ }
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
+ });
557
+ }
558
+
307
559
  // src/tui/ime-text-input.tsx
308
560
  import { jsx, jsxs } from "react/jsx-runtime";
309
561
  var segmenter = new Intl.Segmenter("ko", { granularity: "grapheme" });
@@ -349,6 +601,7 @@ function ImeTextInput(props) {
349
601
  const valueRef = useRef2(props.value);
350
602
  const cursorRef = useRef2(initialSegments.length);
351
603
  const pastingRef = useRef2(false);
604
+ const typingRef = useRef2(IDLE);
352
605
  const moveCursor = (next, length) => {
353
606
  const resolved = clamp(next, 0, length);
354
607
  cursorRef.current = resolved;
@@ -360,6 +613,27 @@ function ImeTextInput(props) {
360
613
  moveCursor(nextCursor, segments.length);
361
614
  props.onChange(nextValue);
362
615
  };
616
+ const { stdin: stdin2, isRawModeSupported } = useStdin();
617
+ const onModeKeyRef = useRef2(void 0);
618
+ onModeKeyRef.current = () => props.onHangulModeChange?.(!props.hangulMode);
619
+ useEffect2(() => {
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
+ }
636
+ }, [props.focus, props.hangulMode]);
363
637
  useEffect2(() => {
364
638
  if (props.value === valueRef.current) return;
365
639
  const previousLength = graphemes(valueRef.current).length;
@@ -368,12 +642,23 @@ function ImeTextInput(props) {
368
642
  valueRef.current = props.value;
369
643
  moveCursor(wasAtEnd ? nextLength : cursorRef.current, nextLength);
370
644
  }, [props.value]);
645
+ const applyComposition = (segments, cursor2, previous, commit, next) => {
646
+ const removed = previous ? 1 : 0;
647
+ const inserted = graphemes(commit + next);
648
+ segments.splice(cursor2 - removed, removed, ...inserted);
649
+ updateValue(segments, cursor2 - removed + inserted.length);
650
+ };
371
651
  useInput(
372
652
  (input, key) => {
373
653
  const current = graphemes(valueRef.current);
374
654
  const currentCursor = clamp(cursorRef.current, 0, current.length);
375
655
  const event = classifyInput(input, pastingRef.current);
656
+ const shown = display(typingRef.current.state);
657
+ const settle = () => {
658
+ typingRef.current = { ...IDLE, capsLock: typingRef.current.capsLock };
659
+ };
376
660
  if (event.kind === "paste-start") {
661
+ settle();
377
662
  pastingRef.current = true;
378
663
  return;
379
664
  }
@@ -389,36 +674,70 @@ function ImeTextInput(props) {
389
674
  }
390
675
  return;
391
676
  }
677
+ if (key.ctrl && (input === "`" || input === "l") || key.meta && input === " ") {
678
+ settle();
679
+ props.onHangulModeChange?.(!props.hangulMode);
680
+ return;
681
+ }
392
682
  if (key.return) {
683
+ settle();
393
684
  props.onSubmit?.(valueRef.current);
394
685
  return;
395
686
  }
396
687
  if (key.leftArrow) {
688
+ settle();
397
689
  moveCursor(currentCursor - 1, current.length);
398
690
  return;
399
691
  }
400
692
  if (key.rightArrow) {
693
+ settle();
401
694
  moveCursor(currentCursor + 1, current.length);
402
695
  return;
403
696
  }
404
697
  if (key.home) {
698
+ settle();
405
699
  moveCursor(0, current.length);
406
700
  return;
407
701
  }
408
702
  if (key.end) {
703
+ settle();
409
704
  moveCursor(current.length, current.length);
410
705
  return;
411
706
  }
412
707
  if (key.backspace || key.delete) {
708
+ const stepped = backspace(typingRef.current);
709
+ if (stepped.handled) {
710
+ typingRef.current = stepped.session;
711
+ applyComposition(current, currentCursor, shown, "", stepped.composing);
712
+ return;
713
+ }
413
714
  if (currentCursor === 0) return;
414
715
  current.splice(currentCursor - 1, 1);
415
716
  updateValue(current, currentCursor - 1);
416
717
  return;
417
718
  }
418
719
  if (key.ctrl || key.meta || key.tab || key.escape || key.upArrow || key.downArrow || key.pageUp || key.pageDown) {
720
+ settle();
419
721
  return;
420
722
  }
421
723
  if (event.kind !== "text") return;
724
+ if (props.hangulMode) {
725
+ let session = typingRef.current;
726
+ let previous = shown;
727
+ let segments = current;
728
+ let cursor2 = currentCursor;
729
+ for (const character of event.text) {
730
+ const result = typeKey(session, character);
731
+ session = result.session;
732
+ applyComposition(segments, cursor2, previous, result.commit, result.composing);
733
+ segments = graphemes(valueRef.current);
734
+ cursor2 = cursorRef.current;
735
+ previous = result.composing;
736
+ }
737
+ typingRef.current = session;
738
+ return;
739
+ }
740
+ settle();
422
741
  const inserted = graphemes(event.text);
423
742
  current.splice(currentCursor, 0, ...inserted);
424
743
  updateValue(current, currentCursor + inserted.length);
@@ -451,8 +770,16 @@ function Header(props) {
451
770
  ] }) : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\uC124\uC815 \uD544\uC694" })
452
771
  ] });
453
772
  }
454
- function Footer({ text }) {
455
- 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
+ ] });
456
783
  }
457
784
  function Loading({ label = "\uBD88\uB7EC\uC624\uB294 \uC911..." }) {
458
785
  return /* @__PURE__ */ jsx2(Box2, { padding: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
@@ -736,7 +1063,8 @@ function ChatPane(props) {
736
1063
  }
737
1064
  function Composer(props) {
738
1065
  return /* @__PURE__ */ jsxs6(Box6, { borderStyle: "round", borderColor: props.focused ? "cyan" : "gray", paddingX: 1, children: [
739
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "\u203A " }),
1066
+ /* @__PURE__ */ jsx6(Text6, { color: props.hangulMode ? "yellow" : void 0, dimColor: !props.hangulMode, children: props.hangulMode ? "\uD55C" : "EN" }),
1067
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: " \u203A " }),
740
1068
  props.disabled ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\uC751\uB2F5\uC744 \uAE30\uB2E4\uB9AC\uB294 \uC911..." }) : /* @__PURE__ */ jsx6(
741
1069
  ImeTextInput,
742
1070
  {
@@ -744,7 +1072,9 @@ function Composer(props) {
744
1072
  onChange: props.onChange,
745
1073
  onSubmit: props.onSubmit,
746
1074
  focus: props.focused,
747
- placeholder: "\uBA54\uC2DC\uC9C0\uB97C \uC785\uB825\uD558\uC138\uC694"
1075
+ placeholder: "\uBA54\uC2DC\uC9C0\uB97C \uC785\uB825\uD558\uC138\uC694",
1076
+ hangulMode: props.hangulMode,
1077
+ onHangulModeChange: props.onHangulModeChange
748
1078
  }
749
1079
  )
750
1080
  ] });
@@ -765,6 +1095,11 @@ function Dashboard(props) {
765
1095
  const [history, setHistory] = useState7(false);
766
1096
  const [start, setStart] = useState7();
767
1097
  const [scrollUp, setScrollUp] = useState7(0);
1098
+ const [hangulMode, setHangulMode] = useState7(props.preferences?.hangulMode ?? false);
1099
+ const changeHangulMode = (enabled) => {
1100
+ setHangulMode(enabled);
1101
+ props.preferences?.onHangulModeChange?.(enabled);
1102
+ };
768
1103
  const viewport = useRef3({ lineCount: 0, height: 0 });
769
1104
  const [starting, setStarting] = useState7(false);
770
1105
  const controller = useRef3(null);
@@ -966,7 +1301,9 @@ function Dashboard(props) {
966
1301
  onChange: setInput,
967
1302
  onSubmit: (value) => void send(value),
968
1303
  focused: focus === "composer",
969
- disabled: chat.running || !selected
1304
+ disabled: chat.running || !selected,
1305
+ hangulMode,
1306
+ onHangulModeChange: changeHangulMode
970
1307
  }
971
1308
  )
972
1309
  ] });
@@ -985,7 +1322,13 @@ function Dashboard(props) {
985
1322
  }
986
1323
  ),
987
1324
  body,
988
- /* @__PURE__ */ jsx6(Footer, { text: "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
+ )
989
1332
  ] });
990
1333
  }
991
1334
 
@@ -1193,7 +1536,10 @@ function ServerScreen(props) {
1193
1536
 
1194
1537
  // src/tui/app.tsx
1195
1538
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1196
- function App({ engine }) {
1539
+ function App({
1540
+ engine,
1541
+ preferences
1542
+ }) {
1197
1543
  const { exit } = useApp2();
1198
1544
  const [route, setRoute] = useState11("boot");
1199
1545
  const [session, setSession] = useState11();
@@ -1385,6 +1731,7 @@ function App({ engine }) {
1385
1731
  return /* @__PURE__ */ jsx10(
1386
1732
  Dashboard,
1387
1733
  {
1734
+ preferences,
1388
1735
  engine,
1389
1736
  session,
1390
1737
  onProfiles: () => void openProfiles(),
@@ -1408,25 +1755,26 @@ function RetryInput({ onRetry }) {
1408
1755
  }
1409
1756
 
1410
1757
  // src/tui/screen.ts
1411
- var ESC = "\x1B";
1412
- var ENTER_ALT_SCREEN = `${ESC}[?1049h`;
1413
- var LEAVE_ALT_SCREEN = `${ESC}[?1049l`;
1414
- 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`;
1415
1762
  var DISABLE_REPORTS = [
1416
- `${ESC}[?1004l`,
1763
+ `${ESC2}[?1004l`,
1417
1764
  // 포커스 들어옴/나감
1418
- `${ESC}[?1000l`,
1765
+ `${ESC2}[?1000l`,
1419
1766
  // 마우스 클릭
1420
- `${ESC}[?1002l`,
1767
+ `${ESC2}[?1002l`,
1421
1768
  // 마우스 드래그
1422
- `${ESC}[?1003l`,
1769
+ `${ESC2}[?1003l`,
1423
1770
  // 마우스 이동 전부
1424
- `${ESC}[?1006l`
1771
+ `${ESC2}[?1006l`
1425
1772
  // SGR 확장 좌표
1426
1773
  ].join("");
1427
- var ENABLE_BRACKETED_PASTE = `${ESC}[?2004h`;
1428
- var DISABLE_BRACKETED_PASTE = `${ESC}[?2004l`;
1429
- 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 = {}) {
1430
1778
  const alt = Boolean(stream.isTTY);
1431
1779
  let entered = false;
1432
1780
  let restored = false;
@@ -1439,16 +1787,49 @@ function createScreenGuard(stream) {
1439
1787
  restore() {
1440
1788
  if (restored) return;
1441
1789
  restored = true;
1442
- 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
+ }
1443
1795
  stream.write(SHOW_CURSOR);
1444
1796
  }
1445
1797
  };
1446
1798
  }
1447
1799
 
1800
+ // src/tui/preferences.ts
1801
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
1802
+ import { dirname, join } from "node:path";
1803
+ function preferencesPath(env = process.env) {
1804
+ return join(dataDirectory(env), "tui.json");
1805
+ }
1806
+ function localeDefaultHangul(env = process.env) {
1807
+ const locale = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
1808
+ return /^ko(_|-|\.|$)/i.test(locale.trim());
1809
+ }
1810
+ async function readPreferences(env = process.env) {
1811
+ const fallback = { hangulMode: localeDefaultHangul(env) };
1812
+ try {
1813
+ const raw = JSON.parse(await readFile(preferencesPath(env), "utf8"));
1814
+ return typeof raw.hangulMode === "boolean" ? { hangulMode: raw.hangulMode } : fallback;
1815
+ } catch {
1816
+ return fallback;
1817
+ }
1818
+ }
1819
+ async function writePreferences(preferences, env = process.env) {
1820
+ try {
1821
+ const path = preferencesPath(env);
1822
+ await mkdir(dirname(path), { recursive: true });
1823
+ await writeFile(path, `${JSON.stringify(preferences, null, 2)}
1824
+ `, "utf8");
1825
+ } catch {
1826
+ }
1827
+ }
1828
+
1448
1829
  // src/tui/index.tsx
1449
1830
  import { jsx as jsx11 } from "react/jsx-runtime";
1450
1831
  async function runTui(engine) {
1451
- const screen = createScreenGuard(stdout);
1832
+ let screen = createScreenGuard(stdout);
1452
1833
  const restore = () => screen.restore();
1453
1834
  const onSignal = () => {
1454
1835
  restore();
@@ -1457,12 +1838,34 @@ async function runTui(engine) {
1457
1838
  process.once("exit", restore);
1458
1839
  process.once("SIGINT", onSignal);
1459
1840
  process.once("SIGTERM", onSignal);
1841
+ const preferences = await readPreferences();
1842
+ const kitty = await supportsKittyKeyboard({ stdin, stdout, isTTY: stdin.isTTY });
1843
+ screen = createScreenGuard(stdout, { kittyKeyboard: kitty });
1460
1844
  screen.enter();
1461
1845
  try {
1462
- const instance = render(/* @__PURE__ */ jsx11(App, { engine }), {
1463
- exitOnCtrlC: true,
1464
- patchConsole: true
1465
- });
1846
+ const instance = render(
1847
+ /* @__PURE__ */ jsx11(
1848
+ App,
1849
+ {
1850
+ engine,
1851
+ preferences: {
1852
+ hangulMode: preferences.hangulMode,
1853
+ onHangulModeChange: (enabled) => void writePreferences({ hangulMode: enabled })
1854
+ }
1855
+ }
1856
+ ),
1857
+ {
1858
+ exitOnCtrlC: true,
1859
+ patchConsole: true,
1860
+ // 모든 키를 이스케이프로 받아야 수식 키 자체가 사건으로 온다.
1861
+ ...kitty ? {
1862
+ kittyKeyboard: {
1863
+ mode: "enabled",
1864
+ flags: ["disambiguateEscapeCodes", "reportAllKeysAsEscapeCodes"]
1865
+ }
1866
+ } : {}
1867
+ }
1868
+ );
1466
1869
  await instance.waitUntilExit();
1467
1870
  } finally {
1468
1871
  restore();
@@ -1474,4 +1877,4 @@ async function runTui(engine) {
1474
1877
  export {
1475
1878
  runTui
1476
1879
  };
1477
- //# sourceMappingURL=tui-KJAW7DSU.js.map
1880
+ //# sourceMappingURL=tui-QF5OJEFW.js.map