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.
Files changed (42) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -1
  3. data/Gemfile +1 -0
  4. data/Gemfile.lock +48 -0
  5. data/README.md +112 -89
  6. data/Rakefile +1 -0
  7. data/eclair.gemspec +7 -4
  8. data/exe/ecl +62 -4
  9. data/lib/eclair.rb +21 -18
  10. data/lib/eclair/color.rb +3 -0
  11. data/lib/eclair/config.rb +68 -21
  12. data/lib/eclair/grid.rb +242 -274
  13. data/lib/eclair/group_item.rb +33 -0
  14. data/lib/eclair/item.rb +42 -0
  15. data/lib/eclair/less_viewer.rb +2 -1
  16. data/lib/eclair/provider.rb +15 -0
  17. data/lib/eclair/providers/ec2.rb +2 -0
  18. data/lib/eclair/providers/ec2/ec2_group_item.rb +16 -0
  19. data/lib/eclair/providers/ec2/ec2_item.rb +136 -0
  20. data/lib/eclair/providers/ec2/ec2_provider.rb +118 -0
  21. data/lib/eclair/providers/gce.rb +2 -0
  22. data/lib/eclair/providers/gce/gce_group_item.rb +15 -0
  23. data/lib/eclair/providers/gce/gce_item.rb +117 -0
  24. data/lib/eclair/providers/gce/gce_provider.rb +34 -0
  25. data/lib/eclair/providers/k8s.rb +2 -0
  26. data/lib/eclair/providers/k8s/k8s_group_item.rb +15 -0
  27. data/lib/eclair/providers/k8s/k8s_item.rb +92 -0
  28. data/lib/eclair/providers/k8s/k8s_provider.rb +34 -0
  29. data/lib/eclair/version.rb +2 -1
  30. data/out.gif +0 -0
  31. data/templates/eclrc.template +81 -35
  32. metadata +60 -27
  33. data/bin/console +0 -10
  34. data/lib/eclair/cell.rb +0 -101
  35. data/lib/eclair/column.rb +0 -52
  36. data/lib/eclair/console.rb +0 -56
  37. data/lib/eclair/group.rb +0 -55
  38. data/lib/eclair/helpers/aws_helper.rb +0 -157
  39. data/lib/eclair/helpers/benchmark_helper.rb +0 -19
  40. data/lib/eclair/helpers/common_helper.rb +0 -24
  41. data/lib/eclair/instance.rb +0 -165
  42. 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
