@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.
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.escapeTomlString = escapeTomlString;
4
+ exports.quoteTomlString = quoteTomlString;
5
+ exports.keyLiteral = keyLiteral;
6
+ exports.parseToml = parseToml;
7
+ /** 裸 key 允许的字符(对齐 TOML bare key;其余 key 编码时加引号)。 */
8
+ const BARE_KEY_RE = /^[A-Za-z0-9_-]+$/;
9
+ /** 转义字符串内容(写入值时用)。 */
10
+ function escapeTomlString(s) {
11
+ let out = '';
12
+ for (const ch of s) {
13
+ switch (ch) {
14
+ case '\\':
15
+ out += '\\\\';
16
+ break;
17
+ case '"':
18
+ out += '\\"';
19
+ break;
20
+ case '\n':
21
+ out += '\\n';
22
+ break;
23
+ case '\t':
24
+ out += '\\t';
25
+ break;
26
+ case '\r':
27
+ out += '\\r';
28
+ break;
29
+ case '\b':
30
+ out += '\\b';
31
+ break;
32
+ case '\f':
33
+ out += '\\f';
34
+ break;
35
+ default:
36
+ out += ch;
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+ /** 带引号的字符串字面量(含转义)。 */
42
+ function quoteTomlString(s) {
43
+ return '"' + escapeTomlString(s) + '"';
44
+ }
45
+ /** 表头/键名编码:裸 key 直接输出,否则带引号。 */
46
+ function keyLiteral(key) {
47
+ return BARE_KEY_RE.test(key) ? key : quoteTomlString(key);
48
+ }
49
+ /**
50
+ * 解析 TOML 子集。无法解析的行跳过(容错:状态文件损坏不阻断主流程)。
51
+ * 无任何可解析内容时返回 null。
52
+ */
53
+ function parseToml(text) {
54
+ const doc = new Map();
55
+ let cur = null;
56
+ let sawContent = false;
57
+ for (const rawLine of text.split('\n')) {
58
+ const line = rawLine.trim();
59
+ if (line === '' || line.startsWith('#')) {
60
+ continue;
61
+ }
62
+ if (line.startsWith('[') && line.endsWith(']')) {
63
+ const name = parseQuotedOrBare(line.slice(1, -1).trim());
64
+ if (name === null) {
65
+ continue;
66
+ }
67
+ cur = new Map();
68
+ doc.set(name, cur);
69
+ sawContent = true;
70
+ continue;
71
+ }
72
+ if (cur === null) {
73
+ continue; // 表头之前的游离键:本包格式不存在,跳过
74
+ }
75
+ const eq = line.indexOf('=');
76
+ if (eq < 0) {
77
+ continue;
78
+ }
79
+ const keyRaw = line.slice(0, eq).trim();
80
+ const key = parseQuotedOrBare(keyRaw);
81
+ if (key === null) {
82
+ continue;
83
+ }
84
+ const valueRaw = line.slice(eq + 1).trim();
85
+ const value = parseTomlValue(valueRaw);
86
+ if (value === null) {
87
+ continue;
88
+ }
89
+ cur.set(key, value);
90
+ sawContent = true;
91
+ }
92
+ if (!sawContent) {
93
+ return null;
94
+ }
95
+ return doc;
96
+ }
97
+ /** 解析 `"quoted"` 或裸字符串;失败返回 null。 */
98
+ function parseQuotedOrBare(s) {
99
+ if (s.startsWith('"')) {
100
+ return parseTomlString(s);
101
+ }
102
+ return s;
103
+ }
104
+ /** 解析一个 TOML 字符串字面量(从首个引号到闭合引号,含转义)。 */
105
+ function parseTomlString(s) {
106
+ if (!s.startsWith('"')) {
107
+ return null;
108
+ }
109
+ let out = '';
110
+ let i = 1;
111
+ while (i < s.length) {
112
+ const ch = s[i];
113
+ if (ch === '"') {
114
+ return out;
115
+ }
116
+ if (ch === '\\') {
117
+ const esc = s[i + 1];
118
+ if (esc === undefined) {
119
+ return null;
120
+ }
121
+ switch (esc) {
122
+ case 'n':
123
+ out += '\n';
124
+ break;
125
+ case 't':
126
+ out += '\t';
127
+ break;
128
+ case 'r':
129
+ out += '\r';
130
+ break;
131
+ case 'b':
132
+ out += '\b';
133
+ break;
134
+ case 'f':
135
+ out += '\f';
136
+ break;
137
+ case '"':
138
+ out += '"';
139
+ break;
140
+ case '\\':
141
+ out += '\\';
142
+ break;
143
+ case 'u': {
144
+ const hex = s.slice(i + 2, i + 6);
145
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
146
+ return null;
147
+ }
148
+ out += String.fromCodePoint(parseInt(hex, 16));
149
+ i += 4;
150
+ break;
151
+ }
152
+ default:
153
+ return null; // 不识别的转义
154
+ }
155
+ i += 2;
156
+ continue;
157
+ }
158
+ // TOML 基本字符串不允许裸控制字符
159
+ if (ch < ' ' && ch !== '\t') {
160
+ return null;
161
+ }
162
+ out += ch;
163
+ i++;
164
+ }
165
+ return null; // 未闭合
166
+ }
167
+ /** 解析字符串或字符串数组字面量;失败返回 null。 */
168
+ function parseTomlValue(raw) {
169
+ if (raw.startsWith('[')) {
170
+ if (!raw.endsWith(']')) {
171
+ return null;
172
+ }
173
+ const inner = raw.slice(1, -1).trim();
174
+ if (inner === '') {
175
+ return [];
176
+ }
177
+ const values = [];
178
+ let i = 0;
179
+ while (i < inner.length) {
180
+ const ch = inner[i];
181
+ if (ch === ',' || ch === ' ' || ch === '\t') {
182
+ i++;
183
+ continue;
184
+ }
185
+ if (ch === '"') {
186
+ const start = i;
187
+ let j = i + 1;
188
+ let escaped = false;
189
+ while (j < inner.length) {
190
+ const c = inner[j];
191
+ if (c === '"' && !escaped) {
192
+ break;
193
+ }
194
+ if (c === '\\' && !escaped) {
195
+ escaped = true;
196
+ }
197
+ else {
198
+ escaped = false;
199
+ }
200
+ j++;
201
+ }
202
+ if (j >= inner.length) {
203
+ return null;
204
+ }
205
+ const literal = parseTomlString(inner.slice(start, j + 1));
206
+ if (literal === null) {
207
+ return null;
208
+ }
209
+ values.push(literal);
210
+ i = j + 1;
211
+ }
212
+ else {
213
+ return null; // 仅支持字符串数组
214
+ }
215
+ }
216
+ return values;
217
+ }
218
+ if (raw.startsWith('"')) {
219
+ return parseTomlString(raw);
220
+ }
221
+ return null; // 布尔/数字等非本包类型
222
+ }
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KeyReader = void 0;
4
+ exports.openTty = openTty;
5
+ exports.enterInteractive = enterInteractive;
6
+ exports.exitInteractive = exitInteractive;
7
+ exports.renderFrame = renderFrame;
8
+ exports.promptConfirm = promptConfirm;
9
+ /**
10
+ * tty — 终端抽象(纯 Node 标准库,无第三方依赖)。
11
+ *
12
+ * 协议约定:交互 TUI 独立于宿主 stdout——posix 上打开 /dev/tty 渲染与输入,
13
+ * 宿主 stdout 只承载选中值($(...) 安全)。无 /dev/tty 时(如 Windows)退化为
14
+ * 宿主自身的 stdio(isTTY 时才可用);两者皆不可用时返回 null,由调用方走
15
+ * 非交互退化路径(与引擎行为一致)。
16
+ *
17
+ * 按键输入:raw mode + ANSI 转义序列解析(方向键/回车/tab/ctrl 组合/UTF-8 字符)。
18
+ */
19
+ const node_fs_1 = require("node:fs");
20
+ const node_string_decoder_1 = require("node:string_decoder");
21
+ const node_tty_1 = require("node:tty");
22
+ /** 尝试打开交互终端;不可用返回 null(调用方退化为非交互)。 */
23
+ function openTty() {
24
+ // POSIX:/dev/tty 独立于宿主 stdout/stderr
25
+ try {
26
+ const fd = (0, node_fs_1.openSync)('/dev/tty', 'r+');
27
+ const read = new node_tty_1.ReadStream(fd);
28
+ const write = new node_tty_1.WriteStream(fd);
29
+ return {
30
+ read,
31
+ write,
32
+ columns: write.columns > 0 ? write.columns : 80,
33
+ rows: write.rows > 0 ? write.rows : 24,
34
+ };
35
+ }
36
+ catch {
37
+ // Windows 等无 /dev/tty:使用宿主 stdio(仅当确为交互终端)
38
+ if (process.stdin.isTTY && process.stdout.isTTY) {
39
+ return {
40
+ read: process.stdin,
41
+ write: process.stdout,
42
+ columns: process.stdout.columns > 0 ? process.stdout.columns : 80,
43
+ rows: process.stdout.rows > 0 ? process.stdout.rows : 24,
44
+ };
45
+ }
46
+ return null;
47
+ }
48
+ }
49
+ /** 进入交互模式:备用屏幕 + 隐藏光标 + raw input。 */
50
+ function enterInteractive(tty) {
51
+ tty.read.setRawMode(true);
52
+ tty.write.write('\x1b[?1049h\x1b[?25l\x1b[2J\x1b[H');
53
+ }
54
+ /** 退出交互模式:恢复原始终端状态。 */
55
+ function exitInteractive(tty) {
56
+ tty.write.write('\x1b[?25h\x1b[?1049l');
57
+ tty.read.setRawMode(false);
58
+ }
59
+ /** 全量重绘一帧画面。 */
60
+ function renderFrame(tty, frame) {
61
+ tty.write.write('\x1b[2J\x1b[H' + frame);
62
+ }
63
+ /** 单行确认询问(auto 首次确认):canonical 输入整行 y/N,超时默认拒绝。 */
64
+ function promptConfirm(tty, text, timeoutMs = 30000) {
65
+ tty.write.write(text);
66
+ return new Promise((resolve) => {
67
+ let buf = '';
68
+ const timer = setTimeout(() => {
69
+ cleanup();
70
+ tty.write.write('\r\n');
71
+ resolve(null);
72
+ }, timeoutMs);
73
+ const onData = (chunk) => {
74
+ const s = chunk.toString('utf8');
75
+ for (const ch of s) {
76
+ if (ch === '\r' || ch === '\n') {
77
+ cleanup();
78
+ tty.write.write('\r\n');
79
+ resolve(buf);
80
+ return;
81
+ }
82
+ buf += ch;
83
+ }
84
+ };
85
+ const cleanup = () => {
86
+ clearTimeout(timer);
87
+ tty.read.removeListener('data', onData);
88
+ };
89
+ tty.read.on('data', onData);
90
+ });
91
+ }
92
+ /**
93
+ * KeyReader — raw 输入流 → 按键序列解析。
94
+ *
95
+ * 支持:ASCII 字符、UTF-8 多字节字符、回车/退格/控制键、CSI 序列
96
+ * (\x1b[A 上、\x1b[B 下、\x1b[C 右、\x1b[D 左)。
97
+ */
98
+ class KeyReader {
99
+ decoder = new node_string_decoder_1.StringDecoder('utf8');
100
+ buf = '';
101
+ waiters = [];
102
+ closed = false;
103
+ constructor(readStream) {
104
+ readStream.on('data', (chunk) => {
105
+ this.buf += this.decoder.write(chunk);
106
+ this.pump();
107
+ });
108
+ readStream.on('end', () => this.pumpEnd());
109
+ readStream.on('error', () => this.pumpEnd());
110
+ }
111
+ /** 取下一个按键;流关闭返回 null。 */
112
+ next() {
113
+ if (this.buf !== '') {
114
+ const k = consumeKey(this.buf);
115
+ if (k !== null) {
116
+ this.buf = this.buf.slice(k.len);
117
+ return Promise.resolve(k.key);
118
+ }
119
+ }
120
+ if (this.closed) {
121
+ return Promise.resolve(null);
122
+ }
123
+ return new Promise((resolve) => this.waiters.push(resolve));
124
+ }
125
+ pumpEnd() {
126
+ this.closed = true;
127
+ while (this.waiters.length > 0) {
128
+ this.waiters.shift()(null);
129
+ }
130
+ }
131
+ pump() {
132
+ while (this.waiters.length > 0 && this.buf !== '') {
133
+ const k = consumeKey(this.buf);
134
+ if (k === null) {
135
+ return; // 等待更多字节
136
+ }
137
+ this.buf = this.buf.slice(k.len);
138
+ this.waiters.shift()(k.key);
139
+ }
140
+ }
141
+ }
142
+ exports.KeyReader = KeyReader;
143
+ /** 从输入缓冲开头消费一个按键;字节不足返回 null(等待更多输入)。 */
144
+ function consumeKey(buf) {
145
+ if (buf === '') {
146
+ return null;
147
+ }
148
+ const c = buf[0];
149
+ // 控制字符
150
+ switch (c) {
151
+ case '\r':
152
+ return { key: 'enter', len: 1 };
153
+ case '\t':
154
+ return { key: 'tab', len: 1 };
155
+ case '\x7f':
156
+ case '\x08':
157
+ return { key: 'backspace', len: 1 };
158
+ case '\x03':
159
+ return { key: 'ctrl+c', len: 1 };
160
+ case '\x10':
161
+ return { key: 'ctrl+p', len: 1 };
162
+ case '\x0e':
163
+ return { key: 'ctrl+n', len: 1 };
164
+ case '\x15':
165
+ return { key: 'ctrl+u', len: 1 };
166
+ case '\x17':
167
+ return { key: 'ctrl+w', len: 1 };
168
+ case ' ':
169
+ return { key: 'space', len: 1 };
170
+ }
171
+ // CSI 序列:\x1b[A / \x1b[B / \x1b[C / \x1b[D
172
+ if (c === '\x1b') {
173
+ if (buf.length === 1) {
174
+ return null; // 等待后续字节确认是否为序列开头
175
+ }
176
+ const d = buf[1];
177
+ if (d === '[') {
178
+ if (buf.length < 3) {
179
+ return null;
180
+ }
181
+ switch (buf[2]) {
182
+ case 'A':
183
+ return { key: 'up', len: 3 };
184
+ case 'B':
185
+ return { key: 'down', len: 3 };
186
+ case 'C':
187
+ case 'D':
188
+ return { key: { type: 'char', value: '' }, len: 3 }; // 左右键忽略
189
+ default:
190
+ return { key: 'esc', len: 3 };
191
+ }
192
+ }
193
+ return { key: 'esc', len: 1 };
194
+ }
195
+ // 可打印字符:单字节 ASCII 或 UTF-8 多字节
196
+ if (c.charCodeAt(0) < 0x20) {
197
+ return { key: { type: 'char', value: c }, len: 1 }; // 其余控制键忽略
198
+ }
199
+ const seqLen = utf8SeqLen(buf);
200
+ if (seqLen > buf.length) {
201
+ return null; // 多字节字符尚未收全
202
+ }
203
+ return { key: { type: 'char', value: buf.slice(0, seqLen) }, len: seqLen };
204
+ }
205
+ /** 计算 buf 起始 UTF-8 字符的字节长度(按首字节高 2 位判断)。 */
206
+ function utf8SeqLen(buf) {
207
+ const b = buf.charCodeAt(0);
208
+ if (b < 0x80) {
209
+ return 1;
210
+ }
211
+ if ((b & 0xe0) === 0xc0) {
212
+ return 2;
213
+ }
214
+ if ((b & 0xf0) === 0xe0) {
215
+ return 3;
216
+ }
217
+ if ((b & 0xf8) === 0xf0) {
218
+ return 4;
219
+ }
220
+ return 1;
221
+ }