ecl 1.2.1 → 3.0.0.pre.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.gitignore +0 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +48 -0
- data/README.md +112 -89
- data/Rakefile +1 -0
- data/eclair.gemspec +7 -4
- data/exe/ecl +62 -4
- data/lib/eclair.rb +21 -18
- data/lib/eclair/color.rb +3 -0
- data/lib/eclair/config.rb +68 -21
- data/lib/eclair/grid.rb +242 -274
- data/lib/eclair/group_item.rb +33 -0
- data/lib/eclair/item.rb +42 -0
- data/lib/eclair/less_viewer.rb +2 -1
- data/lib/eclair/provider.rb +15 -0
- data/lib/eclair/providers/ec2.rb +2 -0
- data/lib/eclair/providers/ec2/ec2_group_item.rb +16 -0
- data/lib/eclair/providers/ec2/ec2_item.rb +136 -0
- data/lib/eclair/providers/ec2/ec2_provider.rb +118 -0
- data/lib/eclair/providers/gce.rb +2 -0
- data/lib/eclair/providers/gce/gce_group_item.rb +15 -0
- data/lib/eclair/providers/gce/gce_item.rb +117 -0
- data/lib/eclair/providers/gce/gce_provider.rb +34 -0
- data/lib/eclair/providers/k8s.rb +2 -0
- data/lib/eclair/providers/k8s/k8s_group_item.rb +15 -0
- data/lib/eclair/providers/k8s/k8s_item.rb +92 -0
- data/lib/eclair/providers/k8s/k8s_provider.rb +34 -0
- data/lib/eclair/version.rb +2 -1
- data/out.gif +0 -0
- data/templates/eclrc.template +81 -35
- metadata +60 -27
- data/bin/console +0 -10
- data/lib/eclair/cell.rb +0 -101
- data/lib/eclair/column.rb +0 -52
- data/lib/eclair/console.rb +0 -56
- data/lib/eclair/group.rb +0 -55
- data/lib/eclair/helpers/aws_helper.rb +0 -157
- data/lib/eclair/helpers/benchmark_helper.rb +0 -19
- data/lib/eclair/helpers/common_helper.rb +0 -24
- data/lib/eclair/instance.rb +0 -165
- data/lib/eclair/matcher.rb +0 -19
    
        data/lib/eclair/config.rb
    CHANGED
    
    | @@ -1,9 +1,20 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require 'curses'
         | 
| 3 | 
            +
             | 
| 1 4 | 
             
            module Eclair
         | 
| 5 | 
            +
              module ConfigHelper
         | 
| 6 | 
            +
                def config
         | 
| 7 | 
            +
                  Eclair.config
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
              end
         | 
| 10 | 
            +
             | 
| 2 11 | 
             
              class Config
         | 
| 3 | 
            -
                 | 
| 4 | 
            -
                 | 
| 12 | 
            +
                KEYS_DIR = "#{ENV['HOME']}/.ecl/keys"
         | 
| 13 | 
            +
                CACHE_DIR = "#{ENV['HOME']}/.ecl/.cache"
         | 
| 5 14 |  | 
| 6 15 | 
             
                def initialize
         | 
| 16 | 
            +
                  @done = false
         | 
| 17 | 
            +
                  @config_file = ENV["ECLRC"] || "#{ENV['HOME']}/.ecl/config.rb"
         | 
| 7 18 | 
             
                  @aws_region = nil
         | 
| 8 19 | 
             
                  @columns = 4
         | 
| 9 20 | 
             
                  @group_by = lambda do |instance|
         | 
| @@ -21,17 +32,14 @@ module Eclair | |
| 21 32 | 
             
                      "ec2-user"
         | 
| 22 33 | 
             
                    end
         | 
| 23 34 | 
             
                  end
         | 
| 35 | 
            +
                  @ssh_command          = "ssh"
         | 
| 24 36 | 
             
                  @ssh_keys             = {}
         | 
| 25 | 
            -
                  @ssh_hostname         = :public_ip_address
         | 
| 26 37 | 
             
                  @ssh_ports            = [22].freeze
         | 
| 27 38 | 
             
                  @ssh_options          = "-o ConnectTimeout=1 -o StrictHostKeyChecking=no".freeze
         | 
| 28 | 
            -
                  @ | 
| 29 | 
            -
                  @ | 
| 30 | 
            -
                  @ | 
| 31 | 
            -
                  @ | 
| 32 | 
            -
                  @disabled_color       = [COLOR_BLACK, -1, A_BOLD].freeze
         | 
| 33 | 
            -
                  @search_color         = [COLOR_BLACK, COLOR_YELLOW].freeze
         | 
| 34 | 
            -
                  @help_color           = [COLOR_BLACK, COLOR_WHITE].freeze
         | 
| 39 | 
            +
                  @dir_keys             = {}
         | 
| 40 | 
            +
                  @exec_format          = "{ssh_command} {ssh_options} -p{port} {ssh_key} {username}@{host}"
         | 
| 41 | 
            +
                  @provider             = :ec2
         | 
| 42 | 
            +
                  @get_pods_option      = ""
         | 
