module Transcriptic module UI # Mute the output of this instance of UI until {#unmute!} is called def mute! @mute = true end # Unmute the output of this instance of UI until {#mute!} is called def unmute! @mute = false end def quiet? @mute end def say(message = '', color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/)) return if quiet? super(message, color, force_new_line) end # @see {say} def info(message = '', color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/)) say(message, color, force_new_line) end def say_status(status, message, log_status = true) return if quiet? super(status, message, log_status) end def warn(message, color = :yellow) return if quiet? say(message, color) end def display(msg = "", newline = true) if newline puts(msg) else print(msg) STDOUT.flush end end def redisplay(line, line_break = false) display("\r\e[0K#{line}", line_break) end def time_ago(elapsed) if elapsed < 60 "#{elapsed.floor}s ago" elsif elapsed < (60 * 60) "#{(elapsed / 60).floor}m ago" else "#{(elapsed / 60 / 60).floor}h ago" end end def truncate(text, length) if text.size > length text[0, length - 2] + '..' else text end end def deprecate(version) display "!!! DEPRECATION WARNING: This command will be removed in version #{version}" display end def format_with_bang(message) return '' if message.to_s.strip == "" " ! " + message.split("\n").join("\n ! ") end def output_with_bang(message="", new_line=true) return if message.to_s.strip == "" display(format_with_bang(message), new_line) end def error(msg, color = :red) say msg, color exit 1 end def confirm_billing display display "This action will cause your account to be billed at the end of the month" display "Are you sure you want to do this? (y/n) ", false if ask.downcase == 'y' transcriptic.confirm_billing return true end end def confirm(message = "Are you sure you wish to continue? (y/n)?") ask(message).downcase == 'y' end def confirm_quote display display " ! To accept this quote and begin execution, type \"confirm\" below." display " ", false if ask("> ").downcase != "confirm" display " ! Aborted." false else true end end def confirm_command(resource_id) confirmed_resource = extract_option('--confirm', false) if confirmed_resource unless confirmed_resource == resource_id raise(Transcriptic::Command::CommandFailed, "Confirmed resource #{confirmed_resource} did not match the selected resource #{resource_id}.") end return true else display display " ! WARNING: Potentially Destructive Action" display " ! This command will affect the resource: #{resource_id}" display " ! To proceed, type \"#{resource_id}\" or re-run this command with --confirm #{resource_id}" display display "> ", false if ask.downcase != resource_id display " ! Input did not match #{resource_id}. Aborted." false else true end end end def format_date(date) date = Time.parse(date) if date.is_a?(String) date.strftime("%Y-%m-%d %H:%M %Z") end def display_table(objects, columns, headers) lengths = [] columns.each_with_index do |column, index| header = headers[index] lengths << longest([header].concat(objects.map { |o| o[column].to_s })) end display_row headers, lengths display_row headers.map { |header| "-" * header.length }, lengths objects.each do |row| display_row columns.map { |column| row[column] }, lengths end end def display_row(row, lengths) row.zip(lengths).each do |column, length| format = column.is_a?(Fixnum) ? "%#{length}s " : "%-#{length}s " display format % column, false end display end def json_encode(object) Transcriptic::OkJson.encode(object) rescue Transcriptic::OkJson::ParserError nil end def json_decode(json) Transcriptic::OkJson.decode(json) rescue Transcriptic::OkJson::ParserError nil end def set_buffer(enable) return unless $stdin.tty? if enable `stty icanon echo` else `stty -icanon -echo` end rescue # fails on windows end def get_terminal_environment { "TERM" => ENV["TERM"], "COLUMNS" => `tput cols`, "LINES" => `tput lines` } rescue { "TERM" => ENV["TERM"] } end def fail(message) raise Transcriptic::Command::CommandFailed, message end ## DISPLAY HELPERS def arrow(message) "====> #{message}" end def indent(message, indent = 3) (" " * indent) + message end def output_with_indent(message, indent = 3) return if message.to_s.strip == "" display (" " * indent) + message.split("\n").join("\n ") end def wait_spinner(message, fps = 10) chars = %w[| / - \\] delay = 1.0 / fps iter = 0 display arrow(message), false spinner = Thread.new do while iter do # Keep spinning until told otherwise print chars[(iter+=1) % chars.length] sleep delay print "\b" end end yield.tap { # After yielding to the block, save the return value iter = false # Tell the thread to exit, cleaning up after itself… spinner.join # …and wait for it to do so. } # Use the block's return value as the method's end def status(message) @status = message end def output(message = "") return if message.to_s.strip == "" display " " + message.split("\n").join("\n ") end def output_with_arrow(message = "") return if message.to_s.strip == "" display "====> " + message.split("\n").join("\n ") end def error_with_failure(message) message.gsub!(/^ +! */, '') display message.split("\n").map { |line| " ! #{line}" }.join("\n") exit 1 end def self.included_into @@included_into ||= [] end def self.extended_into @@extended_into ||= [] end def self.included(base) included_into << base end def self.extended(base) extended_into << base end @@kb = 1024 @@mb = 1024 * @@kb @@gb = 1024 * @@mb def format_bytes(amount) amount = amount.to_i return '(empty)' if amount == 0 return amount if amount < @@kb return "#{(amount / @@kb).round}k" if amount < @@mb return "#{(amount / @@mb).round}M" if amount < @@gb return "#{(amount / @@gb).round}G" end def quantify(string, num) "%d %s" % [ num, num.to_i == 1 ? string : "#{string}s" ] end def longest(items) items.map { |i| i.to_s.length }.sort.last end def self.enable_error_capture included_into.each do |base| base.send(:alias_method, :error_without_failure, :error) base.send(:alias_method, :error, :error_with_failure) end extended_into.each do |base| class << base alias_method :error_without_failure, :error alias_method :error, :error_with_failure end end end def self.disable_error_capture included_into.each do |base| base.send(:alias_method, :error, :error_without_failure) end extended_into.each do |base| class << base alias_method :error, :error_without_failure end end end def string_distance(first, last) distances = [] # 0x0s 0.upto(first.length) do |index| distances << [index] + [0] * last.length end distances[0] = 0.upto(last.length).to_a 1.upto(last.length) do |last_index| 1.upto(first.length) do |first_index| first_char = first[first_index - 1, 1] last_char = last[last_index - 1, 1] if first_char == last_char distances[first_index][last_index] = distances[first_index - 1][last_index - 1] # noop else distances[first_index][last_index] = [ distances[first_index - 1][last_index], # deletion distances[first_index][last_index - 1], # insertion distances[first_index - 1][last_index - 1] # substitution ].min + 1 # cost if first_index > 1 && last_index > 1 first_previous_char = first[first_index - 2, 1] last_previous_char = last[last_index - 2, 1] if first_char == last_previous_char && first_previous_char == last_char distances[first_index][last_index] = [ distances[first_index][last_index], distances[first_index - 2][last_index - 2] + 1 # transposition ].min end end end end end distances[first.length][last.length] end def suggestion(actual, possibilities) distances = Hash.new {|hash,key| hash[key] = []} possibilities.each do |suggestion| distances[string_distance(actual, suggestion)] << suggestion end minimum_distance = distances.keys.min if minimum_distance < 4 suggestions = distances[minimum_distance].sort if suggestions.length == 1 "Perhaps you meant `#{suggestions.first}`." else "Perhaps you meant #{suggestions[0...-1].map {|suggestion| "`#{suggestion}`"}.join(', ')} or `#{suggestions.last}`." end else nil end end end end