Skip to content

Commit

Permalink
Prompt user for completion script installation method
Browse files Browse the repository at this point in the history
- bashrc
- zshrc
- pkg-config bash-completion completionsdir
  - /usr/share/bash-completion/completions
- pkg-config bash-completion compatdir
  - /etc/bash_completion.d

Asks pkg-config for bash-completion variable, otherwise asks only for
bashrc or zshrc
  • Loading branch information
mklabs committed Apr 23, 2016
1 parent 62c4362 commit 73f6090
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 69 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules
*.swp
src
docs
.tern-port
18 changes: 9 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
test: babel
mocha test/ -R min
mocha test/ -R spec

babel:
babel lib/ -d src/
babel lib/ -d src/

lint:
eslint . --env es6
eslint . --env es6

docs:
tomdox lib/complete.js lib/debug.js lib/index.js lib/commands/*.js --primary orange --accent deep_orange --icon keyboard_tab --prefix https://mklabs.github.io/node-tabtab/
tomdox lib/complete.js lib/debug.js lib/index.js lib/commands/*.js --primary orange --accent deep_orange --icon keyboard_tab --prefix https://mklabs.github.io/node-tabtab/

ghpages:
bake docs && git co gh-pages && git rm -r . && cp -r docs/* . && git add . && git ci
bake docs && git co gh-pages && git rm -r . && cp -r docs/* . && git add . && git ci

env:
@echo $(PATH)
@echo $(PATH)

build: test lint
build: test

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

watch:
watchd lib/**/* test/**/* bin/* -c 'bake build'
watchd lib/*.js lib/**/* test/**/* bin/* -c 'bake build'

all: build watch
4 changes: 3 additions & 1 deletion lib/commands/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const join = path.join;
const debug = require('../debug')('tabtab:commands')

const Complete = require('../complete');
import Installer from '../installer';

const {
readFileSync: read,
Expand All @@ -29,6 +30,7 @@ class Commands {
constructor(options) {
this.options = options || {};
this.complete = new Complete(this.options);
this.installer = new Installer(this.options);
}

// Commands
Expand All @@ -37,7 +39,7 @@ class Commands {
install(options) {
options = options || {};
var script = this.complete.script(this.name, this.name || 'tabtab');
console.log(script);
this.installer.handle(this.complete.resolveName());
}

// Public: Delegates to this.handle
Expand Down
100 changes: 53 additions & 47 deletions lib/complete.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,34 +33,31 @@ class Complete extends EventEmitter {

// Public: Complete constructor, accepts options hash with
//
// options - Accepts options hash (default: {})
// options - Accepts options hash (default: {})
//
// Examples
//
// new Complete({
// // the String package name being completed, defaults to process.title
// // (if not node default) or will attempt to determine parent's
// // package.json location and extract the name from it.
// // the String package name being completed, defaults to process.title
// // (if not node default) or will attempt to determine parent's
// // package.json location and extract the name from it.
// name: 'foobar'
// });
constructor(options) {
super();
this.options = options || this.defaults;

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

this.handle();
}

// Public: Takes care of all the completion plumbing mechanism.
//
// It checks the environment to determine if we act in plumbing mode or not,
// to parse COMP args and emit the appropriated events to gather completion
// results.
//
// options - options hash to pass to self#parseEnv
// Public: Takes care of all the completion plumbing mechanism.
//
// It checks the environment to determine if we act in plumbing mode or not,
// to parse COMP args and emit the appropriated events to gather completion
// results.
//
// options - options hash to pass to self#parseEnv
handle(options) {
options = options || this.options;
var env = this.parseEnv(this.options);
Expand Down Expand Up @@ -97,9 +94,9 @@ class Complete extends EventEmitter {
// Public: Completions handlers
//
// will call back this function with an Array of completion items.
//
// err - Error object
// completions - The Array of String completion results to write to stdout
//
// err - Error object
// completions - The Array of String completion results to write to stdout
recv(err, completions) {
debug('Completion results', err, completions);
if (err) return this.emit('error', err);
Expand All @@ -109,27 +106,27 @@ class Complete extends EventEmitter {

// Public: Main utility to extract information from command line arguments and
// Environment variables, namely COMP args in "plumbing" mode.
//
//
// options - The options hash as parsed by minimist, plus an env property
// representing user environment (default: { env: process.env })
// :_ - The arguments Array parsed by minimist (positional arguments)
// :env - The environment Object that holds COMP args (default: process.env)
// :_ - The arguments Array parsed by minimist (positional arguments)
// :env - The environment Object that holds COMP args (default: process.env)
//
// Examples
//
// var env = complete.parseEnv();
// // env:
// // args the positional arguments used
// // complete A Boolean indicating whether we act in "plumbing mode" or not
// // words The Number of words in the completed line
// // point A Number indicating cursor position
// // line The String input line
// // partial The String part of line preceding cursor position
// // last The last String word of the line
// // lastPartial The last word String of partial
// // prev The String word preceding last
//
// Returns the data env object.
// // args the positional arguments used
// // complete A Boolean indicating whether we act in "plumbing mode" or not
// // words The Number of words in the completed line
// // point A Number indicating cursor position
// // line The String input line
// // partial The String part of line preceding cursor position
// // last The last String word of the line
// // lastPartial The last word String of partial
// // prev The String word preceding last
//
// Returns the data env object.
parseEnv(options) {
options = options || {};
options = Object.assign({}, options, this.options);
Expand Down Expand Up @@ -174,12 +171,12 @@ class Complete extends EventEmitter {
//
// Outputs npm's completion script with pkgname and completer placeholder
// replaced.
//
// name - The String package/binary name
// complete - The String completer name, usualy the same as name above. Can
// differ to delegate the completion behavior to another command.
//
// Returns the script content with placeholders replaced
//
// name - The String package/binary name
// complete - The String completer name, usualy the same as name above. Can
// differ to delegate the completion behavior to another command.
//
// Returns the script content with placeholders replaced
script(name, completer) {
return read(join(__dirname, '../scripts/completion.sh'), 'utf8')
.replace(/\{pkgname\}/g, name)
Expand All @@ -192,8 +189,8 @@ class Complete extends EventEmitter {
return this.findParent(module.parent);
}

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

Expand All @@ -204,21 +201,30 @@ class Complete extends EventEmitter {
}

// Public: package name resolver.
//
// When options.name is not defined, this gets called to attempt to determine
//
// When options.name is not defined, this gets called to attempt to determine
// completer name.
//
// It'll attempt to follow the module chain and find the package.json file to
// determine the command name being completed.
//
// It'll attempt to follow the module chain and find the package.json file to
// determine the command name being completed.
resolveName() {
debug('Complete resolve name');
// `module` is special node builtin
var parent = this.findParent(module);
if (!parent) return;

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

// was invoked by cli tabtab, fallback to local package.json
if (parent.filename === path.join(__dirname, '../bin/tabtab')) {
dirname = path.resolve();
console.log('new dirname', dirname);
}

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

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

Expand Down
10 changes: 5 additions & 5 deletions lib/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ const path = require('path');
const util = require('util');
const dbg = require('debug');

process.env.TABTAB_DEBUG = process.env.TABTAB_DEBUG || '/tmp/tabtab.log';
// process.env.TABTAB_DEBUG = process.env.TABTAB_DEBUG || '/tmp/tabtab.log';

let out = process.env.TABTAB_DEBUG ?
fs.createWriteStream(process.env.TABTAB_DEBUG, { flags: 'a' }) :
null;

module.exports = log;
module.exports = debug;

// Internal: Facade to debug module, which provides the exact same interface.
//
Expand All @@ -29,8 +29,8 @@ module.exports = log;
// debug('Foo');
function debug(namespace) {
var log = dbg(namespace);
return () => {
out && out.write(util.format.apply(util, arguments) + '\n');
out || log.apply(null, arguments);
return (...args) => {
out && out.write(util.format.apply(util, args) + '\n');
out || log.apply(null, args);
}
}
4 changes: 2 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const Complete = require('./complete');

module.exports = tabtab;
complete.Commands = require('./commands');
complete.Complete = Complete;
tabtab.Commands = require('./commands');
tabtab.Complete = Complete;

// Public: Tabtab entry point.
//
Expand Down
118 changes: 118 additions & 0 deletions lib/installer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const debug = require('./debug')('tabtab:installer')


import path from 'path'
import inquirer from 'inquirer';
import { spawn, exec } from 'child_process';

// Public: Manage installation / setup of completion scripts.
//
// pkg-config --variable=completionsdir bash-completion
// pkg-config --variable=compatdir bash-completion
export default class Installer {
constructor(options) {
this.options = options || {};
debug('init', this.options);
}

// Called on install command.
//
// Performs the installation process.
handle(name) {
debug('handle', name);
this.options.name = name;

return this.prompt()
.then(this.writeTo.bind(this));
}

writeTo(data) {
var destination = data.destination;
debug('Installing completion script to %s directory', destination);

if (destination === 'bashrc') destination = path.join(this.home, '.bashrc');
else if (destination === 'zshrc') destination = path.join(this.home, '.zshrc');
else destination = path.join(destination, this.options.name);

debug('Installing completion script to %s directory', destination);

}

get home() {
return process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'];
}

// Prompts user for installation location.
prompt() {
var choices = [{
name: '~/.bashrc',
value: 'bashrc',
short: 'bash'
}, {
name: '~/.zshrc',
value: 'zshrc',
short: 'zsh'
}];

var prompts = [{
message: 'Where do you want to setup the completion script',
name: 'destination',
type: 'list',
choices: choices
}];

return this.completionsdir()
.then((dir) => {
if (dir) {
choices.push({
name: dir,
value: dir,
});
}

return this.compatdir();
})
.then((dir) => {
if (dir) {
choices.push({
name: dir,
value: dir
});
}

return this.ask(prompts);
});
}

ask(prompts) {
debug('Ask', prompts);
return inquirer.prompt(prompts);
}

// Public: pkg-config wrapper
pkgconfig(variable) {
return new Promise((r, errback) => {
var cmd = `pkg-config --variable=${variable} bash-completion`;
exec(cmd, function(err, stdout, stderr) {
if (err) return errback(err);
stdout = stdout.trim();
debug('Got %s for %s', stdout, variable);
r(stdout);
});
});
}

// Returns the pkg-config variable for "completionsdir" and bash-completion
// command.
completionsdir() {
debug('Asking pkg-config for completionsdir');
return this.pkgconfig('completionsdir');
}

// Returns the pkg-config variable for "compatdir" and bash-completion
// command.
compatdir() {
debug('Asking pkg-config for compatdir');
return this.pkgconfig('compatdir');
}
}

0 comments on commit 73f6090

Please sign in to comment.