@darksol/terminal 0.4.7 → 0.4.8
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/web/commands.js +200 -28
- package/src/web/server.js +22 -0
- package/src/web/terminal.js +121 -41
package/package.json
CHANGED
package/src/web/commands.js
CHANGED
|
@@ -69,6 +69,96 @@ const USDC_ADDRESSES = {
|
|
|
69
69
|
|
|
70
70
|
const ERC20_ABI = ['function balanceOf(address) view returns (uint256)'];
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Handle interactive menu selections from the client
|
|
74
|
+
*/
|
|
75
|
+
export async function handleMenuSelect(id, value, item, ws) {
|
|
76
|
+
switch (id) {
|
|
77
|
+
case 'wallet_select':
|
|
78
|
+
return await showWalletDetail(value, ws);
|
|
79
|
+
|
|
80
|
+
case 'wallet_action':
|
|
81
|
+
switch (value) {
|
|
82
|
+
case 'receive': {
|
|
83
|
+
const name = getConfig('activeWallet');
|
|
84
|
+
if (!name) return {};
|
|
85
|
+
const { loadWallet } = await import('../wallet/keystore.js');
|
|
86
|
+
const w = loadWallet(name);
|
|
87
|
+
ws.sendLine('');
|
|
88
|
+
ws.sendLine(`${ANSI.gold} ◆ RECEIVE — ${name}${ANSI.reset}`);
|
|
89
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
90
|
+
ws.sendLine('');
|
|
91
|
+
ws.sendLine(` ${ANSI.white}Your address:${ANSI.reset}`);
|
|
92
|
+
ws.sendLine('');
|
|
93
|
+
const addr = w.address;
|
|
94
|
+
ws.sendLine(` ${ANSI.dim}┌${'─'.repeat(addr.length + 4)}┐${ANSI.reset}`);
|
|
95
|
+
ws.sendLine(` ${ANSI.dim}│ ${ANSI.gold}${addr}${ANSI.dim} │${ANSI.reset}`);
|
|
96
|
+
ws.sendLine(` ${ANSI.dim}└${'─'.repeat(addr.length + 4)}┘${ANSI.reset}`);
|
|
97
|
+
ws.sendLine('');
|
|
98
|
+
ws.sendLine(` ${ANSI.dim}Works on ALL EVM chains: Base • Ethereum • Arbitrum • Optimism • Polygon${ANSI.reset}`);
|
|
99
|
+
ws.sendLine(` ${ANSI.red}Make sure the sender is on the same chain!${ANSI.reset}`);
|
|
100
|
+
ws.sendLine('');
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
case 'send':
|
|
104
|
+
ws.sendLine('');
|
|
105
|
+
ws.sendLine(` ${ANSI.gold}◆ SEND${ANSI.reset}`);
|
|
106
|
+
ws.sendLine(` ${ANSI.dim}Sending requires wallet password — use the CLI:${ANSI.reset}`);
|
|
107
|
+
ws.sendLine(` ${ANSI.gold}darksol send --to 0x... --amount 0.1 --token ETH${ANSI.reset}`);
|
|
108
|
+
ws.sendLine(` ${ANSI.gold}darksol send${ANSI.reset} ${ANSI.dim}(interactive mode)${ANSI.reset}`);
|
|
109
|
+
ws.sendLine('');
|
|
110
|
+
return {};
|
|
111
|
+
case 'portfolio':
|
|
112
|
+
return await handleCommand('portfolio', ws);
|
|
113
|
+
case 'history':
|
|
114
|
+
return await handleCommand('history', ws);
|
|
115
|
+
case 'switch': {
|
|
116
|
+
const chains = ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon'];
|
|
117
|
+
const current = getConfig('chain') || 'base';
|
|
118
|
+
ws.sendMenu('chain_select', '◆ Select Chain', chains.map(c => ({
|
|
119
|
+
value: c,
|
|
120
|
+
label: c === current ? `★ ${c}` : c,
|
|
121
|
+
desc: c === current ? 'current' : '',
|
|
122
|
+
})));
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
case 'back':
|
|
126
|
+
ws.sendLine('');
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case 'chain_select':
|
|
132
|
+
setConfig('chain', value);
|
|
133
|
+
ws.sendLine('');
|
|
134
|
+
ws.sendLine(` ${ANSI.green}✓ Chain set to ${value}${ANSI.reset}`);
|
|
135
|
+
ws.sendLine('');
|
|
136
|
+
return {};
|
|
137
|
+
|
|
138
|
+
case 'config_action':
|
|
139
|
+
if (value === 'chain') {
|
|
140
|
+
const chains = ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon'];
|
|
141
|
+
const current = getConfig('chain') || 'base';
|
|
142
|
+
ws.sendMenu('chain_select', '◆ Select Chain', chains.map(c => ({
|
|
143
|
+
value: c,
|
|
144
|
+
label: c === current ? `★ ${c}` : c,
|
|
145
|
+
desc: c === current ? 'current' : '',
|
|
146
|
+
})));
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
if (value === 'keys') {
|
|
150
|
+
return await handleCommand('keys', ws);
|
|
151
|
+
}
|
|
152
|
+
ws.sendLine('');
|
|
153
|
+
return {};
|
|
154
|
+
|
|
155
|
+
case 'main_menu':
|
|
156
|
+
return await handleCommand(value, ws);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
|
|
72
162
|
/**
|
|
73
163
|
* AI status check — shown on connection
|
|
74
164
|
*/
|
|
@@ -402,47 +492,121 @@ async function cmdMarket(args, ws) {
|
|
|
402
492
|
// WALLET
|
|
403
493
|
// ══════════════════════════════════════════════════
|
|
404
494
|
async function cmdWallet(args, ws) {
|
|
405
|
-
const sub = args[0]
|
|
495
|
+
const sub = args[0];
|
|
406
496
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
497
|
+
// If a specific subcommand, handle directly
|
|
498
|
+
if (sub === 'balance') {
|
|
499
|
+
return await showWalletDetail(args[1] || getConfig('activeWallet'), ws);
|
|
500
|
+
}
|
|
411
501
|
|
|
412
|
-
|
|
413
|
-
|
|
502
|
+
if (sub === 'use' && args[1]) {
|
|
503
|
+
setConfig('activeWallet', args[1]);
|
|
504
|
+
ws.sendLine(` ${ANSI.green}✓ Active wallet set to "${args[1]}"${ANSI.reset}`);
|
|
505
|
+
ws.sendLine('');
|
|
506
|
+
return {};
|
|
507
|
+
}
|
|
414
508
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const indicator = w === active ? `${ANSI.gold}► ${ANSI.reset}` : ' ';
|
|
420
|
-
ws.sendLine(` ${indicator}${ANSI.white}${w}${ANSI.reset}`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
509
|
+
// Default: interactive wallet picker
|
|
510
|
+
const { listWallets } = await import('../wallet/keystore.js');
|
|
511
|
+
const wallets = listWallets();
|
|
512
|
+
const active = getConfig('activeWallet');
|
|
423
513
|
|
|
514
|
+
if (wallets.length === 0) {
|
|
515
|
+
ws.sendLine(`${ANSI.gold} ◆ WALLETS${ANSI.reset}`);
|
|
516
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
517
|
+
ws.sendLine(` ${ANSI.dim}No wallets found.${ANSI.reset}`);
|
|
518
|
+
ws.sendLine(` ${ANSI.dim}Create one: ${ANSI.gold}darksol wallet create <name>${ANSI.reset}`);
|
|
424
519
|
ws.sendLine('');
|
|
425
520
|
return {};
|
|
426
521
|
}
|
|
427
522
|
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
|
|
523
|
+
if (wallets.length === 1) {
|
|
524
|
+
// Only one wallet — go straight to it
|
|
525
|
+
return await showWalletDetail(wallets[0].name, ws);
|
|
526
|
+
}
|
|
431
527
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
528
|
+
// Multiple wallets — show interactive menu
|
|
529
|
+
const menuItems = wallets.map(w => ({
|
|
530
|
+
value: w.name,
|
|
531
|
+
label: `${w.name === active ? '★ ' : ''}${w.name}`,
|
|
532
|
+
desc: `${w.address.slice(0, 6)}...${w.address.slice(-4)}`,
|
|
533
|
+
}));
|
|
534
|
+
|
|
535
|
+
ws.sendMenu('wallet_select', '◆ Select Wallet', menuItems);
|
|
536
|
+
return {};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function showWalletDetail(name, ws) {
|
|
540
|
+
if (!name) {
|
|
541
|
+
ws.sendLine(` ${ANSI.red}No wallet selected${ANSI.reset}`);
|
|
542
|
+
ws.sendLine('');
|
|
543
|
+
return {};
|
|
544
|
+
}
|
|
437
545
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
546
|
+
const { loadWallet } = await import('../wallet/keystore.js');
|
|
547
|
+
let walletData;
|
|
548
|
+
try {
|
|
549
|
+
walletData = loadWallet(name);
|
|
550
|
+
} catch {
|
|
551
|
+
ws.sendLine(` ${ANSI.red}Wallet "${name}" not found${ANSI.reset}`);
|
|
441
552
|
ws.sendLine('');
|
|
442
553
|
return {};
|
|
443
554
|
}
|
|
444
555
|
|
|
445
|
-
|
|
556
|
+
const chain = getConfig('chain') || 'base';
|
|
557
|
+
const provider = new ethers.JsonRpcProvider(RPCS[chain]);
|
|
558
|
+
|
|
559
|
+
ws.sendLine(`${ANSI.gold} ◆ WALLET — ${name}${ANSI.reset}`);
|
|
560
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
561
|
+
ws.sendLine(` ${ANSI.darkGold}Address${ANSI.reset} ${ANSI.white}${walletData.address}${ANSI.reset}`);
|
|
562
|
+
ws.sendLine(` ${ANSI.darkGold}Chain${ANSI.reset} ${ANSI.white}${chain}${ANSI.reset}`);
|
|
563
|
+
ws.sendLine(` ${ANSI.darkGold}Created${ANSI.reset} ${ANSI.dim}${walletData.createdAt ? new Date(walletData.createdAt).toLocaleDateString() : 'unknown'}${ANSI.reset}`);
|
|
564
|
+
ws.sendLine(` ${ANSI.darkGold}Encryption${ANSI.reset} ${ANSI.dim}AES-256-GCM + scrypt${ANSI.reset}`);
|
|
565
|
+
|
|
566
|
+
// Fetch balance
|
|
567
|
+
ws.sendLine('');
|
|
568
|
+
ws.sendLine(` ${ANSI.dim}Fetching balance...${ANSI.reset}`);
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
const balance = await provider.getBalance(walletData.address);
|
|
572
|
+
const ethBal = parseFloat(ethers.formatEther(balance));
|
|
573
|
+
|
|
574
|
+
// Also check USDC
|
|
575
|
+
const usdcAddr = USDC_ADDRESSES[chain];
|
|
576
|
+
let usdcBal = 0;
|
|
577
|
+
if (usdcAddr) {
|
|
578
|
+
try {
|
|
579
|
+
const usdc = new ethers.Contract(usdcAddr, ['function balanceOf(address) view returns (uint256)'], provider);
|
|
580
|
+
const raw = await usdc.balanceOf(walletData.address);
|
|
581
|
+
usdcBal = parseFloat(ethers.formatUnits(raw, 6));
|
|
582
|
+
} catch {}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Get ETH price for USD value
|
|
586
|
+
const ethPrice = await getEthPrice();
|
|
587
|
+
const usdValue = (ethBal * ethPrice) + usdcBal;
|
|
588
|
+
|
|
589
|
+
// Overwrite "Fetching..." line
|
|
590
|
+
ws.sendLine(`\x1b[1A\x1b[2K ${ANSI.darkGold}ETH${ANSI.reset} ${ANSI.white}${ethBal.toFixed(6)}${ANSI.reset} ${ANSI.dim}($${(ethBal * ethPrice).toFixed(2)})${ANSI.reset}`);
|
|
591
|
+
ws.sendLine(` ${ANSI.darkGold}USDC${ANSI.reset} ${ANSI.white}$${usdcBal.toFixed(2)}${ANSI.reset}`);
|
|
592
|
+
ws.sendLine(` ${ANSI.darkGold}Total${ANSI.reset} ${ANSI.green}$${usdValue.toFixed(2)}${ANSI.reset}`);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
ws.sendLine(` ${ANSI.red}Balance fetch failed: ${err.message}${ANSI.reset}`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
ws.sendLine('');
|
|
598
|
+
|
|
599
|
+
// Show action menu
|
|
600
|
+
ws.sendMenu('wallet_action', '◆ What would you like to do?', [
|
|
601
|
+
{ value: 'receive', label: '📥 Receive', desc: 'Show address to receive funds' },
|
|
602
|
+
{ value: 'send', label: '📤 Send', desc: 'Send ETH or tokens (CLI required)' },
|
|
603
|
+
{ value: 'portfolio', label: '📊 Portfolio', desc: 'Multi-chain balance view' },
|
|
604
|
+
{ value: 'history', label: '📜 History', desc: 'Transaction history' },
|
|
605
|
+
{ value: 'switch', label: '🔄 Switch chain', desc: `Currently: ${chain}` },
|
|
606
|
+
{ value: 'back', label: '← Back', desc: '' },
|
|
607
|
+
]);
|
|
608
|
+
|
|
609
|
+
return {};
|
|
446
610
|
}
|
|
447
611
|
|
|
448
612
|
// ══════════════════════════════════════════════════
|
|
@@ -581,8 +745,16 @@ async function cmdConfig(ws) {
|
|
|
581
745
|
ws.sendLine(` ${ANSI.darkGold}Wallet${ANSI.reset} ${ANSI.white}${wallet}${ANSI.reset}`);
|
|
582
746
|
ws.sendLine(` ${ANSI.darkGold}Slippage${ANSI.reset} ${ANSI.white}${slippage}%${ANSI.reset}`);
|
|
583
747
|
ws.sendLine(` ${ANSI.darkGold}Mail${ANSI.reset} ${ANSI.white}${email}${ANSI.reset}`);
|
|
584
|
-
ws.sendLine(` ${ANSI.darkGold}AI${ANSI.reset} ${
|
|
748
|
+
ws.sendLine(` ${ANSI.darkGold}AI${ANSI.reset} ${hasAnyLLM() ? `${ANSI.green}● Ready${ANSI.reset}` : `${ANSI.dim}○ Not configured${ANSI.reset}`}`);
|
|
585
749
|
ws.sendLine('');
|
|
750
|
+
|
|
751
|
+
// Offer interactive config
|
|
752
|
+
ws.sendMenu('config_action', '◆ Configure', [
|
|
753
|
+
{ value: 'chain', label: '🔗 Change chain', desc: `Currently: ${chain}` },
|
|
754
|
+
{ value: 'keys', label: '🔑 LLM / API keys', desc: '' },
|
|
755
|
+
{ value: 'back', label: '← Back', desc: '' },
|
|
756
|
+
]);
|
|
757
|
+
|
|
586
758
|
return {};
|
|
587
759
|
}
|
|
588
760
|
|
package/src/web/server.js
CHANGED
|
@@ -100,6 +100,27 @@ export async function startWebShell(opts = {}) {
|
|
|
100
100
|
try {
|
|
101
101
|
const msg = JSON.parse(raw.toString());
|
|
102
102
|
|
|
103
|
+
if (msg.type === 'menu_select') {
|
|
104
|
+
// User selected something from an interactive menu
|
|
105
|
+
try {
|
|
106
|
+
const { handleMenuSelect } = await import('./commands.js');
|
|
107
|
+
const result = await handleMenuSelect(msg.id, msg.value, msg.item, {
|
|
108
|
+
send: (text) => ws.send(JSON.stringify({ type: 'output', data: text })),
|
|
109
|
+
sendLine: (text) => ws.send(JSON.stringify({ type: 'output', data: text + '\r\n' })),
|
|
110
|
+
sendMenu: (id, title, items) => ws.send(JSON.stringify({ type: 'menu', id, title, items })),
|
|
111
|
+
});
|
|
112
|
+
if (result?.output) {
|
|
113
|
+
ws.send(JSON.stringify({ type: 'output', data: result.output }));
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
ws.send(JSON.stringify({
|
|
117
|
+
type: 'output',
|
|
118
|
+
data: `\r\n \x1b[31m✗ Error: ${err.message}\x1b[0m\r\n`,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
if (msg.type === 'command') {
|
|
104
125
|
const cmd = msg.data.trim();
|
|
105
126
|
|
|
@@ -133,6 +154,7 @@ export async function startWebShell(opts = {}) {
|
|
|
133
154
|
const result = await handleCommand(cmd, {
|
|
134
155
|
send: (text) => ws.send(JSON.stringify({ type: 'output', data: text })),
|
|
135
156
|
sendLine: (text) => ws.send(JSON.stringify({ type: 'output', data: text + '\r\n' })),
|
|
157
|
+
sendMenu: (id, title, items) => ws.send(JSON.stringify({ type: 'menu', id, title, items })),
|
|
136
158
|
});
|
|
137
159
|
|
|
138
160
|
if (result?.output) {
|
package/src/web/terminal.js
CHANGED
|
@@ -4,11 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
const PROMPT = '\x1b[38;2;255;215;0m❯\x1b[0m ';
|
|
6
6
|
const PROMPT_LENGTH = 2; // visible chars: ❯ + space
|
|
7
|
+
const A = {
|
|
8
|
+
gold: '\x1b[38;2;255;215;0m',
|
|
9
|
+
dim: '\x1b[38;2;102;102;102m',
|
|
10
|
+
green: '\x1b[38;2;0;255;136m',
|
|
11
|
+
red: '\x1b[38;2;233;69;96m',
|
|
12
|
+
white: '\x1b[1;37m',
|
|
13
|
+
blue: '\x1b[38;2;68;136;255m',
|
|
14
|
+
r: '\x1b[0m',
|
|
15
|
+
};
|
|
7
16
|
|
|
8
17
|
const COMMANDS = [
|
|
9
|
-
'price', 'watch', 'gas', 'portfolio', 'history', 'market',
|
|
10
|
-
'wallet', '
|
|
11
|
-
'help', 'clear', 'banner', 'exit',
|
|
18
|
+
'ai', 'price', 'watch', 'gas', 'portfolio', 'history', 'market',
|
|
19
|
+
'wallet', 'send', 'receive', 'mail', 'keys', 'oracle', 'casino',
|
|
20
|
+
'facilitator', 'config', 'logs', 'help', 'clear', 'banner', 'exit',
|
|
12
21
|
];
|
|
13
22
|
|
|
14
23
|
let term;
|
|
@@ -18,8 +27,14 @@ let commandHistory = [];
|
|
|
18
27
|
let historyIndex = -1;
|
|
19
28
|
let connected = false;
|
|
20
29
|
|
|
30
|
+
// ── MENU STATE ────────────────────────────────
|
|
31
|
+
let menuActive = false;
|
|
32
|
+
let menuItems = [];
|
|
33
|
+
let menuIndex = 0;
|
|
34
|
+
let menuId = '';
|
|
35
|
+
let menuTitle = '';
|
|
36
|
+
|
|
21
37
|
async function init() {
|
|
22
|
-
// Load xterm.js
|
|
23
38
|
term = new Terminal({
|
|
24
39
|
theme: {
|
|
25
40
|
background: '#0a0a1a',
|
|
@@ -64,16 +79,42 @@ async function init() {
|
|
|
64
79
|
|
|
65
80
|
window.addEventListener('resize', () => fitAddon.fit());
|
|
66
81
|
|
|
67
|
-
// Connect WebSocket
|
|
68
82
|
connectWS();
|
|
69
83
|
|
|
70
|
-
// Input handling
|
|
71
84
|
term.onKey(({ key, domEvent }) => {
|
|
72
85
|
const code = domEvent.keyCode;
|
|
73
86
|
const ctrl = domEvent.ctrlKey;
|
|
74
87
|
|
|
88
|
+
// ── MENU MODE ──
|
|
89
|
+
if (menuActive) {
|
|
90
|
+
if (code === 38) { // Up
|
|
91
|
+
if (menuIndex > 0) { menuIndex--; renderMenu(); }
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (code === 40) { // Down
|
|
95
|
+
if (menuIndex < menuItems.length - 1) { menuIndex++; renderMenu(); }
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (code === 13) { // Enter — select
|
|
99
|
+
const selected = menuItems[menuIndex];
|
|
100
|
+
menuActive = false;
|
|
101
|
+
// Clear menu display
|
|
102
|
+
term.write('\x1b[?25h'); // show cursor
|
|
103
|
+
sendMenuSelection(menuId, selected);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (code === 27 || (ctrl && code === 67)) { // Esc or Ctrl+C — cancel
|
|
107
|
+
menuActive = false;
|
|
108
|
+
term.write('\r\n');
|
|
109
|
+
term.write(` ${A.dim}Cancelled${A.r}\r\n\r\n`);
|
|
110
|
+
writePrompt();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
return; // Ignore other keys in menu mode
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── NORMAL MODE ──
|
|
75
117
|
if (ctrl && code === 67) {
|
|
76
|
-
// Ctrl+C — cancel
|
|
77
118
|
currentLine = '';
|
|
78
119
|
term.write('^C\r\n');
|
|
79
120
|
writePrompt();
|
|
@@ -81,14 +122,12 @@ async function init() {
|
|
|
81
122
|
}
|
|
82
123
|
|
|
83
124
|
if (ctrl && code === 76) {
|
|
84
|
-
// Ctrl+L — clear
|
|
85
125
|
term.clear();
|
|
86
126
|
writePrompt();
|
|
87
127
|
return;
|
|
88
128
|
}
|
|
89
129
|
|
|
90
130
|
if (code === 13) {
|
|
91
|
-
// Enter
|
|
92
131
|
term.write('\r\n');
|
|
93
132
|
const cmd = currentLine.trim();
|
|
94
133
|
|
|
@@ -106,7 +145,6 @@ async function init() {
|
|
|
106
145
|
}
|
|
107
146
|
|
|
108
147
|
if (code === 8) {
|
|
109
|
-
// Backspace
|
|
110
148
|
if (currentLine.length > 0) {
|
|
111
149
|
currentLine = currentLine.slice(0, -1);
|
|
112
150
|
term.write('\b \b');
|
|
@@ -115,7 +153,6 @@ async function init() {
|
|
|
115
153
|
}
|
|
116
154
|
|
|
117
155
|
if (code === 38) {
|
|
118
|
-
// Arrow up — history
|
|
119
156
|
if (historyIndex < commandHistory.length - 1) {
|
|
120
157
|
historyIndex++;
|
|
121
158
|
replaceInput(commandHistory[historyIndex]);
|
|
@@ -124,7 +161,6 @@ async function init() {
|
|
|
124
161
|
}
|
|
125
162
|
|
|
126
163
|
if (code === 40) {
|
|
127
|
-
// Arrow down — history
|
|
128
164
|
if (historyIndex > 0) {
|
|
129
165
|
historyIndex--;
|
|
130
166
|
replaceInput(commandHistory[historyIndex]);
|
|
@@ -136,13 +172,11 @@ async function init() {
|
|
|
136
172
|
}
|
|
137
173
|
|
|
138
174
|
if (code === 9) {
|
|
139
|
-
// Tab — autocomplete
|
|
140
175
|
domEvent.preventDefault();
|
|
141
176
|
autocomplete();
|
|
142
177
|
return;
|
|
143
178
|
}
|
|
144
179
|
|
|
145
|
-
// Printable characters
|
|
146
180
|
if (key.length === 1 && !ctrl) {
|
|
147
181
|
currentLine += key;
|
|
148
182
|
term.write(key);
|
|
@@ -151,7 +185,7 @@ async function init() {
|
|
|
151
185
|
|
|
152
186
|
// Paste support
|
|
153
187
|
term.onData((data) => {
|
|
154
|
-
|
|
188
|
+
if (menuActive) return;
|
|
155
189
|
if (data.length > 1 && !data.startsWith('\x1b')) {
|
|
156
190
|
const clean = data.replace(/[\r\n]/g, '');
|
|
157
191
|
currentLine += clean;
|
|
@@ -162,6 +196,67 @@ async function init() {
|
|
|
162
196
|
term.focus();
|
|
163
197
|
}
|
|
164
198
|
|
|
199
|
+
// ── MENU RENDERING ────────────────────────────
|
|
200
|
+
function renderMenu() {
|
|
201
|
+
// Move cursor up to redraw (clear previous menu)
|
|
202
|
+
const totalLines = menuItems.length + 2; // title + items + hint
|
|
203
|
+
term.write(`\x1b[${totalLines}A\x1b[J`);
|
|
204
|
+
|
|
205
|
+
term.write(` ${A.gold}${menuTitle}${A.r}\r\n`);
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < menuItems.length; i++) {
|
|
208
|
+
const item = menuItems[i];
|
|
209
|
+
const label = item.label || item.value || String(item);
|
|
210
|
+
const desc = item.desc ? ` ${A.dim}${item.desc}${A.r}` : '';
|
|
211
|
+
|
|
212
|
+
if (i === menuIndex) {
|
|
213
|
+
term.write(` ${A.gold}► ${A.white}${label}${A.r}${desc}\r\n`);
|
|
214
|
+
} else {
|
|
215
|
+
term.write(` ${A.dim}${label}${A.r}${desc}\r\n`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
term.write(` ${A.dim}↑/↓ navigate • Enter select • Esc cancel${A.r}\r\n`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function showMenu(id, title, items) {
|
|
223
|
+
menuActive = true;
|
|
224
|
+
menuId = id;
|
|
225
|
+
menuTitle = title;
|
|
226
|
+
menuItems = items;
|
|
227
|
+
menuIndex = 0;
|
|
228
|
+
|
|
229
|
+
term.write('\x1b[?25l'); // hide cursor during menu
|
|
230
|
+
term.write('\r\n');
|
|
231
|
+
|
|
232
|
+
// Initial render
|
|
233
|
+
term.write(` ${A.gold}${menuTitle}${A.r}\r\n`);
|
|
234
|
+
for (let i = 0; i < menuItems.length; i++) {
|
|
235
|
+
const item = menuItems[i];
|
|
236
|
+
const label = item.label || item.value || String(item);
|
|
237
|
+
const desc = item.desc ? ` ${A.dim}${item.desc}${A.r}` : '';
|
|
238
|
+
|
|
239
|
+
if (i === menuIndex) {
|
|
240
|
+
term.write(` ${A.gold}► ${A.white}${label}${A.r}${desc}\r\n`);
|
|
241
|
+
} else {
|
|
242
|
+
term.write(` ${A.dim}${label}${A.r}${desc}\r\n`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
term.write(` ${A.dim}↑/↓ navigate • Enter select • Esc cancel${A.r}\r\n`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function sendMenuSelection(id, item) {
|
|
249
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
250
|
+
ws.send(JSON.stringify({
|
|
251
|
+
type: 'menu_select',
|
|
252
|
+
id,
|
|
253
|
+
value: item.value || item.label || String(item),
|
|
254
|
+
item,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── WEBSOCKET ─────────────────────────────────
|
|
165
260
|
function connectWS() {
|
|
166
261
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
167
262
|
ws = new WebSocket(`${protocol}//${window.location.host}`);
|
|
@@ -177,13 +272,15 @@ function connectWS() {
|
|
|
177
272
|
|
|
178
273
|
if (msg.type === 'output') {
|
|
179
274
|
term.write(msg.data);
|
|
180
|
-
writePrompt();
|
|
275
|
+
if (!menuActive) writePrompt();
|
|
181
276
|
} else if (msg.type === 'clear') {
|
|
182
277
|
term.clear();
|
|
183
278
|
writePrompt();
|
|
279
|
+
} else if (msg.type === 'menu') {
|
|
280
|
+
// Server is requesting user to pick from a menu
|
|
281
|
+
showMenu(msg.id, msg.title, msg.items);
|
|
184
282
|
}
|
|
185
283
|
} catch {
|
|
186
|
-
// Raw text fallback
|
|
187
284
|
term.write(event.data);
|
|
188
285
|
}
|
|
189
286
|
};
|
|
@@ -191,12 +288,8 @@ function connectWS() {
|
|
|
191
288
|
ws.onclose = () => {
|
|
192
289
|
connected = false;
|
|
193
290
|
updateStatus(false);
|
|
194
|
-
term.write(
|
|
195
|
-
|
|
196
|
-
// Reconnect after 3s
|
|
197
|
-
setTimeout(() => {
|
|
198
|
-
connectWS();
|
|
199
|
-
}, 3000);
|
|
291
|
+
term.write(`\r\n${A.red} ⚡ Connection lost. Reconnecting...${A.r}\r\n`);
|
|
292
|
+
setTimeout(() => connectWS(), 3000);
|
|
200
293
|
};
|
|
201
294
|
|
|
202
295
|
ws.onerror = () => {
|
|
@@ -209,7 +302,7 @@ function sendCommand(cmd) {
|
|
|
209
302
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
210
303
|
ws.send(JSON.stringify({ type: 'command', data: cmd }));
|
|
211
304
|
} else {
|
|
212
|
-
term.write(
|
|
305
|
+
term.write(`${A.red} ✗ Not connected${A.r}\r\n`);
|
|
213
306
|
writePrompt();
|
|
214
307
|
}
|
|
215
308
|
}
|
|
@@ -219,28 +312,20 @@ function writePrompt() {
|
|
|
219
312
|
}
|
|
220
313
|
|
|
221
314
|
function replaceInput(text) {
|
|
222
|
-
// Clear current input
|
|
223
315
|
const clearLen = currentLine.length;
|
|
224
|
-
for (let i = 0; i < clearLen; i++)
|
|
225
|
-
term.write('\b \b');
|
|
226
|
-
}
|
|
316
|
+
for (let i = 0; i < clearLen; i++) term.write('\b \b');
|
|
227
317
|
currentLine = text;
|
|
228
318
|
term.write(text);
|
|
229
319
|
}
|
|
230
320
|
|
|
231
321
|
function autocomplete() {
|
|
232
322
|
if (!currentLine) return;
|
|
233
|
-
|
|
234
323
|
const matches = COMMANDS.filter((c) => c.startsWith(currentLine.toLowerCase()));
|
|
235
|
-
|
|
236
324
|
if (matches.length === 1) {
|
|
237
325
|
replaceInput(matches[0]);
|
|
238
326
|
} else if (matches.length > 1) {
|
|
239
|
-
// Show options
|
|
240
327
|
term.write('\r\n');
|
|
241
|
-
term.write(
|
|
242
|
-
matches.map((m) => ` \x1b[38;2;102;102;102m${m}\x1b[0m`).join(' ')
|
|
243
|
-
);
|
|
328
|
+
term.write(matches.map((m) => ` ${A.dim}${m}${A.r}`).join(' '));
|
|
244
329
|
term.write('\r\n');
|
|
245
330
|
writePrompt();
|
|
246
331
|
term.write(currentLine);
|
|
@@ -250,13 +335,8 @@ function autocomplete() {
|
|
|
250
335
|
function updateStatus(isConnected) {
|
|
251
336
|
const dot = document.querySelector('.status-dot');
|
|
252
337
|
const text = document.querySelector('.status-text');
|
|
253
|
-
if (dot)
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
if (text) {
|
|
257
|
-
text.textContent = isConnected ? 'Connected' : 'Disconnected';
|
|
258
|
-
}
|
|
338
|
+
if (dot) dot.className = isConnected ? 'status-dot' : 'status-dot disconnected';
|
|
339
|
+
if (text) text.textContent = isConnected ? 'Connected' : 'Disconnected';
|
|
259
340
|
}
|
|
260
341
|
|
|
261
|
-
// Boot
|
|
262
342
|
document.addEventListener('DOMContentLoaded', init);
|