cli-whatsapp 0.1.16 → 0.1.19
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.
|
@@ -3,7 +3,9 @@ import { readdirSync, statSync } from 'node:fs';
|
|
|
3
3
|
import { resolve, join, relative } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { c } from "../../utils/format.js";
|
|
6
|
-
|
|
6
|
+
// Rows shown at once — capped so the whole overlay (list + ~6 lines of chrome)
|
|
7
|
+
// fits the terminal height and never has to scroll.
|
|
8
|
+
const VISIBLE = Math.max(4, Math.min(12, (process.stdout.rows || 24) - 9));
|
|
7
9
|
const WIDTH = 58; // inner box width
|
|
8
10
|
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
9
11
|
function pad(s, w) {
|
|
@@ -16,13 +18,16 @@ function highlight(s) {
|
|
|
16
18
|
}
|
|
17
19
|
const border = (l, r) => c.teal(l + '─'.repeat(WIDTH) + r);
|
|
18
20
|
/**
|
|
19
|
-
* Generic raw-mode overlay runner
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Generic raw-mode overlay runner. Draws the overlay on the lines *below* the
|
|
22
|
+
* prompt and repaints using only RELATIVE cursor moves (never absolute
|
|
23
|
+
* save/restore), so it stays correct even when drawing near the bottom of the
|
|
24
|
+
* screen scrolls the terminal — no stacking/ghosting. `buildRows()` returns the
|
|
25
|
+
* overlay's lines; `makeOnKey(done, repaint)` handles keys, calling `repaint()`
|
|
26
|
+
* after state changes and `done(result)` to close. No-op (null) on a non-TTY.
|
|
23
27
|
*/
|
|
24
|
-
function runOverlay(
|
|
28
|
+
function runOverlay(buildRows, makeOnKey) {
|
|
25
29
|
const input = process.stdin;
|
|
30
|
+
const out = process.stdout;
|
|
26
31
|
if (!input.isTTY)
|
|
27
32
|
return Promise.resolve(null);
|
|
28
33
|
readline.emitKeypressEvents(input);
|
|
@@ -30,20 +35,40 @@ function runOverlay(render, makeOnKey) {
|
|
|
30
35
|
input.setRawMode(true);
|
|
31
36
|
const saved = input.listeners('keypress');
|
|
32
37
|
input.removeAllListeners('keypress');
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
// Cursor rests at the overlay's top-left between paints. All moves are
|
|
39
|
+
// relative to it, which survives terminal scrolling.
|
|
40
|
+
let drawn = 0;
|
|
41
|
+
const paint = () => {
|
|
42
|
+
const rows = buildRows();
|
|
43
|
+
if (drawn === 0) {
|
|
44
|
+
out.write('\n'); // open the first overlay line just below the prompt
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
readline.cursorTo(out, 0);
|
|
48
|
+
readline.clearScreenDown(out); // wipe the previous overlay in place
|
|
49
|
+
}
|
|
50
|
+
out.write(rows.join('\n'));
|
|
51
|
+
if (rows.length > 1)
|
|
52
|
+
readline.moveCursor(out, 0, -(rows.length - 1));
|
|
53
|
+
readline.cursorTo(out, 0);
|
|
54
|
+
drawn = rows.length;
|
|
55
|
+
};
|
|
56
|
+
paint();
|
|
35
57
|
return new Promise((res) => {
|
|
36
58
|
let onKey;
|
|
37
59
|
const done = (result) => {
|
|
38
60
|
input.removeListener('keypress', onKey);
|
|
39
|
-
|
|
61
|
+
readline.cursorTo(out, 0);
|
|
62
|
+
readline.clearScreenDown(out); // wipe the overlay
|
|
63
|
+
readline.moveCursor(out, 0, -1); // back up onto the prompt line
|
|
64
|
+
readline.cursorTo(out, 0);
|
|
40
65
|
if (input.isTTY)
|
|
41
66
|
input.setRawMode(wasRaw);
|
|
42
67
|
for (const l of saved)
|
|
43
68
|
input.on('keypress', l);
|
|
44
69
|
res(result);
|
|
45
70
|
};
|
|
46
|
-
onKey = makeOnKey(done);
|
|
71
|
+
onKey = makeOnKey(done, paint);
|
|
47
72
|
input.on('keypress', onKey);
|
|
48
73
|
});
|
|
49
74
|
}
|
|
@@ -86,7 +111,7 @@ export function pickFile(startDir) {
|
|
|
86
111
|
return [{ name: '..', dir: true }, ...items];
|
|
87
112
|
};
|
|
88
113
|
let entries = load();
|
|
89
|
-
const
|
|
114
|
+
const buildRows = () => {
|
|
90
115
|
if (index >= entries.length)
|
|
91
116
|
index = Math.max(0, entries.length - 1);
|
|
92
117
|
scroll = clampScroll(index, scroll, entries.length);
|
|
@@ -104,17 +129,17 @@ export function pickFile(startDir) {
|
|
|
104
129
|
});
|
|
105
130
|
rows.push(border('╰', '╯'));
|
|
106
131
|
rows.push(c.dim(` @${filter}${c.gray('▏')} ↑↓ move · ⏎ ${entries[index]?.dir ? 'open' : 'attach'} · ⌫ up · esc cancel`));
|
|
107
|
-
|
|
132
|
+
return rows;
|
|
108
133
|
};
|
|
109
|
-
return runOverlay(
|
|
134
|
+
return runOverlay(buildRows, (done, repaint) => (str, key) => {
|
|
110
135
|
if (!key)
|
|
111
136
|
return;
|
|
112
137
|
if (key.name === 'escape' || (key.ctrl && key.name === 'c'))
|
|
113
138
|
return done(null);
|
|
114
139
|
if (key.name === 'up')
|
|
115
|
-
return void ((index = Math.max(0, index - 1)),
|
|
140
|
+
return void ((index = Math.max(0, index - 1)), repaint());
|
|
116
141
|
if (key.name === 'down')
|
|
117
|
-
return void ((index = Math.min(entries.length - 1, index + 1)),
|
|
142
|
+
return void ((index = Math.min(entries.length - 1, index + 1)), repaint());
|
|
118
143
|
if (key.name === 'return') {
|
|
119
144
|
const e = entries[index];
|
|
120
145
|
if (!e)
|
|
@@ -125,7 +150,7 @@ export function pickFile(startDir) {
|
|
|
125
150
|
index = 0;
|
|
126
151
|
scroll = 0;
|
|
127
152
|
entries = load();
|
|
128
|
-
return
|
|
153
|
+
return repaint();
|
|
129
154
|
}
|
|
130
155
|
const full = join(dir, e.name);
|
|
131
156
|
if (e.dir) {
|
|
@@ -134,7 +159,7 @@ export function pickFile(startDir) {
|
|
|
134
159
|
index = 0;
|
|
135
160
|
scroll = 0;
|
|
136
161
|
entries = load();
|
|
137
|
-
return
|
|
162
|
+
return repaint();
|
|
138
163
|
}
|
|
139
164
|
const rel = relative(process.cwd(), full);
|
|
140
165
|
return done(rel && !rel.startsWith('..') ? rel : full);
|
|
@@ -147,14 +172,14 @@ export function pickFile(startDir) {
|
|
|
147
172
|
index = 0;
|
|
148
173
|
scroll = 0;
|
|
149
174
|
entries = load();
|
|
150
|
-
return
|
|
175
|
+
return repaint();
|
|
151
176
|
}
|
|
152
177
|
if (str && str.length === 1 && str >= ' ' && !key.ctrl && !key.meta) {
|
|
153
178
|
filter += str;
|
|
154
179
|
index = 0;
|
|
155
180
|
scroll = 0;
|
|
156
181
|
entries = load();
|
|
157
|
-
return
|
|
182
|
+
return repaint();
|
|
158
183
|
}
|
|
159
184
|
});
|
|
160
185
|
}
|
|
@@ -174,7 +199,7 @@ export function pickFromList(title, allItems, initialFilter = '') {
|
|
|
174
199
|
};
|
|
175
200
|
let items = match();
|
|
176
201
|
const LEFT = 14; // width of the value column
|
|
177
|
-
const
|
|
202
|
+
const buildRows = () => {
|
|
178
203
|
if (index >= items.length)
|
|
179
204
|
index = Math.max(0, items.length - 1);
|
|
180
205
|
scroll = clampScroll(index, scroll, items.length);
|
|
@@ -197,17 +222,17 @@ export function pickFromList(title, allItems, initialFilter = '') {
|
|
|
197
222
|
});
|
|
198
223
|
rows.push(border('╰', '╯'));
|
|
199
224
|
rows.push(c.dim(` ${filter}${c.gray('▏')} ↑↓ move · ⏎ select · esc cancel`));
|
|
200
|
-
|
|
225
|
+
return rows;
|
|
201
226
|
};
|
|
202
|
-
return runOverlay(
|
|
227
|
+
return runOverlay(buildRows, (done, repaint) => (str, key) => {
|
|
203
228
|
if (!key)
|
|
204
229
|
return;
|
|
205
230
|
if (key.name === 'escape' || (key.ctrl && key.name === 'c'))
|
|
206
231
|
return done(null);
|
|
207
232
|
if (key.name === 'up')
|
|
208
|
-
return void ((index = Math.max(0, index - 1)),
|
|
233
|
+
return void ((index = Math.max(0, index - 1)), repaint());
|
|
209
234
|
if (key.name === 'down')
|
|
210
|
-
return void ((index = Math.min(items.length - 1, index + 1)),
|
|
235
|
+
return void ((index = Math.min(items.length - 1, index + 1)), repaint());
|
|
211
236
|
if (key.name === 'return') {
|
|
212
237
|
const it = items[index];
|
|
213
238
|
return done(it ? (it.id ?? it.value) : null);
|
|
@@ -219,14 +244,14 @@ export function pickFromList(title, allItems, initialFilter = '') {
|
|
|
219
244
|
index = 0;
|
|
220
245
|
scroll = 0;
|
|
221
246
|
items = match();
|
|
222
|
-
return
|
|
247
|
+
return repaint();
|
|
223
248
|
}
|
|
224
249
|
if (str && str.length === 1 && str >= ' ' && !key.ctrl && !key.meta) {
|
|
225
250
|
filter += str;
|
|
226
251
|
index = 0;
|
|
227
252
|
scroll = 0;
|
|
228
253
|
items = match();
|
|
229
|
-
return
|
|
254
|
+
return repaint();
|
|
230
255
|
}
|
|
231
256
|
});
|
|
232
257
|
}
|
|
@@ -20,6 +20,7 @@ import { notify } from "../../utils/notify.js";
|
|
|
20
20
|
// ---- tab completion --------------------------------------------------------
|
|
21
21
|
const SLASH_ITEMS = [
|
|
22
22
|
{ value: '/open', desc: 'open a chat — then just type to send', args: true },
|
|
23
|
+
{ value: '/switch', desc: 'switch to another chat/contact', args: true },
|
|
23
24
|
{ value: '/close', desc: 'leave the current chat' },
|
|
24
25
|
{ value: '/chats', desc: 'list conversations' },
|
|
25
26
|
{ value: '/recent', desc: 'recently active chats' },
|
|
@@ -47,7 +48,7 @@ const SLASH_ITEMS = [
|
|
|
47
48
|
];
|
|
48
49
|
const SLASH = SLASH_ITEMS.map((it) => it.value);
|
|
49
50
|
const CHAT_CMDS = new Set([
|
|
50
|
-
'/open', '/send', '/sendfile', '/pin', '/unpin', '/archive', '/unarchive', '/history',
|
|
51
|
+
'/open', '/switch', '/send', '/sendfile', '/pin', '/unpin', '/archive', '/unarchive', '/history',
|
|
51
52
|
]);
|
|
52
53
|
let nameCache = [];
|
|
53
54
|
function refreshNames() {
|
|
@@ -439,6 +440,7 @@ function setSetting(key, value) {
|
|
|
439
440
|
// ---- help ------------------------------------------------------------------
|
|
440
441
|
const HELP = `${c.bold('Commands')} ${c.dim('(slash optional for the first word)')}
|
|
441
442
|
${c.cyan('/open')} <chat> open a chat — then just type to send
|
|
443
|
+
${c.cyan('/switch')} <chat> jump to another chat/contact
|
|
442
444
|
${c.green('@')} open a file picker to attach (↑↓ to choose, ⏎ to pick)
|
|
443
445
|
${c.cyan('/close')} leave the current chat
|
|
444
446
|
${c.cyan('/chats')} · ${c.cyan('/recent')} · ${c.cyan('/contacts')} list chats / recent / contacts
|
|
@@ -511,6 +513,7 @@ async function handle(state, raw) {
|
|
|
511
513
|
banner();
|
|
512
514
|
return;
|
|
513
515
|
case 'open':
|
|
516
|
+
case 'switch':
|
|
514
517
|
if (need(args[0], 'chat'))
|
|
515
518
|
openChat(state, args.join(' '));
|
|
516
519
|
return;
|
|
@@ -521,9 +524,9 @@ async function handle(state, raw) {
|
|
|
521
524
|
print(c.dim('left chat.'));
|
|
522
525
|
return;
|
|
523
526
|
case 'chats':
|
|
524
|
-
chats({ limit: args[0] });
|
|
527
|
+
chats({ limit: args[0] ?? '20' });
|
|
525
528
|
refreshNames();
|
|
526
|
-
return;
|
|
529
|
+
return; // top 20 recent by default
|
|
527
530
|
case 'recent':
|
|
528
531
|
recent({ limit: args[0] });
|
|
529
532
|
return;
|
|
@@ -695,7 +698,7 @@ export async function shell() {
|
|
|
695
698
|
let pickerOpen = false;
|
|
696
699
|
// Commands whose <chat> argument gets the live chat picker. "final" = chat is
|
|
697
700
|
// the last arg (run on pick); "then" = chat then more input (send/sendfile).
|
|
698
|
-
const CHAT_FINAL = new Set(['open', 'history', 'pin', 'unpin', 'archive', 'unarchive']);
|
|
701
|
+
const CHAT_FINAL = new Set(['open', 'switch', 'history', 'pin', 'unpin', 'archive', 'unarchive']);
|
|
699
702
|
const CHAT_THEN = new Set(['send', 'sendfile']);
|
|
700
703
|
const clearLine = () => rl.write(null, { ctrl: true, name: 'u' });
|
|
701
704
|
// Open the chat picker for a command, then insert/run with the chosen chat.
|
|
@@ -761,7 +764,7 @@ export async function shell() {
|
|
|
761
764
|
}
|
|
762
765
|
// Space right after a chat command (typed manually) → open the chat picker.
|
|
763
766
|
if (str === ' ') {
|
|
764
|
-
const m = rl.line.match(/^\/(open|history|pin|unpin|archive|unarchive|send|sendfile) $/);
|
|
767
|
+
const m = rl.line.match(/^\/(open|switch|history|pin|unpin|archive|unarchive|send|sendfile) $/);
|
|
765
768
|
if (m) {
|
|
766
769
|
pickerOpen = true;
|
|
767
770
|
void (async () => {
|
package/package.json
CHANGED