ncurses
Version:
An ncurses binding for node.js
498 lines (486 loc) • 18.8 kB
JavaScript
/* Press ESC to bring up a window list to switch between windows */
var nc = require('..'),
irc = require('./deps/irc'),
widgets = require('../lib/widgets');
var connections = [], config = {
userName: 'nodecurses',
realName: 'node-ncurses IRC client',
port: 6667,
autoRejoin: true,
secure: false
}, clientInfo = {
name: 'nnirc',
version: '1.0',
env: ''
};
var CTCP_DELIM = String.fromCharCode(1);
function getWinName(win, conn) {
var key = '', isChannel = false;
for (var i=0,chans=Object.keys(conn.wins.channels); i<chans.length; i++) {
if (win === conn.wins.channels[chans[i]]) {
key = chans[i];
isChannel = true;
break;
}
}
if (!isChannel) {
for (var i=0,queries=Object.keys(conn.wins.queries); i<queries.length; i++) {
if (win === conn.wins.queries[queries[i]]) {
key = queries[i];
break;
}
}
}
return key;
}
function getTimestamp() {
var time = new Date();
var hours = time.getHours();
var mins = time.getMinutes();
return "" + (hours < 10 ? "0" : "") + hours + ":" + (mins < 10 ? "0" : "") + mins;
}
function updateHeader(win, header, style) {
var curx = win.curx, cury = win.cury;
style = style || {};
header = '' + header;
win.cursor(0, 0);
win.clrtoeol();
if (style.attrs)
win.attron(style.attrs);
if (header.length > 0) {
if (style.pos === 'center')
win.centertext(0, header);
else if (style.pos === 'right')
win.addstr(0, win.width-(Math.min(header.length, win.width)), header, win.width);
else
win.addstr(header, win.width);
}
if (style.attrs)
win.attroff(style.attrs);
win.cursor(cury, curx);
win.refresh();
}
function appendLine(win, message, attrs) {
var curx = win.curx, cury = win.cury, time = getTimestamp();
win.scroll(1);
win.cursor(win.height-3, 0);
win.print("[" + time + "] ");
if (attrs)
win.attron(attrs);
win.print(message);
if (attrs)
win.attroff(attrs);
win.cursor(cury, curx);
win.refresh();
}
function cleanup() {
nc.cleanup();
process.exit(0);
}
// ============================================================================
function setupWindow(win, conn) {
win.on('inputChar', function (c, i) {
if (i === nc.keys.ESC) {
var items = {};
for (var i=0; i<connections.length; i++) {
items[connections[i].client.opt.server] = connections[i].wins.status;
for (var j=0,chans=Object.keys(connections[i].wins.channels); j<chans.length; j++)
items[' ' + chans[j]] = connections[i].wins.channels[chans[j]];
for (var j=0,queries=Object.keys(connections[i].wins.queries); j<queries.length; j++)
items[' Q:' + queries[j]] = connections[i].wins.queries[queries[j]];
}
widgets.ListBox(items,
{
title: 'Windows',
height: Math.min(Object.keys(items).length+2, nc.lines-2),
style: {
colors: {
bg: 'blue',
fg: 'white'
}
}
}, function(window) {
if (window)
window.top();
nc.redraw();
});
} else if (i === nc.keys.BACKSPACE && win.curx > 0) {
win.inbuffer = win.inbuffer.substring(0, win.curx - 1) + win.inbuffer.substring(win.curx);
win.delch(win.height-1, win.curx - 1);
win.refresh();
} else if (i === nc.keys.DEL) {
win.inbuffer = win.inbuffer.substring(0, win.curx) + win.inbuffer.substring(win.curx + 1);
win.delch(win.height-1, win.curx);
win.refresh();
} else if (i === nc.keys.LEFT && win.curx > 0) {
win.cursor(win.height-1, win.curx-1);
win.refresh();
} else if (i === nc.keys.RIGHT && win.curx < win.inbuffer.length) {
win.cursor(win.height-1, win.curx+1);
win.refresh();
} else if (i === nc.keys.END) {
win.cursor(win.height-1, win.inbuffer.length);
win.refresh();
} else if (i === nc.keys.HOME) {
win.cursor(win.height-1, 0);
win.refresh();
} else if (i === nc.keys.NEWLINE) {
if (win.inbuffer.length) {
if (win.inbuffer[0] === '/') {
var cmd = win.inbuffer.substring(1).split(' ', 1).join('').trim(),
args = win.inbuffer.substring(win.inbuffer.indexOf(cmd)+cmd.length+1).trim();
switch (cmd.toLowerCase()) {
case 'j':
case 'join':
if (args.length) {
if (args[0] !== '#' && args[0] !== '&'
&& args[0] !== '+' && args[0] !== '!')
args = "#" + args;
if (args.length > 1 && !conn.wins.channels[args])
conn.client.join(args);
}
break;
case 'part':
var name = getWinName(win, conn);
if (name[0] === '#' || name[0] === '&'
|| name[0] === '+' || name[0] === '!') {
conn.client.part(name);
delete conn.wins.channels[name];
}
break;
case 'msg':
var who = args.substring(0, args.indexOf(' ')),
message = args.substring(args.indexOf(' ')+1);
if (!who.length || !message.length)
appendLine(win, 'Invalid arguments');
else {
conn.client.say(who, message);
appendLine(win, '* ->' + who + ': ' + message);
}
break;
case 'me':
if (win !== conn.wins.status) {
if (args.length) {
var key = getWinName(win, conn);
conn.client.say(key, CTCP_DELIM + 'ACTION ' + args + CTCP_DELIM);
appendLine(win, '* ' + conn.client.nick + ' ' + args, COLOR_ACTION);
}
} else
appendLine(win, 'That command is only valid on channel and query windows');
break;
case 'whois':
if (args.length) {
conn.client.send('WHOIS', args, false);
}
break;
case 'nick':
if (args.length) {
var newnick = args.split(' ', 1).join('');
if (newnick) {
conn.client.prevnick = conn.client.nick;
conn.client.send("NICK", newnick);
conn.client.nick = newnick;
}
}
break;
case 'away':
conn.client.send('AWAY', (args.length ? args : 'I am away'));
appendLine(conn.wins.status, '* You have been marked as being away', nc.attrs.BOLD);
break;
case 'back':
conn.client.send('AWAY');
appendLine(conn.wins.status, '* You are no longer marked as being away', nc.attrs.BOLD);
break;
case 'exit':
case 'quit':
if (args.length)
conn.client.disconnect(args);
else
conn.client.disconnect();
cleanup();
break;
case 'close':
if (win === conn.wins.status)
conn.client.disconnect();
else {
var key = getWinName(win, conn);
if (key[0] === '#' || key[0] === '&'
|| key[0] === '+' || key[0] === '!') {
conn.client.part(key);
delete conn.wins.channels[key];
} else
delete conn.wins.queries[key];
}
win.close();
nc.redraw();
if (nc.numwins === 1)
cleanup();
return;
break;
case 'query':
var to = args.split(' ', 1).join('');
if (!conn.wins.queries[to.toLowerCase()]) {
var query = new nc.Window(nc.lines, nc.cols);
setupWindow(query, conn);
updateHeader(query, 'Query: ' + to, {pos: 'center'});
conn.wins.queries[to.toLowerCase()] = query;
}
break;
case 'connect':
break;
case 'server':
break;
case 'whowas':
break;
case 'who':
break;
case 'info':
break;
case 'banlist':
break;
case 'notice':
break;
case 'invite':
break;
case 'ctcp':
break;
default:
appendLine(win, 'Unknown command: ' + cmd);
}
} else if (win.inbuffer.length && win !== conn.wins.status) {
var key = getWinName(win, conn);
conn.client.say(key, win.inbuffer);
appendLine(win, '<' + conn.client.nick + '> ' + win.inbuffer);
}
win.inbuffer = "";
win.cursor(win.height-1, 0);
win.clrtoeol();
win.refresh();
}
} else if (i >= 32 && i <= 126 && win.curx < win.width-1) {
win.inbuffer = win.inbuffer.slice(0, win.curx) + c + win.inbuffer.slice(win.curx);
if (win.curx < win.inbuffer.length) {
win.insch(i);
win.cursor(win.height - 1, win.curx + 1);
} else {
win.addch(i);
}
win.refresh();
}
});
win.scrollok(true);
win.hline(win.height-2, 0, win.width);
win.setscrreg(1, win.height-3); // Leave one line at the top for the header
win.cursor(win.height-1, 0);
win.refresh();
win.inbuffer = "";
}
function addConnection(server, nick, clientcfg) {
var client = new irc.Client(server, nick, clientcfg),
conn = {
client: client,
wins: {
status: new nc.Window(nc.lines, nc.cols),
channels: {},
queries: {}
}
};
client.on('connect', function() {
appendLine(conn.wins.status, 'Connected!');
for (var i=0,chans=Object.keys(conn.wins.channels); i<chans.length; i++)
appendLine(conn.wins.channels[chans[i]], 'Connected!');
});
client.on('ready', function() {
appendLine(conn.wins.status, 'Ready!');
});
client.on('error', function(err) {
appendLine(conn.wins.status, '* Error: ' + require('sys').inspect(err));
});
client.conn.on('end', function() {
appendLine(conn.wins.status, 'Disconnected!');
});
client.on('message', function(from, to, message) {
var msg;
if (to[0] === '#' || to[0] === '&'
|| to[0] === '+' || to[0] === '!') {
if (message.substr(0, 7) === CTCP_DELIM + 'ACTION') {
msg = '* ' + from + ' ' + message.substring(8, message.length-1);
appendLine(conn.wins.channels[to.toLowerCase()], msg, COLOR_ACTION);
} else {
msg = '<' + from + '> ' + message;
appendLine(conn.wins.channels[to.toLowerCase()], msg);
}
} else {
if (message[0] === CTCP_DELIM && message[message.length-1] === CTCP_DELIM) {
message = message.substring(1, message.length-1);
var which = message.split(' ', 1).join('').toLowerCase();
switch (which) {
case 'action':
if (!conn.wins.queries[from.toLowerCase()]) {
var query = new nc.Window(nc.lines, nc.cols);
setupWindow(query, conn);
updateHeader(query, 'Query: ' + from, {pos: 'center'});
conn.wins.queries[from.toLowerCase()] = query;
}
msg = '* ' + from + ' ' + message.substring(8, message.length-1);
appendLine(conn.wins.queries[from.toLowerCase()], msg, COLOR_ACTION);
break;
case 'version':
client.send('NOTICE', from, CTCP_DELIM + "VERSION"
+ " " + clientInfo.name
+ " " + clientInfo.version
+ " " + clientInfo.env + CTCP_DELIM);
break;
case 'time':
var curTime = new Date(), dow = curTime.getDay(),
day = curTime.getDate(), month = curTime.getMonth(),
year = curTime.getFullYear(), hours = curTime.getHours(),
mins = curTime.getMinutes(), secs = curTime.getSeconds(),
tz = curTime.toString().replace(/^.*\(([^)]+)\)$/, '$1'),
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
daysofweek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
var strTime = daysofweek.slice(dow, (dow == 6 ? dow : dow+1)).toString()
+ " " + months.slice(month, (month == 11 ? month : month+1)).toString()
+ " " + (day < 10 ? "0" : "") + day
+ " " + (hours < 10 ? "0" : "") + hours
+ ":" + (mins < 10 ? "0" : "") + mins
+ ":" + (secs < 10 ? "0" : "") + secs
+ " " + year + " " + tz.toUpperCase();
msg = CTCP_DELIM + "TIME " + strTime + CTCP_DELIM;
client.send('NOTICE', from, msg);
break;
}
} else {
if (!conn.wins.queries[from.toLowerCase()]) {
var query = new nc.Window(nc.lines, nc.cols);
setupWindow(query, conn);
updateHeader(query, 'Query: ' + from, {pos: 'center'});
conn.wins.queries[from.toLowerCase()] = query;
}
appendLine(conn.wins.queries[from.toLowerCase()], '<' + from + '> ' + message);
}
}
});
client.on('kick', function(channel, nick, by, reason) {
var msg = nick + ' has been kicked by ' + by + (reason ? ' (' + reason + ')' : '');
appendLine(conn.wins.channels[channel.toLowerCase()], msg, nc.attrs.BOLD);
});
client.on('join', function(channel, nick) {
var msg;
if (nick === client.nick) {
conn.wins.channels[channel.toLowerCase()] = new nc.Window(nc.lines, nc.cols);
setupWindow(conn.wins.channels[channel.toLowerCase()], conn);
msg = '* Now talking on ' + channel;
appendLine(conn.wins.channels[channel.toLowerCase()], msg);
} else {
msg = nick + ' has joined the channel';
appendLine(conn.wins.channels[channel.toLowerCase()], msg, COLOR_JOIN);
}
});
client.on('part', function(channel, nick, reason) {
if (nick !== client.nick) {
var msg = nick + ' has left the channel' + (reason ? ' (' + reason + ')' : '');
appendLine(conn.wins.channels[channel.toLowerCase()], msg, COLOR_PART);
}
});
client.on('quit', function(channel, nick, reason) {
if (nick !== client.nick) {
var msg = nick + ' has quit IRC' + (reason ? ' (' + reason + ')' : '');
appendLine(conn.wins.channels[channel.toLowerCase()], msg, COLOR_QUIT);
}
});
client.on('topic', function(channel, topic, nick, joining) {
var msg;
if (joining)
msg = '* Topic ' + (topic ? 'set by ' + nick + ': ' + topic : 'not set');
else
msg = '* ' + nick + ' has ' + (topic ? 'set the topic to: ' + topic : 'removed the topic');
appendLine(conn.wins.channels[channel.toLowerCase()], msg, nc.attrs.BOLD);
updateHeader(conn.wins.channels[channel.toLowerCase()], topic || '', {attrs: COLOR_TOPIC});
});
client.on('nick', function(channel, oldnick, newnick) {
var msg, which;
if (newnick === client.nick)
msg = '* You are now known as ' + newnick;
else
msg = '* ' + oldnick + ' is now known as ' + newnick;
if (channel === undefined)
which = conn.wins.status;
else
which = conn.wins.channels[channel.toLowerCase()];
appendLine(which, msg, nc.attrs.BOLD);
});
client.on('away', function(nick, message) {
appendLine(conn.wins.status, '* ' + nick + ' is away: ' + message);
});
client.on('mode', function(channel, issuer, issuee, mode) {
var msg = '* ' + issuer + (mode[0] === '+' ? ' gives ' : ' removes ');
if (mode[1] === 'o')
msg += 'channel operator status';
else if (mode[1] === 'v')
msg += 'voice';
else
msg += 'unknown status';
msg += (mode[0] === '+' ? ' to ' : ' from ') + issuee;
appendLine(conn.wins.channels[channel.toLowerCase()], msg, nc.attrs.BOLD);
});
client.on('whois', function(nick, info) {
var prefix = '[' + nick + '] ', msg;
msg = '(' + info.username + '@' + info.host + '): ' + info.realname;
appendLine(conn.wins.status, prefix + msg, nc.attrs.BOLD);
if (info.channels) {
msg = '';
for (var i=0; i<info.channels.length; i++) {
msg += (info.channels[i].mode ? info.channels[i].mode : '') + info.channels[i].name;
}
appendLine(conn.wins.status, prefix + msg, nc.attrs.BOLD);
}
appendLine(conn.wins.status, prefix + info.server.host + ' ' + info.server.info, nc.attrs.BOLD);
if (info.secureConn)
appendLine(conn.wins.status, prefix + 'is using a secure connection', nc.attrs.BOLD);
if (info.hostinfo)
appendLine(conn.wins.status, prefix + info.hostinfo, nc.attrs.BOLD);
if (info.actually) {
msg = 'is connecting from ' + info.actually.username + '@'
+ info.actually.host + ' ' + info.actually.ip;
appendLine(conn.wins.status, prefix + msg, nc.attrs.BOLD);
}
if (info.idle || info.signon) {
var msg = '';
if (info.idle) {
var secs = info.idle,
hours = Math.floor(secs / 3600),
mins = Math.floor((secs % 3600) / 60);
secs = Math.ceil((secs % 3600) % 60);
hours = (hours < 10 ? '0' + hours : ''+hours);
mins = (mins < 10 ? '0' + mins : ''+mins);
secs = (secs < 10 ? '0' + secs : ''+secs);
msg = 'idle ' + hours + ':' + mins + ':' + secs;
}
if (info.signon)
msg += (msg.length ? ', ' : '') + 'signon: ' + (new Date(info.signon*1000));
appendLine(conn.wins.status, prefix + msg, nc.attrs.BOLD);
}
if (info.account)
appendLine(conn.wins.status, prefix + 'is logged in as ' + info.account, nc.attrs.BOLD);
appendLine(conn.wins.status, prefix + 'End of WHOIS list.', nc.attrs.BOLD);
});
client.on('unhandledMessage', function(message) {
var msg = 'UNHANDLED ' + message.command + ' > prefix: ' + message.prefix
+ ', nick: ' + message.nick + ', args: ' + require('sys').inspect(message.args);
appendLine(conn.wins.status, msg, nc.attrs.BOLD);
});
setupWindow(conn.wins.status, conn);
connections.push(conn);
updateHeader(conn.wins.status, 'Status: ' + server, {attrs: nc.attrs.BOLD});
appendLine(conn.wins.status, 'Connecting ...');
}
var root = new nc.Window(); // unused on purpose -- it creates stdscr
// Setup colors
var COLOR_JOIN = nc.colorPair(2);
var COLOR_PART = nc.colorPair(3);
var COLOR_TOPIC = nc.colorPair(4, nc.colors.BLACK, nc.colors.WHITE);
var COLOR_ACTION = nc.colorPair(5);
var COLOR_QUIT = nc.colorPair(6);
addConnection('irc.freenode.net', 'ncursestest' + Math.ceil(Math.random()*100));