jav-manager 0.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.
@@ -0,0 +1,551 @@
1
+ "use strict";
2
+ /**
3
+ * CLI display utilities — rich terminal output via ANSI escape sequences.
4
+ *
5
+ * Zero external dependencies. Works on modern terminals (Windows Terminal,
6
+ * iTerm2, most Linux terminals). Gracefully degrades when NO_COLOR is set.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.c = c;
10
+ exports.printBanner = printBanner;
11
+ exports.printHelp = printHelp;
12
+ exports.printTorrentList = printTorrentList;
13
+ exports.printSearchResultList = printSearchResultList;
14
+ exports.printHealthResults = printHealthResults;
15
+ exports.printConfig = printConfig;
16
+ exports.printCacheStats = printCacheStats;
17
+ exports.printDownloadList = printDownloadList;
18
+ exports.printLocalFiles = printLocalFiles;
19
+ exports.printSearching = printSearching;
20
+ exports.printSuccess = printSuccess;
21
+ exports.printError = printError;
22
+ exports.printWarning = printWarning;
23
+ exports.printInfo = printInfo;
24
+ exports.printMagnetLink = printMagnetLink;
25
+ exports.getPrompt = getPrompt;
26
+ exports.formatSize = formatBytes;
27
+ // ─── ANSI primitives ────────────────────────────────────────────────
28
+ const isColorDisabled = () => !!process.env.NO_COLOR || process.env.TERM === "dumb";
29
+ function esc(code) {
30
+ return isColorDisabled() ? "" : `\x1b[${code}m`;
31
+ }
32
+ // Reset
33
+ const R = () => esc("0");
34
+ // Styles
35
+ const BOLD = () => esc("1");
36
+ const DIM = () => esc("2");
37
+ const ITALIC = () => esc("3");
38
+ const UNDERLINE = () => esc("4");
39
+ // Foreground colors
40
+ const FG_BLACK = () => esc("30");
41
+ const FG_RED = () => esc("31");
42
+ const FG_GREEN = () => esc("32");
43
+ const FG_YELLOW = () => esc("33");
44
+ const FG_BLUE = () => esc("34");
45
+ const FG_MAGENTA = () => esc("35");
46
+ const FG_CYAN = () => esc("36");
47
+ const FG_WHITE = () => esc("37");
48
+ const FG_GRAY = () => esc("90");
49
+ // Bright foreground
50
+ const FG_BRIGHT_RED = () => esc("91");
51
+ const FG_BRIGHT_GREEN = () => esc("92");
52
+ const FG_BRIGHT_YELLOW = () => esc("93");
53
+ const FG_BRIGHT_BLUE = () => esc("94");
54
+ const FG_BRIGHT_MAGENTA = () => esc("95");
55
+ const FG_BRIGHT_CYAN = () => esc("96");
56
+ const FG_BRIGHT_WHITE = () => esc("97");
57
+ // Background colors
58
+ const BG_BLACK = () => esc("40");
59
+ const BG_RED = () => esc("41");
60
+ const BG_GREEN = () => esc("42");
61
+ const BG_YELLOW = () => esc("43");
62
+ const BG_BLUE = () => esc("44");
63
+ const BG_MAGENTA = () => esc("45");
64
+ const BG_CYAN = () => esc("46");
65
+ // ─── Semantic helpers ───────────────────────────────────────────────
66
+ function c(text, ...codes) {
67
+ if (isColorDisabled())
68
+ return text;
69
+ return codes.map((fn) => fn()).join("") + text + R();
70
+ }
71
+ // Strip ANSI codes for width calculations
72
+ function stripAnsi(s) {
73
+ return s.replace(/\x1b\[[0-9;]*m/g, "");
74
+ }
75
+ function visibleLength(s) {
76
+ const plain = stripAnsi(s);
77
+ let width = 0;
78
+ for (const ch of Array.from(plain)) {
79
+ width += charWidth(ch);
80
+ }
81
+ return width;
82
+ }
83
+ function padEnd(s, width) {
84
+ const vl = visibleLength(s);
85
+ return vl >= width ? s : s + " ".repeat(width - vl);
86
+ }
87
+ function padStart(s, width) {
88
+ const vl = visibleLength(s);
89
+ return vl >= width ? s : " ".repeat(width - vl) + s;
90
+ }
91
+ function centerPad(s, width) {
92
+ const vl = visibleLength(s);
93
+ if (vl >= width)
94
+ return s;
95
+ const left = Math.floor((width - vl) / 2);
96
+ const right = width - vl - left;
97
+ return " ".repeat(left) + s + " ".repeat(right);
98
+ }
99
+ function truncateText(s, width) {
100
+ if (width <= 0)
101
+ return "";
102
+ if (visibleLength(s) <= width)
103
+ return s;
104
+ const chars = Array.from(s);
105
+ if (width <= 3) {
106
+ let out = "";
107
+ let used = 0;
108
+ for (const ch of chars) {
109
+ const w = charWidth(ch);
110
+ if (used + w > width)
111
+ break;
112
+ out += ch;
113
+ used += w;
114
+ }
115
+ return out;
116
+ }
117
+ const target = width - 3;
118
+ let out = "";
119
+ let used = 0;
120
+ for (const ch of chars) {
121
+ const w = charWidth(ch);
122
+ if (used + w > target)
123
+ break;
124
+ out += ch;
125
+ used += w;
126
+ }
127
+ return `${out}...`;
128
+ }
129
+ function charWidth(ch) {
130
+ const codePoint = ch.codePointAt(0);
131
+ if (!codePoint)
132
+ return 0;
133
+ if (isControl(codePoint) || isCombining(codePoint))
134
+ return 0;
135
+ return isFullWidth(codePoint) ? 2 : 1;
136
+ }
137
+ function isControl(codePoint) {
138
+ return (codePoint >= 0 && codePoint < 32) || (codePoint >= 0x7f && codePoint < 0xa0);
139
+ }
140
+ function isCombining(codePoint) {
141
+ return ((codePoint >= 0x0300 && codePoint <= 0x036f) || // Combining Diacritical Marks
142
+ (codePoint >= 0x1ab0 && codePoint <= 0x1aff) || // Combining Diacritical Marks Extended
143
+ (codePoint >= 0x1dc0 && codePoint <= 0x1dff) || // Combining Diacritical Marks Supplement
144
+ (codePoint >= 0x20d0 && codePoint <= 0x20ff) || // Combining Diacritical Marks for Symbols
145
+ (codePoint >= 0xfe20 && codePoint <= 0xfe2f) // Combining Half Marks
146
+ );
147
+ }
148
+ function isFullWidth(codePoint) {
149
+ return (codePoint >= 0x1100 && (codePoint <= 0x115f || // Hangul Jamo
150
+ codePoint === 0x2329 ||
151
+ codePoint === 0x232a ||
152
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) || // CJK ... Yi
153
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) || // Hangul Syllables
154
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) || // CJK Compatibility Ideographs
155
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) || // Vertical forms
156
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) || // CJK Compatibility Forms + Small Form Variants
157
+ (codePoint >= 0xff00 && codePoint <= 0xff60) || // Fullwidth Forms
158
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
159
+ (codePoint >= 0x1f300 && codePoint <= 0x1f64f) || // Emojis
160
+ (codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
161
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd)));
162
+ }
163
+ // ─── Box-drawing characters (Unicode) ───────────────────────────────
164
+ const BOX = {
165
+ tl: "\u250C", // ┌
166
+ tr: "\u2510", // ┐
167
+ bl: "\u2514", // └
168
+ br: "\u2518", // ┘
169
+ h: "\u2500", // ─
170
+ v: "\u2502", // │
171
+ lT: "\u251C", // ├
172
+ rT: "\u2524", // ┤
173
+ tT: "\u252C", // ┬
174
+ bT: "\u2534", // ┴
175
+ cross: "\u253C", // ┼
176
+ // Double-line variants for emphasis
177
+ dh: "\u2550", // ═
178
+ dv: "\u2551", // ║
179
+ dtl: "\u2554", // ╔
180
+ dtr: "\u2557", // ╗
181
+ dbl: "\u255A", // ╚
182
+ dbr: "\u255D", // ╝
183
+ dlT: "\u2560", // ╠
184
+ drT: "\u2563", // ╣
185
+ };
186
+ // ─── Terminal width ─────────────────────────────────────────────────
187
+ function getTermWidth() {
188
+ return process.stdout.columns || 80;
189
+ }
190
+ function clampWidth(desired) {
191
+ return Math.min(desired, getTermWidth());
192
+ }
193
+ // ─── Public components ──────────────────────────────────────────────
194
+ /**
195
+ * Print the app banner at startup.
196
+ */
197
+ function printBanner(appName, version) {
198
+ const w = clampWidth(56);
199
+ const inner = w - 4; // inside the double-line box
200
+ const titleText = `${appName} v${version}`;
201
+ const subtitle = "Content Management CLI";
202
+ const top = `${c(BOX.dtl + BOX.dh.repeat(w - 2) + BOX.dtr, FG_CYAN)}`;
203
+ const bot = `${c(BOX.dbl + BOX.dh.repeat(w - 2) + BOX.dbr, FG_CYAN)}`;
204
+ const mid = `${c(BOX.dlT + BOX.dh.repeat(w - 2) + BOX.drT, FG_CYAN)}`;
205
+ const vb = c(BOX.dv, FG_CYAN);
206
+ const empty = `${vb} ${" ".repeat(inner)} ${vb}`;
207
+ console.log("");
208
+ console.log(top);
209
+ console.log(empty);
210
+ console.log(`${vb} ${centerPad(c(titleText, BOLD, FG_BRIGHT_WHITE), inner)} ${vb}`);
211
+ console.log(`${vb} ${centerPad(c(subtitle, DIM, FG_GRAY), inner)} ${vb}`);
212
+ console.log(empty);
213
+ console.log(mid);
214
+ // Minimal keyboard hints
215
+ const hints = [
216
+ `${c("JAV-ID", FG_BRIGHT_CYAN)} search & download`,
217
+ `${c("help", FG_BRIGHT_CYAN)} show commands`,
218
+ `${c("quit", FG_BRIGHT_CYAN)} exit`,
219
+ ];
220
+ for (const hint of hints) {
221
+ console.log(`${vb} ${padEnd(" " + hint, inner)} ${vb}`);
222
+ }
223
+ console.log(bot);
224
+ console.log("");
225
+ }
226
+ /**
227
+ * Print structured help with category groupings.
228
+ */
229
+ function printHelp(commands) {
230
+ const w = clampWidth(62);
231
+ const inner = w - 4;
232
+ const headerLine = c(BOX.h.repeat(w), FG_CYAN);
233
+ console.log("");
234
+ console.log(headerLine);
235
+ console.log(c(" COMMANDS", BOLD, FG_BRIGHT_WHITE));
236
+ console.log(headerLine);
237
+ // Group by category
238
+ const categories = new Map();
239
+ for (const item of commands) {
240
+ if (!categories.has(item.category)) {
241
+ categories.set(item.category, []);
242
+ }
243
+ categories.get(item.category).push({ cmd: item.cmd, desc: item.desc });
244
+ }
245
+ for (const [cat, items] of categories) {
246
+ console.log("");
247
+ console.log(` ${c(cat, BOLD, FG_YELLOW)}`);
248
+ for (const item of items) {
249
+ const cmdPart = c(item.cmd, FG_BRIGHT_CYAN);
250
+ console.log(` ${padEnd(cmdPart, 22)} ${c(item.desc, FG_WHITE)}`);
251
+ }
252
+ }
253
+ console.log("");
254
+ console.log(headerLine);
255
+ console.log("");
256
+ }
257
+ /**
258
+ * Display search results as a formatted, numbered table.
259
+ */
260
+ function printTorrentList(javId, torrents, legend) {
261
+ if (torrents.length === 0)
262
+ return;
263
+ const totalWidth = getTermWidth();
264
+ let idxW = 3;
265
+ let sizeW = 10;
266
+ let tagsW = 14;
267
+ const colCount = 4;
268
+ const baseOverhead = 2 + 1 + colCount * 2 + (colCount - 1) + 2; // indent + borders/padding
269
+ let titleW = totalWidth - baseOverhead - idxW - sizeW - tagsW;
270
+ if (titleW < 16) {
271
+ tagsW = 10;
272
+ titleW = totalWidth - baseOverhead - idxW - sizeW - tagsW;
273
+ }
274
+ if (titleW < 16) {
275
+ sizeW = 8;
276
+ titleW = totalWidth - baseOverhead - idxW - sizeW - tagsW;
277
+ }
278
+ titleW = Math.max(16, titleW);
279
+ const top = c(BOX.tl + BOX.h.repeat(idxW + 2) + BOX.tT + BOX.h.repeat(titleW + 2) + BOX.tT + BOX.h.repeat(sizeW + 2) + BOX.tT + BOX.h.repeat(tagsW + 2) + BOX.tr, FG_GRAY);
280
+ const mid = c(BOX.lT + BOX.h.repeat(idxW + 2) + BOX.cross + BOX.h.repeat(titleW + 2) + BOX.cross + BOX.h.repeat(sizeW + 2) + BOX.cross + BOX.h.repeat(tagsW + 2) + BOX.rT, FG_GRAY);
281
+ const bot = c(BOX.bl + BOX.h.repeat(idxW + 2) + BOX.bT + BOX.h.repeat(titleW + 2) + BOX.bT + BOX.h.repeat(sizeW + 2) + BOX.bT + BOX.h.repeat(tagsW + 2) + BOX.br, FG_GRAY);
282
+ console.log("");
283
+ console.log(` ${c("Results for", DIM, FG_GRAY)} ${c(javId, BOLD, FG_BRIGHT_WHITE)}`);
284
+ console.log(` ${top}`);
285
+ console.log(` ${c(BOX.v, FG_GRAY)} ${padStart(c("#", BOLD, FG_BRIGHT_WHITE), idxW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Title", BOLD, FG_BRIGHT_WHITE), titleW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Size", BOLD, FG_BRIGHT_WHITE), sizeW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Tags", BOLD, FG_BRIGHT_WHITE), tagsW)} ${c(BOX.v, FG_GRAY)}`);
286
+ console.log(` ${mid}`);
287
+ for (let i = 0; i < torrents.length; i++) {
288
+ const t = torrents[i];
289
+ const idx = String(i + 1);
290
+ const title = truncateText(t.title, titleW);
291
+ const size = t.size !== "-" ? t.size : "-";
292
+ const tags = truncateText(t.tags.join(" "), tagsW);
293
+ console.log(` ${c(BOX.v, FG_GRAY)} ${padStart(c(idx, FG_GRAY), idxW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(title, FG_WHITE), titleW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(size, FG_BRIGHT_YELLOW), sizeW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(tags, FG_WHITE), tagsW)} ${c(BOX.v, FG_GRAY)}`);
294
+ }
295
+ console.log(` ${bot}`);
296
+ if (legend) {
297
+ console.log(` ${c(legend, DIM, FG_GRAY)}`);
298
+ }
299
+ }
300
+ /**
301
+ * Display searchable candidate items before showing torrents.
302
+ */
303
+ function printSearchResultList(query, items) {
304
+ if (items.length === 0)
305
+ return;
306
+ const totalWidth = getTermWidth();
307
+ let idxW = 3;
308
+ let javIdW = 12;
309
+ let sourceW = 8;
310
+ const colCount = 4;
311
+ const baseOverhead = 2 + 1 + colCount * 2 + (colCount - 1) + 2; // indent + borders/padding
312
+ let titleW = totalWidth - baseOverhead - idxW - javIdW - sourceW;
313
+ if (titleW < 18) {
314
+ javIdW = 10;
315
+ sourceW = 6;
316
+ titleW = totalWidth - baseOverhead - idxW - javIdW - sourceW;
317
+ }
318
+ titleW = Math.max(18, titleW);
319
+ const top = c(BOX.tl + BOX.h.repeat(idxW + 2) + BOX.tT + BOX.h.repeat(javIdW + 2) + BOX.tT + BOX.h.repeat(sourceW + 2) + BOX.tT + BOX.h.repeat(titleW + 2) + BOX.tr, FG_GRAY);
320
+ const mid = c(BOX.lT + BOX.h.repeat(idxW + 2) + BOX.cross + BOX.h.repeat(javIdW + 2) + BOX.cross + BOX.h.repeat(sourceW + 2) + BOX.cross + BOX.h.repeat(titleW + 2) + BOX.rT, FG_GRAY);
321
+ const bot = c(BOX.bl + BOX.h.repeat(idxW + 2) + BOX.bT + BOX.h.repeat(javIdW + 2) + BOX.bT + BOX.h.repeat(sourceW + 2) + BOX.bT + BOX.h.repeat(titleW + 2) + BOX.br, FG_GRAY);
322
+ console.log("");
323
+ console.log(` ${c("SEARCH RESULTS", BOLD, FG_BRIGHT_WHITE)} ${c(`(${items.length})`, DIM, FG_GRAY)} ${c(`for ${query}`, DIM, FG_GRAY)}`);
324
+ console.log(` ${top}`);
325
+ console.log(` ${c(BOX.v, FG_GRAY)} ${padStart(c("#", BOLD, FG_BRIGHT_WHITE), idxW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("JAV-ID", BOLD, FG_BRIGHT_WHITE), javIdW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Source", BOLD, FG_BRIGHT_WHITE), sourceW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Title", BOLD, FG_BRIGHT_WHITE), titleW)} ${c(BOX.v, FG_GRAY)}`);
326
+ console.log(` ${mid}`);
327
+ for (let i = 0; i < items.length; i++) {
328
+ const item = items[i];
329
+ const idx = String(i + 1);
330
+ const id = truncateText(item.javId || "-", javIdW);
331
+ const source = truncateText(item.source || "-", sourceW);
332
+ const title = truncateText(item.title || "", titleW);
333
+ console.log(` ${c(BOX.v, FG_GRAY)} ${padStart(c(idx, FG_GRAY), idxW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(id, FG_BRIGHT_CYAN, BOLD), javIdW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(source, FG_GRAY), sourceW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(title, FG_WHITE), titleW)} ${c(BOX.v, FG_GRAY)}`);
334
+ }
335
+ console.log(` ${bot}`);
336
+ }
337
+ /**
338
+ * Format a torrent tag badge.
339
+ */
340
+ function formatTag(tag) {
341
+ const upper = tag.toUpperCase();
342
+ if (upper.includes("UC") || upper.includes("UNCENSOR")) {
343
+ return c(` ${tag} `, BG_MAGENTA, FG_BRIGHT_WHITE, BOLD);
344
+ }
345
+ if (upper.includes("SUB") || upper.includes("字幕")) {
346
+ return c(` ${tag} `, BG_CYAN, FG_BLACK, BOLD);
347
+ }
348
+ if (upper.includes("HD") || upper.includes("4K") || upper.includes("1080")) {
349
+ return c(` ${tag} `, BG_BLUE, FG_BRIGHT_WHITE, BOLD);
350
+ }
351
+ return c(` ${tag} `, BG_BLACK, FG_GRAY);
352
+ }
353
+ /**
354
+ * Print health check results with status indicators.
355
+ */
356
+ function printHealthResults(results) {
357
+ const w = clampWidth(60);
358
+ const line = c(BOX.h.repeat(w), FG_GRAY);
359
+ console.log("");
360
+ console.log(` ${c("SERVICE HEALTH", BOLD, FG_BRIGHT_WHITE)}`);
361
+ console.log(line);
362
+ for (const r of results) {
363
+ const icon = r.healthy
364
+ ? c("\u25CF", FG_BRIGHT_GREEN) // ●
365
+ : c("\u25CF", FG_BRIGHT_RED); // ●
366
+ const status = r.healthy
367
+ ? c("OK", FG_BRIGHT_GREEN)
368
+ : c("FAIL", FG_BRIGHT_RED, BOLD);
369
+ const name = padEnd(c(r.name, FG_WHITE), 20);
370
+ const msg = r.message ? c(r.message, DIM, FG_GRAY) : "";
371
+ console.log(` ${icon} ${name} ${status} ${msg}`);
372
+ }
373
+ console.log(line);
374
+ console.log("");
375
+ }
376
+ /**
377
+ * Print config as a structured key-value display.
378
+ */
379
+ function printConfig(entries) {
380
+ const w = clampWidth(60);
381
+ const line = c(BOX.h.repeat(w), FG_GRAY);
382
+ console.log("");
383
+ console.log(` ${c("CONFIGURATION", BOLD, FG_BRIGHT_WHITE)}`);
384
+ console.log(line);
385
+ let lastSection = "";
386
+ for (const entry of entries) {
387
+ if (entry.section !== lastSection) {
388
+ if (lastSection)
389
+ console.log("");
390
+ console.log(` ${c(entry.section, BOLD, FG_YELLOW)}`);
391
+ lastSection = entry.section;
392
+ }
393
+ const key = padEnd(c(entry.key, FG_CYAN), 18);
394
+ const val = entry.masked
395
+ ? c(entry.value, DIM, FG_GRAY)
396
+ : (entry.value === "-" ? c("-", DIM, FG_GRAY) : c(entry.value, FG_WHITE));
397
+ console.log(` ${key} ${val}`);
398
+ }
399
+ console.log(line);
400
+ console.log("");
401
+ }
402
+ /**
403
+ * Print cache statistics.
404
+ */
405
+ function printCacheStats(totalJav, totalTorrents, sizeBytes) {
406
+ const w = clampWidth(50);
407
+ const line = c(BOX.h.repeat(w), FG_GRAY);
408
+ console.log("");
409
+ console.log(` ${c("CACHE STATISTICS", BOLD, FG_BRIGHT_WHITE)}`);
410
+ console.log(line);
411
+ console.log(` ${c("Items", FG_CYAN)} ${c(String(totalJav), FG_BRIGHT_WHITE, BOLD)}`);
412
+ console.log(` ${c("Torrents", FG_CYAN)} ${c(String(totalTorrents), FG_BRIGHT_WHITE, BOLD)}`);
413
+ console.log(` ${c("Size", FG_CYAN)} ${c(formatBytes(sizeBytes), FG_BRIGHT_YELLOW)}`);
414
+ console.log(line);
415
+ console.log("");
416
+ }
417
+ /**
418
+ * Print a download list.
419
+ */
420
+ function printDownloadList(downloads) {
421
+ if (downloads.length === 0) {
422
+ printInfo("No active downloads");
423
+ return;
424
+ }
425
+ const w = clampWidth(80);
426
+ const line = c(BOX.h.repeat(w), FG_GRAY);
427
+ console.log("");
428
+ console.log(` ${c("DOWNLOADS", BOLD, FG_BRIGHT_WHITE)} ${c(`(${downloads.length})`, DIM, FG_GRAY)}`);
429
+ console.log(line);
430
+ for (const d of downloads) {
431
+ const stateColor = getStateColor(d.state);
432
+ const icon = getStateIcon(d.state);
433
+ const name = c(d.name, FG_WHITE);
434
+ const size = c(d.size, FG_BRIGHT_YELLOW);
435
+ const state = c(d.state, ...stateColor);
436
+ console.log(` ${icon} ${name}`);
437
+ console.log(` ${size} ${state}`);
438
+ }
439
+ console.log(line);
440
+ console.log("");
441
+ }
442
+ /**
443
+ * Print local file results.
444
+ */
445
+ function printLocalFiles(files, options) {
446
+ if (files.length === 0)
447
+ return;
448
+ const startIndex = options?.startIndex ?? 0;
449
+ const totalCount = options?.totalCount ?? files.length;
450
+ const page = options?.page;
451
+ const totalPages = options?.totalPages;
452
+ const totalWidth = getTermWidth();
453
+ let idxW = 3;
454
+ let nameW = 24;
455
+ let sizeW = 10;
456
+ const colCount = 4;
457
+ const baseOverhead = 2 + 1 + colCount * 2 + (colCount - 1) + 2; // indent + borders/padding
458
+ let pathW = totalWidth - baseOverhead - idxW - nameW - sizeW;
459
+ if (pathW < 20) {
460
+ nameW = 18;
461
+ pathW = totalWidth - baseOverhead - idxW - nameW - sizeW;
462
+ }
463
+ if (pathW < 16) {
464
+ sizeW = 8;
465
+ pathW = totalWidth - baseOverhead - idxW - nameW - sizeW;
466
+ }
467
+ pathW = Math.max(16, pathW);
468
+ const top = c(BOX.tl + BOX.h.repeat(idxW + 2) + BOX.tT + BOX.h.repeat(nameW + 2) + BOX.tT + BOX.h.repeat(sizeW + 2) + BOX.tT + BOX.h.repeat(pathW + 2) + BOX.tr, FG_GRAY);
469
+ const mid = c(BOX.lT + BOX.h.repeat(idxW + 2) + BOX.cross + BOX.h.repeat(nameW + 2) + BOX.cross + BOX.h.repeat(sizeW + 2) + BOX.cross + BOX.h.repeat(pathW + 2) + BOX.rT, FG_GRAY);
470
+ const bot = c(BOX.bl + BOX.h.repeat(idxW + 2) + BOX.bT + BOX.h.repeat(nameW + 2) + BOX.bT + BOX.h.repeat(sizeW + 2) + BOX.bT + BOX.h.repeat(pathW + 2) + BOX.br, FG_GRAY);
471
+ console.log("");
472
+ const pageText = page && totalPages ? ` ${c(`Page ${page}/${totalPages}`, DIM, FG_GRAY)}` : "";
473
+ console.log(` ${c("LOCAL FILES", BOLD, FG_BRIGHT_WHITE)} ${c(`(${totalCount})`, DIM, FG_GRAY)}${pageText}`);
474
+ console.log(` ${top}`);
475
+ console.log(` ${c(BOX.v, FG_GRAY)} ${padStart(c("#", BOLD, FG_BRIGHT_WHITE), idxW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Name", BOLD, FG_BRIGHT_WHITE), nameW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Size", BOLD, FG_BRIGHT_WHITE), sizeW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Path", BOLD, FG_BRIGHT_WHITE), pathW)} ${c(BOX.v, FG_GRAY)}`);
476
+ console.log(` ${mid}`);
477
+ for (let i = 0; i < files.length; i++) {
478
+ const f = files[i];
479
+ const idx = String(startIndex + i + 1);
480
+ const name = truncateText(f.name || "-", nameW);
481
+ const size = f.size || "-";
482
+ const path = truncateText(f.path || "-", pathW);
483
+ console.log(` ${c(BOX.v, FG_GRAY)} ${padStart(c(idx, FG_GRAY), idxW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(name, FG_WHITE), nameW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(size, FG_BRIGHT_YELLOW), sizeW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(path, DIM, FG_GRAY), pathW)} ${c(BOX.v, FG_GRAY)}`);
484
+ }
485
+ console.log(` ${bot}`);
486
+ console.log("");
487
+ }
488
+ // ─── Status messages ────────────────────────────────────────────────
489
+ function printSearching(javId) {
490
+ console.log(` ${c("\u25B6", FG_BRIGHT_CYAN)} ${c("Searching", FG_WHITE)} ${c(javId, BOLD, FG_BRIGHT_WHITE)}${c("...", DIM, FG_GRAY)}`);
491
+ }
492
+ function printSuccess(msg) {
493
+ console.log(` ${c("\u2714", FG_BRIGHT_GREEN)} ${c(msg, FG_GREEN)}`);
494
+ }
495
+ function printError(msg) {
496
+ console.log(` ${c("\u2718", FG_BRIGHT_RED)} ${c(msg, FG_RED)}`);
497
+ }
498
+ function printWarning(msg) {
499
+ console.log(` ${c("\u26A0", FG_BRIGHT_YELLOW)} ${c(msg, FG_YELLOW)}`);
500
+ }
501
+ function printInfo(msg) {
502
+ console.log(` ${c("\u2022", FG_CYAN)} ${c(msg, FG_WHITE)}`);
503
+ }
504
+ function printMagnetLink(msg, link) {
505
+ console.log(` ${c("\u26A0", FG_BRIGHT_YELLOW)} ${c(msg, FG_YELLOW)}`);
506
+ console.log(` ${c(link, FG_BRIGHT_CYAN, BOLD, UNDERLINE)}`);
507
+ }
508
+ /**
509
+ * Styled prompt string for readline.
510
+ */
511
+ function getPrompt() {
512
+ return `${c("\u276F", FG_BRIGHT_CYAN)} `;
513
+ }
514
+ // ─── Internal helpers ───────────────────────────────────────────────
515
+ function formatBytes(bytes) {
516
+ if (bytes <= 0)
517
+ return "-";
518
+ if (bytes >= 1024 * 1024 * 1024)
519
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
520
+ if (bytes >= 1024 * 1024)
521
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
522
+ return `${(bytes / 1024).toFixed(2)} KB`;
523
+ }
524
+ function getStateColor(state) {
525
+ const lower = state.toLowerCase();
526
+ if (/download|metadl/i.test(lower))
527
+ return [FG_BRIGHT_CYAN, BOLD];
528
+ if (/stalled/i.test(lower))
529
+ return [FG_BRIGHT_YELLOW];
530
+ if (/upload|seed/i.test(lower))
531
+ return [FG_BRIGHT_GREEN];
532
+ if (/pause/i.test(lower))
533
+ return [FG_GRAY];
534
+ if (/error|missing/i.test(lower))
535
+ return [FG_BRIGHT_RED];
536
+ return [FG_WHITE];
537
+ }
538
+ function getStateIcon(state) {
539
+ const lower = state.toLowerCase();
540
+ if (/download|metadl/i.test(lower))
541
+ return c("\u25BC", FG_BRIGHT_CYAN); // ▼
542
+ if (/stalled/i.test(lower))
543
+ return c("\u25AC", FG_BRIGHT_YELLOW); // ▬
544
+ if (/upload|seed/i.test(lower))
545
+ return c("\u25B2", FG_BRIGHT_GREEN); // ▲
546
+ if (/pause/i.test(lower))
547
+ return c("\u25A0", FG_GRAY); // ■
548
+ if (/error|missing/i.test(lower))
549
+ return c("\u25CF", FG_BRIGHT_RED); // ●
550
+ return c("\u25CB", FG_WHITE); // ○
551
+ }