typr 1.1.4 → 1.2.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 +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +9 -9
- data/example/grid +4 -4
- data/example/text +7 -4
- data/lib/browser.rb +87 -66
- data/lib/curses.rb +11 -7
- data/lib/frontend.rb +7 -3
- data/lib/graphical.rb +10 -13
- data/lib/grid.rb +69 -14
- data/lib/line.rb +57 -27
- data/lib/space.rb +14 -5
- data/lib/stack.rb +9 -9
- data/lib/terminal.rb +185 -20
- data/lib/text.rb +122 -7
- data/lib/typr.rb +5 -6
- data/share/mimetypes +1298 -1
- data/typr.gemspec +2 -2
- metadata +2 -7
- data/Gemfile +0 -10
data/lib/terminal.rb
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
require 'io/console'
|
|
2
|
-
require 'readline'
|
|
3
2
|
require 'unicode/display_width'
|
|
4
3
|
|
|
4
|
+
## Terminal control: raw key input, line editing, cursor movement, ANSI colors
|
|
5
|
+
# and terminal geometry. Widgets get the instance helpers via `include Typr`;
|
|
6
|
+
# class-level helpers (Typr.width, Typr.read_key, ...) drive the whole screen.
|
|
7
|
+
|
|
5
8
|
module Typr
|
|
6
9
|
|
|
10
|
+
# Raw /dev/tty used for key input; falls back to $stdin when unavailable.
|
|
7
11
|
INPUT = (IO.new IO.sysopen("/dev/tty", "r")) rescue $stdin
|
|
12
|
+
|
|
13
|
+
# KEY_* escape sequences come from the terminfo database (infocmp -L1).
|
|
8
14
|
`infocmp -L1`.split.each{ |info| key,value = info[0..-2].split("=")
|
|
9
15
|
if value
|
|
10
16
|
value.gsub! '\E', '\e'
|
|
@@ -12,15 +18,27 @@ module Typr
|
|
|
12
18
|
eval "%s=\"%s\"" % [key.upcase,value] if key
|
|
13
19
|
end }
|
|
14
20
|
|
|
21
|
+
# Common key aliases and the line-erase escape sequence.
|
|
22
|
+
ERASE_LINE = "\e[K"
|
|
15
23
|
KEY_ESCAPE = "\e"
|
|
16
24
|
KEY_RETURN = CARRIAGE_RETURN
|
|
17
25
|
KEY_TAB = "\t"
|
|
18
26
|
KEY_PAGEDOWN = KEY_NPAGE
|
|
19
27
|
KEY_PAGEUP = KEY_PPAGE
|
|
20
28
|
|
|
29
|
+
# Text attributes (reset, bold, italic, ...) and named 8/256-color tables.
|
|
21
30
|
MODES = %i[ reset bold italic underline slow fast invert ]
|
|
22
31
|
COLORS = %i[ black red green yellow blue magenta cyan white ]
|
|
32
|
+
COLOR_MAP = {
|
|
33
|
+
brown: 130, orange: 208, lime: 118, pink: 218,
|
|
34
|
+
maroon: 52, navy: 18, teal: 30, olive: 100,
|
|
35
|
+
coral: 203, tan: 180,
|
|
36
|
+
dark_red: 88, dark_green: 22, dark_yellow: 58,
|
|
37
|
+
dark_blue: 18, dark_magenta: 89, dark_cyan: 30
|
|
38
|
+
}
|
|
23
39
|
|
|
40
|
+
# Pad or clip +str+ to exactly +max+ cells, honoring +align+ (:left/:right),
|
|
41
|
+
# trimming from +side+, with an optional trailing +fade+.
|
|
24
42
|
def prepare str, max, align=:left, side=:right, fade=false
|
|
25
43
|
stop = width = real_size( str )
|
|
26
44
|
unless str.ascii_only? and width == str.size
|
|
@@ -30,7 +48,7 @@ module Typr
|
|
|
30
48
|
width -= str[stop..-1][/^\x1b\[[^m]+m|/].size-1
|
|
31
49
|
stop += 1
|
|
32
50
|
elsif (cwidth = (c.ascii_only? ? 1 : Unicode::DisplayWidth.of(c))) +
|
|
33
|
-
width
|
|
51
|
+
width > max
|
|
34
52
|
break
|
|
35
53
|
else width += cwidth; stop += 1 end
|
|
36
54
|
end
|
|
@@ -40,12 +58,55 @@ module Typr
|
|
|
40
58
|
str.reverse! if align == :right
|
|
41
59
|
str.join
|
|
42
60
|
else
|
|
43
|
-
str = str
|
|
61
|
+
str = clip str, max, side
|
|
44
62
|
str = fade str, side, fade if fade
|
|
45
63
|
return str
|
|
46
64
|
end
|
|
47
65
|
end
|
|
48
66
|
|
|
67
|
+
# Truncate +str+ to at most +max+ cells, keeping ANSI color tokens intact
|
|
68
|
+
# and clipping from +side+ (:left/:right).
|
|
69
|
+
def clip str, max, side
|
|
70
|
+
tokens = str.scan(/(\e\[[0-9;]*m)|([^\e]+)/).flatten.compact
|
|
71
|
+
tokens.reverse! if side == :left
|
|
72
|
+
kept, used = [], 0
|
|
73
|
+
tokens.each do |token|
|
|
74
|
+
if token[0] == ?\e
|
|
75
|
+
if side == :left
|
|
76
|
+
kept << token unless kept.empty?
|
|
77
|
+
break if used >= max
|
|
78
|
+
else
|
|
79
|
+
kept << token if used < max
|
|
80
|
+
end
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
break if used >= max
|
|
84
|
+
size = Unicode::DisplayWidth.of token
|
|
85
|
+
if used + size <= max
|
|
86
|
+
kept << token
|
|
87
|
+
used += size
|
|
88
|
+
elsif used < max
|
|
89
|
+
need = max - used
|
|
90
|
+
chars = token.chars
|
|
91
|
+
chars.reverse! if side == :left
|
|
92
|
+
width = 0
|
|
93
|
+
out = ""
|
|
94
|
+
chars.each do |char|
|
|
95
|
+
width += Unicode::DisplayWidth.of char
|
|
96
|
+
break if width > need
|
|
97
|
+
out << char
|
|
98
|
+
end
|
|
99
|
+
out.reverse! if side == :left
|
|
100
|
+
kept << out
|
|
101
|
+
used = max
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
kept.reverse!.join if side == :left
|
|
105
|
+
kept.join
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Blend the first (or last) +num+ characters of +str+ into greys to hint
|
|
109
|
+
# at clipping; +side+ selects which end fades.
|
|
49
110
|
def fade str, side=:left, num=3
|
|
50
111
|
isleft = side == :left
|
|
51
112
|
chars = str[ isleft ? 0..num : -num..-1 ]
|
|
@@ -57,9 +118,14 @@ module Typr
|
|
|
57
118
|
str[0..-num-1] + chars )
|
|
58
119
|
end
|
|
59
120
|
|
|
121
|
+
# Strip ANSI escape sequences from +str+.
|
|
60
122
|
def printables str; str.gsub(/\x1b\[[^m]+m|/,'') end
|
|
123
|
+
|
|
124
|
+
# Display width of +str+ after stripping ANSI escapes.
|
|
61
125
|
def real_size str; Unicode::DisplayWidth.of( printables(str) ) end
|
|
62
126
|
|
|
127
|
+
# Coerce a string into Integer, Float, or Boolean when it matches those
|
|
128
|
+
# forms (yes/no, true/false); otherwise return it unchanged.
|
|
63
129
|
def coerce_type value
|
|
64
130
|
s = value.to_s
|
|
65
131
|
return nil if s.empty? or /\A(?:nil|NULL)\z/i.match(s)
|
|
@@ -74,12 +140,19 @@ module Typr
|
|
|
74
140
|
end
|
|
75
141
|
end
|
|
76
142
|
|
|
77
|
-
|
|
143
|
+
# Move the cursor to column +x+, row +y+ (0-based); move_code returns the
|
|
144
|
+
# escape sequence without writing it.
|
|
145
|
+
def move x=0,y=0; draw move_code( x, y ) end
|
|
78
146
|
def move_code x=0,y=0; "\e[%i;%if" % [ y+1, x+1 ] end
|
|
79
147
|
|
|
80
|
-
|
|
148
|
+
# Apply a text attribute (+mode+); mode_code returns the escape sequence
|
|
149
|
+
# without writing it.
|
|
150
|
+
def mode name; draw mode_code(name) end
|
|
81
151
|
def mode_code name; if id = MODES.index(name.to_s) then "\e[#{ id }m" end end
|
|
82
152
|
|
|
153
|
+
# Build the ANSI code for +color+ (a Symbol name, greyN, Integer palette id,
|
|
154
|
+
# or [r,g,b] / [fg,bg] Array), optionally as a background; when false the
|
|
155
|
+
# color is emitted as a foreground.
|
|
83
156
|
def color_code color, bg=false
|
|
84
157
|
color = color.to_sym if color.is_a? String
|
|
85
158
|
case color
|
|
@@ -87,6 +160,8 @@ module Typr
|
|
|
87
160
|
if id = COLORS.index(color); "\e[#{ id + (bg ? 40 : 30) }m"
|
|
88
161
|
elsif color[/^gr[ae]y\d{,2}$/]
|
|
89
162
|
"\e[%i;5;%im" % [bg ? 48 : 38, 232 + (color[/\d+/].to_f/100*23).to_i]
|
|
163
|
+
elsif id = COLOR_MAP[color]
|
|
164
|
+
"\e[%i;5;%im" % [bg ? 48 : 38, id]
|
|
90
165
|
end
|
|
91
166
|
when Integer; "\e[%i;5;%im" % [ bg ? 48 : 38, color]
|
|
92
167
|
when Array; case color.count
|
|
@@ -96,31 +171,111 @@ module Typr
|
|
|
96
171
|
end
|
|
97
172
|
end
|
|
98
173
|
|
|
174
|
+
# Read the current background/foreground color pair.
|
|
99
175
|
def get_background; $color[1] end
|
|
100
176
|
def get_foreground; $color[0] end
|
|
101
|
-
|
|
102
|
-
|
|
177
|
+
|
|
178
|
+
# Set and apply the foreground/background color; +color+ sets both from a
|
|
179
|
+
# [fg, bg] pair, a single color, or the module default.
|
|
180
|
+
def foreground c=$default[0]; $color[0]=c; draw color_code(c) end
|
|
181
|
+
def background c=$default[1]; $color[1]=c; draw color_code(c,true) end
|
|
103
182
|
def color c=$default
|
|
104
183
|
c = [c] unless c.is_a? Array and c.count == 2
|
|
105
184
|
foreground c[0] if c[0]
|
|
106
185
|
background c[1] if c[1]
|
|
107
186
|
end
|
|
108
187
|
|
|
109
|
-
|
|
188
|
+
# Write raw +str+ to stdout.
|
|
189
|
+
def draw str; $>.print str end
|
|
190
|
+
|
|
191
|
+
# Clear the whole screen (:screen) or just the current line (:line).
|
|
110
192
|
def self.clear mode=:screen
|
|
111
|
-
$>.print( { screen: CLEAR_SCREEN, line:
|
|
193
|
+
$>.print( { screen: CLEAR_SCREEN, line: ERASE_LINE }[mode] )
|
|
112
194
|
end
|
|
113
195
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
196
|
+
## Reads a single keypress in raw mode.
|
|
197
|
+
#
|
|
198
|
+
# Returns the key sequence as a String (e.g. "a" or "\e[A" for up-arrow),
|
|
199
|
+
# or nil when stdin is not a tty or input is unavailable.
|
|
200
|
+
|
|
201
|
+
def self.read_key
|
|
202
|
+
return INPUT.raw{ |tty| tty.sysread 6 } rescue nil unless INPUT.tty?
|
|
203
|
+
INPUT.raw{ |tty| tty.sysread 6 } rescue return
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
## Interactive line editor used for string prompts (search, %str, ...).
|
|
207
|
+
#
|
|
208
|
+
# Renders +prompt+ and the query at (left, top) and edits it with
|
|
209
|
+
# arrow keys, ctrl-arrow word jumps, home/end, delete and backspace.
|
|
210
|
+
# Returns the query on enter, nil on escape. A block may inspect each
|
|
211
|
+
# keypress after it is applied; a non-nil return short-circuits the
|
|
212
|
+
# editor and becomes its return value. When stdin is not a tty, the
|
|
213
|
+
# query is read from a single line of piped input instead.
|
|
214
|
+
#
|
|
215
|
+
# Typr.read_line "/" do |key, query, cursor|
|
|
216
|
+
# filter query
|
|
217
|
+
# nil
|
|
218
|
+
# end
|
|
219
|
+
|
|
220
|
+
def self.read_line prompt='', left: 0, top: 0, initial: '', &block
|
|
221
|
+
return $stdin.gets&.chomp unless $stdin.tty?
|
|
222
|
+
query, cursor = initial.dup, initial.length
|
|
223
|
+
loop do
|
|
224
|
+
$>.print "\e[%i;%if" % [ top + 1, left + 1 ]
|
|
225
|
+
$>.print ERASE_LINE
|
|
226
|
+
$>.print prompt + query
|
|
227
|
+
$>.print "\e[%i;%if" % [ top + 1, left + 1 +
|
|
228
|
+
text_width( prompt + query[0...cursor] ) ]
|
|
229
|
+
key = read_key
|
|
230
|
+
return nil if key.nil? or key == KEY_ESCAPE
|
|
231
|
+
return query if key == KEY_RETURN or key == "\n"
|
|
232
|
+
case key
|
|
233
|
+
when KEY_LEFT, "\e[D"; cursor -= 1 if cursor > 0
|
|
234
|
+
when KEY_RIGHT, "\e[C"; cursor += 1 if cursor < query.length
|
|
235
|
+
when "\e[1;5D", "\eOd"; cursor = word_prev query, cursor
|
|
236
|
+
when "\e[1;5C", "\eOc"; cursor = word_next query, cursor
|
|
237
|
+
when KEY_HOME; cursor = 0
|
|
238
|
+
when KEY_END; cursor = query.length
|
|
239
|
+
when KEY_DC, "\e[3~"; query.slice!(cursor, 1) if cursor < query.length
|
|
240
|
+
when KEY_BACKSPACE, "\b"
|
|
241
|
+
if cursor > 0
|
|
242
|
+
query.slice!(cursor - 1, 1)
|
|
243
|
+
cursor -= 1
|
|
244
|
+
end
|
|
245
|
+
else
|
|
246
|
+
if key.is_a?(String) and key.each_char.all?{ |char| char.ord.between?(32, 126) }
|
|
247
|
+
query.insert(cursor, key)
|
|
248
|
+
cursor += key.length
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
cursor = 0 if cursor < 0
|
|
252
|
+
cursor = query.length if cursor > query.length
|
|
253
|
+
result = block.call( key, query, cursor ) if block
|
|
254
|
+
return result unless result.nil?
|
|
121
255
|
end
|
|
122
256
|
end
|
|
123
257
|
|
|
258
|
+
# Display width of +str+ after stripping ANSI escapes.
|
|
259
|
+
def self.text_width str
|
|
260
|
+
Unicode::DisplayWidth.of( str.gsub(/\x1b\[[^m]+m/, '') )
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Move the cursor back to the start of the previous word in +str+.
|
|
264
|
+
def self.word_prev str, cursor
|
|
265
|
+
cursor -= 1 while cursor > 0 and str[cursor - 1] == ' '
|
|
266
|
+
cursor -= 1 while cursor > 0 and str[cursor - 1] != ' '
|
|
267
|
+
cursor
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Move the cursor forward to the start of the next word in +str+.
|
|
271
|
+
def self.word_next str, cursor
|
|
272
|
+
cursor += 1 while cursor < str.length and str[cursor] == ' '
|
|
273
|
+
cursor += 1 while cursor < str.length and str[cursor] != ' '
|
|
274
|
+
cursor
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Query the terminal for the current [row, column] via DSR; [0, 0] when
|
|
278
|
+
# stdin is not a tty.
|
|
124
279
|
def self.position
|
|
125
280
|
return [0, 0] unless $stdin.tty?
|
|
126
281
|
result = ''
|
|
@@ -134,21 +289,31 @@ module Typr
|
|
|
134
289
|
result[/[\d;]+/].split(?;).map &:to_i
|
|
135
290
|
end
|
|
136
291
|
|
|
292
|
+
# Terminal [rows, cols] (defaults to 24x80 when undetectable); width/height
|
|
293
|
+
# return the usable last column/row.
|
|
137
294
|
def self.size; IO.console&.winsize || [24, 80] end
|
|
138
295
|
def self.width; size.last-1 end
|
|
139
296
|
def self.height; size.first - 1 end
|
|
297
|
+
|
|
298
|
+
# Current cursor row/column (see position).
|
|
140
299
|
def self.row; position.first end
|
|
141
300
|
def self.column; position.last end
|
|
301
|
+
|
|
302
|
+
# Call +block+ whenever the terminal is resized (SIGWINCH).
|
|
142
303
|
def self.on_resize &block
|
|
143
304
|
trap(:WINCH, &block)
|
|
144
305
|
end
|
|
306
|
+
# Enter interactive mode: seed the default colors, hide the cursor and
|
|
307
|
+
# enable key-mode (application) escapes when stdin is a tty.
|
|
145
308
|
def self.init default=[ :white, :black ]
|
|
146
|
-
$
|
|
309
|
+
$default = default.dup
|
|
310
|
+
$color = $default.dup
|
|
147
311
|
if $stdin.tty?
|
|
148
312
|
print CURSOR_INVISIBLE
|
|
149
313
|
system "tput smkx"
|
|
150
314
|
end
|
|
151
315
|
end
|
|
316
|
+
# Restore the terminal: show the cursor, reset colors and clear the screen.
|
|
152
317
|
def self.exit;
|
|
153
318
|
extend self
|
|
154
319
|
if $stdin.tty?
|
|
@@ -174,10 +339,10 @@ begin
|
|
|
174
339
|
foreground color.to_sym;
|
|
175
340
|
background colors[x].to_sym;
|
|
176
341
|
move x*slice,y
|
|
177
|
-
|
|
342
|
+
draw chars
|
|
178
343
|
}
|
|
179
344
|
}
|
|
180
|
-
(0...256).each{ |i| background i;
|
|
345
|
+
(0...256).each{ |i| background i; draw ' ' }
|
|
181
346
|
|
|
182
347
|
move 0, Typr.height
|
|
183
348
|
color [:white, :black]
|
|
@@ -185,7 +350,7 @@ begin
|
|
|
185
350
|
move 15, Typr.height
|
|
186
351
|
color [fg,bg]
|
|
187
352
|
print " %s / %s " % [fg,bg]
|
|
188
|
-
key = Typr.
|
|
353
|
+
key = Typr.read_key
|
|
189
354
|
case key
|
|
190
355
|
when KEY_UP; fg+=1
|
|
191
356
|
when KEY_DOWN; fg-=1
|
data/lib/text.rb
CHANGED
|
@@ -4,7 +4,7 @@ require_relative 'stack.rb'
|
|
|
4
4
|
module Typr
|
|
5
5
|
|
|
6
6
|
# A vertically scrollable text viewer with word-wrapping, header bar,
|
|
7
|
-
# row selection via hint characters, and search.
|
|
7
|
+
# row selection via hint characters, and pager-style search (/ n p).
|
|
8
8
|
#
|
|
9
9
|
# Accepts String, IO, or StringIO input. Content is split into display
|
|
10
10
|
# rows by newlines and line width, then rendered as a sliding window.
|
|
@@ -17,8 +17,8 @@ module Typr
|
|
|
17
17
|
# top: 2, left: 0.1, right: 0.9, bottom: -4,
|
|
18
18
|
# border: :round,
|
|
19
19
|
# colors: { header: [:yellow, :grey30] }
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
# )
|
|
21
|
+
# text.show
|
|
22
22
|
|
|
23
23
|
class Text < Stack
|
|
24
24
|
|
|
@@ -68,6 +68,7 @@ module Typr
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
@lines << @data.length unless @lines.last == @data.length
|
|
71
|
+
highlight if @re
|
|
71
72
|
end
|
|
72
73
|
alias :<< :build
|
|
73
74
|
|
|
@@ -80,7 +81,14 @@ module Typr
|
|
|
80
81
|
def print row=nil
|
|
81
82
|
header = ( row == :header )
|
|
82
83
|
return super unless row and @lines[ row + 1 ] unless header
|
|
83
|
-
|
|
84
|
+
text = ( header ? @header : @page[ row-@start ] )
|
|
85
|
+
if row.is_a?(Integer) and @search[:matches].include? row
|
|
86
|
+
background @colors[:search]
|
|
87
|
+
draw prepare( text, width )
|
|
88
|
+
background
|
|
89
|
+
else
|
|
90
|
+
draw prepare( text, width )
|
|
91
|
+
end
|
|
84
92
|
end
|
|
85
93
|
|
|
86
94
|
# Returns word-boundary column offsets for a given page row.
|
|
@@ -95,9 +103,9 @@ module Typr
|
|
|
95
103
|
# Full draw cycle: rebuilds on resize, computes visible lines,
|
|
96
104
|
# and renders the viewport with borders and hints.
|
|
97
105
|
#
|
|
98
|
-
# text.
|
|
106
|
+
# text.show
|
|
99
107
|
|
|
100
|
-
def
|
|
108
|
+
def show
|
|
101
109
|
(build; @lastwidth = width) if width != @lastwidth
|
|
102
110
|
range = @lines[@start..@start+height]
|
|
103
111
|
page = StringIO.new @data[range.first..range.last]
|
|
@@ -111,6 +119,62 @@ module Typr
|
|
|
111
119
|
|
|
112
120
|
def rows; @lines.count end
|
|
113
121
|
|
|
122
|
+
##
|
|
123
|
+
# Pager-style search over the buffer, modeled after less/vim.
|
|
124
|
+
#
|
|
125
|
+
# With a +query+ (String, matched literally, or Regexp) it stores the
|
|
126
|
+
# pattern, highlights every matching line, and scrolls to the first
|
|
127
|
+
# match from the current position. +dir+ is +:forward+ or +:backward+.
|
|
128
|
+
# Returns the matched line id, or nil when nothing matches.
|
|
129
|
+
#
|
|
130
|
+
# With no argument it prompts interactively for a pattern at the bottom
|
|
131
|
+
# of the screen (pressing Return searches, Escape aborts).
|
|
132
|
+
#
|
|
133
|
+
# Matching is smart-case: lowercase queries are case-insensitive;
|
|
134
|
+
# a query containing an uppercase letter is case-sensitive.
|
|
135
|
+
#
|
|
136
|
+
# text.search "needle" # => 12 (or nil)
|
|
137
|
+
# text.search /^TODO/ # => 3
|
|
138
|
+
# text.search "needle", :backward # search upward
|
|
139
|
+
|
|
140
|
+
def search query=nil, dir=:forward
|
|
141
|
+
return search_prompt if query.nil?
|
|
142
|
+
@search[:pattern] = query
|
|
143
|
+
@search[:dir] = dir
|
|
144
|
+
@search[:last] = nil
|
|
145
|
+
@re = query.is_a?(Regexp) ? query :
|
|
146
|
+
Regexp.new( Regexp.escape( query ),
|
|
147
|
+
( query[/[A-Z]/] ? 0 : Regexp::IGNORECASE ) )
|
|
148
|
+
highlight
|
|
149
|
+
step
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
## Jump to the next match of the last search (wrap-around). Returns its line id or nil.
|
|
153
|
+
|
|
154
|
+
def search_next; @search[:dir] = :forward; step end
|
|
155
|
+
|
|
156
|
+
## Jump to the previous match of the last search (wrap-around). Returns its line id or nil.
|
|
157
|
+
|
|
158
|
+
def search_prev; @search[:dir] = :backward; step end
|
|
159
|
+
|
|
160
|
+
## Recompute the set of lines containing the current pattern.
|
|
161
|
+
|
|
162
|
+
def highlight
|
|
163
|
+
@search[:matches] = @re ?
|
|
164
|
+
@lines[0...-1].each_index.select{ |i|
|
|
165
|
+
@data[ @lines[i] ... @lines[i+1] ][ @re ] } : []
|
|
166
|
+
return
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
## Clear the search pattern and all match highlights.
|
|
170
|
+
|
|
171
|
+
def highlight_clear
|
|
172
|
+
@re = @search[:pattern] = nil
|
|
173
|
+
@search[:matches] = []
|
|
174
|
+
@search[:last] = nil
|
|
175
|
+
return
|
|
176
|
+
end
|
|
177
|
+
|
|
114
178
|
# Creates a new Text widget.
|
|
115
179
|
#
|
|
116
180
|
# text = Typr::Text.new(input: "hello", header: "Title")
|
|
@@ -119,11 +183,62 @@ module Typr
|
|
|
119
183
|
def initialize args={}
|
|
120
184
|
@tabspace = 2
|
|
121
185
|
@data, @lines, @tail = "", [0], ""
|
|
186
|
+
@search = { pattern: nil, dir: :forward, last: nil, matches: [] }
|
|
122
187
|
super args
|
|
123
|
-
@colors = { header: :yellow }.merge( @colors || {} )
|
|
188
|
+
@colors = { header: :yellow, search: :grey20 }.merge( @colors || {} )
|
|
189
|
+
@keymap = { search: ?/, search_next: ?n, search_prev: ?p }.merge @keymap
|
|
124
190
|
@lastwidth = width
|
|
125
191
|
self << @input
|
|
126
192
|
end
|
|
193
|
+
|
|
194
|
+
def reset type=:all
|
|
195
|
+
case type
|
|
196
|
+
when :search; highlight_clear
|
|
197
|
+
when :all; reset [:position, :selection, :search]; return
|
|
198
|
+
end
|
|
199
|
+
super
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
def live_search query
|
|
205
|
+
query.empty? ? highlight_clear : search(query)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def step
|
|
209
|
+
matches = @search[:matches]
|
|
210
|
+
return nil if matches.empty?
|
|
211
|
+
last = @search[:last]
|
|
212
|
+
if @search[:dir] == :backward
|
|
213
|
+
id = matches.reverse.find{ |m| last and m < last }
|
|
214
|
+
id ||= ( last ? matches.last : matches.reverse.find{ |m| m <= @start } || matches.last )
|
|
215
|
+
else
|
|
216
|
+
id = matches.find{ |m| last and m > last }
|
|
217
|
+
id ||= ( last ? matches.first : matches.find{ |m| m >= @start } || matches.first )
|
|
218
|
+
end
|
|
219
|
+
@search[:last] = id
|
|
220
|
+
@start = id
|
|
221
|
+
return id
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def search_prompt
|
|
225
|
+
query = @search[:pattern].is_a?(String) ? @search[:pattern].dup : ''
|
|
226
|
+
saved = { re: @re, pattern: @search[:pattern],
|
|
227
|
+
matches: @search[:matches].dup, last: @search[:last], start: @start }
|
|
228
|
+
result = Typr.read_line '/', left: left, top: Typr.height - 1, initial: query,
|
|
229
|
+
&->(key, query, _) {
|
|
230
|
+
live_search query
|
|
231
|
+
nil
|
|
232
|
+
}
|
|
233
|
+
if result
|
|
234
|
+
return search(result) unless result.empty?
|
|
235
|
+
return search(@search[:pattern]) if @search[:pattern]
|
|
236
|
+
end
|
|
237
|
+
@re = saved[:re]; @search[:pattern] = saved[:pattern]
|
|
238
|
+
@search[:matches] = saved[:matches]; @search[:last] = saved[:last]
|
|
239
|
+
@start = saved[:start]
|
|
240
|
+
return
|
|
241
|
+
end
|
|
127
242
|
end
|
|
128
243
|
|
|
129
244
|
end
|
data/lib/typr.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# typr is a Ruby library for building interactive terminal-based user interfaces.
|
|
2
|
-
# It provides layout primitives, widgets, and event-driven interaction
|
|
3
|
-
# top of curses.
|
|
2
|
+
# It provides layout primitives, widgets, and event-driven interaction.
|
|
4
3
|
#
|
|
5
4
|
# == Widgets
|
|
6
5
|
#
|
|
@@ -16,8 +15,8 @@
|
|
|
16
15
|
# Typr.init # Initialize terminal (hide cursor, enable key mode)
|
|
17
16
|
# Typr.clear # Clear entire screen
|
|
18
17
|
# Typr.clear :line # Clear current line only
|
|
19
|
-
# key = Typr.
|
|
20
|
-
# line = Typr.
|
|
18
|
+
# key = Typr.read_key # Read a single keypress
|
|
19
|
+
# line = Typr.read_line "> " # Edit a line of text (enter to accept, esc to abort)
|
|
21
20
|
# Typr.exit # Restore terminal state
|
|
22
21
|
#
|
|
23
22
|
# == Layout Properties
|
|
@@ -38,8 +37,8 @@
|
|
|
38
37
|
# header: ['Name', 'Age'],
|
|
39
38
|
# format: [:max, :right]
|
|
40
39
|
# )
|
|
41
|
-
# grid.
|
|
42
|
-
# key = Typr.
|
|
40
|
+
# grid.show
|
|
41
|
+
# key = Typr.read_key
|
|
43
42
|
# Typr.exit
|
|
44
43
|
#
|
|
45
44
|
# == Installation
|