cli-whatsapp 0.1.16 → 0.1.18

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
- const VISIBLE = 12; // rows shown at once
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: sets up keypress capture (stealing readline's
20
- * handlers for the duration), renders below the prompt, and restores everything
21
- * on exit. `render` draws the overlay; `makeOnKey(done)` returns the keypress
22
- * handler and calls `done(result)` to close. No-op (null) on a non-TTY.
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(render, makeOnKey) {
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
- process.stdout.write('\x1b7'); // save cursor at the prompt
34
- render();
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
- process.stdout.write('\x1b8\x1b[0J'); // restore cursor, wipe overlay
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 render = () => {
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
- process.stdout.write('\x1b8\x1b[0J\n' + rows.join('\n'));
132
+ return rows;
108
133
  };
109
- return runOverlay(render, (done) => (str, key) => {
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)), render());
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)), render());
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 render();
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 render();
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 render();
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 render();
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 render = () => {
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
- process.stdout.write('\x1b8\x1b[0J\n' + rows.join('\n'));
225
+ return rows;
201
226
  };
202
- return runOverlay(render, (done) => (str, key) => {
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)), render());
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)), render());
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 render();
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 render();
254
+ return repaint();
230
255
  }
231
256
  });
232
257
  }
@@ -521,9 +521,9 @@ async function handle(state, raw) {
521
521
  print(c.dim('left chat.'));
522
522
  return;
523
523
  case 'chats':
524
- chats({ limit: args[0] });
524
+ chats({ limit: args[0] ?? '20' });
525
525
  refreshNames();
526
- return;
526
+ return; // top 20 recent by default
527
527
  case 'recent':
528
528
  recent({ limit: args[0] });
529
529
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-whatsapp",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "A lightweight, fast, terminal-first WhatsApp client. Read, search, send, media, groups, and live notifications — from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {