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_DELETE, KEY_ENTER, KEY_ESCAPE, KEY_INSERT, KEY_PAGEDOWN, KEY_PAGEUP, KEY_RETURN, KEY_TAB, MIMETYPES, MODES, OUTPUT

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, #color, #color_code, column, exit, #fade, #foreground, #get_background, #get_foreground, height, init, #mode, #mode_code, #move, #move_code, 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



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

def initialize args={}
  @columns,@functions = 0,{}
  @ignore,@rawfilter,@rawsort,@reverse = [],[],[],false
  @filters,@procs = [],{}
  @align,@format,@widths,@totals = [],[],[],[]
  super #args
  @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|
      case value.to_s
        when /^[\d]+$/ then value.to_s.to_i
        when /^\d*[\.\,]\d+$/ then value.to_s.to_f
        else value
      end } }

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

Instance Attribute Details

#alignObject

Returns the value of attribute align.



25
26
27
# File 'lib/grid.rb', line 25

def align
  @align
end

#dataObject

Table



24
25
26
# File 'lib/grid.rb', line 24

def data
  @data
end

#filtersObject

Table



24
25
26
# File 'lib/grid.rb', line 24

def filters
  @filters
end

#formatObject

Table



24
25
26
# File 'lib/grid.rb', line 24

def format
  @format
end

#mapObject

Table



24
25
26
# File 'lib/grid.rb', line 24

def map
  @map
end

#procsObject

Table



24
25
26
# File 'lib/grid.rb', line 24

def procs
  @procs
end

#rawfilterObject

Returns the value of attribute rawfilter.



25
26
27
# File 'lib/grid.rb', line 25

def rawfilter
  @rawfilter
end

#rawsortObject

Returns the value of attribute rawsort.



25
26
27
# File 'lib/grid.rb', line 25

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


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

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)


382
383
384
385
386
387
388
# File 'lib/grid.rb', line 382

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)


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

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



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/grid.rb', line 101

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)


191
192
193
194
195
196
# File 'lib/grid.rb', line 191

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)


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

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


159
160
161
162
# File 'lib/grid.rb', line 159

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

#columnsInteger

Total number of data columns.

grid.columns  #=> 3

Returns:

  • (Integer)


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

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


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
340
341
342
# File 'lib/grid.rb', line 303

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)


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


129
130
131
132
# File 'lib/grid.rb', line 129

def header=( h ); super
  h.each_with_index{ |h,i| Typr.module_eval h.upcase+?=+i.to_s } if
    h.is_a? Array
end

#idsArray<Object>

Row identifiers: Hash keys or integer range.

grid.ids  #=> [0, 1]

Returns:

  • (Array<Object>)


44
45
# File 'lib/grid.rb', line 44

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)


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

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


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

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


291
292
293
294
# File 'lib/grid.rb', line 291

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



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

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



171
172
173
174
175
176
177
178
179
180
# File 'lib/grid.rb', line 171

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)


143
144
145
146
147
148
149
150
# File 'lib/grid.rb', line 143

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

#rowsInteger

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

grid.rows  #=> 2

Returns:

  • (Integer)


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

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

#sortfalse, void

Execute the sort and update @map.

Returns:

  • (false, void)


263
264
265
266
267
268
269
# File 'lib/grid.rb', line 263

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)


251
252
253
254
255
256
# File 'lib/grid.rb', line 251

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)


279
280
281
# File 'lib/grid.rb', line 279

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