omelette.coffee 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. ###
  2. # Omelette Simple Auto Completion for Node
  3. ###
  4. {EventEmitter} = require "events"
  5. path = require "path"
  6. fs = require "fs"
  7. os = require "os"
  8. depthOf = (object) ->
  9. level = 1
  10. for own key of object
  11. if typeof object[key] is 'object'
  12. depth = depthOf(object[key]) + 1
  13. level = Math.max(depth, level)
  14. level
  15. class Omelette extends EventEmitter
  16. {log} = console
  17. constructor: ->
  18. @compgen = process.argv.indexOf "--compgen"
  19. @install = process.argv.indexOf("--completion") > -1
  20. @installFish = process.argv.indexOf("--completion-fish") > -1
  21. isZsh = process.argv.indexOf("--compzsh") > -1
  22. isFish = process.argv.indexOf("--compfish") > -1
  23. @isDebug = process.argv.indexOf("--debug") > -1
  24. @fragment = parseInt(process.argv[@compgen+1])-(if isZsh then 1 else 0)
  25. @line = process.argv[@compgen+3]
  26. @word = @line?.trim().split(/\s+/).pop()
  27. {@HOME, @SHELL} = process.env
  28. setProgram: (programs)->
  29. programs = programs.split '|'
  30. [@program] = programs
  31. @programs = programs.map (program)-> program.replace ///
  32. [
  33. ^ # Do not allow except:
  34. A-Z # .. uppercase
  35. a-z # .. lowercase
  36. 0-9 # .. numbers
  37. \. # .. dots
  38. \_ # .. underscores
  39. \- # .. dashes
  40. ]
  41. ///g, ''
  42. setFragments: (@fragments...)->
  43. generate: ->
  44. data = {before: @word, @fragment, @line, @reply}
  45. @emit "complete", @fragments[@fragment-1], data
  46. @emit @fragments[@fragment-1], data
  47. @emit "$#{@fragment}", data
  48. process.exit()
  49. reply: (words=[])->
  50. console.log words.join? os.EOL
  51. process.exit()
  52. tree: (objectTree={})->
  53. depth = depthOf objectTree
  54. for level in [1..depth]
  55. @on "$#{level}", ({ fragment, reply, line })->
  56. accessor = new Function '_', """
  57. return _['#{line.split(/\s+/).slice(1).filter(Boolean).join("']['")}']
  58. """
  59. replies = if fragment is 1 then Object.keys(objectTree) else accessor(objectTree)
  60. reply do (replies = replies)->
  61. return replies() if replies instanceof Function
  62. return replies if replies instanceof Array
  63. return Object.keys(replies) if replies instanceof Object
  64. this
  65. generateCompletionCode: ->
  66. completions = @programs.map (program)=>
  67. completion = "_#{program}_completion"
  68. """
  69. ### #{program} completion - begin. generated by omelette.js ###
  70. if type compdef &>/dev/null; then
  71. #{completion}() {
  72. compadd -- `#{@program} --compzsh --compgen "${CURRENT}" "${words[CURRENT-1]}" "${BUFFER}"`
  73. }
  74. compdef #{completion} #{program}
  75. elif type complete &>/dev/null; then
  76. #{completion}() {
  77. local cur prev nb_colon
  78. _get_comp_words_by_ref -n : cur prev
  79. nb_colon=$(grep -o ":" <<< "$COMP_LINE" | wc -l)
  80. COMPREPLY=( $(compgen -W '$(#{@program} --compbash --compgen "$((COMP_CWORD - (nb_colon * 2)))" "$prev" "${COMP_LINE}")' -- "$cur") )
  81. __ltrim_colon_completions "$cur"
  82. }
  83. complete -F #{completion} #{program}
  84. fi
  85. ### #{program} completion - end ###
  86. """
  87. # Adding aliases for testing purposes
  88. completions.push @generateTestAliases() if @isDebug
  89. completions.join os.EOL
  90. generateCompletionCodeFish: ->
  91. completions = @programs.map (program)=>
  92. completion = "_#{program}_completion"
  93. """
  94. ### #{program} completion - begin. generated by omelette.js ###
  95. function #{completion}
  96. #{@program} --compfish --compgen (count (commandline -poc)) (commandline -pt) (commandline -pb)
  97. end
  98. complete -f -c #{program} -a '(#{completion})'
  99. ### #{program} completion - end ###
  100. """
  101. # Adding aliases for testing purposes
  102. completions.push @generateTestAliases() if @isDebug
  103. completions.join os.EOL
  104. generateTestAliases: ->
  105. fullPath = path.join process.cwd(), @program
  106. debugAliases = @programs.map((program)-> " alias #{program}=#{fullPath}").join os.EOL
  107. debugUnaliases = @programs.map((program)-> " unalias #{program}").join os.EOL
  108. """
  109. ### test method ###
  110. omelette-debug-#{@program}() {
  111. #{debugAliases}
  112. }
  113. omelette-nodebug-#{@program}() {
  114. #{debugUnaliases}
  115. }
  116. ### tests ###
  117. """
  118. checkInstall: ->
  119. if @install
  120. log @generateCompletionCode()
  121. process.exit()
  122. if @installFish
  123. log @generateCompletionCodeFish()
  124. process.exit()
  125. getActiveShell: ->
  126. {SHELL} = process.env
  127. if SHELL.match /bash/ then 'bash'
  128. else if SHELL.match /zsh/ then 'zsh'
  129. else if SHELL.match /fish/ then 'fish'
  130. getDefaultShellInitFile: ->
  131. fileAt = (root)->
  132. (file)-> path.join root, file
  133. fileAtHome = fileAt @HOME
  134. switch @shell = @getActiveShell()
  135. when 'bash' then fileAtHome '.bash_profile'
  136. when 'zsh' then fileAtHome '.zshrc'
  137. when 'fish' then fileAtHome '.config/fish/config.fish'
  138. setupShellInitFile: (initFile=@getDefaultShellInitFile())->
  139. template = (command)=>
  140. """
  141. # begin #{@program} completion
  142. #{command}
  143. # end #{@program} completion
  144. """
  145. switch @shell
  146. when 'bash'
  147. programFolder = path.join @HOME, ".#{@program}"
  148. completionPath = path.join programFolder, 'completion.sh'
  149. fs.mkdirSync programFolder unless fs.existsSync programFolder
  150. fs.writeFileSync completionPath, @generateCompletionCode()
  151. fs.appendFileSync initFile, template "source #{completionPath}"
  152. when 'zsh'
  153. fs.appendFileSync initFile, template ". <(#{@program} --completion)"
  154. when 'fish'
  155. fs.appendFileSync initFile, template "#{@program} --completion-fish | source"
  156. process.exit();
  157. init: ->
  158. do @generate if @compgen > -1
  159. module.exports = (template, args...)->
  160. if template instanceof Array and args.length > 0
  161. [program, callbacks] = [template[0].trim(), args]
  162. fragments = callbacks.map (callback, index) -> "arg#{index}"
  163. else
  164. [program, fragments...] = template.split /\s+/
  165. callbacks = []
  166. fragments = fragments.map (fragment)-> fragment.replace /^\<+|\>+$/g, ''
  167. _omelette = new Omelette
  168. _omelette.setProgram program
  169. _omelette.setFragments fragments...
  170. _omelette.checkInstall()
  171. for callback, index in callbacks
  172. fragment = "arg#{index}"
  173. do (callback = callback)->
  174. _omelette.on fragment, (args...)->
  175. @reply if callback instanceof Array
  176. callback
  177. else
  178. callback args...
  179. _omelette