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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: baf5925e72ba2632808c7ab3b981b00ed2a94cf1f4fbb634768c4b544251b822
4
- data.tar.gz: 5786405ffab8b20d1bc1623f070483cc6f39d519e1fdc71e8d8ebe9464a7489d
3
+ metadata.gz: cdcaef4b1bc2c40e6fab6bd7d1462ddf19aeca03babcdbb9be74629551230647
4
+ data.tar.gz: b4e927b5237f27e2366ed15057054b33d9f070ef5a50c234533699182c7228ee
5
5
  SHA512:
6
- metadata.gz: b15ea70ada75c5608a69d55e1220dd93a50dda8ce0af64b44ab0f15e6a6da7d075b70159267313d8cdd1f36495a848c38c8827ce267d2f2db503d46445fb2ed1
7
- data.tar.gz: d1368cfd4af314c40ca8af3646e4e10facd508281e9e3180b79c30d17c7fd1b2466e0375826be4e86609898eb4253501c4a3540e8904f78a29eabb41146f515b
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 terminal-based user interfaces. It provides data-object based views and keyboard-driven interaction.
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
- ### Views
11
+ ### Components
8
12
 
9
- - **Grid** — sortable, filterable table with formatted columns, colors, and row selection
10
- - **Text** — scrollable text viewer with word/line picking, search, and borders
11
- - **Browser** — file-system directory browser with MIME type detection
12
- - **Stack, Space** — layout containers for composing views
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
- ### Frontends
18
+ ### Keyboard interaction
16
19
 
17
- typr supports two rendering backends, selectable via `require`:
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
- - **Terminal** (`require 'typr'`) raw terminal IO, no dependencies beyond Ruby stdlib
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
- All views work identically across frontends.
24
+ ### Mouse support
23
25
 
24
- ### Keyboard interaction
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
- Navigation uses **hint numbers** displayed next to selectable items — rows in grids, columns, or 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. All keys are dispatched through a configurable keymap.
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.0'
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.draw
62
- key = Typr.read(:key) # reads a single keypress
63
- @grid.reset # reset grid to default view
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.draw
85
- key = Typr.read(:key)
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("/home") # interactive file picker, returns path
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.draw
142
- @prompt.draw
143
- key = Typr.read(:key)
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.read(type)`
172
- Reads input from the terminal. Types:
173
- - `:key` — a single keypress (returns `KEY_ESCAPE`, `?q`, etc.)
174
- - `:line` — a full line of text input
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 views)
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
- | `sorted_by` | Column index the grid is sorted by (setter) |
187
- | `filters` | Active filters array (getter) |
188
- | `selected` | Selected row indices (getter/setter) |
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(start_dir)` | Interactive file picker, returns the selected path |
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{ show " %s, sort:%s filter:%s selected:%s" %
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.draw
56
- @user.draw
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 @user.draw eval( Typr.read(:line) )
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 / [R]ows, pick [w]ord / [W]ords, [esc] quit",
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.draw
46
- @user.draw
48
+ @text.show
49
+ @user.show
47
50
  case key = Typr.read( :key )
48
51
  when KEY_ESCAPE; exit
49
- # when ?/; @text.search( @user.ask filter: :string )
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, :dir_history
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] + [??]*7 + ["inode","symlink"] unless File.exist?(file)
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
- `file --mime-type '#{missing.keys.join "' '"}'
97
- `.split($/).each.with_index{ |line,id|
98
- next unless line[/^.+:\s+\w+\/[\w\-\.]+$/]
99
- file, mime = line.split(/:\s*/)
100
- data[ missing[file] ][-2,2] = mime.rstrip.split( ?/ ) } if
101
- @mimetype_magic and not missing.empty?
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
- # Recurse through the directory tree, adding subdirectory entries inline
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 wraps the path in single quotes. Enter on a directory
269
- # triggers cd. +type+ optionally filters by MIME family (audio, video,
270
- # image, text, application, inode).
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 # => '/home/user/file.txt'
273
- # browser.pick 'image' # => '/path/to/photo.png' (images only)
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
- draw
279
+ show
284
280
  draw_hints :row
285
- key = Typr.read( :key )
286
- next unless choice = send( key )
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 = ?' + choice.map{ |id|
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 = ?'+@directory+?/+file[NAME]+?' end
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.draw # draws browser with info bar
304
-
305
- def draw; super
306
- info = { "[f]ilter": @filters.empty? ? "no filter" : @filters.to_s[1..-2],
307
- "[s]ort": sorted_by(true), "[v]iews": @view }
308
- move left, top - 1
309
- show prepare( @directory, width - info.values.join.size - 3, :left, :left, 5 )
310
- @colors[:actions].each do |type,col|
311
- color col
312
- show ' ' + info[type].to_s
313
- @positions[type] = Typr.column - (type==@positions.keys.last ? 0:1 )
314
- end
315
- @user.draw
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
- @dir_history, @mimetype_db, @mimetype_magic = [], true, true
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, sort_view: ?s, dir_history: ?d,
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, recurse: ?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 accessed
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}, top: ->{@parent.top-1},
426
- input: [ [->{@parent.directory}, ->{@parent.filters.empty? ? "no filter" :
427
- @parent.filters.to_s[1..-2] }, ->{@parent.sorted_by(true)},
428
- ->{@parent.view} ] ], format: [:max, :min, :min, :min],
429
- colors: { columns: [@colors[:default]] + @colors[:actions].values } )
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[ [H]idden_files [r]eset [d]irectories [m]ark [M]ark_range mark_[a]ll
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