module Keyrack module UI class Console attr_accessor :mode def initialize @highline = HighLine.new @mode = :copy end def get_password @highline.ask("Keyrack password: ") { |q| q.echo = false }.to_s end def menu(options) current_group = options[:group] dirty = options[:dirty] at_top = options[:at_top] open = options[:open] choices = {'n' => :new, 'q' => :quit, 'm' => :mode} entry_choices = print_entries({ :group => current_group, :title => at_top ? "Keyrack Main Menu" : current_group.name, :open => open }) choices.update(entry_choices) @highline.say("Mode: #{@mode}") commands = "Commands:" if at_top if open choices['c'] = :collapse commands << " [c]ollapse" else choices['o'] = :open commands << " [o]pen" end end commands << " [n]ew" if !current_group.sites.empty? choices['e'] = :edit commands << " [e]dit" end choices['g'] = :new_group commands << " [g]roup" if options[:enable_up] choices['u'] = :up commands << " [u]p" end if !at_top choices['t'] = :top commands << " [t]op" end if dirty choices['s'] = :save commands << " [s]ave" end commands << " [m]ode [q]uit" @highline.say(commands) answer = @highline.ask("? ") { |q| q.in = choices.keys }.to_s result = choices[answer] case result when Symbol if result == :quit && dirty && !@highline.agree("Really quit? You have unsaved changes! [yn] ") nil elsif result == :mode @mode = @mode == :copy ? :print : :copy nil else result end when Hash if result.has_key?(:group) {:group => current_group.group(result[:group])} else password = result[:site].password if @mode == :copy Clipboard.copy(password) @highline.say("The password has been copied to your clipboard.") elsif @mode == :print password = @highline.color(password, :cyan) @highline.ask("Here you go: #{password}. Done? ") do |question| question.echo = false if HighLine::SystemExtensions::CHARACTER_MODE != 'stty' question.character = true question.overwrite = true end end end nil end end end def get_new_group(options = {}) @highline.ask("Group: ") { |q| q.validate = /^\w[\w\s]*$/ }.to_s end def get_new_entry result = {} result[:site] = @highline.ask("Label: ").to_s result[:username] = @highline.ask("Username: ").to_s result[:password] = get_new_password result[:password].nil? ? nil : result end def display_first_time_notice @highline.say("This looks like your first time using Keyrack. I'll need to ask you a few questions first.") end def password_setup password = confirmation = nil loop do password = @highline.ask("New passphrase: ") { |q| q.echo = false }.to_s confirmation = @highline.ask("Confirm passphrase: ") { |q| q.echo = false }.to_s break if password == confirmation @highline.say("Passphrases didn't match.") end password end def store_setup result = {} result['type'] = @highline.choose do |menu| menu.header = "Choose storage type" menu.choices("filesystem", "ssh") end case result['type'] when 'filesystem' result['path'] = 'database' when 'ssh' result['host'] = @highline.ask("Host: ").to_s result['user'] = @highline.ask("User: ").to_s result['path'] = @highline.ask("Remote path: ").to_s end result end def choose_entry_to_edit(group) choices = {'c' => :cancel} entry_choices = print_entries({ :group => group, :title => "Choose entry" }) choices.update(entry_choices) @highline.say("c. Cancel") answer = @highline.ask("? ") { |q| q.in = choices.keys }.to_s result = choices[answer] if result == :cancel nil else result end end def edit_entry(site) colored_entry = @highline.color("#{site.name} [#{site.username}]", :cyan) @highline.say("Editing entry: #{colored_entry}") @highline.say("u. Change username") @highline.say("p. Change password") @highline.say("d. Delete") @highline.say("c. Cancel") case @highline.ask("? ") { |q| q.in = %w{u p d c} }.to_s when "u" :change_username when "p" :change_password when "d" :delete when "c" nil end end def change_username(old_username) colored_old_username = @highline.color(old_username, :cyan) @highline.say("Current username: #{colored_old_username}") @highline.ask("New username (blank to cancel): ") { |q| q.validate = /\S/ }.to_s end def confirm_overwrite_entry(site) entry_name = @highline.color("#{site.name} [#{site.username}]", :cyan) @highline.agree("There's already an entry for: #{entry_name}. Do you want to overwrite it? [yn] ") end def confirm_delete_entry(site) entry_name = @highline.color("#{site.name} [#{site.username}]", :red) @highline.agree("You're about to delete #{entry_name}. Are you sure? [yn] ") end def display_invalid_password_notice @highline.say("Invalid password.") end def get_new_password result = nil case @highline.ask("Generate password? [ync] ") { |q| q.in = %w{y n c} }.to_s when "y" result = get_generated_password if result.nil? result = get_manual_password end when "n" result = get_manual_password end result end def get_generated_password password = nil loop do password = Utils.generate_password colored_password = @highline.color(password, :cyan) case @highline.ask("Generated #{colored_password}. Sound good? [ync] ") { |q| q.in = %w{y n c} }.to_s when "y" break when "c" password = nil break end end password end def get_manual_password password = nil loop do password = @highline.ask("Password: ") { |q| q.echo = false }.to_s confirmation = @highline.ask("Password (again): ") { |q| q.echo = false }.to_s if password == confirmation break end @highline.say("Passwords didn't match. Try again!") end password end private def print_entries(options) group = options[:group] title = options[:title] open = options[:open] selections = [] max_width = 0 choices = {} selection_index = 1 if open queue = [group] sites = [] until queue.empty? group = queue.shift group.group_names.each do |group_name| queue.push(group.group(group_name)) end group.sites.each do |site| sites.push([group.name, site]) end end sites.sort! do |a, b| if a[1].name == b[1].name a[1].username <=> b[1].username else a[1].name <=> b[1].name end end sites.each do |(group, site)| choices[selection_index.to_s] = {:site => site} text = "%s [%s] (%s)" % [site.name, site.username, group] width = text.length selections.push({:width => width, :text => text}) max_width = width if width > max_width selection_index += 1 end else group.group_names.each do |group_name| choices[selection_index.to_s] = {:group => group_name} text = @highline.color(group_name, :green) width = group_name.length selections.push({:width => width, :text => text}) max_width = width if width > max_width selection_index += 1 end group.sites.each do |site| choices[selection_index.to_s] = {:site => site} text = "%s [%s]" % [site.name, site.username] width = text.length selections.push({:width => width, :text => text}) max_width = width if width > max_width selection_index += 1 end end title = { :text => @highline.color(title, :yellow), :width => title.length } columnize_menu(selections, max_width, title) choices end def columnize_menu(selections, max_width, title = nil) terminal_size = HighLine::SystemExtensions.terminal_size if selections.empty? if title @highline.say("=== #{title[:text]} ===") end return end # add in width for numbers number_width = Math.log10(selections.count).floor + 1 max_width += number_width + 2 multiples = max_width == 0 ? 1 : terminal_size[0] / max_width num_columns = if multiples > 1 if (terminal_size[0] - (multiples * max_width)) < (multiples - 1) # If there aren't sufficient spaces, decrease column count multiples - 1 else multiples end else 1 end #puts "Terminal width: %d; Max width: %d; Multiples: %d; Columns: %d" % #[ terminal_size[0], max_width, multiples, num_columns ] total_width = num_columns * max_width + (num_columns - 1) if title padding_total = total_width - title[:width] - 2 padding_left = [padding_total / 2, 3].max padding_right = [padding_total - padding_left, 3].max @highline.say(("=" * padding_left) + " #{title[:text]} " + ("=" * padding_right)) end selection_index = 0 catch(:stop) do loop do num_columns.downto(1) do |i| selection = selections[selection_index] throw(:stop) if selection.nil? label = "%#{number_width}d. " % (selection_index + 1) if i == 1 || selection_index == (selections.count - 1) @highline.say(label + selection[:text]) else spaces = max_width - (selection[:width] + number_width + 2) + 1 @highline.say(label + selection[:text] + (" " * spaces)) end selection_index += 1 end end end end end end end