diff --git a/README.md b/README.md index fc90d48..07e5119 100644 --- a/README.md +++ b/README.md @@ -307,7 +307,7 @@ The screen on which every other node renders. true if successful. - **cursorColor(color)** - attempt to change cursor color. returns true if successful. -- **resetCursor()** - attempt to reset cursor. returns true if successful. +- **cursorReset()** - attempt to reset cursor. returns true if successful. #### Element (from Node) @@ -989,15 +989,34 @@ allowing you to have a custom cursor that you control. ``` js var screen = blessed.screen({ - artificialCursor: true, - cursorShape: 'line', - cursorBlink: true, - cursorColor: null // null for default + cursor: { + artificial: true, + shape: 'line', + blink: true, + color: null // null for default + } }); ``` That's it. It's controlled the same way as the regular cursor. +To create a custom cursor: + +``` js +var screen = blessed.screen({ + cursor: { + artificial: true, + shape: { + bg: 'red', + fg: 'white', + bold: true, + ch: '#' + }, + blink: true + } +}); +``` + ### Positioning diff --git a/lib/colors.js b/lib/colors.js index 34b2fd3..96a6c7e 100644 --- a/lib/colors.js +++ b/lib/colors.js @@ -374,13 +374,17 @@ exports.ccolors = { ] }; +exports.ncolors = []; + Object.keys(exports.ccolors).forEach(function(name) { exports.ccolors[name].forEach(function(offset) { if (typeof offset === 'number') { + exports.ncolors[offset] = name; exports.ccolors[offset] = exports.colorNames[name]; return; } for (var i = offset[0], l = offset[1]; i <= l; i++) { + exports.ncolors[i] = name; exports.ccolors[i] = exports.colorNames[name]; } }); diff --git a/lib/program.js b/lib/program.js index a881ca9..24c020a 100644 --- a/lib/program.js +++ b/lib/program.js @@ -85,6 +85,10 @@ function Program(options) { || this.isTerminator || this.isLXDE; + // xterm and rxvt - not accurate + this.isRxvt = /rxvt/i.test(process.env.COLORTERM); + this.isXterm = false; + this._buf = ''; this._flush = this.flush.bind(this); @@ -329,10 +333,24 @@ Program.prototype.listen = function() { }); // Output - this.output.on('resize', function() { + function resize() { self.cols = self.output.columns; self.rows = self.output.rows; self.emit('resize'); + } + + this.output.on('resize', function() { + if (!self.options.resizeTimeout) { + return resize(); + } + if (self._resizeTimer) { + clearTimeout(self._resizeTimer); + delete self._resizeTimer; + } + var time = typeof self.options.resizeTimeout === 'number' + ? self.options.resizeTimeout + : 300; + self._resizeTimer = setTimeout(resize, time); }); }; @@ -1378,6 +1396,18 @@ Program.prototype._bindResponse = function(s) { return; } + + // OSC Ps ; Pt BEL + // OSC Ps ; Pt ST + // Set Text Parameters + if (parts = /^\x1b\](\d+);([^\x07\x1b]+)(?:\x07|\x1b\\)/.exec(s)) { + out.event = 'text-params'; + out.code = 'Set Text Parameters'; + out.ps = +s[1]; + out.pt = s[2]; + this.emit('response', out); + this.emit('response ' + out.event, out); + } }; Program.prototype.response = function(name, text, callback, noBypass) { @@ -1595,7 +1625,7 @@ Program.prototype.cursorShape = function(shape, blink) { break; } return true; - } else if (this.term('xterm')) { + } else if (this.term('xterm') || this.term('screen')) { switch (shape) { case 'block': if (!blink) { @@ -1625,28 +1655,38 @@ Program.prototype.cursorShape = function(shape, blink) { }; Program.prototype.cursorColor = function(color) { - if (this.term('xterm') || this.term('rxvt')) { + if (this.term('xterm') || this.term('rxvt') || this.term('screen')) { this._twrite('\x1b]12;' + color + '\x07'); return true; } return false; }; +Program.prototype.cursorReset = Program.prototype.resetCursor = function() { - if (this.term('rxvt')) { - // urxvt doesnt support OSC 112 - this._twrite('\x1b]12;white\007'); - this._twrite('\x1b[0 q'); - return true; - } else if (this.term('xterm')) { + if (this.term('xterm') || this.term('rxvt') || this.term('screen')) { // XXX // return this.resetColors(); + this._twrite('\x1b[0 q'); this._twrite('\x1b]112\x07'); + // urxvt doesnt support OSC 112 + this._twrite('\x1b]12;white\x07'); return true; } return false; }; +Program.prototype.getTextParams = function(param, callback) { + return this.response('text-params', '\x1b]' + param + ';?\x07', function(err, data) { + if (err) return callback(err); + return callback(null, data.pt); + }); +}; + +Program.prototype.getCursorColor = function(callback) { + return this.getTextParams(12, callback); +}; + /** * Normal */ diff --git a/lib/widget.js b/lib/widget.js index 758fc45..3dda68d 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -261,6 +261,7 @@ function Screen(options) { debug: options.debug, dump: options.dump, term: options.term, + resizeTimeout: options.resizeTimeout, tput: true, buffer: true, zero: true @@ -269,6 +270,7 @@ function Screen(options) { this.program.setupTput(); this.program.useBuffer = true; this.program.zero = true; + this.program.options.resizeTimeout = options.resizeTimeout; } this.tput = this.program.tput; @@ -325,56 +327,30 @@ function Screen(options) { this.title = options.title; } - if (options.artificialCursor) { - this.artificialCursor = true; - this.cursorShape = options.cursorShape || 'block'; - this.cursorBlink = options.cursorBlink || false; - this.cursorColor = options.cursorColor || null; - if (this.cursorColor) { - this.cursorColor = colors.convert(this.cursorColor); - } - this.cursorState = 1; - this._cursorHidden = true; - var hideCursor = this.program.hideCursor; - this.program.hideCursor = function() { - hideCursor.call(self.program); - self._cursorHidden = true; - if (self.renders) self.render(); - }; - var showCursor = this.program.showCursor; - this.program.showCursor = function() { - self._cursorHidden = false; - if (self.program._exiting) showCursor.call(self.program); - if (self.renders) self.render(); - }; - this._blink = setInterval(function() { - if (!self.cursorBlink) return; - self.cursorState ^= 1; - if (self.renders) self.render(); - }, 500); - } + options.cursor = options.cursor || { + artificial: options.artificialCursor, + shape: options.cursorShape, + blink: options.cursorBlink, + color: options.cursorColor + }; - function resize() { + this.cursor = { + artificial: options.cursor.artificial || false, + shape: options.cursor.shape || 'block', + blink: options.cursor.blink || false, + color: options.cursor.color || null, + _set: false, + _state: 1, + _hidden: true + }; + + this.program.on('resize', function() { self.alloc(); self.render(); (function emit(el) { el.emit('resize'); el.children.forEach(emit); })(self); - } - - this.program.on('resize', function() { - if (!self.options.resizeTimeout) { - return resize(); - } - if (self._resizeTimer) { - clearTimeout(self._resizeTimer); - delete self._resizeTimer; - } - var time = typeof self.options.resizeTimeout === 'number' - ? self.options.resizeTimeout - : 300; - self._resizeTimer = setTimeout(resize, time); }); this.program.on('focus', function() { @@ -385,7 +361,24 @@ function Screen(options) { self.emit('blur'); }); - this.enter(); + this.on('newListener', function fn(type) { + if (type === 'keypress' || type.indexOf('key ') === 0 || type === 'mouse') { + if (type === 'keypress' || type.indexOf('key ') === 0) self._listenKeys(); + if (type === 'mouse') self._listenMouse(); + } + if (type === 'mouse' + || type === 'click' + || type === 'mouseover' + || type === 'mouseout' + || type === 'mousedown' + || type === 'mouseup' + || type === 'mousewheel' + || type === 'wheeldown' + || type === 'wheelup' + || type === 'mousemove') { + self._listenMouse(); + } + }); // XXX Not used right now since we're using our own EventEmitter: this.setMaxListeners(Infinity); @@ -419,24 +412,7 @@ function Screen(options) { self.leave(); }); - this.on('newListener', function fn(type) { - if (type === 'keypress' || type.indexOf('key ') === 0 || type === 'mouse') { - if (type === 'keypress' || type.indexOf('key ') === 0) self._listenKeys(); - if (type === 'mouse') self._listenMouse(); - } - if (type === 'mouse' - || type === 'click' - || type === 'mouseover' - || type === 'mouseout' - || type === 'mousedown' - || type === 'mouseup' - || type === 'mousewheel' - || type === 'wheeldown' - || type === 'wheelup' - || type === 'mousemove') { - self._listenMouse(); - } - }); + this.enter(); } Screen.global = null; @@ -457,6 +433,14 @@ Screen.prototype.__defineSetter__('title', function(title) { Screen.prototype.enter = function() { if (this.program.isAlt) return; + if (!this.cursor._set) { + if (this.options.cursor.shape) { + this.cursorShape(this.cursor.shape, this.cursor.blink); + } + if (this.options.cursor.color) { + this.cursorColor(this.cursor.color); + } + } this.program.alternateBuffer(); this.program.hideCursor(); this.program.cup(0, 0); @@ -477,6 +461,7 @@ Screen.prototype.leave = function() { this.program.disableMouse(); } this.program.normalBuffer(); + if (this.cursor._set) this.cursorReset(); this.program.flush(); }; @@ -705,10 +690,6 @@ Screen.prototype.render = function() { }); this._ci = -1; - // Cannot use this.rows here because of the resize delay: - // `rows` and `cols` get set instantly by the resize, but `alloc` has a 300ms - // delay before the new cells are added, which will sometimes throw. Measure - // by lines. this.draw(0, this.lines.length - 1); // XXX Workaround to deal with cursor pos before the screen has rendered and @@ -951,8 +932,7 @@ Screen.prototype.draw = function(start, end) { line = this.lines[y]; o = this.olines[y]; - // if (!line.dirty) continue; - if (!line.dirty && !(this.artificialCursor && y === this.program.y)) { + if (!line.dirty && !(this.cursor.artificial && y === this.program.y)) { continue; } line.dirty = false; @@ -960,36 +940,19 @@ Screen.prototype.draw = function(start, end) { out = ''; attr = this.dattr; - // Cannot use this.cols here because of the resize delay: - // `rows` and `cols` get set instantly by the resize, but `alloc` has a - // 300ms delay before the new cells are added, which will sometimes throw. - // Measure by line length. for (x = 0; x < line.length; x++) { data = line[x][0]; ch = line[x][1]; - if (this.artificialCursor - && !this._cursorHidden - && this.cursorState + // Render the artificial cursor. + if (this.cursor.artificial + && !this.cursor._hidden + && this.cursor._state && x === this.program.x && y === this.program.y) { - if (this.cursorShape === 'line') { - data &= ~(0x1ff << 9); - data |= 7 << 9; - ch = '\u2502'; - } else if (this.cursorShape === 'underline') { - data &= ~(0x1ff << 9); - data |= 7 << 9; - data |= 2 << 18; - } else if (this.cursorShape === 'block') { - data &= ~(0x1ff << 9); - data |= 7 << 9; - data |= 8 << 18; - } - if (this.cursorColor != null) { - data &= ~(0x1ff << 9); - data |= this.cursorColor << 9; - } + var cattr = this._cursorAttr(this.cursor, data); + if (cattr.ch) ch = cattr.ch; + data = cattr.attr; } // Take advantage of xterm's back_color_erase feature by using a @@ -1817,30 +1780,139 @@ Screen.prototype.copyToClipboard = function(text) { }; Screen.prototype.cursorShape = function(shape, blink) { - if (this.artificialCursor) { - this.cursorShape = shape; - this.cursorBlink = blink; + var self = this; + + this.cursor.shape = shape || 'block'; + this.cursor.blink = blink || false; + this.cursor._set = true; + + if (this.cursor.artificial) { + if (!this.program.hideCursor_old) { + var hideCursor = this.program.hideCursor; + this.program.hideCursor_old = this.program.hideCursor; + this.program.hideCursor = function() { + hideCursor.call(self.program); + self.cursor._hidden = true; + if (self.renders) self.render(); + }; + } + if (!this.program.showCursor_old) { + var showCursor = this.program.showCursor; + this.program.showCursor_old = this.program.showCursor; + this.program.showCursor = function() { + self.cursor._hidden = false; + if (self.program._exiting) showCursor.call(self.program); + if (self.renders) self.render(); + }; + } + if (!this._cursorBlink) { + this._cursorBlink = setInterval(function() { + if (!self.cursor.blink) return; + self.cursor._state ^= 1; + if (self.renders) self.render(); + }, 500); + this._cursorBlink.unref(); + } return true; } - return this.program.cursorShape(shape, blink); + + return this.program.cursorShape(this.cursor.shape, this.cursor.blink); }; Screen.prototype.cursorColor = function(color) { - if (this.artificialCursor) { - this.cursorColor = colors.convert(color); + this.cursor.color = color != null + ? colors.convert(color) + : null; + this.cursor._set = true; + + if (this.cursor.artificial) { return true; } - return this.program.cursorColor(color); + + return this.program.cursorColor(colors.ncolors[this.cursor.color]); }; +Screen.prototype.cursorReset = Screen.prototype.resetCursor = function() { - if (this.artificialCursor) { - this.cursorShape = 'block'; - this.cursorBlink = false; - this.cursorColor = null; + this.cursor.shape = 'block'; + this.cursor.blink = false; + this.cursor.color = null; + this.cursor._set = false; + + if (this.cursor.artificial) { + this.cursor.artificial = false; + if (this.program.hideCursor_old) { + this.program.hideCursor = this.program.hideCursor_old; + delete this.program.hideCursor_old; + } + if (this.program.showCursor_old) { + this.program.showCursor = this.program.showCursor_old; + delete this.program.showCursor_old; + } + if (this._cursorBlink) { + clearInterval(this._cursorBlink); + delete this._cursorBlink; + } return true; } - return this.program.resetCursor(); + + return this.program.cursorReset(); +}; + +Screen.prototype._cursorAttr = function(cursor, dattr) { + var attr = dattr || this.dattr + , cattr + , ch; + + if (cursor.shape === 'line') { + attr &= ~(0x1ff << 9); + attr |= 7 << 9; + ch = '\u2502'; + } else if (cursor.shape === 'underline') { + attr &= ~(0x1ff << 9); + attr |= 7 << 9; + attr |= 2 << 18; + } else if (cursor.shape === 'block') { + attr &= ~(0x1ff << 9); + attr |= 7 << 9; + attr |= 8 << 18; + } else if (typeof cursor.shape === 'object' && cursor.shape) { + cattr = Element.prototype.sattr.call(cursor, + cursor.shape, + cursor.shape.fg, + cursor.shape.bg); + + if (cursor.shape.bold || cursor.shape.underline + || cursor.shape.blink || cursor.shape.inverse + || cursor.shape.invisible) { + attr &= ~(0x1ff << 18); + attr |= ((cattr >> 18) & 0x1ff) << 18; + } + + if (cursor.shape.fg) { + attr &= ~(0x1ff << 9); + attr |= ((cattr >> 9) & 0x1ff) << 9; + } + + if (cursor.shape.bg) { + attr &= ~(0x1ff << 0); + attr |= cattr & 0x1ff; + } + + if (cursor.shape.ch) { + ch = cursor.shape.ch; + } + } + + if (cursor.color != null) { + attr &= ~(0x1ff << 9); + attr |= cursor.color << 9; + } + + return { + ch: ch, + attr: attr + }; }; /** @@ -4572,12 +4644,12 @@ List.prototype.appendItem = function(item) { if (this.mouse) { item.on('click', function(data) { + self.focus(); if (self.items[self.selected] === item) { self.emit('action', item, self.selected); self.emit('select', item, self.selected); return; } - self.focus(); self.select(item); self.screen.render(); }); diff --git a/test/widget.js b/test/widget.js index b818e74..cc00440 100644 --- a/test/widget.js +++ b/test/widget.js @@ -4,11 +4,13 @@ var blessed = require('../') screen = blessed.screen({ dump: __dirname + '/logs/widget.log', title: 'widget test', - artificialCursor: true, - cursorShape: 'line', - cursorBlink: true, - cursorColor: null - //cursorColor: 'red' + resizeTimeout: 300, + cursor: { + artificial: true, + shape: 'line', + blink: true, + color: null + } }); screen.append(blessed.text({