typr 1.8.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 +12 -0
- data/lib/browser.rb +80 -12
- data/lib/grid.rb +61 -21
- data/lib/line.rb +41 -7
- data/lib/space.rb +113 -8
- data/lib/stack.rb +62 -14
- data/lib/terminal.rb +377 -55
- data/lib/text.rb +42 -7
- data/lib/typr.rb +4 -2
- data/typr.gemspec +2 -2
- metadata +2 -2
data/lib/terminal.rb
CHANGED
|
@@ -46,41 +46,70 @@ 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
|
|
@@ -88,7 +117,9 @@ module Typr
|
|
|
88
117
|
# Text attributes (reset, bold, italic, ...) and named 8/256-color tables.
|
|
89
118
|
MODES = %i[ reset bold dim italic underline slow fast invert ]
|
|
90
119
|
MODES[25] = :steady
|
|
120
|
+
# The eight basic color names accepted by {#color_code}.
|
|
91
121
|
COLORS = %i[ black red green yellow blue magenta cyan white ]
|
|
122
|
+
# Named colors mapped to 8-bit palette ids (used by {#color_code}).
|
|
92
123
|
COLOR_MAP = {
|
|
93
124
|
brown: 130, orange: 208, lime: 118, pink: 218,
|
|
94
125
|
maroon: 52, navy: 18, teal: 30, olive: 100,
|
|
@@ -97,35 +128,41 @@ module Typr
|
|
|
97
128
|
dark_blue: 18, dark_magenta: 89, dark_cyan: 30
|
|
98
129
|
}
|
|
99
130
|
|
|
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
|
|
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
|
|
125
151
|
end
|
|
126
152
|
|
|
153
|
+
##
|
|
127
154
|
# Truncate +str+ to at most +max+ cells, keeping ANSI color tokens intact
|
|
128
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
|
+
|
|
129
166
|
def clip str, max, side
|
|
130
167
|
tokens = str.scan(/(\e\[[0-9;]*m)|([^\e]+)/).flatten.compact
|
|
131
168
|
tokens.reverse! if side == :left
|
|
@@ -165,8 +202,17 @@ module Typr
|
|
|
165
202
|
kept.join
|
|
166
203
|
end
|
|
167
204
|
|
|
205
|
+
##
|
|
168
206
|
# Blend the first (or last) +num+ characters of +str+ into greys to hint
|
|
169
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
|
+
|
|
170
216
|
def fade str, side=:left, num=3
|
|
171
217
|
isleft = side == :left
|
|
172
218
|
chars = str[ isleft ? 0..num : -num..-1 ]
|
|
@@ -178,19 +224,44 @@ module Typr
|
|
|
178
224
|
str[0..-num-1] + chars )
|
|
179
225
|
end
|
|
180
226
|
|
|
227
|
+
##
|
|
181
228
|
# Strip ANSI escape sequences and control characters (except \n, \t)
|
|
182
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
|
+
|
|
183
236
|
def sanitize str
|
|
184
237
|
str.scrub
|
|
185
238
|
.gsub(/\e(?:\[[0-9;?]*[ -\/]*[@-~]|\][^\a\e]*(?:\a|\e\\)|[()=><0A])/, '')
|
|
186
239
|
.gsub(/[\x00-\x08\x0b-\x1f\x7f]/, '')
|
|
187
240
|
end
|
|
188
241
|
|
|
242
|
+
##
|
|
189
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
|
+
|
|
190
250
|
def real_size str; Unicode::DisplayWidth.of( sanitize(str) ) end
|
|
191
251
|
|
|
252
|
+
##
|
|
192
253
|
# Coerce a string into Integer, Float, or Boolean when it matches those
|
|
193
|
-
# 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
|
+
|
|
194
265
|
def coerce_type value
|
|
195
266
|
s = value.to_s
|
|
196
267
|
return nil if s.empty? or /\A(?:nil|NULL)\z/i.match(s)
|
|
@@ -205,19 +276,67 @@ module Typr
|
|
|
205
276
|
end
|
|
206
277
|
end
|
|
207
278
|
|
|
208
|
-
|
|
209
|
-
#
|
|
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
|
+
|
|
210
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
|
+
|
|
211
301
|
def move_code x=0,y=0; "\e[%i;%if" % [ y+1, x+1 ] end
|
|
212
302
|
|
|
303
|
+
##
|
|
213
304
|
# Apply a text attribute (+mode+); mode_code returns the escape sequence
|
|
214
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
|
+
|
|
215
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
|
+
|
|
216
323
|
def mode_code name; if id = MODES.index(name.to_s.to_sym) then "\e[#{ id }m" end end
|
|
217
324
|
|
|
325
|
+
##
|
|
218
326
|
# Build the ANSI code for +color+ (a Symbol name, greyN, Integer palette id,
|
|
219
327
|
# or [r,g,b] / [fg,bg] Array), optionally as a background; when false the
|
|
220
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
|
+
|
|
221
340
|
def color_code color, bg=false
|
|
222
341
|
color = color.to_sym if color.is_a? String
|
|
223
342
|
case color
|
|
@@ -288,43 +407,102 @@ module Typr
|
|
|
288
407
|
}.join
|
|
289
408
|
end
|
|
290
409
|
|
|
410
|
+
##
|
|
291
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
|
+
|
|
292
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
|
+
|
|
293
427
|
def get_foreground; $color[0] end
|
|
294
428
|
|
|
429
|
+
##
|
|
295
430
|
# Set and apply the foreground/background color; +color+ sets both from a
|
|
296
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
|
+
|
|
297
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
|
+
|
|
298
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
|
+
|
|
299
463
|
def color c=$default
|
|
300
464
|
c = [c] unless c.is_a? Array and c.count == 2
|
|
301
465
|
foreground c[0] if c[0]
|
|
302
466
|
background c[1] if c[1]
|
|
303
467
|
end
|
|
304
468
|
|
|
469
|
+
##
|
|
305
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
|
+
|
|
306
477
|
def draw str; $>.print str end
|
|
307
478
|
|
|
479
|
+
##
|
|
308
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
|
+
|
|
309
488
|
def self.clear mode=:screen
|
|
310
489
|
$>.print( { screen: CLEAR_SCREEN, line: ERASE_LINE }[mode] )
|
|
311
490
|
end
|
|
312
491
|
|
|
313
|
-
##
|
|
492
|
+
##
|
|
493
|
+
# Reads a single keypress in raw mode.
|
|
314
494
|
#
|
|
315
495
|
# Returns the key sequence as a String (e.g. "a" or "\e[A" for up-arrow),
|
|
316
496
|
# or a Typr::Mouse for a mouse report, or nil when stdin is not a tty or
|
|
317
|
-
# 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]
|
|
318
503
|
|
|
319
504
|
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
|
|
505
|
+
drain_quiet
|
|
328
506
|
read = ->(tty) do
|
|
329
507
|
str = tty.sysread 6
|
|
330
508
|
if str.start_with?("\e[M") and str.size < 6
|
|
@@ -344,9 +522,16 @@ module Typr
|
|
|
344
522
|
decode_mouse(raw) || raw
|
|
345
523
|
end
|
|
346
524
|
|
|
347
|
-
##
|
|
525
|
+
##
|
|
526
|
+
# Decode a raw input string into a {Typr::Mouse} event, or nil when +raw+
|
|
348
527
|
# is not a mouse report. Handles both SGR (CSI < b ; x ; y M/m) and the
|
|
349
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]
|
|
350
535
|
|
|
351
536
|
def self.decode_mouse raw
|
|
352
537
|
if (m = raw.match(/\A\e\[<(\d+);(\d+);(\d+)([Mm])\z/))
|
|
@@ -392,18 +577,50 @@ module Typr
|
|
|
392
577
|
# subprocess handoff via +flush_overtype+.
|
|
393
578
|
OVERTYPE_QUIET = 0.5
|
|
394
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
|
+
|
|
395
592
|
def self.suppress_keys timeout=OVERTYPE_QUIET
|
|
593
|
+
@suppress_quiet = timeout
|
|
396
594
|
@suppress_until = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
397
595
|
end
|
|
398
596
|
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
401
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
|
|
402
621
|
end
|
|
403
622
|
|
|
404
|
-
|
|
405
|
-
drain_input if quiet_window?
|
|
406
|
-
end
|
|
623
|
+
private_class_method :drain_quiet
|
|
407
624
|
|
|
408
625
|
## Interactive line editor used for string prompts (search, %str, ...).
|
|
409
626
|
#
|
|
@@ -423,10 +640,14 @@ module Typr
|
|
|
423
640
|
# query as it is rendered; matched regions restore the prompt's trailing
|
|
424
641
|
# color code.
|
|
425
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
|
+
#
|
|
426
646
|
# Typr.read_line "/" do |key, query, cursor|
|
|
427
647
|
# filter query
|
|
428
648
|
# nil
|
|
429
649
|
# end
|
|
650
|
+
# Typr.read_line "> ", initial: "ls ", history: @recent #=> "ls -l"
|
|
430
651
|
|
|
431
652
|
def self.read_line prompt='', left: 0, top: 0, initial: '', completions: nil, redraw: nil, regex: nil, history: nil, &block
|
|
432
653
|
return $stdin.gets&.chomp unless $stdin.tty?
|
|
@@ -525,14 +746,30 @@ module Typr
|
|
|
525
746
|
end
|
|
526
747
|
end
|
|
527
748
|
|
|
749
|
+
##
|
|
528
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
|
+
|
|
529
757
|
def self.text_width str
|
|
530
758
|
Unicode::DisplayWidth.of( str.gsub(/\x1b\[[^m]+m/, '') )
|
|
531
759
|
end
|
|
532
760
|
|
|
761
|
+
##
|
|
533
762
|
# Visible substring of +str+ starting at display-width +offset+, at most
|
|
534
763
|
# +width+ cells wide. ANSI escapes are zero-width and carried through so
|
|
535
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
|
+
|
|
536
773
|
def self.slice_width str, offset, width
|
|
537
774
|
vis = 0
|
|
538
775
|
slice = +""
|
|
@@ -552,22 +789,46 @@ module Typr
|
|
|
552
789
|
slice
|
|
553
790
|
end
|
|
554
791
|
|
|
792
|
+
##
|
|
555
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
|
+
|
|
556
801
|
def self.word_prev str, cursor
|
|
557
802
|
cursor -= 1 while cursor > 0 and str[cursor - 1] == ' '
|
|
558
803
|
cursor -= 1 while cursor > 0 and str[cursor - 1] != ' '
|
|
559
804
|
cursor
|
|
560
805
|
end
|
|
561
806
|
|
|
807
|
+
##
|
|
562
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
|
+
|
|
563
816
|
def self.word_next str, cursor
|
|
564
817
|
cursor += 1 while cursor < str.length and str[cursor] == ' '
|
|
565
818
|
cursor += 1 while cursor < str.length and str[cursor] != ' '
|
|
566
819
|
cursor
|
|
567
820
|
end
|
|
568
821
|
|
|
822
|
+
private_class_method :slice_width, :word_prev, :word_next
|
|
823
|
+
|
|
824
|
+
##
|
|
569
825
|
# Query the terminal for the current [row, column] via DSR; [0, 0] when
|
|
570
|
-
# 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
|
+
|
|
571
832
|
def self.position
|
|
572
833
|
return [0, 0] unless $stdin.tty?
|
|
573
834
|
result = ''
|
|
@@ -581,22 +842,78 @@ module Typr
|
|
|
581
842
|
result[/[\d;]+/].split(?;).map &:to_i
|
|
582
843
|
end
|
|
583
844
|
|
|
584
|
-
|
|
585
|
-
#
|
|
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
|
+
|
|
586
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
|
+
|
|
587
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
|
+
|
|
588
873
|
def self.height; size.first - 1 end
|
|
589
874
|
|
|
590
|
-
|
|
875
|
+
##
|
|
876
|
+
# Current cursor row/column (see {position}).
|
|
877
|
+
#
|
|
878
|
+
# Typr.row #=> 12
|
|
879
|
+
# Typr.column #=> 40
|
|
880
|
+
#
|
|
881
|
+
# @return [Integer]
|
|
882
|
+
|
|
591
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
|
+
|
|
592
892
|
def self.column; position.last end
|
|
593
893
|
|
|
894
|
+
##
|
|
594
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
|
+
|
|
595
902
|
def self.on_resize &block
|
|
596
903
|
trap(:WINCH, &block)
|
|
597
904
|
end
|
|
598
|
-
|
|
599
|
-
|
|
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
|
+
|
|
600
917
|
def self.init default=[ :white, :black ]
|
|
601
918
|
$default = default.dup
|
|
602
919
|
$color = $default.dup
|
|
@@ -606,8 +923,13 @@ module Typr
|
|
|
606
923
|
print MOUSE_ON
|
|
607
924
|
end
|
|
608
925
|
end
|
|
609
|
-
|
|
610
|
-
#
|
|
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]
|
|
611
933
|
def self.exit;
|
|
612
934
|
extend self
|
|
613
935
|
if $stdin.tty?
|