| 35 43 |  | 
| 36 44 | 
             
                  instance_variables.each do |var|
         | 
| 37 45 | 
             
                    Config.class_eval do
         | 
| @@ -39,23 +47,55 @@ module Eclair | |
| 39 47 | 
             
                    end
         | 
| 40 48 | 
             
                  end
         | 
| 41 49 |  | 
| 42 | 
            -
                   | 
| 50 | 
            +
                  # Migrate old ~/.eclrc to ~/.ecl/config.rb
         | 
| 51 | 
            +
                  old_conf  = "#{ENV['HOME']}/.eclrc"
         | 
| 52 | 
            +
                  new_dir = "#{ENV['HOME']}/.ecl"
         | 
| 53 | 
            +
                  new_conf  = "#{ENV['HOME']}/.ecl/config.rb"
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  if !File.exists?(new_conf) && File.exists?(old_conf)
         | 
| 56 | 
            +
                    FileUtils.mkdir_p new_dir
         | 
| 57 | 
            +
                    FileUtils.mv old_conf, new_conf
         | 
| 58 | 
            +
                    puts "#{old_conf} migrated to #{new_conf}"
         | 
| 59 | 
            +
                    puts "Please re-run eclair"
         | 
| 60 | 
            +
                    exit
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  unless File.exists? @config_file
         | 
| 43 64 | 
             
                    template_path = File.join(File.dirname(__FILE__), "..", "..", "templates", "eclrc.template")
         | 
| 44 | 
            -
                    FileUtils. | 
| 45 | 
            -
                     | 
| 65 | 
            +
                    FileUtils.mkdir_p(File.dirname(@config_file))
         | 
| 66 | 
            +
                    FileUtils.cp(template_path, @config_file)
         | 
| 67 | 
            +
                    puts "#{@config_file} successfully created. Edit it and run again!"
         | 
| 46 68 | 
             
                    exit
         | 
| 47 69 | 
             
                  end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  key_path = "#{new_dir}/keys"
         | 
| 72 | 
            +
                  FileUtils.mkdir_p key_path unless Dir.exists? key_path
         | 
| 73 | 
            +
                  # FileUtils.mkdir_p CACHE_DIR unless Dir.exists? CACHE_DIR
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def after_load
         | 
| 77 | 
            +
                  dir_keys = {}
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  Dir["#{KEYS_DIR}/*"].each do |key|
         | 
| 80 | 
            +
                    if File.file? key
         | 
| 81 | 
            +
                      dir_keys[File.basename(key, ".*")] = key
         | 
| 82 | 
            +
                    end
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
                  @ssh_keys.merge!(dir_keys)
         | 
| 48 85 | 
             
                end
         | 
| 49 86 | 
             
              end
         | 
| 50 87 |  | 
| 51 88 | 
             
              extend self
         | 
| 52 | 
            -
              
         | 
| 53 | 
            -
              def config
         | 
| 54 | 
            -
                unless @config
         | 
| 55 | 
            -
                  @config = Config.new
         | 
| 56 | 
            -
                  load Config::RCFILE
         | 
| 57 | 
            -
                end
         | 
| 58 89 |  | 
| 90 | 
            +
              def init_config
         | 
| 91 | 
            +
                @config = Config.new
         | 
| 92 | 
            +
                load @config.config_file
         | 
| 93 | 
            +
                raise unless @done
         | 
| 94 | 
            +
                @config.after_load
         | 
| 95 | 
            +
              end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
             | 
| 98 | 
            +
              def config
         | 
| 59 99 | 
             
                if @config.aws_region
         | 
| 60 100 | 
             
                  ::Aws.config.update(region: @config.aws_region)
         | 
| 61 101 | 
             
                end
         | 
| @@ -63,7 +103,14 @@ module Eclair | |
| 63 103 | 
             
                @config
         | 
| 64 104 | 
             
              end
         | 
| 65 105 |  | 
| 66 | 
            -
              def  | 
| 67 | 
            -
                 | 
| 106 | 
            +
              def profile
         | 
| 107 | 
            +
                ENV["ECL_PROFILE"] || "default"
         | 
| 108 | 
            +
              end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
              def configure name = "default"
         | 
| 111 | 
            +
                if profile == name
         | 
| 112 | 
            +
                  @done = true
         | 
| 113 | 
            +
                  yield config
         | 
| 114 | 
            +
                end
         | 
| 68 115 | 
             
              end
         | 
| 69 116 | 
             
            end
         | 
    
        data/lib/eclair/grid.rb
    CHANGED
    
    | @@ -1,355 +1,323 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "eclair/group_item"
         | 
| 3 | 
            +
            require "eclair/color"
         | 
| 4 | 
            +
             | 
| 1 5 | 
             
            module Eclair
         | 
| 2 | 
            -
               | 
| 3 | 
            -
                 | 
| 4 | 
            -
             | 
| 6 | 
            +
              class Grid
         | 
| 7 | 
            +
                attr_reader :mode
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def initialize keyword = ""
         | 
| 10 | 
            +
                  case config.provider
         | 
| 11 | 
            +
                  when :ec2
         | 
| 12 | 
            +
                    require "eclair/providers/ec2"
         | 
