├── .gitignore ├── .jshintrc ├── index.js ├── lib ├── handler.js ├── handlers │ ├── clear.js │ ├── connect.js │ ├── emit.js │ ├── filter.js │ ├── help.js │ ├── pause.js │ ├── quit.js │ ├── unfilter.js │ └── unpause.js ├── parser.js ├── socket.js ├── ui │ ├── blessed.js │ ├── drawing.js │ ├── index.js │ ├── line.js │ ├── lines │ │ ├── errorMessage.js │ │ ├── incomingEvent.js │ │ ├── internalMessage.js │ │ └── outgoingEvent.js │ └── output.js └── util.js ├── package.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 30 | node_modules 31 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "unused": true 5 | } 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./lib/ui').boot(); 4 | -------------------------------------------------------------------------------- /lib/handler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The basis of a command handler. 3 | * @param {Parser} parser 4 | */ 5 | function Handler (parser) { 6 | this.parser = parser; 7 | } 8 | 9 | /** 10 | * Should take a command in and... do the necessary actions, whatever 11 | * they may be. 12 | * 13 | * @param {String} body 14 | */ 15 | Handler.prototype.dispatch = function (body) { 16 | throw new Error('dispatch not implemented'); 17 | }; 18 | 19 | /** 20 | * Returns the primary command name. 21 | * @return {String} 22 | */ 23 | Handler.prototype.getName = function () { 24 | throw new Error('getName not implemented'); 25 | }; 26 | 27 | /** 28 | * Returns aliases this command can be called with. 29 | * @return {[]String} 30 | */ 31 | Handler.prototype.getAliases = function () { 32 | throw new Error('getAliases not implemented'); 33 | }; 34 | 35 | /** 36 | * Returns helpful info about the handler's usage. 37 | * @return {[]String} 38 | */ 39 | Handler.prototype.getDescription = function () { 40 | throw new Error('getDescription not implemented'); 41 | }; 42 | 43 | module.exports = Handler; 44 | -------------------------------------------------------------------------------- /lib/handlers/clear.js: -------------------------------------------------------------------------------- 1 | var Handler = require('../handler'); 2 | 3 | function Clear () { 4 | Handler.apply(this, arguments); 5 | } 6 | 7 | Clear.prototype = new Handler(); 8 | 9 | Clear.prototype.dispatch = function () { 10 | this.parser.ui.output.clear(); 11 | }; 12 | 13 | Clear.prototype.getName = function () { 14 | return 'clear'; 15 | }; 16 | 17 | Clear.prototype.getAliases = function () { 18 | return ['cls']; 19 | }; 20 | 21 | Clear.prototype.getDescription = function () { 22 | return 'Clears the output frame.'; 23 | }; 24 | 25 | module.exports = Clear; 26 | -------------------------------------------------------------------------------- /lib/handlers/connect.js: -------------------------------------------------------------------------------- 1 | var Handler = require('../handler'); 2 | 3 | function Connect () { 4 | Handler.apply(this, arguments); 5 | } 6 | 7 | Connect.prototype = new Handler(); 8 | 9 | Connect.prototype.dispatch = function (body) { 10 | this.parser.socket.connect(body); 11 | }; 12 | 13 | Connect.prototype.getName = function () { 14 | return 'connect'; 15 | }; 16 | 17 | Connect.prototype.getAliases = function () { 18 | return ['c']; 19 | }; 20 | 21 | Connect.prototype.getDescription = function () { 22 | return 'Connects to a socket.io server: `connect :`'; 23 | }; 24 | 25 | module.exports = Connect; 26 | -------------------------------------------------------------------------------- /lib/handlers/emit.js: -------------------------------------------------------------------------------- 1 | var Handler = require('../handler'); 2 | var util = require('../util'); 3 | var ErrorMessage = require('../ui/lines/errorMessage'); 4 | 5 | function Connect () { 6 | Handler.apply(this, arguments); 7 | } 8 | 9 | Connect.prototype = new Handler(); 10 | 11 | Connect.prototype.dispatch = function (body) { 12 | var parts = util.splitFirst(body, '#'); 13 | var data, event = parts[0].trim(); 14 | try { 15 | data = (new Function('return ' + parts[1] + ';'))(); // jshint ignore:line 16 | } catch (e) { 17 | return this.parser.ui.output.add(new ErrorMessage(e.toString())).draw(); 18 | } 19 | 20 | this.parser.socket.emit(event, data); 21 | }; 22 | 23 | Connect.prototype.getName = function () { 24 | return 'emit'; 25 | }; 26 | 27 | Connect.prototype.getAliases = function () { 28 | return ['e']; 29 | }; 30 | 31 | Connect.prototype.getDescription = function () { 32 | return 'Emits an event like `event#{ foo: \'bar\' }`'; 33 | }; 34 | 35 | module.exports = Connect; 36 | -------------------------------------------------------------------------------- /lib/handlers/filter.js: -------------------------------------------------------------------------------- 1 | var Handler = require('../handler'); 2 | 3 | function Filter () { 4 | Handler.apply(this, arguments); 5 | } 6 | 7 | Filter.prototype = new Handler(); 8 | 9 | Filter.prototype.dispatch = function (body) { 10 | this.parser.ui.output.filter(body); 11 | }; 12 | 13 | Filter.prototype.getName = function () { 14 | return 'filter'; 15 | }; 16 | 17 | Filter.prototype.getAliases = function () { 18 | return ['f']; 19 | }; 20 | 21 | Filter.prototype.getDescription = function () { 22 | return 'Filters events to match a glob pattern.'; 23 | }; 24 | 25 | module.exports = Filter; 26 | -------------------------------------------------------------------------------- /lib/handlers/help.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var Handler = require('../handler'); 3 | var util = require('../util'); 4 | var InternalMessage = require('../ui/lines/internalMessage'); 5 | 6 | var hotkeys = { 7 | 'Ctrl+W': 'Scrolls the console up a page.', 8 | 'Ctrl+S': 'Scrolls the console down a page.', 9 | 'Ctrl+C': 'Clears the line in sio or, if already clear, exits' 10 | }; 11 | 12 | function Help () { 13 | Handler.apply(this, arguments); 14 | } 15 | 16 | Help.prototype = new Handler(); 17 | 18 | Help.prototype.dispatch = function () { 19 | var handlers = this.parser.handlers; 20 | var ui = this.parser.ui; 21 | 22 | // Now output everything... 23 | ui.output.add(new InternalMessage('')); 24 | 25 | // Show commands 26 | ui.output.add(new InternalMessage('Commands:')); 27 | this.pushColumns([ 28 | handlers.map(function (handler) { 29 | return [handler.getName()].concat(handler.getAliases()).join(', '); 30 | }), 31 | _.invoke(handlers, 'getDescription') 32 | ]); 33 | ui.output.add(new InternalMessage('')); 34 | 35 | // And now hotkeys 36 | ui.output.add(new InternalMessage('Hotkeys:')); 37 | this.pushColumns([_.keys(hotkeys), _.values(hotkeys)]); 38 | ui.output.add(new InternalMessage('')); 39 | 40 | // And update the screen 41 | ui.output.draw(); 42 | }; 43 | 44 | /** 45 | * Pushes a set of columns to the output, taburlarizing, indenting, 46 | * and prefixing it correctly. 47 | * @param {[][]String} columns 48 | */ 49 | Help.prototype.pushColumns = function (columns) { 50 | var ui = this.parser.ui; 51 | 52 | tabularize(' ', columns).forEach(function (row) { 53 | ui.output.add(new InternalMessage('\t' + row)); 54 | }); 55 | }; 56 | 57 | Help.prototype.getName = function () { 58 | return 'help'; 59 | }; 60 | 61 | Help.prototype.getAliases = function () { 62 | return ['h']; 63 | }; 64 | 65 | Help.prototype.getDescription = function () { 66 | return 'Shows this help text.'; 67 | }; 68 | 69 | /** 70 | * Makes an array of table rows, so that every column aligns correctly 71 | * with the other vertical cells. 72 | * @param {String} spacing The pad between columns 73 | * @param {[][]String} columns Array of columns (array of arrays of string) 74 | * @return {String} 75 | */ 76 | function tabularize (spacing, columns) { 77 | // Store the max length of each column. 78 | var maxInColumns = []; 79 | columns.forEach(function (column, i) { 80 | maxInColumns[i] = Math.max.apply(null, _.pluck(column, 'length')); 81 | }); 82 | 83 | // Build the output 84 | var output = []; 85 | for (var x = 0, l = columns[0].length; x < l; x++) { 86 | output.push( 87 | columns.map(function (column, y) { 88 | return util.pad(column[x], ' ' , maxInColumns[y]); 89 | }).join(spacing) 90 | ); 91 | }; 92 | 93 | return output; 94 | } 95 | 96 | module.exports = Help; 97 | -------------------------------------------------------------------------------- /lib/handlers/pause.js: -------------------------------------------------------------------------------- 1 | var Handler = require('../handler'); 2 | 3 | function Pause () { 4 | Handler.apply(this, arguments); 5 | } 6 | 7 | Pause.prototype = new Handler(); 8 | 9 | Pause.prototype.dispatch = function () { 10 | this.parser.ui.output.pause(); 11 | }; 12 | 13 | Pause.prototype.getName = function () { 14 | return 'pause'; 15 | }; 16 | 17 | Pause.prototype.getAliases = function () { 18 | return ['p']; 19 | }; 20 | 21 | Pause.prototype.getDescription = function () { 22 | return 'Pauses the output.'; 23 | }; 24 | 25 | module.exports = Pause; 26 | -------------------------------------------------------------------------------- /lib/handlers/quit.js: -------------------------------------------------------------------------------- 1 | var Handler = require('../handler'); 2 | 3 | function Quit () { 4 | Handler.apply(this, arguments); 5 | } 6 | 7 | Quit.prototype = new Handler(); 8 | 9 | Quit.prototype.dispatch = function () { 10 | process.exit(0); 11 | }; 12 | 13 | Quit.prototype.getName = function () { 14 | return 'quit'; 15 | }; 16 | 17 | Quit.prototype.getAliases = function () { 18 | return ['q']; 19 | }; 20 | 21 | Quit.prototype.getDescription = function () { 22 | return 'Exits the sio prompt.'; 23 | }; 24 | 25 | module.exports = Quit; 26 | -------------------------------------------------------------------------------- /lib/handlers/unfilter.js: -------------------------------------------------------------------------------- 1 | var Handler = require('../handler'); 2 | 3 | function Unfilter () { 4 | Handler.apply(this, arguments); 5 | } 6 | 7 | Unfilter.prototype = new Handler(); 8 | 9 | Unfilter.prototype.dispatch = function (body) { 10 | this.parser.ui.output.unfilter(body); 11 | }; 12 | 13 | Unfilter.prototype.getName = function () { 14 | return 'unfilter'; 15 | }; 16 | 17 | Unfilter.prototype.getAliases = function () { 18 | return ['uf']; 19 | }; 20 | 21 | Unfilter.prototype.getDescription = function () { 22 | return 'Removes one or all patterns from the filters.'; 23 | }; 24 | 25 | module.exports = Unfilter; 26 | -------------------------------------------------------------------------------- /lib/handlers/unpause.js: -------------------------------------------------------------------------------- 1 | var Handler = require('../handler'); 2 | 3 | function Unpause () { 4 | Handler.apply(this, arguments); 5 | } 6 | 7 | Unpause.prototype = new Handler(); 8 | 9 | Unpause.prototype.dispatch = function () { 10 | this.parser.ui.output.unpause(); 11 | }; 12 | 13 | Unpause.prototype.getName = function () { 14 | return 'unpause'; 15 | }; 16 | 17 | Unpause.prototype.getAliases = function () { 18 | return ['up']; 19 | }; 20 | 21 | Unpause.prototype.getDescription = function () { 22 | return 'Unpauses the output.'; 23 | }; 24 | 25 | module.exports = Unpause; 26 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var _ = require('lodash'); 4 | 5 | var Socket = require('./socket'); 6 | var util = require('./util'); 7 | 8 | // Command handlers 9 | var handlerPath = __dirname + '/handlers'; 10 | var handlers = fs.readdirSync(handlerPath).map(function (file) { 11 | return require(path.join(handlerPath, file)); 12 | }); 13 | 14 | // Separator between the command and body. 15 | var delimiter = ' '; 16 | 17 | /** 18 | * The parser is responsible for listening for input then dispatching 19 | * that input to the appropriate handler. 20 | * 21 | * @param {Object} ui 22 | */ 23 | function Parser (ui) { 24 | this.ui = ui; 25 | this.socket = new Socket(this); 26 | 27 | this.commandHistory = []; 28 | this.currentCommand = 0; 29 | 30 | var parser = this; 31 | this.handlers = handlers.map(function (Handler) { 32 | return new Handler(parser); 33 | }); 34 | 35 | ui.emitter.on('input', this.parse.bind(this)); 36 | ui.emitter.on('recall-older', this.recall.bind(this, -1)); 37 | ui.emitter.on('recall-newer', this.recall.bind(this, 1)); 38 | } 39 | 40 | /** 41 | * Returns a command by its name or an alias. Or undef, if it doesn't exist. 42 | * @return {Command} 43 | */ 44 | Parser.prototype.getCommand = function (name) { 45 | return _.find(this.handlers, function (handler) { 46 | return handler.getName() === name || handler.getAliases().indexOf(name) !== -1; 47 | }); 48 | }; 49 | 50 | /** 51 | * Parses an input message, dispatching it to a handler. 52 | * @param {String} message 53 | */ 54 | Parser.prototype.parse = function (message) { 55 | // Split the command and its body. 56 | var parts = util.splitFirst(message, delimiter); 57 | var handler = this.getCommand(parts[0].toLowerCase()); 58 | 59 | // If the command was invalid, show the help. 60 | if (!handler) { 61 | handler = this.getCommand('help'); 62 | } else { 63 | this.commandHistory.push(message); 64 | this.currentCommand = this.commandHistory.length; 65 | } 66 | 67 | // Finally, dispatch the command 68 | handler.dispatch(parts[1]); 69 | }; 70 | 71 | /** 72 | * Attempts to recall a command. 73 | * @param {Number} difference Difference from current position 74 | */ 75 | Parser.prototype.recall = function (difference) { 76 | var target = Math.max(Math.min(this.currentCommand + difference, this.commandHistory.length), 0); 77 | 78 | this.currentCommand = target; 79 | this.ui.input.setValue(this.commandHistory[target] || ''); 80 | this.ui.screen.render(); 81 | }; 82 | 83 | module.exports = Parser; 84 | -------------------------------------------------------------------------------- /lib/socket.js: -------------------------------------------------------------------------------- 1 | var io = require('socket.io-client'); 2 | var IncomingEvent = require('./ui/lines/incomingEvent'); 3 | var OutgoingEvent = require('./ui/lines/outgoingEvent'); 4 | var ErrorMessage = require('./ui/lines/errorMessage'); 5 | var InternalMessage = require('./ui/lines/internalMessage'); 6 | 7 | /** 8 | * Manages the websocket connection, including listening for output. 9 | * @param {Parser} parser 10 | */ 11 | function Socket (parser) { 12 | this.parser = parser; 13 | this.socket = null; 14 | this.connected = false; 15 | 16 | this.paused = false; 17 | this.spool = []; 18 | this.filters = []; 19 | } 20 | 21 | /** 22 | * Wraps the socket's own onevent function so that we can intercept 23 | * every event. Calls the given function with the event data. 24 | * @param {Function} fn 25 | */ 26 | Socket.prototype.onEvent = function (fn) { 27 | var onEvent = this.socket.onevent; 28 | var self = this; 29 | 30 | this.socket.onevent = function (packet) { 31 | fn.call(self, packet); 32 | onEvent.apply(self.socket, arguments); 33 | }; 34 | }; 35 | 36 | /** 37 | * Parses a socket.io url for connection. Adds the http if needed, and 38 | * replaces a space with a colon between the host and port. 39 | * @param {String} address 40 | * @return {String} 41 | */ 42 | Socket.prototype.parseAddress = function (address) { 43 | var addr = address.replace(' ', ':'); 44 | if (addr.indexOf('http') !== 0) { 45 | addr = 'http://' + addr; 46 | } 47 | return addr; 48 | }; 49 | 50 | /** 51 | * Tries to emit a new event on the socket. 52 | * @param {String} event 53 | * @param {*} data 54 | * @return {Socket} 55 | */ 56 | Socket.prototype.emit = function (event, data) { 57 | if (this.connected) { 58 | this.socket.emit(event, data); 59 | this.parser.ui.output.add(new OutgoingEvent(event, data)).draw(); 60 | } else { 61 | this.parser.ui.output.add(new ErrorMessage('You must connect to a server first!')).draw(); 62 | } 63 | 64 | return this; 65 | }; 66 | 67 | 68 | /** 69 | * Creates a new socket connection to the address. The address may be 70 | * given in the form ` ` or `:` 71 | * @param {String} address 72 | */ 73 | Socket.prototype.connect = function (address) { 74 | var ui = this.parser.ui; 75 | var addr = this.parseAddress(address); 76 | var socket = this.socket = io(addr); 77 | var self = this; 78 | 79 | ui.output.add(new InternalMessage('Connecting to ' + addr)).draw(); 80 | 81 | socket.on('connect', function () { 82 | self.connected = true; 83 | ui.output.add(new InternalMessage('Socket connected.')).draw(); 84 | }); 85 | 86 | socket.on('disconnect', function () { 87 | self.connected = false; 88 | ui.output.add(new InternalMessage('Socket disconnected.')).draw(); 89 | }); 90 | 91 | this.onEvent(function (packet) { 92 | ui.output.add(new IncomingEvent(packet)).draw(); 93 | }); 94 | }; 95 | 96 | module.exports = Socket; 97 | -------------------------------------------------------------------------------- /lib/ui/blessed.js: -------------------------------------------------------------------------------- 1 | var blessed = require('blessed'); 2 | var EventEmitter = require('events').EventEmitter; 3 | 4 | var screen = blessed.screen(); 5 | screen.title = 'SIO'; 6 | 7 | // Input box for commands 8 | var input = blessed.textbox({ 9 | bottom: 0, 10 | left: 0, 11 | width: '100%', 12 | height: 1, 13 | content: 'Hello!', 14 | style: { 15 | fg: 'white' 16 | }, 17 | mouse: true, 18 | keys: true, 19 | inputOnFocus: true 20 | }); 21 | 22 | // Dividing line between command and outbox 23 | var line = blessed.line({ 24 | bottom: 1, 25 | left: 0, 26 | right: 0, 27 | orientation: 'horizontal', 28 | type: 'bg', 29 | ch: '=', 30 | fg: 'magenta' 31 | }); 32 | 33 | // sio text 34 | var meta = blessed.text({ 35 | bottom: 2, 36 | right: 2, 37 | align: 'center', 38 | shrink: true, 39 | style: { 40 | fg: 5 41 | }, 42 | fg: 5 43 | }); 44 | 45 | var outbox = blessed.box({ 46 | bottom: 3, 47 | right: 0, 48 | left: 0, 49 | top: 0, 50 | valign: 'bottom', 51 | tags: true, 52 | scrollable: true, 53 | scrollbar: { 54 | bg: 'white', 55 | ch: ' ' 56 | } 57 | }); 58 | 59 | // Append our box to the screen. 60 | screen.append(input); 61 | screen.append(line); 62 | screen.append(meta); 63 | screen.append(outbox); 64 | 65 | var emitter = new EventEmitter(); 66 | 67 | // If our box is clicked, change the content. 68 | input.on('submit', function (data) { 69 | emitter.emit('input', data); 70 | input.setValue(''); 71 | input.focus(); 72 | screen.render(); 73 | }); 74 | 75 | // Quit on Escape, q, or Control-C. 76 | screen.key(['escape', 'q', 'C-c'], function () { 77 | return process.exit(0); 78 | }); 79 | 80 | // Scrolling the console 81 | input.key('C-w', function () { 82 | var scrollPos = Math.max(outbox.getScroll() - outbox.height, 1); 83 | outbox.setScroll(scrollPos); 84 | screen.render(); 85 | }); 86 | input.key('C-s', function () { 87 | var scrollPos = outbox.getScroll() + outbox.height; 88 | outbox.setScroll(scrollPos); 89 | screen.render(); 90 | }); 91 | input.key('up', function () { 92 | emitter.emit('recall-older'); 93 | }); 94 | input.key('down', function () { 95 | emitter.emit('recall-newer'); 96 | }); 97 | // Control+C clears the input if there is any. If there isn't, it exists. 98 | input.key('C-c', function () { 99 | if (input.getValue().length > 0) { 100 | input.setValue(''); 101 | screen.render(); 102 | } else { 103 | return process.exit(0); 104 | } 105 | }); 106 | 107 | module.exports = { 108 | emitter: emitter, 109 | outbox: outbox, 110 | screen: screen, 111 | input: input, 112 | meta: meta, 113 | line: line 114 | }; 115 | -------------------------------------------------------------------------------- /lib/ui/drawing.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var InternalMessage = require('./lines/internalMessage'); 4 | var Parser = require('../parser'); 5 | var Output = require('./output'); 6 | 7 | function Drawing (ui) { 8 | _.extend(this, ui); 9 | this.lines = []; 10 | } 11 | 12 | /** 13 | * Does first-time initializations, creating the UI and binding events. 14 | */ 15 | Drawing.prototype.boot = function () { 16 | this.input.focus(); 17 | this.screen.render(); 18 | 19 | this.output = new Output(this); 20 | this.parser = new Parser(this); 21 | 22 | this.welcome(); 23 | }; 24 | 25 | /** 26 | * Displays a welcome message to the user. 27 | */ 28 | Drawing.prototype.welcome = function () { 29 | this.output.add([ 30 | new InternalMessage(''), 31 | new InternalMessage('Welcome to sio. Type `connect ` to get started,'), 32 | new InternalMessage('or `help` for a list of commands.'), 33 | new InternalMessage('') 34 | ]).draw(); 35 | }; 36 | 37 | module.exports = Drawing; 38 | -------------------------------------------------------------------------------- /lib/ui/index.js: -------------------------------------------------------------------------------- 1 | var blessed = require('./blessed'); 2 | var Drawing = require('./drawing'); 3 | 4 | module.exports = new Drawing(blessed); 5 | -------------------------------------------------------------------------------- /lib/ui/line.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic line to be represented in the output. 3 | */ 4 | function Line () { 5 | this.time = new Date(); 6 | } 7 | 8 | /** 9 | * Determines whether this line should be displayed in the output, 10 | * taking into account the active filters. 11 | * 12 | * @param {[]String} filters 13 | * @return {Boolean} 14 | */ 15 | Line.prototype.shouldDisplay = function (filters) { 16 | return true; 17 | }; 18 | 19 | module.exports = Line; 20 | -------------------------------------------------------------------------------- /lib/ui/lines/errorMessage.js: -------------------------------------------------------------------------------- 1 | var InternalMessage = require('./internalMessage'); 2 | 3 | /** 4 | * Represents a line for an error message on the console. 5 | * @param {String} message Error message 6 | */ 7 | function ErrorMessage () { 8 | InternalMessage.apply(this, arguments); 9 | } 10 | 11 | ErrorMessage.prototype = new InternalMessage(); 12 | 13 | ErrorMessage.prototype.toString = function () { 14 | return '{red-fg}ERR{/red-fg} ' + this.message; 15 | }; 16 | 17 | module.exports = ErrorMessage; 18 | -------------------------------------------------------------------------------- /lib/ui/lines/incomingEvent.js: -------------------------------------------------------------------------------- 1 | var minimatch = require('minimatch'); 2 | var util = require('../../util'); 3 | var Line = require('../line'); 4 | 5 | /** 6 | * Represents a line for an incoming event on the console. 7 | * @param {Object} packet Socket.io event packet 8 | */ 9 | function IncomingEvent (packet) { 10 | Line.call(this); 11 | this.packet = packet; 12 | } 13 | 14 | IncomingEvent.prototype = new Line(); 15 | 16 | /** 17 | * Creates a nice timestamp for the time the event was sent. 18 | * @return {String} 19 | */ 20 | IncomingEvent.prototype.fmtTime = function () { 21 | return '' + 22 | padZeroes(this.time.getMinutes(), 2) + 23 | ':' + 24 | padZeroes(this.time.getSeconds(), 2) + 25 | ':' + 26 | padZeroes(this.time.getMilliseconds(), 4); 27 | }; 28 | 29 | IncomingEvent.prototype.shouldDisplay = function (filters) { 30 | // If there are no active filters, display it. 31 | if (filters.length === 0) { 32 | return true; 33 | } 34 | 35 | // Otherwise attempt to glob match the event name. 36 | for (var i = 0, l = filters.length; i < l; i++) { 37 | if (minimatch(this.packet.data[0], filters[i])) { 38 | return true; 39 | } 40 | } 41 | 42 | return false; 43 | }; 44 | 45 | IncomingEvent.prototype.toString = function () { 46 | return '{cyan-fg}' + this.fmtTime() + '{/cyan-fg} {green-fg}>{/green-fg} ' + util.prettifyEvent(this.packet.data); 47 | }; 48 | 49 | function padZeroes (time, len) { 50 | var t = '' + time; 51 | while (t.length < len) { 52 | t = '0' + t; 53 | } 54 | 55 | return t; 56 | } 57 | 58 | module.exports = IncomingEvent; 59 | -------------------------------------------------------------------------------- /lib/ui/lines/internalMessage.js: -------------------------------------------------------------------------------- 1 | var Line = require('../line'); 2 | 3 | /** 4 | * Represents a line for an internal message on the console. 5 | * @param {String} message Error message 6 | */ 7 | function Internal (message) { 8 | Line.call(this); 9 | this.message = message; 10 | } 11 | 12 | Internal.prototype = new Line(); 13 | 14 | Internal.prototype.toString = function () { 15 | return '{cyan-fg}+{/cyan-fg} ' + this.message; 16 | }; 17 | 18 | module.exports = Internal; 19 | -------------------------------------------------------------------------------- /lib/ui/lines/outgoingEvent.js: -------------------------------------------------------------------------------- 1 | var IncomingEvent = require('./incomingEvent'); 2 | var util = require('../../util'); 3 | 4 | /** 5 | * Represents a line for an outgoing event on the console. 6 | * @param {String} event 7 | * @param {Number} data 8 | */ 9 | function OutgoingEvent (event, data) { 10 | IncomingEvent.call(this, { data: [event, data] }); 11 | this.event = event; 12 | this.data = data; 13 | } 14 | 15 | OutgoingEvent.prototype = new IncomingEvent(); 16 | 17 | OutgoingEvent.prototype.shouldDisplay = function () { 18 | return true; 19 | }; 20 | 21 | OutgoingEvent.prototype.toString = function () { 22 | return '{cyan-fg}' + 23 | this.fmtTime() + 24 | '{/cyan-fg} {red-fg}<{/red-fg} ' + 25 | util.prettifyEvent([ this.event, this.data ]); 26 | }; 27 | 28 | module.exports = OutgoingEvent; 29 | -------------------------------------------------------------------------------- /lib/ui/output.js: -------------------------------------------------------------------------------- 1 | var IncomingEvent = require('./lines/incomingEvent'); 2 | var pack = require('../../package'); 3 | var _ = require('lodash'); 4 | 5 | var defaultMeta = 'sio version ' + pack.version; 6 | 7 | /** 8 | * Manages drawing, filtering, and display of the output console. 9 | * @param {Drawing} ui 10 | */ 11 | function Output (ui) { 12 | this.ui = ui; 13 | this.lines = []; 14 | this.lastLength = 0; 15 | 16 | this.filters = []; 17 | this.pausedAt = null; 18 | 19 | this.meta = null; 20 | this.updateMeta(); 21 | this.ui.screen.render(); 22 | } 23 | 24 | /** 25 | * Adds a line or multiple lines to the output. 26 | * @param {Line|[]Line} line 27 | * @return {Output} 28 | */ 29 | Output.prototype.add = function (line) { 30 | // Don't accept incoming messages while paused. 31 | if (this.pausedAt !== null && line instanceof IncomingEvent) { 32 | return this; 33 | } 34 | 35 | if (Array.isArray(line)) { 36 | this.lines = this.lines.concat(line); 37 | } else { 38 | this.lines.push(line); 39 | } 40 | 41 | return this; 42 | }; 43 | 44 | /** 45 | * Pauses the output. 46 | * @return {Output} 47 | */ 48 | Output.prototype.pause = function () { 49 | this.pausedAt = new Date(); 50 | this.updateMeta(); 51 | return this; 52 | }; 53 | 54 | /** 55 | * Unpauses the output. 56 | * @return {Output} 57 | */ 58 | Output.prototype.unpause = function () { 59 | this.pausedAt = null; 60 | this.updateMeta(); 61 | return this; 62 | }; 63 | 64 | 65 | /** 66 | * Adds a filter to the output. 67 | * @param {String} filter 68 | * @return {Output} 69 | */ 70 | Output.prototype.filter = function (filter) { 71 | this.filters.push(filter); 72 | this.updateMeta(); 73 | return this.redraw(); 74 | }; 75 | 76 | /** 77 | * Removes a previously added filters, or all filters if none is given. 78 | * @param {String} filter 79 | * @return {Output} 80 | */ 81 | Output.prototype.unfilter = function (filter) { 82 | if (filter) { 83 | this.filters = _.remove(this.filters, filter); 84 | } else { 85 | this.filters = []; 86 | } 87 | 88 | this.updateMeta(); 89 | return this.redraw(); 90 | }; 91 | 92 | 93 | /** 94 | * Returns all currently visible lines in the output. 95 | * @return {[]Line} 96 | */ 97 | Output.prototype.getVisibleLines = function () { 98 | var filters = this.filters; 99 | 100 | return _.filter(this.lines, function (line) { 101 | 102 | if (!line.shouldDisplay(filters)) { 103 | return false; 104 | } 105 | 106 | return true; 107 | }); 108 | }; 109 | 110 | /** 111 | * Expects fn to be a function that draws some content. This auto-scrolls 112 | * to the bottom after fn is run, if necessary. 113 | * @param {Function} fn 114 | * @return {Output} 115 | */ 116 | Output.prototype.andScroll = function (fn) { 117 | var outbox = this.ui.outbox; 118 | var willscroll = outbox.getScroll() >= outbox.getScrollHeight() - 2; 119 | var out = fn.call(this); 120 | 121 | if (willscroll) { 122 | outbox.setScrollPerc(100); 123 | } 124 | 125 | return out; 126 | }; 127 | 128 | /** 129 | * Draws new lines to the output. 130 | * @return {Output} 131 | */ 132 | Output.prototype.draw = function () { 133 | var lines = this.getVisibleLines(); 134 | var newLines = lines.slice(this.lastLength); 135 | 136 | this.andScroll(function () { 137 | this.ui.outbox.pushLine(_.invoke(newLines, 'toString')); 138 | this.ui.screen.render(); 139 | }); 140 | 141 | this.lastLength = lines.length; 142 | 143 | return this; 144 | }; 145 | 146 | /** 147 | * Clears all outbox content. 148 | * @return {[type]} [description] 149 | */ 150 | Output.prototype.clear = function () { 151 | this.lines = []; 152 | this.lastLength = 0; 153 | this.ui.outbox.setContent(''); 154 | this.ui.screen.render(); 155 | }; 156 | 157 | /** 158 | * Triggers a total redraw of the output. 159 | * @return {Output} 160 | */ 161 | Output.prototype.redraw = function () { 162 | var lines = this.getVisibleLines(); 163 | 164 | this.andScroll(function () { 165 | this.ui.outbox.setContent(''); 166 | this.ui.outbox.pushLine(_.invoke(lines, 'toString')); 167 | this.ui.screen.render(); 168 | }); 169 | 170 | this.lastLength = lines.length; 171 | 172 | return this; 173 | }; 174 | 175 | /** 176 | * Updates the meta text based on current status of the things. 177 | */ 178 | Output.prototype.updateMeta = function () { 179 | // Remove the old meta. 180 | if (this.meta) { 181 | this.ui.screen.remove(this.meta); 182 | } 183 | 184 | // Add status indictors 185 | var meta = []; 186 | if (this.filters.length > 0) { 187 | meta.push('FILTERED'); 188 | } 189 | if (this.pausedAt !== null) { 190 | meta.push('PAUSED'); 191 | } 192 | 193 | // If no status, just show the version 194 | var content; 195 | if (meta.length === 0) { 196 | content = defaultMeta; 197 | } else { 198 | content = meta.join(', '); 199 | } 200 | 201 | // Generate the meta and add it to the ui 202 | this.ui.meta.setText(' ' + content + ' '); 203 | this.ui.meta.width = content.length + 3; 204 | }; 205 | 206 | module.exports = Output; 207 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds padding to the string until it is a given length. 3 | * @param {String} str 4 | * @param {String} padding 5 | * @param {Number} length 6 | * @return {Strin} 7 | */ 8 | module.exports.pad = function (str, padding, length) { 9 | while (str.length < length) { 10 | str += padding; 11 | } 12 | 13 | return str; 14 | }; 15 | 16 | /** 17 | * Wraps a text in a Blessed tag. 18 | * 19 | * @param {String} text 20 | * @param {String} tag 21 | * @return {String} 22 | */ 23 | module.exports.wrap = function (text, tag) { 24 | return '{' + tag + '}' + text + '{/' + tag + '}'; 25 | }; 26 | 27 | /** 28 | * Returns a pretty-printed event string. 29 | * @param {Array} args 30 | * @return {String} 31 | */ 32 | module.exports.prettifyEvent = function (args) { 33 | if (!args.length) { 34 | return ''; 35 | } 36 | 37 | var output = '`' + args[0] + '`'; 38 | if (args.length > 1) { 39 | output += ': ' + args.slice(1).map(function (a) { 40 | return JSON.stringify(a) + ' '; 41 | }); 42 | } 43 | 44 | return output; 45 | }; 46 | 47 | /** 48 | * Splits the string into two substrings at the first occurence of 49 | * the delimiter. 50 | * @param {String} string 51 | * @param {String} delimiter 52 | * @return {[String, String]} 53 | */ 54 | module.exports.splitFirst = function (str, delimiter) { 55 | var index = 0; 56 | do { 57 | index = str.indexOf(delimiter, index); 58 | } while (str.charAt[index - 1] === '\\'); 59 | 60 | if (index === -1) { 61 | return [str]; 62 | } else { 63 | return [ 64 | str.slice(0, index), 65 | str.slice(index + delimiter.length) 66 | ]; 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sio", 3 | "version": "0.0.4", 4 | "description": "Socket.io CLI utility", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/connor4312/sio" 9 | }, 10 | "keywords": [ 11 | "socket.io", 12 | "socketio", 13 | "websocket", 14 | "cli", 15 | "debugger" 16 | ], 17 | "bin": { 18 | "sio": "index.js" 19 | }, 20 | "preferGlobal": true, 21 | "author": "Connor Peet ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/connor4312/sio/issues" 25 | }, 26 | "homepage": "https://github.com/connor4312/sio", 27 | "dependencies": { 28 | "blessed": "0.0.44", 29 | "lodash": "^3.1.0", 30 | "minimatch": "^2.0.1", 31 | "socket.io-client": "*", 32 | "stringify-object": "^1.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # sio, a socket.io debugger 2 | 3 | Sio was created because I was frustrated with making temporary index files and mashing F5 when I wanted to test or debug socket.io applications. 4 | 5 | ![](http://i.imgur.com/9Hz5OYD.gif) 6 | 7 | Installation: 8 | 9 | ```bash 10 | npm install -g sio 11 | ``` 12 | 13 | It's a terminal app. Features: 14 | 15 | * Connect to any socket.io 1.0+ server 16 | * Filtering of incoming events 17 | * Pausing and unpausing output 18 | * Emitting your own events 19 | * More to come? 20 | 21 | ## Usage 22 | 23 | ```bash 24 | # Connect to a server 25 | c 26 | # Emit an event. The "data" will be evaluated, so any valid Js goes. 27 | e # 28 | # Filter event names. They're matched with minimatch. 29 | f 30 | # Removes all filters 31 | uf 32 | # Pauses output 33 | p 34 | # Unpauses output 35 | up 36 | # Clears all output 37 | cls 38 | ``` 39 | --------------------------------------------------------------------------------