### # 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