├── .eslintignore ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── help.js └── less.js ├── examples ├── hacker-news.js └── rock-paper-scissors.js ├── gulpfile.js ├── package.json ├── src ├── help.js └── less.js └── test └── less.js /.eslintignore: -------------------------------------------------------------------------------- 1 | ./dist 2 | ./test 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | 3 | # Runtime data 4 | pids 5 | *.pid 6 | *.seed 7 | 8 | *sublime* 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.11" 5 | - "0.10" 6 | - "iojs-v1" 7 | - "iojs-v2" 8 | sudo: false 9 | branches: 10 | only: 11 | - master -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 DC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vorpal - Less 2 | 3 | [![Build Status](https://travis-ci.org/vorpaljs/vorpal-less.svg)](https://travis-ci.org/vorpaljs/vorpal-less) 4 | [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 5 | 6 | A 100% Javascript (ES2015) implementation of the [less](https://en.wikipedia.org/wiki/Less_%28Unix%29) command. 7 | 8 | A [Vorpal.js](https://github.com/dthree/vorpal) extension, `vorpal-less` lets you pipe vorpal commands and content through less. 9 | 10 | ### Installation 11 | 12 | ```bash 13 | npm install vorpal-less 14 | npm install vorpal 15 | ``` 16 | 17 | ### Getting Started 18 | 19 | ```js 20 | const vorpal = require('vorpal')(); 21 | const hn = require('vorpal-hacker-news'); 22 | const less = require('vorpal-less'); 23 | 24 | vorpal 25 | .delimiter('node~$') 26 | .use(hn) 27 | .use(less) 28 | .show(); 29 | ``` 30 | 31 | ```bash 32 | $ node hacker-news.js 33 | node~$ hacker-news | less 34 | ... 35 | ... content 36 | ... 37 | : 38 | ``` 39 | 40 | ### Examples 41 | 42 | - [Hackers News](https://github.com/vorpaljs/vorpal-less/blob/master/examples/hacker-news.js) 43 | - [Rock Paper Scissors](https://github.com/vorpaljs/vorpal-less/blob/master/examples/rock-paper-scissors.js) 44 | 45 | ### Implementation 46 | 47 | `vorpal-less` aims to be a letter-perfect implementation of the `less` command you know (and love?). All features implemented so far will appear in its help menu: 48 | 49 | ```bash 50 | vorpal~$ less --help 51 | ``` 52 | ##### Implemented: 53 | 54 | - Primary functionality, prompt, screen writing, etc. 55 | - All navigation commands and shortcuts. 56 | - Less-style help menu. 57 | 58 | ### Contributing 59 | 60 | Feel free to contribute! Additional work is needed on: 61 | 62 | - Search options 63 | - File-reading options 64 | - Option flags 65 | 66 | ### License 67 | 68 | MIT © [David Caccavella](https://github.com/dthree) 69 | 70 | -------------------------------------------------------------------------------- /dist/help.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (vorpal) { 4 | var chalk = vorpal.chalk; 5 | var b = chalk.blue; 6 | var y = chalk.yellow; 7 | var r = chalk.red; 8 | var c = chalk.cyan; 9 | var g = chalk.grey; 10 | var bold = chalk.bold; 11 | var help = "\n " + bold("SUMMARY OF LESS COMMANDS") + "\n\n Commands marked with " + r("*") + " may be preceded by a number, N.\n Notes in parentheses indicate the behavior if N is given.\n A key preceded by a caret indicates the " + y("Ctrl") + " key; thus " + y("^") + b("K") + " is " + y("ctrl-") + b("K") + ".\n\n " + b("h H ") + " Display this help.\n " + b("q :q Q :Q ZZ") + " Exit.\n " + g("---------------------------------------------------------------------------") + "\n\n " + bold("MOVING") + "\n\n " + b("e " + y("^") + "E j ^N CR " + r("*")) + " Forward one line (or N lines).\n " + b("y " + y("^") + "Y k " + y("^") + "K " + y("^") + "P " + r("*")) + " Backward one line (or N lines).\n " + b("f " + y("^") + "F " + y("^") + "V " + c("SPACE") + " " + r("*")) + " Forward one window (or N lines).\n " + b("b " + y("^") + "B " + c("ESC-v") + " " + r("*")) + " Backward one window (or N lines).\n " + b("z " + r("*")) + " Forward one window (and set window to N).\n " + b("w " + r("*")) + " Backward one window (and set window to N).\n " + b(c("ESC-SPACE") + " " + r("*")) + " Forward one window, but don't stop at end-of-file.\n " + b("d " + y("^") + "D " + r("*")) + " Forward one half-window (and set half-window to N).\n " + b("u " + y("^") + "U " + r("*")) + " Backward one half-window (and set half-window to N).\n " + b(c("ESC-") + ") " + c("RightArrow") + " " + r("*")) + " Left one half screen width (or N positions).\n " + b(c("ESC-") + "( " + c("LeftArrow") + " " + r("*")) + " Right one half screen width (or N positions).\n ---------------------------------------------------\n Default \"window\" is the screen height.\n Default \"half-window\" is half of the screen height.\n " + g("---------------------------------------------------------------------------") + "\n\n " + bold("JUMPING") + "\n\n " + b("g < " + c("ESC-") + "<") + " " + r("*") + " Go to first line in file (or line N).\n " + b("G > " + c("ESC-") + ">") + " " + r("*") + " Go to last line in file (or line N).\n " + b("p % ") + " " + r("*") + " Go to beginning of file (or N percent into file).\n ---------------------------------------------------\n\n " + bold("MISCELLANEOUS COMMANDS") + "\n\n " + b("V") + " Print version number of \"less\".\n " + g("---------------------------------------------------------------------------") + "\n\n " + bold("OPTIONS") + "\n\n Most options may be changed either on the command line,\n or from within less by using the - or -- command.\n Options may be given in one of two forms: either a single\n character preceded by a -, or a name preceded by --.\n\n " + b("/? ........ --help") + "\n Display help (from command line).\n " + b("-F ........ --quit-if-one-screen") + "\n Quit if entire file fits on first screen.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; 12 | return help; 13 | }; -------------------------------------------------------------------------------- /dist/less.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var util = require('util'); 8 | var slice = require('slice-ansi'); 9 | 10 | var chalk = undefined; 11 | 12 | var utili = { 13 | 14 | padRows: function padRows(str, n) { 15 | for (var i = 0; i < n; ++i) { 16 | str = '\n' + str; 17 | } 18 | return str; 19 | }, 20 | 21 | parseKeypress: function parseKeypress(keypress) { 22 | keypress.e.key = keypress.e.key || {}; 23 | var keyValue = util.inspect(keypress.e.value).indexOf('\\u') > -1 ? undefined : keypress.e.value; 24 | keyValue = String(keyValue).trim() === '' ? undefined : keyValue; 25 | var ctrl = keypress.e.key.ctrl; 26 | var keyName = keypress.e.key.name; 27 | var key = keyValue || keyName; 28 | key = key === 'escape' ? 'ESC' : key; 29 | var mods = ctrl ? '^' + key : key; 30 | return { 31 | value: key, 32 | mods: mods 33 | }; 34 | } 35 | }; 36 | 37 | var less = { 38 | 39 | init: function init(instance, vorpal, args, callback) { 40 | callback = callback || function () {}; 41 | this.instance = instance; 42 | this.vorpal = vorpal; 43 | this.hasQuit = false; 44 | this.callback = callback; 45 | this.iteration = 0; 46 | this.cursorY = 0; 47 | this.cursorX = 0; 48 | this.helpCursorY = 0; 49 | this.helpCursorX = 0; 50 | this.stdin = ''; 51 | this.cache = ''; 52 | this.numbers = ''; 53 | this.prompted = false; 54 | this.help = require('./help')(vorpal); 55 | this.onlyHelp = args.options.help; 56 | this.helpMode = args.options.help; 57 | this.quitIfOneScreen = args.options.quitifonescreen; 58 | var self = this; 59 | this.keypressFn = function (e) { 60 | self.onKeypress(e); 61 | }; 62 | this.vorpal.on('keypress', this.keypressFn); 63 | return this; 64 | }, 65 | 66 | exec: function exec(args) { 67 | var _this = this; 68 | 69 | var self = this; 70 | var stdin = args.stdin || ''; 71 | this.iteration += 1; 72 | this.stdin += stdin + '\n'; 73 | function roll() { 74 | var content = self.prepare(); 75 | if (self.hasQuit) { 76 | return; 77 | } 78 | if (!self.prompted) { 79 | self.prompt(); 80 | } 81 | self.render(content); 82 | } 83 | // If we are getting tons of calls to 84 | // exec due to a continual feed of data, 85 | // only render if no change for 50 millis. 86 | // This is important as otherwise slow 87 | // consoles will take forever on redrawing 88 | // it a million times. 89 | if (this.iteration > 1) { 90 | (function () { 91 | var lastIter = _this.iteration; 92 | setTimeout(function () { 93 | if (self.iteration === lastIter) { 94 | roll(); 95 | } 96 | }, 50); 97 | })(); 98 | } else { 99 | roll(); 100 | } 101 | }, 102 | 103 | prepare: function prepare() { 104 | var self = this; 105 | var stdins = this.helpMode ? this.help : String(this.stdin); 106 | var cursorY = this.helpMode ? this.helpCursorY : this.cursorY; 107 | var cursorX = this.helpMode ? this.helpCursorX : this.cursorX; 108 | var lines = stdins.split('\n').length; 109 | var height = process.stdout.rows - 1; 110 | var diff = height - lines; 111 | if (diff > 0 && !this.quitIfOneScreen) { 112 | stdins = utili.padRows(stdins, diff); 113 | } 114 | stdins = stdins.split('\n').slice(cursorY, cursorY + height).map(function (str) { 115 | str = slice(str, cursorX, cursorX + process.stdout.columns - 1); 116 | return str; 117 | }).join('\n'); 118 | if (this.quitIfOneScreen && diff > 0) { 119 | // If we're logging straight, we want to remove the last \n, 120 | // as console.log takes care of that for us. 121 | stdins = stdins[stdins.length - 1] === '\n' ? stdins.slice(0, stdins.length - 1) : stdins; 122 | self.vorpal.log(stdins); 123 | this.quit({ 124 | redraw: false 125 | }); 126 | return undefined; 127 | } 128 | return stdins; 129 | }, 130 | 131 | render: function render(data) { 132 | this.vorpal.ui.redraw(data); 133 | }, 134 | 135 | onKeypress: function onKeypress(keypress) { 136 | var height = process.stdout.rows - 1; 137 | var width = process.stdout.columns - 1; 138 | var stdin = this.helpMode ? this.help : String(this.stdin); 139 | var lines = String(stdin).split('\n').length; 140 | var bottom = lines - height < 0 ? 0 : lines - height; 141 | var key = utili.parseKeypress(keypress); 142 | var keyCache = this.cache + key.value; 143 | var alphaCache = String(keyCache).replace(/^[0-9]+/g, ''); 144 | var numCache = String(keyCache).replace(/[^0-9]/g, ''); 145 | var factor = !isNaN(numCache) && numCache > 0 ? parseFloat(numCache) : 1; 146 | 147 | var cursorYName = this.helpMode ? 'helpCursorY' : 'cursorY'; 148 | var cursorXName = this.helpMode ? 'helpCursorX' : 'cursorX'; 149 | var cursorY = this[cursorYName]; 150 | var cursorX = this[cursorXName]; 151 | var startedBelowBottom = cursorY > bottom; 152 | var ignore = ['backspace', 'left', 'right', '`', 'tab']; 153 | var flags = { 154 | match: true, 155 | stop: true, 156 | version: false 157 | }; 158 | 159 | function has(arr) { 160 | arr = Array.isArray(arr) ? arr : [arr]; 161 | return arr.indexOf(key.value) > -1 || arr.indexOf(alphaCache) > -1 || arr.indexOf(key.mods) > -1; 162 | } 163 | 164 | if (has(['ESC ', 'ESCspace'])) { 165 | cursorY += height * factor; 166 | flags.stop = false; 167 | } else if (has(['up', 'y', '^Y', 'k', '^K', '^p'])) { 168 | cursorY -= factor; 169 | } else if (has(['down', 'e', '^e', '^n', 'j', 'enter'])) { 170 | cursorY += factor; 171 | } else if (has(['left'])) { 172 | cursorX -= Math.floor(width / 2) * factor; 173 | } else if (has(['right'])) { 174 | cursorX += Math.floor(width / 2) * factor; 175 | } else if (has(['pageup', 'b', '^B', 'ESCv', 'w'])) { 176 | cursorY -= height * factor; 177 | } else if (has(['pagedown', 'f', '^F', '^v', 'space', ' ', 'z'])) { 178 | cursorY += height * factor; 179 | } else if (has(['u', '^u'])) { 180 | cursorY -= Math.floor(height / 2) * factor; 181 | } else if (has(['d', '^d'])) { 182 | cursorY += Math.floor(height / 2) * factor; 183 | } else if (has(['g', 'home', '<', 'ESC<'])) { 184 | cursorY = 0; 185 | } else if (has(['p', '%'])) { 186 | var pct = factor > 100 ? 100 : factor; 187 | cursorY = pct === 1 ? 0 : Math.floor(lines * (pct / 100)); 188 | } else if (has(['G', 'end', '>', 'ESC>'])) { 189 | cursorY = bottom; 190 | } else if (has(['h', 'H'])) { 191 | this.helpMode = true; 192 | } else if (has('V')) { 193 | flags.version = true; 194 | } else if (has(['q', ':q', 'Q', ':Q', 'ZZ'])) { 195 | if (this.helpMode) { 196 | this.helpMode = false; 197 | if (this.onlyHelp) { 198 | this.quit(); 199 | return; 200 | } 201 | } else { 202 | this.quit(); 203 | return; 204 | } 205 | } else if (has(ignore)) { 206 | // Catch and do nothing... 207 | } else { 208 | flags.match = false; 209 | } 210 | 211 | this.cache = !flags.match ? keyCache : ''; 212 | cursorX = cursorX < 0 ? 0 : cursorX; 213 | cursorY = cursorY < 0 ? 0 : cursorY; 214 | cursorY = cursorY > bottom && flags.stop && !startedBelowBottom ? bottom : cursorY; 215 | 216 | var delimiter = undefined; 217 | if (flags.version) { 218 | delimiter = chalk.inverse('vorpal-less 0.0.1 (press RETURN) '); 219 | } else if (cursorY >= bottom && this.helpMode) { 220 | delimiter = chalk.inverse('HELP -- END -- Press g to see it again, or q when done '); 221 | } else if (cursorY >= bottom) { 222 | delimiter = chalk.inverse('END '); 223 | } else if (this.helpMode) { 224 | delimiter = chalk.inverse('HELP -- Press RETURN for more, or q when done '); 225 | } else if (String(this.cache).trim() !== '') { 226 | delimiter = ' '; 227 | } else { 228 | delimiter = ':'; 229 | } 230 | 231 | // Draw. 232 | this[cursorYName] = cursorY; 233 | this[cursorXName] = cursorX; 234 | 235 | var content = this.prepare(); 236 | if (!this.hasQuit) { 237 | this.vorpal.ui.delimiter(delimiter); 238 | this.render(content); 239 | if (cursorY < bottom && !this.helpMode) { 240 | this.vorpal.ui.input(this.cache); 241 | } else { 242 | this.vorpal.ui.input(''); 243 | } 244 | } 245 | }, 246 | 247 | quit: function quit(options) { 248 | var self = this; 249 | self.hasQuit = true; 250 | options = options || { 251 | redraw: true 252 | }; 253 | 254 | function end() { 255 | self.vorpal.removeListener('keypress', self.keypressFn); 256 | if (options.redraw) { 257 | self.vorpal.ui.submit(''); 258 | self.vorpal.ui.redraw.clear(); 259 | self.vorpal.ui.redraw.done(); 260 | } 261 | self.callback(); 262 | } 263 | 264 | // Wait for the prompt to render. 265 | function wait() { 266 | if (!self.vorpal.ui._activePrompt) { 267 | setTimeout(wait, 10); 268 | } else { 269 | end(); 270 | } 271 | } 272 | 273 | wait(); 274 | }, 275 | 276 | prompt: function prompt() { 277 | this.prompted = true; 278 | var self = this; 279 | 280 | // For now, ensure we aren't stuck 281 | // on Vorpal's last prompt. 282 | if (self.vorpal.ui._activePrompt) { 283 | delete self.vorpal.ui._activePrompt; 284 | } 285 | 286 | this.instance.prompt({ 287 | type: 'input', 288 | name: 'continue', 289 | message: ':', 290 | validate: function validate() { 291 | if (self.hasQuit === true) { 292 | return true; 293 | } 294 | // By validating false, and sending 295 | // a keypress event, we can bypass the 296 | // enter key's default inquirer actions 297 | // and treat it like it's just another key. 298 | self.onKeypress({ 299 | e: { key: { name: 'enter' } } 300 | }); 301 | return false; 302 | } 303 | }, function () {}); 304 | } 305 | }; 306 | 307 | /** 308 | * Expose a function that passes in a Vantage 309 | * object and options. 310 | */ 311 | 312 | module.exports = function (vorpal) { 313 | if (vorpal === undefined) { 314 | return less; 315 | } 316 | vorpal.api = vorpal.api || {}; 317 | vorpal.api.less = less; 318 | chalk = vorpal.chalk; 319 | function route(args, cb) { 320 | cb = cb || function () {}; 321 | if (this._less && this._less.hasQuit === true) { 322 | args.stdin = Object.prototype.toString.call(args.stdin) === '[object Array]' ? args.stdin[0] : args.stdin; 323 | if (this._less.quitIfOneScreen && args.stdin && args.stdin !== '') { 324 | vorpal.log(args.stdin); 325 | } 326 | cb(); 327 | return; 328 | } 329 | if (!this._less) { 330 | this._less = Object.create(less); 331 | this._less.init(this, vorpal, args, cb); 332 | this._less.exec(args); 333 | } else { 334 | this._less.exec(args); 335 | cb(); 336 | } 337 | } 338 | 339 | vorpal.command('less', 'Less implementation.').option('-F, --quit-if-one-screen').hidden().help(route).action(route); 340 | }; -------------------------------------------------------------------------------- /examples/hacker-news.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const vorpal = require('vorpal')(); 4 | const chalk = require('chalk'); 5 | const hn = require('vorpal-hacker-news'); 6 | const less = require('./../dist/less'); 7 | 8 | vorpal 9 | .delimiter(`${chalk.grey(`type "${chalk.blue(`hacker-news -l 50 | less`)}":`)}`) 10 | .use(less) 11 | .use(hn) 12 | .show(); 13 | 14 | -------------------------------------------------------------------------------- /examples/rock-paper-scissors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | const vorpal = require('vorpal')(); 5 | const less = require('./../dist/less'); 6 | 7 | function rps(lines, rows) { 8 | function gen() { 9 | const rand1 = ['rock', 'paper', 'scissors']['012'.charAt(Math.floor(Math.random() * 3))]; 10 | const rand2 = ['blue', 'white', 'yellow', 'red', 'magenta', 'green', 'cyan']['0123456'.charAt(Math.floor(Math.random() * 7))]; 11 | return `${chalk[rand2](rand1)} `; 12 | } 13 | let out = ''; 14 | for (let i = 0; i < lines; ++i) { 15 | rows = Math.floor((Math.random() * 20) * (Math.random() * 50)); 16 | out = ''; 17 | out += `${(i + 1)}: `; 18 | for (let j = 0; j < rows; ++j) { 19 | out += gen(j + 1); 20 | } 21 | out += (i === lines - 1) ? '' : '\n'; 22 | this.log(out); 23 | } 24 | return out; 25 | } 26 | 27 | vorpal 28 | .delimiter(`${chalk.grey(`type "${chalk.blue(`rock-paper-scissors | less`)}":`)}`) 29 | .use(less) 30 | .show(); 31 | 32 | vorpal.command('single', 'Spits an epic set of single-page data to less.') 33 | .alias('s') 34 | .parse(function (str) { 35 | return `${str} | less -F`; 36 | }) 37 | .action(function (args, cb) { 38 | const self = this; 39 | this.log(rps(20, 5)); 40 | setTimeout(function () { 41 | self.log(rps(10, 6)); 42 | cb(); 43 | }, 1000); 44 | }); 45 | 46 | vorpal.command('rock-paper-scissors', 'Spits an epic set of data to less.') 47 | .alias('b') 48 | .action(function (args, cb) { 49 | rps.call(this, 1000, null); 50 | // this.log(rps(500, Math.floor((Math.random() * 20) * (Math.random() * 50)))); 51 | setTimeout(function () { 52 | // self.log(rps(10, 6)); 53 | cb(); 54 | }, 1000); 55 | }); 56 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const babel = require('gulp-babel'); 5 | const changed = require('gulp-changed'); 6 | 7 | gulp.task('watch', function () { 8 | gulp.watch('src/**/*.js', ['babel']); 9 | }); 10 | 11 | gulp.task('babel', function () { 12 | const bab = babel(); 13 | bab.on('error', function (e) { 14 | console.log(e.stack); 15 | }); 16 | gulp.src('src/**/*.js') 17 | .pipe(changed('dist')) 18 | .pipe(bab) 19 | .pipe(gulp.dest('dist')); 20 | 21 | return; 22 | }); 23 | 24 | gulp.task('default', ['babel', 'watch']); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vorpal-less", 3 | "version": "0.0.13", 4 | "description": "Less implementation for Vorpal.js", 5 | "main": "./dist/less.js", 6 | "scripts": { 7 | "test": "mocha && xo" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/vorpaljs/vorpal-less.git" 12 | }, 13 | "keywords": [ 14 | "less", 15 | "api", 16 | "cli", 17 | "immersive", 18 | "repl", 19 | "vorpal", 20 | "vorpaljs", 21 | "posix" 22 | ], 23 | "author": "dthree", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/vorpaljs/vorpal-less/issues" 27 | }, 28 | "homepage": "https://github.com/vorpaljs/vorpal-less#readme", 29 | "devDependencies": { 30 | "gulp": "^3.9.0", 31 | "gulp-babel": "^5.2.1", 32 | "gulp-changed": "^1.3.0", 33 | "mocha": "^2.3.3", 34 | "should": "^7.1.0", 35 | "vorpal": "*", 36 | "vorpal-hacker-news": "^1.0.6", 37 | "xo": "^0.9.0" 38 | }, 39 | "dependencies": { 40 | "slice-ansi": "0.0.3" 41 | }, 42 | "engines": { 43 | "node": ">= 0.11.16", 44 | "iojs": ">= 1.0.0" 45 | }, 46 | "files": [ 47 | "dist" 48 | ], 49 | "xo": { 50 | "envs": [ 51 | "node", 52 | "mocha" 53 | ], 54 | "esnext": true, 55 | "space": true, 56 | "rules": { 57 | "no-unused-expressions": 0, 58 | "prefer-arrow-callback": 0, 59 | "prefer-reflect": 0 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/help.js: -------------------------------------------------------------------------------- 1 | module.exports = function (vorpal) { 2 | const chalk = vorpal.chalk; 3 | const b = chalk.blue; 4 | const y = chalk.yellow; 5 | const r = chalk.red; 6 | const c = chalk.cyan; 7 | const g = chalk.grey; 8 | const bold = chalk.bold; 9 | const help = 10 | ` 11 | ${bold(`SUMMARY OF LESS COMMANDS`)} 12 | 13 | Commands marked with ${r(`*`)} may be preceded by a number, N. 14 | Notes in parentheses indicate the behavior if N is given. 15 | A key preceded by a caret indicates the ${y(`Ctrl`)} key; thus ${y(`^`)}${b(`K`)} is ${y(`ctrl-`)}${b(`K`)}. 16 | 17 | ${b(`h H `)} Display this help. 18 | ${b(`q :q Q :Q ZZ`)} Exit. 19 | ${g(`---------------------------------------------------------------------------`)} 20 | 21 | ${bold(`MOVING`)} 22 | 23 | ${b(`e ${y(`^`)}E j ^N CR ${r(`*`)}`)} Forward one line (or N lines). 24 | ${b(`y ${y(`^`)}Y k ${y(`^`)}K ${y(`^`)}P ${r(`*`)}`)} Backward one line (or N lines). 25 | ${b(`f ${y(`^`)}F ${y(`^`)}V ${c(`SPACE`)} ${r(`*`)}`)} Forward one window (or N lines). 26 | ${b(`b ${y(`^`)}B ${c(`ESC-v`)} ${r(`*`)}`)} Backward one window (or N lines). 27 | ${b(`z ${r(`*`)}`)} Forward one window (and set window to N). 28 | ${b(`w ${r(`*`)}`)} Backward one window (and set window to N). 29 | ${b(`${c(`ESC-SPACE`)} ${r(`*`)}`)} Forward one window, but don't stop at end-of-file. 30 | ${b(`d ${y(`^`)}D ${r(`*`)}`)} Forward one half-window (and set half-window to N). 31 | ${b(`u ${y(`^`)}U ${r(`*`)}`)} Backward one half-window (and set half-window to N). 32 | ${b(`${c(`ESC-`)}) ${c(`RightArrow`)} ${r(`*`)}`)} Left one half screen width (or N positions). 33 | ${b(`${c(`ESC-`)}( ${c(`LeftArrow`)} ${r(`*`)}`)} Right one half screen width (or N positions). 34 | --------------------------------------------------- 35 | Default "window" is the screen height. 36 | Default "half-window" is half of the screen height. 37 | ${g(`---------------------------------------------------------------------------`)} 38 | 39 | ${bold(`JUMPING`)} 40 | 41 | ${b(`g < ${c(`ESC-`)}<`)} ${r(`*`)} Go to first line in file (or line N). 42 | ${b(`G > ${c(`ESC-`)}>`)} ${r(`*`)} Go to last line in file (or line N). 43 | ${b(`p % `)} ${r(`*`)} Go to beginning of file (or N percent into file). 44 | --------------------------------------------------- 45 | 46 | ${bold(`MISCELLANEOUS COMMANDS`)} 47 | 48 | ${b(`V`)} Print version number of "less". 49 | ${g(`---------------------------------------------------------------------------`)} 50 | 51 | ${bold(`OPTIONS`)} 52 | 53 | Most options may be changed either on the command line, 54 | or from within less by using the - or -- command. 55 | Options may be given in one of two forms: either a single 56 | character preceded by a -, or a name preceded by --. 57 | 58 | ${b(`/? ........ --help`)} 59 | Display help (from command line). 60 | ${b(`-F ........ --quit-if-one-screen`)} 61 | Quit if entire file fits on first screen. 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | `; 113 | return help; 114 | }; 115 | -------------------------------------------------------------------------------- /src/less.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const util = require('util'); 8 | const slice = require('slice-ansi'); 9 | 10 | let chalk; 11 | 12 | const utili = { 13 | 14 | padRows(str, n) { 15 | for (let i = 0; i < n; ++i) { 16 | str = `\n${str}`; 17 | } 18 | return str; 19 | }, 20 | 21 | parseKeypress(keypress) { 22 | keypress.e.key = keypress.e.key || {}; 23 | let keyValue = (util.inspect(keypress.e.value).indexOf('\\u') > -1) ? undefined : keypress.e.value; 24 | keyValue = (String(keyValue).trim() === '') ? undefined : keyValue; 25 | const ctrl = keypress.e.key.ctrl; 26 | const keyName = keypress.e.key.name; 27 | let key = keyValue || keyName; 28 | key = (key === 'escape') ? 'ESC' : key; 29 | const mods = (ctrl) ? `^${key}` : key; 30 | return ({ 31 | value: key, 32 | mods 33 | }); 34 | } 35 | }; 36 | 37 | const less = { 38 | 39 | init(instance, vorpal, args, callback) { 40 | callback = callback || function () {}; 41 | this.instance = instance; 42 | this.vorpal = vorpal; 43 | this.hasQuit = false; 44 | this.callback = callback; 45 | this.iteration = 0; 46 | this.cursorY = 0; 47 | this.cursorX = 0; 48 | this.helpCursorY = 0; 49 | this.helpCursorX = 0; 50 | this.stdin = ''; 51 | this.cache = ''; 52 | this.numbers = ''; 53 | this.prompted = false; 54 | this.help = require('./help')(vorpal); 55 | this.onlyHelp = (args.options.help); 56 | this.helpMode = (args.options.help); 57 | this.quitIfOneScreen = (args.options.quitifonescreen); 58 | const self = this; 59 | this.keypressFn = function (e) { 60 | self.onKeypress(e); 61 | }; 62 | this.vorpal.on('keypress', this.keypressFn); 63 | return this; 64 | }, 65 | 66 | exec(args) { 67 | const self = this; 68 | const stdin = args.stdin || ''; 69 | this.iteration += 1; 70 | this.stdin += `${stdin}\n`; 71 | function roll() { 72 | const content = self.prepare(); 73 | if (self.hasQuit) { 74 | return; 75 | } 76 | if (!self.prompted) { 77 | self.prompt(); 78 | } 79 | self.render(content); 80 | } 81 | // If we are getting tons of calls to 82 | // exec due to a continual feed of data, 83 | // only render if no change for 50 millis. 84 | // This is important as otherwise slow 85 | // consoles will take forever on redrawing 86 | // it a million times. 87 | if (this.iteration > 1) { 88 | const lastIter = this.iteration; 89 | setTimeout(function () { 90 | if (self.iteration === lastIter) { 91 | roll(); 92 | } 93 | }, 50); 94 | } else { 95 | roll(); 96 | } 97 | }, 98 | 99 | prepare() { 100 | const self = this; 101 | let stdins = (this.helpMode) ? this.help : String(this.stdin); 102 | const cursorY = (this.helpMode) ? this.helpCursorY : this.cursorY; 103 | const cursorX = (this.helpMode) ? this.helpCursorX : this.cursorX; 104 | const lines = stdins.split('\n').length; 105 | const height = process.stdout.rows - 1; 106 | const diff = height - lines; 107 | if (diff > 0 && !this.quitIfOneScreen) { 108 | stdins = utili.padRows(stdins, diff); 109 | } 110 | stdins = stdins.split('\n').slice(cursorY, cursorY + height).map(function (str) { 111 | str = slice(str, cursorX, cursorX + process.stdout.columns - 1); 112 | return str; 113 | }).join('\n'); 114 | if (this.quitIfOneScreen && diff > 0) { 115 | // If we're logging straight, we want to remove the last \n, 116 | // as console.log takes care of that for us. 117 | stdins = (stdins[stdins.length - 1] === '\n') ? stdins.slice(0, stdins.length - 1) : stdins; 118 | self.vorpal.log(stdins); 119 | this.quit({ 120 | redraw: false 121 | }); 122 | return undefined; 123 | } 124 | return stdins; 125 | }, 126 | 127 | render(data) { 128 | this.vorpal.ui.redraw(data); 129 | }, 130 | 131 | onKeypress(keypress) { 132 | const height = process.stdout.rows - 1; 133 | const width = process.stdout.columns - 1; 134 | const stdin = (this.helpMode) ? this.help : String(this.stdin); 135 | const lines = String(stdin).split('\n').length; 136 | const bottom = ((lines - height) < 0) ? 0 : (lines - height); 137 | const key = utili.parseKeypress(keypress); 138 | const keyCache = this.cache + key.value; 139 | const alphaCache = String(keyCache).replace(/^[0-9]+/g, ''); 140 | const numCache = String(keyCache).replace(/[^0-9]/g, ''); 141 | const factor = (!isNaN(numCache) && numCache > 0) ? parseFloat(numCache) : 1; 142 | 143 | const cursorYName = (this.helpMode) ? 'helpCursorY' : 'cursorY'; 144 | const cursorXName = (this.helpMode) ? 'helpCursorX' : 'cursorX'; 145 | let cursorY = this[cursorYName]; 146 | let cursorX = this[cursorXName]; 147 | const startedBelowBottom = (cursorY > bottom); 148 | const ignore = ['backspace', 'left', 'right', '`', 'tab']; 149 | const flags = { 150 | match: true, 151 | stop: true, 152 | version: false 153 | }; 154 | 155 | function has(arr) { 156 | arr = (Array.isArray(arr)) ? arr : [arr]; 157 | return (arr.indexOf(key.value) > -1 || arr.indexOf(alphaCache) > -1 || arr.indexOf(key.mods) > -1); 158 | } 159 | 160 | if (has(['ESC ', 'ESCspace'])) { 161 | cursorY += (height * factor); 162 | flags.stop = false; 163 | } else if (has(['up', 'y', '^Y', 'k', '^K', '^p'])) { 164 | cursorY -= factor; 165 | } else if (has(['down', 'e', '^e', '^n', 'j', 'enter'])) { 166 | cursorY += factor; 167 | } else if (has(['left'])) { 168 | cursorX -= (Math.floor(width / 2) * factor); 169 | } else if (has(['right'])) { 170 | cursorX += (Math.floor(width / 2) * factor); 171 | } else if (has(['pageup', 'b', '^B', 'ESCv', 'w'])) { 172 | cursorY -= (height * factor); 173 | } else if (has(['pagedown', 'f', '^F', '^v', 'space', ' ', 'z'])) { 174 | cursorY += (height * factor); 175 | } else if (has(['u', '^u'])) { 176 | cursorY -= (Math.floor(height / 2) * factor); 177 | } else if (has(['d', '^d'])) { 178 | cursorY += (Math.floor(height / 2) * factor); 179 | } else if (has(['g', 'home', '<', 'ESC<'])) { 180 | cursorY = 0; 181 | } else if (has(['p', '%'])) { 182 | const pct = (factor > 100) ? 100 : factor; 183 | cursorY = (pct === 1) ? 0 : Math.floor(lines * (pct / 100)); 184 | } else if (has(['G', 'end', '>', 'ESC>'])) { 185 | cursorY = bottom; 186 | } else if (has(['h', 'H'])) { 187 | this.helpMode = true; 188 | } else if (has('V')) { 189 | flags.version = true; 190 | } else if (has(['q', ':q', 'Q', ':Q', 'ZZ'])) { 191 | if (this.helpMode) { 192 | this.helpMode = false; 193 | if (this.onlyHelp) { 194 | this.quit(); 195 | return; 196 | } 197 | } else { 198 | this.quit(); 199 | return; 200 | } 201 | } else if (has(ignore)) { 202 | // Catch and do nothing... 203 | } else { 204 | flags.match = false; 205 | } 206 | 207 | this.cache = (!flags.match) ? keyCache : ''; 208 | cursorX = (cursorX < 0) ? 0 : cursorX; 209 | cursorY = (cursorY < 0) ? 0 : cursorY; 210 | cursorY = (cursorY > bottom && flags.stop && !startedBelowBottom) ? bottom : cursorY; 211 | 212 | let delimiter; 213 | if (flags.version) { 214 | delimiter = chalk.inverse('vorpal-less 0.0.1 (press RETURN) '); 215 | } else if (cursorY >= bottom && this.helpMode) { 216 | delimiter = chalk.inverse('HELP -- END -- Press g to see it again, or q when done '); 217 | } else if (cursorY >= bottom) { 218 | delimiter = chalk.inverse('END '); 219 | } else if (this.helpMode) { 220 | delimiter = chalk.inverse('HELP -- Press RETURN for more, or q when done '); 221 | } else if (String(this.cache).trim() !== '') { 222 | delimiter = ' '; 223 | } else { 224 | delimiter = ':'; 225 | } 226 | 227 | // Draw. 228 | this[cursorYName] = cursorY; 229 | this[cursorXName] = cursorX; 230 | 231 | const content = this.prepare(); 232 | if (!this.hasQuit) { 233 | this.vorpal.ui.delimiter(delimiter); 234 | this.render(content); 235 | if (cursorY < bottom && !this.helpMode) { 236 | this.vorpal.ui.input(this.cache); 237 | } else { 238 | this.vorpal.ui.input(''); 239 | } 240 | } 241 | }, 242 | 243 | quit(options) { 244 | const self = this; 245 | self.hasQuit = true; 246 | options = options || { 247 | redraw: true 248 | }; 249 | 250 | function end() { 251 | self.vorpal.removeListener('keypress', self.keypressFn); 252 | if (options.redraw) { 253 | self.vorpal.ui.submit(''); 254 | self.vorpal.ui.redraw.clear(); 255 | self.vorpal.ui.redraw.done(); 256 | } 257 | self.callback(); 258 | } 259 | 260 | // Wait for the prompt to render. 261 | function wait() { 262 | if (!self.vorpal.ui._activePrompt) { 263 | setTimeout(wait, 10); 264 | } else { 265 | end(); 266 | } 267 | } 268 | 269 | wait(); 270 | }, 271 | 272 | prompt() { 273 | this.prompted = true; 274 | const self = this; 275 | 276 | // For now, ensure we aren't stuck 277 | // on Vorpal's last prompt. 278 | if (self.vorpal.ui._activePrompt) { 279 | delete self.vorpal.ui._activePrompt; 280 | } 281 | 282 | this.instance.prompt({ 283 | type: 'input', 284 | name: 'continue', 285 | message: ':', 286 | validate() { 287 | if (self.hasQuit === true) { 288 | return true; 289 | } 290 | // By validating false, and sending 291 | // a keypress event, we can bypass the 292 | // enter key's default inquirer actions 293 | // and treat it like it's just another key. 294 | self.onKeypress({ 295 | e: {key: {name: 'enter'}} 296 | }); 297 | return false; 298 | } 299 | }, function () { 300 | }); 301 | } 302 | }; 303 | 304 | /** 305 | * Expose a function that passes in a Vantage 306 | * object and options. 307 | */ 308 | 309 | module.exports = function (vorpal) { 310 | if (vorpal === undefined) { 311 | return less; 312 | } 313 | vorpal.api = vorpal.api || {}; 314 | vorpal.api.less = less; 315 | chalk = vorpal.chalk; 316 | function route(args, cb) { 317 | cb = cb || function () {}; 318 | if (this._less && this._less.hasQuit === true) { 319 | args.stdin = (Object.prototype.toString.call(args.stdin) === '[object Array]') ? args.stdin[0] : args.stdin; 320 | if (this._less.quitIfOneScreen && args.stdin && args.stdin !== '') { 321 | vorpal.log(args.stdin); 322 | } 323 | cb(); 324 | return; 325 | } 326 | if (!this._less) { 327 | this._less = Object.create(less); 328 | this._less.init(this, vorpal, args, cb); 329 | this._less.exec(args); 330 | } else { 331 | this._less.exec(args); 332 | cb(); 333 | } 334 | } 335 | 336 | vorpal 337 | .command('less', 'Less implementation.') 338 | .option('-F, --quit-if-one-screen') 339 | .hidden() 340 | .help(route) 341 | .action(route); 342 | }; 343 | -------------------------------------------------------------------------------- /test/less.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('assert'); 4 | 5 | var should = require('should'); 6 | var less = require('./../dist/less'); 7 | var vorpal = require('vorpal')(); 8 | 9 | vorpal.command('blabber').action(function (args, cb) { 10 | var blabber = ''; 11 | for (var i = 0; i < 300; ++i) { 12 | for (var j = 0; j < 80; ++j) { 13 | blabber += Math.floor(Math.random() * 100); 14 | } 15 | blabber += '\n'; 16 | } 17 | this.log(blabber); 18 | cb(); 19 | }); 20 | 21 | describe('vorpal-less', function () { 22 | it('should exist and be a function', function () { 23 | should.exist(less); 24 | less.should.be.type('function'); 25 | }); 26 | 27 | it('should import into Vorpal', function () { 28 | (function () { 29 | vorpal.use(less); 30 | }).should.not.throw(); 31 | }); 32 | 33 | it('should exist as a command in Vorpal', function () { 34 | var exists = false; 35 | for (var i = 0; i < vorpal.commands.length; ++i) { 36 | if (vorpal.commands[i]._name === 'less') { 37 | exists = true; 38 | } 39 | } 40 | exists.should.be.true; 41 | }); 42 | }); 43 | --------------------------------------------------------------------------------