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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a6c88b12b4be9c1fe406898172147d1d874e0353af72d211d2530ef5a48d82f
4
- data.tar.gz: 36dd949f072aa3ab232256d4c7d7c10d6989812c97977f65f72bd25ef7d6124a
3
+ metadata.gz: 65f06238b297e2c6f925f139336c89b5606e23c7a08a6fb94ed82848490e9e71
4
+ data.tar.gz: d958ec7e5f7cd5857c2b559b3e32327b590652c8fb759f0557effb33d8c27551
5
5
  SHA512:
6
- metadata.gz: 87a3993d064a814f0c819a5466c4245029d38aeabd3b6b869a9d5f2f505b60daa45aecea6447dac70cee577605e8ec1d66ee5ce858751d1122614905a9983610
7
- data.tar.gz: f29f2787e8cc2b606bfb215adebe21b1384bcc860b986318bfcff334105ebea72f30844124083dc6471c81a215698dbd3aa2fcfca9b63d92594c09ab89373bdf
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 terminal-based user interfaces. It provides data-object based spaces 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
- ### Spaces
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 spaces
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 spaces 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
@@ -59,8 +74,8 @@ Typr.init
59
74
 
60
75
  Typr.clear
61
76
  @grid.show
62
- key = Typr.read(:key) # reads a single keypress
63
- @grid.reset # reset grid to default view
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.read(:key)
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
  ```
@@ -140,7 +155,7 @@ class MyApp
140
155
  loop do
141
156
  @grid.show
142
157
  @prompt.show
143
- key = Typr.read(:key)
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 spaces)
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/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.with_index do |file,id|
67
- if File.symlink? file
68
- target = File.readlink( file ) rescue $!.to_s
69
- next [file, target, "????", false, "?", "?", 0, 0, 0, 0, "inode", "symlink"] unless File.exist?(file)
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
- stat = File.stat(file) rescue nil
72
- next [file, "", "????", false, "?", "?", 0, 0, 0, 0, "?", "?"] unless stat
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 not missing.empty?
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
- switch_to @view
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.switch_to :full # all columns
131
- # browser.switch_to 0 # first view key
132
- # browser.switch_to :stats # name, size, perms, owner, group, modified
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 switch_to view=nil
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
- # Recurse through the directory tree, adding subdirectory entries inline
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; switch_to( popup( type: :"[v]iews", input: @views.keys ).pick :row ) end
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 wraps the path in single quotes. Enter on a directory
266
- # triggers cd. +type+ optionally filters by MIME family (audio, video,
267
- # image, text, application, inode).
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 # => '/home/user/file.txt'
270
- # browser.pick 'image' # => '/path/to/photo.png' (images only)
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
- add_filter ( %w[audio video image text application inode].include?(
278
- type) ? TYPE : SUBTYPE ), type if type != 'file'
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
- reset :display
296
+ @filters.clear
297
+ sort
285
298
  return
286
299
  end
287
- next unless choice = send( key )
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
- reset :display
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
@@ -1,4 +1,4 @@
1
- require 'sdl2'
1
+ require 'sdl3'
2
2
  require_relative 'frontend'
3
3
 
4
4
  module Visuals
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
- fields = (header ? @header : @data[row] ).dup
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?(String) and value =
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 ||= "1234567890qwertyuiopasdfghjklzxcvbnm-=[];'\,./"
305
+ @hints ||= "1234567890"
274
306
  end
275
307
  end
276
308