Skip to content

Commit

Permalink
Implement fish bridge, template system depending on $SHELL
Browse files Browse the repository at this point in the history
  • Loading branch information
mklabs committed Apr 26, 2016
1 parent bed76b3 commit 1823230
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 58 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.completions/tt
.tern-port
src/
.completions/
6 changes: 4 additions & 2 deletions lib/cli.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const debug = require('./debug')('tabtab');
const minimist = require('minimist');
const npmlog = require('npmlog');
const commands = require('./commands');

import Commands from './commands';

let opts = minimist(process.argv.slice(2), {
alias: {
Expand All @@ -11,9 +12,10 @@ let opts = minimist(process.argv.slice(2), {
});

const cmd = opts._[0];

var commands = new Commands(opts);
const allowed = commands.allowed;

debug('>', cmd);
if (opts.help) {
console.log(commands.help());
process.exit(0);
Expand Down
18 changes: 8 additions & 10 deletions lib/commands/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const fs = require('fs');
const path = require('path');
const join = path.join;
const debug = require('../debug')('tabtab:commands')
const fs = require('fs');
const path = require('path');
const join = path.join;
const debug = require('../debug')('tabtab:commands')
const Complete = require('../complete');

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

const {
Expand All @@ -19,7 +19,7 @@ const {
//
// var commands = new Commands({});
// commands.install();
class Commands {
export default class Commands {

get allowed() {
return ['install', 'uninstall', 'list', 'search', 'add', 'rm', 'completion'];
Expand All @@ -40,7 +40,7 @@ class Commands {
options = options || {};
var script = this.complete.script(this.name, this.name || 'tabtab');
this.installer
.handle(this.complete.resolve('name'), options)
.handle(this.options.name || this.complete.resolve('name'), options)
.catch((e) => {
console.error('oh oh', e.stack);
process.exit(1);
Expand All @@ -50,7 +50,7 @@ class Commands {
// Public: Delegates to this.handle
completion(options) {
options = options || this.options;
debug('cmd completion');
debug('command invoke');
return this.complete.handle(options);
}

Expand Down Expand Up @@ -93,5 +93,3 @@ class Commands {
`;
}
}

module.exports = new Commands();
39 changes: 18 additions & 21 deletions lib/complete.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,13 @@ class Complete extends EventEmitter {
// });
constructor(options) {
super();
debug('Init complete');
this.options = options || this.defaults;
this.options.name = this.options.name || this.resolve('name');
}

start() {
debug('Listen for completion');
this.handle();
}

Expand All @@ -59,26 +63,22 @@ class Complete extends EventEmitter {
//
// options - options hash to pass to self#parseEnv
handle(options) {
debug('handle');
options = options || this.options;
options = Object.assign({}, options, this.options);
var env = this.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) {
return debug('Completion command but without COMP args');
}

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

debug(env);
if (!this.completePackage(env)) {
debug('No tabtab configuration in package.json, will emit events.');
process.nextTick(() => {
Expand All @@ -95,27 +95,25 @@ class Complete extends EventEmitter {
completePackage(env) {
debug('Lookup tabtab configuration in package.json');
var config = this.resolve('tabtab');
if (!config) return;

var pkgname = config[this.options.name];

var prop = env.last || env.prev;
if (!prop) return false;

debug(config, prop);
var command = config[prop];
if (!command) {
if (pkgname) return this.recv(null, pkgname, env);
}

var completions = this.recv(null, command, env);
debug('compl', completions);
return true
}

send(evt, env, done) {
debug('Emit evt:', evt);
var res = this.emit(evt, env, done);
debug('>', res);
return res;
}

Expand All @@ -129,20 +127,19 @@ class Complete extends EventEmitter {
recv(err, completions, env) {
env = env || this.env;

debug('Completion results', err, completions);
debug('> Completions:', completions);
if (err) return this.emit('error', err);
completions = Array.isArray(completions) ? completions : [completions];

// only return results that match last part of the line (cursor right next
// to last word, without space)
completions = completions.filter((result) => {
if (!env) return true;

debug('result %s vs %s', result, env.last);
return result.indexOf(env.last) !== -1;
}).map((l) => {
return l + ' ';
})
// completions = completions.filter((result) => {
// if (!env) return true;
//
// return result.indexOf(env.last) !== -1;
// }).map((l) => {
// return l + ' ';
// });

console.log(completions.join('\n'));
return completions;
Expand Down Expand Up @@ -221,8 +218,10 @@ class Complete extends EventEmitter {
// 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')
script(name, completer, shell) {
debug('Script %s name with %s completer', name, completer);
debug('Shell', shell);
return read(join(__dirname, `../scripts/${shell || 'completion'}.sh`), 'utf8')
.replace(/\{pkgname\}/g, name)
.replace(/{completer}/g, completer);
}
Expand Down Expand Up @@ -252,13 +251,11 @@ class Complete extends EventEmitter {
// It'll attempt to follow the module chain and find the package.json file to
// determine the command name being completed.
resolve(prop) {
debug('Complete resolve %s', prop);
// `module` is special node builtin
var parent = this.findParent(module);
if (!parent) return;

var dirname = path.dirname(parent.filename);
debug('d', dirname);

// was invoked by cli tabtab, fallback to local package.json
if (parent.filename === path.join(__dirname, '../bin/tabtab')) {
Expand Down
55 changes: 30 additions & 25 deletions lib/installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,13 @@ export default class Installer {

constructor(options, complete) {
this.options = options || {};
debug('init', this.options);
this.complete = complete;
}

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

if (options.stdout) return new Promise((r, errback) => {
Expand All @@ -50,14 +48,15 @@ export default class Installer {
var destination = data.destination;
debug('Installing completion script to %s directory', destination);

var script = this.complete.script(this.options.name, this.options.name);
debug('Script', this.options.name, this.options.completer, this.options);
var script = this.complete.script(this.options.name, this.options.completer || this.options.name, this.template);

if (destination === 'stdout') return process.stdout.write('\n\n' + script + '\n');

debug('d', destination);
if (destination === 'bashrc') destination = path.join(this.home, '.bashrc');
else if (destination === 'zshrc') destination = path.join(this.home, '.zshrc');
else if (destination === 'fish') destination = path.join(this.home, '.config/fish/config.fish');
else if (destination === 'fishdir') destination = path.join(this.home, '.config/fish/completions', this.options.name + '.fish');
else destination = path.join(destination, this.options.name);

return new Promise(this.createStream.bind(this, destination))
Expand All @@ -72,7 +71,6 @@ export default class Installer {
else if (err) return errback(err);

debug('Create output stream on', destination, flags);

mkdirp(path.dirname(destination), (err) => {
if (err) return errback(err);

Expand All @@ -93,33 +91,36 @@ export default class Installer {
});
}

installCompletion(destination, out, r, errback) {
installCompletion(destination, out) {
var name = this.options.name;
var script = this.complete.script(name, name);
var filename = path.join(__dirname, '../.completions', name);
debug('Shell template:', this.template);
var script = this.complete.script(name, this.options.completer || name, this.template);
var filename = path.join(__dirname, '../.completions', name + '.' + this.template);
debug('Writing actual completion script to %s', filename);

// First write internal completion script in a local .comletions directory
// in this module. This gets sourced in user scripts after, to avoid
// cluttering bash/zshrc files with too much boilerplate.
debug('Writing actual completion script to %s', filename);
fs.writeFile(filename, script, (err) => {
if (err) return errback(err);

var regex = new RegExp(`tabtab source for ${name}`);
fs.readFile(destination, 'utf8', (err, content) => {
return new Promise((r, errback) => {
fs.writeFile(filename, script, (err) => {
if (err) return errback(err);
if (regex.test(content)) {
return debug('Already installed %s in %s', name, destination);
}

console.error('\n[tabtab] Adding source line to load $TABTAB_DIR/.completions/%s\nin %s\n', filename, destination);
var regex = new RegExp(`tabtab source for ${name}`);
fs.readFile(destination, 'utf8', (err, content) => {
if (err) return errback(err);
if (regex.test(content)) {
return debug('Already installed %s in %s', name, destination);
}

console.error('\n[tabtab] Adding source line to load $TABTAB_DIR/.completions/%s\nin %s\n', filename, destination);

out.write('\n');
debug('. %s > %s', filename, destination);
out.write('\n# tabtab source for ' + name + ' package');
out.write('\n# uninstall by removing these lines or running ');
out.write('`tabtab uninstall ' + name + '`');
out.write('\n. ' + filename);
out.write('\n');
debug('. %s > %s', filename, destination);
out.write('\n# tabtab source for ' + name + ' package');
out.write('\n# uninstall by removing these lines or running ');
out.write('`tabtab uninstall ' + name + '`');
out.write('\n. ' + filename);
});
});
});
}
Expand Down Expand Up @@ -170,6 +171,7 @@ export default class Installer {

fish() {
debug('Fish shell detected');
this.template = 'fish';
return new Promise((r, errback) => {
var dir = path.join(this.home, '.config/fish/completions');
return r([{
Expand All @@ -178,13 +180,15 @@ export default class Installer {
short: 'fish'
}, {
name: 'Fish completion directory (' + dir + ')',
value: dir
value: 'fishdir',
short: 'fish'
}]);
});
}

bash() {
debug('Bash shell detected');
this.template = 'bash';
var entries = [{
name: 'Bash config file (~/.bashrc)',
value: 'bashrc',
Expand Down Expand Up @@ -216,6 +220,7 @@ export default class Installer {

zsh() {
debug('Zsh shell detected');
this.template = 'zsh';
return new Promise((r, errback) => {
var dir = '/usr/local/share/zsh/site-functions';
return r([{
Expand Down
22 changes: 22 additions & 0 deletions scripts/bash.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
###-begin-{pkgname}-completion-###
if type complete &>/dev/null; then
_{pkgname}_completion () {
local words cword
if type _get_comp_words_by_ref &>/dev/null; then
_get_comp_words_by_ref -n = -n @ -w words -i cword
else
cword="$COMP_CWORD"
words=("${COMP_WORDS[@]}")
fi

local si="$IFS"
IFS=$'\n' COMPREPLY=($(COMP_CWORD="$cword" \
COMP_LINE="$COMP_LINE" \
COMP_POINT="$COMP_POINT" \
{completer} completion -- "${words[@]}" \
2>/dev/null)) || return $?
IFS="$si"
}
complete -o default -F _{pkgname}_completion {pkgname}
fi
###-end-{pkgname}-completion-###
9 changes: 9 additions & 0 deletions scripts/fish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
###-begin-{pkgname}-completion-###
function _{pkgname}_completion
set cmd (commandline -opc)
set cursor (commandline -C)
eval env DEBUG=\"" \"" COMP_CWORD=\""$cmd\"" COMP_LINE=\""$cmd"\" COMP_POINT="\"$cursor"\" {completer} completion -- $cmd
end

complete -c {pkgname} -a "(_{pkgname}_completion)"
###-end-{pkgname}-completion-###
31 changes: 31 additions & 0 deletions scripts/zsh.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
###-begin-{pkgname}-completion-###
if type compdef &>/dev/null; then
_{pkgname}_completion() {
local si=$IFS
compadd -- $(COMP_CWORD=$((CURRENT-1)) \
COMP_LINE=$BUFFER \
COMP_POINT=0 \
{completer} completion -- "${words[@]}" \
2>/dev/null)
IFS=$si
}
compdef _{pkgname}_completion {pkgname}
elif type compctl &>/dev/null; then
_{pkgname}_completion () {
local cword line point words si
read -Ac words
read -cn cword
let cword-=1
read -l line
read -ln point
si="$IFS"
IFS=$'\n' reply=($(COMP_CWORD="$cword" \
COMP_LINE="$line" \
COMP_POINT="$point" \
{completer} completion -- "${words[@]}" \
2>/dev/null)) || return $?
IFS="$si"
}
compctl -K _{pkgname}_completion {pkgname}
fi
###-end-{pkgname}-completion-###

0 comments on commit 1823230

Please sign in to comment.