Class: Typr::Grid

Inherits:
Stack show all
Defined in:
lib/grid.rb

Overview

Column format tokens: Integer (fixed width), :min (auto), :max (fill remaining).

Built-in procs: :filetree, :datetime, :magnitudes, :convert.

Direct Known Subclasses

Browser

Constant Summary

Constants included from Typr

COLORS, COLOR_MAP, ERASE_LINE, INPUT, KEY_ESCAPE, KEY_PAGEDOWN, KEY_PAGEUP, KEY_RETURN, KEY_TAB, MIMETYPES, MODES

Instance Attribute Summary collapse

Attributes inherited from Stack

#header, #hints, #hints_start, #keymap, #selected, #separator, #start

Attributes inherited from Space

#borders, #bottom, #colors, #interval, #left, #margin, #right, #top

Instance Method Summary collapse

Methods inherited from Stack

#cycle, #down, #draw_hints, #headspace, #height, #page_down, #page_up, #send, #to_bottom, #to_top, #up

Methods inherited from Space

#border=, #draw_border, #height, #start, #stop, #symbolize, #width

Methods included from Typr

#background, clear, #clip, #coerce_type, #color, #color_code, column, #draw, exit, #fade, #foreground, #get_background, #get_foreground, height, init, #mode, #mode_code, #move, #move_code, on_resize, position, #prepare, #printables, read_key, read_line, #real_size, row, size, text_width, width, word_next, word_prev

Constructor Details

#initialize(args = {}) ⇒ Grid

Construct a Grid.

grid = Typr::Grid.new(
input:  [ ["Alice", 30], ["Bob", 25] ],
header: [:name, :age],
format: [:max, :min],
procs:  { 1 => :magnitudes },
colors: { columns: [:cyan, nil] }
)

Parameters:

  • args (Hash) (defaults to: {})

    input, header, format, procs, colors, align, functions



455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/grid.rb', line 455

def initialize args={}
  @columns,@functions = 0,{}
  @ignore,@rawfilter,@rawsort,@reverse = [],[],[],false
  @filters,@procs = [],{}
  @align,@format,@widths = [],[],[]
  super
  @colors = { columns: [], fields: {} }.merge ( @colors  )
  @selected = { fields:[], columns:[] }.merge @selected
  @functions = {
    filetree: Proc.new{ |f| ' ' * [(d = f.count('/') - 1), 0].max + '' + File.basename(f) },
    datetime: Proc.new{|sec|Time.at(sec).strftime"%y-%m-%d %H:%M" rescue ??},
    magnitudes: Proc.new{ |size| mag = (size.to_s.length-1) / 3
      mag>0 ? (size.to_s.insert -(mag*3+1), ?.)[0..4] + %w[B K M G T][mag] :
      size rescue size },
    convert:  Proc.new{ |value| coerce_type value } }

  if @input
    self << @input
    @input = nil
  end
end

Instance Attribute Details

#alignObject

Returns the value of attribute align.



21
22
23
# File 'lib/grid.rb', line 21

def align
  @align
end

#dataObject

Table



20
21
22
# File 'lib/grid.rb', line 20

def data
  @data
end

#filtersObject

Table



20
21
22
# File 'lib/grid.rb', line 20

def filters
  @filters
end

#formatObject

Table



20
21
22
# File 'lib/grid.rb', line 20

def format
  @format
end

#mapObject

Table



20
21
22
# File 'lib/grid.rb', line 20

def map
  @map
end

#procsObject

Table



20
21
22
# File 'lib/grid.rb', line 20

def procs
  @procs
end

#rawfilterObject

Returns the value of attribute rawfilter.



21
22
23
# File 'lib/grid.rb', line 21

def rawfilter
  @rawfilter
end

#rawsortObject

Returns the value of attribute rawsort.



21
22
23
# File 'lib/grid.rb', line 21

def rawsort
  @rawsort
end

#reverseObject

Returns the value of attribute reverse.



21
22
23
# File 'lib/grid.rb', line 21

def reverse
  @reverse
end

#sequenceArray<Integer>

Column render order. Defaults to 0..n-1; override with sequence=.

grid.sequence  #=> [0, 1, 2]

Returns:

  • (Array<Integer>)


59
# File 'lib/grid.rb', line 59

def sequence; @sequence || @columns.times.to_a end