| 13 | 
            +
                    @provider = EC2Provider
         | 
| 14 | 
            +
                  when :k8s
         | 
| 15 | 
            +
                    require "eclair/providers/k8s"
         | 
| 16 | 
            +
                    @provider = K8sProvider
         | 
| 17 | 
            +
                  when :gce
         | 
| 18 | 
            +
                    require "eclair/providers/gce"
         | 
| 19 | 
            +
                    @provider = GCEProvider
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                  @item_class = @provider.item_class
         | 
| 5 22 |  | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
                   | 
| 9 | 
            -
             | 
| 23 | 
            +
                  @scroll = config.columns.times.map{0}
         | 
| 24 | 
            +
                  @header_rows = 4
         | 
| 25 | 
            +
                  @cursor = [0,0]
         | 
| 26 | 
            +
                  @cell_width = Curses.stdscr.maxx/config.columns
         | 
| 27 | 
            +
                  @maxy = Curses.stdscr.maxy - @header_rows
         | 
| 28 | 
            +
                  @mode = :assign
         | 
| 29 | 
            +
                  @search_buffer = ""
         | 
| 10 30 |  | 
| 11 | 
            -
             | 
| 12 | 
            -
                   | 
| 31 | 
            +
                  @provider.prepare keyword
         | 
| 32 | 
            +
                  assign
         | 
| 33 | 
            +
                  at(*@cursor).select(true)
         | 
| 34 | 
            +
                  draw_all
         | 
| 35 | 
            +
                  transit_mode(:nav)
         | 
| 13 36 | 
             
                end
         | 
| 14 | 
            -
                
         | 
| 15 | 
            -
                def render_header
         | 
| 16 | 
            -
                  if mode == :search
         | 
| 17 | 
            -
                    if cursor
         | 
| 18 | 
            -
                      header = ["Searching #{@search_str}", "Found #{cursor.name}", ""]
         | 
| 19 | 
            -
                    else
         | 
| 20 | 
            -
                      header = ["Searching #{@search_str}", "None Found", ""]
         | 
| 21 | 
            -
                    end
         | 
| 22 | 
            -
                  else
         | 
| 23 | 
            -
                    header = cursor.header
         | 
| 24 | 
            -
                  end
         | 
| 25 37 |  | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 38 | 
            +
                def move key
         | 
| 39 | 
            +
                  return unless at(*@cursor)
         | 
| 40 | 
            +
                  x,y = @cursor
         | 
| 41 | 
            +
                  mx,my = {
         | 
| 42 | 
            +
                    Curses::KEY_UP => [0,-1],
         | 
| 43 | 
            +
                    ?k => [0,-1],
         | 
| 44 | 
            +
                    Curses::KEY_DOWN => [0,1],
         | 
| 45 | 
            +
                    ?j => [0,1],
         | 
| 46 | 
            +
                    Curses::KEY_LEFT => [-1,0],
         | 
| 47 | 
            +
                    ?h => [-1,0],
         | 
| 48 | 
            +
                    Curses::KEY_RIGHT => [1,0],
         | 
| 49 | 
            +
                    ?l => [1,0],
         | 
| 50 | 
            +
                  }[key]
         | 
| 33 51 |  | 
| 34 | 
            -
             | 
| 35 | 
            -
                   | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
                   | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
                     | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
                  attron(Color.fetch(*config.help_color)) do
         | 
| 47 | 
            -
                    addstr helps.map{ |key, action|   
         | 
| 48 | 
            -
                      " #{key} => #{action}"
         | 
| 49 | 
            -
                    }.join("    ").slice(0,stdscr.maxx).ljust(stdscr.maxx)
         | 
| 52 | 
            +
                  newx = x
         | 
| 53 | 
            +
                  loop do
         | 
| 54 | 
            +
                    newx = (newx + mx) % @grid.length
         | 
| 55 | 
            +
                    break if @grid[newx].length > 0
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                  newy = (y + my - @scroll[x] + @scroll[newx])
         | 
| 58 | 
            +
                  if my != 0
         | 
| 59 | 
            +
                    newy %= @grid[newx].length
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                  if newy >= @grid[newx].length
         | 
| 62 | 
            +
                    newy = @grid[newx].length-1
         | 
| 50 63 | 
             
                  end
         | 
| 51 | 
            -
                end
         | 
| 52 64 |  | 
| 53 | 
            -
             | 
| 54 | 
            -
                  stdscr.maxx/column_count
         | 
| 65 | 
            +
                  move_cursor(newx, newy)
         | 
| 55 66 | 
             
                end
         | 
| 56 67 |  | 
| 57 | 
            -
                def  | 
| 58 | 
            -
                   | 
| 59 | 
            -
             | 
| 60 | 
            -
                  render_all
         | 
| 61 | 
            -
                end
         | 
| 62 | 
            -
             | 
| 63 | 
            -
                def assign
         | 
| 64 | 
            -
                  sort_function = lambda {|i| [i.name.downcase, -i.launch_time.to_i]}
         | 
| 65 | 
            -
                  @group_map ||= {}
         | 
| 66 | 
            -
                  if config.group_by
         | 
