typr 1.4.0 → 1.5.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 +22 -0
- data/README.md +1 -1
- data/example/grid +22 -27
- data/lib/browser.rb +47 -12
- data/lib/grid.rb +21 -3
- data/lib/line.rb +34 -8
- data/lib/space.rb +21 -0
- data/lib/stack.rb +40 -15
- data/lib/terminal.rb +42 -3
- data/lib/text.rb +31 -19
- 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: 6f464a51f6c8356a1571fcebe51bbe315428ec054d9b1b819753a688764ce347
|
|
4
|
+
data.tar.gz: '0367917e4ce7b2f67c0605bb321f358aa81e17f27ab555ab558a6655b646dc9a'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6f3f59d78bfc8831542c843de4eb51aa1507774b95436f97d9cf906f12945808f25c1add663945ad3d94ba8f33a543bc8f2cb960eac2adf995e6c6ec62fb715e
|
|
7
|
+
data.tar.gz: bf0a175749a29714fa07e0e8124b72a031dd9606c7d077cdcf7f214dc81783d28a2cce6b0e265afbbee9155a82cef742916c0a4956522c3edf56b66b6c223bbd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v1.5.0] — 2026-09-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Shell TAB completion in `read_line` — when `completions: true`, each keypress queries the shell's completion system (`/usr/share/bash-completion/completions/[cmd]`) via `IO.popen` and shows a pickable `Grid` above the edit line; selecting an item inserts it at the cursor. Handles `+`, `|`, `=` command prefixes
|
|
7
|
+
- `Space#data_at` / `Space#extract_key` — the clickable-element lookup every widget overrides (`Stack` returns an Integer row id, `Line` returns a String binding, `Browser`'s head maps its filter/sort/views columns to actions) and the trigger-key extraction for labels like "[h]elp"; `Stack#hit` now delegates to `data_at`
|
|
8
|
+
- Mouse dispatch through `Stack#send` — the mouse is handled in `send` itself (wheel scrolls and returns `:scroll`, a left or right press returns `data_at`), so pickers and `Browser#pick` route clicks through `send`; `Browser#send` intercepts clicks on `@head` and `@user` to dispatch their bindings, and `Grid#send` sorts by column header on click
|
|
9
|
+
- `Line#list` — the line's keybindings as a clickable auto-width KEYBINDINGS grid (starting at column 1, floored to fit its header) and `Line#help` as `list.pick :row`: click a row or press its hint digit to return its row id, Escape or Return to return nil. Apps compose their own help around the grid — Filecon aligns a HELP text panel at `list.right + 2` and mirrors navigation by sending every event to both panels
|
|
10
|
+
- Clicking outside a popup cancels it — `Stack#send` returns `:outside` for a press outside a widget's bounds, and pickers (`Stack#pick`, `Grid#pick`'s column filter, `Line#help`) treat it like Escape: state is restored and nil is returned, while the main browser listing ignores outside clicks
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `Browser#send` chains string dispatches from `@head`/`@user` binding clicks — the modifier-while returned immediately after the first `send`, so clicking a bottom-bar binding that opened a sub-screen (e.g. help) and returned a trigger key would drop it; the loop now re-dispatches until the result is no longer a String
|
|
14
|
+
- Mousewheel scrolling works in live-filter popups (`Grid#pick` with `column:`) — the wheel scrolled `@start` but the trailing filter reapply then reset it to the top, so the popup never visibly moved; mouse events now skip that reset and just redraw
|
|
15
|
+
- `extract_key` strips ANSI color codes before matching — colored binding labels like "\e[31m[f]ilter\e[0m" now resolve to their trigger key (previously the color code's own `[31m` matched and returned nil, so colored bottom-bar bindings and help-grid rows were not clickable)
|
|
16
|
+
- `Browser`'s head `data_at` matches the 1-based mouse row — the head action columns (filter/sort/views) are clickable in a real terminal again, not just at test coordinates
|
|
17
|
+
- `Line#data_at` matches the bottom-bar row — mouse coordinates are 1-based while widget rows are 0-based, so the clickable line sat one row above the rendered bindings; the row check now adds 1, matching `Stack#data_at`
|
|
18
|
+
- `Text` rebuilds on width change instead of blanking — `show` re-wraps the existing content whenever the width differs from the width it was built at (a `left` proc that resolves later, or a terminal resize), so a `Text` panel no longer renders an empty body after its first draw
|
|
19
|
+
- `Text` rows fit the panel interior — `print` draws `width - margin` cells so content rows stay inside the box instead of overflowing onto the next line (the overflow wrapped spaces with the alternating background into the neighboring widget's left-border column)
|
|
20
|
+
- `draw_border` allows the last column and row — the guard now only skips `x > width` / `y > height` (genuinely off-screen), so a panel flush with the terminal edge draws its right border and corners on the last column
|
|
21
|
+
- `Text#build` wraps ANSI-colored content correctly — the capacity calculation uses display width instead of byte length, so escape sequences don't inflate the line count
|
|
22
|
+
- `reset_view` reloads the directory again — it calls `cd @directory` before clearing display/selection/position and re-sorting, so reset picks up new and removed files instead of only reshuffling the stale listing
|
|
23
|
+
- Head path lookup — `@head.path_at(x, y)` maps a click to a target directory: the username column returns the resolved home directory, each `/`-separated segment of the directory cell returns that ancestor path (padding, action columns and array listings return nil); mouse `x` is adjusted from 1-based terminal columns so boundary clicks resolve to the cell under the cursor
|
|
24
|
+
|
|
3
25
|
## [v1.4.0] — 2026-08-15
|
|
4
26
|
|
|
5
27
|
### Added
|
data/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Navigation uses **hint numbers** displayed next to selectable items — rows in
|
|
|
23
23
|
|
|
24
24
|
### Mouse support
|
|
25
25
|
|
|
26
|
-
typr reports and decodes mouse input. `Typr.init` enables mouse reporting and `Typr.exit` disables it; `Typr.read_key` decodes SGR and legacy X10 reports into a `Typr::Mouse` struct (`button`, `x`, `y`, `action`, `modifiers`) with `press?`/`release?`/`wheel?`/`left?`/`middle?`/`right?` predicates. Pickers accept the mouse directly: a left click acts like pressing the row's hint digit,
|
|
26
|
+
typr reports and decodes mouse input. `Typr.init` enables mouse reporting and `Typr.exit` disables it; `Typr.read_key` decodes SGR and legacy X10 reports into a `Typr::Mouse` struct (`button`, `x`, `y`, `action`, `modifiers`) with `press?`/`release?`/`wheel?`/`left?`/`middle?`/`right?` predicates. Pickers accept the mouse directly: a left click acts like pressing the row's hint digit, the wheel scrolls the listing, and a click outside the picker's bounds cancels it like Escape. Any widget can expose clickable elements through `Space#data_at` (a row id, a binding label, or an action) and `Stack#send` dispatches mouse events to them — so `Browser`'s head columns and the bottom binding bar are clickable too, and `Line#help` is `Line#list.pick :row` — the line's keybinding grid, exposed so apps can compose their own help beside it (e.g. Filecon aligns a HELP text panel at `list.right + 2` and mirrors navigation to both panels).
|
|
27
27
|
|
|
28
28
|
### Terminal layer
|
|
29
29
|
|
data/example/grid
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Usage:
|
|
5
5
|
# cd example && ruby grid [file]
|
|
6
6
|
#
|
|
7
|
-
# Reads
|
|
7
|
+
# Reads a comma-separated file and displays it as an interactive table.
|
|
8
8
|
#
|
|
9
9
|
# Keys:
|
|
10
10
|
# s Sort by a column (prompts for column number)
|
|
@@ -20,54 +20,49 @@
|
|
|
20
20
|
# The header line shows current time, active sort, filters,
|
|
21
21
|
# and selected rows. The user line shows available commands.
|
|
22
22
|
|
|
23
|
-
require 'csv'
|
|
24
23
|
require_relative '../lib/typr.rb'
|
|
25
24
|
|
|
26
25
|
class GridTest
|
|
27
|
-
# extend Typr
|
|
28
26
|
include Typr
|
|
29
|
-
|
|
27
|
+
|
|
30
28
|
def initialize
|
|
31
|
-
Typr.init
|
|
32
|
-
input =
|
|
29
|
+
Typr.init
|
|
30
|
+
input = $<.read.split("\n").map{|l| l.split(',') }
|
|
33
31
|
header = input.shift
|
|
34
|
-
@grid = Grid.new(
|
|
35
|
-
# input: open( "top5000.csv").read.split.map{|l| l.split(',') } ,
|
|
32
|
+
@grid = Grid.new(
|
|
36
33
|
input: input,
|
|
37
34
|
top: 1, right: -1, bottom: -2,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
colors: { columns: [ :yellow, :blue, :red, :green, :magenta] } )
|
|
43
|
-
@user = Line.new( top: -1, default:
|
|
35
|
+
format: [ 4, :max, 1, :min, :min ],
|
|
36
|
+
header: header,
|
|
37
|
+
colors: { columns: [ :yellow, :blue, :red, :green, :magenta] } )
|
|
38
|
+
@user = Line.new( top: -1, default:
|
|
44
39
|
"(s)ort, (f)ilter, (r)eset, select (R)ows/(C)olumns/(F)ields, [esc] quit")
|
|
45
40
|
@head = Line.new( interval: 1,
|
|
46
|
-
default: proc{ draw " %s, sort:%s filter:%s selected:%s" %
|
|
47
|
-
[ Time.now.strftime("%D %T"),
|
|
48
|
-
(@grid.header[@grid.sorted_by
|
|
49
|
-
@grid.filters.join, @grid.selected
|
|
41
|
+
default: proc{ draw " %s, sort:%s filter:%s selected:%s" %
|
|
42
|
+
[ Time.now.strftime("%D %T"),
|
|
43
|
+
(@grid.header[@grid.sorted_by] if @grid.sorted_by),
|
|
44
|
+
@grid.filters.join, @grid.selected ] } )
|
|
50
45
|
end
|
|
51
46
|
|
|
52
47
|
def run
|
|
53
48
|
Typr.clear
|
|
54
|
-
loop do
|
|
49
|
+
loop do
|
|
55
50
|
@grid.show
|
|
56
51
|
@user.show
|
|
57
|
-
@head.start
|
|
58
|
-
key = Typr.
|
|
52
|
+
@head.start
|
|
53
|
+
key = Typr.read_key
|
|
59
54
|
@head.stop
|
|
60
55
|
case key
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
when KEY_ESCAPE; exit
|
|
57
|
+
when ?` then (line = Typr.read_line) and @user.show(eval(line))
|
|
63
58
|
when ?s then @grid.sort_by( @user.ask "sort column": [@grid, :column] )
|
|
64
59
|
when ?F then @user.ask("pick fields": [@grid, :fields])
|
|
65
60
|
when ?R then @user.ask("pick rows": [@grid, :rows])
|
|
66
61
|
when ?C then @user.ask("pick columns": [@grid, :columns])
|
|
67
|
-
when ?f then @grid.add_filter( *@user.ask("filter column":
|
|
62
|
+
when ?f then @grid.add_filter( *@user.ask("filter column":
|
|
68
63
|
[@grid, :column], with: :line))
|
|
69
|
-
when ?r then @grid.reset; @user.reset
|
|
70
|
-
else @grid.send key; @user.reset
|
|
64
|
+
when ?r then @grid.reset; @user.reset
|
|
65
|
+
else @grid.send key; @user.reset
|
|
71
66
|
end
|
|
72
67
|
end
|
|
73
68
|
end
|
|
@@ -77,4 +72,4 @@ begin
|
|
|
77
72
|
GridTest.new.run
|
|
78
73
|
ensure
|
|
79
74
|
Typr.exit
|
|
80
|
-
end
|
|
75
|
+
end
|
data/lib/browser.rb
CHANGED
|
@@ -78,7 +78,7 @@ module Typr
|
|
|
78
78
|
end
|
|
79
79
|
missing[file] = id unless mime
|
|
80
80
|
end
|
|
81
|
-
row[
|
|
81
|
+
row[TYPE, 2] = mime.to_s.split( ?/ ) if mime
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
if @mimetype_magic and missing.any?
|
|
@@ -90,7 +90,7 @@ module Typr
|
|
|
90
90
|
out.split($/).each{ |line|
|
|
91
91
|
file, mime = line.match(/^(.*):\s+(\w+\/[\w\-\.]+)$/)&.captures
|
|
92
92
|
next unless file
|
|
93
|
-
data[ missing[file] ][
|
|
93
|
+
data[ missing[file] ][TYPE, 2] = mime.split( ?/ ) if missing[file] }
|
|
94
94
|
end
|
|
95
95
|
data.each { |row| row[0] = File.join(@directory, row[0]) }
|
|
96
96
|
self << data
|
|
@@ -115,14 +115,14 @@ module Typr
|
|
|
115
115
|
target = nil
|
|
116
116
|
if File.symlink? file
|
|
117
117
|
target = File.readlink( file ) rescue $!.to_s
|
|
118
|
-
return [file, target, "????", false, "?", "?", 0, 0, 0, 0, "?", "?"] unless File.exist?(file)
|
|
118
|
+
return [file, target, "????", false, "?", "?", 0, 0, 0, 0, "?", "?", nil] unless File.exist?(file)
|
|
119
119
|
end
|
|
120
120
|
stat = File.stat(file) rescue nil
|
|
121
|
-
return [file, "", "????", false, "?", "?", 0, 0, 0, 0, "?", "?"] unless stat
|
|
121
|
+
return [file, "", "????", false, "?", "?", 0, 0, 0, 0, "?", "?", nil] unless stat
|
|
122
122
|
[ file, target, stat.mode.to_s(8)[-3..-1].chars.map{|d|
|
|
123
123
|
"rwx".chars.map.with_index{|c,i| d.to_i[2-i]==0 ? ?- : c } }.join,
|
|
124
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, "?", "?" ]
|
|
125
|
+
stat.atime.to_i, stat.mtime.to_i, stat.ctime.to_i, "?", "?", nil ]
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
##
|
|
@@ -148,6 +148,10 @@ module Typr
|
|
|
148
148
|
return unless view
|
|
149
149
|
view = @views.keys[ view ] if view.is_a? Integer
|
|
150
150
|
@sequence = @views[ view ].map{|title| @header.index title.to_s}
|
|
151
|
+
if @show_path
|
|
152
|
+
@data.each { |row| row[PATH] = File.dirname(row[NAME]) } if @data.any?
|
|
153
|
+
@sequence.insert(@sequence.index(NAME) || 0, PATH) unless @sequence.include?(PATH)
|
|
154
|
+
end
|
|
151
155
|
reset :format
|
|
152
156
|
@view = view
|
|
153
157
|
return
|
|
@@ -160,6 +164,18 @@ module Typr
|
|
|
160
164
|
# browser.send "1" # => row id if hint "1" is visible
|
|
161
165
|
|
|
162
166
|
def send key
|
|
167
|
+
if key.is_a?(Typr::Mouse) and key.press? and key.left?
|
|
168
|
+
[@head, @user].each do |w|
|
|
169
|
+
next unless w.respond_to?(:data_at)
|
|
170
|
+
value = w.data_at(key.x, key.y)
|
|
171
|
+
next unless value
|
|
172
|
+
key = extract_key(value.to_s)
|
|
173
|
+
while key.is_a?(String)
|
|
174
|
+
key = send(key)
|
|
175
|
+
end
|
|
176
|
+
return key
|
|
177
|
+
end
|
|
178
|
+
end
|
|
163
179
|
if key.is_a?(String) and
|
|
164
180
|
id = @hints[0..[height, rows-1+headspace].min-@hints_start-1].index(key)
|
|
165
181
|
return page[id+@hints_start]
|
|
@@ -303,14 +319,12 @@ module Typr
|
|
|
303
319
|
return @directory
|
|
304
320
|
end
|
|
305
321
|
if key.is_a?(Typr::Mouse)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
next
|
|
309
|
-
elsif key.press? and key.left? and choice = hit( key.y, key.x )
|
|
310
|
-
else next end
|
|
322
|
+
choice = send key
|
|
323
|
+
next if choice == :scroll or choice == :outside
|
|
311
324
|
else
|
|
312
325
|
next unless choice = send( key )
|
|
313
326
|
end
|
|
327
|
+
next unless choice
|
|
314
328
|
case choice
|
|
315
329
|
when Array; choice = choice.map{ |id| @data[id][NAME] }.join('" "')
|
|
316
330
|
when Integer; file = @data[ choice ]
|
|
@@ -414,7 +428,7 @@ module Typr
|
|
|
414
428
|
|
|
415
429
|
def initialize args={}
|
|
416
430
|
@mimetype_db, @mimetype_magic = true, true
|
|
417
|
-
@view, @sort, @views, @positions, @show_hidden = :compact, 0, {}, {}, false
|
|
431
|
+
@view, @sort, @views, @positions, @show_hidden, @show_path = :compact, 0, {}, {}, false, false
|
|
418
432
|
@filter_dirs = args[:filter_dirs]
|
|
419
433
|
super
|
|
420
434
|
@left ||= 0
|
|
@@ -442,7 +456,7 @@ module Typr
|
|
|
442
456
|
toggle_hidden: ?H}.merge @keymap
|
|
443
457
|
|
|
444
458
|
self.header = %w[ name target permissions executable owner group size
|
|
445
|
-
accessed modified created type subtype ]
|
|
459
|
+
accessed modified created type subtype path ]
|
|
446
460
|
|
|
447
461
|
@format[NAME] = :max
|
|
448
462
|
@format[TYPE] = 5
|
|
@@ -475,6 +489,27 @@ module Typr
|
|
|
475
489
|
colors: { columns: [@colors[:default], @colors[:default],
|
|
476
490
|
@colors[:actions][:"[f]ilter"], @colors[:actions][:"[s]ort"],
|
|
477
491
|
@colors[:actions][:"[v]iews"]] } )
|
|
492
|
+
@head.define_singleton_method( :data_at ) do |x, y|
|
|
493
|
+
return unless x and y and y.between?( top + 1, bottom + 1 ) and
|
|
494
|
+
x.between?( left, right )
|
|
495
|
+
offs = positions( 0 )
|
|
496
|
+
col = offs.each_index.find { |i| x <= left + offs[i] + width_for( i ) - 1 }
|
|
497
|
+
@parent.instance_variable_get( :@positions ).keys[ col - 2 ] if col and col >= 2
|
|
498
|
+
end
|
|
499
|
+
@head.define_singleton_method( :path_at ) do |x, y|
|
|
500
|
+
return unless x and y and y.between?( top + 1, bottom + 1 )
|
|
501
|
+
x -= 1
|
|
502
|
+
return unless x.between?( left, right )
|
|
503
|
+
offs = positions( 0 )
|
|
504
|
+
col = offs.each_index.find { |i| x <= left + offs[i] + width_for( i ) - 1 }
|
|
505
|
+
return (Etc.getpwuid(Process.euid).dir rescue (Dir.home rescue "~")) if col == 0
|
|
506
|
+
return unless col == 1 and @parent.directory.is_a?( String )
|
|
507
|
+
text = ((@layer and @layer[0] and @layer[0][1]) || @data[0][1].call).to_s
|
|
508
|
+
rel = x - ( left + offs[1] )
|
|
509
|
+
return if rel < 0 or rel >= real_size( text )
|
|
510
|
+
pre = prepare( text, rel + 1 )
|
|
511
|
+
( pre + ( pre.end_with?( "/" ) ? "" : text[pre.length..-1][/\A[^\/]*/] ) ).sub( %r{(?<=.)/$}, "" )
|
|
512
|
+
end
|
|
478
513
|
|
|
479
514
|
@user ||= Line.new( parent:self, left:->{@parent.left},top:->{@parent.bottom+1},
|
|
480
515
|
bindings: %w[ [h]elp [/]:search ] + @colors[:actions].map{|name,color|
|
data/lib/grid.rb
CHANGED
|
@@ -268,7 +268,11 @@ module Typr
|
|
|
268
268
|
@modifier = nil
|
|
269
269
|
result = Typr.read_line '/', left: left, top: self.bottom + 1,
|
|
270
270
|
&->(key, query, _) {
|
|
271
|
-
if key.is_a?(
|
|
271
|
+
if key.is_a?(Typr::Mouse)
|
|
272
|
+
value = send key
|
|
273
|
+
return value if value.is_a?( Integer )
|
|
274
|
+
return :outside if value == :outside
|
|
275
|
+
elsif key.is_a?(String) and idx = @hints[0..height-@hints_start-1].index(key)
|
|
272
276
|
return page.to_a[idx + @hints_start]
|
|
273
277
|
elsif key.is_a?(String) and digit = key[/\A\e(\d)\z/, 1] and
|
|
274
278
|
idx = @hints[0..height-@hints_start-1].index(digit)
|
|
@@ -278,7 +282,7 @@ module Typr
|
|
|
278
282
|
if key.is_a?(String) and !key.each_char.all?{ |c| c.ord.between?(32, 126) } and
|
|
279
283
|
@keymap.values.include?(key) and ![KEY_BACKSPACE, "\b"].include?(key)
|
|
280
284
|
send key
|
|
281
|
-
|
|
285
|
+
elsif !key.is_a?(Typr::Mouse)
|
|
282
286
|
@filters = [[column, query]]
|
|
283
287
|
@map = base.select{ |id| check id }
|
|
284
288
|
@start = 0
|
|
@@ -288,7 +292,7 @@ module Typr
|
|
|
288
292
|
nil
|
|
289
293
|
}
|
|
290
294
|
case result
|
|
291
|
-
when nil;
|
|
295
|
+
when nil, :outside; @filters, @map, @start = saved
|
|
292
296
|
when Integer; return result
|
|
293
297
|
else
|
|
294
298
|
@filters = result.empty? ? saved[0] : saved[0] + [[column, result]]
|
|
@@ -305,6 +309,19 @@ module Typr
|
|
|
305
309
|
#
|
|
306
310
|
# @param column [Integer, Symbol]
|
|
307
311
|
|
|
312
|
+
def send key
|
|
313
|
+
if key.is_a?(Typr::Mouse) and key.press? and key.left? and
|
|
314
|
+
@header.is_a?(Array) and key.y == top + 1
|
|
315
|
+
rel = key.x - left - 1 - @margin.size
|
|
316
|
+
if rel >= 0
|
|
317
|
+
col = positions(0).count{|pos| pos <= rel} - 1
|
|
318
|
+
sort_by(sequence[col]) if col >= 0 && col < sequence.size
|
|
319
|
+
end
|
|
320
|
+
return :scroll
|
|
321
|
+
end
|
|
322
|
+
super
|
|
323
|
+
end
|
|
324
|
+
|
|
308
325
|
def sort_by column
|
|
309
326
|
column = @header.index( column.to_s ) if column.is_a? Symbol
|
|
310
327
|
if @sort and (column == @sort) then @reverse = !@reverse
|
|
@@ -388,6 +405,7 @@ module Typr
|
|
|
388
405
|
gaps = @margin.size + @separator.size * (columns - 1)
|
|
389
406
|
sum = @widths.sum
|
|
390
407
|
@max = sum + gaps - 1
|
|
408
|
+
@max = [ @max, @header.length ].max if @header.is_a?(String)
|
|
391
409
|
space = width - gaps
|
|
392
410
|
if (extra = sum - space ) > 0
|
|
393
411
|
extra.times{ |i| @widths[ @widths.index(@widths.max) ] -= 1 }
|
data/lib/line.rb
CHANGED
|
@@ -39,17 +39,43 @@ class Line < Space
|
|
|
39
39
|
def append(str); show @data+str end
|
|
40
40
|
alias :<< :append
|
|
41
41
|
|
|
42
|
-
#
|
|
42
|
+
# The keybinding list as a clickable grid of the line's own bindings. It
|
|
43
|
+
# auto-sizes to its widest binding (floored to fit the header) and starts
|
|
44
|
+
# at column 1, so apps can align side panels at +list.right + 2+.
|
|
43
45
|
#
|
|
44
|
-
# line.
|
|
45
|
-
# line.help
|
|
46
|
+
# line.list # => a Grid whose rows are the bindings
|
|
46
47
|
|
|
47
|
-
def
|
|
48
|
-
Grid.new(
|
|
48
|
+
def list
|
|
49
|
+
Grid.new( input: @bindings, top: 2, left: 1, bottom: -3,
|
|
49
50
|
header: "KEYBINDINGS",
|
|
50
|
-
colors: { header: [:
|
|
51
|
-
alternate: false, border:
|
|
52
|
-
|
|
51
|
+
colors: { header: [:yellow,:black], border:[:white,:grey10] },
|
|
52
|
+
alternate: false, border: :light )
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Pick a keybinding from the list: click a row (or press its hint digit) to
|
|
56
|
+
# return the row id, Escape or Return to return nil.
|
|
57
|
+
#
|
|
58
|
+
# line.help # => 0 (the picked row) or nil
|
|
59
|
+
|
|
60
|
+
def help; list.pick :row end
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# The binding label under terminal (x, y) on this line, or nil when the
|
|
64
|
+
# click is outside the row or off the current bindings text (see
|
|
65
|
+
# {Space#data_at}).
|
|
66
|
+
#
|
|
67
|
+
# line.data_at 3, 8 # => "[h]elp"
|
|
68
|
+
|
|
69
|
+
def data_at x, y
|
|
70
|
+
return unless x and y and y == top + 1 and x.between?( left, right ) and
|
|
71
|
+
@bindings and @data == @bindings.join( ' ' )
|
|
72
|
+
pos = left
|
|
73
|
+
@bindings.each do |b|
|
|
74
|
+
width = real_size( b )
|
|
75
|
+
return b if x.between?( pos, pos + width - 1 )
|
|
76
|
+
pos += width + 1
|
|
77
|
+
end
|
|
78
|
+
nil
|
|
53
79
|
end
|
|
54
80
|
|
|
55
81
|
# Reset the line to its default text or prompt.
|
data/lib/space.rb
CHANGED
|
@@ -46,6 +46,27 @@ eval( method % ([name]*11) ) }
|
|
|
46
46
|
# Viewport height in cells (bottom - top + 1).
|
|
47
47
|
def height; bottom - top + 1 end
|
|
48
48
|
|
|
49
|
+
##
|
|
50
|
+
# The clickable element at terminal (x, y), or nil when outside the widget.
|
|
51
|
+
# Widgets with data override this: Stack returns an Integer row id, Line
|
|
52
|
+
# returns a String binding. Applications dispatch mouse events by iterating
|
|
53
|
+
# their spaces and taking the first non-nil result.
|
|
54
|
+
#
|
|
55
|
+
# space.data_at 3, 8 # => nil (base Space has no data)
|
|
56
|
+
|
|
57
|
+
def data_at x, y; nil end
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# The trigger key for a binding label like "[h]elp" or "[/]:search", or nil
|
|
61
|
+
# for named keys ("[up]", "[esc]") which stay keyboard-only.
|
|
62
|
+
#
|
|
63
|
+
# line.extract_key "[R]ecurse" # => "R"
|
|
64
|
+
|
|
65
|
+
def extract_key binding
|
|
66
|
+
key = sanitize( binding.to_s )[/\[([^\]]+)\]/, 1]
|
|
67
|
+
key if key and key.size == 1
|
|
68
|
+
end
|
|
69
|
+
|
|
49
70
|
# Redraw every +interval+ seconds from a background thread.
|
|
50
71
|
def start; @updater = Thread.new{ loop{show; sleep @interval }} end
|
|
51
72
|
# Stop the background refresh thread.
|
data/lib/stack.rb
CHANGED
|
@@ -45,10 +45,18 @@ module Typr
|
|
|
45
45
|
#
|
|
46
46
|
# stack.hit 3, 5 # => 0 (row 3, col 5 hits data row 0)
|
|
47
47
|
|
|
48
|
-
def hit row, col
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
def hit row, col; data_at col, row end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# The data row id at terminal (x, y), or nil when the click is outside the
|
|
52
|
+
# viewport (see {Space#data_at}).
|
|
53
|
+
#
|
|
54
|
+
# stack.data_at 5, 3 # => 0
|
|
55
|
+
|
|
56
|
+
def data_at x, y
|
|
57
|
+
return unless x and y
|
|
58
|
+
rel = y - 1 - top - headspace
|
|
59
|
+
return unless rel.between?(0, height - 1) and x.between?(left, right)
|
|
52
60
|
page.to_a[rel]
|
|
53
61
|
end
|
|
54
62
|
|
|
@@ -146,6 +154,18 @@ module Typr
|
|
|
146
154
|
# stack.send Typr::KEY_RETURN # confirms selection
|
|
147
155
|
|
|
148
156
|
def send key#, map=nil
|
|
157
|
+
if key.is_a?(Typr::Mouse)
|
|
158
|
+
if key.wheel? and key.press?
|
|
159
|
+
send( key.wheel_up? ? @keymap[:up] : @keymap[:down] )
|
|
160
|
+
return :scroll
|
|
161
|
+
end
|
|
162
|
+
if key.press? and ( key.left? or key.right? )
|
|
163
|
+
return :outside unless key.x and key.y and
|
|
164
|
+
key.x.between?( left, right ) and key.y.between?( top + 1, bottom + 1 )
|
|
165
|
+
return data_at( key.x, key.y )
|
|
166
|
+
end
|
|
167
|
+
return
|
|
168
|
+
end
|
|
149
169
|
response = eval @keymap.invert[key].to_s if @keymap.values.include? key
|
|
150
170
|
@start = rows - height if @start > rows - height
|
|
151
171
|
@start = 0 if @start < 0
|
|
@@ -225,27 +245,25 @@ module Typr
|
|
|
225
245
|
limit = type['row'] ? height : positions(row).count
|
|
226
246
|
key = Typr.read_key
|
|
227
247
|
if key.is_a?(Typr::Mouse)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
end
|
|
235
|
-
elsif key.is_a?(String) and value =
|
|
248
|
+
value = send( key )
|
|
249
|
+
next if value == :scroll
|
|
250
|
+
return if value == :outside
|
|
251
|
+
value = nil unless type['row'] and key.left?
|
|
252
|
+
relative = value if value and type['relative_']
|
|
253
|
+
elsif !type['none'] and key.is_a?(String) and value =
|
|
236
254
|
@hints[0..limit-@hints_start-1].index(key)
|
|
237
255
|
value += @hints_start
|
|
238
256
|
relative = value + @start if type['relative_']
|
|
239
257
|
if type['row'] then value = page.to_a[ value ]
|
|
240
258
|
elsif type['column'] and @sequence; value = @sequence[value] end
|
|
241
|
-
elsif key.is_a?(String) and digit = key[/\A\e(\d)\z/, 1] and
|
|
259
|
+
elsif !type['none'] and key.is_a?(String) and digit = key[/\A\e(\d)\z/, 1] and
|
|
242
260
|
value = @hints[0..limit-@hints_start-1].index(digit)
|
|
243
261
|
@modifier = :alt
|
|
244
262
|
value += @hints_start
|
|
245
263
|
relative = value + @start if type['relative_']
|
|
246
264
|
if type['row'] then value = page.to_a[ value ]
|
|
247
265
|
elsif type['column'] and @sequence; value = @sequence[value] end
|
|
248
|
-
end
|
|
266
|
+
end
|
|
249
267
|
end
|
|
250
268
|
|
|
251
269
|
if value
|
|
@@ -278,6 +296,13 @@ module Typr
|
|
|
278
296
|
return
|
|
279
297
|
end
|
|
280
298
|
|
|
299
|
+
def cycle_back
|
|
300
|
+
max = [height-1, rows-1+headspace].min
|
|
301
|
+
@hints_start -= @hints.size
|
|
302
|
+
@hints_start = (max / @hints.size) * @hints.size if @hints_start < 0
|
|
303
|
+
return
|
|
304
|
+
end
|
|
305
|
+
|
|
281
306
|
##
|
|
282
307
|
# Constructs a Stack widget.
|
|
283
308
|
#
|
|
@@ -299,7 +324,7 @@ module Typr
|
|
|
299
324
|
@colors = { hints: [255, :black ], header: [232,:grey60 ],
|
|
300
325
|
selected: :grey25, alternate: :grey10, rows: {} }.merge @colors
|
|
301
326
|
@selected = { fields:[], columns:[], rows:[] }.merge @selected
|
|
302
|
-
@keymap = { cycle: KEY_TAB, confirm: KEY_RETURN, exit: KEY_ESCAPE,
|
|
327
|
+
@keymap = { cycle: KEY_TAB, cycle_back: KEY_BACK_TAB, confirm: KEY_RETURN, exit: KEY_ESCAPE,
|
|
303
328
|
page_down: KEY_PAGEDOWN, page_up: KEY_PAGEUP, up: KEY_UP,
|
|
304
329
|
down: KEY_DOWN, to_top: KEY_HOME, to_bottom: KEY_END }.merge @keymap
|
|
305
330
|
@hints ||= "1234567890"
|
data/lib/terminal.rb
CHANGED
|
@@ -49,6 +49,10 @@ module Typr
|
|
|
49
49
|
KEY_ESCAPE = "\e"
|
|
50
50
|
KEY_RETURN = CARRIAGE_RETURN
|
|
51
51
|
KEY_TAB = "\t"
|
|
52
|
+
KEY_BACK_TAB = "\e[Z"
|
|
53
|
+
KEY_ALT_BACKSPACE = "\e\x7f"
|
|
54
|
+
KEY_ALT_LEFT = "\e[1;3D"
|
|
55
|
+
KEY_ALT_RIGHT = "\e[1;3C"
|
|
52
56
|
KEY_PAGEDOWN = KEY_NPAGE
|
|
53
57
|
KEY_PAGEUP = KEY_PPAGE
|
|
54
58
|
|
|
@@ -56,6 +60,8 @@ module Typr
|
|
|
56
60
|
# terminfo: 1000 = button press/release/wheel, 1006 = SGR coordinates.
|
|
57
61
|
MOUSE_ON = "\e[?1000h\e[?1006h"
|
|
58
62
|
MOUSE_OFF = "\e[?1000l\e[?1006l"
|
|
63
|
+
ALT_SCREEN_ON = "\e[?1049h"
|
|
64
|
+
ALT_SCREEN_OFF = "\e[?1049l"
|
|
59
65
|
|
|
60
66
|
# A mouse event decoded from the input stream. +x+ / +y+ are 1-based
|
|
61
67
|
# terminal coordinates; +button+ is 0=left, 1=middle, 2=right, or 4-7 for
|
|
@@ -310,7 +316,7 @@ module Typr
|
|
|
310
316
|
# nil
|
|
311
317
|
# end
|
|
312
318
|
|
|
313
|
-
def self.read_line prompt='', left: 0, top: 0, initial: '', &block
|
|
319
|
+
def self.read_line prompt='', left: 0, top: 0, initial: '', completions: nil, redraw: nil, &block
|
|
314
320
|
return $stdin.gets&.chomp unless $stdin.tty?
|
|
315
321
|
query, cursor = initial.dup, initial.length
|
|
316
322
|
$>.print CURSOR_NORMAL
|
|
@@ -328,11 +334,15 @@ module Typr
|
|
|
328
334
|
case key
|
|
329
335
|
when KEY_LEFT, "\e[D"; cursor -= 1 if cursor > 0
|
|
330
336
|
when KEY_RIGHT, "\e[C"; cursor += 1 if cursor < query.length
|
|
331
|
-
when
|
|
332
|
-
when
|
|
337
|
+
when KEY_ALT_LEFT, "\eD"; cursor = word_prev query, cursor
|
|
338
|
+
when KEY_ALT_RIGHT, "\eC"; cursor = word_next query, cursor
|
|
333
339
|
when KEY_HOME; cursor = 0
|
|
334
340
|
when KEY_END; cursor = query.length
|
|
335
341
|
when KEY_DC, "\e[3~"; query.slice!(cursor, 1) if cursor < query.length
|
|
342
|
+
when KEY_ALT_BACKSPACE
|
|
343
|
+
old = cursor
|
|
344
|
+
cursor = word_prev query, cursor
|
|
345
|
+
query.slice!(cursor, old - cursor)
|
|
336
346
|
when KEY_BACKSPACE, "\b"
|
|
337
347
|
if cursor > 0
|
|
338
348
|
query.slice!(cursor - 1, 1)
|
|
@@ -348,6 +358,35 @@ module Typr
|
|
|
348
358
|
cursor = query.length if cursor > query.length
|
|
349
359
|
result = block.call( key, query, cursor ) if block
|
|
350
360
|
return result unless result.nil?
|
|
361
|
+
if completions and key == KEY_TAB
|
|
362
|
+
raw = query[0...cursor]
|
|
363
|
+
cmd = raw[/\w+/]
|
|
364
|
+
if cmd
|
|
365
|
+
items = IO.popen([{ 'COMP_LINE' => raw, 'COMP_POINT' => raw.bytesize.to_s },
|
|
366
|
+
'bash', '-c', <<~SH], err: '/dev/null', &:read)
|
|
367
|
+
_init_completion(){ cur=${COMP_WORDS[COMP_CWORD]}; prev=${COMP_WORDS[COMP_CWORD-1]}; }
|
|
368
|
+
source /usr/share/bash-completion/completions/#{cmd} 2>/dev/null || exit
|
|
369
|
+
COMP_WORDS=($COMP_LINE) COMP_CWORD=$(( ${#COMP_WORDS[@]} - 1 ))
|
|
370
|
+
Fn=$(complete -p #{cmd} 2>/dev/null | awk '{print $(NF-1)}')
|
|
371
|
+
eval "$Fn" 2>/dev/null && printf '%s\\n' "${COMPREPLY[@]}"
|
|
372
|
+
SH
|
|
373
|
+
items = items.split("\n").reject(&:empty?).uniq
|
|
374
|
+
if items.any?
|
|
375
|
+
grid = Grid.new(left: left, top: [top - items.size - 3, 0].max,
|
|
376
|
+
input: items.map{ |s| [s] }, border: :light,
|
|
377
|
+
colors: { columns: [[:blue, :default]] })
|
|
378
|
+
picked = grid.pick(:row, column: 0)
|
|
379
|
+
redraw&.call
|
|
380
|
+
$>.print CURSOR_NORMAL
|
|
381
|
+
if picked.is_a?(Integer)
|
|
382
|
+
word_start = cursor
|
|
383
|
+
word_start -= 1 while word_start > 0 and query[word_start - 1] != ' '
|
|
384
|
+
query[word_start...cursor] = items[picked]
|
|
385
|
+
cursor = word_start + items[picked].length
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
351
390
|
end
|
|
352
391
|
ensure
|
|
353
392
|
$>.print CURSOR_INVISIBLE
|
data/lib/text.rb
CHANGED
|
@@ -33,28 +33,40 @@ module Typr
|
|
|
33
33
|
# text << "appended line" # alias for build
|
|
34
34
|
|
|
35
35
|
def build input=nil
|
|
36
|
-
( @lines = [0]
|
|
36
|
+
( input, @data, @lines = @data, "", [0] ) if rebuild = !input
|
|
37
37
|
return self if rebuild && input == ""
|
|
38
|
-
|
|
38
|
+
str = input.respond_to?(:read) ? input.read : input.to_s
|
|
39
39
|
@tail = ""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@lines << @data.length
|
|
40
|
+
pos = 0
|
|
41
|
+
while pos < str.size || !@tail.empty?
|
|
42
|
+
capacity = width - @margin.size - real_size(@tail)
|
|
43
|
+
cap = [capacity, 1].max
|
|
44
|
+
added = 0; cells = 0
|
|
45
|
+
while pos + added < str.size
|
|
46
|
+
c = str[pos + added]
|
|
47
|
+
if c == ?\e
|
|
48
|
+
seq = str[(pos+added)..][/\A\e\[[0-9;]*m/]
|
|
49
|
+
added += seq ? seq.size : 1
|
|
50
|
+
elsif c == ?\n
|
|
51
|
+
added += 1; break
|
|
53
52
|
else
|
|
54
|
-
|
|
53
|
+
cw = Unicode::DisplayWidth.of(c)
|
|
54
|
+
break if cells + cw > cap
|
|
55
|
+
cells += cw; added += 1
|
|
55
56
|
end
|
|
56
57
|
end
|
|
57
|
-
|
|
58
|
+
line = @tail + str[pos, added]
|
|
59
|
+
pos += added
|
|
60
|
+
break if line.empty?
|
|
61
|
+
wrap_idx = line.index("\n") || line.rindex(" ") || line.length
|
|
62
|
+
wrap = [wrap_idx, line.length].min + 1
|
|
63
|
+
@data += line[0...wrap]
|
|
64
|
+
if wrap < line.length
|
|
65
|
+
@tail = line[wrap..-1]
|
|
66
|
+
else
|
|
67
|
+
@tail = ""
|
|
68
|
+
end
|
|
69
|
+
@lines << @data.length if wrap < line.length || line.include?("\n")
|
|
58
70
|
end
|
|
59
71
|
until @tail.empty?
|
|
60
72
|
wrap_idx = @tail.index("\n") || @tail.rindex(" ") || @tail.length
|
|
@@ -84,10 +96,10 @@ module Typr
|
|
|
84
96
|
text = ( header ? @header : @page[ row-@start ] )
|
|
85
97
|
if row.is_a?(Integer) and @search[:matches].include? row
|
|
86
98
|
background @colors[:search]
|
|
87
|
-
draw prepare( text, width )
|
|
99
|
+
draw prepare( text, width - @margin.size )
|
|
88
100
|
background
|
|
89
101
|
else
|
|
90
|
-
draw prepare( text, width )
|
|
102
|
+
draw prepare( text, width - @margin.size )
|
|
91
103
|
end
|
|
92
104
|
end
|
|
93
105
|
|
data/typr.gemspec
CHANGED