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
|
|
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
|
-
|
|
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)
|
|
210
|
+
if (lineWidth + charWidth > width) flush();
|
|
211
211
|
line += char;
|
|
212
212
|
lineWidth += charWidth;
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
454
|
-
|
|
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
|
|
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 || !
|
|
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
|
|
537
|
-
const shown = display(composing);
|
|
656
|
+
const shown = display(typingRef.current.state);
|
|
538
657
|
const settle = () => {
|
|
539
|
-
|
|
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 =
|
|
708
|
+
const stepped = backspace(typingRef.current);
|
|
590
709
|
if (stepped.handled) {
|
|
591
|
-
|
|
592
|
-
applyComposition(current, currentCursor, shown, "",
|
|
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
|
|
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
|
|
612
|
-
|
|
613
|
-
|
|
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 =
|
|
735
|
+
previous = result.composing;
|
|
628
736
|
}
|
|
629
|
-
|
|
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__ */
|
|
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(
|
|
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
|
|
1637
|
-
var ENTER_ALT_SCREEN = `${
|
|
1638
|
-
var LEAVE_ALT_SCREEN = `${
|
|
1639
|
-
var SHOW_CURSOR = `${
|
|
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
|
-
`${
|
|
1763
|
+
`${ESC2}[?1004l`,
|
|
1642
1764
|
// 포커스 들어옴/나감
|
|
1643
|
-
`${
|
|
1765
|
+
`${ESC2}[?1000l`,
|
|
1644
1766
|
// 마우스 클릭
|
|
1645
|
-
`${
|
|
1767
|
+
`${ESC2}[?1002l`,
|
|
1646
1768
|
// 마우스 드래그
|
|
1647
|
-
`${
|
|
1769
|
+
`${ESC2}[?1003l`,
|
|
1648
1770
|
// 마우스 이동 전부
|
|
1649
|
-
`${
|
|
1771
|
+
`${ESC2}[?1006l`
|
|
1650
1772
|
// SGR 확장 좌표
|
|
1651
1773
|
].join("");
|
|
1652
|
-
var ENABLE_BRACKETED_PASTE = `${
|
|
1653
|
-
var DISABLE_BRACKETED_PASTE = `${
|
|
1654
|
-
|
|
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)
|
|
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
|
-
|
|
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-
|
|
1880
|
+
//# sourceMappingURL=tui-QF5OJEFW.js.map
|