Instance Method Details

#<<(data) ⇒ self

Append multiple rows at once. Returns self for chaining.

grid << [["Dave", 28], ["Eve", 32]]
grid << { alice: [30, "eng"] }

Parameters:

  • data (Enumerable<Array>, Enumerable<Hash>)

Returns:

  • (self)


434
435
436
437
438
439
440
# File 'lib/grid.rb', line 434

def << (data)
  @data ||= data.class.new
  skip = @data.size
  data.each_with_index{ |row, id|
    self[ is_hash? ? row.shift : id + skip ] = row }
  sort
end

#[](id) ⇒ Array?

Retrieve a row by id.

grid[0]  #=> ["Alice", 30, "engineer"]

Returns:

  • (Array, nil)


86
# File 'lib/grid.rb', line 86

def []( id ); @data[id] end

#[]=(id, row) ⇒ Object

Set a row by id. Auto-detects numeric types and alignment.

grid[2] = ["Charlie", 35, "manager"]
grid[:charlie] = [35, "manager"]  # Hash-backed

Parameters:

  • id (Integer, Object)

    row key

  • row (Object, Array)

    field values



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/grid.rb', line 97

def []=( id,row )
  @data ||= id.is_a?(Integer) ? [] : {}
  row = [row] unless row.is_a? Array
  row = [id] + row if is_hash?
  row.flatten! if is_hash?
  @data[id] = row.map.with_index{ |field, column|
    if column == @columns
      @colors[:columns][column] ||= [:grey90,:grey60][column%2]
      @format[column] ||= :min
      @columns += 1
    end
    field = @functions[ :convert ].call field unless @ignore[column]
    @align[column] ||= [Integer,Float].include?(field.class) ?
      :right : :left
    field
  }
  process id
  filter id
end

#add_filter(column = nil, query = nil) ⇒ Object

Add a column filter (substring or regex). :all matches any column.

grid.add_filter(0, "Ali")    # name contains "Ali"
grid.add_filter(1, /^\d{2}$/) # age matches regex
grid.add_filter(:all, "ip")  # any column contains "ip"

Parameters:

  • column (Integer, Symbol) (defaults to: nil)
  • query (String, Regexp) (defaults to: nil)


189
190
191
192
193
194
# File 'lib/grid.rb', line 189

def add_filter column=nil, query=nil
  return unless column and query
  @filters << [column, query]
  filter :all
  return
end

#check(row) ⇒ Boolean

Test whether a row passes all active filters.

grid.check(0)  #=> true

Parameters:

  • row (Integer)

Returns:

  • (Boolean)


204
205
206
207
208
209
210
# File 'lib/grid.rb', line 204

def check row
  @filters.reject{ |column,query|
    cols = column == :all ? @data[row].each_index.to_a : [column]
    cols.any?{ |col|
      ( (@procs[col] and not @rawfilter[col]) ?
        @layer : @data )[row][col].to_s[query] } }.empty?
end

#clearvoid

This method returns an undefined value.

Clear all data, columns, and transforms.

grid.clear


156
157
158
159
# File 'lib/grid.rb', line 156

def clear
  @columns = 0; @layer = @map = @data = nil
  reset [:format, :positions, :selection]
end

#columnsInteger

Total number of data columns.

grid.columns  #=> 3

Returns:

  • (Integer)


68
# File 'lib/grid.rb', line 68

def columns; @sequence ? @sequence.count : @columns end

#filter(row = :all) ⇒ Object

Apply or revise filters. :all recomputes the full map; a row id adds/removes it.

grid.filter :all
grid.filter 0

Parameters:

  • row (Integer, Symbol) (defaults to: :all)


220
221
222
223
224
225
226
227
228
229
# File 'lib/grid.rb', line 220

def filter row=:all
  return if @filters.empty?
  if row == :all
    @map = ids.select{ |id| check id }
    @start = 0
  elsif check row then
    @map ||= []
    @map << row unless @map.include? row
  else @map.delete(row) if @map end
end

#header=(h) ⇒ Object

Set column headers. Symbols are auto-defined as UPCASE_I constants.

grid.header = [:name, :age]
# defines NAME_0 = "name", AGE_1 = "age"

Parameters:

  • h (Array<String, Symbol>)


125
126
127
128
129
# File 'lib/grid.rb', line 125

