typr 1.9.0 → 1.9.3

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: 74b0a2cd082b9a2ee58f9fb64356a7ed4f5a970c0427768acae334ae3325748a
4
- data.tar.gz: af90bbcfff025ba60f1d8de7cae5171152f8939a00a7fac5975cc95785a28547
3
+ metadata.gz: cdabc3f928eb42904b0f1cd0778384bca290ae56dfbb050236cda6a6578d19e8
4
+ data.tar.gz: 236970a28d8cd9265651985504a60a8113cdfaf2b8378303388c0a9e4883469e
5
5
  SHA512:
6
- metadata.gz: c29e087834938ef34608086cb90ec221760ca50f0be054e7d9602f46cc6e63c73fbe06a960c8b55e551b17159f404d796f275e3a11cddcb3f70baccefc67636b
7
- data.tar.gz: a33662ba4009f34cef2e91b5295a6fc1e3f2b17ebd9edf118052caa016e6dc0d8dd4d6f2c3fb468244f2e488e589c1ec79236f2a7001bc4e6622064fcf9a3365
6
+ metadata.gz: 82420afecbc67164d9271e315cd571f33898f4f4ae4ee4186da231cbb6d461df5714180e47c6237d7a54224fd0742777084d44c38e221931f763418bdc19df7d
7
+ data.tar.gz: cafd2cf36cdbf58e70249042e7952ca1048325b61ae404b0c4a154eabcb9684cbe8bf75b3aaee239d0c169da7057826a1b7e1bcae1e6dd2b81fa9145942424d9
data/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [vx.y.z] — __DATE__
4
4
 
5
+ ## [v1.9.3] — 2026-10-19
6
+
7
+ ### Fixed
8
+ - Grid rendering can no longer crash with `ArgumentError: negative argument` on emoji-wide cell text — the pad in `prepare` is clamped at zero, and `clip` measures grapheme clusters (flags, `‼️`-style presentation sequences) instead of single chars so its width accounting matches `real_size`
9
+ - Numeric hints no longer vanish when the list shrinks below the Tab-cycled hint offset (after a `move`/transfer or `cd` into a smaller directory) — `show` re-clamps `hints_start` so the existing single redraw shows the digits again without any extra drawing
10
+ - A one-entry filtered pick auto-returns before the popup is drawn — `Grid#pick` checks the initial match set before showing, so a single-entry grid returns its entry without ever displaying
11
+ - The `text` example wires `regex: :generic` colors into its Text widget, so regex highlighting is demonstrated out of the box
12
+
13
+ ## [v1.9.2] — 2026-10-13
14
+
15
+ ### Fixed
16
+ - Text renders multibyte UTF-8 correctly — `show` split rows through StringIO, whose byte-based reads and positions shifted and garbled every row after the first multibyte character; page rows are sliced by character offsets instead. Chunks that fill the viewport exactly also record a line break now instead of merging with (and hiding) their continuation
17
+
18
+ ## [v1.9.1] — 2026-10-12
19
+
20
+ ### Changed
21
+ - `rake docs:check` — fails when any class, method, attribute, or constant is undocumented (CI-style gate for the RI pass; currently 221/221)
22
+
5
23
  ## [v1.9.0] — 2026-10-12
6
24
 
7
25
  ### Changed
@@ -11,7 +29,9 @@
11
29
  - Live list narrowing is labeled `[/]:narrow`, not `[/]:search` — the bar action shrinks the listing as you type, while `search` keeps meaning only pager find-in-text (`/`, `n`, `p`). The method is renamed `Browser#search` → `#narrow` (`search` stays as an alias, so stale configs wired to `!search` keep working)
12
30
 
13
31
  ### Fixed
32
+ - Aborting the mark-range prompt no longer crashes — Escape returns nil from `ask` and `mark_range` passed it straight to `join`; it now returns nil like `mark`
14
33
  - Overtype guard scoped to `Grid#pick` and fixed for canonical ttys — private `drain_quiet` (called bare by `read_key`) waits out an armed window in raw mode, since canonical-buffered keystrokes are invisible to `select` and the old guard timed out only to deliver them late; each arrival restarts the window; `flush_overtype` and the `quiet_window?` predicate are gone, `drain_input` is back to a plain pending-bytes flush, and `suppress_keys` remains the sole arming seam for `Grid#pick` selections (filecon drops its two `flush_overtype` calls)
