├── README.md ├── ex.js ├── ex2.js ├── index.js ├── package.json ├── screenshot.png └── util.js /README.md: -------------------------------------------------------------------------------- 1 | # txt-blit 2 | 3 | > Draw lines of text onto other lines of text. 4 | 5 | Handy low-level module that can be used for building 6 | [choo](https://github.com/choojs/choo)-like apps for the terminal. 7 | 8 | Combines nicely with [neat-log](https://github.com/joehand/neat-log) for 9 | building reactive terminal apps. 10 | 11 | Unicode code points of 0-2 fixed-width font cells are supported, to the extent 12 | made possible by [wcswidth](https://github.com/timoxley/wcwidth). 13 | 14 | # Usage: simple component 15 | 16 | Let's make a reusable render component (a pure function that returns an array of 17 | text lines) and draw it at different offsets: 18 | 19 | ```js 20 | var blit = require('txt-blit') 21 | 22 | function renderHelloBox () { 23 | return [ 24 | '|-----------------------|', 25 | '| |', 26 | '| HELLO THERE |', 27 | '| ~~~~~~~~~~~~~~~ |', 28 | '| |', 29 | '|-----------------------|', 30 | ] 31 | } 32 | 33 | var screen = [] // empty; blank screen 34 | 35 | blit(screen, renderHelloBox(), 2, 2) 36 | blit(screen, renderHelloBox(), 25, 10) 37 | blit(screen, renderHelloBox(), 10, 7) 38 | 39 | console.log(screen.join('\n')) 40 | ``` 41 | 42 | outputs 43 | 44 | ``` 45 | 46 | 47 | |-----------------------| 48 | | | 49 | | HELLO THERE | 50 | | ~~~~~~~~~~~~~~~ | 51 | | | 52 | |-------|-----------------------| 53 | | | 54 | | HELLO THERE | 55 | | ~~~~~~~~~~~~~~~ |--------------| 56 | | | | 57 | |-----------------------|LO THERE | 58 | | ~~~~~~~~~~~~~~~ | 59 | | | 60 | |-----------------------| 61 | ``` 62 | 63 | ## Usage: reactive terminal app 64 | 65 | Let's combine txt-blit with [neat-log](https://github.com/joehand/neat-log) to 66 | combine some dynamic components: 67 | 68 | ```js 69 | var blit = require('.') 70 | var neatLog = require('neat-log') 71 | var chalk = require('chalk') 72 | 73 | var app = neatLog(view) 74 | app.use(countTheSeconds) 75 | 76 | // update state 77 | function countTheSeconds (state, bus) { 78 | state.seconds = 0 79 | setInterval(function () { 80 | state.seconds++ 81 | bus.emit('render') 82 | }, 200) 83 | } 84 | 85 | // draw screen based on state 86 | function view (state) { 87 | var screen = [] 88 | 89 | // move hello-box in a sine wave 90 | var x = Math.floor(Math.sin(state.seconds / 5.0) * 10 + process.stdout.columns/2) 91 | blit(screen, renderHelloBox(), x, 3) 92 | 93 | // draw timer 94 | blit(screen, renderTimer(state), 10, 10) 95 | 96 | // dump screen string to standard out 97 | return screen.join('\n') 98 | } 99 | 100 | function renderHelloBox (state) { 101 | return [ 102 | '|-----------------------|', 103 | '| |', 104 | '| HELLO THERE |', 105 | '| ~~~~~~~~~~~~~~~ |', 106 | '| |', 107 | '|-----------------------|', 108 | ] 109 | } 110 | 111 | function renderTimer (state) { 112 | var colours = [ 113 | chalk.black, 114 | chalk.red, 115 | chalk.green, 116 | chalk.yellow, 117 | chalk.blue, 118 | chalk.magenta, 119 | chalk.cyan, 120 | chalk.white, 121 | chalk.gray, 122 | chalk.redBright, 123 | chalk.greenBright, 124 | chalk.yellowBright, 125 | chalk.blueBright, 126 | chalk.magentaBright, 127 | chalk.cyanBright, 128 | chalk.whiteBright 129 | ] 130 | 131 | var colourize = colours[Math.floor(Math.random() * colours.length)] 132 | 133 | return [ 134 | colours[7]('|TIME: ') + colourize(state.seconds) + colours[7](' |') 135 | ] 136 | } 137 | 138 | function renderHelloBox () { 139 | return [ 140 | '|-----------------------|', 141 | '| |', 142 | '| HELLO THERE |', 143 | '| ~~~~~~~~~~~~~~~ |', 144 | '| |', 145 | '|-----------------------|', 146 | ] 147 | } 148 | 149 | ``` 150 | 151 | outputs something like 152 | 153 | ![screenshot of dancing hello box and incrementing timer](screenshot.png) 154 | 155 | ## API 156 | 157 | ```js 158 | var blit = require('txt-blit') 159 | ``` 160 | 161 | ### blit(screen, component, x, y) 162 | 163 | Mutates the array of strings (lines) `screen` so that the array of strings 164 | `component` is drawn at the offset `x, y`. 165 | 166 | `blit` is smart enough to recognize ANSI escape codes (like colours) and compute 167 | the correct offsets. 168 | 169 | `blit` operates fastest if you pass in an array of lines for `screen`, but you 170 | can also pass in a newline-delimited string and `blit` will split/re-join it for 171 | you, though it will not be as fast. 172 | 173 | ## Install 174 | 175 | With [npm](https://npmjs.org/) installed, run 176 | 177 | ``` 178 | $ npm install txt-blit 179 | ``` 180 | 181 | ## License 182 | 183 | ISC 184 | -------------------------------------------------------------------------------- /ex.js: -------------------------------------------------------------------------------- 1 | var blit = require('.') 2 | 3 | function renderHelloBox () { 4 | return [ 5 | '|-----------------------|', 6 | '| |', 7 | '| HELLO THERE |', 8 | '| ~~~~~~~~~~~~~~~ |', 9 | '| |', 10 | '| 🥰 🌸 😳 |', 11 | '| |', 12 | '|-----------------------|' 13 | ] 14 | } 15 | 16 | var screen = [] // empty; blank screen 17 | 18 | blit(screen, renderHelloBox(), 2, 2) 19 | blit(screen, renderHelloBox(), 25, 10) 20 | blit(screen, renderHelloBox(), 10, 7) 21 | 22 | console.log(screen.join('\n')) 23 | -------------------------------------------------------------------------------- /ex2.js: -------------------------------------------------------------------------------- 1 | var blit = require('.') 2 | var neatLog = require('neat-log') 3 | var chalk = require('chalk') 4 | 5 | var app = neatLog(view) 6 | app.use(countTheSeconds) 7 | 8 | var colours = [ 9 | chalk.black, 10 | chalk.red, 11 | chalk.green, 12 | chalk.yellow, 13 | chalk.blue, 14 | chalk.magenta, 15 | chalk.cyan, 16 | chalk.white, 17 | chalk.gray 18 | ] 19 | 20 | function view (state) { 21 | var screen = [] 22 | 23 | var x = Math.floor(Math.sin(state.seconds / 5.0) * 10 + process.stdout.columns / 2) 24 | blit(screen, renderHelloBox(), x, 3) 25 | blit(screen, renderTimer(state), 10, 10) 26 | 27 | return screen.join('\n') 28 | } 29 | 30 | function countTheSeconds (state, bus) { 31 | state.seconds = 0 32 | setInterval(function () { 33 | state.seconds++ 34 | bus.emit('render') 35 | }, 250) 36 | } 37 | 38 | function renderTimer (state) { 39 | var colourize = colours[Math.floor(Math.random() * colours.length)] 40 | 41 | return [ 42 | colours[7]('|TIME: ') + colourize(state.seconds) + colours[7](' |') 43 | ] 44 | } 45 | 46 | function renderHelloBox () { 47 | return [ 48 | '|-----------------------|', 49 | '| |', 50 | '| HELLO THERE |', 51 | '| ~~~~~~~~~~~~~~~ |', 52 | '| |', 53 | '| IN Юあ㏲㈝🥲 UNICODE |', 54 | '| |', 55 | '|-----------------------|' 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var util = require('./util') 2 | module.exports = blit 3 | 4 | // Applies 'lines' to 'screen' at coordinates x/y 5 | function blit (screen, lines, x, y) { 6 | var tmp = screen 7 | if (typeof screen === 'string') tmp = screen.split('\n') 8 | 9 | // add any extra needed lines 10 | var extraLinesNeeded = (y + lines.length) - tmp.length 11 | if (extraLinesNeeded > 0) { 12 | tmp.push.apply(tmp, new Array(extraLinesNeeded).fill('')) 13 | } 14 | 15 | // patch lines 16 | for (var i = y; i < y + lines.length; i++) { 17 | tmp[i] = mergeString(tmp[i], lines[i - y], x) 18 | } 19 | 20 | if (typeof screen === 'string') return tmp.join('\n') 21 | else return tmp 22 | } 23 | 24 | // String, String -> String 25 | function mergeString (src, string, x) { 26 | var extraWidthNeeded = (x - 1 + util.strlenAnsi(string)) - util.strlenAnsi(src) 27 | if (extraWidthNeeded > 0) { 28 | src += (new Array(extraWidthNeeded).fill(' ')).join('') 29 | } 30 | 31 | return util.sliceAnsi(src, 0, x) + string + util.sliceAnsi(src, x + util.strlenAnsi(string)) 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "txt-blit", 3 | "description": "draw lines of text onto other lines of text", 4 | "author": "Kira Oakley ", 5 | "version": "2.0.1", 6 | "repository": { 7 | "url": "git://github.com/hackergrrl/txt-blit.git" 8 | }, 9 | "homepage": "https://github.com/hackergrrl/txt-blit", 10 | "bugs": "https://github.com/hackergrrl/txt-blit/issues", 11 | "main": "index.js", 12 | "scripts": { 13 | "lint": "standard" 14 | }, 15 | "keywords": [], 16 | "dependencies": { 17 | "strip-ansi": "^6.0.1", 18 | "wcwidth": "^1.0.1" 19 | }, 20 | "devDependencies": { 21 | "neat-log": "^3.0.1", 22 | "standard": "~10.0.0" 23 | }, 24 | "license": "ISC" 25 | } 26 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackergrrl/txt-blit/ba4b8643256e72ddaff6b984e32159e69d5271ec/screenshot.png -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | module.exports = { sliceAnsi, strlenAnsi } 2 | var stripAnsi = require('strip-ansi') 3 | var wcwidth = require('wcwidth') 4 | 5 | // Like String#slice, but taking ANSI codes into account. 6 | function sliceAnsi (str, from, to) { 7 | var len = 0 8 | var insideCode = false 9 | var res = '' 10 | to = (to === undefined) ? str.length : to 11 | 12 | for (var i=0; i < str.length; i++) { 13 | var chr = str.charAt(i) 14 | if (chr === '\033') insideCode = true 15 | if (!insideCode) len += wcwidth(chr) 16 | if (chr === 'm' && insideCode) insideCode = false 17 | 18 | if (len > from && len <= to) { 19 | res += chr 20 | } 21 | } 22 | 23 | return res 24 | } 25 | 26 | // Returns the horizontal visual extent (# of fixed-width chars) a string takes 27 | // up, taking ANSI escape codes into account. Assumes a UTF-8 encoded string. 28 | function strlenAnsi (str) { 29 | str = stripAnsi(str) 30 | return wcwidth(str) 31 | } 32 | --------------------------------------------------------------------------------