- RCFILE = ENV["ECLRC"] || "#{ENV['HOME']}/.eclrc"
4
- include Curses
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
- @instance_color = [COLOR_WHITE, -1].freeze
29
- @group_color = [COLOR_WHITE, -1, A_BOLD].freeze
30
- @current_color = [COLOR_BLACK, COLOR_CYAN].freeze
31
- @selected_color = [COLOR_YELLOW, -1, A_BOLD].freeze
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
- unless File.exists? RCFILE
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.cp(template_path, RCFILE)
45
- puts "#{RCFILE} successfully created. Edit it and run again!"
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 configure
67
- yield config
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
- module Grid
3
- include CommonHelper
4
- extend self
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
- HEADER_ROWS = 4
7
- SORT_FUNCTIONS = {
8
- "Name" => lambda {|i| [i.name.downcase, -i.launch_time.to_i]},
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
- def maxy
12
- stdscr.maxy - HEADER_ROWS
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
- header.each_with_index do |line,i|
27
- setpos(i,0)
28
- clrtoeol
29
- addstr(line)
30
- end
31
- render_help
32
- end
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
- def render_help
35
- setpos(3,0)
36
- clrtoeol
37
-
38
- helps = {
39
- "Enter" => "SSH",
40
- "Space" => "Select",
41
- "[a-z0-9]" => "Search",
42
- "?" => "Inspect",
43
- "!" => "Open Ruby REPL",
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
- def cell_width
54
- stdscr.maxx/column_count
65
+ move_cursor(newx, newy)
55
66
  end
56
67
 
57
- def start
58
- assign
59
- move_cursor(x: 0, y: 0)
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
- def render_all
102
- clear
103
- columns.each do |cols|
104
- cols.each do |c|
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
- render_header
109
- refresh
78
+
79
+
80
+ draw(*@cursor)
110
81
  end
111
82
 
112
- def ssh
113
- targets = selected.select{|i| i.is_a?(Instance) && i.connectable?}
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
- cmd = ""
118
- if targets.count == 1
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
- session_name = nil
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
- cmds << "tmux new-window -- '#{target.ssh_cmd}'"
130
- else
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
- session_cmd = "-t #{session_name}"
133
- cmds << "tmux new-session -d -s #{session_name} -- '#{target.ssh_cmd}'"
104
+ target_cmd = "-t #{session_name}"
105
+ `tmux new-session -d -s #{session_name} -- '#{target.command}'`
134
106
  end
135
- else
136
- cmds << "tmux split-window #{session_cmd} -- '#{target.ssh_cmd}'"
137
- cmds << "tmux select-layout #{session_cmd} tiled"
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 << "tmux set-window-option #{session_cmd} synchronize-panes on"
141
- cmds << "tmux attach #{session_cmd}" unless ENV['TMUX']
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 cmd
145
- exit
116
+ system(cmd)
117
+ exit()
118
+ resize
146
119
  end
147
120
 
148
- def column_count
149
- columns.count{|col| !col.empty?}
150
- end
151
-
152
- def columns
153
- @columns ||= config.columns.times.map{|idx| Column.new(idx)}.to_a
154
- end
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 selected
166
- @selected ||= []
167
- end
130
+ def transit_mode to
131
+ return if to == @mode
168
132
 
169
- def mode
170
- @mode ||= :navi
171
- end
172
-
173
- def select
174
- end_search if mode == :search
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
- def move key
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
- newx = (@x + mx) % column_count
200
- newy = (@y + my - columns[@x].scroll + columns[newx].scroll)
201
- if my != 0
202
- newy %= columns[newx].count
203
- end
204
- if newy >= columns[newx].count
205
- newy = columns[newx].count-1
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
- move_cursor(x: newx, y: newy)
152
+ draw_all
209
153
  end
210
154
 
211
- def cursor
212
- @x ||= -1
213
- @y ||= -1
214
- if @x >=0 && @y >= 0
215
- columns[@x][@y]
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
- nil
163
+ transit_mode(:nav)
218
164
  end
219
165
  end
220
166
 
221
- def query
222
- return nil if @search_str == ""
223
- result = columns.map(&:expand).flatten.grep(Instance).map(&:name).max_by{|name| name.score @search_str}
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 start_search
229
- @rollback_cursor = [@x, @y]
230
- @rollback_mode = @mode
231
- @search_str = ""
232
- end
172
+ def append_search key
173
+ return unless key
233
174
 
234
- def end_search
235
- if cursor
236
- move_cursor(mode: @rollback_mode)
237
- @mode = @rollback_mode
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
- cancel_search
184
+ return
240
185
  end
241
- end
242
186
 
243
- def cancel_search
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 **options, &block
250
- if cursor
251
- cursor.toggle_select if mode == :navi
252
- cursor.decurrent
253
- end
192
+ def move_cursor x, y
193
+ prev = @cursor.dup
194
+ @cursor = [x, y]
254
195
 
255
- new_mode = options.delete(:mode)
256
- if new_mode && mode != new_mode
257
- case new_mode
258
- when :search
259
- start_search
260
- end
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
- if cursor
272
- cursor.toggle_select if mode == :navi
273
- cursor.current
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 search key
278
- @search_str ||= ""
279
-
280
- move_cursor(mode: :search) do
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
- goto = query
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
- result = nil
290
- columns.each do |col|
291
- target = col.find {|item| item.is_a?(Instance) && item.name == goto}
292
- if target
293
- result = [target.x, target.y]
294
- break
295
- end
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 resize
305
- columns.each{|col| col.scroll = 0}
306
- render_all
307
- cursor.check_scroll
247
+ def at x, y
248
+ @grid[x][y]
308
249
  end
309
250
 
310
- def cursor_inspect
311
- close_screen
312
- LessViewer.show cursor.info
313
- render_all
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 debug
317
- trap("INT") { raise Interrupt }
318
- close_screen
319
- binding.pry
320
- render_all
321
- trap("INT") { exit }
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 next_sort_function
325
- @sort_function_idx ||= -1
326
- @sort_function_idx = (@sort_function_idx + 1) % SORT_FUNCTIONS.count
327
- SORT_FUNCTIONS.values[@sort_function_idx]
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 sorted_by
331
- SORT_FUNCTIONS.keys[@sort_function_idx]
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 change_sort
335
- stored_cursor = cursor
336
- sort_function = next_sort_function
337
- columns.each do |column|
338
- column.groups.each do |group|
339
- group.items.sort_by!(&sort_function)
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 reload
347
- clear
348
- addstr("reloading")
349
- refresh
350
- Aws.reload_instances
351
- assign
352
- render_all
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