Class: Typr::Grid

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

Overview

A sortable, filterable, multi-column table widget.

grid = Typr::Grid.new(
input:  [ ["Alice", 30], ["Bob", 25] ],
header: [:name, :age],
format: [:max, :right]
)
grid.draw

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, 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, #pick, #send, #to_bottom, #to_top, #up

Methods inherited from Space

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

Methods included from Typr

#background, clear, #coerce_type, #color, #color_code, column, exit, #fade, #foreground, #get_background, #get_foreground, height, init, #mode, #mode_code, #move, #move_code, on_resize, position, #prepare, #printables, read, #real_size, row, #show, size, width

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



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# File 'lib/grid.rb', line 400

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| f.gsub /.*\/[^$]/, ' ' },
    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

#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)


379
380
381
382
383
384
385
# File 'lib/grid.rb', line 379

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).

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

Parameters:

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


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

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)


203
204
205
206
207
# File 'lib/grid.rb', line 203

def check row
  @filters.reject{ |column,query|
    ( (@procs[column] and not @rawfilter[column]) ?
      @layer : @data )[row][column].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

#drawvoid

This method returns an undefined value.

Compute column widths from visible data and render the grid.

grid.draw


300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/grid.rb', line 300

def draw
  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

#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)


217
218
219
220
221
222
223
224
225
226
# File 'lib/grid.rb', line 217

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

#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>)


288
289
290
291
# File 'lib/grid.rb', line 288

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



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/grid.rb', line 349

def print row=nil
  return super unless row
  header = ( row == :header )
  return show @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
    show @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
    show 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

#sortfalse, void

Execute the sort and update @map.

Returns:

  • (false, void)


260
261
262
263
264
265
266
# File 'lib/grid.rb', line 260

def sort
  return false unless @sort
  data = (@procs[@sort] and not @rawsort[@sort]) ? @layer : @data
  @map = ids.sort{ |a,b| (data[a][@sort] <=> data[b][@sort]) || 0 }
  @map.reverse! if @reverse
  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)


248
249
250
251
252
253
# File 'lib/grid.rb', line 248

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)


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

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)


276
277
278
# File 'lib/grid.rb', line 276

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