def header=( h ); super
  h.each_with_index{ |h,i|
    Typr.const_set(h.upcase, i) unless Typr.const_defined?(h.upcase)
  } if h.is_a? Array
end

#idsArray<Object>

Row identifiers: Hash keys or integer range.

grid.ids  #=> [0, 1]

Returns:

  • (Array<Object>)


40
41
# File 'lib/grid.rb', line 40

def ids; @map || ( is_hash? ?
@data.keys : (@data||[]).count.times.to_a ) end

#is_hash?Boolean

True if data is a Hash.

grid.is_hash?  #=> false

Returns:

  • (Boolean)


77
# File 'lib/grid.rb', line 77

def is_hash?; @data.is_a? Hash end

#pageArray<Object>

Row ids visible on the current page after filtering.

grid.page  #=> [0, 1]

Returns:

  • (Array<Object>)


50
# File 'lib/grid.rb', line 50

def page; ids[ super ] end

#pick(type = :row, row = 0, column: nil) ⇒ Object

Pick a row with live-filtering.

When column is given, Grid overrides Stack#pick to add interactive text filtering of rows by the given column. Filter text is shown at the bottom of the grid. Passes through to super for all other types or when no column is given.

grid.pick :row, 0, column: :name
grid.pick :row, 0, column: :all   # match any column

Parameters:

  • type (Symbol) (defaults to: :row)

    type is ignored (always :row); passed to super otherwise

  • column (Symbol, String) (defaults to: nil)

    column name to filter on, or :all (default nil)



257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/grid.rb', line 257

def pick type = :row, row=0, column: nil
  return super(type, row) unless column

  saved = [@filters.dup, @map, @start]
  base = @map || ids
  @filters = [[column, '']]
  @map = base.select{ |id| check id }
  @start = 0
  show
  draw_hints
  result = Typr.read_line '/', left: left, top: self.bottom + 1,
    &->(key, query, _) {
      if key.is_a?(String) and idx = @hints[0..height-@hints_start-1].index(key)
        return page.to_a[idx + @hints_start]
      end
      if key.is_a?(String) and !key.each_char.all?{ |c| c.ord.between?(32, 126) } and
        @keymap.values.include?(key) and ![KEY_BACKSPACE, "\b"].include?(key)
        send key
      else
        @filters = [[column, query]]
        @map = base.select{ |id| check id }
        @start = 0
      end
      show
      draw_hints
      nil
    }
  case result
  when nil;      @filters, @map, @start = saved
  when Integer; return result
  else
    @filters = result.empty? ? saved[0] : saved[0] + [[column, result]]
    filter :all
  end
  return
end

#positions(row = 0) ⇒ Array<Integer>

X-offset for each visual column on a row.

grid.positions  #=> [0, 13, 25]

Parameters:

  • row (Integer) (defaults to: 0)

Returns:

  • (Array<Integer>)


343
344
345
346
# File 'lib/grid.rb', line 343

def positions row=0; x=0
  [0] + (sequence.count-1).times.map{ |id|
    x += width_for(id) + @separator.size }
end

Render a single row or the header bar to the terminal.

grid.print :header
grid.print 0

Parameters:

  • row (Integer, Symbol) (defaults to: nil)

    row id, or :header



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/grid.rb', line 404

def print row=nil
  return super unless row
  header = ( row == :header )
  return draw @header[0..width-1].ljust(width) if
    header and @header.is_a? String
  fields = (header ? @header : @data[row] ).dup
  @layer[row].each{ |col,value| fields[col] = value } if
    @layer[row] if @layer unless header
   for col,id in sequence.each_with_index
    draw @separator unless id == 0
    select = ( @selected[:columns].include? col or
      @selected[:fields].include? [row,col] ) unless
        @selected[:rows].include?(row) or header
    color( @colors[:fields][[row,col]] ||
      @colors[:columns][col] || @colors[:default] ) unless header
    background @colors[:selected] if select
    draw prepare( fields[col].to_s, width_for( id ), @align[col] )
    background @colors[:default][1] if select
  end
end

#process(row) ⇒ Object

Apply column processors to a row's cells, storing results in @layer.

grid.process(0)

Parameters:

  • row (Integer)

    row id



168
169
170
171
172
173
174
175
176
177
# File 'lib/grid.rb', line 168