| 67 | 
            -
                    Aws.instances.group_by(&config.group_by).each do |group, instances|
         | 
| 68 | 
            -
                      if @group_map[group]
         | 
| 69 | 
            -
                        group_cell  = @group_map[group]
         | 
| 70 | 
            -
                      else
         | 
| 71 | 
            -
                        col = columns[target_col]
         | 
| 72 | 
            -
                        group_cell = Group.new(group, col)
         | 
| 73 | 
            -
                        col << group_cell
         | 
| 74 | 
            -
                        @group_map[group] = group_cell
         | 
| 75 | 
            -
                      end
         | 
| 76 | 
            -
                      instances.each do |i|
         | 
| 77 | 
            -
                        unless group_cell.find{|j| j.instance_id == i.instance_id}
         | 
| 78 | 
            -
                          obj = Instance.new(i.instance_id, col)
         | 
| 79 | 
            -
                          group_cell << obj
         | 
| 80 | 
            -
                        end
         | 
| 81 | 
            -
                      end
         | 
| 82 | 
            -
                      group_cell.items.sort_by!(&sort_function)
         | 
| 83 | 
            -
                    end
         | 
| 84 | 
            -
                  else
         | 
| 85 | 
            -
                    col_limit = (Aws.instances.count - 1) / config.columns + 1
         | 
| 86 | 
            -
                    iter = Aws.instances.map{|i| Instance.new(i.instance_id)}.sort_by(&sort_function).each
         | 
| 87 | 
            -
                    columns.each do |col|
         | 
| 88 | 
            -
                      col_limit.times do 
         | 
| 89 | 
            -
                        begin
         | 
| 90 | 
            -
                          i = iter.next
         | 
| 91 | 
            -
                          i.column = col
         | 
| 92 | 
            -
                          col << i
         | 
| 93 | 
            -
                        rescue StopIteration
         | 
| 94 | 
            -
                          break
         | 
| 95 | 
            -
                        end
         | 
| 96 | 
            -
                      end
         | 
| 97 | 
            -
                    end
         | 
| 68 | 
            +
                def space
         | 
| 69 | 
            +
                  if @mode == :nav
         | 
| 70 | 
            +
                    transit_mode(:sel)
         | 
| 98 71 | 
             
                  end
         | 
| 99 | 
            -
                end
         | 
| 100 72 |  | 
| 101 | 
            -
             | 
| 102 | 
            -
             | 
| 103 | 
            -
                   | 
| 104 | 
            -
                     | 
| 105 | 
            -
                      c.render
         | 
| 106 | 
            -
                    end
         | 
| 73 | 
            +
                  at(*@cursor)&.toggle_select
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  if @mode == :sel && @provider.items.all?{|i| !i.selected}
         | 
| 76 | 
            +
                    transit_mode(:nav)
         | 
| 107 77 | 
             
                  end
         | 
| 108 | 
            -
             | 
| 109 | 
            -
             | 
| 78 | 
            +
             | 
| 79 | 
            +
             | 
| 80 | 
            +
                  draw(*@cursor)
         | 
| 110 81 | 
             
                end
         | 
| 111 82 |  | 
| 112 | 
            -
                def  | 
| 113 | 
            -
                  targets =  | 
| 83 | 
            +
                def action
         | 
| 84 | 
            +
                  targets = @provider.items.select{|i| i.selected && i.connectable?}
         | 
| 85 | 
            +
             | 
| 114 86 | 
             
                  return if targets.empty?
         | 
| 115 | 
            -
                  close_screen
         | 
| 87 | 
            +
                  Curses.close_screen
         | 
| 116 88 |  | 
| 117 | 
            -
                   | 
| 118 | 
            -
             | 
| 119 | 
            -
                    target = targets.first
         | 
| 120 | 
            -
                    cmd = target.ssh_cmd
         | 
| 89 | 
            +
                  if targets.length == 1
         | 
| 90 | 
            +
                    cmd = targets.first.command
         | 
| 121 91 | 
             
                  else
         | 
| 122 92 | 
             
                    cmds = []
         | 
| 123 | 
            -
                     | 
| 124 | 
            -
                    session_cmd = nil
         | 
| 93 | 
            +
                    target_cmd = ""
         | 
| 125 94 |  | 
| 126 95 | 
             
                    targets.each_with_index do |target, i|
         | 
| 127 | 
            -
                      if i==0
         | 
| 128 | 
            -
                        if ENV['TMUX']
         | 
| 129 | 
            -
                           | 
| 130 | 
            -
             | 
| 96 | 
            +
                      if i == 0
         | 
| 97 | 
            +
                        if ENV['TMUX'] # Eclair called inside of tmux
         | 
| 98 | 
            +
                          # Create new session and save window id
         | 
| 99 | 
            +
                          window_name = `tmux new-window -P -- '#{target.command}'`.strip
         | 
| 100 | 
            +
                          target_cmd = "-t #{window_name}"
         | 
| 101 | 
            +
                        else # Eclair called from outside of tmux
         | 
| 102 | 
            +
                          # Create new session and save session
         | 
| 131 103 | 
             
                          session_name = "eclair#{Time.now.to_i}"
         | 