34
+ - RI accepted-values pass — every enum-like parameter now lists its values with examples: `Stack#pick` documents all types (`:row`/`:rows`, `:column`/`:columns`, `:field(s)`, `:relative_row`, `:none`) with per-type returns and the Grid/Text-only constraint; `Grid` format tokens (`Integer`/`:min`/`:max`) explained and the `:right`-in-format examples corrected (alignment is `align:`, numbers auto-right-align); `Grid#pick` column and `add_filter` column are `Integer`/`:all` only; `Line#ask` documents the `with:`-as-question convention; `read_line` gains full `@param`s plus the single-arg completions contract; `Stack#send`/`Browser#pick` gain `@param`/`@return`; stale claims removed (`keysyms`, `colors[:dir_history]`, `cd` dir_history, `sorted_by` false, README `cli` `completions:`); three one-word code fixes where docs were right and code wrong (`Grid#<<`/`Text#build` return `self`, `Line#append` returns the content, as their examples claim)
15
35
  - Doc comments corrected where they disagreed with the code — `Grid#header=` defines `Typr::NAME` holding the column index (not `NAME_0 = "name"`); the mouse header-click sort doc sat on `sort_by` instead of `Grid#send`; `Grid#pick` now documents the single-match auto-return, the 0.5s overtype quiet window, and `:none` disabling both; `Line#ask` documents the `[:line, { initial: }]` inline-editor form; `Line#cli` no longer mentions a removed `completions:` option; `Browser#bottom` reserves only the command-line row (the path bar is `top`)
16
36
 
17
37
  ## [v1.8.0] — 2026-10-11
data/README.md CHANGED
@@ -67,7 +67,7 @@ Typr.init
67
67
  ["Carol", 35, "manager"]
68
68
  ],
69
69
  header: ["Name", "Age", "Role"],
70
- format: [:min, :right, :max],
70
+ format: [:min, :min, :max],
71
71
  colors: { columns: [:yellow, :cyan, :magenta] }
72
72
  )
73
73
 
@@ -139,7 +139,7 @@ class MyApp
139
139
  ["Item C", 20, true]
140
140
  ],
141
141
  header: ["Name", "Qty", "Active"],
142
- format: [:max, :right, :min],
142
+ format: [:max, :min, :min],
143
143
  colors: { columns: [:yellow, :cyan, :green] }
144
144
  )
145
145
  @prompt = Typr::Line.new(
@@ -202,10 +202,10 @@ Strict question types:
202
202
  The question and answer are rendered in `@colors[:question]`/`@colors[:answer]`.
203
203
 
204
204
  ### `Line#cli`
205
- Prompts for a single command line with TAB completion matched against the shell (bash commands, options, and files). `completions: true` switches on the content; `initial:` seeds the line; `redraw:` repaints while the completion grid is up. With `regex: :shell` the typed command tints red for the first word, blue for `--options`. Completion candidates are plain strings.
205
+ Prompts for a single command line with TAB completion matched against the shell (bash commands, options, and files). `initial:` seeds the line; `history:` recalls previous commands with Up/Down; `prefixes:` lists command-start characters stripped before matching; `redraw:` repaints while the completion grid is up. With `regex: :shell` the typed command tints red for the first word, blue for `--options`. Completion candidates are plain strings.
206
206
 
207
207
  ```ruby
208
- line.cli "> ", initial: "cp %name %dir", completions: true, redraw: -> { show }
208
+ line.cli "> ", initial: "ls ", history: @recent
209
209
  ```
210
210
 
211
211
  ### Layout properties (all components)
data/example/text CHANGED
@@ -33,7 +33,7 @@ class TextTest
33
33
  input: $<, header: "Text Area Header",
34
34
  top: 2, left: 0.1, right: 0.9, bottom: -4,
35
35
  border: :round, borders:{ left: ?┆, right: ?┆, top: ?╌, bottom: ?╌ },
36
- colors: { header: [:yellow, :grey30], hints: [:black, :white] }
36
+ colors: { header: [:yellow, :grey30], hints: [:black, :white], regex: :generic }
37
37
  )
