ansi-scrollbox
Version:
a basic scrollable area for terminal apps
131 lines (109 loc) • 3.37 kB
JavaScript
var assert = require('assert')
var wrapAnsi = require('wrap-ansi')
module.exports = function scrollbox (opts) {
opts = opts || {}
assert.strictEqual(typeof opts, 'object', 'opts must be object')
// user facing state
var content = opts.content || ''
var width = opts.width
var height = opts.height
// list of lines (split `content` according to `width`)
// so we can easily show parts of the scrollable content
var lines = []
var linesDirty = false // if true `lines` needs to be updated before use
var sub = Subscribable()
var instance = {
subscribe: sub.subscribe,
setContent: setContent,
resize: resize,
scroll: scroll,
scrollUp: scrollUp,
scrollDown: scrollDown,
keypress: keypress,
toString: toString,
get content () { return content },
set content (_content) { setContent(_content) },
get width () { return width },
get height () { return height }
}
var offset = 0
function setContent (_content) {
assert.strictEqual(typeof _content, 'string', 'content must be string')
content = _content
linesDirty = true
sub.notify()
}
function resize (s) {
assert.strictEqual(typeof s, 'object', 'size must be an object { width, height }')
assert.strictEqual(typeof s.width, 'number', 'size.width must be number')
assert.strictEqual(typeof s.height, 'number', 'size.height must be number')
width = s.width
height = s.height
linesDirty = true
sub.notify()
}
function scroll (_offset) {
assert.strictEqual(typeof _offset, 'number', 'offset must be number')
offset = _offset
sub.notify()
}
function scrollUp (d) {
scroll(offset < 0 ? offset - d : Math.max(offset - d, 0))
}
function scrollDown (d) {
scroll(offset >= 0 ? offset + d : Math.min(offset + d, -1))
}
// TODO optimize somehow
function updateLinesCache () {
lines = wrapAnsi(content, width, { wordWrap: false }).split('\n')
linesDirty = false
}
// ensure `offset` is an in-range scroll offset (first visible line index)
// or -1 (stick to bottom)
function resolveOffset () {
if (offset === -1) return
if (offset >= lines.length - height) {
offset = Math.max(0, lines.length - height)
} else if (offset < -1) {
offset = Math.max(0, lines.length + offset + 1 - height)
}
}
function toString () {
if (linesDirty) updateLinesCache()
resolveOffset()
var o = offset === -1 ? lines.length - height : Math.min(lines.length - height, offset)
var visible = lines.slice(o, o + height)
// fill the rest of the space so height doesn't vary
while (visible.length < height) visible.push('')
return visible.join('\n')
}
function keypress (ch, key) {
if (!key) return
if (key.name === 'down' || key.name === 'j') {
scrollDown(1)
}
if (key.name === 'up' || key.name === 'k') {
scrollUp(1)
}
if (key.name === 'home') offset = 0
if (key.name === 'end') offset = -1
}
return instance
}
function Subscribable () {
var listeners = []
function subscribe (fn) {
listeners.push(fn)
return function () {
var i = listeners.indexOf(fn)
if (i !== -1) listeners.splice(i, 1)
}
}
function notify () {
listeners.forEach(function (fn) { fn() })
}
return {
subscribe: subscribe,
notify: notify
}
}