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
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,33 @@
|
|
|
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
|
+
|
|
23
|
+
## [v1.3.0] — 2026-08-03
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- Mouse support — `Typr.init`/`Typr.exit` enable/disable mouse reporting (buttons + SGR coordinates, `CSI ? 1000 h/l` + `CSI ? 1006 h/l`); `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
|
|
27
|
+
- `Stack#hit` — maps a 1-based terminal row/col to the data row id under the cursor (nil for header/margins/outside)
|
|
28
|
+
- Click-to-act in pickers — a left click on a row in `Stack#pick`/`Browser#pick` acts like pressing its hint digit (selects the row; in `Browser` cd's into directories and returns file paths)
|
|
29
|
+
- Wheel-to-scroll in pickers — mouse wheel up/down scrolls the listing in `Stack#pick`/`Browser#pick`
|
|
30
|
+
|
|
3
31
|
## [v1.2.0] — 2026-08-02
|
|
4
32
|
|
|
5
33
|
### Added
|
data/README.md
CHANGED
|
@@ -1,29 +1,44 @@
|
|
|
1
1
|
# typr — Terminal UI Toolkit
|
|
2
2
|
|
|
3
|
-
typr is a Ruby library for building interactive
|
|
3
|
+
typr is a Ruby library for building interactive, keyboard-driven terminal user interfaces. It is built from data-object based components: grids for arrays, a pager for text, a file picker, and lines for user interaction — each of which renders itself, handles its own keymap, and can be combined into full applications.
|
|
4
|
+
|
|
5
|
+
typr is designed for speed: every interactive choice is one keystroke away. Selectable rows and columns carry number hints, popups open in `/` search mode so long lists can be distilled down and picked from in a couple of keystrokes. It is the toolkit behind the [Filecon](https://codeberg.org/typr/filecon) file manager.
|
|
6
|
+
|
|
7
|
+
typr's terminal layer is self-contained: it ships a precompiled terminfo key table and a MIME-type database inside the gem, so it has no ncurses dependency and works on any raw, ANSI-capable terminal — including Android/Termux.
|
|
4
8
|
|
|
5
9
|
## Features
|
|
6
10
|
|
|
7
|
-
###
|
|
11
|
+
### Components
|
|
8
12
|
|
|
9
|
-
- **Grid** — sortable, filterable table with formatted columns, colors, and row selection
|
|
10
|
-
- **Text** — scrollable text viewer with word/
|
|
11
|
-
- **Browser** — file-system directory browser with MIME type
|
|
12
|
-
- **
|
|
13
|
-
- **Line** — interactive prompt/input with colored questions and answers
|
|
13
|
+
- **Grid** — sortable, filterable table with formatted columns, per-cell colors, and row selection. Columns can be `:min`/`:max`/fixed-width, with built-in processors for dates, file sizes, and file trees. `Grid#pick column:` adds live `/` search
|
|
14
|
+
- **Text** — scrollable text viewer with word/row picking, `/` search with `n`/`p` navigation and match highlighting, and borders
|
|
15
|
+
- **Browser** — file-system directory browser with MIME-type detection (bundled database, optional magic detection), per-type coloring, and an interactive picker
|
|
16
|
+
- **Line** — interactive prompt/input with colored questions and answers, configurable bindings, and strict `ask` question types (`Array`, `:key`, `:line`)
|
|
14
17
|
|
|
15
|
-
###
|
|
18
|
+
### Keyboard interaction
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
Navigation uses **hint numbers** displayed next to selectable items — rows in grids, words in text, and other elements. Press the number to select that item directly. When more items are visible than fit in one set of hints (1–9, 0), press **Tab** to cycle through hint groups. Arrow keys, Page Up/Down, Home/End work as expected, and all keys are dispatched through a configurable keymap.
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
- **Graphical** (`require 'typr/graphical'`) — SDL2-based, for desktop GUI windows
|
|
22
|
+
**Live `/` search** is built into listings and pickers: popups open in search mode, so you start typing to distill the list down to the matches, then press a number hint to pick from the few remaining results. `Text` adds less-style search (`/`, `n`, `p`) with smart-case matching and wrap-around.
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
### Mouse support
|
|
23
25
|
|
|
24
|
-
|
|
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, and the wheel scrolls the listing.
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
### Terminal layer
|
|
29
|
+
|
|
30
|
+
The default terminal frontend is pure ANSI on raw TTY input — **no ncurses**. Key sequences and keypad escapes are precompiled into a bundled terminfo table (`share/terminfo`), and MIME types come from a bundled database (`share/mimetypes`); both ship with the gem, so typr runs anywhere a raw, ANSI-capable terminal exists. The only runtime dependency is `unicode-display_width`.
|
|
31
|
+
|
|
32
|
+
### Colors
|
|
33
|
+
|
|
34
|
+
Named colors (basic, shades, and `dark_*`), grey shades `grey0`–`grey100`, the terminal's 8-bit palette, and RGB triplets for true-color terminals. Every component takes a per-part `colors:` hash (columns, headers, hints, search matches, …).
|
|
35
|
+
|
|
36
|
+
### Frontends
|
|
37
|
+
|
|
38
|
+
- **Terminal** (`require 'typr'`) — the default: raw terminal IO, no ncurses, works over SSH and on Termux
|
|
39
|
+
- **Graphical** (`require 'typr/graphical'`) — experimental: SDL2-based windows
|
|
40
|
+
|
|
41
|
+
All components work identically across frontends.
|
|
27
42
|
|
|
28
43
|
## Installation
|
|
29
44
|
|
|
@@ -34,7 +49,7 @@ gem install typr
|
|
|
34
49
|
Or in your Gemfile:
|
|
35
50
|
|
|
36
51
|
```ruby
|
|
37
|
-
gem 'typr', '~> 1.
|
|
52
|
+
gem 'typr', '~> 1.2'
|
|
38
53
|
```
|
|
39
54
|
|
|
40
55
|
## Usage
|
|
@@ -59,8 +74,8 @@ Typr.init
|
|
|
59
74
|
|
|
60
75
|
Typr.clear
|
|
61
76
|
@grid.show
|
|
62
|
-
key = Typr.
|
|
63
|
-
@grid.reset
|
|
77
|
+
key = Typr.read_key # reads a single keypress
|
|
78
|
+
@grid.reset # reset grid to default view
|
|
64
79
|
Typr.exit
|
|
65
80
|
```
|
|
66
81
|
|
|
@@ -82,7 +97,7 @@ Typr.init
|
|
|
82
97
|
Typr.clear
|
|
83
98
|
loop do
|
|
84
99
|
@text.show
|
|
85
|
-
key = Typr.
|
|
100
|
+
key = Typr.read_key
|
|
86
101
|
case key
|
|
87
102
|
when ?q then break
|
|
88
103
|
else @text.send(key) # spacebar scrolls, j/k move up/down, etc.
|
|
@@ -104,7 +119,7 @@ Typr.init
|
|
|
104
119
|
colors: { hints: [:white, :black] }
|
|
105
120
|
)
|
|
106
121
|
|
|
107
|
-
selected = @browser.pick
|
|
122
|
+
selected = @browser.pick # interactive file picker, returns the absolute path
|
|
108
123
|
puts "You selected: #{selected}"
|
|
109
124
|
Typr.exit
|
|
110
125
|
```
|
|
@@ -140,7 +155,7 @@ class MyApp
|
|
|
140
155
|
loop do
|
|
141
156
|
@grid.show
|
|
142
157
|
@prompt.show
|
|
143
|
-
key = Typr.
|
|
158
|
+
key = Typr.read_key
|
|
144
159
|
case key
|
|
145
160
|
when ?s then @grid.sort_by(@prompt.ask("sort column": [@grid, :column]))
|
|
146
161
|
when ?f then @grid.add_filter(*@prompt.ask("filter column": [@grid, :column], with: :line))
|
|
@@ -163,31 +178,46 @@ end
|
|
|
163
178
|
## API Reference
|
|
164
179
|
|
|
165
180
|
### `Typr.init` / `Typr.exit`
|
|
166
|
-
Initialize and tear down the terminal UI. Always call these at the start and end of your program.
|
|
181
|
+
Initialize and tear down the terminal UI (hides the cursor, enables key-mode and mouse reporting; restores them on exit). Always call these at the start and end of your program.
|
|
167
182
|
|
|
168
183
|
### `Typr.clear([scope])`
|
|
169
184
|
Clears the full screen or a single line (`:line`).
|
|
170
185
|
|
|
171
|
-
### `Typr.
|
|
172
|
-
Reads
|
|
173
|
-
|
|
174
|
-
|
|
186
|
+
### `Typr.read_key`
|
|
187
|
+
Reads a single keypress in raw mode. Returns the key as a String (`KEY_ESCAPE`, `?q`, arrow escape sequences), a `Typr::Mouse` struct for mouse reports, or `nil` when input is unavailable.
|
|
188
|
+
|
|
189
|
+
### `Typr.read_line(prompt, ...)`
|
|
190
|
+
The shared interactive line editor. Renders a prompt and query at any position with arrow keys, ctrl-arrow word jumps, Home/End/Delete/Backspace; Enter confirms, Escape aborts (`nil`). An optional block runs after each keypress and may short-circuit with a result:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
Typr.read_line "/" do |key, query, cursor|
|
|
194
|
+
filter query
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### `Line#ask`
|
|
200
|
+
Strict question types:
|
|
201
|
+
- `Array` — pick a row from an object/column (hint-driven)
|
|
202
|
+
- `:key` — read a single key
|
|
203
|
+
- `:line` — read a line of text
|
|
204
|
+
|
|
205
|
+
The question and answer are rendered in `@colors[:question]`/`@colors[:answer]`.
|
|
175
206
|
|
|
176
|
-
### Layout properties (all
|
|
207
|
+
### Layout properties (all components)
|
|
177
208
|
- `top`, `bottom`, `left`, `right` — placement. Integers for absolute offsets, floats (0-1) for relative sizing. Negative values position from the opposite edge. (`bottom: -1` = last row)
|
|
178
209
|
|
|
179
210
|
### Grid options
|
|
180
211
|
| Option | Description |
|
|
181
212
|
|---|---|
|
|
182
213
|
| `input` | Array of arrays (data rows) |
|
|
183
|
-
| `header` | Column titles array |
|
|
214
|
+
| `header` | Column titles array (symbols define `Typr::NAME`-style constants) |
|
|
184
215
|
| `format` | Per-column format: `:min`, `:max`, or a number for fixed width |
|
|
216
|
+
| `procs` | Per-column processors (built-ins: `:datetime`, `:magnitudes`, `:filetree`, `:convert`) |
|
|
185
217
|
| `colors` | `{ columns: [:yellow, :cyan], header: [:white, :black] }` etc. |
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
|
|
|
189
|
-
| `.sort_by` | Sort the grid on a given column |
|
|
190
|
-
| `.select_rows / select_columns` | Multi-select via index picker |
|
|
218
|
+
| `.sort_by` | Sort the grid on a given column (toggles direction) |
|
|
219
|
+
| `.add_filter` | Add a substring/regex filter on a column (or `:all`) |
|
|
220
|
+
| `.pick :row, column:` | Interactive row picker with live `/` search and number hints |
|
|
191
221
|
|
|
192
222
|
### Text options
|
|
193
223
|
| Option | Description |
|
|
@@ -195,12 +225,13 @@ Reads input from the terminal. Types:
|
|
|
195
225
|
| `input` | String or IO-like stream to display |
|
|
196
226
|
| `header` | Header bar text |
|
|
197
227
|
| `border` | Border style: `:round`, `" "`, etc. |
|
|
228
|
+
| `.search` | Search the text (`/`); `.search_next`/`.search_prev` (`n`/`p`) |
|
|
198
229
|
|
|
199
230
|
### Browser options
|
|
200
231
|
| Option | Description |
|
|
201
232
|
|---|---|
|
|
202
233
|
| `directory` | Root directory to start browsing |
|
|
203
|
-
| `.pick(
|
|
234
|
+
| `.pick([type])` | Interactive file picker, returns the absolute path; `type` filters by MIME family (`"image"`, `"audio"`, `"video"`, `"text"`, `"application"`, `"inode"`) |
|
|
204
235
|
|
|
205
236
|
## License
|
|
206
237
|
|
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}
|
|
@@ -216,12 +229,7 @@ module Typr
|
|
|
216
229
|
def help; @user.help end
|
|
217
230
|
|
|
218
231
|
##
|
|
219
|
-
#
|
|
220
|
-
# via +cd(path, true)+. Sorts by name with the filetree processor.
|
|
221
|
-
#
|
|
222
|
-
# browser.recurse # flattens tree into leaf names
|
|
223
|
-
|
|
224
|
-
## Reset display, selection, and position state then re-sort.
|
|
232
|
+
# Reset display, selection, and position state then re-sort.
|
|
225
233
|
#
|
|
226
234
|
# browser.reset_view
|
|
227
235
|
|
|
@@ -231,10 +239,12 @@ module Typr
|
|
|
231
239
|
#
|
|
232
240
|
# browser.filter_view # prompts for filter text
|
|
233
241
|
|
|
234
|
-
def search; pick :none, 0, column: NAME end
|
|
235
|
-
|
|
236
242
|
def filter_view; add_filter *@user.ask(
|
|
237
|
-
filter: [ popup( type: :"[f]ilter" ), :row, NAME], with: :line ) end
|
|
243
|
+
filter: [ popup( type: :"[f]ilter" ), :row, NAME, { column: 0 }], with: :line ) end
|
|
244
|
+
|
|
245
|
+
## Open live `/` search filtered on the name column.
|
|
246
|
+
#
|
|
247
|
+
# browser.search # type to narrow, Escape restores, Enter commits
|
|
238
248
|
|
|
239
249
|
def search
|
|
240
250
|
@suppress_user = true
|
|
@@ -246,13 +256,13 @@ module Typr
|
|
|
246
256
|
#
|
|
247
257
|
# browser.sort_view # pick a column header to sort
|
|
248
258
|
|
|
249
|
-
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
|
|
250
260
|
|
|
251
261
|
## Open a view-picker popup and switch to the selected view.
|
|
252
262
|
#
|
|
253
263
|
# browser.views # choose :simple, :compact, :stats, or :full
|
|
254
264
|
|
|
255
|
-
def views;
|
|
265
|
+
def views; change_view( popup( type: :"[v]iews", input: @views.keys ).pick( :row, column: 0 ) ) end
|
|
256
266
|
|
|
257
267
|
## True when the row is a directory or passes base Grid filtering.
|
|
258
268
|
#
|
|
@@ -262,36 +272,53 @@ module Typr
|
|
|
262
272
|
|
|
263
273
|
##
|
|
264
274
|
# Interactive file/directory picker. Blocks until a selection or Escape.
|
|
265
|
-
# Return on a file
|
|
266
|
-
#
|
|
267
|
-
#
|
|
275
|
+
# Return on a file yields its raw absolute path; multi-select joins the
|
|
276
|
+
# chosen paths double-quoted with spaces. Enter on a directory triggers
|
|
277
|
+
# cd. +type+ optionally filters by MIME family (audio, video, image,
|
|
278
|
+
# text, application, inode). Mouse: left-click a row to pick it; wheel
|
|
279
|
+
# up/down scrolls the listing.
|
|
268
280
|
#
|
|
269
|
-
# browser.pick # =>
|
|
270
|
-
# browser.pick 'image' # =>
|
|
281
|
+
# browser.pick # => /home/user/file.txt
|
|
282
|
+
# browser.pick 'image' # => /path/to/photo.png (images only)
|
|
271
283
|
# browser.pick nil # => anything, same as 'file'
|
|
272
284
|
|
|
273
285
|
def pick type='file', **kw
|
|
274
286
|
type ||= 'file'
|
|
275
287
|
type = type.to_s
|
|
276
288
|
return super(type, **kw) if type[/row|column|field|none/]
|
|
277
|
-
|
|
278
|
-
|
|
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 )
|
|
279
291
|
loop do
|
|
280
292
|
show
|
|
281
293
|
draw_hints :row
|
|
282
294
|
key = Typr.read_key
|
|
283
295
|
if key == @keymap[:exit]
|
|
284
|
-
|
|
296
|
+
@filters.clear
|
|
297
|
+
sort
|
|
285
298
|
return
|
|
286
299
|
end
|
|
287
|
-
|
|
300
|
+
if key == @keymap[:confirm] and type == 'directory'
|
|
301
|
+
@filters.clear
|
|
302
|
+
sort
|
|
303
|
+
return @directory
|
|
304
|
+
end
|
|
305
|
+
if key.is_a?(Typr::Mouse)
|
|
306
|
+
if key.wheel? and key.press?
|
|
307
|
+
send( key.wheel_up? ? @keymap[:up] : @keymap[:down] )
|
|
308
|
+
next
|
|
309
|
+
elsif key.press? and key.left? and choice = hit( key.y, key.x )
|
|
310
|
+
else next end
|
|
311
|
+
else
|
|
312
|
+
next unless choice = send( key )
|
|
313
|
+
end
|
|
288
314
|
case choice
|
|
289
315
|
when Array; choice = choice.map{ |id| @data[id][NAME] }.join('" "')
|
|
290
316
|
when Integer; file = @data[ choice ]
|
|
291
317
|
if file[SUBTYPE] == 'directory'; cd file[TARGET] || file[NAME]; next
|
|
292
318
|
else choice = file[NAME] end
|
|
293
319
|
end
|
|
294
|
-
|
|
320
|
+
@filters.clear
|
|
321
|
+
sort
|
|
295
322
|
return choice
|
|
296
323
|
end
|
|
297
324
|
end
|
|
@@ -303,7 +330,7 @@ module Typr
|
|
|
303
330
|
# browser.show # shows browser with info bar
|
|
304
331
|
|
|
305
332
|
def show; super
|
|
306
|
-
user = ENV["USER"] || Etc.getlogin
|
|
333
|
+
user = Etc.getpwuid(Process.euid).name rescue ENV["USER"] || Etc.getlogin
|
|
307
334
|
@head.colors[:columns][0] = user == "root" ? [:red, :black] : [:green, :black]
|
|
308
335
|
@head.reset :format
|
|
309
336
|
@head.show
|
|
@@ -440,7 +467,7 @@ module Typr
|
|
|
440
467
|
@parent = self
|
|
441
468
|
@head ||= Grid.new( parent:self, left: ->{@parent.left}, margin: '',
|
|
442
469
|
right: ->{@parent.width + @parent.left - 1}, top: ->{@parent.top-1},
|
|
443
|
-
input: [ [->{ENV["USER"] || Etc.getlogin}, ->{@parent.directory},
|
|
470
|
+
input: [ [->{Etc.getpwuid(Process.euid).name rescue ENV["USER"] || Etc.getlogin}, ->{@parent.directory},
|
|
444
471
|
->{@parent.filter_display},
|
|
445
472
|
->{ (s = @parent.sorted_by(true)) &&
|
|
446
473
|
"#{s} #{@parent.reverse ? "\u2193" : "\u2191"}"},
|
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/space.rb
CHANGED
|
@@ -29,6 +29,7 @@ include Typr
|
|
|
29
29
|
STR
|
|
30
30
|
eval( method % ([name]*11) ) }
|
|
31
31
|
|
|
32
|
+
# Deep-convert String keys to Symbols so string-keyed config (e.g. YAML) is honored.
|
|
32
33
|
def symbolize obj
|
|
33
34
|
case obj
|
|
34
35
|
when Hash
|
|
@@ -40,17 +41,23 @@ eval( method % ([name]*11) ) }
|
|
|
40
41
|
attr_writer :left, :top, :right, :bottom
|
|
41
42
|
attr_accessor :margin, :colors, :interval, :borders
|
|
42
43
|
|
|
44
|
+
# Viewport width in cells (right - left + 1).
|
|
43
45
|
def width; right - left + 1 end
|
|
46
|
+
# Viewport height in cells (bottom - top + 1).
|
|
44
47
|
def height; bottom - top + 1 end
|
|
45
48
|
|
|
49
|
+
# Redraw every +interval+ seconds from a background thread.
|
|
46
50
|
def start; @updater = Thread.new{ loop{show; sleep @interval }} end
|
|
51
|
+
# Stop the background refresh thread.
|
|
47
52
|
def stop; @updater.terminate end
|
|
48
53
|
|
|
54
|
+
# Draw a border char at screen (x, y); skips nil and off-screen positions.
|
|
49
55
|
def draw_border x,y,char
|
|
50
56
|
draw move_code(x, y) + char unless not char or
|
|
51
57
|
x < 0 or x > Typr.width or y < 0 or y > Typr.height
|
|
52
58
|
end
|
|
53
59
|
|
|
60
|
+
# Draw the widget's border box (no-op when no borders are set).
|
|
54
61
|
def show
|
|
55
62
|
return if @borders.empty?
|
|
56
63
|
color @colors[:border]
|
|
@@ -66,6 +73,8 @@ eval( method % ([name]*11) ) }
|
|
|
66
73
|
draw_border(right + 1, bottom + 1, @borders[:bottom_right])
|
|
67
74
|
end
|
|
68
75
|
|
|
76
|
+
# Set border style: :light/:heavy/:double/:round, a char repeated across
|
|
77
|
+
# the box, or nil for none.
|
|
69
78
|
def border=(border)
|
|
70
79
|
chars = case border
|
|
71
80
|
when Symbol
|
|
@@ -78,6 +87,8 @@ eval( method % ([name]*11) ) }
|
|
|
78
87
|
bottom_right ].map.with_index{ |part,id| [part.to_sym, chars[id]] }.to_h if chars
|
|
79
88
|
end
|
|
80
89
|
|
|
90
|
+
# Build a widget from boundary (left/top/right/bottom), border, margin,
|
|
91
|
+
# colors, interval, and keymap.
|
|
81
92
|
def initialize args={}
|
|
82
93
|
@max, @keymap, @colors, @margin = 0, {}, {}, ' '
|
|
83
94
|
@left, @top, @interval, @borders = 0, 0, 1, {}
|
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.
|
|
@@ -38,6 +38,20 @@ module Typr
|
|
|
38
38
|
|
|
39
39
|
def headspace; @header ? 1 : 0 end
|
|
40
40
|
|
|
41
|
+
##
|
|
42
|
+
# Map a mouse click at 1-based terminal (row, col) to a data row id on
|
|
43
|
+
# the current page, or nil when the click lands outside the widget (border,
|
|
44
|
+
# header, margins). Row ids match what hint presses return from `pick`.
|
|
45
|
+
#
|
|
46
|
+
# stack.hit 3, 5 # => 0 (row 3, col 5 hits data row 0)
|
|
47
|
+
|
|
48
|
+
def hit row, col
|
|
49
|
+
return unless row and col
|
|
50
|
+
rel = row - 1 - top - headspace
|
|
51
|
+
return unless rel.between?(0, height - 1) and col.between?(left, right)
|
|
52
|
+
page.to_a[rel]
|
|
53
|
+
end
|
|
54
|
+
|
|
41
55
|
##
|
|
42
56
|
# The usable height after subtracting any header space.
|
|
43
57
|
#
|
|
@@ -193,10 +207,13 @@ module Typr
|
|
|
193
207
|
# stack.pick # single row picker
|
|
194
208
|
# stack.pick :column, 3 # pick a column in row 3
|
|
195
209
|
# stack.pick "rows" # multi-select rows
|
|
210
|
+
#
|
|
211
|
+
# Mouse: left-click a row to pick it; wheel up/down scrolls the page.
|
|
196
212
|
|
|
197
213
|
def pick type = :row, row=0 #, key=nil
|
|
198
214
|
type = type.to_s
|
|
199
215
|
multiple = type[-1] == ?s
|
|
216
|
+
@modifier = nil
|
|
200
217
|
loop do
|
|
201
218
|
if type['field']
|
|
202
219
|
row = pick( :row ) or return
|
|
@@ -207,12 +224,27 @@ module Typr
|
|
|
207
224
|
draw_hints type, row unless type['none']
|
|
208
225
|
limit = type['row'] ? height : positions(row).count
|
|
209
226
|
key = Typr.read_key
|
|
210
|
-
if key.is_a?(
|
|
227
|
+
if key.is_a?(Typr::Mouse)
|
|
228
|
+
if key.wheel? and key.press?
|
|
229
|
+
send( key.wheel_up? ? @keymap[:up] : @keymap[:down] )
|
|
230
|
+
next
|
|
231
|
+
elsif key.press? and key.left? and type['row'] and
|
|
232
|
+
value = hit( key.y, key.x )
|
|
233
|
+
relative = value if type['relative_']
|
|
234
|
+
end
|
|
235
|
+
elsif key.is_a?(String) and value =
|
|
211
236
|
@hints[0..limit-@hints_start-1].index(key)
|
|
212
237
|
value += @hints_start
|
|
213
238
|
relative = value + @start if type['relative_']
|
|
214
239
|
if type['row'] then value = page.to_a[ value ]
|
|
215
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
|
|
216
248
|
end unless type['none']
|
|
217
249
|
end
|
|
218
250
|
|
|
@@ -270,7 +302,7 @@ module Typr
|
|
|
270
302
|
@keymap = { cycle: KEY_TAB, confirm: KEY_RETURN, exit: KEY_ESCAPE,
|
|
271
303
|
page_down: KEY_PAGEDOWN, page_up: KEY_PAGEUP, up: KEY_UP,
|
|
272
304
|
down: KEY_DOWN, to_top: KEY_HOME, to_bottom: KEY_END }.merge @keymap
|
|
273
|
-
@hints ||= "
|
|
305
|
+
@hints ||= "1234567890"
|
|
274
306
|
end
|
|
275
307
|
end
|
|
276
308
|
|