UNPKG

@tastekim/chat-cli

Version:

💬Connect with developers worldwide through an interactive terminal chat experience while you code!💻

598 lines 22.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ChatInterface = void 0; const blessed = __importStar(require("blessed")); const client_1 = require("../core/client"); class ChatInterface { constructor(nickname, room, location) { this.joinedRooms = []; this.availableRooms = []; this.unreadMessages = {}; this.chatHistory = {}; this.pendingRoomJoin = null; this.nickname = nickname; this.room = room; // Initial room, will be Lobby this.currentRoomId = room; this.location = location; this.client = new client_1.WebSocketClient(); this.screen = blessed.screen({ smartCSR: true, title: 'Chat CLI', fullUnicode: true, mouse: false, // Disable mouse events to prevent interference sendFocus: false, // Disable focus events useBCE: true, // Use background color erase resizeTimeout: 300, // Debounce resize events }); // Main layout const mainLayout = blessed.box({ parent: this.screen, width: '100%', height: '100%-1', }); // Room List Panel (Left) this.roomListPanel = blessed.box({ parent: mainLayout, width: '30%', height: '100%', border: 'line', label: 'Rooms', }); // Chat Panel (Right) this.chatPanel = blessed.box({ parent: mainLayout, left: '30%', width: '70%', height: '100%', border: 'line', label: `Chat - ${this.currentRoomId}`, }); // Message Log (inside Chat Panel) this.messageLog = blessed.log({ parent: this.chatPanel, top: 0, left: 0, width: '100%-2', height: '100%-2', scrollable: true, alwaysScroll: true, scrollbar: { ch: ' ', }, tags: true, // To support blessed formatting tags }); // Input Box (Bottom of the screen) this.inputBox = blessed.textbox({ parent: this.chatPanel, label: ' Message ', bottom: 0, height: 3, border: 'line', mouse: false, clickable: false, inputOnFocus: true, keys: true, style: { border: { fg: 'cyan' }, focus: { border: { fg: 'green' } }, }, }); // Right side - room tabs for switching between joined and available rooms this.roomTabs = blessed.box({ parent: this.roomListPanel, top: 0, height: 3, border: 'line', style: { border: { fg: 'cyan' }, }, content: ' {bold}Joined{/bold} | Available ', tags: true, }); // Remove click handler to prevent mouse interference // Joined Rooms List this.joinedRoomsList = blessed.list({ parent: this.roomListPanel, label: ' Joined Rooms ', top: 3, height: '50%-2', border: 'line', style: { border: { fg: 'green' }, selected: { bg: 'blue' }, }, keys: true, vi: true, scrollbar: { ch: ' ', }, }); // Available Rooms List this.availableRoomsList = blessed.list({ parent: this.roomListPanel, label: ' Available Rooms ', top: 3, height: '50%-2', border: 'line', style: { border: { fg: 'yellow' }, selected: { bg: 'blue' }, }, keys: true, vi: true, scrollbar: { ch: ' ', }, }); this.setupKeyHandlers(); } handleMessageSubmit(text) { if (!text.trim()) { this.inputBox.clearValue(); this.inputBox.focus(); return; } if (text.trim().toLowerCase() === '/create-room') { this.showCreateRoomForm(); } else { this.client.sendWebSocketMessage({ type: 'SEND_MESSAGE', payload: { roomId: this.currentRoomId, content: text, }, }); } this.inputBox.clearValue(); this.inputBox.focus(); this.screen.render(); } setupKeyHandlers() { // Exit on Ctrl+C and Escape this.screen.key(['escape', 'C-c'], () => { this.client.disconnect(); return process.exit(0); }); // Number keys (1-9) for quick room switching - only when not in input for (let i = 1; i <= 9; i++) { this.screen.key([i.toString()], () => { if (this.screen.focused !== this.inputBox) { this.switchToRoomByIndex(i - 1); } }); } // Alt + Number combinations for room switching (alternative method) this.screen.key(['M-1', 'M-2', 'M-3', 'M-4', 'M-5', 'M-6', 'M-7', 'M-8', 'M-9'], (ch, key) => { const match = key.name.match(/M-(\d)/); if (match) { const roomIndex = parseInt(match[1]) - 1; this.switchToRoomByIndex(roomIndex); } }); // Tab key to toggle between room list tabs - only when not in input this.screen.key(['tab'], () => { if (this.screen.focused !== this.inputBox) { if (this.joinedRoomsList.hidden) { this.joinedRoomsList.show(); this.availableRoomsList.hide(); } else { this.joinedRoomsList.hide(); this.availableRoomsList.show(); } this.screen.render(); } }); // Simple submission handling with debounce let lastSubmitTime = 0; this.inputBox.on('submit', (text) => { const now = Date.now(); if (now - lastSubmitTime < 200) return; // Prevent rapid submissions lastSubmitTime = now; this.handleMessageSubmit(text); }); // Auto-focus for printable characters (minimal interference) this.screen.key('*', (ch, key) => { if (this.screen.focused !== this.inputBox) { if (ch && ch.length === 1 && ch >= ' ' && ch <= '~') { if (!['up', 'down', 'left', 'right', 'enter', 'tab'].includes(key.name)) { if (!key.name || (!key.name.startsWith('f') && !key.name.startsWith('M-') && !key.ctrl && !key.shift)) { this.inputBox.focus(); } } } } }); // Handle room selection const handleRoomSelect = (list, rooms) => { const selectedIndex = list.selected || 0; const room = rooms[selectedIndex]; if (!room || room.name === this.currentRoomId) { return; } if (room.isPrivate) { const prompt = blessed.prompt({ parent: this.screen, top: 'center', left: 'center', height: 'shrink', width: 'half', border: 'line', label: `Password for ${room.name}`, }); prompt.input('Enter password:', '', (err, password) => { if (!err && password !== null) { this.joinRoom(room.name, password); } this.inputBox.focus(); }); } else { this.joinRoom(room.name); } }; this.joinedRoomsList.on('select', () => handleRoomSelect(this.joinedRoomsList, this.joinedRooms)); this.availableRoomsList.on('select', () => handleRoomSelect(this.availableRoomsList, this.availableRooms)); } async start() { this.screen.render(); this.inputBox.focus(); try { this.logMessage('system', 'Connecting to server...'); await this.client.connectWithParams(this.nickname, this.room, this.location); this.logMessage('system', 'Connected! Welcome to the Lobby.'); this.setupClientEventHandlers(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logMessage('error', `Failed to connect: ${errorMessage}`); } } setupClientEventHandlers() { this.client.on('message', (data) => { try { const message = JSON.parse(data); switch (message.type) { case 'CHAT_MESSAGE': this.handleChatMessage(message.payload); break; case 'ROOM_LIST': this.handleRoomList(message.payload); break; case 'ROOM_CREATED': this.handleRoomCreated(message.payload); break; case 'ROOM_DELETED': this.handleRoomDeleted(message.payload); break; case 'USER_COUNT_UPDATE': this.handleUserCountUpdate(message.payload); break; case 'ERROR': this.logMessage('error', `Server Error: ${message.payload.message}`); break; default: this.logMessage('system', `Unknown message type: ${message.type}`); } } catch (err) { this.logMessage('error', `Failed to parse message from server: ${err instanceof Error ? err.message : 'Unknown error'}`); } }); this.client.on('disconnected', (data) => { this.logMessage('system', `Disconnected: ${data.reason || 'Connection lost'}`); }); } handleChatMessage(payload) { const { roomId, sender, content } = payload; if (!this.chatHistory[roomId]) { this.chatHistory[roomId] = []; } const formattedMessage = `${sender}: ${content}`; this.chatHistory[roomId].push(formattedMessage); if (roomId === this.currentRoomId) { const type = sender === this.nickname ? 'own' : 'user'; this.logMessage(type, formattedMessage); } else { this.unreadMessages[roomId] = true; this.updateRoomLists(); } } handleRoomList(payload) { this.availableRooms = payload.rooms || []; // When the full list comes, re-evaluate joined rooms. this.joinedRooms = this.availableRooms.filter((r) => this.joinedRooms.some((jr) => jr.name === r.name) || r.name === this.currentRoomId); this.updateRoomLists(); } handleRoomCreated(payload) { // Avoid duplicates if (!this.availableRooms.some((r) => r.name === payload.name)) { this.availableRooms.push(payload); this.updateRoomLists(); } } handleRoomDeleted(payload) { this.availableRooms = this.availableRooms.filter((r) => r.name !== payload.name); this.joinedRooms = this.joinedRooms.filter((r) => r.name !== payload.name); this.updateRoomLists(); } handleUserCountUpdate(payload) { let roomToUpdate; roomToUpdate = this.availableRooms.find((r) => r.name === payload.name); if (roomToUpdate) roomToUpdate.userCount = payload.userCount; roomToUpdate = this.joinedRooms.find((r) => r.name === payload.name); if (roomToUpdate) roomToUpdate.userCount = payload.userCount; // Check if this update confirms a pending room join if (this.pendingRoomJoin && this.pendingRoomJoin === payload.name) { this.currentRoomId = this.pendingRoomJoin; this.pendingRoomJoin = null; // Add to joined rooms if not already there if (!this.joinedRooms.some((r) => r.name === this.currentRoomId)) { const newJoinedRoom = this.availableRooms.find((r) => r.name === this.currentRoomId); if (newJoinedRoom) { this.joinedRooms.push(newJoinedRoom); } } // Clear the message log and display history for the new room this.messageLog.setContent(''); const history = this.chatHistory[this.currentRoomId] || []; history.forEach((msg) => this.messageLog.log(msg)); this.chatPanel.setLabel(`Chat - ${this.currentRoomId}`); this.unreadMessages[this.currentRoomId] = false; } this.updateRoomLists(); } // Helper to log messages to the chat window logMessage(type, message) { const colorMap = { user: '{blue-fg}', own: '{green-fg}', system: '{yellow-fg}', error: '{red-fg}', }; this.messageLog.log(`${colorMap[type]}${message}{/}`); } // Show help information about keyboard shortcuts and commands showHelp() { this.logMessage('system', '📋 Chat CLI Help & Keyboard Shortcuts'); this.logMessage('system', ''); this.logMessage('system', '🚀 Quick Room Switching:'); this.logMessage('system', ' • Press 1-9 keys to switch between joined rooms (when not typing)'); this.logMessage('system', ' • Alt+1 to Alt+9 for quick room switching (alternative)'); this.logMessage('system', ' • Tab key to toggle between "Joined" and "Available" room tabs'); this.logMessage('system', ''); this.logMessage('system', '🗨️ Chat Commands:'); this.logMessage('system', ' • Type /create-room to create a new room'); this.logMessage('system', ' • Click room names to join/switch rooms'); this.logMessage('system', ' • Private rooms require password verification'); this.logMessage('system', ''); this.logMessage('system', '⌨️ Navigation:'); this.logMessage('system', ' • Arrow keys to navigate room lists'); this.logMessage('system', ' • Enter to select a room'); this.logMessage('system', ' • Escape or Ctrl+C to exit'); this.logMessage('system', ''); this.logMessage('system', '💡 Pro Tips:'); this.logMessage('system', ' • Rooms with • badge have unread messages'); this.logMessage('system', ' • ⚿ icon indicates private rooms'); this.logMessage('system', ' • Numbers in room list show user count'); this.logMessage('system', ' • Your joined rooms are numbered 1-9 for quick switching'); } // Rerenders the room lists based on the current state updateRoomLists() { const formatRoom = (r, index) => { const unread = this.unreadMessages[r.name] ? '{red-fg}•{/red-fg}' : ' '; const icon = r.isPrivate ? '⚿' : '#'; const number = index + 1; return `${number}. ${unread} ${icon} ${r.name} (${r.userCount})`; }; const formatAvailableRoom = (r) => { const unread = this.unreadMessages[r.name] ? '{red-fg}•{/red-fg}' : ' '; const icon = r.isPrivate ? '⚿' : '#'; return `${unread} ${icon} ${r.name} (${r.userCount})`; }; this.joinedRoomsList.setItems(this.joinedRooms.map((room, index) => formatRoom(room, index))); this.availableRoomsList.setItems(this.availableRooms.map(formatAvailableRoom)); this.screen.render(); } joinRoom(roomName, password) { this.pendingRoomJoin = roomName; this.client.sendWebSocketMessage({ type: 'JOIN_ROOM', payload: { name: roomName, password: password || '', }, }); } showCreateRoomForm() { const form = blessed.form({ parent: this.screen, width: '50%', height: 10, top: 'center', left: 'center', border: 'line', label: 'Create New Room', keys: true, mouse: false, // Disable mouse events clickable: false, // Prevent click events }); // Name label and input blessed.text({ parent: form, top: 1, left: 2, content: 'Name:', }); const nameInput = blessed.textbox({ parent: form, top: 1, left: 12, height: 1, width: '100%-14', name: 'name', inputOnFocus: true, style: { bg: 'black' }, }); // Type label and checkbox blessed.text({ parent: form, top: 3, left: 2, content: 'Type:', }); const privateCheckbox = blessed.checkbox({ parent: form, top: 3, left: 12, name: 'isPrivate', text: 'Private', checked: false, }); // Password label and input blessed.text({ parent: form, top: 5, left: 2, content: 'Password:', hidden: true, }); const passwordLabel = blessed.text({ parent: form, top: 5, left: 12, content: 'Password:', hidden: true, }); const passwordInput = blessed.textbox({ parent: form, top: 5, left: 12, height: 1, width: '100%-14', name: 'password', censor: true, hidden: true, inputOnFocus: true, style: { bg: 'black' }, }); privateCheckbox.on('check', () => { passwordLabel.show(); passwordInput.show(); this.screen.render(); }); privateCheckbox.on('uncheck', () => { passwordLabel.hide(); passwordInput.hide(); this.screen.render(); }); const submitButton = blessed.button({ parent: form, bottom: 1, left: 2, width: 8, height: 1, name: 'submit', content: 'Create', align: 'center', style: { bg: 'blue', focus: { bg: 'red' } }, }); const cancelButton = blessed.button({ parent: form, bottom: 1, left: 12, width: 8, height: 1, name: 'cancel', content: 'Cancel', align: 'center', style: { bg: 'grey', focus: { bg: 'red' } }, }); submitButton.on('press', () => form.submit()); cancelButton.on('press', () => { form.destroy(); this.screen.render(); this.inputBox.focus(); }); form.on('submit', (data) => { if (data.name && /^[a-zA-Z0-9]{1,15}$/.test(data.name)) { this.client.sendWebSocketMessage({ type: 'CREATE_ROOM', payload: { name: data.name, isPrivate: Boolean(data.isPrivate), password: data.password || '', }, }); form.destroy(); this.inputBox.focus(); } else { const errorPopup = blessed.message({ parent: this.screen, top: 'center', left: 'center', height: 'shrink', width: 'half', border: 'line', style: { border: { fg: 'red' } }, }); errorPopup.error('Invalid room name. Use 1-15 alphanumeric characters.', () => { nameInput.focus(); }); } }); nameInput.focus(); this.screen.render(); } switchToRoomByIndex(index) { if (index >= 0 && index < this.joinedRooms.length) { const room = this.joinedRooms[index]; if (room && room.name !== this.currentRoomId) { this.joinRoom(room.name); this.logMessage('system', `Switching to room: ${room.name}`); } } } } exports.ChatInterface = ChatInterface; //# sourceMappingURL=chat-interface.js.map