| 132 | 
            -
                           | 
| 133 | 
            -
                           | 
| 104 | 
            +
                          target_cmd = "-t #{session_name}"
         | 
| 105 | 
            +
                          `tmux new-session -d -s #{session_name} -- '#{target.command}'`
         | 
| 134 106 | 
             
                        end
         | 
| 135 | 
            -
                      else
         | 
| 136 | 
            -
                        cmds << " | 
| 137 | 
            -
                        cmds << " | 
| 107 | 
            +
                      else # Split layout and
         | 
| 108 | 
            +
                        cmds << "split-window #{target_cmd} -- '#{target.command}'"
         | 
| 109 | 
            +
                        cmds << "select-layout #{target_cmd} tiled"
         | 
| 138 110 | 
             
                      end
         | 
| 139 111 | 
             
                    end
         | 
| 140 | 
            -
                    cmds << " | 
| 141 | 
            -
                    cmds << " | 
| 142 | 
            -
                    cmd = cmds.join("  | 
| 112 | 
            +
                    cmds << "set-window-option #{target_cmd} synchronize-panes on"
         | 
| 113 | 
            +
                    cmds << "attach #{target_cmd}" unless ENV['TMUX']
         | 
| 114 | 
            +
                    cmd = "tmux #{cmds.join(" \\; ")}"
         | 
| 143 115 | 
             
                  end
         | 
| 144 | 
            -
                  system | 
| 145 | 
            -
                  exit
         | 
| 116 | 
            +
                  system(cmd)
         | 
| 117 | 
            +
                  exit()
         | 
| 118 | 
            +
                  resize
         | 
| 146 119 | 
             
                end
         | 
| 147 120 |  | 
| 148 | 
            -
                def  | 
| 149 | 
            -
                   | 
| 150 | 
            -
             | 
| 151 | 
            -
             | 
| 152 | 
            -
             | 
| 153 | 
            -
                   | 
| 154 | 
            -
             | 
| 155 | 
            -
             | 
| 156 | 
            -
                def rows
         | 
| 157 | 
            -
                  columns.map(&:count).max
         | 
| 158 | 
            -
                end
         | 
| 159 | 
            -
             | 
| 160 | 
            -
                def target_col
         | 
| 161 | 
            -
                  counts = columns.map(&:count)
         | 
| 162 | 
            -
                  counts.index(counts.min)
         | 
| 121 | 
            +
                def resize
         | 
| 122 | 
            +
                  Curses.clear
         | 
| 123 | 
            +
                  @scroll.fill(0)
         | 
| 124 | 
            +
                  @cell_width = Curses.stdscr.maxx/config.columns
         | 
| 125 | 
            +
                  @maxy = Curses.stdscr.maxy - @header_rows
         | 
| 126 | 
            +
                  rescroll(*@cursor)
         | 
| 127 | 
            +
                  draw_all
         | 
| 163 128 | 
             
                end
         | 
| 164 129 |  | 
| 165 | 
            -
                def  | 
| 166 | 
            -
                   | 
| 167 | 
            -
                end
         | 
| 130 | 
            +
                def transit_mode to
         | 
| 131 | 
            +
                  return if to == @mode
         | 
| 168 132 |  | 
| 169 | 
            -
             | 
| 170 | 
            -
                   | 
| 171 | 
            -
             | 
| 172 | 
            -
             | 
| 173 | 
            -
             | 
| 174 | 
            -
                   | 
| 175 | 
            -
                  if mode == :navi
         | 
| 176 | 
            -
                    @mode = :select
         | 
| 177 | 
            -
                    cursor.toggle_select
         | 
| 178 | 
            -
                  end
         | 
| 179 | 
            -
                  cursor.toggle_select
         | 
| 180 | 
            -
                  if selected.empty?
         | 
| 181 | 
            -
                    @mode = :navi
         | 
| 182 | 
            -
                    cursor.toggle_select
         | 
| 133 | 
            +
                  case @mode
         | 
| 134 | 
            +
                  when :nav
         | 
| 135 | 
            +
                    at(*@cursor)&.select(false)
         | 
| 136 | 
            +
                  when :sel
         | 
| 137 | 
            +
                  when :search
         | 
| 138 | 
            +
                  when :assign
         | 
| 183 139 | 
             
                  end
         | 
| 184 | 
            -
                end
         | 
| 185 140 |  | 
| 186 | 
            -
             | 
| 187 | 
            -
                  end_search if mode == :search
         | 
| 188 | 
            -
                  mx,my = {
         | 
| 189 | 
            -
                    KEY_UP => [0,-1],
         | 
| 190 | 
            -
                    "k" => [0,-1],
         | 
| 191 | 
            -
                    KEY_DOWN => [0,1],
         | 
| 192 | 
            -
                    "j" => [0,1],
         | 
| 193 | 
            -
                    KEY_LEFT => [-1,0],
         | 
| 194 | 
            -
                    "h" => [-1,0],
         | 
| 195 | 
            -
                    KEY_RIGHT => [1,0],
         | 
| 196 | 
            -
                    "l" => [1,0],
         | 
| 197 | 
            -
                  }[key]
         | 
| 141 | 
            +
                  @mode = to
         | 
| 198 142 |  | 
| 199 | 
            -
                   | 
| 200 | 
            -
                   | 
| 201 | 
            -
             | 
| 202 | 
            -
             | 
| 203 | 
            -
                   | 
| 204 | 
            -
                   | 
| 205 | 
            -
                     | 
| 143 | 
            +
                  case @mode
         | 
| 144 | 
            +
                  when :nav
         | 
| 145 | 
            +
                    at(*@cursor)&.select(true)
         | 
| 146 | 
            +
                  when :sel
         | 
| 147 | 
            +
                  when :search
         | 
| 148 | 
            +
                  when :assign
         | 
| 149 | 
            +
                    move_cursor(0,0)
         | 
| 206 150 | 
             
                  end
         | 
| 207 151 |  | 
| 208 | 
            -
                   | 
| 152 | 
            +
                  draw_all
         | 
| 209 153 | 
             
                end
         | 
| 210 154 |  | 
| 211 | 
            -
                def  | 
| 212 | 
            -
                   | 
| 213 | 
            -
             | 
| 214 | 
            -
             | 
| 215 | 
            -
             | 
| 155 | 
            +
                def start_search
         | 
| 156 | 
            +
                  transit_mode(:search)
         | 
| 157 | 
            +
                end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                def end_search
         | 
| 160 | 
            +
                  if @provider.items.any?{|i| i.selected}
         | 
| 161 | 
            +
                    transit_mode(:sel)
         | 
| 216 162 | 
             
                  else
         | 
| 217 | 
            -
                     | 
| 163 | 
            +
                    transit_mode(:nav)
         | 
| 218 164 | 
             
                  end
         | 
| 219 165 | 
             
                end
         | 
| 220 166 |  | 
| 221 | 
            -
                def  | 
| 222 | 
            -
                   | 
| 223 | 
            -
                   | 
| 224 | 
            -
                  return nil if result.score(@search_str) == 0.0
         | 
| 225 | 
            -
                  result
         | 
| 167 | 
            +
                def clear_search
         | 
| 168 | 
            +
                  @search_buffer = ""
         | 
| 169 | 
            +
                  update_search
         | 
| 226 170 | 
             
                end
         | 
| 227 171 |  | 
| 228 | 
            -
                def  | 
| 229 | 
            -
                   | 
| 230 | 
            -
                  @rollback_mode = @mode
         | 
| 231 | 
            -
                  @search_str = ""
         | 
| 232 | 
            -
                end
         | 
| 172 | 
            +
                def append_search key
         | 
| 173 | 
            +
                  return unless key
         | 
| 233 174 |  | 
| 234 | 
            -
             | 
| 235 | 
            -
             | 
| 236 | 
            -
             | 
| 237 | 
            -
                     | 
| 175 | 
            +
                  if @search_buffer.length > 0 && key == 127 # backspace
         | 
| 176 | 
            +
                    @search_buffer = @search_buffer.chop
         | 
| 177 | 
            +
                  elsif key.to_s.length == 1
         | 
| 178 | 
            +
                    begin
         | 
| 179 | 
            +
                      @search_buffer = @search_buffer + key.to_s
         | 
| 180 | 
            +
                    rescue
         | 
| 181 | 
            +
                      return
         | 
| 182 | 
            +
                    end
         | 
| 238 183 | 
             
                  else
         | 
| 239 | 
            -
                     | 
| 184 | 
            +
                    return
         | 
| 240 185 | 
             
                  end
         | 
| 241 | 
            -
                end
         | 
| 242 186 |  | 
| 243 | 
            -
             | 
| 244 | 
            -
                  x,y = @rollback_cursor
         | 
| 245 | 
            -
                  move_cursor(x: x, y: y, mode: @rollback_mode)
         | 
| 187 | 
            +
                  update_search
         | 
| 246 188 | 
             
                end
         | 
| 247 189 |  | 
| 190 | 
            +
                private
         | 
| 248 191 |  | 
| 249 | 
            -
                def move_cursor  | 
| 250 | 
            -
                   | 
| 251 | 
            -
             | 
| 252 | 
            -
                    cursor.decurrent
         | 
| 253 | 
            -
                  end
         | 
| 192 | 
            +
                def move_cursor x, y
         | 
| 193 | 
            +
                  prev = @cursor.dup
         | 
| 194 | 
            +
                  @cursor = [x, y]
         | 
| 254 195 |  | 
| 255 | 
            -
                   | 
| 256 | 
            -
                   | 
| 257 | 
            -
             | 
| 258 | 
            -
             | 
| 259 | 
            -
             | 
| 260 | 
            -
                     | 
| 261 | 
            -
                    @mode = new_mode
         | 
| 196 | 
            +
                  prev_item = at(*prev)
         | 
| 197 | 
            +
                  curr_item = at(*@cursor)
         | 
| 198 | 
            +
                  rescroll(*@cursor)
         | 
| 199 | 
            +
                  if @mode == :nav
         | 
| 200 | 
            +
                    prev_item.select(false)
         | 
| 201 | 
            +
                    curr_item.select(true)
         | 
| 262 202 | 
             
                  end
         | 
| 203 | 
            +
                  draw(*prev) if prev_item
         | 
| 204 | 
            +
                  draw(*@cursor) if curr_item
         | 
| 205 | 
            +
                  update_header(curr_item.header) if curr_item
         | 
| 206 | 
            +
                end
         | 
| 263 207 |  | 
| 264 | 
            -
                  if block
         | 
| 265 | 
            -
                    @x, @y = block.call
         | 
| 266 | 
            -
                  else
         | 
| 267 | 
            -
                    @x = options.delete(:x) || @x
         | 
| 268 | 
            -
                    @y = options.delete(:y) || @y
         | 
| 269 | 
            -
                  end
         | 
| 270 208 |  | 
| 271 | 
            -
             | 
| 272 | 
            -
             | 
| 273 | 
            -
             | 
| 209 | 
            +
                def update_header str, pos = 0
         | 
| 210 | 
            +
                  Curses.setpos(0, 0)
         | 
| 211 | 
            +
                  Curses.clrtoeol
         | 
| 212 | 
            +
                  Curses.addstr(@mode.to_s)
         | 
| 213 | 
            +
                  str.split("\n").map(&:strip).each_with_index do |line, i|
         | 
| 214 | 
            +
                    Curses.setpos(i + pos + 1, 0)
         | 
| 215 | 
            +
                    Curses.clrtoeol
         | 
| 216 | 
            +
                    Curses.addstr(line)
         | 
| 274 217 | 
             
                  end
         | 
| 275 218 | 
             
                end
         | 
| 276 219 |  | 
| 277 | 
            -
                def  | 
| 278 | 
            -
                   | 
| 279 | 
            -
             | 
| 280 | 
            -
                   | 
| 281 | 
            -
                    if key
         | 
| 282 | 
            -
                      @search_str = @search_str+key
         | 
| 283 | 
            -
                    else
         | 
| 284 | 
            -
                      @search_str.chop!
         | 
| 285 | 
            -
                    end
         | 
| 220 | 
            +
                def update_search
         | 
| 221 | 
            +
                  assign
         | 
| 222 | 
            +
                  Curses.clear
         | 
| 223 | 
            +
                  draw_all
         | 
| 286 224 |  | 
| 287 | 
            -
             | 
| 225 | 
            +
                  Curses.setpos(@header_rows - 1, 0)
         | 
| 226 | 
            +
                  Curses.clrtoeol
         | 
| 227 | 
            +
                  if @mode != :search && @search_buffer.empty?
         | 
| 228 | 
            +
                    update_header('/: Start search')
         | 
| 229 | 
            +
                  else
         | 
| 230 | 
            +
                    update_header("/#{@search_buffer}")
         | 
| 231 | 
            +
                  end
         | 
| 232 | 
            +
                end
         | 
| 288 233 |  | 
| 289 | 
            -
             | 
| 290 | 
            -
             | 
| 291 | 
            -
             | 
| 292 | 
            -
                       | 
| 293 | 
            -
             | 
| 294 | 
            -
             | 
| 295 | 
            -
             | 
| 234 | 
            +
                def rescroll x, y
         | 
| 235 | 
            +
                  unless (@scroll[x]...@maxy+@scroll[x]).include? y
         | 
| 236 | 
            +
                    if y < @scroll[x]
         | 
| 237 | 
            +
                      @scroll[x] = y
         | 
| 238 | 
            +
                    elsif y >= @maxy
         | 
| 239 | 
            +
                      @scroll[x] = y - @maxy + 1
         | 
| 240 | 
            +
                    end
         | 
| 241 | 
            +
                    (@scroll[x]...@maxy+@scroll[x]).each do |ty|
         | 
| 242 | 
            +
                      draw_item(x, ty)
         | 
| 296 243 | 
             
                    end
         | 
| 297 | 
            -
             | 
| 298 | 
            -
                    result || [-1,-1]
         | 
| 299 244 | 
             
                  end
         | 
| 300 | 
            -
                  
         | 
| 301 | 
            -
                  render_header 
         | 
| 302 245 | 
             
                end
         | 
| 303 246 |  | 
| 304 | 
            -
                def  | 
| 305 | 
            -
                   | 
| 306 | 
            -
                  render_all
         | 
| 307 | 
            -
                  cursor.check_scroll
         | 
| 247 | 
            +
                def at x, y
         | 
| 248 | 
            +
                  @grid[x][y]
         | 
| 308 249 | 
             
                end
         | 
| 309 250 |  | 
| 310 | 
            -
                def  | 
| 311 | 
            -
                   | 
| 312 | 
            -
                   | 
| 313 | 
            -
                   | 
| 251 | 
            +
                def make_label target
         | 
| 252 | 
            +
                  ind = (@mode != :nav && target.selected) ? "*" : " "
         | 
| 253 | 
            +
                  label = "#{target.label} #{ind}"
         | 
| 254 | 
            +
                  label.slice(0, @cell_width).ljust(@cell_width)
         | 
| 314 255 | 
             
                end
         | 
| 315 256 |  | 
| 316 | 
            -
                def  | 
| 317 | 
            -
                   | 
| 318 | 
            -
             | 
| 319 | 
            -
             | 
| 320 | 
            -
             | 
| 321 | 
            -
                   | 
| 257 | 
            +
                def draw_all
         | 
| 258 | 
            +
                  @grid.each_with_index do |column, x|
         | 
| 259 | 
            +
                    column.each_with_index do |_, y|
         | 
| 260 | 
            +
                      draw_item(x,y)
         | 
| 261 | 
            +
                    end
         | 
| 262 | 
            +
                  end
         | 
| 263 | 
            +
                  case @mode
         | 
| 264 | 
            +
                  when :nav, :sel
         | 
| 265 | 
            +
                    update_header(at(*@cursor)&.header || "No Match")
         | 
| 266 | 
            +
                  end
         | 
| 322 267 | 
             
                end
         | 
| 323 268 |  | 
| 324 | 
            -
                def  | 
| 325 | 
            -
                  @ | 
| 326 | 
            -
             | 
| 327 | 
            -
                   | 
| 269 | 
            +
                def color x, y
         | 
| 270 | 
            +
                  if @cursor == [x,y]
         | 
| 271 | 
            +
                    Color.fetch(Curses::COLOR_BLACK, Curses::COLOR_CYAN)
         | 
| 272 | 
            +
                  else
         | 
| 273 | 
            +
                    Color.fetch(*@grid[x][y].color)
         | 
| 274 | 
            +
                  end
         | 
| 328 275 | 
             
                end
         | 
| 329 276 |  | 
| 330 | 
            -
                def  | 
| 331 | 
            -
                   | 
| 277 | 
            +
                def draw_item x, y
         | 
| 278 | 
            +
                  target = @grid[x].select{|item| item.visible}[y]
         | 
| 279 | 
            +
             | 
| 280 | 
            +
                  drawy = y - @scroll[x]
         | 
| 281 | 
            +
                  if drawy < 0 || drawy + @header_rows >= Curses.stdscr.maxy
         | 
| 282 | 
            +
                    return
         | 
| 283 | 
            +
                  end
         | 
| 284 | 
            +
                  cell_color = color(x,y)
         | 
| 285 | 
            +
                  Curses.setpos(drawy + @header_rows, x * @cell_width)
         | 
| 286 | 
            +
                  Curses.attron(cell_color) do
         | 
| 287 | 
            +
                    Curses.addstr make_label(target)
         | 
| 288 | 
            +
                  end
         | 
| 289 | 
            +
                  Curses.refresh
         | 
| 332 290 | 
             
                end
         | 
| 333 291 |  | 
| 334 | 
            -
                def  | 
| 335 | 
            -
                   | 
| 336 | 
            -
                   | 
| 337 | 
            -
                   | 
| 338 | 
            -
                     | 
| 339 | 
            -
                       | 
| 292 | 
            +
                def draw x, y
         | 
| 293 | 
            +
                  target = @grid[x][y]
         | 
| 294 | 
            +
                  draw_item(x, y)
         | 
| 295 | 
            +
                  if target.is_a?(GroupItem)
         | 
| 296 | 
            +
                    (y+1).upto(y+target.length).each do |ny|
         | 
| 297 | 
            +
                      draw_item(x, ny)
         | 
| 340 298 | 
             
                    end
         | 
| 341 299 | 
             
                  end
         | 
| 342 | 
            -
                  @x, @y = stored_cursor.x, stored_cursor.y
         | 
| 343 | 
            -
                  render_all
         | 
| 344 300 | 
             
                end
         | 
| 345 301 |  | 
| 346 | 
            -
                def  | 
| 347 | 
            -
                   | 
| 348 | 
            -
                   | 
| 349 | 
            -
                   | 
| 350 | 
            -
                   | 
| 351 | 
            -
                   | 
| 352 | 
            -
                   | 
| 302 | 
            +
                def assign
         | 
| 303 | 
            +
                  old_mode = @mode
         | 
| 304 | 
            +
                  transit_mode(:assign)
         | 
| 305 | 
            +
                  @grid = config.columns.times.map{[]}
         | 
| 306 | 
            +
                  visible_items = @provider.filter_items(@search_buffer)
         | 
| 307 | 
            +
                  @groups = visible_items.group_by(&config.group_by)
         | 
| 308 | 
            +
                  @groups.each do |name, items|
         | 
| 309 | 
            +
                    group_name = "#{name} (#{items.length})"
         | 
| 310 | 
            +
                    target = @grid.min_by(&:length)
         | 
| 311 | 
            +
                    target << @provider.group_class.new(group_name, items)
         | 
| 312 | 
            +
                    items.sort_by(&:label).each do |item|
         | 
| 313 | 
            +
                      target << item
         | 
| 314 | 
            +
                    end
         | 
| 315 | 
            +
                  end
         | 
| 316 | 
            +
                  transit_mode(old_mode)
         | 
| 317 | 
            +
                end
         | 
| 318 | 
            +
             | 
| 319 | 
            +
                def config
         | 
| 320 | 
            +
                  Eclair.config
         | 
| 353 321 | 
             
                end
         | 
| 354 322 | 
             
              end
         | 
| 355 323 | 
             
            end
         |