typr 1.4.0 → 1.5.1
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 +26 -0
- data/README.md +1 -1
- data/example/grid +22 -27
- data/lib/browser.rb +60 -14
- 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 +43 -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: f04e1f9234a97079a70786d6a427c7c6394775515c78a19feee8e17e3dc1370e
|
|
4
|
+
data.tar.gz: b82771f5976e12fabbd56446d347ac8f49739e41d4dbe089b7ffc5738938dd17
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99278addeb2d176281564448ddb5b34de23aebb1bac5a3e186c0a4735aafad0387274100bd38fa33018988d7672a7959c6c9791fd2bb8bf50c2e2545075552c6
|
|
7
|
+
data.tar.gz: 9efd4720c0cc7c58f41acf31ca8a38827300127b72ffadaf9d18ebd53b8be0500f53de1386eed6255149f858ab3e5a943523ba79630cadf11eddf8c0f8a6e072
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v1.5.1] — 2026-09-11
|
|
4
|
+
|
|
5
|
+
## [v1.5.0] — 2026-09-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- 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
|
|
9
|
+
- `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`
|
|
10
|
+
- 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
|
|
11
|
+
- `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
|
|
12
|
+
- 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
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- `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
|
|
16
|
+
- 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
|
|
17
|
+
- `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)
|
|
18
|
+
- `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
|
|
19
|
+
- `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`
|
|
20
|
+
- `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
|
|
21
|
+
- `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)
|
|
22
|
+
- `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
|
|
23
|
+
- `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
|
|
24
|
+
- `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
|
|
25
|
+
- 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
|
|
26
|
+
- `Mouse#alt?` — reports the SGRmeta bit, so callers can treat Alt+click like a right-click (e.g. opening a command list where right-click is hard to send, as on Termux touchscreens)
|
|
27
|
+
- `cd` survives unreadable directories — `chdir` and the listing are probed before any state changes, so `EACCES`/`ENOENT` (e.g. Android's `/data`) shows a warning and stays put instead of crashing on an emptied listing; returns `false` on failure
|
|
28
|
+
|
|
3
29
|
## [v1.4.0] — 2026-08-15
|
|
4
30
|
|
|
5
31
|
### 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
|
@@ -58,11 +58,22 @@ module Typr
|
|
|
58
58
|
return unless dir
|
|
59
59
|
data, missing = [],{}
|
|
60
60
|
unless append
|
|
61
|
+
begin
|
|
62
|
+
path = File.expand_path( dir )
|
|
63
|
+
Dir.chdir( path )
|
|
64
|
+
entries = Dir.new( path ).children
|
|
65
|
+
rescue SystemCallError
|
|
66
|
+
@user.show "cannot open directory: #{dir}"
|
|
67
|
+
Typr.read_key
|
|
68
|
+
return false
|
|
69
|
+
end
|
|
61
70
|
@colors[:fields] = {}
|
|
62
71
|
clear
|
|
63
|
-
|
|
72
|
+
@directory = path
|
|
73
|
+
else
|
|
74
|
+
entries = Dir.new( @directory ).children
|
|
64
75
|
end
|
|
65
|
-
data =
|
|
76
|
+
data = entries.reject{|f|
|
|
66
77
|
!@show_hidden and f[0] == ?. }.map { |file| scan_file file }
|
|
67
78
|
|
|
68
79
|
data.each_with_index{ |row, id|
|
|
@@ -78,7 +89,7 @@ module Typr
|
|
|
78
89
|
end
|
|
79
90
|
missing[file] = id unless mime
|
|
80
91
|
end
|
|
81
|
-
row[
|
|
92
|
+
row[TYPE, 2] = mime.to_s.split( ?/ ) if mime
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
if @mimetype_magic and missing.any?
|
|
@@ -90,7 +101,7 @@ module Typr
|
|
|
90
101
|
out.split($/).each{ |line|
|
|
91
102
|
file, mime = line.match(/^(.*):\s+(\w+\/[\w\-\.]+)$/)&.captures
|
|
92
103
|
next unless file
|
|
93
|
-
data[ missing[file] ][
|
|
104
|
+
data[ missing[file] ][TYPE, 2] = mime.split( ?/ ) if missing[file] }
|
|
94
105
|
end
|
|
95
106
|
data.each { |row| row[0] = File.join(@directory, row[0]) }
|
|
96
107
|
self << data
|
|
@@ -115,14 +126,14 @@ module Typr
|
|
|
115
126
|
target = nil
|
|
116
127
|
if File.symlink? file
|
|
117
128
|
target = File.readlink( file ) rescue $!.to_s
|
|
118
|
-
return [file, target, "????", false, "?", "?", 0, 0, 0, 0, "?", "?"] unless File.exist?(file)
|
|
129
|
+
return [file, target, "????", false, "?", "?", 0, 0, 0, 0, "?", "?", nil] unless File.exist?(file)
|
|
119
130
|
end
|
|
120
131
|
stat = File.stat(file) rescue nil
|
|
121
|
-
return [file, "", "????", false, "?", "?", 0, 0, 0, 0, "?", "?"] unless stat
|
|
132
|
+
return [file, "", "????", false, "?", "?", 0, 0, 0, 0, "?", "?", nil] unless stat
|
|
122
133
|
[ file, target, stat.mode.to_s(8)[-3..-1].chars.map{|d|
|
|
123
134
|
"rwx".chars.map.with_index{|c,i| d.to_i[2-i]==0 ? ?- : c } }.join,
|
|
124
135
|
( 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, "?", "?" ]
|
|
136
|
+
stat.atime.to_i, stat.mtime.to_i, stat.ctime.to_i, "?", "?", nil ]
|
|
126
137
|
end
|
|
127
138
|
|
|
128
139
|
##
|
|
@@ -148,6 +159,10 @@ module Typr
|
|
|
148
159
|
return unless view
|
|
149
160
|
view = @views.keys[ view ] if view.is_a? Integer
|
|
150
161
|
@sequence = @views[ view ].map{|title| @header.index title.to_s}
|
|
162
|
+
if @show_path
|
|
163
|
+
@data.each { |row| row[PATH] = File.dirname(row[NAME]) } if @data.any?
|
|
164
|
+
@sequence.insert(@sequence.index(NAME) || 0, PATH) unless @sequence.include?(PATH)
|
|
165
|
+
end
|
|
151
166
|
reset :format
|
|
152
167
|
@view = view
|
|
153
168
|
return
|
|
@@ -160,6 +175,18 @@ module Typr
|
|
|
160
175
|
# browser.send "1" # => row id if hint "1" is visible
|
|
161
176
|
|
|
162
177
|
def send key
|
|
178
|
+
if key.is_a?(Typr::Mouse) and key.press? and key.left?
|
|
179
|
+
[@head, @user].each do |w|
|
|
180
|
+
next unless w.respond_to?(:data_at)
|
|
181
|
+
value = w.data_at(key.x, key.y)
|
|
182
|
+
next unless value
|
|
183
|
+
key = extract_key(value.to_s)
|
|
184
|
+
while key.is_a?(String)
|
|
185
|
+
key = send(key)
|
|
186
|
+
end
|
|
187
|
+
return key
|
|
188
|
+
end
|
|
189
|
+
end
|
|
163
190
|
if key.is_a?(String) and
|
|
164
191
|
id = @hints[0..[height, rows-1+headspace].min-@hints_start-1].index(key)
|
|
165
192
|
return page[id+@hints_start]
|
|
@@ -303,14 +330,12 @@ module Typr
|
|
|
303
330
|
return @directory
|
|
304
331
|
end
|
|
305
332
|
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
|
|
333
|
+
choice = send key
|
|
334
|
+
next if choice == :scroll or choice == :outside
|
|
311
335
|
else
|
|
312
336
|
next unless choice = send( key )
|
|
313
337
|
end
|
|
338
|
+
next unless choice
|
|
314
339
|
case choice
|
|
315
340
|
when Array; choice = choice.map{ |id| @data[id][NAME] }.join('" "')
|
|
316
341
|
when Integer; file = @data[ choice ]
|
|
@@ -414,7 +439,7 @@ module Typr
|
|
|
414
439
|
|
|
415
440
|
def initialize args={}
|
|
416
441
|
@mimetype_db, @mimetype_magic = true, true
|
|
417
|
-
@view, @sort, @views, @positions, @show_hidden = :compact, 0, {}, {}, false
|
|
442
|
+
@view, @sort, @views, @positions, @show_hidden, @show_path = :compact, 0, {}, {}, false, false
|
|
418
443
|
@filter_dirs = args[:filter_dirs]
|
|
419
444
|
super
|
|
420
445
|
@left ||= 0
|
|
@@ -442,7 +467,7 @@ module Typr
|
|
|
442
467
|
toggle_hidden: ?H}.merge @keymap
|
|
443
468
|
|
|
444
469
|
self.header = %w[ name target permissions executable owner group size
|
|
445
|
-
accessed modified created type subtype ]
|
|
470
|
+
accessed modified created type subtype path ]
|
|
446
471
|
|
|
447
472
|
@format[NAME] = :max
|
|
448
473
|
@format[TYPE] = 5
|
|
@@ -475,6 +500,27 @@ module Typr
|
|
|
475
500
|
colors: { columns: [@colors[:default], @colors[:default],
|
|
476
501
|
@colors[:actions][:"[f]ilter"], @colors[:actions][:"[s]ort"],
|
|
477
502
|
@colors[:actions][:"[v]iews"]] } )
|
|
503
|
+
@head.define_singleton_method( :data_at ) do |x, y|
|
|
504
|
+
return unless x and y and y.between?( top + 1, bottom + 1 ) and
|
|
505
|
+
x.between?( left, right )
|
|
506
|
+
offs = positions( 0 )
|
|
507
|
+
col = offs.each_index.find { |i| x <= left + offs[i] + width_for( i ) - 1 }
|
|
508
|
+
@parent.instance_variable_get( :@positions ).keys[ col - 2 ] if col and col >= 2
|
|
509
|
+
end
|
|
510
|
+
@head.define_singleton_method( :path_at ) do |x, y|
|
|
511
|
+
return unless x and y and y.between?( top + 1, bottom + 1 )
|
|
512
|
+
x -= 1
|
|
513
|
+
return unless x.between?( left, right )
|
|
514
|
+
offs = positions( 0 )
|
|
515
|
+
col = offs.each_index.find { |i| x <= left + offs[i] + width_for( i ) - 1 }
|
|
516
|
+
return (Etc.getpwuid(Process.euid).dir rescue (Dir.home rescue "~")) if col == 0
|
|
517
|
+
return unless col == 1 and @parent.directory.is_a?( String )
|
|
518
|
+
text = ((@layer and @layer[0] and @layer[0][1]) || @data[0][1].call).to_s
|
|
519
|
+
rel = x - ( left + offs[1] )
|
|
520
|
+
return if rel < 0 or rel >= real_size( text )
|
|
521
|
+
pre = prepare( text, rel + 1 )
|
|
522
|
+
( pre + ( pre.end_with?( "/" ) ? "" : text[pre.length..-1][/\A[^\/]*/] ) ).sub( %r{(?<=.)/$}, "" )
|
|
523
|
+
end
|
|
478
524
|
|
|
479
525
|
@user ||= Line.new( parent:self, left:->{@parent.left},top:->{@parent.bottom+1},
|
|
480
526
|
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
|
|
@@ -70,6 +76,7 @@ module Typr
|
|
|
70
76
|
def left?; button == 0 end
|
|
71
77
|
def middle?; button == 1 end
|
|
72
78
|
def right?; button == 2 end
|
|
79
|
+
def alt?; ( modifiers & 8 ) != 0 end
|
|
73
80
|
def to_s; "%s button=%i x=%i y=%i%s" % [ action, button, x, y,
|
|
74
81
|
( modifiers > 0 ? " modifiers=#{modifiers}" : "" ) ] end
|
|
75
82
|
end
|
|
@@ -310,7 +317,7 @@ module Typr
|
|
|
310
317
|
# nil
|
|
311
318
|
# end
|
|
312
319
|
|
|
313
|
-
def self.read_line prompt='', left: 0, top: 0, initial: '', &block
|
|
320
|
+
def self.read_line prompt='', left: 0, top: 0, initial: '', completions: nil, redraw: nil, &block
|
|
314
321
|
return $stdin.gets&.chomp unless $stdin.tty?
|
|
315
322
|
query, cursor = initial.dup, initial.length
|
|
316
323
|
$>.print CURSOR_NORMAL
|
|
@@ -328,11 +335,15 @@ module Typr
|
|
|
328
335
|
case key
|
|
329
336
|
when KEY_LEFT, "\e[D"; cursor -= 1 if cursor > 0
|
|
330
337
|
when KEY_RIGHT, "\e[C"; cursor += 1 if cursor < query.length
|
|
331
|
-
when
|
|
332
|
-
when
|
|
338
|
+
when KEY_ALT_LEFT, "\eD"; cursor = word_prev query, cursor
|
|
339
|
+
when KEY_ALT_RIGHT, "\eC"; cursor = word_next query, cursor
|
|
333
340
|
when KEY_HOME; cursor = 0
|
|
334
341
|
when KEY_END; cursor = query.length
|
|
335
342
|
when KEY_DC, "\e[3~"; query.slice!(cursor, 1) if cursor < query.length
|
|
343
|
+
when KEY_ALT_BACKSPACE
|
|
344
|
+
old = cursor
|
|
345
|
+
cursor = word_prev query, cursor
|
|
346
|
+
query.slice!(cursor, old - cursor)
|
|
336
347
|
when KEY_BACKSPACE, "\b"
|
|
337
348
|
if cursor > 0
|
|
338
349
|
query.slice!(cursor - 1, 1)
|
|
@@ -348,6 +359,35 @@ module Typr
|
|
|
348
359
|
cursor = query.length if cursor > query.length
|
|
349
360
|
result = block.call( key, query, cursor ) if block
|
|
350
361
|
return result unless result.nil?
|
|
362
|
+
if completions and key == KEY_TAB
|
|
363
|
+
raw = query[0...cursor]
|
|
364
|
+
cmd = raw[/\w+/]
|
|
365
|
+
if cmd
|
|
366
|
+
items = IO.popen([{ 'COMP_LINE' => raw, 'COMP_POINT' => raw.bytesize.to_s },
|
|
367
|
+
'bash', '-c', <<~SH], err: '/dev/null', &:read)
|
|
368
|
+
_init_completion(){ cur=${COMP_WORDS[COMP_CWORD]}; prev=${COMP_WORDS[COMP_CWORD-1]}; }
|
|
369
|
+
source /usr/share/bash-completion/completions/#{cmd} 2>/dev/null || exit
|
|
370
|
+
COMP_WORDS=($COMP_LINE) COMP_CWORD=$(( ${#COMP_WORDS[@]} - 1 ))
|
|
371
|
+
Fn=$(complete -p #{cmd} 2>/dev/null | awk '{print $(NF-1)}')
|
|
372
|
+
eval "$Fn" 2>/dev/null && printf '%s\\n' "${COMPREPLY[@]}"
|
|
373
|
+
SH
|
|
374
|
+
items = items.split("\n").reject(&:empty?).uniq
|
|
375
|
+
if items.any?
|
|
376
|
+
grid = Grid.new(left: left, top: [top - items.size - 3, 0].max,
|
|
377
|
+
input: items.map{ |s| [s] }, border: :light,
|
|
378
|
+
colors: { columns: [[:blue, :default]] })
|
|
379
|
+
picked = grid.pick(:row, column: 0)
|
|
380
|
+
redraw&.call
|
|
381
|
+
$>.print CURSOR_NORMAL
|
|
382
|
+
if picked.is_a?(Integer)
|
|
383
|
+
word_start = cursor
|
|
384
|
+
word_start -= 1 while word_start > 0 and query[word_start - 1] != ' '
|
|
385
|
+
query[word_start...cursor] = items[picked]
|
|
386
|
+
cursor = word_start + items[picked].length
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
351
391
|
end
|
|
352
392
|
ensure
|
|
353
393
|
$>.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