wispy-cli 2.7.23 → 2.7.24
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/lib/cjk-text-input.mjs +163 -0
- package/lib/wispy-tui.mjs +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cjk-text-input.mjs — CJK-aware text input for Ink TUI
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for `ink-text-input` that handles Korean/CJK IME
|
|
5
|
+
* composition properly by using Node's UTF-8 encoded stdin data events
|
|
6
|
+
* instead of raw byte-by-byte useInput hooks.
|
|
7
|
+
*
|
|
8
|
+
* Key insight: When stdin is in "raw" mode and encoding is set to "utf8",
|
|
9
|
+
* Node.js emits fully-composed UTF-8 strings (including Korean/CJK characters)
|
|
10
|
+
* rather than raw bytes. The terminal emulator handles IME composition before
|
|
11
|
+
* sending the final composed character to stdin.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
15
|
+
import { Box, Text, useStdin } from "ink";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* CJKTextInput — A text input component that properly handles CJK/Korean input.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} props
|
|
21
|
+
* @param {string} props.value - Controlled value
|
|
22
|
+
* @param {function} props.onChange - Called with new value on change
|
|
23
|
+
* @param {function} props.onSubmit - Called with value on Enter
|
|
24
|
+
* @param {string} [props.placeholder] - Placeholder text when empty
|
|
25
|
+
* @param {boolean} [props.focus=true] - Whether this input is focused
|
|
26
|
+
* @param {boolean} [props.showCursor=true] - Whether to show cursor block
|
|
27
|
+
*/
|
|
28
|
+
function CJKTextInput({
|
|
29
|
+
value = "",
|
|
30
|
+
onChange,
|
|
31
|
+
onSubmit,
|
|
32
|
+
placeholder = "",
|
|
33
|
+
focus = true,
|
|
34
|
+
showCursor = true,
|
|
35
|
+
}) {
|
|
36
|
+
const { stdin, setRawMode } = useStdin();
|
|
37
|
+
const valueRef = useRef(value);
|
|
38
|
+
|
|
39
|
+
// Keep ref in sync with controlled value
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
valueRef.current = value;
|
|
42
|
+
}, [value]);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!focus || !stdin) return;
|
|
46
|
+
|
|
47
|
+
// Set UTF-8 encoding so Node decodes multi-byte sequences (Korean, CJK)
|
|
48
|
+
// before emitting the data event — this is the key fix for CJK input.
|
|
49
|
+
const prevEncoding = stdin.readableEncoding;
|
|
50
|
+
stdin.setEncoding("utf8");
|
|
51
|
+
|
|
52
|
+
const onData = (chunk) => {
|
|
53
|
+
// chunk is now a UTF-8 decoded string (not a Buffer)
|
|
54
|
+
const str = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < str.length; ) {
|
|
57
|
+
// Check for escape sequences (arrow keys, etc.)
|
|
58
|
+
if (str[i] === "\x1b") {
|
|
59
|
+
const seq = str.slice(i);
|
|
60
|
+
// Arrow keys
|
|
61
|
+
if (seq.startsWith("\x1b[D") || seq.startsWith("\x1bOD")) {
|
|
62
|
+
// Left arrow — not handled here, skip
|
|
63
|
+
i += seq.startsWith("\x1b[D") ? 3 : 3;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (seq.startsWith("\x1b[C") || seq.startsWith("\x1bOC")) {
|
|
67
|
+
// Right arrow — not handled here, skip
|
|
68
|
+
i += 3;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// Other escape sequences — skip
|
|
72
|
+
i += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const ch = str[i];
|
|
77
|
+
const code = ch.charCodeAt(0);
|
|
78
|
+
|
|
79
|
+
if (ch === "\r" || ch === "\n") {
|
|
80
|
+
onSubmit?.(valueRef.current);
|
|
81
|
+
i++;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (ch === "\x7f" || ch === "\b") {
|
|
86
|
+
// Backspace
|
|
87
|
+
const newVal = valueRef.current.slice(0, -1);
|
|
88
|
+
valueRef.current = newVal;
|
|
89
|
+
onChange?.(newVal);
|
|
90
|
+
i++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (ch === "\x03") {
|
|
95
|
+
// Ctrl+C — let Ink handle it
|
|
96
|
+
i++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (ch === "\t") {
|
|
101
|
+
// Tab — do NOT consume, let Ink's useInput handle tab for view switching
|
|
102
|
+
i++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (ch === "\x15") {
|
|
107
|
+
// Ctrl+U — clear line
|
|
108
|
+
valueRef.current = "";
|
|
109
|
+
onChange?.("");
|
|
110
|
+
i++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (ch === "\x01") {
|
|
115
|
+
// Ctrl+A — move to beginning (just clear for simplicity)
|
|
116
|
+
i++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Normal printable character (ASCII or multi-byte CJK/Korean)
|
|
121
|
+
// code >= 32 for ASCII printable; any non-control Unicode char is fine
|
|
122
|
+
if (code >= 32 || code > 127) {
|
|
123
|
+
const newVal = valueRef.current + ch;
|
|
124
|
+
valueRef.current = newVal;
|
|
125
|
+
onChange?.(newVal);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
stdin.on("data", onData);
|
|
133
|
+
|
|
134
|
+
return () => {
|
|
135
|
+
stdin.removeListener("data", onData);
|
|
136
|
+
// Restore previous encoding
|
|
137
|
+
if (prevEncoding !== null) {
|
|
138
|
+
stdin.setEncoding(prevEncoding);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}, [focus, stdin, onChange, onSubmit]);
|
|
142
|
+
|
|
143
|
+
// Render
|
|
144
|
+
const displayValue = value;
|
|
145
|
+
const showPlaceholder = !displayValue && placeholder;
|
|
146
|
+
|
|
147
|
+
if (showPlaceholder) {
|
|
148
|
+
return React.createElement(Text, { dimColor: true }, placeholder);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (showCursor && focus) {
|
|
152
|
+
return React.createElement(
|
|
153
|
+
Box,
|
|
154
|
+
{},
|
|
155
|
+
React.createElement(Text, {}, displayValue),
|
|
156
|
+
React.createElement(Text, { inverse: true }, " "),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return React.createElement(Text, {}, displayValue);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default CJKTextInput;
|
package/lib/wispy-tui.mjs
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
13
13
|
import { render, Box, Text, useApp, useInput, useStdout } from "ink";
|
|
14
14
|
import Spinner from "ink-spinner";
|
|
15
|
-
import
|
|
15
|
+
import CJKTextInput from "./cjk-text-input.mjs";
|
|
16
|
+
// CJKTextInput is a drop-in replacement for ink-text-input with Korean/CJK support
|
|
17
|
+
const TextInput = CJKTextInput;
|
|
16
18
|
|
|
17
19
|
import { COMMANDS, filterCommands } from "./command-registry.mjs";
|
|
18
20
|
|
package/package.json
CHANGED