@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/dist/esm/tui.js CHANGED
@@ -1,70 +1,485 @@
1
1
  /**
2
- * tui — 交互选择 / 菜单(引擎跑 TUI,宿主零渲染)。
2
+ * tui — pick / menu / rawPick(纯 TS 实现,零引擎依赖)。
3
3
  *
4
- * 交互渲染与按键输入走引擎的 /dev/tty;本模块的 stdout 仅收选中值——
5
- * `$(picktui pick ...)` 同样的安全语义。无 TTY 时引擎自动退化
6
- * (无 query 取首个、有 query 过滤取首),绑定无需分支。
4
+ * 交互渲染与按键输入走 /dev/tty(posix)或宿主 stdio(Windows fallback),
5
+ * 宿主 stdout 只承载选中值——与引擎协议相同的安全语义。无 TTY 时自动退化
6
+ * (无 query 取首个、有 query 过滤取首),调用方无需分支;取消返回 null。
7
+ *
8
+ * 流程对齐 Go 引擎 pickWith/menuWith:--auto 确认链、-1 自动选中、
9
+ * 非交互开关($PICKTUI_PICK=off)、选择记忆(--label)、候选来源
10
+ * (位置参数/stdin/--from/--map)。
7
11
  */
8
- import { assertSuccess, invokeEngine, serializeCandidates } from './engine.js';
12
+ import { exec } from 'node:child_process';
13
+ import { existsSync } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { extname, join } from 'node:path';
16
+ import { toStructuredCands } from './cand.js';
17
+ import { confirm, isConfirmed } from './confirm.js';
18
+ import { autoResolve, filterCands } from './filter.js';
19
+ import { lastPick, loadPickHistory, savePick } from './history.js';
20
+ import { newModel, selectedValues, setColorEnabled, update, view, } from './model.js';
21
+ import { KeyReader, enterInteractive, exitInteractive, openTty, promptConfirm, renderFrame, } from './tty.js';
22
+ import { PicktuiError } from './types.js';
23
+ /** 命令名前缀(对齐引擎默认名,宿主品牌对齐不在本包范围)。 */
24
+ const NAME = 'picktui';
25
+ /** 非交互开关环境变量(值为 "off" 时 pick 跳过 TUI)。 */
26
+ export const OFF_ENV = 'PICKTUI_PICK';
27
+ /** shell 候选来源/转换器超时(对齐引擎 10s)。 */
28
+ const CMD_TIMEOUT_MS = 10_000;
29
+ /** 光标闪烁周期(对齐引擎 500ms)。 */
30
+ const TICK_MS = 500;
9
31
  /**
10
32
  * pick — 交互过滤选择(fzf 风格)。
11
33
  *
12
- * @param cands 候选(经 stdin 传入,与位置参数等价且无 argv 长度限制)
13
- * @param flags 透传引擎 flags(query/label/sep/fuzzy/select1/auto
14
- * @returns 选中值;用户取消(esc/ctrl+c,退出码 130)返回 null
34
+ * @param cands 候选(不传时从宿主 stdin 管道读取)
35
+ * @param flags query/label/sep/fuzzy/select1/auto
36
+ * @returns 选中值;用户取消(esc/ctrl+c)返回 null
15
37
  */
