wispy-cli 2.7.24 → 2.7.26
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 +69 -118
- package/package.json +1 -1
package/lib/cjk-text-input.mjs
CHANGED
|
@@ -1,30 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* cjk-text-input.mjs — CJK-aware text input for Ink TUI
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
4
|
+
* Uses Ink's native useInput hook (not raw stdin) so it works alongside
|
|
5
|
+
* other Ink components. Korean/CJK works because useInput already receives
|
|
6
|
+
* decoded UTF-8 characters — the real fix is handling them correctly.
|
|
12
7
|
*/
|
|
13
8
|
|
|
14
|
-
import React, { useState, useEffect, useRef
|
|
15
|
-
import { Box, Text,
|
|
9
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
10
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
16
11
|
|
|
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
12
|
function CJKTextInput({
|
|
29
13
|
value = "",
|
|
30
14
|
onChange,
|
|
@@ -33,118 +17,85 @@ function CJKTextInput({
|
|
|
33
17
|
focus = true,
|
|
34
18
|
showCursor = true,
|
|
35
19
|
}) {
|
|
36
|
-
const {
|
|
37
|
-
const valueRef = useRef(value);
|
|
38
|
-
|
|
39
|
-
// Keep ref in sync with controlled value
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
valueRef.current = value;
|
|
42
|
-
}, [value]);
|
|
20
|
+
const { stdout } = useStdout();
|
|
43
21
|
|
|
22
|
+
// Show real terminal cursor when focused so IME composition popup
|
|
23
|
+
// appears at the correct position (not bottom-right corner)
|
|
44
24
|
useEffect(() => {
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
}
|
|
25
|
+
if (focus && stdout) {
|
|
26
|
+
stdout.write("\x1b[?25h"); // show cursor
|
|
27
|
+
return () => {
|
|
28
|
+
stdout.write("\x1b[?25l"); // hide cursor on unfocus
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}, [focus, stdout]);
|
|
32
|
+
|
|
33
|
+
useInput(
|
|
34
|
+
(input, key) => {
|
|
35
|
+
// Skip keys handled by parent (Tab, Ctrl+C, arrows for view switching)
|
|
36
|
+
if (
|
|
37
|
+
key.tab ||
|
|
38
|
+
(key.shift && key.tab) ||
|
|
39
|
+
key.upArrow ||
|
|
40
|
+
key.downArrow ||
|
|
41
|
+
(key.ctrl && input === "c")
|
|
42
|
+
) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
105
45
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
i++;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
46
|
+
if (key.return) {
|
|
47
|
+
onSubmit?.(value);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
113
50
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
51
|
+
if (key.backspace || key.delete) {
|
|
52
|
+
// For CJK: slice off one character (which may be multi-byte)
|
|
53
|
+
const newVal = [...value].slice(0, -1).join("");
|
|
54
|
+
onChange?.(newVal);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
119
57
|
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
valueRef.current = newVal;
|
|
125
|
-
onChange?.(newVal);
|
|
126
|
-
}
|
|
58
|
+
if (key.leftArrow || key.rightArrow) {
|
|
59
|
+
// Cursor movement — skip for now (append-only input)
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
127
62
|
|
|
128
|
-
|
|
63
|
+
// Ctrl+U — clear line
|
|
64
|
+
if (key.ctrl && input === "u") {
|
|
65
|
+
onChange?.("");
|
|
66
|
+
return;
|
|
129
67
|
}
|
|
130
|
-
};
|
|
131
68
|
|
|
132
|
-
|
|
69
|
+
// Ctrl+W — delete last word
|
|
70
|
+
if (key.ctrl && input === "w") {
|
|
71
|
+
const trimmed = value.trimEnd();
|
|
72
|
+
const lastSpace = trimmed.lastIndexOf(" ");
|
|
73
|
+
onChange?.(lastSpace >= 0 ? trimmed.slice(0, lastSpace + 1) : "");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
133
76
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
stdin.setEncoding(prevEncoding);
|
|
77
|
+
// Normal character input — this includes CJK characters
|
|
78
|
+
// Ink's useInput already decodes UTF-8, so 'input' is a proper string
|
|
79
|
+
if (input && input.length > 0) {
|
|
80
|
+
onChange?.(value + input);
|
|
139
81
|
}
|
|
140
|
-
}
|
|
141
|
-
|
|
82
|
+
},
|
|
83
|
+
{ isActive: focus }
|
|
84
|
+
);
|
|
142
85
|
|
|
143
86
|
// Render
|
|
144
87
|
const displayValue = value;
|
|
145
88
|
const showPlaceholder = !displayValue && placeholder;
|
|
146
89
|
|
|
147
90
|
if (showPlaceholder) {
|
|
91
|
+
if (showCursor && focus) {
|
|
92
|
+
return React.createElement(
|
|
93
|
+
Box,
|
|
94
|
+
{},
|
|
95
|
+
React.createElement(Text, { inverse: true }, placeholder[0] || " "),
|
|
96
|
+
React.createElement(Text, { dimColor: true }, placeholder.slice(1))
|
|
97
|
+
);
|
|
98
|
+
}
|
|
148
99
|
return React.createElement(Text, { dimColor: true }, placeholder);
|
|
149
100
|
}
|
|
150
101
|
|
|
@@ -153,7 +104,7 @@ function CJKTextInput({
|
|
|
153
104
|
Box,
|
|
154
105
|
{},
|
|
155
106
|
React.createElement(Text, {}, displayValue),
|
|
156
|
-
React.createElement(Text, { inverse: true }, " ")
|
|
107
|
+
React.createElement(Text, { inverse: true }, " ")
|
|
157
108
|
);
|
|
158
109
|
}
|
|
159
110
|
|
package/package.json
CHANGED