@neuraiproject/neurai-depin-terminal 1.0.1 → 2.0.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.md +27 -8
- package/dist/index.cjs +143 -188
- package/package.json +11 -10
- package/src/config/ConfigManager.js +43 -26
- package/src/constants.js +18 -5
- package/src/domain/messageTypes.js +26 -0
- package/src/errors.js +17 -0
- package/src/index.js +129 -30
- package/src/messaging/MessagePoller.js +132 -3
- package/src/messaging/MessageSender.js +99 -41
- package/src/messaging/MessageStore.js +26 -0
- package/src/messaging/RecipientDirectory.js +186 -0
- package/src/ui/CharsmUI.js +690 -0
- package/src/ui/RecipientSelector.js +114 -0
- package/src/ui/TabManager.js +162 -0
- package/src/ui/render.js +173 -0
- package/src/utils.js +304 -43
- package/src/ui/TerminalUI.js +0 -272
- package/src/ui/components/ErrorOverlay.js +0 -99
- package/src/ui/components/InputBox.js +0 -63
- package/src/ui/components/MessageBox.js +0 -51
- package/src/ui/components/StatusBar.js +0 -32
- package/src/ui/components/TopBar.js +0 -63
package/src/utils.js
CHANGED
|
@@ -75,10 +75,10 @@ export function truncate(str, length, suffix = '...') {
|
|
|
75
75
|
*/
|
|
76
76
|
export function formatTimestamp(timestamp, timezone = 'UTC') {
|
|
77
77
|
const date = timestamp instanceof Date ? timestamp : new Date(timestamp * 1000);
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
// Handle UTC explicitly
|
|
80
80
|
if (timezone === 'UTC') {
|
|
81
|
-
return date.toLocaleTimeString('en-US', {
|
|
81
|
+
return date.toLocaleTimeString('en-US', {
|
|
82
82
|
timeZone: 'UTC',
|
|
83
83
|
hour12: false,
|
|
84
84
|
hour: '2-digit',
|
|
@@ -93,7 +93,7 @@ export function formatTimestamp(timestamp, timezone = 'UTC') {
|
|
|
93
93
|
// Create a new date shifted by the offset hours
|
|
94
94
|
// We use UTC for display to avoid local system timezone interference
|
|
95
95
|
const shiftedDate = new Date(date.getTime() + (offset * 60 * 60 * 1000));
|
|
96
|
-
return shiftedDate.toLocaleTimeString('en-US', {
|
|
96
|
+
return shiftedDate.toLocaleTimeString('en-US', {
|
|
97
97
|
timeZone: 'UTC',
|
|
98
98
|
hour12: false,
|
|
99
99
|
hour: '2-digit',
|
|
@@ -103,7 +103,7 @@ export function formatTimestamp(timestamp, timezone = 'UTC') {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// Fallback to UTC
|
|
106
|
-
return date.toLocaleTimeString('en-US', {
|
|
106
|
+
return date.toLocaleTimeString('en-US', {
|
|
107
107
|
timeZone: 'UTC',
|
|
108
108
|
hour12: false,
|
|
109
109
|
hour: '2-digit',
|
|
@@ -123,53 +123,260 @@ export function createMessageKey(hash, signature) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
*
|
|
126
|
+
* Drain stdin until silence is detected
|
|
127
|
+
* @param {Object} stdin - Stdin stream
|
|
128
|
+
* @param {number} silenceMs - Milliseconds of silence to wait for (default: 300)
|
|
129
|
+
* @param {number} maxWaitMs - Maximum time to wait in total (default: 2000)
|
|
130
|
+
* @returns {Promise<void>}
|
|
131
|
+
*/
|
|
132
|
+
export async function drainInput(stdin, silenceMs = 300, maxWaitMs = 2000) {
|
|
133
|
+
// If not TTY, we can't really drain in the same way, but we can try small read
|
|
134
|
+
if (!stdin.isTTY) {
|
|
135
|
+
if (stdin.readableLength > 0) stdin.read();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Enable raw mode to catch all chars
|
|
140
|
+
try {
|
|
141
|
+
stdin.setRawMode(true);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
// Ignore
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await new Promise(resolve => {
|
|
147
|
+
stdin.resume();
|
|
148
|
+
|
|
149
|
+
let silenceTimer;
|
|
150
|
+
let maxTimer;
|
|
151
|
+
|
|
152
|
+
const cleanup = () => {
|
|
153
|
+
if (silenceTimer) clearTimeout(silenceTimer);
|
|
154
|
+
if (maxTimer) clearTimeout(maxTimer);
|
|
155
|
+
stdin.removeAllListeners('data');
|
|
156
|
+
|
|
157
|
+
// Pause so we don't eat future input intended for others
|
|
158
|
+
stdin.pause();
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
stdin.setRawMode(false);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
// Ignore
|
|
164
|
+
}
|
|
165
|
+
resolve();
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const resetSilenceTimer = () => {
|
|
169
|
+
if (silenceTimer) clearTimeout(silenceTimer);
|
|
170
|
+
silenceTimer = setTimeout(cleanup, silenceMs);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// If we hit the max wait time, just force proceed
|
|
174
|
+
maxTimer = setTimeout(cleanup, maxWaitMs);
|
|
175
|
+
|
|
176
|
+
// Listen for ANY data and drain it
|
|
177
|
+
stdin.on('data', () => {
|
|
178
|
+
resetSilenceTimer();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Start the initial timer
|
|
182
|
+
resetSilenceTimer();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Final sanity check drain
|
|
186
|
+
if (stdin.readableLength > 0) {
|
|
187
|
+
stdin.read();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Read password from stdin with character masking (pure implementation without readline)
|
|
127
193
|
* @param {string} prompt - Prompt to display
|
|
128
194
|
* @param {string} [maskChar='*'] - Character to display for each typed character
|
|
129
195
|
* @returns {Promise<string>} The entered password
|
|
130
196
|
*/
|
|
131
|
-
export function readPassword(prompt, maskChar = '*') {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
197
|
+
export async function readPassword(prompt, maskChar = '*') {
|
|
198
|
+
const stdin = process.stdin;
|
|
199
|
+
|
|
200
|
+
// Use robust drain before starting
|
|
201
|
+
await drainInput(stdin, 200, 1000);
|
|
202
|
+
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
let onDataHandler = null;
|
|
205
|
+
|
|
206
|
+
// Comprehensive cleanup function to ensure stdin is in pristine state
|
|
207
|
+
const cleanup = (removeListener = true) => {
|
|
208
|
+
if (removeListener && onDataHandler) {
|
|
209
|
+
stdin.removeListener('data', onDataHandler);
|
|
210
|
+
onDataHandler = null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Remove ALL listeners to avoid any residual state
|
|
214
|
+
stdin.removeAllListeners('data');
|
|
215
|
+
|
|
216
|
+
if (stdin.isTTY) {
|
|
217
|
+
try {
|
|
218
|
+
stdin.setRawMode(false);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
// Ignore raw mode reset failures
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// DON'T pause stdin - leave it ready for next use
|
|
225
|
+
// CharsmUI will manage its own resume/pause
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Ensure stdin is in correct initial state
|
|
229
|
+
if (!stdin.isTTY) {
|
|
230
|
+
reject(new Error('stdin is not a TTY'));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Start fresh
|
|
235
|
+
stdin.removeAllListeners('data');
|
|
236
|
+
stdin.removeAllListeners('keypress');
|
|
136
237
|
stdin.resume();
|
|
238
|
+
|
|
239
|
+
// Set raw mode for character-by-character input
|
|
240
|
+
try {
|
|
241
|
+
stdin.setRawMode(true);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
reject(new Error(`Failed to set raw mode: ${error.message}`));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
137
247
|
stdin.setEncoding('utf8');
|
|
248
|
+
|
|
249
|
+
// Always mask password (show asterisks)
|
|
250
|
+
const shouldMask = true;
|
|
138
251
|
let password = '';
|
|
252
|
+
let done = false;
|
|
253
|
+
let escapeState = 'normal';
|
|
139
254
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
resolve(password);
|
|
150
|
-
break;
|
|
255
|
+
const finish = () => {
|
|
256
|
+
if (done) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
done = true;
|
|
260
|
+
cleanup(true);
|
|
261
|
+
process.stdout.write('\n');
|
|
262
|
+
resolve(password);
|
|
263
|
+
};
|
|
151
264
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
265
|
+
onDataHandler = (chunk) => {
|
|
266
|
+
for (const ch of chunk) {
|
|
267
|
+
if (done) {
|
|
155
268
|
break;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const codePoint = ch.charCodeAt(0);
|
|
156
272
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
273
|
+
// State machine for filtering ANSI escape sequences
|
|
274
|
+
if (escapeState === 'esc') {
|
|
275
|
+
if (ch === '[') {
|
|
276
|
+
escapeState = 'csi';
|
|
277
|
+
} else if (ch === ']') {
|
|
278
|
+
escapeState = 'osc';
|
|
279
|
+
} else {
|
|
280
|
+
escapeState = 'normal';
|
|
162
281
|
}
|
|
163
|
-
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
164
284
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
285
|
+
if (escapeState === 'csi') {
|
|
286
|
+
if (ch >= '@' && ch <= '~') {
|
|
287
|
+
escapeState = 'normal';
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (escapeState === 'osc') {
|
|
293
|
+
if (ch === '\x07') {
|
|
294
|
+
escapeState = 'normal';
|
|
295
|
+
} else if (codePoint === 0x9c) {
|
|
296
|
+
escapeState = 'normal';
|
|
297
|
+
} else if (ch === '\x1b') {
|
|
298
|
+
escapeState = 'osc-esc';
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (escapeState === 'osc-esc') {
|
|
304
|
+
if (ch === '\\') {
|
|
305
|
+
escapeState = 'normal';
|
|
306
|
+
} else if (ch !== '\x1b') {
|
|
307
|
+
escapeState = 'osc';
|
|
308
|
+
}
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Process actual characters
|
|
313
|
+
switch (ch) {
|
|
314
|
+
case KEY_CODES.ENTER:
|
|
315
|
+
case KEY_CODES.CARRIAGE_RETURN:
|
|
316
|
+
case KEY_CODES.CTRL_D:
|
|
317
|
+
finish();
|
|
318
|
+
return;
|
|
319
|
+
|
|
320
|
+
case KEY_CODES.CTRL_C:
|
|
321
|
+
cleanup(true);
|
|
322
|
+
process.stdout.write('\n');
|
|
323
|
+
resetTerminal();
|
|
324
|
+
process.exit(0);
|
|
325
|
+
|
|
326
|
+
case KEY_CODES.BACKSPACE:
|
|
327
|
+
case KEY_CODES.BACKSPACE_ALT:
|
|
328
|
+
if (password.length > 0) {
|
|
329
|
+
password = password.slice(0, -1);
|
|
330
|
+
if (shouldMask) {
|
|
331
|
+
process.stdout.write(TERMINAL.BACKSPACE);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
|
|
336
|
+
default:
|
|
337
|
+
// Start of escape sequence
|
|
338
|
+
if (ch === '\x1b') {
|
|
339
|
+
escapeState = 'esc';
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// C1 control characters (ignore)
|
|
344
|
+
if (codePoint === 0x9b) {
|
|
345
|
+
escapeState = 'csi';
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
if (codePoint === 0x9d) {
|
|
349
|
+
escapeState = 'osc';
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
if (codePoint >= 0x80 && codePoint <= 0x9f) {
|
|
353
|
+
// Ignore C1 control characters
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// C0 control characters (ignore except Enter, Backspace, Ctrl+C handled above)
|
|
358
|
+
if (codePoint < 0x20) {
|
|
359
|
+
// Ignore C0 control characters
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Valid printable character
|
|
364
|
+
password += ch;
|
|
365
|
+
if (shouldMask) {
|
|
366
|
+
process.stdout.write(maskChar);
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
169
370
|
}
|
|
170
371
|
};
|
|
171
372
|
|
|
172
|
-
|
|
373
|
+
// Attach listener FIRST so it can filter any incoming ANSI sequences
|
|
374
|
+
stdin.on('data', onDataHandler);
|
|
375
|
+
|
|
376
|
+
// Show prompt after a tiny delay
|
|
377
|
+
setTimeout(() => {
|
|
378
|
+
process.stdout.write(prompt);
|
|
379
|
+
}, 10);
|
|
173
380
|
});
|
|
174
381
|
}
|
|
175
382
|
|
|
@@ -186,12 +393,12 @@ export async function withSuppressedConsole(fn) {
|
|
|
186
393
|
const originalStdoutWrite = process.stdout.write;
|
|
187
394
|
const originalStderrWrite = process.stderr.write;
|
|
188
395
|
|
|
189
|
-
console.log = () => {};
|
|
190
|
-
console.warn = () => {};
|
|
191
|
-
console.error = () => {};
|
|
192
|
-
console.info = () => {};
|
|
193
|
-
process.stdout.write = () => {};
|
|
194
|
-
process.stderr.write = () => {};
|
|
396
|
+
console.log = () => { };
|
|
397
|
+
console.warn = () => { };
|
|
398
|
+
console.error = () => { };
|
|
399
|
+
console.info = () => { };
|
|
400
|
+
process.stdout.write = () => { };
|
|
401
|
+
process.stderr.write = () => { };
|
|
195
402
|
|
|
196
403
|
try {
|
|
197
404
|
return await fn();
|
|
@@ -213,6 +420,12 @@ export function resetTerminal() {
|
|
|
213
420
|
if (process.stdout.isTTY) {
|
|
214
421
|
try {
|
|
215
422
|
process.stdout.write(TERMINAL.EXIT_ALT_SCREEN);
|
|
423
|
+
// Disable mouse reporting (1000, 1002, 1003, 1006, 1015)
|
|
424
|
+
process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l');
|
|
425
|
+
// Disable bracketed paste (2004)
|
|
426
|
+
process.stdout.write('\x1b[?2004l');
|
|
427
|
+
// Disable focus tracking (1004)
|
|
428
|
+
process.stdout.write('\x1b[?1004l');
|
|
216
429
|
process.stdout.write(TERMINAL.SHOW_CURSOR);
|
|
217
430
|
process.stdout.write(TERMINAL.RESET_ATTRIBUTES);
|
|
218
431
|
process.stdout.write(TERMINAL.NEW_LINE);
|
|
@@ -231,6 +444,54 @@ export function resetTerminal() {
|
|
|
231
444
|
}
|
|
232
445
|
}
|
|
233
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Emergency terminal cleanup
|
|
449
|
+
* Called at startup to ensure terminal is in a clean state
|
|
450
|
+
*/
|
|
451
|
+
export function emergencyTerminalCleanup() {
|
|
452
|
+
if (process.stdin.isTTY) {
|
|
453
|
+
try {
|
|
454
|
+
// Remove any stale listeners
|
|
455
|
+
process.stdin.removeAllListeners('keypress');
|
|
456
|
+
process.stdin.removeAllListeners('data');
|
|
457
|
+
|
|
458
|
+
// Ensure raw mode is off
|
|
459
|
+
process.stdin.setRawMode(false);
|
|
460
|
+
|
|
461
|
+
// Resume stdin to allow reading buffered data
|
|
462
|
+
process.stdin.resume();
|
|
463
|
+
|
|
464
|
+
// Aggressively flush all buffered data (may need multiple reads)
|
|
465
|
+
let flushed = 0;
|
|
466
|
+
while (process.stdin.readableLength > 0 && flushed < 10) {
|
|
467
|
+
process.stdin.read();
|
|
468
|
+
flushed++;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Now pause for normal operation
|
|
472
|
+
process.stdin.pause();
|
|
473
|
+
} catch (err) {
|
|
474
|
+
// Ignore errors during cleanup
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (process.stdout.isTTY) {
|
|
479
|
+
try {
|
|
480
|
+
// Reset terminal attributes
|
|
481
|
+
process.stdout.write(TERMINAL.RESET_ATTRIBUTES);
|
|
482
|
+
// Disable mouse reporting (1000, 1002, 1003, 1006, 1015)
|
|
483
|
+
process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l');
|
|
484
|
+
// Disable bracketed paste (2004)
|
|
485
|
+
process.stdout.write('\x1b[?2004l');
|
|
486
|
+
// Disable focus tracking (1004)
|
|
487
|
+
process.stdout.write('\x1b[?1004l');
|
|
488
|
+
process.stdout.write(TERMINAL.SHOW_CURSOR);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
// Ignore errors
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
234
495
|
/**
|
|
235
496
|
* Parse RPC host from URL for display
|
|
236
497
|
* @param {string} url - Full RPC URL
|
|
@@ -277,8 +538,8 @@ export function validatePassword(password, minLength, maxLength) {
|
|
|
277
538
|
*/
|
|
278
539
|
export function isPubkeyRevealed(pubkeyResponse) {
|
|
279
540
|
return pubkeyResponse?.pubkey &&
|
|
280
|
-
|
|
281
|
-
|
|
541
|
+
pubkeyResponse?.revealed === 1 &&
|
|
542
|
+
pubkeyResponse.pubkey.trim().length > 0;
|
|
282
543
|
}
|
|
283
544
|
|
|
284
545
|
/**
|
package/src/ui/TerminalUI.js
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Terminal UI for Neurai DePIN Terminal
|
|
3
|
-
* Full-screen blessed-based interface with top bar, message area, input, and status bar
|
|
4
|
-
* @module TerminalUI
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import blessed from 'blessed';
|
|
8
|
-
import {
|
|
9
|
-
COLORS,
|
|
10
|
-
ICONS,
|
|
11
|
-
BLESSED_KEYS,
|
|
12
|
-
ADDRESS,
|
|
13
|
-
PRIVACY,
|
|
14
|
-
TIME
|
|
15
|
-
} from '../constants.js';
|
|
16
|
-
import { formatTimestamp, resetTerminal } from '../utils.js';
|
|
17
|
-
import { TopBar } from './components/TopBar.js';
|
|
18
|
-
import { MessageBox } from './components/MessageBox.js';
|
|
19
|
-
import { InputBox } from './components/InputBox.js';
|
|
20
|
-
import { StatusBar } from './components/StatusBar.js';
|
|
21
|
-
import { ErrorOverlay } from './components/ErrorOverlay.js';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Terminal UI manager using blessed library
|
|
25
|
-
*/
|
|
26
|
-
export class TerminalUI {
|
|
27
|
-
/**
|
|
28
|
-
* Create a new TerminalUI instance
|
|
29
|
-
* @param {Object} config - Configuration object
|
|
30
|
-
* @param {string} config.rpc_url - RPC server URL
|
|
31
|
-
* @param {string} config.token - DePIN token name
|
|
32
|
-
* @param {WalletManager} walletManager - Wallet manager instance
|
|
33
|
-
* @param {RpcService} rpcService - RPC service instance
|
|
34
|
-
*/
|
|
35
|
-
constructor(config, walletManager, rpcService) {
|
|
36
|
-
this.config = config;
|
|
37
|
-
this.walletManager = walletManager;
|
|
38
|
-
this.rpcService = rpcService;
|
|
39
|
-
this.myAddress = walletManager.getAddress();
|
|
40
|
-
this.sendCallback = null;
|
|
41
|
-
this.displayedMessages = [];
|
|
42
|
-
this.hasPrivacy = false;
|
|
43
|
-
this.encryptionType = PRIVACY.DEFAULT_ENCRYPTION;
|
|
44
|
-
this.totalMessages = 0;
|
|
45
|
-
this.lastConnectionStatus = false;
|
|
46
|
-
this.lastPollTime = null;
|
|
47
|
-
|
|
48
|
-
this.initializeScreen();
|
|
49
|
-
this.createComponents();
|
|
50
|
-
this.setupKeybindings();
|
|
51
|
-
this.inputBox.focus();
|
|
52
|
-
this.screen.render();
|
|
53
|
-
this.updateTopBar({ connected: false, lastPoll: null });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Initialize blessed screen
|
|
58
|
-
*/
|
|
59
|
-
initializeScreen() {
|
|
60
|
-
this.screen = blessed.screen({
|
|
61
|
-
smartCSR: true,
|
|
62
|
-
title: 'Neurai DePIN Terminal'
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Create UI components (top bar, message box, input box, status bar)
|
|
68
|
-
*/
|
|
69
|
-
createComponents() {
|
|
70
|
-
this.topBar = new TopBar(this.screen, this.config, this.myAddress);
|
|
71
|
-
this.messageBox = new MessageBox(this.screen);
|
|
72
|
-
this.inputBox = new InputBox(this.screen, (msg) => this.handleSend(msg));
|
|
73
|
-
this.statusBar = new StatusBar(this.screen);
|
|
74
|
-
this.errorOverlay = new ErrorOverlay(this.screen);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Show blocking error overlay
|
|
79
|
-
* @param {string[]} errors - List of error messages
|
|
80
|
-
*/
|
|
81
|
-
showBlockingErrors(errors) {
|
|
82
|
-
this.inputBox.disable();
|
|
83
|
-
this.errorOverlay.show(errors);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Clear blocking error overlay
|
|
88
|
-
*/
|
|
89
|
-
clearBlockingErrors() {
|
|
90
|
-
if (this.errorOverlay.isVisible()) {
|
|
91
|
-
this.errorOverlay.hide();
|
|
92
|
-
this.inputBox.enable();
|
|
93
|
-
this.inputBox.focus();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Setup keyboard bindings
|
|
99
|
-
*/
|
|
100
|
-
setupKeybindings() {
|
|
101
|
-
// Exit on Ctrl+C or Escape
|
|
102
|
-
this.screen.key(BLESSED_KEYS.QUIT, () => {
|
|
103
|
-
this.cleanup();
|
|
104
|
-
process.exit(0);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Update top bar with connection status and info
|
|
110
|
-
* @param {Object} status - Status object
|
|
111
|
-
* @param {boolean} status.connected - Connection status
|
|
112
|
-
* @param {Date|null} status.lastPoll - Last poll time
|
|
113
|
-
*/
|
|
114
|
-
updateTopBar(status) {
|
|
115
|
-
this.lastConnectionStatus = status.connected;
|
|
116
|
-
|
|
117
|
-
if (status.lastPoll) {
|
|
118
|
-
this.lastPollTime = status.lastPoll;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
this.topBar.update({
|
|
122
|
-
connected: status.connected,
|
|
123
|
-
lastPoll: status.lastPoll
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Format message line for display
|
|
129
|
-
* @param {Object} msg - Message object
|
|
130
|
-
* @returns {string} Formatted message line with blessed tags
|
|
131
|
-
*/
|
|
132
|
-
formatMessageLine(msg) {
|
|
133
|
-
const time = formatTimestamp(msg.timestamp, this.config.timezone);
|
|
134
|
-
const isMe = msg.sender === this.myAddress;
|
|
135
|
-
const senderLabel = isMe ? 'YOU' : msg.sender.slice(0, ADDRESS.TRUNCATE_LENGTH);
|
|
136
|
-
const color = isMe ? COLORS.MY_MESSAGE : COLORS.OTHER_MESSAGE;
|
|
137
|
-
|
|
138
|
-
return `{${color}}[${time}] ${senderLabel}: ${msg.message}{/}`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Add a new message to the display
|
|
143
|
-
* Maintains chronological order (oldest to newest)
|
|
144
|
-
* @param {Object} msg - Message object
|
|
145
|
-
* @param {string} msg.sender - Sender address
|
|
146
|
-
* @param {string} msg.message - Message content
|
|
147
|
-
* @param {number} msg.timestamp - Unix timestamp in seconds
|
|
148
|
-
* @param {string} msg.hash - Message hash
|
|
149
|
-
*/
|
|
150
|
-
addMessage(msg) {
|
|
151
|
-
this.displayedMessages.push(msg);
|
|
152
|
-
|
|
153
|
-
// Sort by timestamp (oldest to newest)
|
|
154
|
-
this.displayedMessages.sort((a, b) => a.timestamp - b.timestamp);
|
|
155
|
-
|
|
156
|
-
// Redraw all messages
|
|
157
|
-
this.redrawMessages();
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Redraw all messages in the message box
|
|
162
|
-
*/
|
|
163
|
-
redrawMessages() {
|
|
164
|
-
const formattedMessages = this.displayedMessages.map(msg => this.formatMessageLine(msg));
|
|
165
|
-
// Clear and rebuild message box content
|
|
166
|
-
this.messageBox.component.setContent(formattedMessages.join('\n'));
|
|
167
|
-
this.messageBox.component.setScrollPerc(100);
|
|
168
|
-
this.screen.render();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Show error message in message box
|
|
173
|
-
* @param {string} errorMsg - Error message
|
|
174
|
-
*/
|
|
175
|
-
showError(errorMsg) {
|
|
176
|
-
const line = `{${COLORS.ERROR}}[ERROR] ${errorMsg}{/}`;
|
|
177
|
-
this.messageBox.addMessage(line);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Show info message in message box
|
|
182
|
-
* @param {string} infoMsg - Info message
|
|
183
|
-
*/
|
|
184
|
-
showInfo(infoMsg) {
|
|
185
|
-
const line = `{${COLORS.INFO}}[INFO] ${infoMsg}{/}`;
|
|
186
|
-
this.messageBox.addMessage(line);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Show success message in message box
|
|
191
|
-
* @param {string} successMsg - Success message
|
|
192
|
-
*/
|
|
193
|
-
showSuccess(successMsg) {
|
|
194
|
-
const line = `{${COLORS.SUCCESS}}[${ICONS.SUCCESS}] ${successMsg}{/}`;
|
|
195
|
-
this.messageBox.addMessage(line);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Update status bar with send status
|
|
200
|
-
* @param {string} message - Status message
|
|
201
|
-
* @param {string} [type='info'] - Message type: 'success', 'error', or 'info'
|
|
202
|
-
*/
|
|
203
|
-
updateSendStatus(message, type = 'info') {
|
|
204
|
-
this.statusBar.update(message, type);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Clear status bar
|
|
209
|
-
*/
|
|
210
|
-
clearSendStatus() {
|
|
211
|
-
this.statusBar.update('');
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Handle send message action
|
|
216
|
-
* @param {string} message - Message to send
|
|
217
|
-
*/
|
|
218
|
-
handleSend(message) {
|
|
219
|
-
if (!message) return;
|
|
220
|
-
|
|
221
|
-
if (this.sendCallback) {
|
|
222
|
-
this.sendCallback(message);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Register callback for send action
|
|
228
|
-
* @param {Function} callback - Callback function(message)
|
|
229
|
-
*/
|
|
230
|
-
onSend(callback) {
|
|
231
|
-
this.sendCallback = callback;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Update pool information from depingetmsginfo RPC call
|
|
236
|
-
* @param {Object} poolInfo - Pool information
|
|
237
|
-
* @param {number} [poolInfo.messages] - Total messages in pool
|
|
238
|
-
* @param {string} [poolInfo.cipher] - Encryption cipher name
|
|
239
|
-
* @param {string} [poolInfo.depinpoolpkey] - Server privacy public key
|
|
240
|
-
*/
|
|
241
|
-
updatePoolInfo(poolInfo) {
|
|
242
|
-
if (poolInfo) {
|
|
243
|
-
this.totalMessages = poolInfo.messages || 0;
|
|
244
|
-
this.encryptionType = poolInfo.cipher || PRIVACY.DEFAULT_ENCRYPTION;
|
|
245
|
-
this.hasPrivacy = poolInfo.depinpoolpkey && poolInfo.depinpoolpkey !== PRIVACY.NO_KEY_VALUE;
|
|
246
|
-
|
|
247
|
-
this.topBar.setTotalMessages(this.totalMessages);
|
|
248
|
-
this.topBar.setEncryptionType(this.encryptionType);
|
|
249
|
-
|
|
250
|
-
// Refresh top bar
|
|
251
|
-
this.updateTopBar({
|
|
252
|
-
connected: this.lastConnectionStatus,
|
|
253
|
-
lastPoll: this.lastPollTime
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Cleanup terminal state
|
|
260
|
-
* Removes listeners, destroys screen, resets terminal
|
|
261
|
-
*/
|
|
262
|
-
cleanup() {
|
|
263
|
-
if (this.screen) {
|
|
264
|
-
try {
|
|
265
|
-
this.screen.destroy();
|
|
266
|
-
} catch (err) {
|
|
267
|
-
// Ignore cleanup errors
|
|
268
|
-
}
|
|
269
|
-
resetTerminal();
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|