def process row #=:all
  @layer ||= @data.class.new
  return if @procs.empty?
  @layer[row] = @procs.map{ |col,proc|
    proc = @functions[proc] if proc.is_a? Symbol
    cell = @data[row][col]
    next unless cell
    [ col, proc.lambda? ? proc.call( cell ) : proc.call( cell, row, col ) ] unless
      cell.is_a?(Proc) }.to_h
end

#reset(type = :all) ⇒ Object

Reset grid state.

grid.reset :all       # everything
grid.reset :display   # filters, sort, map
grid.reset :format    # widths, maxed

Parameters:

  • type (:all, :display, :format, Array<Symbol>) (defaults to: :all)


140
141
142
143
144
145
146
147
# File 'lib/grid.rb', line 140

def reset type=:all
  case type
    when :display; @map = nil; @sort = nil; @filters.clear
    when :format; @widths.clear
    when :all; reset [:display, :format]
  end
  super
end

#rowsInteger

Number of rows currently visible (filtered count, or total).

grid.rows  #=> 2

Returns:

  • (Integer)


31
# File 'lib/grid.rb', line 31

def rows; (@map || @data || [] ).count end

#showvoid

This method returns an undefined value.

Compute column widths from visible data and render the grid.

grid.show


355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/grid.rb', line 355

def show
  if not @data or @data.empty? or ( @map and @map.empty? )
    @widths = [ width/columns ] * columns if @widths.count < columns
    @rest = 0
    return super
  end
  for row in page.compact
    fields = @data[row].dup
    fields = [ fields ] unless fields.is_a?(Array)
    for col,id in sequence.each_with_index
      field = fields[col]
      if field.is_a? Proc
        field = field.call
        field = @procs[col].call field,row,col if @procs[col]
        @layer[row] ||= {}
        @layer[row][col] = field
      end
      field = @layer[row][col] if @procs[col]
      if @format[col].is_a? Integer
        @widths[id] ||= @format[col]
      else
        @widths[id] ||= 0
        size = real_size( field.to_s )
        @widths[id] = size if size > @widths[id]
      end
    end
  end
  gaps = @margin.size + @separator.size * (columns - 1)
  sum = @widths.sum
  @max = sum + gaps - 1
  space = width - gaps
  if (extra = sum - space  ) > 0
    extra.times{ |i| @widths[ @widths.index(@widths.max) ] -= 1 }
    @rest = 0
  else
    format = @format.values_at( *sequence )
    @rest = space - sum / format.count(:max) if format.include? :max
  end
  super
end

#sortfalse, void

Execute the sort and update @map.

Returns:

  • (false, void)


314
315
316
317
318
319
320
321
# File 'lib/grid.rb', line 314

def sort
  return false unless @sort
  data = (@procs[@sort] and not @rawsort[@sort]) ? @layer : @data
  @map = (0...@data.size).to_a.sort{ |a,b| (data[a][@sort] <=> data[b][@sort]) || 0 }
  @map.reverse! if @reverse
  @map = @map.select{ |id| check id } unless @filters.empty?
  return
end

#sort_by(column) ⇒ Object

Sort by column (toggle direction on repeat).

grid.sort_by :age
grid.sort_by :age  # reverses

Parameters:

  • column (Integer, Symbol)


302
303
304
305
306
307
# File 'lib/grid.rb', line 302

def sort_by column
  column = @header.index( column.to_s ) if column.is_a? Symbol
  if @sort and (column == @sort) then @reverse = !@reverse
  else @sort = column end
  sort
end

#sorted_by(name = false) ⇒ String, ...

Current sort column name (when called with arg) or index (without).

grid.sort_by :age
grid.sorted_by       #=> "age"
grid.sorted_by true  #=> "age"

Parameters:

  • name (Boolean) (defaults to: false)

    pass truthy to get display name

Returns:

  • (String, Integer, false, nil)


241
# File 'lib/grid.rb', line 241

def sorted_by name=false; (name and @sort) ? @header[@sort] : @sort end

#width_for(col) ⇒ Integer

Pixel width for a single visual column (includes :max bonus).

grid.width_for(0)  #=> 12

Parameters:

  • col (Integer)

    visual column index

Returns:

  • (Integer)


331
332
333
# File 'lib/grid.rb', line 331

def width_for col
  @widths[col] + ( @format[sequence[col]] == :max ? @rest : 0 )
end