novaprime 1.4.0 → 1.4.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/package.json +1 -1
- package/src/prompt.js +124 -11
package/package.json
CHANGED
package/src/prompt.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const readline = require('readline');
|
|
3
3
|
|
|
4
|
-
// Use Node's readline
|
|
4
|
+
// Use Node's readline for single-line prompts (login key, y/N) — it handles
|
|
5
|
+
// editing and history. The multi-line chat box uses a paste-aware reader below.
|
|
5
6
|
let rl = null;
|
|
6
7
|
function getRl() {
|
|
7
8
|
if (!rl) {
|
|
@@ -22,19 +23,131 @@ function confirm(message) {
|
|
|
22
23
|
return ask(message + ' (y/N) ').then((a) => /^y(es)?$/i.test((a || '').trim()));
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
function close() { if (rl) { rl.close(); rl = null; } }
|
|
27
|
+
|
|
28
|
+
// ---- bracketed-paste-aware chat input ----------------------------------
|
|
29
|
+
// Terminals that support bracketed paste wrap pasted text in these markers.
|
|
30
|
+
// We capture everything between them (newlines included) WITHOUT submitting,
|
|
31
|
+
// so pasting a big multi-line prompt no longer auto-sends on the first line.
|
|
32
|
+
const PASTE_START = '\x1b[200~';
|
|
33
|
+
const PASTE_END = '\x1b[201~';
|
|
34
|
+
|
|
35
|
+
function normalize(t) { return t.replace(/\r\n?/g, '\n'); }
|
|
36
|
+
|
|
37
|
+
// length of the longest suffix of s that is a prefix of marker (handles a
|
|
38
|
+
// marker that got split across two stdin chunks)
|
|
39
|
+
function partialTail(s, marker) {
|
|
40
|
+
const max = Math.min(s.length, marker.length - 1);
|
|
41
|
+
for (let n = max; n > 0; n--) {
|
|
42
|
+
if (marker.startsWith(s.slice(s.length - n))) return n;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Handle a run of plain (non-paste) bytes: typing, Enter, backspace, Ctrl+C/D,
|
|
48
|
+
// and swallow escape sequences (arrow keys, etc.) so they don't corrupt input.
|
|
49
|
+
function handlePlain(state, s, sink) {
|
|
50
|
+
for (let i = 0; i < s.length && !state.finished; i++) {
|
|
51
|
+
const ch = s[i];
|
|
52
|
+
if (ch === '\x03') { sink.cancel(); return; } // Ctrl+C
|
|
53
|
+
if (ch === '\x04') { if (!state.buf) { sink.cancel(); return; } continue; } // Ctrl+D on empty
|
|
54
|
+
if (ch === '\r' || ch === '\n') { sink.submit(state.buf); return; } // Enter -> send
|
|
55
|
+
if (ch === '\x7f' || ch === '\b') { // backspace
|
|
56
|
+
if (state.buf.length) {
|
|
57
|
+
const last = state.buf[state.buf.length - 1];
|
|
58
|
+
state.buf = state.buf.slice(0, -1);
|
|
59
|
+
if (last !== '\n') sink.out('\b \b');
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (ch === '\x1b') { // skip CSI/SS3 escape (arrows, fn keys)
|
|
64
|
+
if (s[i + 1] === '[' || s[i + 1] === 'O') {
|
|
65
|
+
i += 2;
|
|
66
|
+
while (i < s.length && !/[A-Za-z~]/.test(s[i])) i++;
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === '\t') { state.buf += ' '; sink.out(' '); continue; } // tab -> 2 spaces
|
|
71
|
+
if (ch >= ' ') { state.buf += ch; sink.out(ch); } // printable
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Pure state machine — testable. state = { buf, pasting, pending, finished }.
|
|
76
|
+
// sink = { out(str), submit(value), cancel() }.
|
|
77
|
+
function processChunk(state, chunk, sink) {
|
|
78
|
+
let s = state.pending + chunk;
|
|
79
|
+
state.pending = '';
|
|
80
|
+
while (s.length && !state.finished) {
|
|
81
|
+
if (state.pasting) {
|
|
82
|
+
const end = s.indexOf(PASTE_END);
|
|
83
|
+
if (end === -1) {
|
|
84
|
+
const hold = partialTail(s, PASTE_END);
|
|
85
|
+
const seg = normalize(s.slice(0, s.length - hold));
|
|
86
|
+
if (seg) { state.buf += seg; sink.out(seg); }
|
|
87
|
+
state.pending = s.slice(s.length - hold);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const seg = normalize(s.slice(0, end));
|
|
91
|
+
if (seg) { state.buf += seg; sink.out(seg); }
|
|
92
|
+
state.pasting = false;
|
|
93
|
+
s = s.slice(end + PASTE_END.length);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const start = s.indexOf(PASTE_START);
|
|
97
|
+
if (start === -1) {
|
|
98
|
+
const hold = partialTail(s, PASTE_START);
|
|
99
|
+
const plain = s.slice(0, s.length - hold);
|
|
100
|
+
handlePlain(state, plain, sink);
|
|
101
|
+
if (!state.finished) state.pending = s.slice(s.length - hold);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
handlePlain(state, s.slice(0, start), sink);
|
|
105
|
+
if (state.finished) return;
|
|
106
|
+
state.pasting = true;
|
|
107
|
+
s = s.slice(start + PASTE_START.length);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ttyRaw() { return process.stdin.isTTY && typeof process.stdin.setRawMode === 'function'; }
|
|
112
|
+
|
|
113
|
+
// Chat input box. Top border above; readline-free raw reader captures multi-line
|
|
114
|
+
// pastes; bottom border drawn after submit so the message shows as a complete box.
|
|
27
115
|
function boxInput(top, bottom, prompt) {
|
|
116
|
+
// Fallback for piped / non-TTY input (e.g. `echo ... | novaprime`): plain readline.
|
|
117
|
+
if (!ttyRaw()) {
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
process.stdout.write(top + '\n');
|
|
120
|
+
const r = getRl();
|
|
121
|
+
r.question(prompt, (answer) => { process.stdout.write(bottom + '\n'); resolve(answer); });
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (rl) { rl.close(); rl = null; } // release stdin from readline
|
|
125
|
+
|
|
28
126
|
return new Promise((resolve) => {
|
|
127
|
+
const stdin = process.stdin;
|
|
128
|
+
const state = { buf: '', pasting: false, pending: '', finished: false };
|
|
129
|
+
|
|
130
|
+
const onData = (chunk) => processChunk(state, chunk, sink);
|
|
131
|
+
const cleanup = () => {
|
|
132
|
+
try { process.stdout.write('\x1b[?2004l'); } catch (_) {}
|
|
133
|
+
try { stdin.setRawMode(false); } catch (_) {}
|
|
134
|
+
stdin.pause();
|
|
135
|
+
stdin.removeListener('data', onData);
|
|
136
|
+
};
|
|
137
|
+
const sink = {
|
|
138
|
+
out: (str) => process.stdout.write(str),
|
|
139
|
+
submit: (val) => { state.finished = true; cleanup(); process.stdout.write('\n' + bottom + '\n'); resolve(val); },
|
|
140
|
+
cancel: () => { state.finished = true; cleanup(); process.stdout.write('\n' + bottom + '\n'); resolve(null); },
|
|
141
|
+
};
|
|
142
|
+
|
|
29
143
|
process.stdout.write(top + '\n');
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
144
|
+
process.stdout.write(prompt);
|
|
145
|
+
stdin.setRawMode(true);
|
|
146
|
+
stdin.resume();
|
|
147
|
+
stdin.setEncoding('utf8');
|
|
148
|
+
process.stdout.write('\x1b[?2004h'); // enable bracketed paste
|
|
149
|
+
stdin.on('data', onData);
|
|
35
150
|
});
|
|
36
151
|
}
|
|
37
152
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
module.exports = { ask, confirm, boxInput, close };
|
|
153
|
+
module.exports = { ask, confirm, boxInput, close, processChunk };
|