typr 1.7.0 → 1.9.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 +27 -1
- data/README.md +1 -1
- data/lib/browser.rb +82 -15
- data/lib/grid.rb +63 -23
- data/lib/line.rb +43 -9
- data/lib/space.rb +113 -8
- data/lib/stack.rb +62 -14
- data/lib/terminal.rb +394 -66
- data/lib/text.rb +50 -17
- data/lib/typr.rb +4 -2
- data/typr.gemspec +2 -2
- metadata +2 -2
data/lib/terminal.rb
CHANGED
|
@@ -46,48 +46,80 @@ module Typr
|
|
|
46
46
|
const_set cap.upcase, entry[cap] || seq
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
#
|
|
49
|
+
# Line-erase escape sequence (used by {Typr.clear} :line).
|
|
50
50
|
ERASE_LINE = "\e[K"
|
|
51
51
|
# Reset foreground/background to the terminal defaults.
|
|
52
52
|
ORIG_COLORS = "\e[39;49m"
|
|
53
|
+
# Escape: abort a pick or prompt.
|
|
53
54
|
KEY_ESCAPE = "\e"
|
|
55
|
+
# Enter: confirm a pick (alias of CARRIAGE_RETURN).
|
|
54
56
|
KEY_RETURN = CARRIAGE_RETURN
|
|
57
|
+
# Tab: cycle hint chunks; completes the word in {Typr.read_line}.
|
|
55
58
|
KEY_TAB = "\t"
|
|
59
|
+
# Shift+Tab: cycle hint chunks backwards.
|
|
56
60
|
KEY_BACK_TAB = "\e[Z"
|
|
61
|
+
# Alt+Backspace: delete the word before the cursor.
|
|
57
62
|
KEY_ALT_BACKSPACE = "\e\x7f"
|
|
63
|
+
# Alt+Left: move the cursor to the start of the previous word.
|
|
58
64
|
KEY_ALT_LEFT = "\e[1;3D"
|
|
65
|
+
# Alt+Right: move the cursor to the start of the next word.
|
|
59
66
|
KEY_ALT_RIGHT = "\e[1;3C"
|
|
67
|
+
# Page down: jump one viewport (alias of KEY_NPAGE).
|
|
60
68
|
KEY_PAGEDOWN = KEY_NPAGE
|
|
69
|
+
# Page up: jump one viewport backwards (alias of KEY_PPAGE).
|
|
61
70
|
KEY_PAGEUP = KEY_PPAGE
|
|
62
71
|
|
|
63
|
-
# Enable
|
|
64
|
-
# terminfo: 1000 = button press/release/wheel, 1006 = SGR coordinates.
|
|
72
|
+
# Enable mouse reporting (buttons + wheel): 1000 press/release, 1006 SGR coordinates.
|
|
65
73
|
MOUSE_ON = "\e[?1000h\e[?1006h"
|
|
74
|
+
# Disable mouse reporting.
|
|
66
75
|
MOUSE_OFF = "\e[?1000l\e[?1006l"
|
|
76
|
+
# Enter the alternate screen buffer.
|
|
67
77
|
ALT_SCREEN_ON = "\e[?1049h"
|
|
78
|
+
# Leave the alternate screen buffer.
|
|
68
79
|
ALT_SCREEN_OFF = "\e[?1049l"
|
|
69
80
|
|
|
70
81
|
# A mouse event decoded from the input stream. +x+ / +y+ are 1-based
|
|
71
82
|
# terminal coordinates; +button+ is 0=left, 1=middle, 2=right, or 4-7 for
|
|
72
83
|
# the wheel (up/down/left/right); +modifiers+ holds the SGR shift/alt/ctrl
|
|
73
84
|
# bits. Non-widget code can test `key.is_a?(Typr::Mouse)`.
|
|
85
|
+
#
|
|
86
|
+
# Typr::Mouse.new(0, 10, 5, :press, 0)
|
|
87
|
+
# m = Typr.decode_mouse "\e[<0;10;5M"
|
|
88
|
+
# m.press? #=> true
|
|
89
|
+
# m.left? #=> true
|
|
90
|
+
# m.alt? #=> true (with the SGR alt bit set)
|
|
91
|
+
# m.to_s #=> "press button=0 x=10 y=5"
|
|
74
92
|
Mouse = Struct.new(:button, :x, :y, :action, :modifiers) do
|
|
93
|
+
# True on button press (the moment to pick a row).
|
|
75
94
|
def press?; action == :press end
|
|
95
|
+
# True on button release.
|
|
76
96
|
def release?; action == :release end
|
|
97
|
+
# True for any wheel button (4-7).
|
|
77
98
|
def wheel?; button.between?(4, 7) end
|
|
99
|
+
# True for wheel-up (button 4); scrolls a picker up.
|
|
78
100
|
def wheel_up?; button == 4 end
|
|
101
|
+
# True for wheel-down (button 5); scrolls a picker down.
|
|
79
102
|
def wheel_down?; button == 5 end
|
|
103
|
+
# True for the left button; pickers treat it as a hint-digit press.
|
|
80
104
|
def left?; button == 0 end
|
|
105
|
+
# True for the middle button.
|
|
81
106
|
def middle?; button == 1 end
|
|
107
|
+
# True for the right button.
|
|
82
108
|
def right?; button == 2 end
|
|
109
|
+
# True when the SGR alt/meta bit is set — callers may treat an
|
|
110
|
+
# Alt+click like a right-click.
|
|
83
111
|
def alt?; ( modifiers & 8 ) != 0 end
|
|
112
|
+
# Human-readable one-line summary.
|
|
84
113
|
def to_s; "%s button=%i x=%i y=%i%s" % [ action, button, x, y,
|
|
85
114
|
( modifiers > 0 ? " modifiers=#{modifiers}" : "" ) ] end
|
|
86
115
|
end
|
|
87
116
|
|
|
88
117
|
# Text attributes (reset, bold, italic, ...) and named 8/256-color tables.
|
|
89
|
-
MODES = %i[ reset bold italic underline slow fast invert ]
|
|
118
|
+
MODES = %i[ reset bold dim italic underline slow fast invert ]
|
|
119
|
+
MODES[25] = :steady
|
|
120
|
+
# The eight basic color names accepted by {#color_code}.
|
|
90
121
|
COLORS = %i[ black red green yellow blue magenta cyan white ]
|
|
122
|
+
# Named colors mapped to 8-bit palette ids (used by {#color_code}).
|
|
91
123
|
COLOR_MAP = {
|
|
92
124
|
brown: 130, orange: 208, lime: 118, pink: 218,
|
|
93
125
|
maroon: 52, navy: 18, teal: 30, olive: 100,
|
|
@@ -96,35 +128,41 @@ module Typr
|
|
|
96
128
|
dark_blue: 18, dark_magenta: 89, dark_cyan: 30
|
|
97
129
|
}
|
|
98
130
|
|
|
99
|
-
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
else
|
|
120
|
-
str = clip str, max, side
|
|
121
|
-
str = fade str, side, fade if fade
|
|
122
|
-
return str
|
|
123
|
-
end
|
|
131
|
+
##
|
|
132
|
+
# Fit +str+ to exactly +max+ cells: pad the shortfall with spaces
|
|
133
|
+
# (before the text when +align+ is :right), or truncate the overflow
|
|
134
|
+
# from +side+, keeping ANSI escapes intact.
|
|
135
|
+
#
|
|
136
|
+
# space.prepare "ab", 5 #=> "ab "
|
|
137
|
+
# space.prepare "ab", 5, :right #=> " ab"
|
|
138
|
+
# space.prepare "long text", 4 #=> "long" (clipped)
|
|
139
|
+
# space.prepare "\e[31mRED\e[0m", 5 #=> "\e[31mRED\e[0m "
|
|
140
|
+
#
|
|
141
|
+
# @param str [String] text; ANSI escapes count as zero width
|
|
142
|
+
# @param max [Integer] target width in cells
|
|
143
|
+
# @param align [Symbol] :left pads after, :right pads before
|
|
144
|
+
# @param side [Symbol] truncation side, :left keeps the tail
|
|
145
|
+
# @return [String]
|
|
146
|
+
|
|
147
|
+
def prepare str, max, align=:left, side=:right
|
|
148
|
+
str = clip str, max, side if real_size(str) >= max
|
|
149
|
+
pad = " " * (max - real_size(str))
|
|
150
|
+
align == :right ? pad + str : str + pad
|
|
124
151
|
end
|
|
125
152
|
|
|
153
|
+
##
|
|
126
154
|
# Truncate +str+ to at most +max+ cells, keeping ANSI color tokens intact
|
|
127
155
|
# and clipping from +side+ (:left/:right).
|
|
156
|
+
#
|
|
157
|
+
# space.clip "abcdef", 4, :right #=> "abcd"
|
|
158
|
+
# space.clip "abcdef", 4, :left #=> "cdef"
|
|
159
|
+
# space.clip "\e[31mlongtext\e[0m", 4, :left #=> "\e[31mtext"
|
|
160
|
+
#
|
|
161
|
+
# @param str [String] text possibly containing ANSI escapes
|
|
162
|
+
# @param max [Integer] maximum width in cells
|
|
163
|
+
# @param side [Symbol] :left keeps the tail, :right keeps the head
|
|
164
|
+
# @return [String]
|
|
165
|
+
|
|
128
166
|
def clip str, max, side
|
|
129
167
|
tokens = str.scan(/(\e\[[0-9;]*m)|([^\e]+)/).flatten.compact
|
|
130
168
|
tokens.reverse! if side == :left
|
|
@@ -164,8 +202,17 @@ module Typr
|
|
|
164
202
|
kept.join
|
|
165
203
|
end
|
|
166
204
|
|
|
205
|
+
##
|
|
167
206
|
# Blend the first (or last) +num+ characters of +str+ into greys to hint
|
|
168
207
|
# at clipping; +side+ selects which end fades.
|
|
208
|
+
#
|
|
209
|
+
# space.fade "longtext", :right #=> "long" + grey(e,x,t)
|
|
210
|
+
#
|
|
211
|
+
# @param str [String] already-clipped text
|
|
212
|
+
# @param side [Symbol] :left or :right (which end fades)
|
|
213
|
+
# @param num [Integer] number of characters to fade
|
|
214
|
+
# @return [String]
|
|
215
|
+
|
|
169
216
|
def fade str, side=:left, num=3
|
|
170
217
|
isleft = side == :left
|
|
171
218
|
chars = str[ isleft ? 0..num : -num..-1 ]
|
|
@@ -177,19 +224,44 @@ module Typr
|
|
|
177
224
|
str[0..-num-1] + chars )
|
|
178
225
|
end
|
|
179
226
|
|
|
227
|
+
##
|
|
180
228
|
# Strip ANSI escape sequences and control characters (except \n, \t)
|
|
181
229
|
# from shell-generated text, leaving only printable content.
|
|
230
|
+
#
|
|
231
|
+
# space.sanitize "\e[31mred\e[0m plain" #=> "red plain"
|
|
232
|
+
#
|
|
233
|
+
# @param str [String] raw text
|
|
234
|
+
# @return [String]
|
|
235
|
+
|
|
182
236
|
def sanitize str
|
|
183
237
|
str.scrub
|
|
184
238
|
.gsub(/\e(?:\[[0-9;?]*[ -\/]*[@-~]|\][^\a\e]*(?:\a|\e\\)|[()=><0A])/, '')
|
|
185
239
|
.gsub(/[\x00-\x08\x0b-\x1f\x7f]/, '')
|
|
186
240
|
end
|
|
187
241
|
|
|
242
|
+
##
|
|
188
243
|
# Display width of +str+ after stripping ANSI escapes and control chars.
|
|
244
|
+
#
|
|
245
|
+
# space.real_size "\e[31mabc\e[0m" #=> 3
|
|
246
|
+
#
|
|
247
|
+
# @param str [String] text possibly containing ANSI escapes
|
|
248
|
+
# @return [Integer] width in cells
|
|
249
|
+
|
|
189
250
|
def real_size str; Unicode::DisplayWidth.of( sanitize(str) ) end
|
|
190
251
|
|
|
252
|
+
##
|
|
191
253
|
# Coerce a string into Integer, Float, or Boolean when it matches those
|
|
192
|
-
# forms (yes/no, true/false); otherwise return it unchanged.
|
|
254
|
+
# forms (yes/no, true/false); otherwise return it unchanged. Empty
|
|
255
|
+
# strings and +nil+/+NULL+ become nil.
|
|
256
|
+
#
|
|
257
|
+
# space.coerce_type "42" #=> 42
|
|
258
|
+
# space.coerce_type "3.14" #=> 3.14
|
|
259
|
+
# space.coerce_type "yes" #=> true
|
|
260
|
+
# space.coerce_type "hello" #=> "hello" (unchanged)
|
|
261
|
+
#
|
|
262
|
+
# @param value [Object] cell value (usually a String)
|
|
263
|
+
# @return [Integer, Float, Boolean, nil, Object]
|
|
264
|
+
|
|
193
265
|
def coerce_type value
|
|
194
266
|
s = value.to_s
|
|
195
267
|
return nil if s.empty? or /\A(?:nil|NULL)\z/i.match(s)
|
|
@@ -204,19 +276,67 @@ module Typr
|
|
|
204
276
|
end
|
|
205
277
|
end
|
|
206
278
|
|
|
207
|
-
|
|
208
|
-
#
|
|
279
|
+
##
|
|
280
|
+
# Move the cursor to column +x+, row +y+ (0-based); move_code returns
|
|
281
|
+
# the escape sequence without writing it.
|
|
282
|
+
#
|
|
283
|
+
# space.move 0, 0 # prints "\e[1;1f"
|
|
284
|
+
# space.move_code 0, 0 #=> "\e[1;1f"
|
|
285
|
+
#
|
|
286
|
+
# @param x [Integer] column (0-based)
|
|
287
|
+
# @param y [Integer] row (0-based)
|
|
288
|
+
# @return [void] / [String] move_code returns the sequence
|
|
289
|
+
|
|
209
290
|
def move x=0,y=0; draw move_code( x, y ) end
|
|
291
|
+
|
|
292
|
+
##
|
|
293
|
+
# The escape sequence for {#move}, without writing it.
|
|
294
|
+
#
|
|
295
|
+
# space.move_code 0, 0 #=> "\e[1;1f"
|
|
296
|
+
#
|
|
297
|
+
# @param x [Integer] column (0-based)
|
|
298
|
+
# @param y [Integer] row (0-based)
|
|
299
|
+
# @return [String]
|
|
300
|
+
|
|
210
301
|
def move_code x=0,y=0; "\e[%i;%if" % [ y+1, x+1 ] end
|
|
211
302
|
|
|
303
|
+
##
|
|
212
304
|
# Apply a text attribute (+mode+); mode_code returns the escape sequence
|
|
213
305
|
# without writing it.
|
|
306
|
+
#
|
|
307
|
+
# space.mode :bold # prints "\e[1m"
|
|
308
|
+
# space.mode_code :bold #=> "\e[1m"
|
|
309
|
+
#
|
|
310
|
+
# @param name [Symbol] one of +MODES+ (:bold, :underline, :invert, ...)
|
|
311
|
+
# @return [void] / [String] mode_code returns the sequence
|
|
312
|
+
|
|
214
313
|
def mode name; draw mode_code(name) end
|
|
314
|
+
|
|
315
|
+
##
|
|
316
|
+
# The escape sequence for {#mode}, without writing it.
|
|
317
|
+
#
|
|
318
|
+
# space.mode_code :bold #=> "\e[1m"
|
|
319
|
+
#
|
|
320
|
+
# @param name [Symbol] one of +MODES+
|
|
321
|
+
# @return [String, nil] nil for unknown attributes
|
|
322
|
+
|
|
215
323
|
def mode_code name; if id = MODES.index(name.to_s.to_sym) then "\e[#{ id }m" end end
|
|
216
324
|
|
|
325
|
+
##
|
|
217
326
|
# Build the ANSI code for +color+ (a Symbol name, greyN, Integer palette id,
|
|
218
327
|
# or [r,g,b] / [fg,bg] Array), optionally as a background; when false the
|
|
219
328
|
# color is emitted as a foreground.
|
|
329
|
+
#
|
|
330
|
+
# space.color_code :red #=> "\e[31m"
|
|
331
|
+
# space.color_code :red, true #=> "\e[41m" (background)
|
|
332
|
+
# space.color_code :grey50 #=> "\e[38;5;243m"
|
|
333
|
+
# space.color_code [10, 200, 30] #=> "\e[38;2;10;200;30m"
|
|
334
|
+
# space.color_code [:cyan, :black] #=> "\e[36m\e[40m" (fg, bg pair)
|
|
335
|
+
#
|
|
336
|
+
# @param color [Symbol, Integer, Array] color name, palette id, RGB, or pair
|
|
337
|
+
# @param bg [Boolean] emit as background when true
|
|
338
|
+
# @return [String, nil] ANSI escape sequence (nil for unknown names)
|
|
339
|
+
|
|
220
340
|
def color_code color, bg=false
|
|
221
341
|
color = color.to_sym if color.is_a? String
|
|
222
342
|
case color
|
|
@@ -287,43 +407,102 @@ module Typr
|
|
|
287
407
|
}.join
|
|
288
408
|
end
|
|
289
409
|
|
|
410
|
+
##
|
|
290
411
|
# Read the current background/foreground color pair.
|
|
412
|
+
#
|
|
413
|
+
# space.get_foreground #=> :white
|
|
414
|
+
# space.get_background #=> :black
|
|
415
|
+
#
|
|
416
|
+
# @return [Object] the active color value
|
|
417
|
+
|
|
291
418
|
def get_background; $color[1] end
|
|
419
|
+
|
|
420
|
+
##
|
|
421
|
+
# The currently applied foreground color.
|
|
422
|
+
#
|
|
423
|
+
# space.get_foreground #=> :white
|
|
424
|
+
#
|
|
425
|
+
# @return [Object] the active color value
|
|
426
|
+
|
|
292
427
|
def get_foreground; $color[0] end
|
|
293
428
|
|
|
429
|
+
##
|
|
294
430
|
# Set and apply the foreground/background color; +color+ sets both from a
|
|
295
431
|
# [fg, bg] pair, a single color, or the module default.
|
|
432
|
+
#
|
|
433
|
+
# space.foreground :red # print in red
|
|
434
|
+
# space.background :blue # paint the background blue
|
|
435
|
+
# space.color [:cyan, :black] # set both
|
|
436
|
+
# space.color # restore the default pair
|
|
437
|
+
#
|
|
438
|
+
# @param c [Symbol, Integer, Array, nil] color or [fg, bg] pair
|
|
439
|
+
# @return [void]
|
|
440
|
+
|
|
296
441
|
def foreground c=$default[0]; $color[0]=c; draw color_code(c) end
|
|
442
|
+
|
|
443
|
+
##
|
|
444
|
+
# Paint the background with +c+ (see {#foreground}).
|
|
445
|
+
#
|
|
446
|
+
# space.background :blue
|
|
447
|
+
#
|
|
448
|
+
# @param c [Symbol, Integer, Array, nil] background color
|
|
449
|
+
# @return [void]
|
|
450
|
+
|
|
297
451
|
def background c=$default[1]; $color[1]=c; draw color_code(c,true) end
|
|
452
|
+
|
|
453
|
+
##
|
|
454
|
+
# Set both foreground and background: a [fg, bg] pair, a single color
|
|
455
|
+
# applied to both, or no argument to restore the module default pair.
|
|
456
|
+
#
|
|
457
|
+
# space.color [:cyan, :black]
|
|
458
|
+
# space.color # default pair back
|
|
459
|
+
#
|
|
460
|
+
# @param c [Symbol, Integer, Array, nil] color or [fg, bg] pair
|
|
461
|
+
# @return [void]
|
|
462
|
+
|
|
298
463
|
def color c=$default
|
|
299
464
|
c = [c] unless c.is_a? Array and c.count == 2
|
|
300
465
|
foreground c[0] if c[0]
|
|
301
466
|
background c[1] if c[1]
|
|
302
467
|
end
|
|
303
468
|
|
|
469
|
+
##
|
|
304
470
|
# Write raw +str+ to stdout.
|
|
471
|
+
#
|
|
472
|
+
# space.draw "hi" # prints hi at the cursor
|
|
473
|
+
#
|
|
474
|
+
# @param str [String] escape sequences and text to emit
|
|
475
|
+
# @return [nil]
|
|
476
|
+
|
|
305
477
|
def draw str; $>.print str end
|
|
306
478
|
|
|
479
|
+
##
|
|
307
480
|
# Clear the whole screen (:screen) or just the current line (:line).
|
|
481
|
+
#
|
|
482
|
+
# Typr.clear # clear the screen
|
|
483
|
+
# Typr.clear :line # clear the current line
|
|
484
|
+
#
|
|
485
|
+
# @param mode [:screen, :line]
|
|
486
|
+
# @return [nil]
|
|
487
|
+
|
|
308
488
|
def self.clear mode=:screen
|
|
309
489
|
$>.print( { screen: CLEAR_SCREEN, line: ERASE_LINE }[mode] )
|
|
310
490
|
end
|
|
311
491
|
|
|
312
|
-
##
|
|
492
|
+
##
|
|
493
|
+
# Reads a single keypress in raw mode.
|
|
313
494
|
#
|
|
314
495
|
# Returns the key sequence as a String (e.g. "a" or "\e[A" for up-arrow),
|
|
315
496
|
# or a Typr::Mouse for a mouse report, or nil when stdin is not a tty or
|
|
316
|
-
# input is unavailable.
|
|
497
|
+
# input is unavailable. Keys arriving inside an overtype quiet window
|
|
498
|
+
# ({#suppress_keys}) are discarded first.
|
|
499
|
+
#
|
|
500
|
+
# key = Typr.read_key #=> "s" | "\e[A" | #<Typr::Mouse> | nil
|
|
501
|
+
#
|
|
502
|
+
# @return [String, Typr::Mouse, nil]
|
|
317
503
|
|
|
318
504
|
def self.read_key
|
|
319
|
-
|
|
320
|
-
loop do
|
|
321
|
-
remaining = @suppress_until - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
322
|
-
break if remaining <= 0
|
|
323
|
-
break unless IO.select([INPUT], nil, nil, remaining)
|
|
324
|
-
begin INPUT.read_nonblock(1024) rescue break end
|
|
325
|
-
end
|
|
326
|
-
end
|
|
505
|
+
drain_quiet
|
|
327
506
|
read = ->(tty) do
|
|
328
507
|
str = tty.sysread 6
|
|
329
508
|
if str.start_with?("\e[M") and str.size < 6
|
|
@@ -343,9 +522,16 @@ module Typr
|
|
|
343
522
|
decode_mouse(raw) || raw
|
|
344
523
|
end
|
|
345
524
|
|
|
346
|
-
##
|
|
525
|
+
##
|
|
526
|
+
# Decode a raw input string into a {Typr::Mouse} event, or nil when +raw+
|
|
347
527
|
# is not a mouse report. Handles both SGR (CSI < b ; x ; y M/m) and the
|
|
348
528
|
# legacy X10 (CSI M + three bytes) encodings.
|
|
529
|
+
#
|
|
530
|
+
# Typr.decode_mouse "\e[<0;10;5M" #=> #<struct button=0 x=10 y=5 action=:press modifiers=0>
|
|
531
|
+
# Typr.decode_mouse "abc" #=> nil
|
|
532
|
+
#
|
|
533
|
+
# @param raw [String] raw bytes read from the terminal
|
|
534
|
+
# @return [Typr::Mouse, nil]
|
|
349
535
|
|
|
350
536
|
def self.decode_mouse raw
|
|
351
537
|
if (m = raw.match(/\A\e\[<(\d+);(\d+);(\d+)([Mm])\z/))
|
|
@@ -391,23 +577,55 @@ module Typr
|
|
|
391
577
|
# subprocess handoff via +flush_overtype+.
|
|
392
578
|
OVERTYPE_QUIET = 0.5
|
|
393
579
|
|
|
580
|
+
##
|
|
581
|
+
# Arm the overtype quiet window: every key arriving in the next
|
|
582
|
+
# +timeout+ seconds is discarded by {Typr.read_key}, so a double-tapped
|
|
583
|
+
# Enter or trailing characters after an auto-confirmed pick cannot
|
|
584
|
+
# re-trigger anything. Called by +Grid#pick+ on auto-return.
|
|
585
|
+
#
|
|
586
|
+
# Typr.suppress_keys # 0.5s window (OVERTYPE_QUIET)
|
|
587
|
+
# Typr.suppress_keys 1.0 # custom window in seconds
|
|
588
|
+
#
|
|
589
|
+
# @param timeout [Numeric] window length in seconds
|
|
590
|
+
# @return [Numeric] the monotonic deadline
|
|
591
|
+
|
|
394
592
|
def self.suppress_keys timeout=OVERTYPE_QUIET
|
|
593
|
+
@suppress_quiet = timeout
|
|
395
594
|
@suppress_until = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
396
595
|
end
|
|
397
596
|
|
|
398
|
-
|
|
399
|
-
|
|
597
|
+
##
|
|
598
|
+
# Wait out an armed overtype window: switch to raw mode (canonical-
|
|
599
|
+
# buffered keystrokes are invisible to select) and discard arrivals
|
|
600
|
+
# until a full window passes with none — each arrival restarts it.
|
|
601
|
+
# No-op unless armed by {Typr.suppress_keys}. Best-effort: never raises.
|
|
602
|
+
#
|
|
603
|
+
# @return [nil]
|
|
604
|
+
|
|
605
|
+
def self.drain_quiet
|
|
606
|
+
return unless @suppress_until and $stdin.tty? and
|
|
400
607
|
@suppress_until > Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
608
|
+
INPUT.raw do
|
|
609
|
+
loop do
|
|
610
|
+
wait = @suppress_until - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
611
|
+
break if wait <= 0
|
|
612
|
+
break unless IO.select([INPUT], nil, nil, wait)
|
|
613
|
+
begin INPUT.read_nonblock(1024)
|
|
614
|
+
rescue IO::WaitReadable then break end
|
|
615
|
+
@suppress_until = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @suppress_quiet
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
nil
|
|
619
|
+
rescue StandardError
|
|
620
|
+
nil
|
|
401
621
|
end
|
|
402
622
|
|
|
403
|
-
|
|
404
|
-
drain_input if quiet_window?
|
|
405
|
-
end
|
|
623
|
+
private_class_method :drain_quiet
|
|
406
624
|
|
|
407
625
|
## Interactive line editor used for string prompts (search, %str, ...).
|
|
408
626
|
#
|
|
409
627
|
# Renders +prompt+ and the query at (left, top) and edits it with
|
|
410
|
-
# arrow keys,
|
|
628
|
+
# arrow keys, alt-arrow word jumps, home/end, delete and backspace.
|
|
411
629
|
# Returns the query on enter, nil on escape. A block may inspect each
|
|
412
630
|
# keypress after it is applied; a non-nil return short-circuits the
|
|
413
631
|
# editor and becomes its return value. When stdin is not a tty, the
|
|
@@ -422,10 +640,14 @@ module Typr
|
|
|
422
640
|
# query as it is rendered; matched regions restore the prompt's trailing
|
|
423
641
|
# color code.
|
|
424
642
|
#
|
|
643
|
+
# +history+ is an Array of previous queries recalled with Up/Down;
|
|
644
|
+
# +redraw+ repaints the caller's screen after a completion pick.
|
|
645
|
+
#
|
|
425
646
|
# Typr.read_line "/" do |key, query, cursor|
|
|
426
647
|
# filter query
|
|
427
648
|
# nil
|
|
428
649
|
# end
|
|
650
|
+
# Typr.read_line "> ", initial: "ls ", history: @recent #=> "ls -l"
|
|
429
651
|
|
|
430
652
|
def self.read_line prompt='', left: 0, top: 0, initial: '', completions: nil, redraw: nil, regex: nil, history: nil, &block
|
|
431
653
|
return $stdin.gets&.chomp unless $stdin.tty?
|
|
@@ -434,13 +656,16 @@ module Typr
|
|
|
434
656
|
$>.print CURSOR_NORMAL
|
|
435
657
|
begin
|
|
436
658
|
loop do
|
|
659
|
+
$>.print CURSOR_INVISIBLE
|
|
437
660
|
$>.print "\e[%i;%if" % [ top + 1, left + 1 ]
|
|
438
|
-
$>.print ERASE_LINE
|
|
439
661
|
before = text_width( prompt + query[0...cursor] )
|
|
440
662
|
offset = [ left + before - Typr.width, 0 ].max
|
|
441
663
|
display = prompt + paint( query, regex, prompt[/\e\[[0-9;]*m\z/].to_s )
|
|
442
|
-
|
|
664
|
+
line = slice_width( display, offset, Typr.width - left + 1 )
|
|
665
|
+
$>.print line
|
|
666
|
+
$>.print ' ' * (Typr.width - left + 1 - text_width(line))
|
|
443
667
|
$>.print "\e[%i;%if" % [ top + 1, left + 1 + before - offset ]
|
|
668
|
+
$>.print CURSOR_NORMAL
|
|
444
669
|
key = read_key
|
|
445
670
|
return nil if key.nil? or key == KEY_ESCAPE
|
|
446
671
|
return query if key == KEY_RETURN or key == "\n"
|
|
@@ -521,14 +746,30 @@ module Typr
|
|
|
521
746
|
end
|
|
522
747
|
end
|
|
523
748
|
|
|
749
|
+
##
|
|
524
750
|
# Display width of +str+ after stripping ANSI escapes.
|
|
751
|
+
#
|
|
752
|
+
# Typr.text_width "\e[31mabc\e[0m" #=> 3
|
|
753
|
+
#
|
|
754
|
+
# @param str [String] text possibly containing ANSI escapes
|
|
755
|
+
# @return [Integer] width in cells
|
|
756
|
+
|
|
525
757
|
def self.text_width str
|
|
526
758
|
Unicode::DisplayWidth.of( str.gsub(/\x1b\[[^m]+m/, '') )
|
|
527
759
|
end
|
|
528
760
|
|
|
761
|
+
##
|
|
529
762
|
# Visible substring of +str+ starting at display-width +offset+, at most
|
|
530
763
|
# +width+ cells wide. ANSI escapes are zero-width and carried through so
|
|
531
764
|
# color state applies inside the window.
|
|
765
|
+
#
|
|
766
|
+
# Typr.slice_width "hello world", 6, 5 #=> "world"
|
|
767
|
+
#
|
|
768
|
+
# @param str [String] text possibly containing ANSI escapes
|
|
769
|
+
# @param offset [Integer] display-width start of the window
|
|
770
|
+
# @param width [Integer] window size in cells
|
|
771
|
+
# @return [String]
|
|
772
|
+
|
|
532
773
|
def self.slice_width str, offset, width
|
|
533
774
|
vis = 0
|
|
534
775
|
slice = +""
|
|
@@ -548,22 +789,46 @@ module Typr
|
|
|
548
789
|
slice
|
|
549
790
|
end
|
|
550
791
|
|
|
792
|
+
##
|
|
551
793
|
# Move the cursor back to the start of the previous word in +str+.
|
|
794
|
+
#
|
|
795
|
+
# Typr.word_prev "foo bar baz", 8 #=> 4
|
|
796
|
+
#
|
|
797
|
+
# @param str [String] the text being edited
|
|
798
|
+
# @param cursor [Integer] current cursor position
|
|
799
|
+
# @return [Integer] new cursor position
|
|
800
|
+
|
|
552
801
|
def self.word_prev str, cursor
|
|
553
802
|
cursor -= 1 while cursor > 0 and str[cursor - 1] == ' '
|
|
554
803
|
cursor -= 1 while cursor > 0 and str[cursor - 1] != ' '
|
|
555
804
|
cursor
|
|
556
805
|
end
|
|
557
806
|
|
|
807
|
+
##
|
|
558
808
|
# Move the cursor forward to the start of the next word in +str+.
|
|
809
|
+
#
|
|
810
|
+
# Typr.word_next "foo bar baz", 4 #=> 7
|
|
811
|
+
#
|
|
812
|
+
# @param str [String] the text being edited
|
|
813
|
+
# @param cursor [Integer] current cursor position
|
|
814
|
+
# @return [Integer] new cursor position
|
|
815
|
+
|
|
559
816
|
def self.word_next str, cursor
|
|
560
817
|
cursor += 1 while cursor < str.length and str[cursor] == ' '
|
|
561
818
|
cursor += 1 while cursor < str.length and str[cursor] != ' '
|
|
562
819
|
cursor
|
|
563
820
|
end
|
|
564
821
|
|
|
822
|
+
private_class_method :slice_width, :word_prev, :word_next
|
|
823
|
+
|
|
824
|
+
##
|
|
565
825
|
# Query the terminal for the current [row, column] via DSR; [0, 0] when
|
|
566
|
-
# stdin is not a tty.
|
|
826
|
+
# stdin is not a tty. Blocks until the terminal answers \e[6n.
|
|
827
|
+
#
|
|
828
|
+
# Typr.position #=> [12, 40] (row, column)
|
|
829
|
+
#
|
|
830
|
+
# @return [Array<Integer>] [row, column]
|
|
831
|
+
|
|
567
832
|
def self.position
|
|
568
833
|
return [0, 0] unless $stdin.tty?
|
|
569
834
|
result = ''
|
|
@@ -577,22 +842,78 @@ module Typr
|
|
|
577
842
|
result[/[\d;]+/].split(?;).map &:to_i
|
|
578
843
|
end
|
|
579
844
|
|
|
580
|
-
|
|
581
|
-
#
|
|
845
|
+
##
|
|
846
|
+
# Terminal [rows, cols] (defaults to 24x80 when undetectable);
|
|
847
|
+
# +width+/+height+ return the usable last column/row.
|
|
848
|
+
#
|
|
849
|
+
# Typr.size #=> [24, 80]
|
|
850
|
+
# Typr.width #=> 79
|
|
851
|
+
# Typr.height #=> 23
|
|
852
|
+
#
|
|
853
|
+
# @return [Array<Integer>] [rows, cols]
|
|
854
|
+
|
|
582
855
|
def self.size; IO.console&.winsize || [24, 80] end
|
|
856
|
+
|
|
857
|
+
##
|
|
858
|
+
# Usable last column (terminal width minus one).
|
|
859
|
+
#
|
|
860
|
+
# Typr.width #=> 79
|
|
861
|
+
#
|
|
862
|
+
# @return [Integer]
|
|
863
|
+
|
|
583
864
|
def self.width; size.last-1 end
|
|
865
|
+
|
|
866
|
+
##
|
|
867
|
+
# Usable last row (terminal height minus one).
|
|
868
|
+
#
|
|
869
|
+
# Typr.height #=> 23
|
|
870
|
+
#
|
|
871
|
+
# @return [Integer]
|
|
872
|
+
|
|
584
873
|
def self.height; size.first - 1 end
|
|
585
874
|
|
|
586
|
-
|
|
875
|
+
##
|
|
876
|
+
# Current cursor row/column (see {position}).
|
|
877
|
+
#
|
|
878
|
+
# Typr.row #=> 12
|
|
879
|
+
# Typr.column #=> 40
|
|
880
|
+
#
|
|
881
|
+
# @return [Integer]
|
|
882
|
+
|
|
587
883
|
def self.row; position.first end
|
|
884
|
+
|
|
885
|
+
##
|
|
886
|
+
# Current cursor column (see {position}).
|
|
887
|
+
#
|
|
888
|
+
# Typr.column #=> 40
|
|
889
|
+
#
|
|
890
|
+
# @return [Integer]
|
|
891
|
+
|
|
588
892
|
def self.column; position.last end
|
|
589
893
|
|
|
894
|
+
##
|
|
590
895
|
# Call +block+ whenever the terminal is resized (SIGWINCH).
|
|
896
|
+
#
|
|
897
|
+
# Typr.on_resize { grid.show }
|
|
898
|
+
#
|
|
899
|
+
# @param block [Proc] handler invoked on resize
|
|
900
|
+
# @return [Integer] the signal handler (trap's return)
|
|
901
|
+
|
|
591
902
|
def self.on_resize &block
|
|
592
903
|
trap(:WINCH, &block)
|
|
593
904
|
end
|
|
594
|
-
|
|
595
|
-
|
|
905
|
+
|
|
906
|
+
##
|
|
907
|
+
# Enter interactive mode: seed the default colors, hide the cursor,
|
|
908
|
+
# enable key-mode (application) escapes and mouse reporting when stdin
|
|
909
|
+
# is a tty.
|
|
910
|
+
#
|
|
911
|
+
# Typr.init # default [:white, :black]
|
|
912
|
+
# Typr.init [:cyan, :grey10] # custom default pair
|
|
913
|
+
#
|
|
914
|
+
# @param default [Array<Symbol>] initial [fg, bg] pair
|
|
915
|
+
# @return [void]
|
|
916
|
+
|
|
596
917
|
def self.init default=[ :white, :black ]
|
|
597
918
|
$default = default.dup
|
|
598
919
|
$color = $default.dup
|
|
@@ -602,8 +923,13 @@ module Typr
|
|
|
602
923
|
print MOUSE_ON
|
|
603
924
|
end
|
|
604
925
|
end
|
|
605
|
-
|
|
606
|
-
#
|
|
926
|
+
##
|
|
927
|
+
# Restore the terminal: show the cursor, reset keypad/colors, disable
|
|
928
|
+
# mouse reporting, and clear.
|
|
929
|
+
#
|
|
930
|
+
# Typr.exit # => nil
|
|
931
|
+
#
|
|
932
|
+
# @return [nil]
|
|
607
933
|
def self.exit;
|
|
608
934
|
extend self
|
|
609
935
|
if $stdin.tty?
|
|
@@ -619,14 +945,15 @@ if __FILE__ == $0
|
|
|
619
945
|
begin
|
|
620
946
|
include Typr
|
|
621
947
|
Typr.init
|
|
622
|
-
colors = COLORS + (
|
|
948
|
+
colors = COLORS + COLOR_MAP.keys.map(&:to_s) + (0..10).map{|i| "grey#{i*10}" }
|
|
623
949
|
chars = (33..126).map(&:chr)
|
|
624
950
|
slice = chars.count / colors.count + 1
|
|
625
951
|
characters = chars.each_slice( slice ).map(&:join)
|
|
626
952
|
key,fg,bg = "",150,0
|
|
627
953
|
Typr.clear
|
|
954
|
+
rows = [colors.count, Typr.height - 4].min
|
|
628
955
|
loop do
|
|
629
|
-
colors.each_with_index{ |color,y|
|
|
956
|
+
colors.first(rows).each_with_index{ |color,y|
|
|
630
957
|
characters.each_with_index{ |chars,x|
|
|
631
958
|
foreground color.to_sym;
|
|
632
959
|
background colors[x].to_sym;
|
|
@@ -634,20 +961,21 @@ begin
|
|
|
634
961
|
draw chars
|
|
635
962
|
}
|
|
636
963
|
}
|
|
964
|
+
move 0, rows
|
|
637
965
|
(0...256).each{ |i| background i; draw ' ' }
|
|
638
966
|
|
|
639
967
|
move 0, Typr.height
|
|
640
968
|
color [:white, :black]
|
|
641
|
-
|
|
969
|
+
draw "KEY: %s / %s" % [key, key]
|
|
642
970
|
move 15, Typr.height
|
|
643
971
|
color [fg,bg]
|
|
644
972
|
print " %s / %s " % [fg,bg]
|
|
645
973
|
key = Typr.read_key
|
|
646
974
|
case key
|
|
647
|
-
when KEY_UP; fg
|
|
648
|
-
when KEY_DOWN; fg
|
|
649
|
-
when KEY_LEFT; bg
|
|
650
|
-
when KEY_RIGHT; bg
|
|
975
|
+
when KEY_UP; fg = [fg + 1, 255].min
|
|
976
|
+
when KEY_DOWN; fg = [fg - 1, 0].max
|
|
977
|
+
when KEY_LEFT; bg = [bg - 1, 0].max
|
|
978
|
+
when KEY_RIGHT; bg = [bg + 1, 255].max
|
|
651
979
|
when KEY_ESCAPE; exit
|
|
652
980
|
end
|
|
653
981
|
end
|