typr 1.3.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 +20 -0
- data/lib/browser.rb +57 -37
- data/lib/graphical.rb +1 -1
- data/lib/grid.rb +8 -1
- data/lib/line.rb +4 -2
- data/lib/stack.rb +10 -2
- data/lib/terminal.rb +76 -39
- data/typr.gemspec +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65f06238b297e2c6f925f139336c89b5606e23c7a08a6fb94ed82848490e9e71
|
|
4
|
+
data.tar.gz: d958ec7e5f7cd5857c2b559b3e32327b590652c8fb759f0557effb33d8c27551
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d651d702b4e5d019636814a021ef4ab62f300b841f482de5ed4aac8f3cc3fe636802ac34551b107bfaef2c19723805e7742fafa95ada76cb6c20f91066e101b
|
|
7
|
+
data.tar.gz: 723ef1823511febee158925266361dbe0afda106ad3974fc1f75edd83f31e5a2ed6be272e634762bbf6797df41e6e27d24c908fe48902892fcf1c34271f1d6d1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v1.4.0] — 2026-08-15
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Browser#pick :directory` confirms the current directory with Return (clearing filters and sorting first), so callers can accept the directory itself; the `pick` type filter only applies to directory and MIME-type picks — `:file` and custom types like `:cmd` show the full listing
|
|
7
|
+
- `sanitize` terminal helper — strips ANSI escape sequences and control characters (except `\n`, `\t`) from shell-generated text
|
|
8
|
+
- Alt+digit hint selection — pressing Alt + a hint digit (sent as `ESC` + digit) selects that hint row and sets a `modifier` accessor on the picker (`:alt`), so callers can detect the modified selection; `Stack#pick` and `Grid#pick` both reset `modifier` to `nil` at the start of each pick
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- `Browser#switch_to` renamed to `change_view` — clearer name for switching column views (callers in `cd` and the `views` popup updated)
|
|
12
|
+
- `read_line` scrolls horizontally instead of wrapping when the query exceeds the terminal width — the cursor stays pinned to the right edge of the line while overflowing
|
|
13
|
+
- `read_line` now shows the terminal cursor while editing and hides it again on exit (previously the cursor stayed invisible after `Typr.init`)
|
|
14
|
+
- `real_size` now measures via `sanitize`, so non-SGR CSI/OSC sequences and control characters count as zero width; the `printables` helper was removed in favor of `sanitize`
|
|
15
|
+
- Sort, filter, and view popups open in `/` search mode — typing narrows the list and a hint digit picks the result; `Line#ask` Array questions can forward pick options as a 4th element (e.g. `{ column: 0 }`)
|
|
16
|
+
- Default hints are digits only (`0-9`) — typing letters in any picker always filters/navigates instead of selecting by hint; explicit `hints:` overrides removed from `Browser`
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Popup grids keep their height while live-filtering in `/` search mode — the box no longer collapses as rows are filtered out
|
|
20
|
+
- `read_key` assembles split escape sequences — CSI input (cursor keys, SGR mouse) reads on to its final byte, SS3 (`\eO`) completes to three bytes, and a lone `ESC` briefly polls for an Alt+key continuation, so a split key can no longer be mistaken for a plain Escape (which would close a popup or exit)
|
|
21
|
+
- Spec cleanup — merged the duplicated `Text#print` describes, removed the redundant empty-input edge case, dropped stack-spec `#pick` examples that duplicated grid-spec coverage, and updated the browser username specs to stub the passwd lookup (the info bar intentionally shows the real user, not stale `$USER`) plus the grid format-reset spec to assert `@widths` clearing instead of the removed `@maxed` state
|
|
22
|
+
|
|
3
23
|
## [v1.3.0] — 2026-08-03
|
|
4
24
|
|
|
5
25
|
### Added
|
data/lib/browser.rb
CHANGED
|
@@ -63,33 +63,25 @@ module Typr
|
|
|
63
63
|
Dir.chdir( @directory = File.expand_path( dir ) )
|
|
64
64
|
end
|
|
65
65
|
data = Dir.new(@directory).children.reject{|f|
|
|
66
|
-
!@show_hidden and f[0] == ?. }.map
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
!@show_hidden and f[0] == ?. }.map { |file| scan_file file }
|
|
67
|
+
|
|
68
|
+
data.each_with_index{ |row, id|
|
|
69
|
+
file = row[NAME]
|
|
70
|
+
ftype = File.ftype(file) rescue next
|
|
71
|
+
ftype = File.stat(file).ftype if ftype == 'link' and File.exist?(file)
|
|
72
|
+
mime = { 'directory' => 'inode/directory', 'characterSpecial' => 'inode/chardevice',
|
|
73
|
+
'blockSpecial' => 'inode/blockdevice', 'link' => 'inode/symlink',
|
|
74
|
+
'fifo' => 'inode/fifo', 'socket' => 'inode/socket' }[ftype]
|
|
75
|
+
unless mime
|
|
76
|
+
if @mimetype_db and ext = file.match(/.+\.(\w+)$/)
|
|
77
|
+
mime = MIMETYPES[ext[1].to_sym]
|
|
78
|
+
end
|
|
79
|
+
missing[file] = id unless mime
|
|
70
80
|
end
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
type = stat.ftype.to_sym
|
|
74
|
-
|
|
75
|
-
mime = { "directory": "inode/directory",
|
|
76
|
-
"characterSpecial": "inode/chardevice",
|
|
77
|
-
"blockSpecial": "inode/blockdevice", "link": "inode/symlink",
|
|
78
|
-
"fifo": "inode/fifo", "socket": "inode/socket"}[type]
|
|
79
|
-
if type == :file and @mimetype_db
|
|
80
|
-
ext = file.match(/.+\.(\w+)$/)
|
|
81
|
-
mime ||= MIMETYPES[ext[1].to_sym] if ext
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
( missing[file] = id ; mime = "?/?" ) unless mime
|
|
85
|
-
[ file, target, stat.mode.to_s(8)[-3..-1].chars.map{|d|
|
|
86
|
-
"rwx".chars.map.with_index{|c,i| d.to_i[2-i]==0 ? ?- : c } }.join,
|
|
87
|
-
File.executable?(file), stat.uid, stat.gid, stat.size,
|
|
88
|
-
stat.atime.to_i, stat.mtime.to_i, stat.ctime.to_i,
|
|
89
|
-
*mime.to_s.split( ?/ ) ]
|
|
90
|
-
end
|
|
81
|
+
row[-2,2] = mime.to_s.split( ?/ ) if mime
|
|
82
|
+
}
|
|
91
83
|
|
|
92
|
-
if @mimetype_magic and
|
|
84
|
+
if @mimetype_magic and missing.any?
|
|
93
85
|
begin
|
|
94
86
|
out = IO.popen(['file', '--mime-type', *missing.keys]){ |io| io.read }
|
|
95
87
|
rescue Errno::ENOENT
|
|
@@ -106,12 +98,33 @@ module Typr
|
|
|
106
98
|
filter
|
|
107
99
|
unless append
|
|
108
100
|
@directory += ?/ unless @directory == ?/
|
|
109
|
-
|
|
101
|
+
change_view @view
|
|
110
102
|
reset :position
|
|
111
103
|
end
|
|
112
104
|
return
|
|
113
105
|
end
|
|
114
106
|
|
|
107
|
+
##
|
|
108
|
+
# Build a row for a single directory entry from filesystem metadata only.
|
|
109
|
+
# MIME type detection (extension database and +file --mime-type+) happens
|
|
110
|
+
# in a batch pass inside {#cd}; type/subtype are left as +"?"+ here.
|
|
111
|
+
#
|
|
112
|
+
# browser.scan_file "image.png" # => 12-column row
|
|
113
|
+
|
|
114
|
+
def scan_file file
|
|
115
|
+
target = nil
|
|
116
|
+
if File.symlink? file
|
|
117
|
+
target = File.readlink( file ) rescue $!.to_s
|
|
118
|
+
return [file, target, "????", false, "?", "?", 0, 0, 0, 0, "?", "?"] unless File.exist?(file)
|
|
119
|
+
end
|
|
120
|
+
stat = File.stat(file) rescue nil
|
|
121
|
+
return [file, "", "????", false, "?", "?", 0, 0, 0, 0, "?", "?"] unless stat
|
|
122
|
+
[ file, target, stat.mode.to_s(8)[-3..-1].chars.map{|d|
|
|
123
|
+
"rwx".chars.map.with_index{|c,i| d.to_i[2-i]==0 ? ?- : c } }.join,
|
|
124
|
+
( File.executable?(file) and stat.file? ), stat.uid, stat.gid, stat.size,
|
|
125
|
+
stat.atime.to_i, stat.mtime.to_i, stat.ctime.to_i, "?", "?" ]
|
|
126
|
+
end
|
|
127
|
+
|
|
115
128
|
##
|
|
116
129
|
# Colors symlink names green/red based on target existence and applies
|
|
117
130
|
# per-MIME-type foreground colors from +@colors[:types]+.
|
|
@@ -127,11 +140,11 @@ module Typr
|
|
|
127
140
|
##
|
|
128
141
|
# Switch to a named or indexed column view. Resets format widths.
|
|
129
142
|
#
|
|
130
|
-
# browser.
|
|
131
|
-
# browser.
|
|
132
|
-
# browser.
|
|
143
|
+
# browser.change_view :full # all columns
|
|
144
|
+
# browser.change_view 0 # first view key
|
|
145
|
+
# browser.change_view :stats # name, size, perms, owner, group, modified
|
|
133
146
|
|
|
134
|
-
def
|
|
147
|
+
def change_view view=nil
|
|
135
148
|
return unless view
|
|
136
149
|
view = @views.keys[ view ] if view.is_a? Integer
|
|
137
150
|
@sequence = @views[ view ].map{|title| @header.index title.to_s}
|
|
@@ -227,7 +240,7 @@ module Typr
|
|
|
227
240
|
# browser.filter_view # prompts for filter text
|
|
228
241
|
|
|
229
242
|
def filter_view; add_filter *@user.ask(
|
|
230
|
-
filter: [ popup( type: :"[f]ilter" ), :row, NAME], with: :line ) end
|
|
243
|
+
filter: [ popup( type: :"[f]ilter" ), :row, NAME, { column: 0 }], with: :line ) end
|
|
231
244
|
|
|
232
245
|
## Open live `/` search filtered on the name column.
|
|
233
246
|
#
|
|
@@ -243,13 +256,13 @@ module Typr
|
|
|
243
256
|
#
|
|
244
257
|
# browser.sort_view # pick a column header to sort
|
|
245
258
|
|
|
246
|
-
def sort_view; sort_by @user.ask( "sort by": [ popup( type: :"[s]ort" ), :row] ) end
|
|
259
|
+
def sort_view; sort_by @user.ask( "sort by": [ popup( type: :"[s]ort" ), :row, nil, { column: 0 }] ) end
|
|
247
260
|
|
|
248
261
|
## Open a view-picker popup and switch to the selected view.
|
|
249
262
|
#
|
|
250
263
|
# browser.views # choose :simple, :compact, :stats, or :full
|
|
251
264
|
|
|
252
|
-
def views;
|
|
265
|
+
def views; change_view( popup( type: :"[v]iews", input: @views.keys ).pick( :row, column: 0 ) ) end
|
|
253
266
|
|
|
254
267
|
## True when the row is a directory or passes base Grid filtering.
|
|
255
268
|
#
|
|
@@ -273,16 +286,22 @@ module Typr
|
|
|
273
286
|
type ||= 'file'
|
|
274
287
|
type = type.to_s
|
|
275
288
|
return super(type, **kw) if type[/row|column|field|none/]
|
|
276
|
-
|
|
277
|
-
|
|
289
|
+
types = %w[audio video image text application inode]
|
|
290
|
+
add_filter( types.include?( type ) ? TYPE : SUBTYPE, type ) if type == 'directory' or types.include?( type )
|
|
278
291
|
loop do
|
|
279
292
|
show
|
|
280
293
|
draw_hints :row
|
|
281
294
|
key = Typr.read_key
|
|
282
295
|
if key == @keymap[:exit]
|
|
283
|
-
|
|
296
|
+
@filters.clear
|
|
297
|
+
sort
|
|
284
298
|
return
|
|
285
299
|
end
|
|
300
|
+
if key == @keymap[:confirm] and type == 'directory'
|
|
301
|
+
@filters.clear
|
|
302
|
+
sort
|
|
303
|
+
return @directory
|
|
304
|
+
end
|
|
286
305
|
if key.is_a?(Typr::Mouse)
|
|
287
306
|
if key.wheel? and key.press?
|
|
288
307
|
send( key.wheel_up? ? @keymap[:up] : @keymap[:down] )
|
|
@@ -298,7 +317,8 @@ module Typr
|
|
|
298
317
|
if file[SUBTYPE] == 'directory'; cd file[TARGET] || file[NAME]; next
|
|
299
318
|
else choice = file[NAME] end
|
|
300
319
|
end
|
|
301
|
-
|
|
320
|
+
@filters.clear
|
|
321
|
+
sort
|
|
302
322
|
return choice
|
|
303
323
|
end
|
|
304
324
|
end
|
data/lib/graphical.rb
CHANGED
data/lib/grid.rb
CHANGED
|
@@ -262,12 +262,18 @@ module Typr
|
|
|
262
262
|
@filters = [[column, '']]
|
|
263
263
|
@map = base.select{ |id| check id }
|
|
264
264
|
@start = 0
|
|
265
|
+
@bottom ||= bottom
|
|
265
266
|
show
|
|
266
267
|
draw_hints
|
|
268
|
+
@modifier = nil
|
|
267
269
|
result = Typr.read_line '/', left: left, top: self.bottom + 1,
|
|
268
270
|
&->(key, query, _) {
|
|
269
271
|
if key.is_a?(String) and idx = @hints[0..height-@hints_start-1].index(key)
|
|
270
272
|
return page.to_a[idx + @hints_start]
|
|
273
|
+
elsif key.is_a?(String) and digit = key[/\A\e(\d)\z/, 1] and
|
|
274
|
+
idx = @hints[0..height-@hints_start-1].index(digit)
|
|
275
|
+
@modifier = :alt
|
|
276
|
+
return page.to_a[idx + @hints_start]
|
|
271
277
|
end
|
|
272
278
|
if key.is_a?(String) and !key.each_char.all?{ |c| c.ord.between?(32, 126) } and
|
|
273
279
|
@keymap.values.include?(key) and ![KEY_BACKSPACE, "\b"].include?(key)
|
|
@@ -406,7 +412,8 @@ module Typr
|
|
|
406
412
|
header = ( row == :header )
|
|
407
413
|
return draw @header[0..width-1].ljust(width) if
|
|
408
414
|
header and @header.is_a? String
|
|
409
|
-
|
|
415
|
+
return unless row_data = header ? @header : @data[row]
|
|
416
|
+
fields = row_data.dup
|
|
410
417
|
@layer[row].each{ |col,value| fields[col] = value } if
|
|
411
418
|
@layer[row] if @layer unless header
|
|
412
419
|
for col,id in sequence.each_with_index
|
data/lib/line.rb
CHANGED
|
@@ -64,7 +64,9 @@ class Line < Space
|
|
|
64
64
|
#
|
|
65
65
|
# The +type+ selects how the answer is collected:
|
|
66
66
|
# Array - interactive pick via <widget>.pick(<type>); an optional third
|
|
67
|
-
# element renders the chosen row's field into the line
|
|
67
|
+
# element renders the chosen row's field into the line, and an
|
|
68
|
+
# optional fourth element is passed as pick keyword arguments
|
|
69
|
+
# (e.g. { column: 0 } to open the pick in / search mode)
|
|
68
70
|
# :key - a single keypress via Typr.read_key
|
|
69
71
|
# :line - interactive line editor via Typr.read_line at the cursor
|
|
70
72
|
#
|
|
@@ -95,7 +97,7 @@ class Line < Space
|
|
|
95
97
|
draw prepare( colored, width, @align, @trim )
|
|
96
98
|
color( @colors[:answer] )
|
|
97
99
|
case object
|
|
98
|
-
when Array then answer << object.first.pick( object[1] )
|
|
100
|
+
when Array then answer << object.first.pick( object[1], **(object[3] || {}) )
|
|
99
101
|
when :key then answer << Typr.read_key
|
|
100
102
|
when :line then answer << Typr.read_line( colored + color_code( @colors[:answer] ),
|
|
101
103
|
left: left, top: top )
|
data/lib/stack.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Typr
|
|
|
21
21
|
## +alternate+ toggles striped rows (+:grey10+ background alternating with the default bg).
|
|
22
22
|
|
|
23
23
|
attr_accessor :start, :keymap, :selected, :header
|
|
24
|
-
attr_accessor :separator, :hints, :hints_start
|
|
24
|
+
attr_accessor :separator, :hints, :hints_start, :modifier
|
|
25
25
|
|
|
26
26
|
##
|
|
27
27
|
# Returns a +Range+ of internal data indices visible on the current page.
|
|
@@ -213,6 +213,7 @@ module Typr
|
|
|
213
213
|
def pick type = :row, row=0 #, key=nil
|
|
214
214
|
type = type.to_s
|
|
215
215
|
multiple = type[-1] == ?s
|
|
216
|
+
@modifier = nil
|
|
216
217
|
loop do
|
|
217
218
|
if type['field']
|
|
218
219
|
row = pick( :row ) or return
|
|
@@ -237,6 +238,13 @@ module Typr
|
|
|
237
238
|
relative = value + @start if type['relative_']
|
|
238
239
|
if type['row'] then value = page.to_a[ value ]
|
|
239
240
|
elsif type['column'] and @sequence; value = @sequence[value] end
|
|
241
|
+
elsif key.is_a?(String) and digit = key[/\A\e(\d)\z/, 1] and
|
|
242
|
+
value = @hints[0..limit-@hints_start-1].index(digit)
|
|
243
|
+
@modifier = :alt
|
|
244
|
+
value += @hints_start
|
|
245
|
+
relative = value + @start if type['relative_']
|
|
246
|
+
if type['row'] then value = page.to_a[ value ]
|
|
247
|
+
elsif type['column'] and @sequence; value = @sequence[value] end
|
|
240
248
|
end unless type['none']
|
|
241
249
|
end
|
|
242
250
|
|
|
@@ -294,7 +302,7 @@ module Typr
|
|
|
294
302
|
@keymap = { cycle: KEY_TAB, confirm: KEY_RETURN, exit: KEY_ESCAPE,
|
|
295
303
|
page_down: KEY_PAGEDOWN, page_up: KEY_PAGEUP, up: KEY_UP,
|
|
296
304
|
down: KEY_DOWN, to_top: KEY_HOME, to_bottom: KEY_END }.merge @keymap
|
|
297
|
-
@hints ||= "
|
|
305
|
+
@hints ||= "1234567890"
|
|
298
306
|
end
|
|
299
307
|
end
|
|
300
308
|
|
data/lib/terminal.rb
CHANGED
|
@@ -166,11 +166,16 @@ module Typr
|
|
|
166
166
|
str[0..-num-1] + chars )
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
-
# Strip ANSI escape sequences
|
|
170
|
-
|
|
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
|
|
171
176
|
|
|
172
|
-
# Display width of +str+ after stripping ANSI escapes.
|
|
173
|
-
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
|
|
174
179
|
|
|
175
180
|
# Coerce a string into Integer, Float, or Boolean when it matches those
|
|
176
181
|
# forms (yes/no, true/false); otherwise return it unchanged.
|
|
@@ -250,11 +255,15 @@ module Typr
|
|
|
250
255
|
def self.read_key
|
|
251
256
|
read = ->(tty) do
|
|
252
257
|
str = tty.sysread 6
|
|
253
|
-
if str.start_with?("\e[
|
|
254
|
-
|
|
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.
|
|
255
262
|
str << tty.sysread(1) until str[-1].ord >= 0x40
|
|
256
|
-
elsif str.start_with?("\
|
|
257
|
-
str << tty.sysread(
|
|
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
|
|
258
267
|
end
|
|
259
268
|
str
|
|
260
269
|
end
|
|
@@ -304,38 +313,44 @@ module Typr
|
|
|
304
313
|
def self.read_line prompt='', left: 0, top: 0, initial: '', &block
|
|
305
314
|
return $stdin.gets&.chomp unless $stdin.tty?
|
|
306
315
|
query, cursor = initial.dup, initial.length
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
text_width( prompt + query[0...cursor] )
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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?
|
|
334
351
|
end
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
result = block.call( key, query, cursor ) if block
|
|
338
|
-
return result unless result.nil?
|
|
352
|
+
ensure
|
|
353
|
+
$>.print CURSOR_INVISIBLE
|
|
339
354
|
end
|
|
340
355
|
end
|
|
341
356
|
|
|
@@ -344,6 +359,28 @@ module Typr
|
|
|
344
359
|
Unicode::DisplayWidth.of( str.gsub(/\x1b\[[^m]+m/, '') )
|
|
345
360
|
end
|
|
346
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
|
+
|
|
347
384
|
# Move the cursor back to the start of the previous word in +str+.
|
|
348
385
|
def self.word_prev str, cursor
|
|
349
386
|
cursor -= 1 while cursor > 0 and str[cursor - 1] == ' '
|
data/typr.gemspec
CHANGED