require 'tempfile' require 'shellwords' module Commander ## # = User Interaction # # Commander's user interaction module mixes in common # methods which extend HighLine's functionality such # as a #password method rather than calling #ask directly. module UI module_function #-- # Auto include growl when available. #++ begin require 'growl' rescue LoadError # Do nothing else include Growl end ## # Ask the user for a password. Specify a custom # _message_ other than 'Password: ' or override the # default _mask_ of '*'. def password(message = 'Password: ', mask = '*') pass = ask(message) { |q| q.echo = mask } pass = password message, mask if pass.nil? || pass.empty? pass end ## # Choose from a set array of _choices_. def choose(message = nil, *choices, &block) say message if message super(*choices, &block) end ## # 'Log' an _action_ to the terminal. This is typically used # for verbose output regarding actions performed. For example: # # create path/to/file.rb # remove path/to/old_file.rb # remove path/to/old_file2.rb # def log(action, *args) say format('%15s %s', action, args.join(' ')) end ## # 'Say' something using the OK color (green). # # === Examples # say_ok 'Everything is fine' # say_ok 'It is ok', 'This is ok too' # def say_ok(*args) args.each do |arg| say $terminal.color(arg, :green) end end ## # 'Say' something using the WARNING color (yellow). # # === Examples # say_warning 'This is a warning' # say_warning 'Be careful', 'Think about it' # def say_warning(*args) args.each do |arg| say $terminal.color(arg, :yellow) end end ## # 'Say' something using the ERROR color (red). # # === Examples # say_error 'Everything is not fine' # say_error 'It is not ok', 'This is not ok too' # def say_error(*args) args.each do |arg| say $terminal.color(arg, :red) end end ## # 'Say' something using the specified color # # === Examples # color 'I am blue', :blue # color 'I am bold', :bold # color 'White on Red', :white, :on_red # # === Notes # You may use: # * color: black blue cyan green magenta red white yellow # * style: blink bold clear underline # * highligh: on_ def color(*args) say $terminal.color(*args) end ## # Speak _message_ using _voice_ at a speaking rate of _rate_ # # Voice defaults to 'Alex', which is one of the better voices. # Speaking rate defaults to 175 words per minute # # === Examples # # speak 'What is your favorite food? ' # food = ask 'favorite food?: ' # speak "Wow, I like #{food} too. We have so much in common." # speak "I like #{food} as well!", "Victoria", 190 # # === Notes # # * MacOS only # def speak(message, voice = :Alex, rate = 175) Thread.new { applescript "say #{message.inspect} using #{voice.to_s.inspect} speaking rate #{rate}" } end ## # Converse with speech recognition. # # Currently a "poorman's" DSL to utilize applescript and # the MacOS speech recognition server. # # === Examples # # case converse 'What is the best food?', :cookies => 'Cookies', :unknown => 'Nothing' # when :cookies # speak 'o.m.g. you are awesome!' # else # case converse 'That is lame, shall I convince you cookies are the best?', :yes => 'Ok', :no => 'No', :maybe => 'Maybe another time' # when :yes # speak 'Well you see, cookies are just fantastic.' # else # speak 'Ok then, bye.' # end # end # # === Notes # # * MacOS only # def converse(prompt, responses = {}) i, commands = 0, responses.map { |_key, value| value.inspect }.join(',') statement = responses.inject '' do |inner_statement, (key, value)| inner_statement << ( (i += 1) == 1 ? %(if response is "#{value}" then\n) : %(else if response is "#{value}" then\n) ) << %(do shell script "echo '#{key}'"\n) end applescript( %( tell application "SpeechRecognitionServer" set response to listen for {#{commands}} with prompt "#{prompt}" #{statement} end if end tell ), ).strip.to_sym end ## # Execute apple _script_. def applescript(script) `osascript -e "#{ script.gsub('"', '\"') }"` end ## # Normalize IO streams, allowing for redirection of # +input+ and/or +output+, for example: # # $ foo # => read from terminal I/O # $ foo in # => read from 'in' file, output to terminal output stream # $ foo in out # => read from 'in' file, output to 'out' file # $ foo < in > out # => equivalent to above (essentially) # # Optionally a +block+ may be supplied, in which case # IO will be reset once the block has executed. # # === Examples # # command :foo do |c| # c.syntax = 'foo [input] [output]' # c.when_called do |args, options| # # or io(args.shift, args.shift) # io *args # str = $stdin.gets # puts 'input was: ' + str.inspect # end # end # def io(input = nil, output = nil, &block) $stdin = File.new(input) if input $stdout = File.new(output, 'r+') if output return unless block yield reset_io end ## # Reset IO to initial constant streams. def reset_io $stdin, $stdout = STDIN, STDOUT end ## # Find an editor available in path. Optionally supply the _preferred_ # editor. Returns the name as a string, nil if none is available. def available_editor(preferred = nil) [preferred, ENV['EDITOR'], 'mate -w', 'vim', 'vi', 'emacs', 'nano', 'pico'] .compact .find { |name| system("hash #{name.split.first} 2>&-") } end ## # Prompt an editor for input. Optionally supply initial # _input_ which is written to the editor. # # _preferred_editor_ can be hinted. # # === Examples # # ask_editor # => prompts EDITOR with no input # ask_editor('foo') # => prompts EDITOR with default text of 'foo' # ask_editor('foo', 'mate -w') # => prompts TextMate with default text of 'foo' # def ask_editor(input = nil, preferred_editor = nil) editor = available_editor preferred_editor program = Commander::Runner.instance.program(:name).downcase rescue 'commander' tmpfile = Tempfile.new program begin tmpfile.write input if input tmpfile.close system("#{editor} #{tmpfile.path.shellescape}") ? IO.read(tmpfile.path) : nil ensure tmpfile.unlink end end ## # Enable paging of output after called. def enable_paging return unless $stdout.tty? return unless Process.respond_to? :fork read, write = IO.pipe # Kernel.fork is not supported on all platforms and configurations. # As of Ruby 1.9, `Process.respond_to? :fork` should return false on # configurations that don't support it, but versions before 1.9 don't # seem to do this reliably and instead raise a NotImplementedError # (which is rescued below). if Kernel.fork $stdin.reopen read write.close read.close Kernel.select [$stdin] ENV['LESS'] = 'FSRX' unless ENV.key? 'LESS' pager = ENV['PAGER'] || 'less' exec pager rescue exec '/bin/sh', '-c', pager else # subprocess $stdout.reopen write $stderr.reopen write if $stderr.tty? write.close read.close end rescue NotImplementedError ensure write.close if write && !write.closed? read.close if read && !read.closed? end ## # Output progress while iterating _arr_. # # === Examples # # uris = %w( http://vision-media.ca http://google.com ) # progress uris, :format => "Remaining: :time_remaining" do |uri| # res = open uri # end # def progress(arr, options = {}) bar = ProgressBar.new arr.length, options bar.show arr.each { |v| bar.increment yield(v) } end ## # Implements ask_for_CLASS methods. module AskForClass # All special cases in HighLine::Question#convert, except those that implement #parse ( [Float, Integer, String, Symbol, Regexp, Array, File, Pathname] + # All Classes that respond to #parse Object.constants.map do |const| # Ignore constants that trigger deprecation warnings Object.const_get(const) unless [:Config, :TimeoutError].include?(const) end.select do |const| const.class == Class && const.respond_to?(:parse) end ).each do |klass| define_method "ask_for_#{klass.to_s.downcase}" do |prompt| $terminal.ask(prompt, klass) end end end ## # Substitute _hash_'s keys with their associated values in _str_. def replace_tokens(str, hash) #:nodoc: hash.inject(str) do |string, (key, value)| string.gsub ":#{key}", value.to_s end end ## # = Progress Bar # # Terminal progress bar utility. In its most basic form # requires that the developer specifies when the bar should # be incremented. Note that a hash of tokens may be passed to # #increment, (or returned when using Object#progress). # # uris = %w( # http://vision-media.ca # http://yahoo.com # http://google.com # ) # # bar = Commander::UI::ProgressBar.new uris.length, options # threads = [] # uris.each do |uri| # threads << Thread.new do # begin # res = open uri # bar.increment :uri => uri # rescue Exception => e # bar.increment :uri => "#{uri} failed" # end # end # end # threads.each { |t| t.join } # # The Object method #progress is also available: # # progress uris, :width => 10 do |uri| # res = open uri # { :uri => uri } # Can now use :uri within :format option # end # class ProgressBar ## # Creates a new progress bar. # # === Options # # :title Title, defaults to "Progress" # :width Width of :progress_bar # :progress_str Progress string, defaults to "=" # :incomplete_str Incomplete bar string, defaults to '.' # :format Defaults to ":title |:progress_bar| :percent_complete% complete " # :tokens Additional tokens replaced within the format string # :complete_message Defaults to "Process complete" # # === Tokens # # :title # :percent_complete # :progress_bar # :step # :steps_remaining # :total_steps # :time_elapsed # :time_remaining # def initialize(total, options = {}) @total_steps, @step, @start_time = total, 0, Time.now @title = options.fetch :title, 'Progress' @width = options.fetch :width, 25 @progress_str = options.fetch :progress_str, '=' @incomplete_str = options.fetch :incomplete_str, '.' @complete_message = options.fetch :complete_message, 'Process complete' @format = options.fetch :format, ':title |:progress_bar| :percent_complete% complete ' @tokens = options.fetch :tokens, {} end ## # Completion percentage. def percent_complete if @total_steps.zero? 100 else @step * 100 / @total_steps end end ## # Time that has elapsed since the operation started. def time_elapsed Time.now - @start_time end ## # Estimated time remaining. def time_remaining (time_elapsed / @step) * steps_remaining end ## # Number of steps left. def steps_remaining @total_steps - @step end ## # Formatted progress bar. def progress_bar (@progress_str * (@width * percent_complete / 100)).ljust @width, @incomplete_str end ## # Generates tokens for this step. def generate_tokens { title: @title, percent_complete: percent_complete, progress_bar: progress_bar, step: @step, steps_remaining: steps_remaining, total_steps: @total_steps, time_elapsed: format('%0.2fs', time_elapsed), time_remaining: @step > 0 ? format('%0.2fs', time_remaining) : '', }.merge! @tokens end ## # Output the progress bar. def show return if finished? erase_line if completed? $terminal.say UI.replace_tokens(@complete_message, generate_tokens) if @complete_message.is_a? String else $terminal.say UI.replace_tokens(@format, generate_tokens) << ' ' end end ## # Whether or not the operation is complete, and we have finished. def finished? @step == @total_steps + 1 end ## # Whether or not the operation has completed. def completed? @step == @total_steps end ## # Increment progress. Optionally pass _tokens_ which # can be displayed in the output format. def increment(tokens = {}) @step += 1 @tokens.merge! tokens if tokens.is_a? Hash show end ## # Erase previous terminal line. def erase_line # highline does not expose the output stream $terminal.instance_variable_get('@output').print "\r\e[K" end end end end