typr 1.2.0 → 1.4.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 +28 -0
- data/README.md +64 -33
- data/lib/browser.rb +80 -53
- data/lib/graphical.rb +1 -1
- data/lib/grid.rb +8 -1
- data/lib/line.rb +4 -2
- data/lib/space.rb +11 -0
- data/lib/stack.rb +35 -3
- data/lib/terminal.rb +174 -49
- data/lib/text.rb +7 -0
- data/lib/typr.rb +3 -3
- data/share/terminfo +691 -0
- data/typr.gemspec +4 -3
- metadata +2 -2
- data/lib/curses.rb +0 -124
data/lib/terminal.rb
CHANGED
|
@@ -10,22 +10,70 @@ module Typr
|
|
|
10
10
|
# Raw /dev/tty used for key input; falls back to $stdin when unavailable.
|
|
11
11
|
INPUT = (IO.new IO.sysopen("/dev/tty", "r")) rescue $stdin
|
|
12
12
|
|
|
13
|
-
# KEY_*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
# KEY_* escapes and keypad sequences are precompiled into share/terminfo by
|
|
14
|
+
# bin/build_terminfo, so the runtime needs no ncurses/infocmp (e.g. Termux).
|
|
15
|
+
TERMINFO = begin
|
|
16
|
+
eval File.read(File.expand_path("../share/terminfo", __dir__))
|
|
17
|
+
rescue StandardError
|
|
18
|
+
{}
|
|
19
|
+
end
|
|
20
|
+
# Known-good fallback so KEY_* are always defined, even for TERM=dumb.
|
|
21
|
+
DEFAULT_KEYS = { "key_up" => "\eOA", "key_down" => "\eOB", "key_left" => "\eOD",
|
|
22
|
+
"key_right" => "\eOC", "key_home" => "\eOH", "key_end" => "\eOF",
|
|
23
|
+
"key_backspace" => "\x7f", "key_dc" => "\e[3~", "key_npage" => "\e[6~",
|
|
24
|
+
"key_ppage" => "\e[5~", "carriage_return" => "\r",
|
|
25
|
+
"keypad_xmit" => "\e[?1h\e=", "keypad_local" => "\e[?1l\e>",
|
|
26
|
+
"cursor_invisible" => "\e[?25l", "cursor_normal" => "\e[?25h",
|
|
27
|
+
"clear_screen" => "\e[H\e[2J", "user7" => "\e[6n" }
|
|
28
|
+
term = ENV["TERM"].to_s
|
|
29
|
+
entry = nil
|
|
30
|
+
[ term, term.sub(/-.*/, ""), "xterm" ].each do |name|
|
|
31
|
+
found = TERMINFO[name]
|
|
32
|
+
if found && found.any? { |cap, _| cap.start_with?("key_") }
|
|
33
|
+
entry = found
|
|
34
|
+
break
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
entry ||= {}
|
|
38
|
+
# Fill gaps with safe ANSI defaults, but never fabricate keypad modes:
|
|
39
|
+
# those must match the terminal exactly or arrows get misread.
|
|
40
|
+
DEFAULT_KEYS.each do |cap, seq|
|
|
41
|
+
next if cap.start_with?("keypad_") && !entry[cap]
|
|
42
|
+
const_set cap.upcase, entry[cap] || seq
|
|
43
|
+
end
|
|
20
44
|
|
|
21
45
|
# Common key aliases and the line-erase escape sequence.
|
|
22
46
|
ERASE_LINE = "\e[K"
|
|
47
|
+
# Reset foreground/background to the terminal defaults.
|
|
48
|
+
ORIG_COLORS = "\e[39;49m"
|
|
23
49
|
KEY_ESCAPE = "\e"
|
|
24
50
|
KEY_RETURN = CARRIAGE_RETURN
|
|
25
51
|
KEY_TAB = "\t"
|
|
26
52
|
KEY_PAGEDOWN = KEY_NPAGE
|
|
27
53
|
KEY_PAGEUP = KEY_PPAGE
|
|
28
54
|
|
|
55
|
+
# Enable/disable mouse reporting. Universal private modes, independent of
|
|
56
|
+
# terminfo: 1000 = button press/release/wheel, 1006 = SGR coordinates.
|
|
57
|
+
MOUSE_ON = "\e[?1000h\e[?1006h"
|
|
58
|
+
MOUSE_OFF = "\e[?1000l\e[?1006l"
|
|
59
|
+
|
|
60
|
+
# A mouse event decoded from the input stream. +x+ / +y+ are 1-based
|
|
61
|
+
# terminal coordinates; +button+ is 0=left, 1=middle, 2=right, or 4-7 for
|
|
62
|
+
# the wheel (up/down/left/right); +modifiers+ holds the SGR shift/alt/ctrl
|
|
63
|
+
# bits. Non-widget code can test `key.is_a?(Typr::Mouse)`.
|
|
64
|
+
Mouse = Struct.new(:button, :x, :y, :action, :modifiers) do
|
|
65
|
+
def press?; action == :press end
|
|
66
|
+
def release?; action == :release end
|
|
67
|
+
def wheel?; button.between?(4, 7) end
|
|
68
|
+
def wheel_up?; button == 4 end
|
|
69
|
+
def wheel_down?; button == 5 end
|
|
70
|
+
def left?; button == 0 end
|
|
71
|
+
def middle?; button == 1 end
|
|
72
|
+
def right?; button == 2 end
|
|
73
|
+
def to_s; "%s button=%i x=%i y=%i%s" % [ action, button, x, y,
|
|
74
|
+
( modifiers > 0 ? " modifiers=#{modifiers}" : "" ) ] end
|
|
75
|
+
end
|
|
76
|
+
|
|
29
77
|
# Text attributes (reset, bold, italic, ...) and named 8/256-color tables.
|
|
30
78
|
MODES = %i[ reset bold italic underline slow fast invert ]
|
|
31
79
|
COLORS = %i[ black red green yellow blue magenta cyan white ]
|
|
@@ -118,11 +166,16 @@ module Typr
|
|
|
118
166
|
str[0..-num-1] + chars )
|
|
119
167
|
end
|
|
120
168
|
|
|
121
|
-
# Strip ANSI escape sequences
|
|
122
|
-
|
|
169
|
+
# Strip ANSI escape sequences and control characters (except \n, \t)
|
|
170
|
+
# from shell-generated text, leaving only printable content.
|
|
171
|
+
def sanitize str
|
|
172
|
+
str.scrub
|
|
173
|
+
.gsub(/\e(?:\[[0-9;?]*[ -\/]*[@-~]|\][^\a\e]*(?:\a|\e\\)|[()=><0A])/, '')
|
|
174
|
+
.gsub(/[\x00-\x08\x0b-\x1f\x7f]/, '')
|
|
175
|
+
end
|
|
123
176
|
|
|
124
|
-
# Display width of +str+ after stripping ANSI escapes.
|
|
125
|
-
def real_size str; Unicode::DisplayWidth.of(
|
|
177
|
+
# Display width of +str+ after stripping ANSI escapes and control chars.
|
|
178
|
+
def real_size str; Unicode::DisplayWidth.of( sanitize(str) ) end
|
|
126
179
|
|
|
127
180
|
# Coerce a string into Integer, Float, or Boolean when it matches those
|
|
128
181
|
# forms (yes/no, true/false); otherwise return it unchanged.
|
|
@@ -196,11 +249,51 @@ module Typr
|
|
|
196
249
|
## Reads a single keypress in raw mode.
|
|
197
250
|
#
|
|
198
251
|
# 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
|
|
252
|
+
# or a Typr::Mouse for a mouse report, or nil when stdin is not a tty or
|
|
253
|
+
# input is unavailable.
|
|
200
254
|
|
|
201
255
|
def self.read_key
|
|
202
|
-
|
|
203
|
-
|
|
256
|
+
read = ->(tty) do
|
|
257
|
+
str = tty.sysread 6
|
|
258
|
+
if str.start_with?("\e[M") and str.size < 6
|
|
259
|
+
str << tty.sysread(6 - str.size) # X10 mouse reports
|
|
260
|
+
elsif str.start_with?("\e[")
|
|
261
|
+
# CSI sequences (arrows, SGR mouse, ...) end with a byte >= 0x40.
|
|
262
|
+
str << tty.sysread(1) until str[-1].ord >= 0x40
|
|
263
|
+
elsif str.start_with?("\eO") and str.size < 3
|
|
264
|
+
str << tty.sysread(3 - str.size) # SS3 (application) cursor keys
|
|
265
|
+
elsif str == "\e" and IO.select([tty], nil, nil, 0.03)
|
|
266
|
+
str << tty.read_nonblock(6) rescue nil # alt+key continuation
|
|
267
|
+
end
|
|
268
|
+
str
|
|
269
|
+
end
|
|
270
|
+
raw = (INPUT.raw{ |tty| read.call tty } rescue nil)
|
|
271
|
+
return unless raw
|
|
272
|
+
decode_mouse(raw) || raw
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
## Decode a raw input string into a {Typr::Mouse} event, or nil when +raw+
|
|
276
|
+
# is not a mouse report. Handles both SGR (CSI < b ; x ; y M/m) and the
|
|
277
|
+
# legacy X10 (CSI M + three bytes) encodings.
|
|
278
|
+
|
|
279
|
+
def self.decode_mouse raw
|
|
280
|
+
if (m = raw.match(/\A\e\[<(\d+);(\d+);(\d+)([Mm])\z/))
|
|
281
|
+
code, x, y, final = m[1].to_i, m[2].to_i, m[3].to_i, m[4]
|
|
282
|
+
elsif raw.start_with?("\e[M")
|
|
283
|
+
code, x, y = raw.getbyte(3) - 32, raw.getbyte(4) - 32, raw.getbyte(5) - 32
|
|
284
|
+
final = code == 3 ? ?m : ?M
|
|
285
|
+
else
|
|
286
|
+
return nil
|
|
287
|
+
end
|
|
288
|
+
modifiers = code & 28
|
|
289
|
+
base = code & ~28
|
|
290
|
+
motion = (base & 32) != 0
|
|
291
|
+
wheel = base >= 64
|
|
292
|
+
button = wheel ? base - 64 + 4 : base & 3
|
|
293
|
+
action = if motion then :motion
|
|
294
|
+
elsif wheel then final == ?m ? :release : :press
|
|
295
|
+
else ( final == ?m or code == 3 ) ? :release : :press end
|
|
296
|
+
Mouse.new button, x, y, action, modifiers
|
|
204
297
|
end
|
|
205
298
|
|
|
206
299
|
## Interactive line editor used for string prompts (search, %str, ...).
|
|
@@ -220,38 +313,44 @@ module Typr
|
|
|
220
313
|
def self.read_line prompt='', left: 0, top: 0, initial: '', &block
|
|
221
314
|
return $stdin.gets&.chomp unless $stdin.tty?
|
|
222
315
|
query, cursor = initial.dup, initial.length
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
text_width( prompt + query[0...cursor] )
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
316
|
+
$>.print CURSOR_NORMAL
|
|
317
|
+
begin
|
|
318
|
+
loop do
|
|
319
|
+
$>.print "\e[%i;%if" % [ top + 1, left + 1 ]
|
|
320
|
+
$>.print ERASE_LINE
|
|
321
|
+
before = text_width( prompt + query[0...cursor] )
|
|
322
|
+
offset = [ left + before - Typr.width, 0 ].max
|
|
323
|
+
$>.print slice_width( prompt + query, offset, Typr.width - left + 1 )
|
|
324
|
+
$>.print "\e[%i;%if" % [ top + 1, left + 1 + before - offset ]
|
|
325
|
+
key = read_key
|
|
326
|
+
return nil if key.nil? or key == KEY_ESCAPE
|
|
327
|
+
return query if key == KEY_RETURN or key == "\n"
|
|
328
|
+
case key
|
|
329
|
+
when KEY_LEFT, "\e[D"; cursor -= 1 if cursor > 0
|
|
330
|
+
when KEY_RIGHT, "\e[C"; cursor += 1 if cursor < query.length
|
|
331
|
+
when "\e[1;5D", "\eOd"; cursor = word_prev query, cursor
|
|
332
|
+
when "\e[1;5C", "\eOc"; cursor = word_next query, cursor
|
|
333
|
+
when KEY_HOME; cursor = 0
|
|
334
|
+
when KEY_END; cursor = query.length
|
|
335
|
+
when KEY_DC, "\e[3~"; query.slice!(cursor, 1) if cursor < query.length
|
|
336
|
+
when KEY_BACKSPACE, "\b"
|
|
337
|
+
if cursor > 0
|
|
338
|
+
query.slice!(cursor - 1, 1)
|
|
339
|
+
cursor -= 1
|
|
340
|
+
end
|
|
341
|
+
else
|
|
342
|
+
if key.is_a?(String) and key.each_char.all?{ |char| char.ord.between?(32, 126) }
|
|
343
|
+
query.insert(cursor, key)
|
|
344
|
+
cursor += key.length
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
cursor = 0 if cursor < 0
|
|
348
|
+
cursor = query.length if cursor > query.length
|
|
349
|
+
result = block.call( key, query, cursor ) if block
|
|
350
|
+
return result unless result.nil?
|
|
250
351
|
end
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
result = block.call( key, query, cursor ) if block
|
|
254
|
-
return result unless result.nil?
|
|
352
|
+
ensure
|
|
353
|
+
$>.print CURSOR_INVISIBLE
|
|
255
354
|
end
|
|
256
355
|
end
|
|
257
356
|
|
|
@@ -260,6 +359,28 @@ module Typr
|
|
|
260
359
|
Unicode::DisplayWidth.of( str.gsub(/\x1b\[[^m]+m/, '') )
|
|
261
360
|
end
|
|
262
361
|
|
|
362
|
+
# Visible substring of +str+ starting at display-width +offset+, at most
|
|
363
|
+
# +width+ cells wide. ANSI escapes are zero-width and carried through so
|
|
364
|
+
# color state applies inside the window.
|
|
365
|
+
def self.slice_width str, offset, width
|
|
366
|
+
vis = 0
|
|
367
|
+
slice = +""
|
|
368
|
+
i = 0
|
|
369
|
+
while i < str.length
|
|
370
|
+
if str[i] == "\e"
|
|
371
|
+
seq = str[i..][/\A\e(\[[0-9;?]*[ -\/]*[@-~]|\][^\a\e]*(?:\a|\e\\)|[()=>0-9])/]
|
|
372
|
+
slice << seq if seq
|
|
373
|
+
i += seq ? seq.length : 1
|
|
374
|
+
next
|
|
375
|
+
end
|
|
376
|
+
cell = Unicode::DisplayWidth.of(str[i])
|
|
377
|
+
vis += cell
|
|
378
|
+
slice << str[i] if vis > offset and vis <= offset + width
|
|
379
|
+
i += 1
|
|
380
|
+
end
|
|
381
|
+
slice
|
|
382
|
+
end
|
|
383
|
+
|
|
263
384
|
# Move the cursor back to the start of the previous word in +str+.
|
|
264
385
|
def self.word_prev str, cursor
|
|
265
386
|
cursor -= 1 while cursor > 0 and str[cursor - 1] == ' '
|
|
@@ -303,21 +424,25 @@ module Typr
|
|
|
303
424
|
def self.on_resize &block
|
|
304
425
|
trap(:WINCH, &block)
|
|
305
426
|
end
|
|
306
|
-
# Enter interactive mode: seed the default colors, hide the cursor
|
|
307
|
-
#
|
|
427
|
+
# Enter interactive mode: seed the default colors, hide the cursor, enable
|
|
428
|
+
# key-mode (application) escapes and mouse reporting when stdin is a tty.
|
|
308
429
|
def self.init default=[ :white, :black ]
|
|
309
430
|
$default = default.dup
|
|
310
431
|
$color = $default.dup
|
|
311
432
|
if $stdin.tty?
|
|
312
433
|
print CURSOR_INVISIBLE
|
|
313
|
-
|
|
434
|
+
print KEYPAD_XMIT if defined?(KEYPAD_XMIT)
|
|
435
|
+
print MOUSE_ON
|
|
314
436
|
end
|
|
315
437
|
end
|
|
316
|
-
# Restore the terminal: show the cursor, reset colors
|
|
438
|
+
# Restore the terminal: show the cursor, reset keypad/colors, disable mouse
|
|
439
|
+
# reporting and clear.
|
|
317
440
|
def self.exit;
|
|
318
441
|
extend self
|
|
319
442
|
if $stdin.tty?
|
|
320
443
|
$>.print CURSOR_NORMAL;
|
|
444
|
+
$>.print KEYPAD_LOCAL if defined?(KEYPAD_LOCAL)
|
|
445
|
+
$>.print MOUSE_OFF
|
|
321
446
|
$>.print ORIG_COLORS; color; clear
|
|
322
447
|
end
|
|
323
448
|
end
|
data/lib/text.rb
CHANGED
|
@@ -191,6 +191,8 @@ module Typr
|
|
|
191
191
|
self << @input
|
|
192
192
|
end
|
|
193
193
|
|
|
194
|
+
# Reset state: +:search+ clears the pattern and highlights; +:all+ resets
|
|
195
|
+
# position, selection, and search; else falls through to {Stack#reset}.
|
|
194
196
|
def reset type=:all
|
|
195
197
|
case type
|
|
196
198
|
when :search; highlight_clear
|
|
@@ -201,10 +203,13 @@ module Typr
|
|
|
201
203
|
|
|
202
204
|
private
|
|
203
205
|
|
|
206
|
+
# Re-apply search for a live query (empty clears the highlights).
|
|
204
207
|
def live_search query
|
|
205
208
|
query.empty? ? highlight_clear : search(query)
|
|
206
209
|
end
|
|
207
210
|
|
|
211
|
+
# Move to the next/previous match from the current position; returns the
|
|
212
|
+
# matched line id or nil when there are none.
|
|
208
213
|
def step
|
|
209
214
|
matches = @search[:matches]
|
|
210
215
|
return nil if matches.empty?
|
|
@@ -221,6 +226,8 @@ module Typr
|
|
|
221
226
|
return id
|
|
222
227
|
end
|
|
223
228
|
|
|
229
|
+
# Interactive `/` prompt via Typr.read_line; Escape restores the previous
|
|
230
|
+
# search state, Enter searches (empty restores the last pattern).
|
|
224
231
|
def search_prompt
|
|
225
232
|
query = @search[:pattern].is_a?(String) ? @search[:pattern].dup : ''
|
|
226
233
|
saved = { re: @re, pattern: @search[:pattern],
|
data/lib/typr.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# typr is a Ruby library for building interactive terminal-based user interfaces.
|
|
2
2
|
# It provides layout primitives, widgets, and event-driven interaction.
|
|
3
3
|
#
|
|
4
|
-
# ==
|
|
4
|
+
# == Classes
|
|
5
5
|
#
|
|
6
6
|
# * Typr::Grid - Sortable, filterable table with formatted columns and row selection
|
|
7
7
|
# * Typr::Text - Scrollable text viewer with word/line picking and search
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
#
|
|
13
13
|
# == Terminal Control
|
|
14
14
|
#
|
|
15
|
-
# Typr.init # Initialize terminal (hide cursor, enable key mode)
|
|
15
|
+
# Typr.init # Initialize terminal (hide cursor, enable key mode and mouse reporting)
|
|
16
16
|
# Typr.clear # Clear entire screen
|
|
17
17
|
# Typr.clear :line # Clear current line only
|
|
18
18
|
# key = Typr.read_key # Read a single keypress
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
#
|
|
22
22
|
# == Layout Properties
|
|
23
23
|
#
|
|
24
|
-
# All
|
|
24
|
+
# All classes accept +left+, +top+, +right+, +bottom+ as:
|
|
25
25
|
# Integer - absolute position
|
|
26
26
|
# Float - fraction of terminal (0.0..1.0)
|
|
27
27
|
# Proc - evaluated each layout pass
|