import { WebSocket } from 'ws'; import readline from 'readline'; // --------------------------------------------------------------------------- // ANSI helpers // --------------------------------------------------------------------------- const c = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m', brightWhite: '\x1b[97m', }; // --------------------------------------------------------------------------- // Config // --------------------------------------------------------------------------- const port = process.argv[2] || process.env.PORT || 3000; const url = `ws://localhost:${port}/ws`; // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- let currentState = null; let connected = false; // --------------------------------------------------------------------------- // Startup banner // --------------------------------------------------------------------------- console.log(`\n${c.bold}${c.cyan} Segnapunti Anto — Terminal Controller${c.reset}`); console.log(`${c.dim} Connessione a ${url}...${c.reset}\n`); // --------------------------------------------------------------------------- // WebSocket // --------------------------------------------------------------------------- const ws = new WebSocket(url); ws.on('error', (err) => { console.error(`${c.red}Errore di connessione: ${err.message}${c.reset}`); console.error(`${c.dim}Assicurati che il server sia avviato su ${url}${c.reset}`); process.exit(1); }); ws.on('open', () => { connected = true; console.log(` ${c.green}Connesso.${c.reset} Digita ${c.bold}help${c.reset} per i comandi disponibili.\n`); ws.send(JSON.stringify({ type: 'register', role: 'controller' })); }); ws.on('message', (data) => { try { const msg = JSON.parse(data.toString()); if (msg.type === 'state') { currentState = msg.state; printState(currentState); } else if (msg.type === 'error') { clearLine(); console.error(` ${c.red}Errore server: ${msg.message}${c.reset}`); } } catch { // ignora messaggi malformati } rl.prompt(true); }); ws.on('close', () => { console.log(`\n${c.dim} Connessione chiusa.${c.reset}\n`); process.exit(0); }); // --------------------------------------------------------------------------- // Output helpers // --------------------------------------------------------------------------- /** Cancella la riga corrente del terminale (evita di sovrascrivere il prompt). */ function clearLine() { process.stdout.write('\r\x1b[K'); } function printState(s) { if (!s) return; const { nomi, punt, set, servHome } = s.sp; const homeServ = servHome ? `${c.yellow}▶${c.reset}` : ' '; const guestServ = !servHome ? `${c.yellow}◀${c.reset}` : ' '; const homeName = nomi.home.padEnd(15); const guestName = nomi.guest.padEnd(15); const homeScore = String(punt.home).padStart(3); const guestScore = String(punt.guest).padStart(3); clearLine(); console.log( ` ${homeServ} ${c.bold}${homeName}${c.reset}` + `${c.brightWhite}${homeScore}${c.reset} ${c.dim}(set ${set.home})${c.reset}` + ` ${c.dim}│${c.reset} ` + `${c.brightWhite}${guestScore}${c.reset} ${c.dim}(set ${set.guest})${c.reset}` + ` ${c.bold}${guestName}${c.reset} ${guestServ}` + ` ${c.dim}[${s.modalitaPartita}]${c.reset}` ); } function printHelp() { console.log(` ${c.bold} Punteggio${c.reset} ${c.cyan}+${c.reset} / ${c.cyan}pc${c.reset} Punto casa (shortcut) ${c.cyan}-${c.reset} / ${c.cyan}po${c.reset} Punto ospite (shortcut) ${c.cyan}punto casa${c.reset} Punto casa ${c.cyan}punto ospite${c.reset} Punto ospite ${c.cyan}undo${c.reset} / ${c.cyan}u${c.reset} Annulla ultimo punto ${c.bold} Set${c.reset} ${c.cyan}set casa${c.reset} Incrementa set casa ${c.cyan}set ospite${c.reset} Incrementa set ospite ${c.bold} Partita${c.reset} ${c.cyan}serv${c.reset} Cambia servizio (solo se 0-0) ${c.cyan}reset${c.reset} Resetta la partita (chiede conferma) ${c.cyan}nomi ${c.reset} Imposta nomi squadre ${c.cyan}modalita 2/3${c.reset} Imposta modalità best-of-3 ${c.cyan}modalita 3/5${c.reset} Imposta modalità best-of-5 ${c.bold} Informazioni${c.reset} ${c.cyan}stato${c.reset} Mostra punteggio attuale ${c.cyan}help${c.reset} Mostra questo aiuto ${c.cyan}exit${c.reset} / ${c.cyan}q${c.reset} Esci ${c.dim}Suggerimento: usa Tab per il completamento automatico dei comandi.${c.reset} `); } // --------------------------------------------------------------------------- // Command dispatch // --------------------------------------------------------------------------- function sendAction(action) { if (!connected || ws.readyState !== WebSocket.OPEN) { console.error(` ${c.red}Non connesso al server.${c.reset}`); rl.prompt(); return; } ws.send(JSON.stringify({ type: 'action', action })); } function parseCommand(line) { const parts = line.trim().split(/\s+/); const cmd = parts[0].toLowerCase(); switch (cmd) { case '+': case 'pc': sendAction({ type: 'incPunt', team: 'home' }); break; case '-': case 'po': sendAction({ type: 'incPunt', team: 'guest' }); break; case 'punto': { const team = parts[1]?.toLowerCase(); if (team === 'casa' || team === 'home') { sendAction({ type: 'incPunt', team: 'home' }); } else if (team === 'ospite' || team === 'guest') { sendAction({ type: 'incPunt', team: 'guest' }); } else { console.error(` ${c.red}Uso: punto casa | punto ospite${c.reset}`); rl.prompt(); } break; } case 'undo': case 'u': sendAction({ type: 'decPunt' }); break; case 'set': { const team = parts[1]?.toLowerCase(); if (team === 'casa' || team === 'home') { sendAction({ type: 'incSet', team: 'home' }); } else if (team === 'ospite' || team === 'guest') { sendAction({ type: 'incSet', team: 'guest' }); } else { console.error(` ${c.red}Uso: set casa | set ospite${c.reset}`); rl.prompt(); } break; } case 'serv': sendAction({ type: 'cambiaPalla' }); break; case 'reset': rl.question(` ${c.yellow}Confermi reset partita? (s/N) ${c.reset}`, (answer) => { if (answer.trim().toLowerCase() === 's') { sendAction({ type: 'resetta' }); } else { console.log(` ${c.dim}Reset annullato.${c.reset}`); rl.prompt(); } }); return; case 'nomi': { const home = parts[1]; const guest = parts[2]; if (!home) { console.error(` ${c.red}Uso: nomi ${c.reset}`); rl.prompt(); break; } const payload = { type: 'setNomi', home }; if (guest) payload.guest = guest; sendAction(payload); break; } case 'modalita': { const m = parts[1]; if (m !== '2/3' && m !== '3/5') { console.error(` ${c.red}Uso: modalita 2/3 | modalita 3/5${c.reset}`); rl.prompt(); break; } sendAction({ type: 'setModalita', modalita: m }); break; } case 'stato': printState(currentState); rl.prompt(); break; case 'help': printHelp(); rl.prompt(); break; case 'exit': case 'q': ws.close(); break; default: console.error( ` ${c.red}Comando non riconosciuto: "${cmd}"${c.reset}` + ` — digita ${c.bold}help${c.reset} per la lista` ); rl.prompt(); } } // --------------------------------------------------------------------------- // REPL // --------------------------------------------------------------------------- const TAB_COMPLETIONS = [ '+', '-', 'pc', 'po', 'punto casa', 'punto ospite', 'undo', 'u', 'set casa', 'set ospite', 'serv', 'reset', 'nomi', 'modalita 2/3', 'modalita 3/5', 'stato', 'help', 'exit', 'q', ]; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '> ', historySize: 100, completer(line) { const hits = TAB_COMPLETIONS.filter((entry) => entry.startsWith(line)); return [hits.length ? hits : TAB_COMPLETIONS, line]; }, }); rl.on('line', (line) => { if (!line.trim()) { rl.prompt(); return; } parseCommand(line.trim()); }); rl.on('close', () => { ws.close(); });