38
38
  @user = Line.new( top: -1,
39
39
  default: "[/]:search [n]ext [p]rev, pick [r]ow/[R]ows, [w]ord/[W]ords, [esc] quit",
data/lib/browser.rb CHANGED
@@ -71,17 +71,15 @@ module Typr
71
71
  def top; super + 1 end
72
72
 
73
73
  ##
74
- # Change to +dir+ (absolute path, +".."+, +"~"+, or integer index into
75
- # dir_history). When +append+ is false (default), clears data and colors
76
- # before scanning. Pushes onto dir_history.
74
+ # Change to +dir+ (absolute path, +".."+, or +"~"+). Unless +append+,
75
+ # clears data and colors before scanning.
77
76
  #
78
77
  # browser.cd "/tmp" # => changes to /tmp
79
78
  # browser.cd "~" # => changes to home
80
79
  # browser.cd ".." # => parent directory
81
- # browser.cd 2 # => jump to history entry 2
82
80
  #
83
- # @param dir [String, Integer] target path or history index
84
- # @param append [Boolean] scan without clearing (used by history)
81
+ # @param dir [String] target path
82
+ # @param append [Boolean] scan without clearing
85
83
  # @param trim [Integer] path prefix trim for the +path+ column
86
84
  # @return [false, nil] false when the directory cannot be opened
87
85
 
@@ -195,6 +193,8 @@ module Typr
195
193
  # browser.change_view :full # all columns
196
194
  # browser.change_view 0 # first view key
197
195
  # browser.change_view :stats # name, size, perms, owner, group, modified
196
+ #
197
+ # @return [nil]
198
198
 
199
199
  def change_view view=nil
200
200
  return unless view
@@ -257,11 +257,11 @@ module Typr
257
257
  #
258
258
  # browser.mark_range # prompts for from/to, toggles selection
259
259
  #
260
- # @return [Array<Integer>] the toggled ids
260
+ # @return [Array<Integer>, nil] the toggled ids, or nil on abort
261
261
 
262
- def mark_range; return ids[ ( eval @user.ask(
262
+ def mark_range; return unless picked = @user.ask(
263
263
  'mark from': [self, :relative_row], 'to': [self, :relative_row]
264
- ).join '..' ) ].each{|id| @selected[:rows].include?(id) ?
264
+ ); return ids[ ( eval picked.join '..' ) ].each{|id| @selected[:rows].include?(id) ?
265
265
  @selected[:rows].delete(id) : @selected[:rows] << id } end
266
266
 
267
267
  ## Selects all non-directory rows.
@@ -383,6 +383,12 @@ module Typr
383
383
  # browser.pick # => /home/user/file.txt
384
384
  # browser.pick 'image' # => /path/to/photo.png (images only)
385
385
  # browser.pick nil # => anything, same as 'file'
386
+ #
387
+ # @param type [String, Symbol, nil] MIME family, +"file"+ (default), nil for anything,
388
+ # or a picker type (+:row+, +:rows+, +:column+, +:columns+, +:field+,
389
+ # +:fields+, +:relative_row+, +:none+) forwarded to {Stack#pick}
390
+ # @param kw [Hash] +column:+ for / search (forwarded to {Grid#pick})
391
+ # @return [String, nil] quoted path(s), the directory on Enter (directory type), or nil
386
392
 
387
393
  def pick type='file', **kw
388
394
  type ||= 'file'
@@ -470,6 +476,8 @@ module Typr
470
476
  # within each prefix grouping.
471
477
  #
472
478
  # browser.sort # re-sorts with dirs first
479
+ #
480
+ # @return [nil]
473
481
 
474
482
  def sort
475
483
  @sort ||= NAME
@@ -500,10 +508,13 @@ module Typr
500
508
  # Option | Default | Description
501
509
  # --------------------|---------------------------------|-----------------------------------
502
510
  # +directory+ | +"~"+ | Starting directory to browse
511
+ # +views+ | {} | Extra named column lists (Array of headers) merged over the built-ins
512
+ # +bindings+ | (built-in keybar) | Bottom-bar label to method-name map (e.g. +"[v]iews"+ to +"!views"+)
513
+ # +filter_dirs+ | nil | When set, directories must pass +filters+ like files
514
+ # +keymap+ | {} | Custom name-to-key mappings (merged over navigation defaults)
503
515
  # +mimetype_db+ | true | Load MIME from file extensions
504
516
  # +mimetype_magic+ | true | Use +file --mime-type+ for unknowns
505
517
  # +colors[:actions]+ | { filter: :red, ... } | Status bar item colors
506
- # +colors[:dir_history]+ | :yellow | History picker column color
507
518
  # +colors[:types]+ | (see below) | Per MIME-type foreground color
508
519
  #
509
520
  # == Type Color Defaults
data/lib/grid.rb CHANGED
@@ -8,11 +8,15 @@ module Typr
8
8
  # grid = Typr::Grid.new(
9
9
  # input: [ ["Alice", 30], ["Bob", 25] ],
10
10
  # header: [:name, :age],
11
- # format: [:max, :right]
11
+ # format: [:max, :min]
12
12
  # )
13
13
  # grid.show
14
14
  #
15
- # Column format tokens: +Integer+ (fixed width), +:min+ (auto), +:max+ (fill remaining).
15
+ # Column widths (+format+): an +Integer+ fixes the width; +:min+ sizes to
16
+ # the widest visible cell; +:max+ sizes to content and absorbs leftover
17
+ # viewport width. Anything else behaves like +:min+. Numbers right-align
18
+ # automatically (see +align+). When the content overflows the viewport,
19
+ # the widest columns shrink to fit.
16
20
  #
17
21
  # Built-in procs: +:filetree+, +:datetime+, +:magnitudes+, +:convert+.
18
22
 
@@ -25,11 +29,20 @@ module Typr
25
29
  #
26
30
  # grid.filters #=> [[0, "Ali"]]
27
31
  # grid.map #=> [0, 2]
32
+ #
33
+ # +procs+ values are built-in names (+:filetree+, +:datetime+,
34
+ # +:magnitudes+, +:convert+) or callables: a lambda takes the cell,
35
+ # a full proc takes cell, row, and column.
36
+ #
37
+ # grid.procs[1] = ->(v) { v * 2 } # cell only
28
38
 
29
39
  attr_accessor :format, :procs, :filters, :data, :map
30
40
  ##
31
41
  # +align+ is per-column alignment, +rawsort+/+rawfilter+ flag columns
32
42
  # to bypass their +procs+, and +reverse+ flips the sort direction.
43
+ # +align+ is +:left+ or +:right+ (numeric cells default to +:right+);
44
+ # +rawsort+/+rawfilter+ are arrays indexed by column where truthy
45
+ # bypasses the column's proc.
33
46
  #
34
47
  # grid.reverse = true
35
48
  # grid.rawsort[0] = true # sort raw values for column 0
@@ -139,12 +152,14 @@ module Typr
139
152
  ##
140
153
  # Set column headers. Symbols are auto-defined as +UPCASE+ constants
141
154
  # holding the column index.
155
+ # A plain String instead draws as a label bar with no column meaning.
142
156
  #
143
157
  # grid.header = ["name", "age"]
144
158
  # Typr::NAME #=> 0
145
159
  # Typr::AGE #=> 1
160
+ # grid.header = "People" # label bar, defines no constants
146
161
  #
147
- # @param h [Array<String, Symbol>] header labels
162
+ # @param h [Array<String, Symbol>, String] header labels or a label bar
148
163
 
149
164
  def header=( h ); super
150
165
  h.each_with_index{ |h,i|
@@ -206,8 +221,8 @@ module Typr
206
221
  # grid.add_filter(1, /^\d{2}$/) # age matches regex
207
222
  # grid.add_filter(:all, "ip") # any column contains "ip"
208
223
  #
209
- # @param column [Integer, Symbol]
210
- # @param query [String, Regexp]
224
+ # @param column [Integer, :all] column index (Typr::NAME-style) or +:all+
225
+ # @param query [String, Regexp] substring or pattern the cell must match
211
226
 
212
227
  def add_filter column=nil, query=nil
213
228
  return unless column and query
@@ -259,27 +274,51 @@ module Typr
259
274
  # grid.sorted_by true #=> "age"
260
275
  #
261
276
  # @param name [Boolean] pass truthy to get display name
262
- # @return [String, Integer, false, nil]
277
+ # @return [String, Integer, nil]
263
278
 
264
279
  def sorted_by name=false; (name and @sort) ? @header[@sort] : @sort end
265
280
 
266
281
  ##
267
- # Pick a row with live filtering. When the query narrows the list to a
268
- # single row it returns immediately (no Enter needed) and arms a 0.5s
269
- # overtype quiet window via +Typr.suppress_keys+, so a trailing Enter
270
- # or extra characters cannot re-trigger anything. +:none+ disables the
271
- # auto-return (used by {Browser#narrow}, where Escape must restore).
272
- #
273
- # With no +column+, passes through to {Stack#pick}.
282
+ # Pick a row with live filtering (+column:+ given) or via {Stack#pick}
283
+ # (without it, supporting all its types). In filter mode the query
284
+ # narrows the list as you type; when one row remains it returns
285
+ # immediately (no Enter needed) and arms a 0.5s overtype quiet window
286
+ # via +Typr.suppress_keys+. +:none+ disables the auto-return (used by
287
+ # {Browser#narrow}, where Escape must restore); other types behave as
288
+ # single picks here.
289
+ #
290
+ # Without +column:+, all {Stack#pick} types apply. Anything ending in
291
+ # +s+ multi-selects: hint presses toggle, Return confirms with the
292
+ # selection, Escape aborts (confirming an empty selection returns nil).
293
+ #
294
+ # Type | Picks
295
+ # -------------------|------------------------------------------------
296
+ # +:row+ | Single row id
297
+ # +:rows+ | Multi-select row ids (Return confirms)
298
+ # +:column+ | One column of the given row (real index via {#sequence})
299
+ # +:columns+ | Multi-select columns (Return confirms)
300
+ # +:field+ | One [row, column] pair, picked row-then-column
301
+ # +:fields+ | Multi-select [row, column] pairs
302
+ # +:relative_row+ | Viewport-absolute row index (0-based)
303
+ # +:none+ | Navigation only; always returns nil
274
304
  #
275
305
  # grid.pick :row, 0, column: 0 # type to narrow, auto-returns at 1 match
276
306
  # grid.pick :row, 0, column: :all # match any column
277
307
  # grid.pick :none, column: 0 # narrowing only, Enter commits
308
+ # grid.pick :column, 3 # column of the 4th visible row (page index, not id)
309
+ # grid.pick :rows # multi-select rows
278
310
  #
279
- # @param type [Symbol, String] picker type; +:none+ disables auto-return
280
- # @param row [Integer] ignored (always :row); passed to super otherwise
281
- # @param column [Integer, Symbol, String] column index/name to filter on, or :all
282
- # @return [Integer, nil] picked row id, or nil on abort
311
+ # Pressing Alt with a hint digit (ESC + digit) picks that hint row and
312
+ # sets +modifier+ to +:alt+ for the caller. The Return and Escape keys
313
+ # come from +@keymap+ (+confirm+/+exit+, Return/Escape by default).
314
+ #
315
+ # Mouse: left-click a row to pick it (row pickers only); wheel up/down
316
+ # scrolls the page; clicking outside the grid cancels like Escape.
317
+ #
318
+ # @param type [Symbol, String] picker type (tables above; symbols documented)
319
+ # @param row [Integer] unused in filter mode; page-relative row for +:column+ hints otherwise
320
+ # @param column [Integer, :all] column index (Typr::NAME-style) or +:all+
321
+ # @return [Integer, Array, nil] row id, [row, column], confirmed selection, or nil
283
322
 
284
323
  def pick type = :row, row=0, column: nil
285
324
  return super(type, row) unless column
@@ -290,6 +329,7 @@ module Typr
290
329
  @filters = [[column, '']]
291
330
  @map = base.select{ |id| check id }
292
331
  @start = 0
332
+ return @map.first if auto and @map.one?
293
333
  @bottom ||= bottom
294
334
  show
295
335
  draw_hints
@@ -378,7 +418,7 @@ module Typr
378
418
  ##
379
419
  # Execute the sort and update +@map+.
380
420
  #
381
- # @return [false, void]
421
+ # @return [false, nil]
382
422
 
383
423
  def sort
384
424
  return false unless @sort
@@ -508,6 +548,7 @@ module Typr
508
548
  data.each_with_index{ |row, id|
509
549
  self[ is_hash? ? row.shift : id + skip ] = row }
510
550
  sort
551
+ self
511
552
  end
512
553
 
513
554
  ##
@@ -521,7 +562,16 @@ module Typr
521
562
  # colors: { columns: [:cyan, nil] }
522
563
  # )
523
564
  #
524
- # @param args [Hash] +input+, +header+, +format+, +procs+, +colors+, +align+, +functions+
565
+ # @param args [Hash]
566
+ #
567
+ # Option | Value
568
+ # ----------|--------------------------------------------------------
569
+ # +input+ | Array of rows, or Hash of key to row
570
+ # +header+ | column labels (see {#header=})
571
+ # +format+ | per-column +Integer+ width, +:min+, or +:max+
572
+ # +procs+ | column index to built-in name or callable (see above)
573
+ # +colors+ | +columns:+ per-column, +fields:+ per +[row, col]+
574
+ # +align+ | per-column +:left+/+:right+ (numbers default +:right+)
525
575
 
526
576
  def initialize args={}
527
577
  @columns,@functions = 0,{}
data/lib/line.rb CHANGED
@@ -13,9 +13,10 @@ module Typr
13
13
 
14
14
  class Line < Space
15
15
  ##
16
- # Line state: +default+ text restored by {#reset}, +prompt+ character
17
- # before answers, +bindings+ the clickable keybinding labels, +data+ the
18
- # currently rendered text, +align+ (:left/:right) horizontal alignment,
16
+ # Line state: +default+ (a String, Proc, or Space) restored by {#reset},
17
+ # +prompt+ (a single char, +>?+ by default) shown before answers,
18
+ # +bindings+ the clickable keybinding labels, +data+ the currently
19
+ # rendered text, +align+ (:left/:right) horizontal alignment,
19
20
  # +trim+ (:left/:right) the side clipped on overflow.
20
21
  #
21
22
  # line.default = "status: ok"
@@ -46,7 +47,7 @@ class Line < Space
46
47
  # line.append " world" #=> "hello world"
47
48
  # line << "!" #=> "hello world!"
48
49
 
49
- def append(str); show @data+str end
50
+ def append(str); show @data+str; @data end
50
51
  alias :<< :append
51
52
 
52
53
  # The keybinding list as a clickable grid of the line's own bindings. It
@@ -109,6 +110,11 @@ class Line < Space
109
110
  # :key - a single keypress via Typr.read_key
110
111
  # :line - interactive line editor via Typr.read_line at the cursor
111
112
  #
113
+ # Every entry — including a trailing +with:+ — is simply another
114
+ # question in order (a +with:+ entry is labeled "with" when drawn), so
115
+ # +ask(pick: [grid, :row], with: :line)+ first picks, then reads
116
+ # free text. Hash order is insertion order.
117
+ #
112
118
  # Pressing the exit key (@keymap[:exit], Escape by default) aborts the
113
119
  # remaining questions and returns nil.
114
120
  #
@@ -116,6 +122,7 @@ class Line < Space
116
122
  # line.ask(keys: :key) #=> "a"
117
123
  # line.ask(open: [grid, :row, :name]) #=> 2 (picked row id)
118
124
  # line.ask(nick: [:line, { initial: "bob" }]) #=> edited string
125
+ # line.ask(pick: [grid, :row], with: :line) #=> [2, "typed text"]
119
126
  # line.ask([ ["Name?", :line], ["Ok?", :key] ]) #=> ["Alice", "a"]
120
127
 
121
128
  def ask sentence
@@ -181,6 +188,7 @@ class Line < Space
181
188
  # line.cli "> ", history: @recent, prefixes: "$>"
182
189
  #
183
190
  # @param prompt [String] rendered before the typed input
191
+ # @param initial [String] prefilled input
184
192
  # @return [String, nil] the accepted command, or nil on Escape
185
193
 
186
194
  def cli prompt, initial: '', redraw: nil, history: nil, prefixes: nil, regex: nil
data/lib/stack.rb CHANGED
@@ -95,7 +95,9 @@ module Typr
95
95
  # stack.reset # resets position and selection
96
96
  # stack.reset :position # only scrolls back to top
97
97
  # stack.reset :selection # clears all selected rows/columns/fields
98
- # stack.reset [:display, :format]
98
+ # stack.reset [:position, :selection]
99
+ #
100
+ # @param type [:all, :position, :selection, Array<Symbol>] anything else is ignored
99
101
 
100
102
  def reset type=:all
101
103
  case type
@@ -147,6 +149,7 @@ module Typr
147
149
  mode :reset
148
150
  end
149
151
  list = page.to_a
152
+ @hints_start = 0 if @hints_start > [height-1, rows-1+headspace].min
150
153
  list[ height-1 ] = nil if list.length < height
151
154
  for id, relative_id in list.each_with_index
152
155
  move left,top + relative_id + headspace
@@ -165,10 +168,16 @@ module Typr
165
168
 
166
169
  ##
167
170
  # Dispatches a key from the terminal to its semantic action and clamps the current page
168
- # offset.
171
+ # offset. Unknown keys only clamp. +confirm+ dispatches to a +confirm+
172
+ # method, which only Browser defines.
173
+ #
174
+ # stack.send Typr::KEY_DOWN # scrolls down one row
169
175
  #
170
- # stack.send Typr::KEY_UP # scrolls up one row
171
- # stack.send Typr::KEY_RETURN # confirms selection
176
+ # Mouse: wheel scrolls (+:scroll+), a click that misses is +:outside+,
177
+ # a left/right press on a row returns its id.
178
+ #
179
+ # @param key [String, Typr::Mouse] key sequence or mouse event
180
+ # @return [Object] the keymap action's result; +:scroll+/:outside+/row id for mouse
172
181
 
173
182
  def send key#, map=nil
174
183
  if key.is_a?(Typr::Mouse)
@@ -244,33 +253,45 @@ module Typr
244
253
  def page_up; @start -= height; return end
245
254
 
246
255
  ##
247
- # Blocks the calling thread and lets the user interactively pick items from the current
248
- # page using hint characters or arrow keys. After selection, the user must press Return to
249
- # confirm (or Escape to abort).
256
+ # Blocks the calling thread for an interactive pick from the current
257
+ # page with hint characters or arrow keys. Single picks return the
258
+ # moment they're made; multi (+...+s+) picks toggle until Return
259
+ # confirms the selection (Escape aborts, and confirming an empty
260
+ # selection returns nil).
250
261
  #
251
262
  # The +type+ argument controls what kind of picker is shown:
252
263
  #
253
- # Symbol/String type | Purpose
254
- # -----------------------------|-------------------------------------------
255
- # +:row, +"file"+ | Pick a single row id
256
- # +"rows"+ | Multi-select rows
257
- # +:column, +"field"+ | Nested: pick a row then a column
258
- # +:fields+ | Multi-select fields
259
- # +:none+ | Navigation-only mode
260
- # anything with +relative_| | Returns the internal integer index
264
+ # Type | Picks
265
+ # -------------------|------------------------------------------------
266
+ # +:row+ | Single row id
267
+ # +:rows+ | Multi-select row ids (Return confirms)
268
+ # +:column+ | One column of the given row (Grid: real index, Text: word offset)
269
+ # +:columns+ | Multi-select columns (Return confirms)
270
+ # +:field+ | One [row, column] pair, picked row-then-column
271
+ # +:fields+ | Multi-select [row, column] pairs
272
+ # +:relative_row+ | Viewport-absolute row index (0-based)
273
+ # +:none+ | Navigation only; always returns nil
274
+ #
275
+ # +:column+, +:columns+, +:field+ (+:fields+) and +:none+ need row
276
+ # positions, so they only work on Grid and Text — a bare Stack
277
+ # supports +:row+ and +:rows+. (The +"file"+ type belongs to
278
+ # {Browser#pick}.)
261
279
  #
262
280
  # stack.pick # single row picker
263
- # stack.pick :column, 3 # pick a column in row 3
264
- # stack.pick "rows" # multi-select rows
281
+ # stack.pick :column, 3 # column of the 4th visible row (page index, not id)
282
+ # stack.pick :rows # multi-select rows
283
+ # stack.pick :relative_row # 0-based index into the viewport
265
284
  #
266
285
  # Pressing Alt with a hint digit (ESC + digit) picks that hint row and
267
- # sets +modifier+ to +:alt+ for the caller.
286
+ # sets +modifier+ to +:alt+ for the caller. The Return and Escape keys
287
+ # come from +@keymap+ (+confirm+/+exit+, Return/Escape by default).
268
288
  #
269
- # Mouse: left-click a row to pick it; wheel up/down scrolls the page.
289
+ # Mouse: left-click a row to pick it (row pickers only); wheel up/down
290
+ # scrolls the page; clicking outside cancels like Escape.
270
291
  #
271
- # @param type [Symbol, String] picker type (table above)
272
- # @param row [Integer] starting row for column/field hints
273
- # @return [Integer, Array, nil] picked id(s), selection on confirm, or nil on abort
292
+ # @param type [Symbol, String] picker type (table above; symbols documented)
293
+ # @param row [Integer] page-relative row for +:column+ hints (ignored otherwise)
294
+ # @return [Integer, Array, nil] row id, [row, column], confirmed selection, or nil
274
295
 
275
296
  def pick type = :row, row=0 #, key=nil
276
297
  type = type.to_s
@@ -360,11 +381,15 @@ module Typr
360
381
  #
361
382
  # Option | Default | Description
362
383
  # ----------------|---------|-------------------------------------------
363
- # +keysyms+ | {} | Custom key-to-method mappings
364
- # +hints+ | +"1234..."+ | Characters used in the hint overlay
384
+ # +keymap+ | {} | Custom name-to-key mappings (override navigation defaults)
385
+ # +hints+ | +"1234567890"+ | Characters used in the hint overlay
365
386
  # +alternate+ | true | Striped row backgrounds
366
387
  # +header+ | nil | Label shown in a header bar
367
388
  # +separator+ | +" "+ | String drawn between columns (used by Grid)
389
+ # +colors+ | {} | Per-part colors (+hints+, +header+, +selected+, +alternate+, +rows+)
390
+ # +selected+ | {} | Pre-picked ids per type (+rows+, +columns+, +fields+)
391
+ #
392
+ # stack = Typr::Stack.new(keymap: { exit: "q" }) # quit on q instead
368
393
 
369
394
  def initialize args={}
370
395
  @keymap, @separator, @selected = {}, " ", {}
data/lib/terminal.rb CHANGED
@@ -89,6 +89,7 @@ module Typr
89
89
  # m.left? #=> true
90
90
  # m.alt? #=> true (with the SGR alt bit set)
91
91
  # m.to_s #=> "press button=0 x=10 y=5"
92
+ # +action+ is +:press+, +:release+, or +:motion+ (move with a button held).
92
93
  Mouse = Struct.new(:button, :x, :y, :action, :modifiers) do
93
94
  # True on button press (the moment to pick a row).
94
95
  def press?; action == :press end
@@ -146,7 +147,7 @@ module Typr
146
147
 
147
148
  def prepare str, max, align=:left, side=:right
148
149
  str = clip str, max, side if real_size(str) >= max
149
- pad = " " * (max - real_size(str))
150
+ pad = " " * [max - real_size(str), 0].max
150
151
  align == :right ? pad + str : str + pad
151
152
  end
152
153
 
@@ -184,14 +185,14 @@ module Typr
184
185
  used += size
185
186
  elsif used < max
186
187
  need = max - used
187
- chars = token.chars
188
- chars.reverse! if side == :left
188
+ clusters = token.grapheme_clusters
189
+ clusters.reverse! if side == :left
189
190
  width = 0
190
191
  out = ""
191
- chars.each do |char|
192
- width += Unicode::DisplayWidth.of char
192
+ clusters.each do |cluster|
193
+ width += Unicode::DisplayWidth.of cluster
193
194
  break if width > need
194
- out << char
195
+ out << cluster
195
196
  end
196
197
  out.reverse! if side == :left
197
198
  kept << out
@@ -600,6 +601,9 @@ module Typr
600
601
  # until a full window passes with none — each arrival restarts it.
601
602
  # No-op unless armed by {Typr.suppress_keys}. Best-effort: never raises.
602
603
  #
604
+ # Typr.suppress_keys # arm a 0.5s window
605
+ # Typr.send(:drain_quiet) # wait it out, swallowing arrivals
606
+ #
603
607
  # @return [nil]
604
608
 
605
609
  def self.drain_quiet
@@ -635,6 +639,7 @@ module Typr
635
639
  # of candidates; TAB then replaces the word through a pick grid. Each
636
640
  # candidate is either a plain String or [text, color-pair] to tint the
637
641
  # pick entry. The callable's content is arbitrary - shells, tags, ...
642
+ # It takes the text before the cursor and returns an Array of candidates.
638
643
  #
639
644
  # +regex+ is a Hash of Regexp => color (see {.paint}) applied to the
640
645
  # query as it is rendered; matched regions restore the prompt's trailing
@@ -649,6 +654,17 @@ module Typr
649
654
  # end
650
655
  # Typr.read_line "> ", initial: "ls ", history: @recent #=> "ls -l"
651
656
 
657
+ # @param prompt [String] text rendered before the query (keep a color code trailing)
658
+ # @param left [Integer] screen column (0-based)
659
+ # @param top [Integer] screen row (0-based)
660
+ # @param initial [String] prefilled query
661
+ # @param completions [#call, nil] +->(prefix) { candidates }+ or nil
662
+ # @param redraw [#call, nil] zero-arg repaint after a completion pick
663
+ # @param regex [Hash, Symbol, nil] paint map or scheme (see {.paint})
664
+ # @param history [Array<String>, nil] previous queries for Up/Down
665
+ # @param block [Proc] +->(key, query, cursor)+ inspector (non-nil short-circuits)
666
+ # @return [Object, nil] the query, a block short-circuit, or nil on Escape
667
+
652
668
  def self.read_line prompt='', left: 0, top: 0, initial: '', completions: nil, redraw: nil, regex: nil, history: nil, &block
653
669
  return $stdin.gets&.chomp unless $stdin.tty?
654
670
  query, cursor = initial.dup, initial.length
data/lib/text.rb CHANGED
@@ -4,7 +4,9 @@ require_relative 'stack.rb'
4
4
  module Typr
5
5
 
6
6
  # A vertically scrollable text viewer with word-wrapping, header bar,
7
- # row selection via hint characters, and pager-style search (/ n p).
7
+ # row selection via hint characters (+pick :row+), word selection via
8
+ # +pick :column+ (hints sit on word boundaries), and pager-style
9
+ # search (/ n p).
8
10
  #
9
11
  # Accepts String, IO, or StringIO input. Content is split into display
10
12
  # rows by newlines and line width, then rendered as a sliding window.
@@ -37,6 +39,9 @@ module Typr
37
39
  # text.build("one\ntwo three") # => #<Typr::Text ...>
38
40
  # text << "appended line" # alias for build
39
41
 
42
+ # @param input [String, IO, nil] text (or stream) to append; nil reflows
43
+ # @return [Text] self
44
+
40
45
  def build input=nil
41
46
  ( input, @data, @lines = @data, "", [0] ) if rebuild = !input
42
47
  return self if rebuild && input == ""
@@ -82,7 +87,7 @@ module Typr
82
87
  else
83
88
  @tail = ""
84
89
  end
85
- @lines << @data.length if wrap < line.length || line.include?("\n")
90
+ @lines << @data.length if line.include?("\n") || pos < str.size || !@tail.empty?
86
91
  end
87
92
  until @tail.empty?
88
93
  wrap_idx = @tail.index("\n") || @tail.rindex(" ") || @tail.length
@@ -97,6 +102,7 @@ module Typr
97
102
  end
98
103
  @lines << @data.length unless @lines.last == @data.length
99
104
  highlight if @re
105
+ self
100
106
  end
101
107
  alias :<< :build
102
108
 
@@ -137,8 +143,7 @@ module Typr
137
143
  def show
138
144
  (build; @lastwidth = width) if width != @lastwidth
139
145
  range = @lines[@start..@start+height]
140
- page = StringIO.new @data[range.first..range.last]
141
- @page=range[1..-1].map{|pos| page.read(pos-range.first-page.pos).rstrip}
146
+ @page = range.each_cons(2).map{ |a, b| @data[a...b].rstrip }
142
147
  super
143
148
  end
144
149
 
@@ -229,6 +234,12 @@ module Typr
229
234
  #
230
235
  # text = Typr::Text.new(input: "hello", header: "Title")
231
236
  # text = Typr::Text.new(input: $stdin, tabspace: 4)
237
+ #
238
+ # Option | Value
239
+ # -----------|------------------------------------------------
240
+ # +input+ | String, IO, or StringIO (nil reflows from empty)
241
+ # +tabspace+ | tabstop width in spaces (default 2)
242
+ # +colors+ | +regex:+ paint map or scheme name (see {Typr.paint})
232
243
 
233
244
  def initialize args={}
234
245
  @tabspace = 2
data/lib/typr.rb CHANGED
@@ -37,7 +37,7 @@
37
37
  # grid = Typr::Grid.new(
38
38
  # input: [['Alice', 30], ['Bob', 25]],
39
39
  # header: ['Name', 'Age'],
40
- # format: [:max, :right]
40
+ # format: [:max, :min]
41
41
  # )
42
42
  # grid.show
43
43
  # key = Typr.read_key
data/typr.gemspec CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "typr"
5
- s.version = "1.9.0"
6
- s.date = "2026-10-12"
5
+ s.version = "1.9.3"
6
+ s.date = "2026-10-19"
7
7
  s.homepage = "https://codeberg.org/typr/typr"
8
8
  s.licenses = ["MIT"]
9
9
  s.require_paths = ["lib"]
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typr
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kilian Reitmayr
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-12 00:00:00.000000000 Z
10
+ date: 2026-10-19 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: unicode-display_width