16
38
  export async function pick(cands = [], flags = {}) {
17
- const args = ['pick'];
18
- if (flags.query !== undefined) {
19
- args.push('-q', flags.query);
39
+ const structured = cands.length > 0 ? toStructuredCands(cands) : [];
40
+ return pickFlow({
41
+ cands: structured,
42
+ fromCmd: '',
43
+ mapper: '',
44
+ flags,
45
+ });
46
+ }
47
+ /**
48
+ * menu — 多值缩写 TUI 菜单(数字 1-9 直选)。
49
+ * @returns 选中值;取消返回 null;参数不足抛 PicktuiError(退出码 2)
50
+ */
51
+ export async function menu(label, cands) {
52
+ if (cands.length < 1) {
53
+ const msg = `usage: ${NAME} _menu <label> <cand...>`;
54
+ throw new PicktuiError(msg, 2, msg);
55
+ }
56
+ const tty = openTty();
57
+ if (tty === null) {
58
+ // 非交互:取首个候选(对齐引擎退化:保证脚本不卡)
59
+ return cands[0];
60
+ }
61
+ try {
62
+ const res = await runTui(newModel({
63
+ title: `${NAME} ${label}`,
64
+ subtitle: '多值缩写',
65
+ cands: cands.map((v) => ({ value: v, desc: '' })),
66
+ query: '',
67
+ filterOpts: { mode: 'substring' },
68
+ initial: memoryInitial(label, cands.map((v) => ({ value: v, desc: '' }))),
69
+ jumpKeys: true,
70
+ statusHint: 'type to filter · ↑↓ move · 1-9 jump · enter confirm · esc cancel',
71
+ multi: false,
72
+ selected: [],
73
+ }), tty);
74
+ if (res.cancelled) {
75
+ return null;
76
+ }
77
+ if (label !== '') {
78
+ savePick(label, res.value);
79
+ }
80
+ return res.value;
20
81
  }
21
- if (flags.label !== undefined) {
22
- args.push('--label', flags.label);
82
+ finally {
83
+ tty.read.destroy();
84
+ }
85
+ }
86
+ /**
87
+ * rawPick — 透传引擎 pick 参数(--from/--map/-q/--label/--sep/--fuzzy/-1/--auto)。
88
+ * @example rawPick(['--from', 'git branch', '--label', 'git co', '-1'])
89
+ */
90
+ export async function rawPick(args) {
91
+ const parsed = parsePickArgs(args);
92
+ const structured = parsed.rest.length > 0 ? toStructuredCands(parsed.rest) : [];
93
+ return pickFlow({
94
+ cands: structured,
95
+ fromCmd: parsed.from,
96
+ mapper: parsed.mapper,
97
+ flags: parsed.flags,
98
+ });
99
+ }
100
+ /** 解析 pick 参数(对齐引擎 flags:--query=、--from=、--map= 等长格式均支持)。 */
101
+ function parsePickArgs(args) {
102
+ const flags = {};
103
+ let from = '';
104
+ let mapper = '';
105
+ const rest = [];
106
+ for (let i = 0; i < args.length; i++) {
107
+ const a = args[i];
108
+ const take = (name) => {
109
+ if (i + 1 < args.length) {
110
+ i++;
111
+ return args[i];
112
+ }
113
+ throw new PicktuiError(`${NAME} pick: ${name} 缺少参数`, 2, '');
114
+ };
115
+ if (a === '-q' || a === '--query') {
116
+ flags.query = take('--query');
117
+ }
118
+ else if (a.startsWith('--query=')) {
119
+ flags.query = a.slice('--query='.length);
120
+ }
121
+ else if (a === '--from') {
122
+ from = take('--from');
123
+ }
124
+ else if (a.startsWith('--from=')) {
125
+ from = a.slice('--from='.length);
126
+ }
127
+ else if (a === '--map') {
128
+ mapper = take('--map');
129
+ }
130
+ else if (a.startsWith('--map=')) {
131
+ mapper = a.slice('--map='.length);
132
+ }
133
+ else if (a === '--label') {
134
+ flags.label = take('--label');
135
+ }
136
+ else if (a.startsWith('--label=')) {
137
+ flags.label = a.slice('--label='.length);
138
+ }
139
+ else if (a === '--sep') {
140
+ flags.sep = take('--sep');
141
+ }
142
+ else if (a.startsWith('--sep=')) {
143
+ flags.sep = a.slice('--sep='.length);
144
+ }
145
+ else if (a === '--fuzzy') {
146
+ flags.fuzzy = true;
147
+ }
148
+ else if (a === '-1' || a === '--select-1') {
149
+ flags.select1 = true;
150
+ }
151
+ else if (a === '--auto') {
152
+ flags.auto = true;
153
+ }
154
+ else if (a.startsWith('-') && a !== '-') {
155
+ const msg = `${NAME} pick: 未知参数 ${a}`;
156
+ throw new PicktuiError(msg, 2, msg);
157
+ }
158
+ else {
159
+ rest.push(a);
160
+ }
23
161
  }
24
- if (flags.sep !== undefined) {
25
- args.push('--sep', flags.sep);
162
+ return { flags, from, mapper, rest };
163
+ }
164
+ /** pick 全流程(对齐引擎 pickWith)。 */
165
+ async function pickFlow(params) {
166
+ const { flags, fromCmd, mapper } = params;
167
+ const query = flags.query ?? '';
168
+ if (mapper !== '' && params.cands.length > 0) {
169
+ const msg = `${NAME} pick: --map 只与 --from 配合,不能与位置参数同用`;
170
+ throw new PicktuiError(msg, 2, msg);
26
171
  }
172
+ // 构造 FilterOptions(对齐引擎:--fuzzy 优先,--sep 切 token 模式)
173
+ const filterOpts = { mode: 'substring' };
27
174
  if (flags.fuzzy) {
28
- args.push('--fuzzy');
175
+ filterOpts.mode = 'fuzzy';
176
+ }
177
+ else if (flags.sep) {
178
+ filterOpts.mode = 'token';
179
+ filterOpts.sep = flags.sep;
180
+ }
181
+ // 候选来源:显式候选(参数)或 --from 命令执行。
182
+ // 注意:不读宿主进程自身的 stdin——宿主 stdin 是宿主自己的输入流,
183
+ // 由库消费会造成半开 pipe 阻塞并侵入宿主输入(对齐旧版绑定行为)。
184
+ let cands = params.cands;
185
+ if (fromCmd !== '') {
186
+ const raw = await runShell(fromCmd, '');
187
+ if (!raw.ok) {
188
+ throw new PicktuiError(`${NAME} pick: ${fromCmd}: ${raw.stderr}`, 1, raw.stderr);
189
+ }
190
+ cands = toStructuredCands(raw.stdout.split('\n'));
191
+ // --map 转换器:接收 --from 原始输出
192
+ if (mapper !== '') {
193
+ cands = await runMapper(mapper, raw.stdout);
194
+ }
195
+ }
196
+ if (cands.length === 0) {
197
+ const msg = `${NAME} pick: no candidates`;
198
+ throw new PicktuiError(msg, 1, msg);
29
199
  }
200
+ const label = flags.label ?? '';
201
+ const off = process.env[OFF_ENV] === 'off';
202
+ // --auto 自动选中:query 精确命中或唯一前缀命中时直接输出(跳过 TUI)。
203
+ // 首次匹配该 (label, value) 需用户确认(记录 confirm 记录,之后不再询问);
204
+ // off / 无 tty / 无 label 时跳过确认;拒绝确认回退下方 TUI。
205
+ if (flags.auto && query !== '') {
206
+ const v = autoResolve(cands, query);
207
+ if (v !== null) {
208
+ if (label === '' || off || isConfirmed(label, v)) {
209
+ savePickIfLabeled(label, v);
210
+ return v;
211
+ }
212
+ const tty = openTty();
213
+ if (tty !== null) {
214
+ try {
215
+ const answer = await promptConfirm(tty, `\r\n${NAME}: 首次自动匹配 ${label} → ${v},确认执行?[y/N] `);
216
+ if (answer !== null && /^(y|yes)$/i.test(answer.trim())) {
217
+ confirm(label, v);
218
+ savePickIfLabeled(label, v);
219
+ return v;
220
+ }
221
+ // 用户拒绝确认:回退到下方 TUI 选择(query 预填过滤)
222
+ }
223
+ finally {
224
+ tty.read.destroy();
225
+ }
226
+ }
227
+ else {
228
+ // 无 /dev/tty:非交互环境跳过确认,保持确定性执行
229
+ savePickIfLabeled(label, v);
230
+ return v;
231
+ }
232
+ }
233
+ }
234
+ // -1 自动选中(仅一个匹配时)
30
235
  if (flags.select1) {
31
- args.push('-1');
236
+ const filtered = filterCands(cands, query, filterOpts);
237
+ if (filtered.length === 1) {
238
+ savePickIfLabeled(label, filtered[0].value);
239
+ return filtered[0].value;
240
+ }
241
+ }
242
+ // 非交互开关或无 tty → 退化路径
243
+ const tty = openTty();
244
+ if (off || tty === null) {
245
+ return pickNonInteractive(cands, query, filterOpts, label, flags.auto === true);
32
246
  }
33
- if (flags.auto) {
34
- args.push('--auto');
247
+ try {
248
+ const res = await runTui(newModel({
249
+ title: `${NAME} pick`,
250
+ subtitle: label,
251
+ cands,
252
+ query,
253
+ filterOpts,
254
+ initial: memoryInitial(label, cands),
255
+ jumpKeys: false,
256
+ statusHint: 'type to filter · ↑↓ move · enter select · esc cancel',
257
+ multi: false,
258
+ selected: [],
259
+ }), tty);
260
+ if (res.cancelled) {
261
+ return null;
262
+ }
263
+ // 自动解析流程中用户从 TUI 显式选中 → 视为对该解析的确认
264
+ if (flags.auto && label !== '') {
265
+ confirm(label, res.value);
266
+ }
267
+ savePickIfLabeled(label, res.value);
268
+ return res.value;
35
269
  }
36
- const inv = await invokeEngine(args, { input: serializeCandidates(cands) });
37
- if (inv.exitCode === 130) {
38
- return null; // 取消
270
+ finally {
271
+ tty.read.destroy();
39
272
  }
40
- assertSuccess(inv, 'picktui pick');
41
- return inv.stdout.replace(/\r?\n$/, '');
42
273
  }
43
- /**
44
- * menu 多值缩写 TUI 菜单(数字 1-9 直选)。
45
- *
46
- * @param label 菜单标题与选择记忆键
47
- * @param cands 候选(经位置参数传入,注意 argv 长度限制;超长场景用 pick)
48
- * @returns 选中值;取消返回 null
49
- */
50
- export async function menu(label, cands) {
51
- const inv = await invokeEngine(['_menu', label, ...cands]);
52
- if (inv.exitCode === 130) {
53
- return null;
274
+ /** 非交互模式(对齐引擎 pickNonInteractive)。 */
275
+ function pickNonInteractive(cands, query, opts, label, auto) {
276
+ if (auto && query !== '') {
277
+ const v = autoResolve(cands, query);
278
+ if (v !== null) {
279
+ savePickIfLabeled(label, v);
280
+ return v;
281
+ }
282
+ const msg = `${NAME} pick: no unique match for "${query}"`;
283
+ throw new PicktuiError(msg, 1, msg);
284
+ }
285
+ if (query === '') {
286
+ const chosen = cands[0].value;
287
+ savePickIfLabeled(label, chosen);
288
+ return chosen;
54
289
  }
55
- assertSuccess(inv, 'picktui _menu');
56
- return inv.stdout.replace(/\r?\n$/, '');
290
+ const filtered = filterCands(cands, query, opts);
291
+ if (filtered.length === 0) {
292
+ const msg = `${NAME} pick: no match for "${query}"`;
293
+ throw new PicktuiError(msg, 1, msg);
294
+ }
295
+ const chosen = filtered[0].value;
296
+ savePickIfLabeled(label, chosen);
297
+ return chosen;
298
+ }
299
+ /** label 非空时记录选择记忆(失败不中断主流程,与引擎一致)。 */
300
+ function savePickIfLabeled(label, value) {
301
+ if (label === '') {
302
+ return;
303
+ }
304
+ try {
305
+ savePick(label, value);
306
+ }
307
+ catch {
308
+ // 记忆只是锦上添花,记录失败不影响选中结果
309
+ }
310
+ }
311
+ /** 选择记忆:仅 query 为空时应用(有预填过滤词说明是精确搜索,不记忆)。 */
312
+ function memoryInitial(label, cands) {
313
+ if (label === '') {
314
+ return 0;
315
+ }
316
+ try {
317
+ const last = lastPick(loadPickHistory(), label);
318
+ if (last !== '') {
319
+ for (let i = 0; i < cands.length; i++) {
320
+ if (cands[i].value === last) {
321
+ return i;
322
+ }
323
+ }
324
+ }
325
+ }
326
+ catch {
327
+ // 记忆损坏时静默回退首行
328
+ }
329
+ return 0;
330
+ }
331
+ /** 在 tty 上运行 TUI 选择器,返回(取消, 选中值, 多选勾选)。 */
332
+ export async function runTui(model, tty) {
333
+ enterInteractive(tty);
334
+ const reader = new KeyReader(tty.read);
335
+ const onResize = () => {
336
+ model.width = tty.write.columns > 0 ? tty.write.columns : model.width;
337
+ model.height = tty.write.rows > 0 ? tty.write.rows : model.height;
338
+ };
339
+ tty.write.on('resize', onResize);
340
+ onResize();
341
+ let pending = null;
342
+ const nextKey = () => {
343
+ if (pending === null) {
344
+ pending = reader.next().finally(() => {
345
+ pending = null;
346
+ });
347
+ }
348
+ return pending;
349
+ };
350
+ try {
351
+ for (;;) {
352
+ const [kind, value] = await Promise.race([
353
+ nextKey().then((k) => ['key', k]),
354
+ sleep(TICK_MS).then(() => ['tick', null]),
355
+ ]);
356
+ if (kind === 'key') {
357
+ const k = value;
358
+ if (k === null) {
359
+ break; // 输入流关闭(EOF)
360
+ }
361
+ model = update(model, toKeyLike(k));
362
+ if (model.quit) {
363
+ break;
364
+ }
365
+ }
366
+ else {
367
+ model = { ...model, phase: model.phase ^ 1 };
368
+ }
369
+ renderFrame(tty, view(model));
370
+ }
371
+ }
372
+ finally {
373
+ exitInteractive(tty);
374
+ tty.write.removeListener('resize', onResize);
375
+ tty.write.write('\x1b[2J\x1b[H');
376
+ }
377
+ if (model.cancelled || model.filtered.length === 0) {
378
+ return { cancelled: true, value: '', selected: [] };
379
+ }
380
+ if (model.opts.multi) {
381
+ return {
382
+ cancelled: false,
383
+ value: model.filtered[model.cursor].value,
384
+ selected: selectedValues(model),
385
+ };
386
+ }
387
+ return { cancelled: false, value: model.filtered[model.cursor].value, selected: [] };
388
+ }
389
+ /** 归一化按键:可打印字符转字符串('k'/'j' 等移动键语义由 model 处理)。 */
390
+ function toKeyLike(k) {
391
+ if (typeof k === 'object') {
392
+ return k.value === '' ? 'x' : k.value; // 左右方向键等忽略键映射为无操作
393
+ }
394
+ return k;
395
+ }
396
+ /** 执行 shell 命令(10s 超时,input 喂 stdin);失败时附带 stderr 诊断。 */
397
+ function runShell(cmd, input) {
398
+ return new Promise((resolve) => {
399
+ const child = exec(cmd, {
400
+ timeout: CMD_TIMEOUT_MS,
401
+ maxBuffer: 64 * 1024 * 1024,
402
+ windowsHide: true,
403
+ }, (err, stdout) => {
404
+ if (err) {
405
+ const stderr = err.stderr ?? '';
406
+ resolve({ ok: false, stderr: stderr.trim() || err.message });
407
+ return;
408
+ }
409
+ resolve({ ok: true, stdout: stdout ?? '' });
410
+ });
411
+ child.stdin?.end(input);
412
+ });
413
+ }
414
+ /** 执行 --map 转换器:stdin 喂 input,输出按结构化候选解析;脚本缺失 fail-fast。 */
415
+ async function runMapper(mapper, input) {
416
+ const missing = mapperScriptMissing(mapper);
417
+ if (missing !== '') {
418
+ const msg = `map 脚本文件不存在: ${missing}\n → 脚本可能已被移动、删除或重命名`;
419
+ throw new PicktuiError(msg, 1, msg);
420
+ }
421
+ const res = await runShell(mapper, input);
422
+ if (!res.ok) {
423
+ throw new PicktuiError(`${NAME} pick: --map ${mapper}: ${res.stderr}`, 1, res.stderr);
424
+ }
425
+ return toStructuredCands(res.stdout.split('\n'));
57
426
  }
58
427
  /**
59
- * rawPick 透传任意引擎 pick 参数(--from/--map 等绑定未包装的 flags)。
60
- *
61
- * @example rawPick(['--from', 'git branch', '--label', 'git co', '-1'])
428
+ * 扫描 mapper 命令,识别"解释器 + 脚本路径"形态,返回第一个不存在的脚本文件路径。
429
+ * 保守策略:跳过 flags/引号/变量/管道等 shell 语法,只检查形如路径的 token
430
+ * (/ ./ ../ ~/ 前缀、带常见脚本扩展名),避免误拦 node -e / python3 -m 等合法用法。
62
431
  */
63
- export async function rawPick(args) {
64
- const inv = await invokeEngine(['pick', ...args]);
65
- if (inv.exitCode === 130) {
66
- return null;
432
+ export function mapperScriptMissing(mapper) {
433
+ const fields = mapper.trim().split(/\s+/).filter(Boolean);
434
+ if (fields.length < 2) {
435
+ return '';
436
+ }
437
+ for (const tok of fields.slice(1)) {
438
+ if (tok.startsWith('-')) {
439
+ continue;
440
+ }
441
+ if (/['"`${}|;&<>?*=]/.test(tok)) {
442
+ continue;
443
+ }
444
+ if (!scriptPathLike(tok)) {
445
+ continue;
446
+ }
447
+ const p = expandTilde(tok);
448
+ if (existsSync(p)) {
449
+ continue;
450
+ }
451
+ return p;
452
+ }
453
+ return '';
454
+ }
455
+ /** token 是否形如脚本文件路径:/ ./ ../ ~/ 前缀,或带常见脚本扩展名。 */
456
+ function scriptPathLike(tok) {
457
+ if (tok.startsWith('/') || tok.startsWith('./') || tok.startsWith('../') || tok.startsWith('~/')) {
458
+ return true;
459
+ }
460
+ return isScriptExt(extname(tok).toLowerCase());
461
+ }
462
+ /** 常见脚本解释器可直接执行的文件扩展名。 */
463
+ function isScriptExt(ext) {
464
+ return [
465
+ '.mjs', '.cjs', '.js', '.ts', '.py', '.sh', '.bash', '.zsh', '.fish',
466
+ '.pl', '.rb', '.php', '.lua', '.exs', '.r',
467
+ ].includes(ext);
468
+ }
469
+ /** 展开 ~ 与 ~/ 前缀为用户主目录。 */
470
+ function expandTilde(p) {
471
+ if (p === '~') {
472
+ return homedir();
67
473
  }
68
- assertSuccess(inv, 'picktui pick');
69
- return inv.stdout.replace(/\r?\n$/, '');
474
+ if (p.startsWith('~/')) {
475
+ return join(homedir(), p.slice(2));
476
+ }
477
+ return p;
478
+ }
479
+ function sleep(ms) {
480
+ return new Promise((resolve) => setTimeout(resolve, ms));
481
+ }
482
+ // 颜色开关:NO_COLOR 环境变量时降级纯文本
483
+ if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') {
484
+ setColorEnabled(false);
70
485
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * cand — 结构化候选:key<TAB>description 拆分与清洗(纯函数,对齐协议总览)。
3
+ *
4
+ * 过滤只匹配 key,TUI 展示 description,选中输出 key——"展示与选中分离"。
5
+ * 行为与 Go 引擎 StructuredCandidates 完全一致:
6
+ * 1. 先按 TAB 拆 key<TAB>description(无 TAB 则整行为 key);
7
+ * 2. key 部分 trim → 去 "* "/"+ " 前缀(git branch 标记)→ 再 trim;
8
+ * 3. 跳空 key;按 key 去重保序——后到行可为先到同名 key 补缺失描述;
9
+ * 4. description 部分 trim(key 为空的行——如行首 TAB——直接跳过)。
10
+ */
11
+ import type { Candidate } from './types.js';
12
+ /** 将 `string | Candidate` 输入序列化为协议结构化行(key<TAB>description)。 */
13
+ export declare function serializeLines(cands: (string | Candidate)[]): string[];
14
+ /** 将原始行列表解析为结构化候选(与 Go StructuredCandidates 同语义)。 */
15
+ export declare function structuredCandidates(lines: string[]): Candidate[];
16
+ /** 输入候选统一解析为结构化候选(filter/resolve/pick 共用入口)。 */
17
+ export declare function toStructuredCands(cands: (string | Candidate)[]): Candidate[];
@@ -0,0 +1,2 @@
1
+ /** 状态文件目录(history.toml / confirm.toml 所在)。 */
2
+ export declare function dataDir(): string;
@@ -1,7 +1,16 @@
1
+ /** 自动匹配确认记录文件名。 */
2
+ export declare const CONFIRM_FILE = "confirm.toml";
3
+ /** 返回确认记录文件路径。 */
4
+ export declare function confirmPath(): string;
5
+ /** 解码 confirm.toml 为 label → 已确认值列表;缺失/损坏时返回空 Map。 */
6
+ export declare function loadConfirmed(): Map<string, string[]>;
7
+ /** 报告 (label, value) 是否已被确认过;label 为空时恒为 false。 */
8
+ export declare function isConfirmed(label: string, value: string): boolean;
9
+ /** 记录 (label, value) 为已确认(幂等;label 为空不落盘)。 */
10
+ export declare function confirm(label: string, value: string): void;
11
+ /** 解析确认回答:y/yes(大小写不敏感,容忍首尾空白)为确认。 */
12
+ export declare function parseConfirmAnswer(s: string): boolean;
1
13
  /** 报告 (label, value) 是否已被确认过。 */
2
14
  export declare function confirmCheck(label: string, value: string): Promise<boolean>;
3
- /**
4
- * 记录 (label, value) 为已确认(幂等)。
5
- * @returns 确认后的实际状态(label 为空时不落盘 → false)
6
- */
15
+ /** 记录 (label, value) 为已确认(幂等);返回确认后的实际状态。 */
7
16
  export declare function confirmAdd(label: string, value: string): Promise<boolean>;
@@ -1,16 +1,48 @@
1
- import type { Candidate, FilterOptions, FilteredCandidate } from './types.js';
1
+ import type { Candidate, FilteredCandidate, FilterMode, FilterOptions } from './types.js';
2
+ /** 将查询字符串拆分为小写关键字列表(空格分词、小写化、过滤空串)。 */
3
+ export declare function splitKeywords(query: string): string[];
4
+ /** 子串 AND:所有关键字都需作为子串出现(大小写不敏感)。 */
5
+ export declare function matchSubstringAll(s: string, kws: string[]): boolean;
6
+ /** token 前缀:按分隔符集合切 token,每关键字须匹配某 token 的前缀。 */
7
+ export declare function matchTokenPrefixAll(s: string, kws: string[], sep: string): boolean;
8
+ /** 子序列模糊:所有关键字都需作为子序列出现(大小写不敏感)。 */
9
+ export declare function matchFuzzyAll(s: string, kws: string[]): boolean;
10
+ /** pat 是否为 s 的子序列(不要求连续)。 */
11
+ export declare function isSubsequence(pat: string, s: string): boolean;
12
+ /** 排序并合并重叠的 [start, end) 区间(与 Go mergeRanges 一致)。 */
13
+ export declare function mergeRanges(ranges: [number, number][]): [number, number][];
14
+ /**
15
+ * highlightRanges 返回 s 中匹配关键字的 [start, end) rune 索引区间(已排序合并)。
16
+ * 空查询返回 []。
17
+ */
18
+ export declare function highlightRanges(s: string, query: string, opts?: FilterOptions): [number, number][];
19
+ /** 匹配模式分派:单候选是否命中全部关键字。 */
20
+ export declare function matchCandidate(s: string, kws: string[], opts?: FilterOptions): boolean;
21
+ export interface FilterOptsNormalized {
22
+ mode: FilterMode;
23
+ sep: string;
24
+ }
25
+ /** 归一化 FilterOptions(对齐协议 --mode/--sep 语义)。 */
26
+ export declare function normalizeOpts(opts?: FilterOptions): FilterOptsNormalized;
27
+ /**
28
+ * filterCands — 过滤 + 高亮区间(纯同步,保留输入顺序)。
29
+ * 描述不参与过滤——过滤永远针对"选中什么"而非"展示什么"。空查询返回全部。
30
+ */
31
+ export declare function filterCands(cands: Candidate[], query: string, opts?: FilterOptions): FilteredCandidate[];
32
+ /**
33
+ * autoResolve — 把查询字符串解析为唯一候选(供 --auto 自动选中):
34
+ * 1. 精确匹配(大小写敏感)直接命中;
35
+ * 2. 否则大小写不敏感的唯一前缀匹配;
36
+ * 无匹配或多个前缀匹配时返回 null。
37
+ */
38
+ export declare function autoResolve(cands: Candidate[], query: string): string | null;
2
39
  /**
3
40
  * filter — 过滤候选并返回命中项(保留输入顺序)+ 高亮区间。
4
- *
5
- * @param cands 候选(字符串或结构化 Candidate)
6
- * @param query 过滤关键字(空格分词,空串返回全部)
7
- * @param opts 匹配模式(默认 substring)
8
- * @returns 命中候选,`ranges` 为命中的 rune 区间(空查询恒为 [])
41
+ * 与协议 filter --json 等价;空查询返回全部(ranges 恒为 [])。
9
42
  */
10
43
  export declare function filter(cands: (string | Candidate)[], query?: string, opts?: FilterOptions): Promise<FilteredCandidate[]>;
11
44
  /**
12
45
  * resolve — 把 query 解析为唯一候选(精确 → 唯一前缀,大小写不敏感前缀)。
13
- *
14
- * @returns 解析出的选中值;无匹配/多个前缀匹配返回 null(引擎退出码 1)
46
+ * @returns 解析出的选中值;无匹配/多个前缀匹配/空 query 返回 null
15
47
  */
16
- export declare function resolve(cands: (string | Candidate)[], query: string, opts?: FilterOptions): Promise<string | null>;
48
+ export declare function resolve(cands: (string | Candidate)[], query: string, _opts?: FilterOptions): Promise<string | null>;
@@ -1,4 +1,16 @@
1
+ /** 选择记忆文件名;曾用名 picks.toml 因易与 pick 命令混淆而弃用。 */
2
+ export declare const HISTORY_FILE = "history.toml";
3
+ /** 旧版文件名,仅用于首次读取时自动迁移。 */
4
+ export declare const LEGACY_HISTORY_FILE = "picks.toml";
5
+ /** 返回选择记忆文件路径。 */
6
+ export declare function historyPath(): string;
7
+ /** history.toml 原始内容(label → { last }),与引擎同一份。 */
8
+ export declare function loadPickHistory(): Map<string, string>;
9
+ /** 取回 label 上次选中的值;无记录返回 ""。 */
10
+ export declare function lastPick(raw: Map<string, string>, label: string): string;
11
+ /** 记录 label 的上次选中值(保留其它 label 的记忆后写回)。 */
12
+ export declare function savePick(label: string, value: string): void;
1
13
  /** 读取 label 上次选中的值;无记录返回 ""。 */
2
14
  export declare function histGet(label: string): Promise<string>;
3
- /** 记录 label 的上次选中值(成功无输出;失败抛 PicktuiError)。 */
15
+ /** 记录 label 的上次选中值(失败抛 PicktuiError,含退出码 1)。 */
4
16
  export declare function histSet(label: string, value: string): Promise<void>;
@@ -4,8 +4,9 @@
4
4
  * import { pick } from '@havocrao/picktui/tui'
5
5
  * import { histGet } from '@havocrao/picktui/history'
6
6
  */
7
+ /** 本 TS 实现的语义版本(对齐 npm 包版本号)。 */
8
+ export declare const VERSION = "0.1.0";
7
9
  export * from './types.js';
8
- export * from './engine.js';
9
10
  export * from './filter.js';
10
11
  export * from './tui.js';
11
12
  export * from './history.js';