typr 1.1.4 → 1.3.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 +41 -0
- data/README.md +68 -37
- data/example/grid +4 -4
- data/example/text +7 -4
- data/lib/browser.rb +106 -78
- data/lib/frontend.rb +7 -3
- data/lib/graphical.rb +10 -13
- data/lib/grid.rb +69 -14
- data/lib/line.rb +57 -27
- data/lib/space.rb +25 -5
- data/lib/stack.rb +34 -10
- data/lib/terminal.rb +280 -27
- data/lib/text.rb +129 -7
- data/lib/typr.rb +8 -9
- data/share/mimetypes +1298 -1
- data/share/terminfo +691 -0
- data/typr.gemspec +4 -3
- metadata +3 -8
- data/Gemfile +0 -10
- data/lib/curses.rb +0 -120
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cdcaef4b1bc2c40e6fab6bd7d1462ddf19aeca03babcdbb9be74629551230647
|
|
4
|
+
data.tar.gz: b4e927b5237f27e2366ed15057054b33d9f070ef5a50c234533699182c7228ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 889fd403e345e89dc6777c02c6736d0865b7266b16157e13e4cf2469b9a3664d68f4fe672d1f9b5b6d72316d697c2c8e20e1f920853ae495014c9fb049fed6cd
|
|
7
|
+
data.tar.gz: 662df0d3ef61d964cd8a03a1cd1f7caa7fb14a15d87c22afa3a2cdf9115f4780b944446ebcdc9f538c33ee80753e48c362fea7f716d2ef7fca2070f07fe5483f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v1.3.0] — 2026-08-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- 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
|
|
7
|
+
- `Stack#hit` — maps a 1-based terminal row/col to the data row id under the cursor (nil for header/margins/outside)
|
|
8
|
+
- 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)
|
|
9
|
+
- Wheel-to-scroll in pickers — mouse wheel up/down scrolls the listing in `Stack#pick`/`Browser#pick`
|
|
10
|
+
|
|
11
|
+
## [v1.2.0] — 2026-08-02
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `Typr.read_key` / `Typr.read_line` — the public terminal input API: `read_key` reads a raw key; `read_line` is the shared interactive line editor that renders a prompt + query at any position with arrow keys, ctrl-arrow word jumps, Home/End/Delete/Backspace, Escape aborts (`nil`), Enter confirms, and an optional block runs after each keypress
|
|
15
|
+
- `Line#ask` strict question types — `Array` (pick from an object/column), `:key`, `:line`; the question and answer are rendered in `@colors[:question]`/`@colors[:answer]`
|
|
16
|
+
- `Line#align` attribute (`:left`/`:right`) for text alignment in draw
|
|
17
|
+
- `dark_*` named colors (`dark_red`, `dark_green`, `dark_yellow`, `dark_blue`, `dark_magenta`, `dark_cyan`) and a `COLOR_MAP` of popular named colors in `color_code`
|
|
18
|
+
- `Text#search` / `Text#search_next` / `Text#search_prev` (`/`, `n`, `p`) — less-style live search with smart-case matching, String/Regexp queries, and wrap-around; `Text#highlight` / `Text#highlight_clear` per-line match highlighting (`@colors[:search]`)
|
|
19
|
+
- `Browser#search` — `/` binding routing through `pick :none, column: NAME`
|
|
20
|
+
- `Browser#cd` data rows include an `executable` column (`Typr::EXECUTABLE`, after `permissions`), shown in the `:full` view
|
|
21
|
+
- `Browser#pick` returns raw absolute paths (double-quoted, joined with a space for multi-select)
|
|
22
|
+
- `Space` symbolizes string keys in the `colors:` hash, so string-keyed config (as loaded from YAML) is honored
|
|
23
|
+
- Full documentation comments for the `Typr` module and `Line#ask`
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Renamed `Typr.read` → `Typr.read_key` and `Typr.edit` → `Typr.read_line`
|
|
27
|
+
- Renamed the frontend `show` → `draw` and the widget render `draw` → `show`
|
|
28
|
+
- `Line#ask` dispatch is strict — only `Array`, `:key`, and `:line` question types; `:string`/`:integer` are gone
|
|
29
|
+
- Removed the Readline dependency
|
|
30
|
+
- `Typr.init` duplicates the default color array instead of aliasing the caller's
|
|
31
|
+
|
|
32
|
+
### Removed
|
|
33
|
+
- Alphabet hinting from `Grid#pick` and `Stack` — hints are numbers only
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- `Grid#pick column:` live-filter/search — prompt drawn at `self.bottom + 1`, backspace broadens the result set, Escape restores the pre-search state, PageUp/PageDown/Home/End scroll, TAB cycles hints, `:none` pick works
|
|
37
|
+
- `Stack#send` key normalization — only printable ASCII normalized so control keys match keymap entries
|
|
38
|
+
- `Browser#cd` MIME lookup handles filenames with `:`, `'`, `(`, and full-width UTF-8 (argv invocation, greedy regex parsing)
|
|
39
|
+
- `Typr.read_line` renders colored prompts without shifting the cursor (`Typr.text_width` is escape-aware)
|
|
40
|
+
- `Grid#sort` re-applies active filters after sorting
|
|
41
|
+
- `Typr.height`/`width` consistency so `bottom: -1` stays on screen
|
|
42
|
+
- Suppressed constant-redefinition warnings from `Grid#header=`
|
|
43
|
+
|
|
3
44
|
## [v1.1.4] — 2026-07-28
|
|
4
45
|
|
|
5
46
|
### 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
|
|
@@ -58,9 +73,9 @@ Typr.init
|
|
|
58
73
|
)
|
|
59
74
|
|
|
60
75
|
Typr.clear
|
|
61
|
-
@grid.
|
|
62
|
-
key = Typr.
|
|
63
|
-
@grid.reset
|
|
76
|
+
@grid.show
|
|
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
|
|
|
@@ -81,8 +96,8 @@ Typr.init
|
|
|
81
96
|
|
|
82
97
|
Typr.clear
|
|
83
98
|
loop do
|
|
84
|
-
@text.
|
|
85
|
-
key = Typr.
|
|
99
|
+
@text.show
|
|
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
|
```
|
|
@@ -138,9 +153,9 @@ class MyApp
|
|
|
138
153
|
def run
|
|
139
154
|
Typr.clear
|
|
140
155
|
loop do
|
|
141
|
-
@grid.
|
|
142
|
-
@prompt.
|
|
143
|
-
key = Typr.
|
|
156
|
+
@grid.show
|
|
157
|
+
@prompt.show
|
|
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/example/grid
CHANGED
|
@@ -43,7 +43,7 @@ class GridTest
|
|
|
43
43
|
@user = Line.new( top: -1, default:
|
|
44
44
|
"(s)ort, (f)ilter, (r)eset, select (R)ows/(C)olumns/(F)ields, [esc] quit")
|
|
45
45
|
@head = Line.new( interval: 1,
|
|
46
|
-
default: proc{
|
|
46
|
+
default: proc{ draw " %s, sort:%s filter:%s selected:%s" %
|
|
47
47
|
[ Time.now.strftime("%D %T"),
|
|
48
48
|
(@grid.header[@grid.sorted_by.abs] if @grid.sorted_by),
|
|
49
49
|
@grid.filters.join, @grid.selected ||= [] ] } )
|
|
@@ -52,14 +52,14 @@ class GridTest
|
|
|
52
52
|
def run
|
|
53
53
|
Typr.clear
|
|
54
54
|
loop do
|
|
55
|
-
@grid.
|
|
56
|
-
@user.
|
|
55
|
+
@grid.show
|
|
56
|
+
@user.show
|
|
57
57
|
@head.start
|
|
58
58
|
key = Typr.read( :key )
|
|
59
59
|
@head.stop
|
|
60
60
|
case key
|
|
61
61
|
when KEY_ESCAPE; exit
|
|
62
|
-
when ?` then
|
|
62
|
+
when ?` then (line = Typr.read(:line)) and @user.show(eval(line))
|
|
63
63
|
when ?s then @grid.sort_by( @user.ask "sort column": [@grid, :column] )
|
|
64
64
|
when ?F then @user.ask("pick fields": [@grid, :fields])
|
|
65
65
|
when ?R then @user.ask("pick rows": [@grid, :rows])
|
data/example/text
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
# word-wrapped scrollable viewport with a border.
|
|
10
10
|
#
|
|
11
11
|
# Keys:
|
|
12
|
+
# / Search (type pattern, Enter to run)
|
|
13
|
+
# n Next match
|
|
14
|
+
# p Previous match
|
|
12
15
|
# arrows Scroll up/down
|
|
13
16
|
# w Pick a single word (hint-driven)
|
|
14
17
|
# W Pick multiple words
|
|
@@ -35,18 +38,18 @@ class TextTest
|
|
|
35
38
|
colors: { header: [:yellow, :grey30], hints: [:black, :white] }
|
|
36
39
|
)
|
|
37
40
|
@user = Line.new( top: -1,
|
|
38
|
-
default: "pick [r]ow
|
|
41
|
+
default: "[/]:search [n]ext [p]rev, pick [r]ow/[R]ows, [w]ord/[W]ords, [esc] quit",
|
|
39
42
|
colors: { question: :blue, answer: :yellow } )
|
|
40
43
|
end
|
|
41
44
|
|
|
42
45
|
def run
|
|
43
46
|
Typr.clear
|
|
44
47
|
loop do
|
|
45
|
-
@text.
|
|
46
|
-
@user.
|
|
48
|
+
@text.show
|
|
49
|
+
@user.show
|
|
47
50
|
case key = Typr.read( :key )
|
|
48
51
|
when KEY_ESCAPE; exit
|
|
49
|
-
|
|
52
|
+
when ?/; @text.search
|
|
50
53
|
when ?w; @user.ask( 'pick word': [@text, :field] )
|
|
51
54
|
when ?W; @user.ask( 'pick words': [@text, :fields] )
|
|
52
55
|
when ?r; @user.ask( 'pick rows': [@text, :row] )
|
data/lib/browser.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Typr
|
|
|
33
33
|
# Escape | quit, returns nil
|
|
34
34
|
|
|
35
35
|
class Browser < Grid
|
|
36
|
-
attr_accessor :directory, :user, :pwd, :view
|
|
36
|
+
attr_accessor :directory, :user, :pwd, :view
|
|
37
37
|
attr_accessor :mimetype_db, :mimetype_magic
|
|
38
38
|
|
|
39
39
|
## Height minus 2 rows reserved for path bar and command line.
|
|
@@ -59,21 +59,17 @@ module Typr
|
|
|
59
59
|
data, missing = [],{}
|
|
60
60
|
unless append
|
|
61
61
|
@colors[:fields] = {}
|
|
62
|
-
dir = @dir_history[dir+1] if dir.is_a? Integer
|
|
63
62
|
clear
|
|
64
63
|
Dir.chdir( @directory = File.expand_path( dir ) )
|
|
65
|
-
unless [?/, ENV["HOME"]].include? dir
|
|
66
|
-
@dir_history.unshift @directory
|
|
67
|
-
@dir_history.uniq!
|
|
68
|
-
end
|
|
69
64
|
end
|
|
70
65
|
data = Dir.new(@directory).children.reject{|f|
|
|
71
66
|
!@show_hidden and f[0] == ?. }.map.with_index do |file,id|
|
|
72
67
|
if File.symlink? file
|
|
73
68
|
target = File.readlink( file ) rescue $!.to_s
|
|
74
|
-
next [file, target
|
|
69
|
+
next [file, target, "????", false, "?", "?", 0, 0, 0, 0, "inode", "symlink"] unless File.exist?(file)
|
|
75
70
|
end
|
|
76
|
-
stat = File.stat(file)
|
|
71
|
+
stat = File.stat(file) rescue nil
|
|
72
|
+
next [file, "", "????", false, "?", "?", 0, 0, 0, 0, "?", "?"] unless stat
|
|
77
73
|
type = stat.ftype.to_sym
|
|
78
74
|
|
|
79
75
|
mime = { "directory": "inode/directory",
|
|
@@ -88,17 +84,23 @@ module Typr
|
|
|
88
84
|
( missing[file] = id ; mime = "?/?" ) unless mime
|
|
89
85
|
[ file, target, stat.mode.to_s(8)[-3..-1].chars.map{|d|
|
|
90
86
|
"rwx".chars.map.with_index{|c,i| d.to_i[2-i]==0 ? ?- : c } }.join,
|
|
91
|
-
stat.uid, stat.gid, stat.size,
|
|
87
|
+
File.executable?(file), stat.uid, stat.gid, stat.size,
|
|
92
88
|
stat.atime.to_i, stat.mtime.to_i, stat.ctime.to_i,
|
|
93
89
|
*mime.to_s.split( ?/ ) ]
|
|
94
90
|
end
|
|
95
91
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
if @mimetype_magic and not missing.empty?
|
|
93
|
+
begin
|
|
94
|
+
out = IO.popen(['file', '--mime-type', *missing.keys]){ |io| io.read }
|
|
95
|
+
rescue Errno::ENOENT
|
|
96
|
+
out = ''
|
|
97
|
+
end
|
|
98
|
+
out.split($/).each{ |line|
|
|
99
|
+
file, mime = line.match(/^(.*):\s+(\w+\/[\w\-\.]+)$/)&.captures
|
|
100
|
+
next unless file
|
|
101
|
+
data[ missing[file] ][-2,2] = mime.split( ?/ ) if missing[file] }
|
|
102
|
+
end
|
|
103
|
+
data.each { |row| row[0] = File.join(@directory, row[0]) }
|
|
102
104
|
self << data
|
|
103
105
|
sort
|
|
104
106
|
filter
|
|
@@ -214,17 +216,7 @@ module Typr
|
|
|
214
216
|
def help; @user.help end
|
|
215
217
|
|
|
216
218
|
##
|
|
217
|
-
#
|
|
218
|
-
# via +cd(path, true)+. Sorts by name with the filetree processor.
|
|
219
|
-
#
|
|
220
|
-
# browser.recurse # flattens tree into leaf names
|
|
221
|
-
|
|
222
|
-
def recurse
|
|
223
|
-
sort_by NAME
|
|
224
|
-
@procs[NAME] = :filetree
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
## Reset display, selection, and position state then re-sort.
|
|
219
|
+
# Reset display, selection, and position state then re-sort.
|
|
228
220
|
#
|
|
229
221
|
# browser.reset_view
|
|
230
222
|
|
|
@@ -235,7 +227,17 @@ module Typr
|
|
|
235
227
|
# browser.filter_view # prompts for filter text
|
|
236
228
|
|
|
237
229
|
def filter_view; add_filter *@user.ask(
|
|
238
|
-
filter: [ popup( type: :"[f]ilter" ), :row], with: :line ) end
|
|
230
|
+
filter: [ popup( type: :"[f]ilter" ), :row, NAME], with: :line ) end
|
|
231
|
+
|
|
232
|
+
## Open live `/` search filtered on the name column.
|
|
233
|
+
#
|
|
234
|
+
# browser.search # type to narrow, Escape restores, Enter commits
|
|
235
|
+
|
|
236
|
+
def search
|
|
237
|
+
@suppress_user = true
|
|
238
|
+
pick :none, column: NAME
|
|
239
|
+
@suppress_user = false
|
|
240
|
+
end
|
|
239
241
|
|
|
240
242
|
## Open a sort-picker popup to choose a column to sort by.
|
|
241
243
|
#
|
|
@@ -249,47 +251,52 @@ module Typr
|
|
|
249
251
|
|
|
250
252
|
def views; switch_to( popup( type: :"[v]iews", input: @views.keys ).pick :row ) end
|
|
251
253
|
|
|
252
|
-
## Open a directory history picker and cd to the chosen entry.
|
|
253
|
-
#
|
|
254
|
-
# browser.dir_history # pick from previously visited dirs
|
|
255
|
-
|
|
256
|
-
def dir_history; cd( popup( { input: @dir_history[1..-1],
|
|
257
|
-
colors: { columns: [ colors[:dir_history] ] },
|
|
258
|
-
align: [:left], left: left } ).pick(:row) ) end
|
|
259
|
-
|
|
260
254
|
## True when the row is a directory or passes base Grid filtering.
|
|
261
255
|
#
|
|
262
256
|
# browser.check 0 # => true if navigable
|
|
263
257
|
|
|
264
|
-
def check row; super or @data[row][SUBTYPE] == "directory" end
|
|
258
|
+
def check row; super or ( !@filter_dirs and @data[row][SUBTYPE] == "directory" ) end
|
|
265
259
|
|
|
266
260
|
##
|
|
267
261
|
# Interactive file/directory picker. Blocks until a selection or Escape.
|
|
268
|
-
# Return on a file
|
|
269
|
-
#
|
|
270
|
-
#
|
|
262
|
+
# Return on a file yields its raw absolute path; multi-select joins the
|
|
263
|
+
# chosen paths double-quoted with spaces. Enter on a directory triggers
|
|
264
|
+
# cd. +type+ optionally filters by MIME family (audio, video, image,
|
|
265
|
+
# text, application, inode). Mouse: left-click a row to pick it; wheel
|
|
266
|
+
# up/down scrolls the listing.
|
|
271
267
|
#
|
|
272
|
-
# browser.pick # =>
|
|
273
|
-
# browser.pick 'image' # =>
|
|
268
|
+
# browser.pick # => /home/user/file.txt
|
|
269
|
+
# browser.pick 'image' # => /path/to/photo.png (images only)
|
|
274
270
|
# browser.pick nil # => anything, same as 'file'
|
|
275
271
|
|
|
276
|
-
def pick type='file'
|
|
272
|
+
def pick type='file', **kw
|
|
277
273
|
type ||= 'file'
|
|
278
274
|
type = type.to_s
|
|
279
|
-
return super if type[/row|column|field|none/]
|
|
275
|
+
return super(type, **kw) if type[/row|column|field|none/]
|
|
280
276
|
add_filter ( %w[audio video image text application inode].include?(
|
|
281
277
|
type) ? TYPE : SUBTYPE ), type if type != 'file'
|
|
282
278
|
loop do
|
|
283
|
-
|
|
279
|
+
show
|
|
284
280
|
draw_hints :row
|
|
285
|
-
key = Typr.
|
|
286
|
-
|
|
281
|
+
key = Typr.read_key
|
|
282
|
+
if key == @keymap[:exit]
|
|
283
|
+
reset :display
|
|
284
|
+
return
|
|
285
|
+
end
|
|
286
|
+
if key.is_a?(Typr::Mouse)
|
|
287
|
+
if key.wheel? and key.press?
|
|
288
|
+
send( key.wheel_up? ? @keymap[:up] : @keymap[:down] )
|
|
289
|
+
next
|
|
290
|
+
elsif key.press? and key.left? and choice = hit( key.y, key.x )
|
|
291
|
+
else next end
|
|
292
|
+
else
|
|
293
|
+
next unless choice = send( key )
|
|
294
|
+
end
|
|
287
295
|
case choice
|
|
288
|
-
when Array; choice =
|
|
289
|
-
@directory+?/+@data[id][NAME] }.join("' '") + ?'
|
|
296
|
+
when Array; choice = choice.map{ |id| @data[id][NAME] }.join('" "')
|
|
290
297
|
when Integer; file = @data[ choice ]
|
|
291
298
|
if file[SUBTYPE] == 'directory'; cd file[TARGET] || file[NAME]; next
|
|
292
|
-
else choice =
|
|
299
|
+
else choice = file[NAME] end
|
|
293
300
|
end
|
|
294
301
|
reset :display
|
|
295
302
|
return choice
|
|
@@ -300,19 +307,35 @@ module Typr
|
|
|
300
307
|
# Renders the grid with an info bar above showing the current directory
|
|
301
308
|
# path and colored status items for filter, sort, and view.
|
|
302
309
|
#
|
|
303
|
-
# browser.
|
|
304
|
-
|
|
305
|
-
def
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
@
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
310
|
+
# browser.show # shows browser with info bar
|
|
311
|
+
|
|
312
|
+
def show; super
|
|
313
|
+
user = Etc.getpwuid(Process.euid).name rescue ENV["USER"] || Etc.getlogin
|
|
314
|
+
@head.colors[:columns][0] = user == "root" ? [:red, :black] : [:green, :black]
|
|
315
|
+
@head.reset :format
|
|
316
|
+
@head.show
|
|
317
|
+
move @head.positions(0)[1] - @head.separator.size, @parent.top - 1
|
|
318
|
+
color @colors[:default]
|
|
319
|
+
draw '@'
|
|
320
|
+
@positions = { :"[f]ilter" => 2, :"[s]ort" => 3, :"[v]iews" => 4
|
|
321
|
+
}.transform_values do |col|
|
|
322
|
+
@head.left + @head.positions(0)[col] + @head.width_for(col) - 1
|
|
323
|
+
end
|
|
324
|
+
@user.show unless @suppress_user
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
## Formats active filters as 'column: query', joined by ' / '.
|
|
328
|
+
#
|
|
329
|
+
# browser.filter_display # => "name: foo / size: big"
|
|
330
|
+
#
|
|
331
|
+
# @return [String]
|
|
332
|
+
|
|
333
|
+
def filter_display
|
|
334
|
+
return "no filter" if @filters.empty?
|
|
335
|
+
@filters.map { |col, q|
|
|
336
|
+
q = "/#{q.source}/" if q.is_a?(Regexp)
|
|
337
|
+
"#{col.is_a?(Symbol) ? col : @header[col] || col}:#{q}"
|
|
338
|
+
}.join(" / ")
|
|
316
339
|
end
|
|
317
340
|
|
|
318
341
|
## Returns row ids for all directories in the current listing.
|
|
@@ -370,15 +393,15 @@ module Typr
|
|
|
370
393
|
# 'x-msdownload': :red 'x-pie-executable': :red
|
|
371
394
|
|
|
372
395
|
def initialize args={}
|
|
373
|
-
@
|
|
396
|
+
@mimetype_db, @mimetype_magic = true, true
|
|
374
397
|
@view, @sort, @views, @positions, @show_hidden = :compact, 0, {}, {}, false
|
|
398
|
+
@filter_dirs = args[:filter_dirs]
|
|
375
399
|
super
|
|
376
400
|
@left ||= 0
|
|
377
401
|
@top ||= 0
|
|
378
402
|
@directory ||= ?~
|
|
379
403
|
@colors[:actions] = { "[f]ilter": :red, "[s]ort": :blue, "[v]iews": :green
|
|
380
404
|
}.merge @colors[:actions] || {}
|
|
381
|
-
@colors[:dir_history] ||= :yellow
|
|
382
405
|
@colors[ :types ] = {
|
|
383
406
|
image: :blue,
|
|
384
407
|
audio: :green,
|
|
@@ -393,13 +416,13 @@ module Typr
|
|
|
393
416
|
}.merge( @colors[ :types ] || {} )
|
|
394
417
|
|
|
395
418
|
@keymap = { back: KEY_BACKSPACE, confirm: KEY_RETURN, help: ?h,
|
|
396
|
-
home: ?~, views: ?v,
|
|
419
|
+
home: ?~, views: ?v, sort_view: ?s, search: ?/,
|
|
397
420
|
mark: ?m, mark_range: ?M, mark_all: ?a, mark_invert: ?i,
|
|
398
|
-
filter_view: ?f, reset_view: ?r,
|
|
421
|
+
filter_view: ?f, reset_view: ?r,
|
|
399
422
|
toggle_hidden: ?H}.merge @keymap
|
|
400
423
|
|
|
401
|
-
self.header = %w[ name target permissions owner group size
|
|
402
|
-
modified created type subtype ]
|
|
424
|
+
self.header = %w[ name target permissions executable owner group size
|
|
425
|
+
accessed modified created type subtype ]
|
|
403
426
|
|
|
404
427
|
@format[NAME] = :max
|
|
405
428
|
@format[TYPE] = 5
|
|
@@ -413,26 +436,31 @@ module Typr
|
|
|
413
436
|
@procs[GROUP] = Proc.new{|gid| Etc.getgrgid(gid).name rescue gid }
|
|
414
437
|
|
|
415
438
|
@procs[NAME] = Proc.new{ |name, row| target = @data[row][TARGET]
|
|
416
|
-
name + ( target ? " > " + color_code(File.exist?(target) ? :green : :red) +
|
|
439
|
+
File.basename(name.to_s) + ( target ? " > " + color_code(File.exist?(target) ? :green : :red) +
|
|
417
440
|
target : "" ) }
|
|
418
441
|
|
|
419
442
|
@views = { simple: %i[ name ],
|
|
420
443
|
compact: %i[ name size type],
|
|
421
444
|
stats: %i[ name size permissions owner group modified ],
|
|
422
|
-
full: %i[ name size permissions owner group
|
|
445
|
+
full: %i[ name size permissions executable owner group
|
|
423
446
|
accessed modified created type subtype ] }.merge @views
|
|
424
447
|
@parent = self
|
|
425
|
-
@head ||= Grid.new( parent:self, left: ->{@parent.left},
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
->{@parent.
|
|
429
|
-
|
|
448
|
+
@head ||= Grid.new( parent:self, left: ->{@parent.left}, margin: '',
|
|
449
|
+
right: ->{@parent.width + @parent.left - 1}, top: ->{@parent.top-1},
|
|
450
|
+
input: [ [->{Etc.getpwuid(Process.euid).name rescue ENV["USER"] || Etc.getlogin}, ->{@parent.directory},
|
|
451
|
+
->{@parent.filter_display},
|
|
452
|
+
->{ (s = @parent.sorted_by(true)) &&
|
|
453
|
+
"#{s} #{@parent.reverse ? "\u2193" : "\u2191"}"},
|
|
454
|
+
->{@parent.view} ] ], format: [:min, :max, :min, :min, :min],
|
|
455
|
+
colors: { columns: [@colors[:default], @colors[:default],
|
|
456
|
+
@colors[:actions][:"[f]ilter"], @colors[:actions][:"[s]ort"],
|
|
457
|
+
@colors[:actions][:"[v]iews"]] } )
|
|
430
458
|
|
|
431
459
|
@user ||= Line.new( parent:self, left:->{@parent.left},top:->{@parent.bottom+1},
|
|
432
|
-
bindings: %w[ [h]elp ] + @colors[:actions].map{|name,color|
|
|
460
|
+
bindings: %w[ [h]elp [/]:search ] + @colors[:actions].map{|name,color|
|
|
433
461
|
color_code( color ) + name.to_s + color_code(@colors[:default]) } +
|
|
434
|
-
%w[ [
|
|
435
|
-
[i]nvert_mark [~]:home [back]:.. [up] [down] [pageup] [pagedown] [home] [end] [esc]:quit [return]:confirm\ ])
|
|
462
|
+
%w[ [r]eset [d]irectories [m]ark [M]ark_range mark_[a]ll
|
|
463
|
+
[i]nvert_mark [~]:home [H]idden_files [back]:.. [up] [down] [pageup] [pagedown] [home] [end] [esc]:quit [return]:confirm\ ])
|
|
436
464
|
cd @directory
|
|
437
465
|
end
|
|
438
466
|
end
|
data/lib/frontend.rb
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require 'readline'
|
|
2
|
-
|
|
3
1
|
module Visuals
|
|
4
2
|
|
|
5
3
|
COLORS = {
|
|
@@ -11,7 +9,13 @@ module Visuals
|
|
|
11
9
|
magenta: [ 180, 100, 180],
|
|
12
10
|
cyan: [ 100, 180, 180],
|
|
13
11
|
white: [ 255, 255, 255],
|
|
14
|
-
grey: [ 158, 158, 158]
|
|
12
|
+
grey: [ 158, 158, 158],
|
|
13
|
+
dark_red: [ 90, 30, 30],
|
|
14
|
+
dark_green: [ 30, 90, 30],
|
|
15
|
+
dark_yellow: [ 90, 80, 30],
|
|
16
|
+
dark_blue: [ 30, 30, 90],
|
|
17
|
+
dark_magenta: [ 90, 30, 90],
|
|
18
|
+
dark_cyan: [ 30, 80, 80] }
|
|
15
19
|
|
|
16
20
|
COLORS.merge! 1.upto(9).map{ |i|
|
|
17
21
|
["grey#{ i*10 }".to_sym, [i*25]*3 ] }.to_h
|