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