diff --git a/README.md b/README.md index dfeda9a..7694c3c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@ A curses-like library for node.js. As of right now, it does not read all terminfo. It was designed for one -terminal's terminfo: **xterm**. +terminal's terminfo: **xterm**, but if you understand the differences between +the three popular vt100-based terminals (xterm, screen, rxvt), you should be +able to use it for any terminal. I want this library to eventually become a high-level library for terminal widgets. @@ -11,52 +13,74 @@ widgets. ## Example Usage ``` js -var Program = require('blessed') - , program = new Program; +var blessed = require('blessed') + , program = blessed(); -program.on('key', function(ch, key) { - if (key.ctrl && key.name === 'c') { - console.log('This would have been SIGINT!'); +program.on('keypress', function(ch, key) { + if (key.name === 'q') { + program.clear(); + program.disableMouse(); + program.showCursor(); + program.normalBuffer(); + process.exit(0); } }); -program.setMouse({ normalMouse: true }); - program.on('mouse', function(data) { - console.log('Mouse event received:'); - console.log(data.button, data.x, data.y); + if (data.action === 'mouseup') return; + program.move(1, program.rows); + program.eraseInLine('right'); + if (data.action === 'wheelup') { + program.write('Mouse wheel up at: ' + data.x + ', ' + data.y); + } else if (data.action === 'wheeldown') { + program.write('Mouse wheel down at: ' + data.x + ', ' + data.y); + } else if (data.action === 'mousedown' && data.button === 'left') { + program.write('Left button down at: ' + data.x + ', ' + data.y); + } else if (data.action === 'mousedown' && data.button === 'right') { + program.write('Right button down at: ' + data.x + ', ' + data.y); + } else { + program.write('Mouse at: ' + data.x + ', ' + data.y); + } + program.move(data.x, data.y); + program.bg('red'); + program.write(' '); + program.bg('!red'); +}); + +program.on('focus', function() { + program.move(1, program.rows); + program.write('Gained focus.'); +}); + +program.on('blur', function() { + program.move(1, program.rows); + program.write('Lost focus.'); }); program.alternateBuffer(); - +program.enableMouse(); +program.hideCursor(); program.clear(); -program.bg('white'); +program.move(1, 1); +program.bg('black'); program.write('Hello world', 'blue fg'); -program.setx(1); +program.setx((program.cols / 2 | 0) - 4); program.down(5); program.write('Hi again!'); -program.bg('!white'); +program.bg('!black'); program.feed(); program.getCursor(function(err, data) { if (!err) { - console.log('Cursor is at: %s, %s.', data.x, data.y); + program.write('Cursor is at: ' + data.x + ', ' + data.y + '.'); + program.feed(); } program.charset('SCLD'); program.write('abcdefghijklmnopqrstuvwxyz0123456789'); program.charset('US'); - program.setx(0); - - setTimeout(function() { - program.eraseInLine('right'); - setTimeout(function() { - program.clear(); - program.normalBuffer(); - program.setMouse({ normalMouse: false }); - }, 2000); - }, 2000); + program.setx(1); }); ``` diff --git a/example/index.js b/example/index.js new file mode 100644 index 0000000..2d54dce --- /dev/null +++ b/example/index.js @@ -0,0 +1,77 @@ +/** + * Example Program for Blessed + * Copyright (c) 2013, Christopher Jeffrey (MIT License). + * https://github.com/chjj/blessed + */ + +var blessed = require('blessed') + , program = blessed(); + +process.title = 'blessed'; + +program.on('keypress', function(ch, key) { + if (key.name === 'q') { + program.clear(); + program.disableMouse(); + program.showCursor(); + program.normalBuffer(); + process.exit(0); + } +}); + +program.on('mouse', function(data) { + if (data.action === 'mouseup') return; + program.move(1, program.rows); + program.eraseInLine('right'); + if (data.action === 'wheelup') { + program.write('Mouse wheel up at: ' + data.x + ', ' + data.y); + } else if (data.action === 'wheeldown') { + program.write('Mouse wheel down at: ' + data.x + ', ' + data.y); + } else if (data.action === 'mousedown' && data.button === 'left') { + program.write('Left button down at: ' + data.x + ', ' + data.y); + } else if (data.action === 'mousedown' && data.button === 'right') { + program.write('Right button down at: ' + data.x + ', ' + data.y); + } else { + program.write('Mouse at: ' + data.x + ', ' + data.y); + } + program.move(data.x, data.y); + program.bg('red'); + program.write(' '); + program.bg('!red'); +}); + +program.on('focus', function() { + program.move(1, program.rows); + program.write('Gained focus.'); +}); + +program.on('blur', function() { + program.move(1, program.rows); + program.write('Lost focus.'); +}); + +program.alternateBuffer(); +program.enableMouse(); +program.hideCursor(); +program.clear(); + +program.move(1, 1); +program.bg('black'); +program.write('Hello world', 'blue fg'); +program.setx((program.cols / 2 | 0) - 4); +program.down(5); +program.write('Hi again!'); +program.bg('!black'); +program.feed(); + +program.getCursor(function(err, data) { + if (!err) { + program.write('Cursor is at: ' + data.x + ', ' + data.y + '.'); + program.feed(); + } + + program.charset('SCLD'); + program.write('abcdefghijklmnopqrstuvwxyz0123456789'); + program.charset('US'); + program.setx(1); +}); diff --git a/lib/program.js b/lib/program.js index f44ade4..162f2a5 100644 --- a/lib/program.js +++ b/lib/program.js @@ -15,6 +15,10 @@ var EventEmitter = require('events').EventEmitter; */ function Program(input, output) { + if (!(this instanceof Program)) { + return new Program(input, output); + } + var self = this; EventEmitter.call(this); @@ -348,6 +352,214 @@ Program.prototype._bindMouse = function(s) { } }; +Program.prototype.bindResponse = function() { + this.on('data', this._bindResponse.bind(this)); + this.bindResponse = function() {}; +}; + +Program.prototype._bindResponse = function(s) { + var self = this; + + // FROM: node/lib/readline.js + var ch + , key = { + name: undefined, + ctrl: false, + meta: false, + shift: false + } + , parts; + + if (Buffer.isBuffer(s)) { + if (s[0] > 127 && s[1] === undefined) { + s[0] -= 128; + s = '\x1b' + s.toString('utf-8'); + } else { + s = s.toString('utf-8'); + } + } + + // CSI Ps n Device Status Report (DSR). + // Ps = 5 -> Status Report. Result (``OK'') is + // CSI 0 n + // CSI ? Ps n + // Device Status Report (DSR, DEC-specific). + // Ps = 1 5 -> Report Printer status as CSI ? 1 0 n (ready). + // or CSI ? 1 1 n (not ready). + // Ps = 2 5 -> Report UDK status as CSI ? 2 0 n (unlocked) + // or CSI ? 2 1 n (locked). + // Ps = 2 6 -> Report Keyboard status as + // CSI ? 2 7 ; 1 ; 0 ; 0 n (North American). + // The last two parameters apply to VT400 & up, and denote key- + // board ready and LK01 respectively. + // Ps = 5 3 -> Report Locator status as + // CSI ? 5 3 n Locator available, if compiled-in, or + // CSI ? 5 0 n No Locator, if not. + if (parts = /^\x1b\[(\?)?(\d+)(?:;(\d+);(\d+);(\d+))?n/.exec(s)) { + if (!parts[1] && parts[2] === '0' && !parts[3]) { + return this.emit('response', { + deviceStatus: 'OK' + }); + } + + if (parts[1] && (parts[2] === '10' || parts[2] === '11') && !parts[3]) { + return this.emit('response', { + printerStatus: parts[2] === '10' + ? 'ready' + : 'not ready' + }); + } + + if (parts[1] && (parts[2] === '20' || parts[2] === '21') && !parts[3]) { + return this.emit('response', { + UDKStatus: parts[2] === '20' + ? 'unlocked' + : 'locked' + }); + } + + if (parts[1] + && parts[2] === '27' + && parts[3] === '1' + && parts[4] === '0' + && parts[5] === '0') { + return this.emit('response', { + keyboardStatus: 'OK' + }); + } + + if (parts[1] && (parts[2] === '53' || parts[2] === '50') && !parts[3]) { + return this.emit('response', { + locator: parts[2] === '53' + ? 'available' + : 'unavailable' + }); + } + } + +// CSI Ps n Device Status Report (DSR). +// Ps = 5 -> Status Report. Result (``OK'') is +// CSI 0 n +// Ps = 6 -> Report Cursor Position (CPR) [row;column]. +// Result is +// CSI r ; c R +// CSI ? Ps n +// Device Status Report (DSR, DEC-specific). +// Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI +// ? r ; c R (assumes page is zero). +// Ps = 1 5 -> Report Printer status as CSI ? 1 0 n (ready). +// or CSI ? 1 1 n (not ready). +// Ps = 2 5 -> Report UDK status as CSI ? 2 0 n (unlocked) +// or CSI ? 2 1 n (locked). +// Ps = 2 6 -> Report Keyboard status as +// CSI ? 2 7 ; 1 ; 0 ; 0 n (North American). +// The last two parameters apply to VT400 & up, and denote key- +// board ready and LK01 respectively. +// Ps = 5 3 -> Report Locator status as +// CSI ? 5 3 n Locator available, if compiled-in, or +// CSI ? 5 0 n No Locator, if not. + + // Ps = 6 -> Report Cursor Position (CPR) [row;column]. + // Result is + // CSI r ; c R + // Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI + // ? r ; c R (assumes page is zero). + if (parts = /^\x1b\[(\?)?(\d+);(\d+)R/.exec(s)) { + return this.emit('response', { + cursor: { + x: +parts[3], + y: +parts[2], + page: !parts[1] ? undefined : 0 + } + }); + } + + // CSI Ps ; Ps ; Ps t + // Window manipulation (from dtterm, as well as extensions). + // These controls may be disabled using the allowWindowOps + // resource. Valid values for the first (and any additional + // parameters) are: + // Ps = 1 1 -> Report xterm window state. If the xterm window + // is open (non-iconified), it returns CSI 1 t . If the xterm + // window is iconified, it returns CSI 2 t . + // Ps = 1 3 -> Report xterm window position. Result is CSI 3 + // ; x ; y t + // Ps = 1 4 -> Report xterm window in pixels. Result is CSI + // 4 ; height ; width t + // Ps = 1 8 -> Report the size of the text area in characters. + // Result is CSI 8 ; height ; width t + // Ps = 1 9 -> Report the size of the screen in characters. + // Result is CSI 9 ; height ; width t + if (parts = /^\x1b\[(\d+)(?:;(\d+);(\d+))?t/.exec(s)) { + if ((parts[1] === '1' || parts[1] === '2') && !parts[2]) { + return this.emit('response', { + windowState: parts[1] === '1' + ? 'non-iconified' + : 'iconified' + }); + } + + if (parts[1] === '3' && parts[2]) { + return this.emit('response', { + windowPosition: { + x: +parts[2], + y: +parts[3] + } + }); + } + + if (parts[1] === '4' && parts[2]) { + return this.emit('response', { + windowSizePixels: { + height: +parts[2], + width: +parts[3] + } + }); + } + + if (parts[1] === '8' && parts[2]) { + return this.emit('response', { + textAreaSizeCharacters: { + height: +parts[2], + width: +parts[3] + } + }); + } + + if (parts[1] === '9' && parts[2]) { + return this.emit('response', { + screenSizeCharacters: { + height: +parts[2], + width: +parts[3] + } + }); + } + } + + // CSI Ps ; Ps ; Ps t + // Window manipulation (from dtterm, as well as extensions). + // These controls may be disabled using the allowWindowOps + // resource. Valid values for the first (and any additional + // parameters) are: + // Ps = 2 0 -> Report xterm window's icon label. Result is + // OSC L label ST + // Ps = 2 1 -> Report xterm window's title. Result is OSC l + // label ST + if (parts = /^\x1b\](l|L)([^\x07\x1b]*)(?:\x07|\\\x1b)/.exec(s)) { + if (parts[1] === 'L') { + return this.emit('response', { + windowIconLabel: parts[2] + }); + } + + if (parts[1] === 'l') { + return this.emit('response', { + windowTitle: parts[2] + }); + } + } +}; + Program.prototype.receive = function(text, callback) { var listeners = this.listeners('keypress') , bak = listeners.slice(); @@ -386,8 +598,8 @@ Program.prototype.echo = function(text, attr) { }; Program.prototype.setx = function(x) { - // return this.cursorCharAbsolute(x); - return this.charPosAbsolute(x); + return this.cursorCharAbsolute(x); + // return this.charPosAbsolute(x); }; Program.prototype.sety = function(y) { @@ -2058,4 +2270,6 @@ Program.prototype.deleteColumns = function() { * Expose */ -module.exports = Program; +exports = Program; +exports.Program = Program; +module.exports = exports;