dgxay-code-cli 1.0.0 → 1.1.0
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-zh.md +50 -8
- package/README.md +50 -9
- package/package.json +1 -1
- package/src/cli/ansi.js +38 -8
- package/src/cli/approval.js +11 -11
- package/src/cli/editor.js +456 -202
- package/src/cli/i18n.js +276 -0
- package/src/cli/index.js +34 -34
- package/src/cli/print.js +5 -5
- package/src/cli/render.js +147 -76
- package/src/cli/repl.js +205 -141
- package/src/cli/slash.js +29 -28
- package/src/cli/ui.js +8 -8
- package/src/commands/chat.js +45 -45
- package/src/commands/config.js +81 -81
- package/src/commands/context.js +5 -5
- package/src/commands/doctor.js +34 -34
- package/src/commands/export.js +13 -13
- package/src/commands/mcp.js +51 -51
- package/src/commands/model.js +38 -38
- package/src/commands/oauth.js +61 -61
- package/src/commands/permission.js +19 -19
- package/src/commands/provider.js +70 -70
- package/src/commands/run.js +20 -20
- package/src/commands/skill.js +15 -15
- package/src/commands/workspace.js +18 -18
- package/src/core/agent.js +19 -18
- package/src/core/llm.js +10 -10
- package/src/core/mcp.js +5 -5
- package/src/core/oauth.js +36 -36
- package/src/core/prompt.js +46 -46
- package/src/core/sessions.js +7 -7
- package/src/core/tools.js +24 -24
- package/src/services/input.js +1 -1
- package/src/services/settings.js +28 -28
- package/src/services/skills.js +1 -1
- package/src/services/terminal.js +3 -3
package/src/cli/editor.js
CHANGED
|
@@ -1,37 +1,76 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* 行编辑器:带边框的输入框、斜杠补全菜单、历史、多行输入。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* - 空格覆盖多余内容;
|
|
11
|
-
* - 需要时整行重写(单行内完成,绝不跨行)。
|
|
6
|
+
* 终端兼容性(前几次踩坑的教训):
|
|
7
|
+
* - 不假设终端支持光标上移 / 清行;
|
|
8
|
+
* - 启动时探测一次(见 detectCapabilities),支持则做「区域重绘」,不支持则降级;
|
|
9
|
+
* - 无论如何,输入被提交 / 消息输出后,都不会出现重复行。
|
|
12
10
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* 交互:
|
|
12
|
+
* - 输入框是一个方框(┌─┐ │文字│ └─┘),宽度随终端变化;
|
|
13
|
+
* - 输入 `/` 弹出命令菜单,↑↓ 选择、Tab 补全、Enter 执行;
|
|
14
|
+
* - 方向键 ↑ 回填上一条用户提示词(不是命令历史,而是对话里说过的话);
|
|
15
|
+
* - 行尾 `\` 续行,Enter 提交;Shift+Enter 也可以换行。
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
const readline = require('readline');
|
|
18
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
COLORS,
|
|
21
|
+
displayWidth,
|
|
22
|
+
truncate,
|
|
23
|
+
termWidth,
|
|
24
|
+
useUnicode,
|
|
25
|
+
cursorUp,
|
|
26
|
+
clearLine,
|
|
27
|
+
eraseLine,
|
|
28
|
+
clearScreen
|
|
29
|
+
} = require('./ansi');
|
|
30
|
+
const { t } = require('./i18n');
|
|
31
|
+
|
|
32
|
+
/* ------------------------------ 终端能力探测 ------------------------------ */
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 探测终端是否支持光标上移。
|
|
36
|
+
* 做法:写一行标记 → 上移覆盖 → 下移读回,然后清屏擦掉痕迹。
|
|
37
|
+
* 保守起见:只要不是明确的 Windows conhost,就认为支持(Windows Terminal / VSCode / iTerm 都支持)。
|
|
38
|
+
*/
|
|
39
|
+
function detectCapabilities(output = process.stdout) {
|
|
40
|
+
if (!output.isTTY) return { cursorUp: false, clearLine: false, interactive: false };
|
|
41
|
+
if (process.env.DGXAY_NO_CURSOR_UP === '1') return { cursorUp: false, clearLine: false, interactive: true };
|
|
42
|
+
const modern =
|
|
43
|
+
Boolean(process.env.WT_SESSION) ||
|
|
44
|
+
Boolean(process.env.TERM_PROGRAM) ||
|
|
45
|
+
process.env.TERM === 'xterm-256color' ||
|
|
46
|
+
process.env.TERM === 'screen-256color' ||
|
|
47
|
+
Boolean(process.env.COLORTERM) ||
|
|
48
|
+
process.platform !== 'win32';
|
|
49
|
+
return { cursorUp: modern, clearLine: true, interactive: true };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ------------------------------ 编辑��� ------------------------------ */
|
|
19
53
|
|
|
20
54
|
class LineEditor {
|
|
21
55
|
constructor(options = {}) {
|
|
22
56
|
this.input = options.input || process.stdin;
|
|
23
57
|
this.output = options.output || process.stdout;
|
|
24
|
-
this.prompt = options.prompt || '
|
|
25
|
-
this.
|
|
26
|
-
this.historyIndex = this.history.length;
|
|
58
|
+
this.prompt = options.prompt || '';
|
|
59
|
+
this.placeholder = options.placeholder || '';
|
|
27
60
|
this.completer = options.completer || null;
|
|
28
61
|
this.multiline = options.multiline !== false;
|
|
29
62
|
this.maxHistory = options.maxHistory || 500;
|
|
30
63
|
|
|
64
|
+
/** 提交过的输入(/命令与普通提问都在这里,用于 ↑) */
|
|
65
|
+
this.history = Array.isArray(options.history) ? options.history.slice() : [];
|
|
66
|
+
this.historyIndex = this.history.length;
|
|
67
|
+
/** 外部注入的「上一条用户提示词」,优先级高于普通历史 */
|
|
68
|
+
this.lastUserPrompt = options.lastUserPrompt || '';
|
|
69
|
+
|
|
70
|
+
this.caps = options.capabilities || detectCapabilities(this.output);
|
|
71
|
+
|
|
31
72
|
this.value = '';
|
|
32
73
|
this.cursor = 0;
|
|
33
|
-
/** 当前提示行屏幕上的实际内容(含提示符),用于 \r 覆盖时擦尾巴 */
|
|
34
|
-
this.onScreen = '';
|
|
35
74
|
this.popup = null;
|
|
36
75
|
this.draft = '';
|
|
37
76
|
this.active = false;
|
|
@@ -40,7 +79,14 @@ class LineEditor {
|
|
|
40
79
|
this.keyHandler = null;
|
|
41
80
|
this.keyQueue = Promise.resolve();
|
|
42
81
|
this.abortHandler = null;
|
|
43
|
-
|
|
82
|
+
|
|
83
|
+
/** 已经画在屏幕上的盒子行数(用于区域重绘) */
|
|
84
|
+
this.drawnRows = 0;
|
|
85
|
+
this.frameCursorRow = 0;
|
|
86
|
+
this.frameBoxTop = 0;
|
|
87
|
+
/** 是否已为 input 挂过 keypress 处理器(整个编辑器生命周期只挂一次) */
|
|
88
|
+
this.keypressBound = false;
|
|
89
|
+
this.newlineMark = this.unicode() ? '⏎' : '\\n';
|
|
44
90
|
}
|
|
45
91
|
|
|
46
92
|
/* ------------------------------ 生命周期 ------------------------------ */
|
|
@@ -54,14 +100,19 @@ class LineEditor {
|
|
|
54
100
|
this.value = '';
|
|
55
101
|
this.cursor = 0;
|
|
56
102
|
this.popup = null;
|
|
57
|
-
this.
|
|
103
|
+
this.drawnRows = 0;
|
|
58
104
|
this.historyIndex = this.history.length;
|
|
59
105
|
this.active = true;
|
|
60
106
|
this.finished = false;
|
|
61
107
|
|
|
62
108
|
return new Promise((resolve) => {
|
|
63
109
|
this.resolveQuestion = resolve;
|
|
64
|
-
|
|
110
|
+
// 同一进程里可能多次 question()(权限确认 / 二次提问),keypress 只注册一次,
|
|
111
|
+
// 避免同一按键被多个监听器重复处理
|
|
112
|
+
if (!this.keypressBound) {
|
|
113
|
+
readline.emitKeypressEvents(this.input);
|
|
114
|
+
this.keypressBound = true;
|
|
115
|
+
}
|
|
65
116
|
if (this.input.isTTY) {
|
|
66
117
|
try {
|
|
67
118
|
this.input.setRawMode(true);
|
|
@@ -72,12 +123,10 @@ class LineEditor {
|
|
|
72
123
|
this.input.resume();
|
|
73
124
|
|
|
74
125
|
this.keyHandler = (str, key) => {
|
|
75
|
-
// 按键必须按顺序处理:handleKey 是异步的(Ctrl+C 可能等待中断回调),
|
|
76
|
-
// 用队列串行化,避免快速输入时状态错乱。
|
|
77
126
|
this.keyQueue = this.keyQueue.then(() => this.handleKey(str || '', key || {})).catch(() => {});
|
|
78
127
|
};
|
|
79
128
|
this.input.on('keypress', this.keyHandler);
|
|
80
|
-
this.
|
|
129
|
+
this.draw();
|
|
81
130
|
});
|
|
82
131
|
}
|
|
83
132
|
|
|
@@ -97,58 +146,174 @@ class LineEditor {
|
|
|
97
146
|
/* ignore */
|
|
98
147
|
}
|
|
99
148
|
}
|
|
100
|
-
//
|
|
149
|
+
// 收起盒子,只留下光标在下一行
|
|
150
|
+
this.clearDrawn();
|
|
101
151
|
this.output.write('\n');
|
|
102
|
-
this.onScreen = '';
|
|
103
152
|
const resolve = this.resolveQuestion;
|
|
104
153
|
this.resolveQuestion = null;
|
|
105
154
|
if (resolve) resolve(result);
|
|
106
155
|
}
|
|
107
156
|
|
|
108
|
-
/**
|
|
157
|
+
/** 正文输出前:收起输入框 */
|
|
109
158
|
suspend() {
|
|
110
159
|
this.popup = null;
|
|
111
|
-
this.
|
|
160
|
+
this.clearDrawn();
|
|
112
161
|
}
|
|
113
162
|
|
|
114
|
-
/**
|
|
163
|
+
/** 正文输出结束后:重画输入框 */
|
|
115
164
|
resume() {
|
|
116
165
|
if (!this.active) return;
|
|
117
|
-
this.
|
|
118
|
-
this.
|
|
166
|
+
this.resetFrame();
|
|
167
|
+
this.draw();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* ------------------------------ 绘制 ------------------------------ */
|
|
171
|
+
|
|
172
|
+
/** 盒子宽度:优先用输出流自己的列数(测试与嵌入式场景可覆盖),否则取终端宽度 */
|
|
173
|
+
width() {
|
|
174
|
+
const columns = Number(this.output.columns) || termWidth();
|
|
175
|
+
return Math.max(20, Math.min(columns - 1, 100));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 当前输出流是否适合用 Unicode 框线 */
|
|
179
|
+
unicode() {
|
|
180
|
+
return useUnicode(this.output);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 计算盒子的所有行(不含光标定位) */
|
|
184
|
+
boxLines() {
|
|
185
|
+
const width = this.width();
|
|
186
|
+
const inner = width - 2;
|
|
187
|
+
const unicode = this.unicode();
|
|
188
|
+
const box = unicode
|
|
189
|
+
? { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' }
|
|
190
|
+
: { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|' };
|
|
191
|
+
|
|
192
|
+
const text = this.displayText();
|
|
193
|
+
const empty = text.length === 0;
|
|
194
|
+
|
|
195
|
+
// 内容行:按内宽折行
|
|
196
|
+
const rawLines = empty ? [this.placeholder] : wrapDisplay(text, inner);
|
|
197
|
+
const lines = [];
|
|
198
|
+
let cursorRow = 0;
|
|
199
|
+
let cursorCol = 0;
|
|
200
|
+
|
|
201
|
+
rawLines.forEach((line, row) => {
|
|
202
|
+
const visible = empty ? COLORS.faint(line) : line;
|
|
203
|
+
// 内容行 = 左竖线 + 空格 + 文本 + 填充 + 右竖线,总宽必须等于 width
|
|
204
|
+
const fill = Math.max(0, inner - 1 - displayWidth(line));
|
|
205
|
+
lines.push(`${box.v} ${visible}${' '.repeat(fill)}${box.v}`);
|
|
206
|
+
if (!empty) {
|
|
207
|
+
// 光标所在行:用「到当前行累计宽度」判断
|
|
208
|
+
const beforeAll = wrapDisplay(text.slice(0, this.cursorDisplayIndex()), inner);
|
|
209
|
+
if (row === beforeAll.length - 1) {
|
|
210
|
+
cursorRow = row;
|
|
211
|
+
cursorCol = 2 + displayWidth(beforeAll[beforeAll.length - 1] || '');
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
cursorRow = 0;
|
|
215
|
+
cursorCol = 2;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const top = `${box.tl}${box.h.repeat(width - 2)}${box.tr}`;
|
|
220
|
+
const bottom = `${box.bl}${box.h.repeat(width - 2)}${box.br}`;
|
|
221
|
+
return { top, bottom, lines, cursorRow, cursorCol, width };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 绘制输入区。
|
|
226
|
+
* - 支持光标上移:画出完整盒子(有菜单时连同菜单),之后原地擦除重绘;
|
|
227
|
+
* - 不支持:降级为单行提示,避免无法擦除导致界面叠加。
|
|
228
|
+
*/
|
|
229
|
+
draw() {
|
|
230
|
+
if (!this.active) return;
|
|
231
|
+
|
|
232
|
+
if (!this.caps.cursorUp) {
|
|
233
|
+
this.drawInline();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.drawAll();
|
|
119
238
|
}
|
|
120
239
|
|
|
121
|
-
|
|
240
|
+
/**
|
|
241
|
+
* 降级绘制:只用 \r + 空格覆盖当前物理行。
|
|
242
|
+
* 格式:❯ <文字>,超宽时横向滚动,保证永远只有一个物理行。
|
|
243
|
+
*/
|
|
244
|
+
drawInline() {
|
|
245
|
+
const width = this.width();
|
|
246
|
+
const marker = this.unicode() ? '❯ ' : '> ';
|
|
247
|
+
const prefix = `${COLORS.accent(marker)}`;
|
|
248
|
+
const available = Math.max(10, width - displayWidth(marker));
|
|
122
249
|
|
|
123
|
-
|
|
124
|
-
|
|
250
|
+
let text = this.displayText();
|
|
251
|
+
if (!text) {
|
|
252
|
+
const placeholder = truncate(this.placeholder || '', available);
|
|
253
|
+
this.repaintLine(`${prefix}${COLORS.faint(placeholder)}`);
|
|
254
|
+
this.moveCursorBack(available - displayWidth(placeholder) + (this.unicode() ? 2 : 2));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 超宽时保留光标附近的窗口
|
|
259
|
+
let cursorWidth = displayWidth(Array.from(text).slice(0, this.cursor).join(''));
|
|
260
|
+
let shown = truncate(text, available);
|
|
261
|
+
let cursorInShown = Math.min(cursorWidth, displayWidth(shown));
|
|
262
|
+
if (displayWidth(text) > available) {
|
|
263
|
+
const keep = Math.max(0, available - 4);
|
|
264
|
+
const startWidth = Math.max(0, cursorWidth - keep);
|
|
265
|
+
let acc = 0;
|
|
266
|
+
let cut = 0;
|
|
267
|
+
const chars = Array.from(text);
|
|
268
|
+
while (cut < chars.length && acc < startWidth) {
|
|
269
|
+
acc += displayWidth(chars[cut]);
|
|
270
|
+
cut += 1;
|
|
271
|
+
}
|
|
272
|
+
const marker2 = this.unicode() ? '…' : '...';
|
|
273
|
+
shown = marker2 + chars.slice(cut).join('');
|
|
274
|
+
shown = truncate(shown, available);
|
|
275
|
+
cursorInShown = displayWidth(marker2) + (cursorWidth - acc);
|
|
276
|
+
if (cursorInShown > available) cursorInShown = available;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.repaintLine(`${prefix}${shown}`);
|
|
280
|
+
const back = displayWidth(shown) - cursorInShown;
|
|
281
|
+
if (back > 0) this.moveCursorBack(back);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** 用 \r 覆盖当前行并擦掉多余尾部 */
|
|
285
|
+
repaintLine(text) {
|
|
125
286
|
const visible = displayWidth(text);
|
|
126
287
|
const previous = displayWidth(this.onScreen);
|
|
127
288
|
const pad = Math.max(0, previous - visible);
|
|
128
289
|
this.output.write(`\r${text}${' '.repeat(pad)}`);
|
|
129
|
-
// 把光标退回行尾,方便下一次覆盖
|
|
130
290
|
if (pad) this.output.write('\b'.repeat(pad));
|
|
131
291
|
this.onScreen = text;
|
|
132
292
|
}
|
|
133
293
|
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
294
|
+
moveCursorBack(count) {
|
|
295
|
+
if (count > 0) this.output.write('\b'.repeat(count));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
clearDrawn() {
|
|
299
|
+
if (this.caps.cursorUp && this.drawnRows) {
|
|
300
|
+
this.eraseFrame();
|
|
301
|
+
this.resetFrame();
|
|
137
302
|
return;
|
|
138
303
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
304
|
+
if (this.onScreen) {
|
|
305
|
+
this.output.write(`\r${' '.repeat(displayWidth(this.onScreen))}\r`);
|
|
306
|
+
this.onScreen = '';
|
|
307
|
+
}
|
|
308
|
+
this.resetFrame();
|
|
142
309
|
}
|
|
143
310
|
|
|
144
|
-
/* ------------------------------
|
|
311
|
+
/* ------------------------------ 文本工具 ------------------------------ */
|
|
145
312
|
|
|
146
|
-
/** 把内部文本转成单行显示:换行用 ⏎ 表示(部分终端不支持跨行光标定位) */
|
|
147
313
|
displayText() {
|
|
148
314
|
return this.value.replace(/\n/g, ` ${this.newlineMark} `);
|
|
149
315
|
}
|
|
150
316
|
|
|
151
|
-
/** 计算光标在显示文本中的位置 */
|
|
152
317
|
cursorDisplayIndex() {
|
|
153
318
|
const before = Array.from(this.value.slice(0, this.cursor));
|
|
154
319
|
let length = 0;
|
|
@@ -159,102 +324,156 @@ class LineEditor {
|
|
|
159
324
|
return length;
|
|
160
325
|
}
|
|
161
326
|
|
|
162
|
-
|
|
163
|
-
if (!this.active) return;
|
|
164
|
-
const promptText = this.prompt;
|
|
165
|
-
const promptWidth = displayWidth(promptText);
|
|
166
|
-
const totalWidth = Math.max(20, termWidth() - 1);
|
|
167
|
-
const available = Math.max(10, totalWidth - promptWidth);
|
|
168
|
-
|
|
169
|
-
let text = this.displayText();
|
|
170
|
-
let cursor = this.cursorDisplayIndex();
|
|
171
|
-
|
|
172
|
-
// 超宽时横向滚动:保留光标前后一部分,避免行被终端自动折行(折行后无法定位)
|
|
173
|
-
let offset = 0;
|
|
174
|
-
if (displayWidth(text) > available) {
|
|
175
|
-
const beforeCursor = Array.from(text).slice(0, cursor);
|
|
176
|
-
const cursorWidth = displayWidth(beforeCursor.join(''));
|
|
177
|
-
if (cursorWidth > available - 4) {
|
|
178
|
-
// 从前面裁掉
|
|
179
|
-
let cut = 0;
|
|
180
|
-
let removed = 0;
|
|
181
|
-
const target = cursorWidth - (available - 8);
|
|
182
|
-
while (cut < beforeCursor.length && removed < target) {
|
|
183
|
-
removed += displayWidth(beforeCursor[cut]);
|
|
184
|
-
cut += 1;
|
|
185
|
-
}
|
|
186
|
-
const head = useUnicode() ? '…' : '...';
|
|
187
|
-
text = head + Array.from(text).slice(cut).join('');
|
|
188
|
-
offset = displayWidth(head);
|
|
189
|
-
cursor = cursor - removed + offset;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const clipped = truncate(text, Math.max(1, available));
|
|
194
|
-
this.writeLine(`${promptText}${clipped}`);
|
|
327
|
+
/* ------------------------------ 补全菜单 ------------------------------ */
|
|
195
328
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (back > 0) this.output.write('\b'.repeat(back));
|
|
329
|
+
menuItems() {
|
|
330
|
+
if (!this.completer) return null;
|
|
331
|
+
return this.completer(this.value, this.cursor);
|
|
200
332
|
}
|
|
201
333
|
|
|
202
|
-
|
|
334
|
+
openMenu() {
|
|
335
|
+
const trigger = this.menuItems();
|
|
336
|
+
if (!trigger || !trigger.items.length) return false;
|
|
337
|
+
this.popup = { ...trigger, index: 0 };
|
|
338
|
+
this.drawMenu();
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
203
341
|
|
|
204
|
-
|
|
342
|
+
/**
|
|
343
|
+
* 统一重绘:菜单(若有)+ 输入框作为一个整体,先整体擦除再整体画出。
|
|
344
|
+
* 这样菜单开合、上下选择、宽度变化都不会留下残影。
|
|
345
|
+
*/
|
|
346
|
+
drawAll() {
|
|
205
347
|
if (!this.active) return;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
this.popup = null;
|
|
209
|
-
this.repaint();
|
|
348
|
+
if (!this.caps.cursorUp) {
|
|
349
|
+
this.drawInline();
|
|
210
350
|
return;
|
|
211
351
|
}
|
|
212
|
-
|
|
213
|
-
if (this.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
352
|
+
// 先擦除上一次的框架(记录过几何信息)
|
|
353
|
+
if (this.drawnRows) this.eraseFrame();
|
|
354
|
+
|
|
355
|
+
const rows = [];
|
|
356
|
+
let menuRowCount = 0;
|
|
357
|
+
if (this.popup) {
|
|
358
|
+
const menu = this.menuRows();
|
|
359
|
+
menuRowCount = menu.length;
|
|
360
|
+
rows.push(...menu);
|
|
217
361
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
362
|
+
const box = this.boxLines();
|
|
363
|
+
rows.push(box.top, ...box.lines, box.bottom);
|
|
364
|
+
const boxTop = menuRowCount;
|
|
365
|
+
|
|
366
|
+
// 逐行写出。注意:每行末尾的 \n 会把光标推到下一行;最后一行的 \n 让光标
|
|
367
|
+
// 落在「框架下方一行」,eraseFrame 正是按这个前提计算回退行数的。
|
|
368
|
+
for (const row of rows) this.output.write(`${row}\n`);
|
|
369
|
+
|
|
370
|
+
// 记住几何位置,下次擦除时据此回到框架顶部
|
|
371
|
+
// 注意:写出最后一行后光标落在「框架下方一行」(索引 = rows.length),
|
|
372
|
+
// 而 frameCursorRow 是「光标目标行」在这套坐标里的索引,从 0 开始数:
|
|
373
|
+
// 菜单第 0 行 … 菜单最后一行 → 顶框 → 内容行 … → 底框
|
|
374
|
+
this.drawnRows = rows.length;
|
|
375
|
+
this.frameBoxTop = boxTop;
|
|
376
|
+
this.frameCursorRow = boxTop + 1 + box.cursorRow;
|
|
377
|
+
|
|
378
|
+
// 光标从「框架下方一行」上移到目标行,距离 = rows.length - frameCursorRow
|
|
379
|
+
const up = this.drawnRows - this.frameCursorRow;
|
|
380
|
+
if (up > 0) this.output.write(cursorUp(up));
|
|
381
|
+
this.output.write('\r');
|
|
382
|
+
if (box.cursorCol > 0) this.output.write(`\x1b[${box.cursorCol}C`);
|
|
221
383
|
}
|
|
222
384
|
|
|
223
385
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
386
|
+
* 擦除上一次画出的框架,并把光标停在「框架顶行、第 0 列」。
|
|
387
|
+
*
|
|
388
|
+
* 这是整个编辑器最容易出错的地方,做法必须精确:
|
|
389
|
+
* 1. 从光标所在行上移到框架顶行、回到第 0 列;
|
|
390
|
+
* 2. 逐行「清行」,然后把光标移到下一行的第 0 列 —— 用 ESC[nE 而不是写 '\n',
|
|
391
|
+
* 因为窗口底部写 '\n' 会触发终端向上滚屏,行距就错位了;
|
|
392
|
+
* 3. 走到框架下方后,一次性上移 rows 行回到顶行。
|
|
393
|
+
* 只用到 A / E / 2K,没有别的依赖;框线恰好占满终端宽度也不会自动换行。
|
|
226
394
|
*/
|
|
227
|
-
|
|
228
|
-
if (!this.
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
395
|
+
eraseFrame() {
|
|
396
|
+
if (!this.drawnRows) return;
|
|
397
|
+
const rows = this.drawnRows;
|
|
398
|
+
const fromCursor = this.frameCursorRow || 0;
|
|
399
|
+
this.output.write('\r');
|
|
400
|
+
if (fromCursor > 0) this.output.write(cursorUp(fromCursor));
|
|
401
|
+
this.output.write('\r');
|
|
402
|
+
for (let i = 0; i < rows; i += 1) {
|
|
403
|
+
// eraseLine() 返回字符串 `\x1b[2K`;注意不能用 clearLine()(它会直接写 stdout 并返回 undefined)
|
|
404
|
+
this.output.write(eraseLine());
|
|
405
|
+
this.output.write('\x1b[1E'); // 清完这一行,下去一行的第 0 列
|
|
406
|
+
}
|
|
407
|
+
// 此时光标在框架下方第 rows 行,抬回顶部
|
|
408
|
+
this.output.write(cursorUp(rows));
|
|
409
|
+
this.output.write('\r');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** 清掉记下的几何信息:下一次 drawAll 会从光标所在行从头画起 */
|
|
413
|
+
resetFrame() {
|
|
414
|
+
this.drawnRows = 0;
|
|
415
|
+
this.frameCursorRow = 0;
|
|
416
|
+
this.frameBoxTop = 0;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** 菜单的每一行(宽度与输入框一致) */
|
|
420
|
+
menuRows() {
|
|
421
|
+
const { items, index, title, hint } = this.popup;
|
|
422
|
+
const width = this.width();
|
|
423
|
+
const inner = width - 2;
|
|
424
|
+
const unicode = this.unicode();
|
|
425
|
+
const v = unicode ? '│' : '|';
|
|
426
|
+
const tl = unicode ? '┌' : '+';
|
|
427
|
+
const bl = unicode ? '└' : '+';
|
|
428
|
+
const tr = unicode ? '┐' : '+';
|
|
429
|
+
const br = unicode ? '┘' : '+';
|
|
430
|
+
const h = unicode ? '─' : '-';
|
|
431
|
+
|
|
432
|
+
const pageSize = 8;
|
|
433
|
+
const start = Math.max(0, Math.min(index - Math.floor(pageSize / 2), Math.max(0, items.length - pageSize)));
|
|
434
|
+
const shown = items.slice(start, start + pageSize);
|
|
435
|
+
|
|
436
|
+
const rows = [];
|
|
437
|
+
const headLabel = ` ${title || 'Menu'} `;
|
|
438
|
+
const headFill = Math.max(0, width - 2 - displayWidth(headLabel));
|
|
439
|
+
rows.push(`${tl}${COLORS.bold(headLabel)}${h.repeat(headFill)}${tr}`);
|
|
232
440
|
|
|
233
|
-
// 先把提示行清掉,候选从空行开始打印
|
|
234
|
-
this.clearLine();
|
|
235
|
-
this.output.write('\n');
|
|
236
|
-
if (title) this.output.write(`${COLORS.faint(` ${title}`)}\n`);
|
|
237
|
-
const shown = items.slice(0, 12);
|
|
238
441
|
shown.forEach((item, i) => {
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
|
|
442
|
+
const realIndex = start + i;
|
|
443
|
+
const active = realIndex === index;
|
|
444
|
+
const marker = active ? COLORS.accent('›') : ' ';
|
|
445
|
+
const label = truncate(item.label, Math.max(8, inner - 6));
|
|
446
|
+
const hintWidth = Math.max(0, inner - 4 - displayWidth(label));
|
|
447
|
+
const tail = item.hint && hintWidth > 6 ? COLORS.faint(` ${truncate(item.hint, hintWidth)}`) : '';
|
|
448
|
+
const body = `${marker} ${active ? COLORS.accent(label) : label}${tail}`;
|
|
449
|
+
const fill = Math.max(0, inner - displayWidth(`${marker} ${label}${tail ? ` ${truncate(item.hint, hintWidth)}` : ''}`));
|
|
450
|
+
rows.push(`${COLORS.faint(v)} ${body}${' '.repeat(fill)}${COLORS.faint(v)}`);
|
|
244
451
|
});
|
|
245
|
-
if (items.length > shown.length) {
|
|
246
|
-
this.output.write(COLORS.faint(` …还有 ${items.length - shown.length} 项\n`));
|
|
247
|
-
}
|
|
248
452
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
453
|
+
const footer = items.length > shown.length ? ` ${start + 1}-${start + shown.length}/${items.length} ` : '';
|
|
454
|
+
rows.push(`${bl}${h.repeat(Math.max(0, width - 2 - displayWidth(footer)))}${footer}${br}`);
|
|
455
|
+
if (hint) rows.push(COLORS.faint(` ${hint}`));
|
|
456
|
+
return rows;
|
|
252
457
|
}
|
|
253
458
|
|
|
254
|
-
|
|
459
|
+
/** 打开/刷新菜单后整体重绘 */
|
|
460
|
+
drawMenu() {
|
|
461
|
+
this.drawAll();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
closeMenu() {
|
|
255
465
|
if (!this.popup) return;
|
|
256
466
|
this.popup = null;
|
|
257
|
-
this.
|
|
467
|
+
this.drawAll();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
applyItem(item) {
|
|
471
|
+
if (!item) return;
|
|
472
|
+
const next = typeof item.apply === 'string' ? item.apply : typeof item.value === 'string' ? item.value : null;
|
|
473
|
+
if (next !== null) {
|
|
474
|
+
this.value = next;
|
|
475
|
+
this.cursor = Array.from(next).length;
|
|
476
|
+
}
|
|
258
477
|
}
|
|
259
478
|
|
|
260
479
|
/* ------------------------------ 编辑 ------------------------------ */
|
|
@@ -276,58 +495,65 @@ class LineEditor {
|
|
|
276
495
|
this.historyIndex = this.history.length;
|
|
277
496
|
}
|
|
278
497
|
|
|
498
|
+
/** 取上一条可回填的内容:优先用户提示词,其次普通历史 */
|
|
499
|
+
previousEntry() {
|
|
500
|
+
if (this.lastUserPrompt) return this.lastUserPrompt;
|
|
501
|
+
if (this.history.length) return this.history[this.history.length - 1];
|
|
502
|
+
return '';
|
|
503
|
+
}
|
|
504
|
+
|
|
279
505
|
/* ------------------------------ 按键 ------------------------------ */
|
|
280
506
|
|
|
281
507
|
async handleKey(str, key) {
|
|
282
508
|
if (!this.active) return;
|
|
283
509
|
const ctrl = Boolean(key.ctrl);
|
|
284
510
|
const chars = Array.from(this.value);
|
|
285
|
-
const
|
|
511
|
+
const isSubmit = key.name === 'return' || key.name === 'enter' || str === '\r' || str === '\n';
|
|
286
512
|
|
|
287
|
-
/* ---
|
|
513
|
+
/* --- 菜单激活时优先处理 --- */
|
|
288
514
|
if (this.popup) {
|
|
289
515
|
const { items } = this.popup;
|
|
290
516
|
if (key.name === 'up') {
|
|
291
517
|
this.popup.index = (this.popup.index - 1 + items.length) % items.length;
|
|
292
|
-
this.
|
|
518
|
+
this.drawMenu();
|
|
293
519
|
return;
|
|
294
520
|
}
|
|
295
|
-
if (key.name === 'down'
|
|
521
|
+
if (key.name === 'down') {
|
|
296
522
|
this.popup.index = (this.popup.index + 1) % items.length;
|
|
297
|
-
this.
|
|
523
|
+
this.drawMenu();
|
|
298
524
|
return;
|
|
299
525
|
}
|
|
300
526
|
if (key.name === 'escape') {
|
|
527
|
+
this.closeMenu();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (key.name === 'tab') {
|
|
531
|
+
// Tab 补全当前选中项
|
|
532
|
+
this.applyItem(items[this.popup.index]);
|
|
301
533
|
this.popup = null;
|
|
302
|
-
this.
|
|
534
|
+
this.draw();
|
|
303
535
|
return;
|
|
304
536
|
}
|
|
305
|
-
if (
|
|
306
|
-
|
|
537
|
+
if (isSubmit) {
|
|
538
|
+
// Enter 执行当前选中项
|
|
539
|
+
this.applyItem(items[this.popup.index]);
|
|
307
540
|
this.popup = null;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (applied && typeof applied.value === 'string') {
|
|
317
|
-
this.value = applied.value;
|
|
318
|
-
this.cursor = Number.isInteger(applied.cursor) ? applied.cursor : Array.from(applied.value).length;
|
|
319
|
-
} else if (typeof item.apply === 'string') {
|
|
320
|
-
this.value = item.apply;
|
|
321
|
-
this.cursor = Array.from(item.apply).length;
|
|
322
|
-
} else if (typeof item.value === 'string') {
|
|
323
|
-
this.value = item.value;
|
|
324
|
-
this.cursor = Array.from(item.value).length;
|
|
325
|
-
}
|
|
326
|
-
this.repaint();
|
|
327
|
-
// 补全后把选择权交回用户:再按一次回车才提交
|
|
328
|
-
return;
|
|
541
|
+
this.submit();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (key.name === 'backspace') {
|
|
545
|
+
if (this.cursor > 0) {
|
|
546
|
+
chars.splice(this.cursor - 1, 1);
|
|
547
|
+
this.cursor -= 1;
|
|
548
|
+
this.value = chars.join('');
|
|
329
549
|
}
|
|
550
|
+
this.popup = null;
|
|
551
|
+
this.draw();
|
|
552
|
+
return;
|
|
330
553
|
}
|
|
554
|
+
// 其他字符:关闭菜单继续输入
|
|
555
|
+
this.popup = null;
|
|
556
|
+
this.draw();
|
|
331
557
|
}
|
|
332
558
|
|
|
333
559
|
/* --- 控制键 --- */
|
|
@@ -335,11 +561,10 @@ class LineEditor {
|
|
|
335
561
|
if (this.value) {
|
|
336
562
|
this.value = '';
|
|
337
563
|
this.cursor = 0;
|
|
338
|
-
this.
|
|
339
|
-
this.repaint();
|
|
564
|
+
this.draw();
|
|
340
565
|
return;
|
|
341
566
|
}
|
|
342
|
-
this.
|
|
567
|
+
this.clearDrawn();
|
|
343
568
|
this.active = false;
|
|
344
569
|
if (this.keyHandler) {
|
|
345
570
|
this.input.removeListener('keypress', this.keyHandler);
|
|
@@ -359,7 +584,7 @@ class LineEditor {
|
|
|
359
584
|
this.finished = false;
|
|
360
585
|
this.value = '';
|
|
361
586
|
this.cursor = 0;
|
|
362
|
-
this.
|
|
587
|
+
this.drawnRows = 0;
|
|
363
588
|
if (this.input.isTTY) {
|
|
364
589
|
try {
|
|
365
590
|
this.input.setRawMode(true);
|
|
@@ -371,7 +596,7 @@ class LineEditor {
|
|
|
371
596
|
this.keyQueue = this.keyQueue.then(() => this.handleKey(s || '', k || {})).catch(() => {});
|
|
372
597
|
};
|
|
373
598
|
this.input.on('keypress', this.keyHandler);
|
|
374
|
-
this.
|
|
599
|
+
this.draw();
|
|
375
600
|
return;
|
|
376
601
|
}
|
|
377
602
|
this.finish({ type: 'cancel', value: '' });
|
|
@@ -385,7 +610,7 @@ class LineEditor {
|
|
|
385
610
|
}
|
|
386
611
|
chars.splice(this.cursor, 1);
|
|
387
612
|
this.value = chars.join('');
|
|
388
|
-
this.
|
|
613
|
+
this.draw();
|
|
389
614
|
return;
|
|
390
615
|
}
|
|
391
616
|
|
|
@@ -393,14 +618,14 @@ class LineEditor {
|
|
|
393
618
|
chars.splice(0, this.cursor);
|
|
394
619
|
this.value = chars.join('');
|
|
395
620
|
this.cursor = 0;
|
|
396
|
-
this.
|
|
621
|
+
this.draw();
|
|
397
622
|
return;
|
|
398
623
|
}
|
|
399
624
|
|
|
400
625
|
if (ctrl && key.name === 'k') {
|
|
401
626
|
chars.splice(this.cursor);
|
|
402
627
|
this.value = chars.join('');
|
|
403
|
-
this.
|
|
628
|
+
this.draw();
|
|
404
629
|
return;
|
|
405
630
|
}
|
|
406
631
|
|
|
@@ -411,104 +636,111 @@ class LineEditor {
|
|
|
411
636
|
chars.splice(i, this.cursor - i);
|
|
412
637
|
this.cursor = i;
|
|
413
638
|
this.value = chars.join('');
|
|
414
|
-
this.
|
|
639
|
+
this.draw();
|
|
415
640
|
return;
|
|
416
641
|
}
|
|
417
642
|
|
|
418
643
|
if (ctrl && key.name === 'a') {
|
|
419
644
|
this.cursor = 0;
|
|
420
|
-
this.
|
|
645
|
+
this.draw();
|
|
421
646
|
return;
|
|
422
647
|
}
|
|
423
648
|
|
|
424
649
|
if (ctrl && key.name === 'e') {
|
|
425
650
|
this.cursor = chars.length;
|
|
426
|
-
this.
|
|
651
|
+
this.draw();
|
|
427
652
|
return;
|
|
428
653
|
}
|
|
429
654
|
|
|
430
655
|
if (ctrl && key.name === 'l') {
|
|
431
|
-
this.output
|
|
432
|
-
this.
|
|
433
|
-
this.
|
|
656
|
+
clearScreen(this.output);
|
|
657
|
+
this.drawnRows = 0;
|
|
658
|
+
this.draw();
|
|
434
659
|
return;
|
|
435
660
|
}
|
|
436
661
|
|
|
437
|
-
/* --- 回车 --- */
|
|
438
|
-
if (
|
|
662
|
+
/* --- 回车 / 续行 --- */
|
|
663
|
+
if (isSubmit) {
|
|
439
664
|
if (this.multiline && chars[this.cursor - 1] === '\\') {
|
|
440
665
|
chars.splice(this.cursor - 1, 1, '\n');
|
|
441
666
|
this.value = chars.join('');
|
|
442
|
-
this.
|
|
667
|
+
this.draw();
|
|
443
668
|
return;
|
|
444
669
|
}
|
|
445
670
|
this.submit();
|
|
446
671
|
return;
|
|
447
672
|
}
|
|
448
673
|
|
|
449
|
-
/* ---
|
|
450
|
-
if (key.name === '
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
this.value =
|
|
455
|
-
this.
|
|
674
|
+
/* --- 方向键 --- */
|
|
675
|
+
if (key.name === 'up') {
|
|
676
|
+
// ↑ 回填上一条用户提示词
|
|
677
|
+
const previous = this.previousEntry();
|
|
678
|
+
if (previous && !this.value) {
|
|
679
|
+
this.value = previous;
|
|
680
|
+
this.cursor = Array.from(previous).length;
|
|
681
|
+
this.draw();
|
|
456
682
|
}
|
|
457
683
|
return;
|
|
458
684
|
}
|
|
459
|
-
if (key.name === '
|
|
460
|
-
if (this.
|
|
461
|
-
|
|
462
|
-
this.value =
|
|
463
|
-
this.
|
|
685
|
+
if (key.name === 'down') {
|
|
686
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
687
|
+
this.historyIndex += 1;
|
|
688
|
+
this.value = this.history[this.historyIndex] || '';
|
|
689
|
+
this.cursor = Array.from(this.value).length;
|
|
690
|
+
} else {
|
|
691
|
+
this.historyIndex = this.history.length;
|
|
692
|
+
this.value = this.draft;
|
|
693
|
+
this.cursor = Array.from(this.value).length;
|
|
464
694
|
}
|
|
695
|
+
this.draw();
|
|
465
696
|
return;
|
|
466
697
|
}
|
|
467
698
|
if (key.name === 'left') {
|
|
468
699
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
469
|
-
this.
|
|
470
|
-
this.repaint();
|
|
700
|
+
this.draw();
|
|
471
701
|
return;
|
|
472
702
|
}
|
|
473
703
|
if (key.name === 'right') {
|
|
474
704
|
this.cursor = Math.min(chars.length, this.cursor + 1);
|
|
475
|
-
this.
|
|
476
|
-
this.repaint();
|
|
705
|
+
this.draw();
|
|
477
706
|
return;
|
|
478
707
|
}
|
|
479
708
|
if (key.name === 'home') {
|
|
480
709
|
this.cursor = 0;
|
|
481
|
-
this.
|
|
710
|
+
this.draw();
|
|
482
711
|
return;
|
|
483
712
|
}
|
|
484
713
|
if (key.name === 'end') {
|
|
485
714
|
this.cursor = chars.length;
|
|
486
|
-
this.
|
|
715
|
+
this.draw();
|
|
487
716
|
return;
|
|
488
717
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
718
|
+
|
|
719
|
+
/* --- 删除 --- */
|
|
720
|
+
if (key.name === 'backspace') {
|
|
721
|
+
if (this.cursor > 0) {
|
|
722
|
+
chars.splice(this.cursor - 1, 1);
|
|
723
|
+
this.cursor -= 1;
|
|
724
|
+
this.value = chars.join('');
|
|
725
|
+
this.draw();
|
|
726
|
+
}
|
|
497
727
|
return;
|
|
498
728
|
}
|
|
499
|
-
if (key.name === '
|
|
500
|
-
if (this.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
this.repaint();
|
|
729
|
+
if (key.name === 'delete') {
|
|
730
|
+
if (this.cursor < chars.length) {
|
|
731
|
+
chars.splice(this.cursor, 1);
|
|
732
|
+
this.value = chars.join('');
|
|
733
|
+
this.draw();
|
|
734
|
+
}
|
|
506
735
|
return;
|
|
507
736
|
}
|
|
737
|
+
|
|
738
|
+
/* --- Tab:打开或切换菜单 --- */
|
|
508
739
|
if (key.name === 'tab') {
|
|
509
|
-
this.
|
|
740
|
+
if (!this.popup) this.openMenu();
|
|
510
741
|
return;
|
|
511
742
|
}
|
|
743
|
+
|
|
512
744
|
if (key.name === 'escape') {
|
|
513
745
|
return;
|
|
514
746
|
}
|
|
@@ -518,7 +750,7 @@ class LineEditor {
|
|
|
518
750
|
const filtered = String(str).replace(/[\r\n]/g, '');
|
|
519
751
|
if (filtered) {
|
|
520
752
|
this.insert(filtered);
|
|
521
|
-
this.
|
|
753
|
+
this.draw();
|
|
522
754
|
}
|
|
523
755
|
}
|
|
524
756
|
}
|
|
@@ -528,15 +760,36 @@ class LineEditor {
|
|
|
528
760
|
this.value = '';
|
|
529
761
|
this.cursor = 0;
|
|
530
762
|
this.popup = null;
|
|
763
|
+
this.historyIndex = this.history.length;
|
|
764
|
+
this.draft = '';
|
|
531
765
|
this.finish({ type: 'submit', value });
|
|
532
766
|
}
|
|
533
767
|
}
|
|
534
768
|
|
|
769
|
+
/* ------------------------------ 文本折行 ------------------------------ */
|
|
770
|
+
|
|
771
|
+
/** 按显示宽度把文本折成多行 */
|
|
772
|
+
function wrapDisplay(text, width) {
|
|
773
|
+
const limit = Math.max(4, width);
|
|
774
|
+
const out = [];
|
|
775
|
+
let line = '';
|
|
776
|
+
let lineWidth = 0;
|
|
777
|
+
for (const char of String(text ?? '')) {
|
|
778
|
+
const w = displayWidth(char);
|
|
779
|
+
if (lineWidth + w > limit) {
|
|
780
|
+
out.push(line);
|
|
781
|
+
line = '';
|
|
782
|
+
lineWidth = 0;
|
|
783
|
+
}
|
|
784
|
+
line += char;
|
|
785
|
+
lineWidth += w;
|
|
786
|
+
}
|
|
787
|
+
out.push(line);
|
|
788
|
+
return out.length ? out : [''];
|
|
789
|
+
}
|
|
790
|
+
|
|
535
791
|
/* ------------------------------ 非 TTY 回退 ------------------------------ */
|
|
536
792
|
|
|
537
|
-
/**
|
|
538
|
-
* 非 TTY(管道 / 重定向)用的极简行读取:不做任何光标控制。
|
|
539
|
-
*/
|
|
540
793
|
function createLineReader(stream = process.stdin) {
|
|
541
794
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
542
795
|
const iterator = rl[Symbol.asyncIterator]();
|
|
@@ -554,6 +807,7 @@ function createLineReader(stream = process.stdin) {
|
|
|
554
807
|
},
|
|
555
808
|
setAbortHandler() {},
|
|
556
809
|
history: [],
|
|
810
|
+
lastUserPrompt: '',
|
|
557
811
|
pushHistory() {},
|
|
558
812
|
suspend() {},
|
|
559
813
|
resume() {},
|
|
@@ -571,4 +825,4 @@ function createLineReader(stream = process.stdin) {
|
|
|
571
825
|
};
|
|
572
826
|
}
|
|
573
827
|
|
|
574
|
-
module.exports = { LineEditor, createLineReader };
|
|
828
|
+
module.exports = { LineEditor, createLineReader, detectCapabilities, wrapDisplay, t };
|