├── .gitignore ├── TermUI.coffee ├── TermUI.js ├── examples └── Buttons.coffee ├── package.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /TermUI.coffee: -------------------------------------------------------------------------------- 1 | keypress = require 'keypress' 2 | util = require 'util' 3 | tty = require 'tty' 4 | {EventEmitter} = require 'events' 5 | _ = require 'underscore' 6 | _.mixin require 'underscore.string' 7 | 8 | # ===============================================================[ TermUI ]==== 9 | module.exports = T = new class TermUI extends EventEmitter 10 | constructor: -> 11 | 12 | if tty.isatty process.stdin 13 | process.stdin.setRawMode true 14 | process.stdin.resume() 15 | keypress process.stdin 16 | 17 | process.stdin.on 'keypress', @handleKeypress 18 | process.stdin.on 'data', @handleData 19 | 20 | if process.listeners('SIGWINCH').length is 0 21 | process.on 'SIGWINCH', @handleSizeChange 22 | 23 | @handleSizeChange() 24 | 25 | @enableMouse() 26 | @isTerm = true 27 | else 28 | @isTerm = false 29 | 30 | C: { k: 0, r: 1, g: 2, y: 3, b: 4, m: 5, c: 6, w: 7, x: 9 } 31 | 32 | S: { normal: 0, bold: 1, underline: 4, blink: 5, inverse: 8 } 33 | 34 | SYM: { 35 | star: '\u2605', 36 | check: '\u2714', 37 | x: '\u2718', 38 | triUp: '\u25b2', 39 | triDown: '\u25bc', 40 | triLeft: '\u25c0', 41 | triRight: '\u25b6', 42 | fn: '\u0192', 43 | arrowUp: '\u2191', 44 | arrowDown: '\u2193', 45 | arrowLeft: '\u2190', 46 | arrowRight: '\u2192' 47 | } 48 | 49 | handleSizeChange: => 50 | winsize = process.stdout.getWindowSize() 51 | @width = winsize[1] 52 | @height = winsize[0] 53 | @emit 'resize', {w: @width, h: @height} 54 | 55 | out: (buf) -> 56 | if @isTerm 57 | process.stdout.write(buf) 58 | this 59 | 60 | hideCursor: -> 61 | @out '\x1b[?25l' 62 | 63 | showCursor: -> 64 | @out '\x1b[?25h' 65 | 66 | clear: -> 67 | @out '\x1b[2J' 68 | @home 69 | this 70 | 71 | pos: (x, y) -> 72 | x = if x < 0 then @width - x else x 73 | y = if y < 0 then @height - y else y 74 | x = Math.max(Math.min(x, @width), 1) 75 | y = Math.max(Math.min(y, @height), 1) 76 | @out "\x1b[#{y};#{x}H" 77 | this 78 | 79 | home: -> 80 | @pos 1, 1 81 | this 82 | 83 | end: -> 84 | @pos 1, -1 85 | this 86 | 87 | fg: (c) -> 88 | @out "\x1b[3#{c}m" 89 | this 90 | 91 | bg: (c) -> 92 | @out "\x1b[4#{c}m" 93 | this 94 | 95 | hifg: (c) -> 96 | @out "\x1b[38;5;#{c}m" 97 | this 98 | 99 | hibg: (c) -> 100 | @out "\x1b[48;5;#{c}m" 101 | this 102 | 103 | enableMouse: -> 104 | @out '\x1b[?1000h' 105 | @out '\x1b[?1002h' 106 | this 107 | 108 | disableMouse: -> 109 | @out '\x1b[?1000l' 110 | @out '\x1b[?1002l' 111 | this 112 | 113 | eraseLine: -> 114 | @out '\x1b[2K' 115 | this 116 | 117 | handleKeypress: (c, key) => 118 | if (key && key.ctrl && key.name == 'c') 119 | @quit() 120 | else 121 | @emit 'keypress', c, key 122 | 123 | handleData: (d) => 124 | eventData = {} 125 | buttons = [ 'left', 'middle', 'right' ] 126 | 127 | if d[0] is 0x1b and d[1] is 0x5b && d[2] is 0x4d # mouse event 128 | 129 | switch (d[3] & 0x60) 130 | 131 | when 0x20 # button 132 | if (d[3] & 0x3) < 0x3 133 | event = 'mousedown' 134 | eventData.button = buttons[ d[3] & 0x3 ] 135 | else 136 | event = 'mouseup' 137 | 138 | when 0x40 # drag 139 | event = 'drag' 140 | if (d[3] & 0x3) < 0x3 141 | eventData.button = buttons[ d[3] & 0x3 ] 142 | 143 | when 0x60 # scroll 144 | event = 'wheel' 145 | if d[3] & 0x1 146 | eventData.direction = 'down' 147 | else 148 | eventData.direction = 'up' 149 | 150 | eventData.shift = (d[3] & 0x4) > 0 151 | eventData.x = d[4] - 32 152 | eventData.y = d[5] - 32 153 | 154 | @emit event, eventData 155 | @emit 'any', event, eventData 156 | 157 | quit: -> 158 | @fg(@C.x).bg(@C.x) 159 | @disableMouse() 160 | @showCursor() 161 | process.stdin.setRawMode false 162 | process.exit() 163 | 164 | 165 | # ===============================================================[ Widget ]==== 166 | class T.Widget extends EventEmitter 167 | constructor: (@options = {}) -> 168 | @bounds = { 169 | x: @options.bounds?.x or 0 170 | y: @options.bounds?.y or 0 171 | w: @options.bounds?.w or 0 172 | h: @options.bounds?.h or 0 173 | } 174 | 175 | T.Widget.instances.unshift this 176 | 177 | draw: -> 178 | 179 | hitTest: (x, y) -> 180 | (@bounds.x <= x <= (@bounds.x + @bounds.w - 1)) and 181 | (@bounds.y <= y <= (@bounds.y + @bounds.h - 1)) 182 | 183 | T.Widget.instances = [] 184 | 185 | T.on 'any', (event, eventData) -> 186 | for widget in T.Widget.instances 187 | if widget.hitTest eventData.x, eventData.y 188 | eventData.target = widget 189 | widget.emit event, eventData 190 | 191 | # ===============================================================[ Button ]==== 192 | class T.Button extends T.Widget 193 | constructor: (opts) -> 194 | super(opts) 195 | 196 | @fg = @options.fg ? T.C.w 197 | @bg = @options.fg ? T.C.b 198 | 199 | @label = @options.label ? '' 200 | 201 | # labelAnchor values correspond to the locations of the numbers on a 202 | # standard keyboard numpad 203 | # 7 8 9 204 | # 4 5 6 205 | # 1 2 3 206 | @labelAnchor = @options.labelAnchor ? 5 207 | 208 | draw: -> 209 | T.fg(@fg).bg(@bg).pos(@bounds.x, @bounds.y) 210 | 211 | align = ['lpad', 'rpad', 'center'][@labelAnchor % 3] 212 | labelStr = _[align] @label, @bounds.w, ' ' 213 | 214 | if @bounds.h > 1 215 | emptyStr = _.pad ' ', @bounds.w, ' ' 216 | 217 | switch ((@labelAnchor-1) / 3) | 0 218 | when 0 then labelRow = @bounds.y + @bounds.h - 1 219 | when 1 then labelRow = @bounds.y + (@bounds.h / 2) | 0 220 | when 2 then labelRow = @bounds.y 221 | 222 | for y in [(@bounds.y)..(@bounds.y + @bounds.h - 1)] 223 | T.pos @bounds.x, y 224 | if y is labelRow 225 | T.out labelStr 226 | else 227 | T.out emptyStr 228 | T.fg(T.C.x).bg(T.C.x).end() 229 | -------------------------------------------------------------------------------- /TermUI.js: -------------------------------------------------------------------------------- 1 | require('coffee-script'); 2 | module.exports = require('./TermUI.coffee'); 3 | -------------------------------------------------------------------------------- /examples/Buttons.coffee: -------------------------------------------------------------------------------- 1 | T = require '../TermUI' 2 | T.enableMouse() 3 | T.hideCursor() 4 | 5 | T.clear() 6 | 7 | # First Row 8 | b7 = new T.Button 9 | bounds: 10 | x: 1 11 | y: 1 12 | w: 20 13 | h: 3 14 | label: 'hello 7' 15 | labelAnchor: 7 16 | b7.draw() 17 | 18 | b8 = new T.Button 19 | bounds: 20 | x: 22 21 | y: 1 22 | w: 20 23 | h: 3 24 | label: 'hello 8' 25 | labelAnchor: 8 26 | b8.draw() 27 | 28 | b9 = new T.Button 29 | bounds: 30 | x: 43 31 | y: 1 32 | w: 20 33 | h: 3 34 | label: 'hello 9' 35 | labelAnchor: 9 36 | b9.draw() 37 | 38 | 39 | # Second Row 40 | b4 = new T.Button 41 | bounds: 42 | x: 1 43 | y: 5 44 | w: 20 45 | h: 3 46 | label: 'hello 4' 47 | labelAnchor: 4 48 | b4.draw() 49 | 50 | b5 = new T.Button 51 | bounds: 52 | x: 22 53 | y: 5 54 | w: 20 55 | h: 3 56 | label: 'hello 5' 57 | labelAnchor: 5 58 | b5.draw() 59 | 60 | b5.on 'mousedown', -> 61 | b5.bg = T.C.y 62 | b5.draw() 63 | 64 | b5.on 'mouseup', -> 65 | b5.bg = T.C.b 66 | b5.draw() 67 | 68 | 69 | b6 = new T.Button 70 | bounds: 71 | x: 43 72 | y: 5 73 | w: 20 74 | h: 3 75 | label: 'hello 6' 76 | labelAnchor: 6 77 | b6.draw() 78 | 79 | 80 | # Third Row 81 | b1 = new T.Button 82 | bounds: 83 | x: 1 84 | y: 9 85 | w: 20 86 | h: 3 87 | label: 'hello 1' 88 | labelAnchor: 1 89 | b1.draw() 90 | 91 | b2 = new T.Button 92 | bounds: 93 | x: 22 94 | y: 9 95 | w: 20 96 | h: 3 97 | label: 'hello 2' 98 | labelAnchor: 2 99 | b2.draw() 100 | 101 | b3 = new T.Button 102 | bounds: 103 | x: 43 104 | y: 9 105 | w: 20 106 | h: 3 107 | label: 'hello 3' 108 | labelAnchor: 3 109 | b3.draw() 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-term-ui", 3 | "version": "0.0.5", 4 | "main": "TermUI.js", 5 | "dependencies": { 6 | "coffee-script": "", 7 | "keypress": "", 8 | "underscore": "", 9 | "underscore.string": "" 10 | }, 11 | "devDependencies": {}, 12 | "engines": { 13 | "node": "*" 14 | }, 15 | "author": "Josh Faul (jocafa.com)", 16 | "description": "UI Toolkit for node.js console apps", 17 | "homepage": "https://github.com/jocafa/node-term-ui", 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/jocafa/node-term-ui.git" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | TermUI 2 | ====== 3 | 4 | TermUI is a library for Node.js that makes it easier to create rich console 5 | interfaces. 6 | 7 | ## General Usage 8 | 9 | ### Rendering 10 | 11 | - `out(text)` - prints text to the screen from the current cursor position 12 | - `clear()` - clears the screen 13 | - `pos(x,y)` - positions the cursor 14 | - `home()` - sends the cursor to the top left corner 15 | - `end()` - sends the cursor to the bottom right corner 16 | - `fg(color)` - sets the foreground color 17 | - `bg(color)` - sets the background color 18 | - `hifg(color)` - sets the foreground color for 256 color terminals 19 | - `hibg(color)` - sets the background color for 256 color terminals 20 | - `enableMouse()` - enables mouse event handling 21 | - `disableMouse()` - disables mouse event handling 22 | - `eraseLine()` - erases the entire line that the cursor is on 23 | - `hideCursor()` - hides the cursor 24 | - `showCursor()` - shows the cursor 25 | 26 | The following will print "Hello, world!" at 10, 20 in the terminal in white text 27 | on a red background: 28 | 29 | ```coffeescript 30 | TermUI.pos(10,20).fg(TermUI.C.w).bg(TermUI.C.w).out("Hello, world!") 31 | ``` 32 | 33 | As you can see, pretty much everything is chainable. 34 | 35 | ### Handy Rendering Shortcuts 36 | The `C` object contains definitions for common colors so that you don't have 37 | to remember the numeric values. 38 | 39 | - k: black 40 | - r: red 41 | - g: green 42 | - y: yellow 43 | - b: blue 44 | - m: magenta 45 | - c: cyan 46 | - w: white 47 | - x: the terminal's default color 48 | 49 | The `S` object is similar: it contains the text style definitions -- normal, 50 | bold, underline, blink, and inverse. 51 | 52 | The `SYM` object contains shortcuts for some handy UTF8 characters: star, check 53 | x, triUp, triDown, triLeft, triRight, fn, arrowUp, arrowDown, arrowLeft, and 54 | arrowRight. 55 | 56 | ### Events 57 | `resize` is fired when the user resizes their terminal. The listener receives 58 | an object with 'w' and 'h' properties set to the new width and height of the 59 | terminal. 60 | 61 | `keypress` is fired when a key is pressed. This works just like the `keypress` 62 | event on `process.stdin` 63 | 64 | `mousedown, mouseup, drag, wheel` are all the mouse events that are fired. The 65 | receiver is sent an object that contains which button was pressed, which direction 66 | the wheel scrolled, the x/y location, and whether or not shift was pressed. 67 | 68 | 69 | ## Widgets 70 | 71 | ### Button 72 | 73 | Buttons are simply clickable rectangular areas that can have a label on them. 74 | Here's how to use one... 75 | 76 | ```coffeescript 77 | TermUI = require 'TermUI' 78 | 79 | TermUI.enableMouse() 80 | 81 | button = new TermUI.Button 82 | bounds: 83 | x: 0 84 | y: 0 85 | w: 30 86 | h: 3 87 | label: 'I am a banana!' 88 | labelAnchor: 5 89 | 90 | button.on 'mousedown', -> 91 | button.bg = TermUI.C.y 92 | button.draw() 93 | 94 | button.on 'mouseup', -> 95 | button.bg = TermUI.C.b 96 | button.draw() 97 | 98 | button.draw() 99 | ``` 100 | --------------------------------------------------------------------------------