@tastekim/chat-cli
Version:
💬Connect with developers worldwide through an interactive terminal chat experience while you code!💻
598 lines • 22.5 kB
JavaScript
"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