217 lines
6.4 KiB
CoffeeScript
217 lines
6.4 KiB
CoffeeScript
###
|
|
# Omelette Simple Auto Completion for Node
|
|
###
|
|
{EventEmitter} = require "events"
|
|
path = require "path"
|
|
fs = require "fs"
|
|
os = require "os"
|
|
|
|
depthOf = (object) ->
|
|
level = 1
|
|
for own key of object
|
|
if typeof object[key] is 'object'
|
|
depth = depthOf(object[key]) + 1
|
|
level = Math.max(depth, level)
|
|
level
|
|
|
|
class Omelette extends EventEmitter
|
|
|
|
{log} = console
|
|
|
|
constructor: ->
|
|
@compgen = process.argv.indexOf "--compgen"
|
|
@install = process.argv.indexOf("--completion") > -1
|
|
@installFish = process.argv.indexOf("--completion-fish") > -1
|
|
isZsh = process.argv.indexOf("--compzsh") > -1
|
|
isFish = process.argv.indexOf("--compfish") > -1
|
|
@isDebug = process.argv.indexOf("--debug") > -1
|
|
|
|
@fragment = parseInt(process.argv[@compgen+1])-(if isZsh then 1 else 0)
|
|
@line = process.argv[@compgen+3]
|
|
@word = @line?.trim().split(/\s+/).pop()
|
|
|
|
{@HOME, @SHELL} = process.env
|
|
|
|
setProgram: (programs)->
|
|
programs = programs.split '|'
|
|
[@program] = programs
|
|
@programs = programs.map (program)-> program.replace ///
|
|
[
|
|
^ # Do not allow except:
|
|
A-Z # .. uppercase
|
|
a-z # .. lowercase
|
|
0-9 # .. numbers
|
|
\. # .. dots
|
|
\_ # .. underscores
|
|
\- # .. dashes
|
|
]
|
|
///g, ''
|
|
|
|
setFragments: (@fragments...)->
|
|
|
|
generate: ->
|
|
data = {before: @word, @fragment, @line, @reply}
|
|
@emit "complete", @fragments[@fragment-1], data
|
|
@emit @fragments[@fragment-1], data
|
|
@emit "$#{@fragment}", data
|
|
process.exit()
|
|
|
|
reply: (words=[])->
|
|
console.log words.join? os.EOL
|
|
process.exit()
|
|
|
|
tree: (objectTree={})->
|
|
depth = depthOf objectTree
|
|
for level in [1..depth]
|
|
@on "$#{level}", ({ fragment, reply, line })->
|
|
accessor = new Function '_', """
|
|
return _['#{line.split(/\s+/).slice(1).filter(Boolean).join("']['")}']
|
|
"""
|
|
replies = if fragment is 1 then Object.keys(objectTree) else accessor(objectTree)
|
|
reply do (replies = replies)->
|
|
return replies() if replies instanceof Function
|
|
return replies if replies instanceof Array
|
|
return Object.keys(replies) if replies instanceof Object
|
|
this
|
|
|
|
generateCompletionCode: ->
|
|
completions = @programs.map (program)=>
|
|
completion = "_#{program}_completion"
|
|
"""
|
|
### #{program} completion - begin. generated by omelette.js ###
|
|
if type compdef &>/dev/null; then
|
|
#{completion}() {
|
|
compadd -- `#{@program} --compzsh --compgen "${CURRENT}" "${words[CURRENT-1]}" "${BUFFER}"`
|
|
}
|
|
compdef #{completion} #{program}
|
|
elif type complete &>/dev/null; then
|
|
#{completion}() {
|
|
local cur prev nb_colon
|
|
_get_comp_words_by_ref -n : cur prev
|
|
nb_colon=$(grep -o ":" <<< "$COMP_LINE" | wc -l)
|
|
|
|
COMPREPLY=( $(compgen -W '$(#{@program} --compbash --compgen "$((COMP_CWORD - (nb_colon * 2)))" "$prev" "${COMP_LINE}")' -- "$cur") )
|
|
|
|
__ltrim_colon_completions "$cur"
|
|
}
|
|
complete -F #{completion} #{program}
|
|
fi
|
|
### #{program} completion - end ###
|
|
"""
|
|
|
|
# Adding aliases for testing purposes
|
|
completions.push @generateTestAliases() if @isDebug
|
|
completions.join os.EOL
|
|
|
|
generateCompletionCodeFish: ->
|
|
completions = @programs.map (program)=>
|
|
completion = "_#{program}_completion"
|
|
"""
|
|
### #{program} completion - begin. generated by omelette.js ###
|
|
function #{completion}
|
|
#{@program} --compfish --compgen (count (commandline -poc)) (commandline -pt) (commandline -pb)
|
|
end
|
|
complete -f -c #{program} -a '(#{completion})'
|
|
### #{program} completion - end ###
|
|
"""
|
|
|
|
# Adding aliases for testing purposes
|
|
completions.push @generateTestAliases() if @isDebug
|
|
completions.join os.EOL
|
|
|
|
generateTestAliases: ->
|
|
fullPath = path.join process.cwd(), @program
|
|
debugAliases = @programs.map((program)-> " alias #{program}=#{fullPath}").join os.EOL
|
|
debugUnaliases = @programs.map((program)-> " unalias #{program}").join os.EOL
|
|
|
|
"""
|
|
### test method ###
|
|
omelette-debug-#{@program}() {
|
|
#{debugAliases}
|
|
}
|
|
omelette-nodebug-#{@program}() {
|
|
#{debugUnaliases}
|
|
}
|
|
### tests ###
|
|
"""
|
|
|
|
checkInstall: ->
|
|
if @install
|
|
log @generateCompletionCode()
|
|
process.exit()
|
|
if @installFish
|
|
log @generateCompletionCodeFish()
|
|
process.exit()
|
|
|
|
getActiveShell: ->
|
|
{SHELL} = process.env
|
|
if SHELL.match /bash/ then 'bash'
|
|
else if SHELL.match /zsh/ then 'zsh'
|
|
else if SHELL.match /fish/ then 'fish'
|
|
|
|
getDefaultShellInitFile: ->
|
|
|
|
fileAt = (root)->
|
|
(file)-> path.join root, file
|
|
|
|
fileAtHome = fileAt @HOME
|
|
|
|
switch @shell = @getActiveShell()
|
|
when 'bash' then fileAtHome '.bash_profile'
|
|
when 'zsh' then fileAtHome '.zshrc'
|
|
when 'fish' then fileAtHome '.config/fish/config.fish'
|
|
|
|
setupShellInitFile: (initFile=@getDefaultShellInitFile())->
|
|
|
|
template = (command)=>
|
|
"""
|
|
|
|
# begin #{@program} completion
|
|
#{command}
|
|
# end #{@program} completion
|
|
|
|
"""
|
|
|
|
switch @shell
|
|
when 'bash'
|
|
programFolder = path.join @HOME, ".#{@program}"
|
|
completionPath = path.join programFolder, 'completion.sh'
|
|
|
|
fs.mkdirSync programFolder unless fs.existsSync programFolder
|
|
fs.writeFileSync completionPath, @generateCompletionCode()
|
|
fs.appendFileSync initFile, template "source #{completionPath}"
|
|
|
|
when 'zsh'
|
|
fs.appendFileSync initFile, template ". <(#{@program} --completion)"
|
|
|
|
when 'fish'
|
|
fs.appendFileSync initFile, template "#{@program} --completion-fish | source"
|
|
|
|
process.exit();
|
|
|
|
init: ->
|
|
do @generate if @compgen > -1
|
|
|
|
module.exports = (template, args...)->
|
|
if template instanceof Array and args.length > 0
|
|
[program, callbacks] = [template[0].trim(), args]
|
|
fragments = callbacks.map (callback, index) -> "arg#{index}"
|
|
else
|
|
[program, fragments...] = template.split /\s+/
|
|
callbacks = []
|
|
|
|
fragments = fragments.map (fragment)-> fragment.replace /^\<+|\>+$/g, ''
|
|
_omelette = new Omelette
|
|
_omelette.setProgram program
|
|
_omelette.setFragments fragments...
|
|
_omelette.checkInstall()
|
|
for callback, index in callbacks
|
|
fragment = "arg#{index}"
|
|
do (callback = callback)->
|
|
_omelette.on fragment, (args...)->
|
|
@reply if callback instanceof Array
|
|
callback
|
|
else
|
|
callback args...
|
|
_omelette
|