typr 1.1.0
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 +7 -0
- data/example/browser +45 -0
- data/example/grid +80 -0
- data/example/text +65 -0
- data/example/top500.csv +501 -0
- data/lib/browser.rb +431 -0
- data/lib/curses.rb +153 -0
- data/lib/frontend.rb +33 -0
- data/lib/graphical.rb +174 -0
- data/lib/grid.rb +431 -0
- data/lib/line.rb +99 -0
- data/lib/space.rb +85 -0
- data/lib/stack.rb +278 -0
- data/lib/terminal.rb +286 -0
- data/lib/text.rb +140 -0
- data/lib/typr.rb +55 -0
- data/share/mimetypes +1 -0
- metadata +73 -0
data/lib/graphical.rb
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/ruby
|
|
2
|
+
require 'sdl2'
|
|
3
|
+
#require 'io/console'
|
|
4
|
+
#require 'readline'
|
|
5
|
+
require_relative 'frontend'
|
|
6
|
+
|
|
7
|
+
module Visuals
|
|
8
|
+
include SDL2
|
|
9
|
+
|
|
10
|
+
public
|
|
11
|
+
i=-1
|
|
12
|
+
# SDL2.init(SDL2::INIT_EVERYTHING)
|
|
13
|
+
SDL2.init(SDL2::INIT_VIDEO)
|
|
14
|
+
TTF.init
|
|
15
|
+
WINDOW = Window.create("visuals",
|
|
16
|
+
Window::POS_CENTERED, #POS_UNDEFINED,
|
|
17
|
+
Window::POS_CENTERED, #POS_UNDEFINED,
|
|
18
|
+
1324, 768, 0)
|
|
19
|
+
GO = WINDOW.create_renderer(-1,
|
|
20
|
+
Renderer::Flags::ACCELERATED|Renderer::Flags::TARGETTEXTURE)
|
|
21
|
+
Key.constants.each{ |c| c = c.to_s
|
|
22
|
+
eval 'KEY_'+c.upcase+'=Key::'+c }
|
|
23
|
+
|
|
24
|
+
def self.width; WINDOW.size[0] / $charsize[1] end
|
|
25
|
+
def self.height; WINDOW.size[1] / $charsize[0] end
|
|
26
|
+
def self.row; $row end
|
|
27
|
+
def self.column; $col end
|
|
28
|
+
def self.x; $x end
|
|
29
|
+
def self.y; $y end
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def color_for c
|
|
33
|
+
return unless c
|
|
34
|
+
c = COLORS[c] if c.is_a?( Symbol )
|
|
35
|
+
c.map{ |c| (c/1000.0*256).to_i }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def get_background; $color[1] end
|
|
39
|
+
def get_foreground; $color[0] end
|
|
40
|
+
def background color; return unless color
|
|
41
|
+
$color[1] = color_for( color ) end
|
|
42
|
+
def foreground color;
|
|
43
|
+
$color[0] = (color ? color_for( color ) : $default[0]) end
|
|
44
|
+
|
|
45
|
+
def color c
|
|
46
|
+
c = [c] unless c.is_a? Array and c.count == 2 #.flatten
|
|
47
|
+
foreground c[0]
|
|
48
|
+
background c[1]
|
|
49
|
+
# $letters[c] = (32.upto 126).map{ |n| n = n.chr
|
|
50
|
+
# [n, $font.render_blended( n, c )] }.to_h unless $letters[c]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def move col, row
|
|
54
|
+
$x = ($col = col) * $charsize[1] #width
|
|
55
|
+
$y = ($row = row) * $charsize[0] #$height
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def clear type = :screen #bg=
|
|
59
|
+
GO.draw_color = $default[1] #bg
|
|
60
|
+
case type
|
|
61
|
+
# when Array; GO.fill_rect Rect.new()
|
|
62
|
+
when :screen; GO.fill_rect Rect.new(0, 0, *WINDOW.size)
|
|
63
|
+
when :line; GO.fill_rect Rect.new(
|
|
64
|
+
Visuals.x, Visuals.y, WINDOW.size[0], Visuals.y+$charsize[0])
|
|
65
|
+
when :down; GO.fill_rect Rect.new(
|
|
66
|
+
0, Visuals.y, *WINDOW.size)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def show( str ) #solid_blended_shaded(
|
|
71
|
+
return unless str
|
|
72
|
+
# chars = str.chars.map{ |char| $letters[foreground][char] }
|
|
73
|
+
# width = chars.map( &:w ).sum
|
|
74
|
+
# if $color[1] != $default[1] #get_background
|
|
75
|
+
if get_background
|
|
76
|
+
GO.draw_color = get_background
|
|
77
|
+
GO.fill_rect Rect.new($x, $y, str.size*$charsize[1], $charsize[0])
|
|
78
|
+
end
|
|
79
|
+
# GO.fill_rect Rect.new($x, $y, width, $charsize[0])
|
|
80
|
+
# text = Surface.new( width, $charsize[0], 32, 0, 0, 0, 0 )
|
|
81
|
+
# x=0
|
|
82
|
+
# for char in chars #.each{|c|
|
|
83
|
+
# Surface.blit char, nil, text, Rect.new( x, 0, char.w, char.h )
|
|
84
|
+
# x += char.w
|
|
85
|
+
# end
|
|
86
|
+
# GO.copy GO.create_texture_from(text),nil,Rect.new($x, $y, width, $charsize[0])
|
|
87
|
+
text = str.rstrip
|
|
88
|
+
unless text.empty?
|
|
89
|
+
image = $font.render_blended( text, get_foreground )
|
|
90
|
+
GO.copy GO.create_texture_from( image ), nil,
|
|
91
|
+
Rect.new($x, $y, image.w, $charsize[0])
|
|
92
|
+
end
|
|
93
|
+
move $col+str.size, $row
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def refresh; GO.present end
|
|
97
|
+
|
|
98
|
+
def read input
|
|
99
|
+
case input
|
|
100
|
+
when :event then e = Event.poll until e
|
|
101
|
+
when :key then
|
|
102
|
+
e = Event.poll until e.is_a? Event::KeyDown
|
|
103
|
+
return e.sym.between?(32, 126) ? e.sym.chr : e.sym
|
|
104
|
+
when :line#; line = ''
|
|
105
|
+
# File.mkfifo( 'input' ) unless File.exist? 'input'
|
|
106
|
+
# Readline.output=input=open('input')
|
|
107
|
+
# Readline.output=input=StringIO.new
|
|
108
|
+
# fork{ Readline.readline }
|
|
109
|
+
x, y = Visuals.row, Visuals.column
|
|
110
|
+
listen = Thread.new{
|
|
111
|
+
# until (char = input.read) == $/
|
|
112
|
+
# until (line = Readline.line_buffer)[-1] == $/
|
|
113
|
+
loop do
|
|
114
|
+
move x, y
|
|
115
|
+
show line
|
|
116
|
+
sleep 0.1
|
|
117
|
+
end
|
|
118
|
+
}
|
|
119
|
+
line = Readline.readline
|
|
120
|
+
listen.terminate
|
|
121
|
+
# until input.string[-1]==$/
|
|
122
|
+
return line
|
|
123
|
+
# until (key = read :key) == KEY_RETURN
|
|
124
|
+
# str << key if key.is_a? String
|
|
125
|
+
# end
|
|
126
|
+
#
|
|
127
|
+
# text = ""
|
|
128
|
+
# TextInput.start
|
|
129
|
+
# loop do
|
|
130
|
+
## ev = Event.poll
|
|
131
|
+
# case ev = read( :event )
|
|
132
|
+
# when Event::KeyDown
|
|
133
|
+
# if ev.sym == KEY_RETURN
|
|
134
|
+
# TextInput.stop
|
|
135
|
+
# return text
|
|
136
|
+
# end
|
|
137
|
+
# when Event::TextInput
|
|
138
|
+
# text += ev.text
|
|
139
|
+
# show ev.text
|
|
140
|
+
# p ev.text
|
|
141
|
+
# end
|
|
142
|
+
# end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.exit
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def self.init args={}
|
|
150
|
+
$default = [
|
|
151
|
+
color_for( args[:foreground] || :grey80 ),
|
|
152
|
+
color_for( args[:background] || :grey10 ) ]
|
|
153
|
+
$color = $default.dup
|
|
154
|
+
$charsize = args[:charsize] || [18,11]
|
|
155
|
+
$font = TTF.open( args[:font] || "/usr/share/fonts/X11/TTF/VeraMono.ttf", $charsize[0] )
|
|
156
|
+
move 0, 0
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if __FILE__ == $0
|
|
162
|
+
# require_relative 'base'
|
|
163
|
+
include Visuals
|
|
164
|
+
Visuals.init font:ARGV[0] #[[200,200,200],[20,20,20]], [80,50]
|
|
165
|
+
characters = (32..126).map(&:chr).join
|
|
166
|
+
loop do
|
|
167
|
+
clear
|
|
168
|
+
COLORS.keys.each_with_index{ |c,i|
|
|
169
|
+
foreground c; move 0,i; show characters }
|
|
170
|
+
refresh
|
|
171
|
+
# GO.present
|
|
172
|
+
exit if read(:key) == KEY_ESCAPE
|
|
173
|
+
end
|
|
174
|
+
end
|
data/lib/grid.rb
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require_relative 'space.rb'
|
|
6
|
+
|
|
7
|
+
module Typr
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# A sortable, filterable, multi-column table widget.
|
|
11
|
+
#
|
|
12
|
+
# grid = Typr::Grid.new(
|
|
13
|
+
# input: [ ["Alice", 30], ["Bob", 25] ],
|
|
14
|
+
# header: [:name, :age],
|
|
15
|
+
# format: [:max, :right]
|
|
16
|
+
# )
|
|
17
|
+
# grid.draw
|
|
18
|
+
#
|
|
19
|
+
# Column format tokens: +Integer+ (fixed width), +:min+ (auto), +:max+ (fill remaining).
|
|
20
|
+
#
|
|
21
|
+
# Built-in procs: +:filetree+, +:datetime+, +:magnitudes+, +:convert+.
|
|
22
|
+
|
|
23
|
+
class Grid < Stack # Table
|
|
24
|
+
attr_accessor :format, :procs, :filters, :data, :map
|
|
25
|
+
attr_accessor :align, :rawsort, :rawfilter
|
|
26
|
+
attr_writer :sequence
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# Number of rows currently visible (filtered count, or total).
|
|
30
|
+
#
|
|
31
|
+
# grid.rows #=> 2
|
|
32
|
+
#
|
|
33
|
+
# @return [Integer]
|
|
34
|
+
|
|
35
|
+
def rows; (@map || @data || [] ).count end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# Row identifiers: Hash keys or integer range.
|
|
39
|
+
#
|
|
40
|
+
# grid.ids #=> [0, 1]
|
|
41
|
+
#
|
|
42
|
+
# @return [Array<Object>]
|
|
43
|
+
|
|
44
|
+
def ids; @map || ( is_hash? ?
|
|
45
|
+
@data.keys : (@data||[]).count.times.to_a ) end
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# Row ids visible on the current page after filtering.
|
|
49
|
+
#
|
|
50
|
+
# grid.page #=> [0, 1]
|
|
51
|
+
#
|
|
52
|
+
# @return [Array<Object>]
|
|
53
|
+
|
|
54
|
+
def page; ids[ super ] end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# Column render order. Defaults to 0..n-1; override with +sequence=+.
|
|
58
|
+
#
|
|
59
|
+
# grid.sequence #=> [0, 1, 2]
|
|
60
|
+
#
|
|
61
|
+
# @return [Array<Integer>]
|
|
62
|
+
|
|
63
|
+
def sequence; @sequence || @columns.times.to_a end
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# Total number of data columns.
|
|
67
|
+
#
|
|
68
|
+
# grid.columns #=> 3
|
|
69
|
+
#
|
|
70
|
+
# @return [Integer]
|
|
71
|
+
|
|
72
|
+
def columns; @sequence ? @sequence.count : @columns end
|
|
73
|
+
|
|
74
|
+
##
|
|
75
|
+
# True if data is a Hash.
|
|
76
|
+
#
|
|
77
|
+
# grid.is_hash? #=> false
|
|
78
|
+
#
|
|
79
|
+
# @return [Boolean]
|
|
80
|
+
|
|
81
|
+
def is_hash?; @data.is_a? Hash end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Retrieve a row by id.
|
|
85
|
+
#
|
|
86
|
+
# grid[0] #=> ["Alice", 30, "engineer"]
|
|
87
|
+
#
|
|
88
|
+
# @return [Array, nil]
|
|
89
|
+
|
|
90
|
+
def []( id ); @data[id] end
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# Set a row by id. Auto-detects numeric types and alignment.
|
|
94
|
+
#
|
|
95
|
+
# grid[2] = ["Charlie", 35, "manager"]
|
|
96
|
+
# grid[:charlie] = [35, "manager"] # Hash-backed
|
|
97
|
+
#
|
|
98
|
+
# @param id [Integer, Object] row key
|
|
99
|
+
# @param row [Object, Array] field values
|
|
100
|
+
|
|
101
|
+
def []=( id,row )
|
|
102
|
+
@data ||= id.is_a?(Integer) ? [] : {}
|
|
103
|
+
row = [row] unless row.is_a? Array
|
|
104
|
+
row = [id] + row if is_hash?
|
|
105
|
+
row.flatten! if is_hash?
|
|
106
|
+
@data[id] = row.map.with_index{ |field, column|
|
|
107
|
+
if column == @columns
|
|
108
|
+
@colors[:columns][column] ||= [:grey90,:grey60][column%2]
|
|
109
|
+
@format[column] ||= :min
|
|
110
|
+
@columns += 1
|
|
111
|
+
end
|
|
112
|
+
field = @functions[ :convert ].call field unless @ignore[column]
|
|
113
|
+
@align[column] ||= [Integer,Float].include?(field.class) ?
|
|
114
|
+
:right : :left
|
|
115
|
+
field
|
|
116
|
+
}
|
|
117
|
+
process id
|
|
118
|
+
filter id
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
# Set column headers. Symbols are auto-defined as +UPCASE_I+ constants.
|
|
123
|
+
#
|
|
124
|
+
# grid.header = [:name, :age]
|
|
125
|
+
# # defines NAME_0 = "name", AGE_1 = "age"
|
|
126
|
+
#
|
|
127
|
+
# @param h [Array<String, Symbol>]
|
|
128
|
+
|
|
129
|
+
def header=( h ); super
|
|
130
|
+
h.each_with_index{ |h,i| Typr.module_eval h.upcase+?=+i.to_s } if
|
|
131
|
+
h.is_a? Array
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
##
|
|
135
|
+
# Reset grid state.
|
|
136
|
+
#
|
|
137
|
+
# grid.reset :all # everything
|
|
138
|
+
# grid.reset :display # filters, sort, map
|
|
139
|
+
# grid.reset :format # widths, maxed
|
|
140
|
+
#
|
|
141
|
+
# @param type [:all, :display, :format, Array<Symbol>]
|
|
142
|
+
|
|
143
|
+
def reset type=:all
|
|
144
|
+
case type
|
|
145
|
+
when :display; @map = nil; @sort = nil; @filters.clear
|
|
146
|
+
when :format; @widths.clear; @maxed = false
|
|
147
|
+
when :all; reset [:display, :format]
|
|
148
|
+
end
|
|
149
|
+
super
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
##
|
|
153
|
+
# Clear all data, columns, and transforms.
|
|
154
|
+
#
|
|
155
|
+
# grid.clear
|
|
156
|
+
#
|
|
157
|
+
# @return [void]
|
|
158
|
+
|
|
159
|
+
def clear
|
|
160
|
+
@columns = 0; @layer = @map = @data = nil
|
|
161
|
+
reset [:format, :positions, :selection]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
##
|
|
165
|
+
# Apply column processors to a row's cells, storing results in +@layer+.
|
|
166
|
+
#
|
|
167
|
+
# grid.process(0)
|
|
168
|
+
#
|
|
169
|
+
# @param row [Integer] row id
|
|
170
|
+
|
|
171
|
+
def process row #=:all
|
|
172
|
+
@layer ||= @data.class.new
|
|
173
|
+
return if @procs.empty?
|
|
174
|
+
@layer[row] = @procs.map{ |col,proc|
|
|
175
|
+
proc = @functions[proc] if proc.is_a? Symbol
|
|
176
|
+
cell = @data[row][col]
|
|
177
|
+
next unless cell
|
|
178
|
+
[ col, proc.lambda? ? proc.call( cell ) : proc.call( cell, row, col ) ] unless
|
|
179
|
+
cell.is_a?(Proc) }.to_h
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
##
|
|
183
|
+
# Add a column filter (substring or regex).
|
|
184
|
+
#
|
|
185
|
+
# grid.add_filter(0, "Ali") # name contains "Ali"
|
|
186
|
+
# grid.add_filter(1, /^\d{2}$/) # age matches regex
|
|
187
|
+
#
|
|
188
|
+
# @param column [Integer, Symbol]
|
|
189
|
+
# @param query [String, Regexp]
|
|
190
|
+
|
|
191
|
+
def add_filter column=nil, query=nil
|
|
192
|
+
return unless column and query
|
|
193
|
+
@filters << [column, query]
|
|
194
|
+
filter :all
|
|
195
|
+
return
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
##
|
|
199
|
+
# Test whether a row passes all active filters.
|
|
200
|
+
#
|
|
201
|
+
# grid.check(0) #=> true
|
|
202
|
+
#
|
|
203
|
+
# @param row [Integer]
|
|
204
|
+
# @return [Boolean]
|
|
205
|
+
|
|
206
|
+
def check row
|
|
207
|
+
@filters.reject{ |column,query|
|
|
208
|
+
( (@procs[column] and not @rawfilter[column]) ?
|
|
209
|
+
@layer : @data )[row][column].to_s[query] }.empty?
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
##
|
|
213
|
+
# Apply or revise filters. +:all+ recomputes the full map; a row id adds/removes it.
|
|
214
|
+
#
|
|
215
|
+
# grid.filter :all
|
|
216
|
+
# grid.filter 0
|
|
217
|
+
#
|
|
218
|
+
# @param row [Integer, Symbol]
|
|
219
|
+
|
|
220
|
+
def filter row=:all
|
|
221
|
+
return if @filters.empty?
|
|
222
|
+
if row == :all
|
|
223
|
+
@map = ids.select{ |id| check id }
|
|
224
|
+
@start = 0
|
|
225
|
+
elsif check row then
|
|
226
|
+
@map ||= []
|
|
227
|
+
@map << row unless @map.include? row
|
|
228
|
+
else @map.delete(row) if @map end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
##
|
|
232
|
+
# Current sort column name (when called with arg) or index (without).
|
|
233
|
+
#
|
|
234
|
+
# grid.sort_by :age
|
|
235
|
+
# grid.sorted_by #=> "age"
|
|
236
|
+
# grid.sorted_by true #=> "age"
|
|
237
|
+
#
|
|
238
|
+
# @param name [Boolean] pass truthy to get display name
|
|
239
|
+
# @return [String, Integer, false, nil]
|
|
240
|
+
|
|
241
|
+
def sorted_by name=false; (name and @sort) ? @header[@sort] : @sort end
|
|
242
|
+
|
|
243
|
+
##
|
|
244
|
+
# Sort by column (toggle direction on repeat).
|
|
245
|
+
#
|
|
246
|
+
# grid.sort_by :age
|
|
247
|
+
# grid.sort_by :age # reverses
|
|
248
|
+
#
|
|
249
|
+
# @param column [Integer, Symbol]
|
|
250
|
+
|
|
251
|
+
def sort_by column
|
|
252
|
+
column = @header.index( column.to_s ) if column.is_a? Symbol
|
|
253
|
+
if @sort and (column == @sort) then @reverse = !@reverse
|
|
254
|
+
else @sort = column end
|
|
255
|
+
sort
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
##
|
|
259
|
+
# Execute the sort and update +@map+.
|
|
260
|
+
#
|
|
261
|
+
# @return [false, void]
|
|
262
|
+
|
|
263
|
+
def sort
|
|
264
|
+
return false unless @sort
|
|
265
|
+
data = (@procs[@sort] and not @rawsort[@sort]) ? @layer : @data
|
|
266
|
+
@map = ids.sort{ |a,b| (data[a][@sort] <=> data[b][@sort]) || 0 }
|
|
267
|
+
@map.reverse! if @reverse
|
|
268
|
+
return
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
##
|
|
272
|
+
# Pixel width for a single visual column (includes +:max+ bonus).
|
|
273
|
+
#
|
|
274
|
+
# grid.width_for(0) #=> 12
|
|
275
|
+
#
|
|
276
|
+
# @param col [Integer] visual column index
|
|
277
|
+
# @return [Integer]
|
|
278
|
+
|
|
279
|
+
def width_for col
|
|
280
|
+
@widths[col] + ( @format[sequence[col]] == :max ? @rest : 0 )
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
##
|
|
284
|
+
# X-offset for each visual column on a row.
|
|
285
|
+
#
|
|
286
|
+
# grid.positions #=> [0, 13, 25]
|
|
287
|
+
#
|
|
288
|
+
# @param row [Integer]
|
|
289
|
+
# @return [Array<Integer>]
|
|
290
|
+
|
|
291
|
+
def positions row=0; x=0
|
|
292
|
+
[0] + (sequence.count-1).times.map{ |id|
|
|
293
|
+
x += width_for(id) + @separator.size }
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
##
|
|
297
|
+
# Compute column widths from visible data and render the grid.
|
|
298
|
+
#
|
|
299
|
+
# grid.draw
|
|
300
|
+
#
|
|
301
|
+
# @return [void]
|
|
302
|
+
|
|
303
|
+
def draw
|
|
304
|
+
if not @data or @data.empty? or ( @map and @map.empty? )
|
|
305
|
+
@widths = [ width/columns ] * columns if @widths.count < columns
|
|
306
|
+
@rest = 0
|
|
307
|
+
return super
|
|
308
|
+
end
|
|
309
|
+
for row in page.compact
|
|
310
|
+
fields = @data[row].dup
|
|
311
|
+
fields = [ fields ] unless fields.is_a?(Array)
|
|
312
|
+
for col,id in sequence.each_with_index
|
|
313
|
+
field = fields[col]
|
|
314
|
+
if field.is_a? Proc
|
|
315
|
+
field = field.call
|
|
316
|
+
field = @procs[col].call field,row,col if @procs[col]
|
|
317
|
+
@layer[row] ||= {}
|
|
318
|
+
@layer[row][col] = field
|
|
319
|
+
end
|
|
320
|
+
field = @layer[row][col] if @procs[col]
|
|
321
|
+
if @format[col].is_a? Integer
|
|
322
|
+
@widths[id] ||= @format[col]
|
|
323
|
+
else
|
|
324
|
+
@widths[id] ||= 0
|
|
325
|
+
size = real_size( field.to_s )
|
|
326
|
+
@widths[id] = size if size > @widths[id]
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
gaps = @margin.size + @separator.size * (columns - 1)
|
|
331
|
+
sum = @widths.sum
|
|
332
|
+
@max = sum + gaps - 1
|
|
333
|
+
space = width - gaps
|
|
334
|
+
if (extra = sum - space ) > 0
|
|
335
|
+
extra.times{ |i| @widths[ @widths.index(@widths.max) ] -= 1 }
|
|
336
|
+
@rest = 0
|
|
337
|
+
else
|
|
338
|
+
format = @format.values_at( *sequence )
|
|
339
|
+
@rest = space - sum / format.count(:max) if format.include? :max
|
|
340
|
+
end
|
|
341
|
+
super
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
##
|
|
345
|
+
# Render a single row or the header bar to the terminal.
|
|
346
|
+
#
|
|
347
|
+
# grid.print :header
|
|
348
|
+
# grid.print 0
|
|
349
|
+
#
|
|
350
|
+
# @param row [Integer, Symbol] row id, or +:header+
|
|
351
|
+
|
|
352
|
+
def print row=nil
|
|
353
|
+
return super unless row
|
|
354
|
+
header = ( row == :header )
|
|
355
|
+
return show @header[0..width-1].ljust(width) if
|
|
356
|
+
header and @header.is_a? String
|
|
357
|
+
fields = (header ? @header : @data[row] ).dup
|
|
358
|
+
@layer[row].each{ |col,value| fields[col] = value } if
|
|
359
|
+
@layer[row] if @layer unless header
|
|
360
|
+
for col,id in sequence.each_with_index
|
|
361
|
+
show @separator unless id == 0
|
|
362
|
+
select = ( @selected[:columns].include? col or
|
|
363
|
+
@selected[:fields].include? [row,col] ) unless
|
|
364
|
+
@selected[:rows].include?(row) or header
|
|
365
|
+
color( @colors[:fields][[row,col]] ||
|
|
366
|
+
@colors[:columns][col] || @colors[:default] ) unless header
|
|
367
|
+
background @colors[:selected] if select
|
|
368
|
+
show prepare( fields[col].to_s, width_for( id ), @align[col] )
|
|
369
|
+
background @colors[:default][1] if select
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
##
|
|
374
|
+
# Append multiple rows at once. Returns +self+ for chaining.
|
|
375
|
+
#
|
|
376
|
+
# grid << [["Dave", 28], ["Eve", 32]]
|
|
377
|
+
# grid << { alice: [30, "eng"] }
|
|
378
|
+
#
|
|
379
|
+
# @param data [Enumerable<Array>, Enumerable<Hash>]
|
|
380
|
+
# @return [self]
|
|
381
|
+
|
|
382
|
+
def << (data)
|
|
383
|
+
@data ||= data.class.new
|
|
384
|
+
skip = @data.size
|
|
385
|
+
data.each_with_index{ |row, id|
|
|
386
|
+
self[ is_hash? ? row.shift : id + skip ] = row }
|
|
387
|
+
sort
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
##
|
|
391
|
+
# Construct a Grid.
|
|
392
|
+
#
|
|
393
|
+
# grid = Typr::Grid.new(
|
|
394
|
+
# input: [ ["Alice", 30], ["Bob", 25] ],
|
|
395
|
+
# header: [:name, :age],
|
|
396
|
+
# format: [:max, :min],
|
|
397
|
+
# procs: { 1 => :magnitudes },
|
|
398
|
+
# colors: { columns: [:cyan, nil] }
|
|
399
|
+
# )
|
|
400
|
+
#
|
|
401
|
+
# @param args [Hash] +input+, +header+, +format+, +procs+, +colors+, +align+, +functions+
|
|
402
|
+
|
|
403
|
+
def initialize args={}
|
|
404
|
+
@columns,@functions = 0,{}
|
|
405
|
+
@ignore,@rawfilter,@rawsort,@reverse = [],[],[],false
|
|
406
|
+
@filters,@procs = [],{}
|
|
407
|
+
@align,@format,@widths,@totals = [],[],[],[]
|
|
408
|
+
super #args
|
|
409
|
+
@colors = { columns: [], fields: {} }.merge ( @colors )
|
|
410
|
+
@selected = { fields:[], columns:[] }.merge @selected
|
|
411
|
+
@functions = {
|
|
412
|
+
filetree: Proc.new{ |f| f.gsub /.*\/[^$]/, ' ' },
|
|
413
|
+
datetime: Proc.new{|sec|Time.at(sec).strftime"%y-%m-%d %H:%M" rescue ??},
|
|
414
|
+
magnitudes: Proc.new{ |size| mag = (size.to_s.length-1) / 3
|
|
415
|
+
mag>0 ? (size.to_s.insert -(mag*3+1), ?.)[0..4] + %w[B K M G T][mag] :
|
|
416
|
+
size rescue size },
|
|
417
|
+
convert: Proc.new{ |value|
|
|
418
|
+
case value.to_s
|
|
419
|
+
when /^[\d]+$/ then value.to_s.to_i
|
|
420
|
+
when /^\d*[\.\,]\d+$/ then value.to_s.to_f
|
|
421
|
+
else value
|
|
422
|
+
end } }
|
|
423
|
+
|
|
424
|
+
if @input
|
|
425
|
+
self << @input
|
|
426
|
+
@input = nil
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
end
|
data/lib/line.rb
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require_relative 'space'
|
|
2
|
+
|
|
3
|
+
module Typr
|
|
4
|
+
|
|
5
|
+
# A single-line UI element for displaying text and collecting input.
|
|
6
|
+
#
|
|
7
|
+
# == Example
|
|
8
|
+
#
|
|
9
|
+
# line = Line.new( top: 5, left: 10, width: 40 )
|
|
10
|
+
# line.draw "Hello world"
|
|
11
|
+
# line.append " more"
|
|
12
|
+
# line.ask([ ["Name?", :string], ["Age?", :integer] ])
|
|
13
|
+
|
|
14
|
+
class Line < Space
|
|
15
|
+
attr_accessor :default, :prompt, :bindings, :data
|
|
16
|
+
|
|
17
|
+
# Render +msg+ (String, Proc, or Space) on this line.
|
|
18
|
+
#
|
|
19
|
+
# line.draw "status: ok"
|
|
20
|
+
# line.draw Proc.new { show_time }
|
|
21
|
+
|
|
22
|
+
def draw msg=@data
|
|
23
|
+
@data = msg
|
|
24
|
+
move left, top
|
|
25
|
+
color @colors[:default]
|
|
26
|
+
case msg
|
|
27
|
+
when Proc; msg.call
|
|
28
|
+
when Space; msg.draw
|
|
29
|
+
when String; show( prepare msg, width )
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Append +str+ to the current content and redraw.
|
|
34
|
+
#
|
|
35
|
+
# line.draw "hello"
|
|
36
|
+
# line.append " world" #=> "hello world"
|
|
37
|
+
# line << "!" #=> "hello world!"
|
|
38
|
+
|
|
39
|
+
def append(str); draw @data+str end
|
|
40
|
+
alias :<< :append
|
|
41
|
+
|
|
42
|
+
# Display keybinding help in a floating grid.
|
|
43
|
+
#
|
|
44
|
+
# line.bindings = ["Ctrl-S: Save", "Ctrl-Q: Quit"]
|
|
45
|
+
# line.help
|
|
46
|
+
|
|
47
|
+
def help conf={}
|
|
48
|
+
Grid.new( { input: ['']+@bindings+[''], top: 3, left:0.4,
|
|
49
|
+
header: "KEYBINDINGS",
|
|
50
|
+
colors: { header: [:white,:black], border:[:white,:grey10] },
|
|
51
|
+
alternate: false, border: ' ' }.merge conf ).pick :none
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Reset the line to its default text or prompt.
|
|
56
|
+
#
|
|
57
|
+
# line.reset
|
|
58
|
+
|
|
59
|
+
def reset; draw( @default || ( @bindings || [@prompt] ).join(' ') ) end
|
|
60
|
+
|
|
61
|
+
# Prompt the user with a sequence of questions.
|
|
62
|
+
# +sentence+ is an array of [question, type] pairs.
|
|
63
|
+
# Returns a single answer or an array when multiple questions.
|
|
64
|
+
#
|
|
65
|
+
# line.ask([ ["Name?", :string] ]) #=> "Alice"
|
|
66
|
+
# line.ask([ ["Name?", :string], ["Ok?", ["y","n"]] ]) #=> ["Alice", "y"]
|
|
67
|
+
|
|
68
|
+
def ask sentence
|
|
69
|
+
answer = []
|
|
70
|
+
move left, top
|
|
71
|
+
Typr.clear :line
|
|
72
|
+
draw @prompt
|
|
73
|
+
for question, object in sentence
|
|
74
|
+
color( @colors[:question] )
|
|
75
|
+
append " " + question.to_s + " "
|
|
76
|
+
column = Typr.column
|
|
77
|
+
color( @colors[:answer] )
|
|
78
|
+
case object
|
|
79
|
+
when Array then answer << object.first.pick( object.last )
|
|
80
|
+
else answer << Typr.read( object, @data )
|
|
81
|
+
end
|
|
82
|
+
return if !answer.last or answer.last == @keymap[:exit]
|
|
83
|
+
move column, top
|
|
84
|
+
append answer.last.to_s
|
|
85
|
+
end
|
|
86
|
+
return ( answer.one? ? answer.first : answer )
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def initialize args
|
|
90
|
+
@right= -1
|
|
91
|
+
@prompt = ?>
|
|
92
|
+
super
|
|
93
|
+
@bottom = @top
|
|
94
|
+
@colors = { question: :green, answer: :yellow }.merge @colors
|
|
95
|
+
reset
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
end
|