@havocrao/picktui 0.1.0 → 0.1.1
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/README.md +17 -14
- package/dist/cjs/cand.js +52 -0
- package/dist/cjs/config.js +19 -0
- package/dist/cjs/confirm.js +93 -16
- package/dist/cjs/filter.js +294 -31
- package/dist/cjs/history.js +105 -11
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/model.js +343 -0
- package/dist/cjs/toml.js +222 -0
- package/dist/cjs/tty.js +221 -0
- package/dist/cjs/tui.js +464 -46
- package/dist/esm/cand.js +47 -0
- package/dist/esm/config.js +16 -0
- package/dist/esm/confirm.js +87 -16
- package/dist/esm/filter.js +283 -31
- package/dist/esm/history.js +100 -11
- package/dist/esm/index.js +2 -1
- package/dist/esm/model.js +332 -0
- package/dist/esm/toml.js +216 -0
- package/dist/esm/tty.js +212 -0
- package/dist/esm/tui.js +461 -46
- package/dist/types/cand.d.ts +17 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/confirm.d.ts +13 -4
- package/dist/types/filter.d.ts +41 -9
- package/dist/types/history.d.ts +13 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/model.d.ts +48 -0
- package/dist/types/toml.d.ts +24 -0
- package/dist/types/tty.d.ts +42 -0
- package/dist/types/tui.d.ts +21 -9
- package/dist/types/types.d.ts +0 -7
- package/package.json +4 -4
package/dist/esm/tty.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tty — 终端抽象(纯 Node 标准库,无第三方依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 协议约定:交互 TUI 独立于宿主 stdout——posix 上打开 /dev/tty 渲染与输入,
|
|
5
|
+
* 宿主 stdout 只承载选中值($(...) 安全)。无 /dev/tty 时(如 Windows)退化为
|
|
6
|
+
* 宿主自身的 stdio(isTTY 时才可用);两者皆不可用时返回 null,由调用方走
|
|
7
|
+
* 非交互退化路径(与引擎行为一致)。
|
|
8
|
+
*
|
|
9
|
+
* 按键输入:raw mode + ANSI 转义序列解析(方向键/回车/tab/ctrl 组合/UTF-8 字符)。
|
|
10
|
+
*/
|
|
11
|
+
import { openSync } from 'node:fs';
|
|
12
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
13
|
+
import { ReadStream, WriteStream } from 'node:tty';
|
|
14
|
+
/** 尝试打开交互终端;不可用返回 null(调用方退化为非交互)。 */
|
|
15
|
+
export function openTty() {
|
|
16
|
+
// POSIX:/dev/tty 独立于宿主 stdout/stderr
|
|
17
|
+
try {
|
|
18
|
+
const fd = openSync('/dev/tty', 'r+');
|
|
19
|
+
const read = new ReadStream(fd);
|
|
20
|
+
const write = new WriteStream(fd);
|
|
21
|
+
return {
|
|
22
|
+
read,
|
|
23
|
+
write,
|
|
24
|
+
columns: write.columns > 0 ? write.columns : 80,
|
|
25
|
+
rows: write.rows > 0 ? write.rows : 24,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Windows 等无 /dev/tty:使用宿主 stdio(仅当确为交互终端)
|
|
30
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
31
|
+
return {
|
|
32
|
+
read: process.stdin,
|
|
33
|
+
write: process.stdout,
|
|
34
|
+
columns: process.stdout.columns > 0 ? process.stdout.columns : 80,
|
|
35
|
+
rows: process.stdout.rows > 0 ? process.stdout.rows : 24,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** 进入交互模式:备用屏幕 + 隐藏光标 + raw input。 */
|
|
42
|
+
export function enterInteractive(tty) {
|
|
43
|
+
tty.read.setRawMode(true);
|
|
44
|
+
tty.write.write('\x1b[?1049h\x1b[?25l\x1b[2J\x1b[H');
|
|
45
|
+
}
|
|
46
|
+
/** 退出交互模式:恢复原始终端状态。 */
|
|
47
|
+
export function exitInteractive(tty) {
|
|
48
|
+
tty.write.write('\x1b[?25h\x1b[?1049l');
|
|
49
|
+
tty.read.setRawMode(false);
|
|
50
|
+
}
|
|
51
|
+
/** 全量重绘一帧画面。 */
|
|
52
|
+
export function renderFrame(tty, frame) {
|
|
53
|
+
tty.write.write('\x1b[2J\x1b[H' + frame);
|
|
54
|
+
}
|
|
55
|
+
/** 单行确认询问(auto 首次确认):canonical 输入整行 y/N,超时默认拒绝。 */
|
|
56
|
+
export function promptConfirm(tty, text, timeoutMs = 30000) {
|
|
57
|
+
tty.write.write(text);
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
let buf = '';
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
cleanup();
|
|
62
|
+
tty.write.write('\r\n');
|
|
63
|
+
resolve(null);
|
|
64
|
+
}, timeoutMs);
|
|
65
|
+
const onData = (chunk) => {
|
|
66
|
+
const s = chunk.toString('utf8');
|
|
67
|
+
for (const ch of s) {
|
|
68
|
+
if (ch === '\r' || ch === '\n') {
|
|
69
|
+
cleanup();
|
|
70
|
+
tty.write.write('\r\n');
|
|
71
|
+
resolve(buf);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
buf += ch;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const cleanup = () => {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
tty.read.removeListener('data', onData);
|
|
80
|
+
};
|
|
81
|
+
tty.read.on('data', onData);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* KeyReader — raw 输入流 → 按键序列解析。
|
|
86
|
+
*
|
|
87
|
+
* 支持:ASCII 字符、UTF-8 多字节字符、回车/退格/控制键、CSI 序列
|
|
88
|
+
* (\x1b[A 上、\x1b[B 下、\x1b[C 右、\x1b[D 左)。
|
|
89
|
+
*/
|
|
90
|
+
export class KeyReader {
|
|
91
|
+
decoder = new StringDecoder('utf8');
|
|
92
|
+
buf = '';
|
|
93
|
+
waiters = [];
|
|
94
|
+
closed = false;
|
|
95
|
+
constructor(readStream) {
|
|
96
|
+
readStream.on('data', (chunk) => {
|
|
97
|
+
this.buf += this.decoder.write(chunk);
|
|
98
|
+
this.pump();
|
|
99
|
+
});
|
|
100
|
+
readStream.on('end', () => this.pumpEnd());
|
|
101
|
+
readStream.on('error', () => this.pumpEnd());
|
|
102
|
+
}
|
|
103
|
+
/** 取下一个按键;流关闭返回 null。 */
|
|
104
|
+
next() {
|
|
105
|
+
if (this.buf !== '') {
|
|
106
|
+
const k = consumeKey(this.buf);
|
|
107
|
+
if (k !== null) {
|
|
108
|
+
this.buf = this.buf.slice(k.len);
|
|
109
|
+
return Promise.resolve(k.key);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (this.closed) {
|
|
113
|
+
return Promise.resolve(null);
|
|
114
|
+
}
|
|
115
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
116
|
+
}
|
|
117
|
+
pumpEnd() {
|
|
118
|
+
this.closed = true;
|
|
119
|
+
while (this.waiters.length > 0) {
|
|
120
|
+
this.waiters.shift()(null);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
pump() {
|
|
124
|
+
while (this.waiters.length > 0 && this.buf !== '') {
|
|
125
|
+
const k = consumeKey(this.buf);
|
|
126
|
+
if (k === null) {
|
|
127
|
+
return; // 等待更多字节
|
|
128
|
+
}
|
|
129
|
+
this.buf = this.buf.slice(k.len);
|
|
130
|
+
this.waiters.shift()(k.key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** 从输入缓冲开头消费一个按键;字节不足返回 null(等待更多输入)。 */
|
|
135
|
+
function consumeKey(buf) {
|
|
136
|
+
if (buf === '') {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const c = buf[0];
|
|
140
|
+
// 控制字符
|
|
141
|
+
switch (c) {
|
|
142
|
+
case '\r':
|
|
143
|
+
return { key: 'enter', len: 1 };
|
|
144
|
+
case '\t':
|
|
145
|
+
return { key: 'tab', len: 1 };
|
|
146
|
+
case '\x7f':
|
|
147
|
+
case '\x08':
|
|
148
|
+
return { key: 'backspace', len: 1 };
|
|
149
|
+
case '\x03':
|
|
150
|
+
return { key: 'ctrl+c', len: 1 };
|
|
151
|
+
case '\x10':
|
|
152
|
+
return { key: 'ctrl+p', len: 1 };
|
|
153
|
+
case '\x0e':
|
|
154
|
+
return { key: 'ctrl+n', len: 1 };
|
|
155
|
+
case '\x15':
|
|
156
|
+
return { key: 'ctrl+u', len: 1 };
|
|
157
|
+
case '\x17':
|
|
158
|
+
return { key: 'ctrl+w', len: 1 };
|
|
159
|
+
case ' ':
|
|
160
|
+
return { key: 'space', len: 1 };
|
|
161
|
+
}
|
|
162
|
+
// CSI 序列:\x1b[A / \x1b[B / \x1b[C / \x1b[D
|
|
163
|
+
if (c === '\x1b') {
|
|
164
|
+
if (buf.length === 1) {
|
|
165
|
+
return null; // 等待后续字节确认是否为序列开头
|
|
166
|
+
}
|
|
167
|
+
const d = buf[1];
|
|
168
|
+
if (d === '[') {
|
|
169
|
+
if (buf.length < 3) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
switch (buf[2]) {
|
|
173
|
+
case 'A':
|
|
174
|
+
return { key: 'up', len: 3 };
|
|
175
|
+
case 'B':
|
|
176
|
+
return { key: 'down', len: 3 };
|
|
177
|
+
case 'C':
|
|
178
|
+
case 'D':
|
|
179
|
+
return { key: { type: 'char', value: '' }, len: 3 }; // 左右键忽略
|
|
180
|
+
default:
|
|
181
|
+
return { key: 'esc', len: 3 };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { key: 'esc', len: 1 };
|
|
185
|
+
}
|
|
186
|
+
// 可打印字符:单字节 ASCII 或 UTF-8 多字节
|
|
187
|
+
if (c.charCodeAt(0) < 0x20) {
|
|
188
|
+
return { key: { type: 'char', value: c }, len: 1 }; // 其余控制键忽略
|
|
189
|
+
}
|
|
190
|
+
const seqLen = utf8SeqLen(buf);
|
|
191
|
+
if (seqLen > buf.length) {
|
|
192
|
+
return null; // 多字节字符尚未收全
|
|
193
|
+
}
|
|
194
|
+
return { key: { type: 'char', value: buf.slice(0, seqLen) }, len: seqLen };
|
|
195
|
+
}
|
|
196
|
+
/** 计算 buf 起始 UTF-8 字符的字节长度(按首字节高 2 位判断)。 */
|
|
197
|
+
function utf8SeqLen(buf) {
|
|
198
|
+
const b = buf.charCodeAt(0);
|
|
199
|
+
if (b < 0x80) {
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
if ((b & 0xe0) === 0xc0) {
|
|
203
|
+
return 2;
|
|
204
|
+
}
|
|
205
|
+
if ((b & 0xf0) === 0xe0) {
|
|
206
|
+
return 3;
|
|
207
|
+
}
|
|
208
|
+
if ((b & 0xf8) === 0xf0) {
|
|
209
|
+
return 4;
|
|
210
|
+
}
|
|
211
|
+
return 1;
|
|
212
|
+
}
|