├── .gitignore ├── History.md ├── Readme.md ├── example.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.2.1 / 2013-11-21 3 | ================== 4 | 5 | * add j k shortcuts 6 | * fix: ignore keypress if key is null or undefined 7 | 8 | 0.2.0 / 2013-07-19 9 | ================== 10 | 11 | * bind / unbind in .start() / .stop() 12 | 13 | 0.1.0 / 2013-07-18 14 | ================== 15 | 16 | * add .get(id) 17 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # term-list 2 | 3 | Renders an interactive list to the terminal that users 4 | can navigate using the arrow keys. Developers can bind 5 | to "keypress" events to support removal or opening of items etc. 6 | 7 | ![interactive terminal list](http://dsz91cxz97a03.cloudfront.net/YNqOchbrMD-150x150.png) 8 | 9 | ## Installation 10 | 11 | ``` 12 | $ npm install term-list 13 | ``` 14 | 15 | ## Example 16 | 17 | A fully interactive list demonstrating removal via backspace, 18 | and opening of the websites via the return key. 19 | 20 | ```js 21 | var List = require('term-list'); 22 | var exec = require('child_process').exec; 23 | 24 | var list = new List({ marker: '\033[36m› \033[0m', markerLength: 2 }); 25 | list.add('http://google.com', 'Google'); 26 | list.add('http://yahoo.com', 'Yahoo'); 27 | list.add('http://cloudup.com', 'Cloudup'); 28 | list.add('http://github.com', 'Github'); 29 | list.start(); 30 | 31 | list.on('keypress', function(key, item){ 32 | switch (key.name) { 33 | case 'return': 34 | exec('open ' + item); 35 | list.stop(); 36 | console.log('opening %s', item); 37 | break; 38 | case 'backspace': 39 | list.remove(list.selected); 40 | break; 41 | } 42 | }); 43 | 44 | list.on('empty', function(){ 45 | list.stop(); 46 | }); 47 | ``` 48 | 49 | ### API 50 | 51 | - [List()](#list) 52 | - [List.add()](#listaddidstringlabelstring) 53 | - [List.remove()](#listremoveidstring) 54 | - [List.at()](#listatinumber) 55 | - [List.select()](#listselectidstring) 56 | - [List.draw()](#listdraw) 57 | - [List.up()](#listup) 58 | - [List.down()](#listdown) 59 | - [List.stop()](#liststop) 60 | - [List.start()](#liststart) 61 | 62 | ### List() 63 | 64 | Initialize a new `List` with `opts`: 65 | 66 | - `marker` optional marker string defaulting to '› ' 67 | - `markerLength` optional marker length, otherwise marker.length is used 68 | 69 | ### List.add(id:String, label:String) 70 | 71 | Add item `id` with `label`. 72 | 73 | ### List.remove(id:String) 74 | 75 | Remove item `id`. 76 | 77 | ### List.at(i:Number) 78 | 79 | Return item at `i`. 80 | 81 | ### List.select(id:String) 82 | 83 | Select item `id`. 84 | 85 | ### List.draw() 86 | 87 | Re-draw the list. 88 | 89 | ### List.up() 90 | 91 | Select the previous item if any. 92 | 93 | ### List.down() 94 | 95 | Select the next item if any. 96 | 97 | ### List.stop() 98 | 99 | Reset state and stop the list. 100 | 101 | ### List.start() 102 | 103 | Start the list. 104 | 105 | ## License 106 | 107 | MIT 108 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 2 | var List = require('./'); 3 | var exec = require('child_process').exec; 4 | 5 | var list = new List({ marker: '\033[36m› \033[0m', markerLength: 2 }); 6 | list.add('http://google.com', 'Google'); 7 | list.add('http://yahoo.com', 'Yahoo'); 8 | list.add('http://cloudup.com', 'Cloudup'); 9 | list.add('http://github.com', 'Github'); 10 | list.start(); 11 | 12 | setTimeout(function(){ 13 | list.add('http://cuteoverload.com', 'Cute Overload'); 14 | list.draw(); 15 | }, 2000); 16 | 17 | setTimeout(function(){ 18 | list.add('http://uglyoverload.com', 'Ugly Overload'); 19 | list.draw(); 20 | }, 4000); 21 | 22 | list.on('keypress', function(key, item){ 23 | switch (key.name) { 24 | case 'return': 25 | exec('open ' + item); 26 | list.stop(); 27 | console.log('opening %s', item); 28 | break; 29 | case 'backspace': 30 | list.remove(list.selected); 31 | break; 32 | case 'c': 33 | if (key.ctrl) { 34 | list.stop(); 35 | process.exit(); 36 | } 37 | break; 38 | } 39 | }); 40 | 41 | list.on('empty', function(){ 42 | list.stop(); 43 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Emitter = require('events').EventEmitter; 7 | var Canvas = require('term-canvas'); 8 | var canvas = new Canvas(100, 200); 9 | var ctx = canvas.getContext('2d'); 10 | 11 | /** 12 | * Stdin. 13 | */ 14 | 15 | var stdin = process.stdin; 16 | require('keypress')(stdin); 17 | 18 | /** 19 | * Expose `List`. 20 | */ 21 | 22 | module.exports = List; 23 | 24 | /** 25 | * Initialize a new `List` with `opts`: 26 | * 27 | * - `marker` optional marker string defaulting to '› ' 28 | * - `markerLength` optional marker length, otherwise marker.length is used 29 | * 30 | * @param {Object} opts 31 | * @api public 32 | */ 33 | 34 | function List(opts) { 35 | opts = opts || {}; 36 | this.items = []; 37 | this.map = {}; 38 | this.marker = opts.marker || '› '; 39 | this.markerLength = opts.markerLength || this.marker.length; 40 | this.onkeypress = this.onkeypress.bind(this); 41 | } 42 | 43 | /** 44 | * Inherit from `Emitter.prototype`. 45 | */ 46 | 47 | List.prototype.__proto__ = Emitter.prototype; 48 | 49 | /** 50 | * Handle keypress. 51 | */ 52 | 53 | List.prototype.onkeypress = function(ch, key){ 54 | if (!key) return; 55 | 56 | this.emit('keypress', key, this.selected); 57 | switch (key.name) { 58 | case 'k': 59 | case 'up': 60 | this.up(); 61 | break; 62 | case 'j': 63 | case 'down': 64 | this.down(); 65 | break; 66 | case 'c': 67 | key.ctrl && this.stop(); 68 | break; 69 | } 70 | }; 71 | 72 | /** 73 | * Add item `id` with `label`. 74 | * 75 | * @param {String} id 76 | * @param {String} label 77 | * @api public 78 | */ 79 | 80 | List.prototype.add = function(id, label){ 81 | if (!this.selected) this.select(id); 82 | this.items.push({ id: id, label: label }); 83 | }; 84 | 85 | /** 86 | * Remove item `id`. 87 | * 88 | * @param {String} id 89 | * @api public 90 | */ 91 | 92 | List.prototype.remove = function(id){ 93 | this.emit('remove', id); 94 | var i = this.items.map(prop('id')).indexOf(id); 95 | this.items.splice(i, 1); 96 | if (!this.items.length) this.emit('empty'); 97 | var item = this.at(i) || this.at(i - 1); 98 | if (item) this.select(item.id); 99 | else this.draw(); 100 | }; 101 | 102 | /** 103 | * Return item at `i`. 104 | * 105 | * @param {Number} i 106 | * @return {Object} 107 | * @api public 108 | */ 109 | 110 | List.prototype.at = function(i){ 111 | return this.items[i]; 112 | }; 113 | 114 | /** 115 | * Get item by `id`. 116 | * 117 | * @param {String} id 118 | * @return {Object} 119 | * @api public 120 | */ 121 | 122 | List.prototype.get = function(id){ 123 | var i = this.items.map(prop('id')).indexOf(id); 124 | return this.at(i); 125 | }; 126 | 127 | /** 128 | * Select item `id`. 129 | * 130 | * @param {String} id 131 | * @api public 132 | */ 133 | 134 | List.prototype.select = function(id){ 135 | this.emit('select', id); 136 | this.selected = id; 137 | this.draw(); 138 | }; 139 | 140 | /** 141 | * Re-draw the list. 142 | * 143 | * @api public 144 | */ 145 | 146 | List.prototype.draw = function(){ 147 | var self = this; 148 | var y = 0; 149 | ctx.clear(); 150 | ctx.save(); 151 | ctx.translate(3, 3); 152 | this.items.forEach(function(item){ 153 | if (self.selected == item.id) { 154 | ctx.fillText(self.marker + item.label, 0, y++); 155 | } else { 156 | var pad = Array(self.markerLength + 1).join(' '); 157 | ctx.fillText(pad + item.label, 0, y++); 158 | } 159 | }); 160 | ctx.write('\n\n'); 161 | ctx.restore(); 162 | }; 163 | 164 | /** 165 | * Select the previous item if any. 166 | * 167 | * @api public 168 | */ 169 | 170 | List.prototype.up = function(){ 171 | var ids = this.items.map(prop('id')); 172 | var i = ids.indexOf(this.selected) - 1; 173 | var item = this.items[i]; 174 | if (!item) return; 175 | this.select(item.id); 176 | }; 177 | 178 | /** 179 | * Select the next item if any. 180 | * 181 | * @api public 182 | */ 183 | 184 | List.prototype.down = function(){ 185 | var ids = this.items.map(prop('id')); 186 | var i = ids.indexOf(this.selected) + 1; 187 | var item = this.items[i]; 188 | if (!item) return; 189 | this.select(item.id); 190 | }; 191 | 192 | /** 193 | * Reset state and stop the list. 194 | * 195 | * @api public 196 | */ 197 | 198 | List.prototype.stop = function(){ 199 | ctx.reset(); 200 | process.stdin.pause(); 201 | stdin.removeListener('keypress', this.onkeypress); 202 | }; 203 | 204 | /** 205 | * Start the list. 206 | * 207 | * @api public 208 | */ 209 | 210 | List.prototype.start = function(){ 211 | stdin.on('keypress', this.onkeypress); 212 | this.draw(); 213 | ctx.hideCursor(); 214 | stdin.setRawMode(true); 215 | stdin.resume(); 216 | }; 217 | 218 | /** 219 | * Prop helper. 220 | */ 221 | 222 | function prop(name) { 223 | return function(obj){ 224 | return obj[name]; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "term-list", 3 | "version": "0.2.1", 4 | "description": "interactive terminal lists", 5 | "keywords": [ 6 | "ansi", 7 | "term", 8 | "terminal", 9 | "list", 10 | "ui", 11 | "interactive" 12 | ], 13 | "license": "MIT", 14 | "dependencies": { 15 | "term-canvas": "0.0.5", 16 | "keypress": "~0.2.1" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/visionmedia/node-term-list.git" 21 | } 22 | } 23 | --------------------------------------------------------------------------------