Skip to content

Commit

Permalink
Main API and plumbing system done
Browse files Browse the repository at this point in the history
  • Loading branch information
mklabs committed Apr 21, 2016
1 parent 0361905 commit c3cba1d
Show file tree
Hide file tree
Showing 10 changed files with 540 additions and 37 deletions.
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
test:
mocha test/
test: babel
mocha test/ -R min

babel:
babel lib/ -d src/

lint:
eslint .
eslint . --env es6

env:
@echo $(PATH)

build: babel test lint
build: test lint

tt:
COMP_LINE="list --foo" COMP_CWORD=2 COMP_POINT=4 tabtab completion

watch:
watchd lib/**/* test/**/* bin/* -c 'bake build'
Expand Down
6 changes: 2 additions & 4 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,15 @@ let opts = minimist(process.argv.slice(2), {
});

const cmd = opts._[0];


debug('Init tabtab with %s cmd', cmd);
const allowed = commands.allowed;

if (opts.help) {
console.log(commands.help());
process.exit(0);
} else if (opts.version) {
console.log(commands.help());
process.exit(0);
} else if (commands[cmd]) {
} else if (allowed.indexOf(cmd) !== -1) {
debug('Run command %s with options', cmd, opts);
commands[cmd](opts);
} else {
Expand Down
35 changes: 27 additions & 8 deletions lib/commands/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
const debug = require('debug')('tabtab:commands');
const { join } = require('path');
// const debug = require('debug')('tabtab:commands');
const fs = require('fs');
const path = require('path');
const join = path.join;
const debug = require('../debug');

const Complete = require('../complete');

const {
readFileSync: read,
Expand All @@ -8,14 +13,29 @@ const {

class Commands {

get completion() {
return read(join(__dirname, '../../scripts/completion.sh'), 'utf8')

get allowed() {
return ['install', 'uninstall', 'list', 'search', 'add', 'rm', 'completion'];
}

// Constructor
constructor(options) {
this.options = options || {};
this.complete = new Complete(this.options);
}

// Commands

// Fow now, just output to the console
install() {
var script = this.completion;
console.log(this.completion);
install(options) {
options = options || {};
var script = this.complete.script(this.name, this.name || 'tabtab');
console.log(script);
}

completion(options) {
options = options || this.options;
return this.complete.handle(options);
}

uninstall() {}
Expand Down Expand Up @@ -46,5 +66,4 @@ class Commands {
}
}


module.exports = new Commands();
183 changes: 183 additions & 0 deletions lib/complete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// const debug = require('debug')('tabtab:complete');
const fs = require('fs');
const path = require('path');
const read = fs.readFileSync;
const exists = fs.existsSync;
const join = path.join;
const debug = require('./debug');

const { EventEmitter } = require('events');

class Complete extends EventEmitter {

// Defaults
get defaults() {
return {
name: process.title !== 'node' ? process.title : ''
};
}

// Constructor: Extends EventEmitter, accepts options hash with:
//
// - name - the package beeing completed name, defaults to process.title (if
// not node default) or will attempt to determine parent's package.json
// location and extract the name from it
constructor(options) {
super();
this.options = options || this.defaults;

if (!this.options.name) {
this.resolveName();
}

this.handle();
}


// Main handler
//
// Responsible for the `completion` command, both in normal and "plumbing"
// mode.
handle(options) {
options = options || this.options;
var env = this.parseEnv(this.options);

var name = options.name;
if (!name) throw new Error('Cannot determine package name');

if (env.args[0] !== 'completion') return;

debug('Completion plumbing mode detected');
if (!env.complete) {
debug('Completion command but without COMP args. Will output script:', name);
return console.log(this.script(name, name));
}

debug('Trigger completion', env.line);
var line = env.line.replace(name, '').trim();
var first = line.split(' ')[0];

if (first) {
first = first.trim();
debug('Emit "%s" event', first);
process.nextTick(() => {
this.emit(first, env, this.recv.bind(this));
});
}

debug('Emit "%s" general event', name);
process.nextTick(() => {
this.emit(name, env, this.recv.bind(this));
});
}

// Completions callback
//
// completions handlers will call back this function with an Array of
// completion items.
recv(err, completions) {
debug('Completion results', err, completions);
if (err) return this.emit('error', err);
completions = Array.isArray(completions) ? completions : [completions];
console.log(completions.join(' '));
}

// Main utility to extract information from command line arguments and
// Environment variables, namely COMP args in "plumbing" mode.
parseEnv(options) {
options = Object.assign({}, options, this.options);
var args = options._ || process.argv.slice(2);
var env = options.env || process.env;

var cword = Number(env.COMP_CWORD);
var point = Number(env.COMP_POINT);
var line = env.COMP_LINE || '';

if (isNaN(cword)) cword = 0;
if (isNaN(point)) point = 0;

var partial = line.slice(0, point);

var parts = line.split(' ');
var prev = parts.slice(0, -1).slice(-1)[0];

var last = parts.slice(-1).join('');
var lastPartial = partial.split(' ').slice(-1).join('');

var complete = args[0] === 'completion';

if (!env.COMP_CWORD || !env.COMP_POINT || typeof env.COMP_LINE === 'undefined') {
complete = false;
}

return {
args: args,

// whether we act in "plumbing mode" or not
complete: complete,

// number of words
words: cword,

// cursor position
point: point,

// input line
line: line,

// part of line preceding point
partial: partial,

// last word of the line
last: last,

// last word of partial
lastPartial: lastPartial,

// word preceding last
prev: prev || ''
};
}

// Script templating helper
//
// Outputs npm's completion script with pkgname and completer placeholder
// replaced.
script(name, completer) {
return read(join(__dirname, '../scripts/completion.sh'), 'utf8')
.replace(/\{pkgname\}/g, name)
.replace(/{completer}/g, completer);
}

// Recursively walk up the `module.parent` chain to find original file.
findParent(module, last) {
if (!module.parent) return module;
return this.findParent(module.parent);
}

// Recursively walk up the directories, untill it finds the `file` provided,
// or reach the user $HOME dir.
findUp(file, dir) {
dir = path.resolve(dir || './');

// stop at user $HOME dir
if (dir === process.env.HOME) return;
if (exists(join(dir, file))) return join(dir, file);
return this.findUp(file, path.dirname(dir));
}

// When options.name is not defined, this gets called to attempt to determine
// completer name.
resolveName() {
// `module` is special node builtin
var parent = this.findParent(module);
if (!parent) return;

var jsonfile = this.findUp('package.json', path.dirname(parent.filename));
if (!jsonfile) return;

this.options.name = require(jsonfile).name;
}
}

module.exports = Complete;

0 comments on commit c3cba1d

Please sign in to comment.