typr 1.8.0 → 1.9.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: 02fb4a45192b9a6bc2b3a751033a71e058b2abcb33b77d8873c991a2d6031dd1
4
- data.tar.gz: 878f286e0c40eb45b0b5c5216633bc0a9ff82c7f6bc03d18531a80090936e2af
3
+ metadata.gz: 74b0a2cd082b9a2ee58f9fb64356a7ed4f5a970c0427768acae334ae3325748a
4
+ data.tar.gz: af90bbcfff025ba60f1d8de7cae5171152f8939a00a7fac5975cc95785a28547
5
5
  SHA512:
6
- metadata.gz: 921906f7edae965d3ea48ed62e8eae91a770ad63dcf7cc3f20bb8463c9bbef280bcca2d2577317e4dd6c71d48b3740630bfaedf2d8c617cc9878d01381eebb9e
7
- data.tar.gz: d0db793255d8969a1181193dee6b06227efc2c0248fff99ca6407f6d5944ece37dc330e8cdabafa26229e98a299a576ebc245ea4e4ebd5a25661b20526d094cc
6
+ metadata.gz: c29e087834938ef34608086cb90ec221760ca50f0be054e7d9602f46cc6e63c73fbe06a960c8b55e551b17159f404d796f275e3a11cddcb3f70baccefc67636b
7
+ data.tar.gz: a33662ba4009f34cef2e91b5295a6fc1e3f2b17ebd9edf118052caa016e6dc0d8dd4d6f2c3fb468244f2e488e589c1ec79236f2a7001bc4e6622064fcf9a3365
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [vx.y.z] — __DATE__
4
4
 
5
+ ## [v1.9.0] — 2026-10-12
6
+
7
+ ### Changed
8
+ - RI documentation pass — every class, method, attribute, and constant now carries a doc comment with behavior, `@param`/`@return` contracts, and runnable examples: RDoc reports 222/222 parts documented (previously 38 undocumented). Grouped comments were split so RDoc attaches them to each sibling attribute/method, and the generated `Space` edge accessors are documented through their `attr_writer` entry
9
+ - `Typr#prepare` slimmed to pad-or-delegate — the 13-line manual width scan duplicated `real_size` and `clip`, so it now clips overflow via `clip` (the single truncation engine) and pads the result to exact width; the uncalled and broken `fade` flag (standalone `fade` is unchanged) is dropped
10
+ - Read-line internals made private — `Typr.slice_width` and `word_prev`/`word_next` serve only `read_line`, so they are `private_class_method` now; their doc blocks stay for RI
11
+ - 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
+
13
+ ### Fixed
14
+ - 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)
15
+ - 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
+
5
17
  ## [v1.8.0] — 2026-10-11
6
18
 
7
19
  ### Changed
data/lib/browser.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'line.rb'
5
5
 
6
6
  module Typr
7
7
 
8
+ # Bundled extension => MIME-type database (share/mimetypes).
8
9
  MIMETYPES = eval File.read( __dir__ + "/../share/mimetypes" )
9
10
 
10
11
  # A file-system directory browser built on Grid. Renders entries as rows
@@ -33,14 +34,39 @@ module Typr
33
34
  # Escape | quit, returns nil
34
35
 
35
36
  class Browser < Grid
37
+ ##
38
+ # Browser state: +directory+ the current path, +user+ the login name
39
+ # shown in the info bar, +pwd+ alias of +directory+, +view+ the active
40
+ # column view name.
41
+ #
42
+ # browser.directory #=> "/tmp/"
43
+ # browser.view #=> :compact
44
+
36
45
  attr_accessor :directory, :user, :pwd, :view
46
+ ##
47
+ # +mimetype_db+ toggles the bundled extension database, +mimetype_magic+
48
+ # the +file --mime-type+ fallback for unknown extensions.
49
+ #
50
+ # browser.mimetype_db = false # skip extension lookups
51
+
37
52
  attr_accessor :mimetype_db, :mimetype_magic
38
53
 
39
- ## Height minus 2 rows reserved for path bar and command line.
54
+ ##
55
+ # Bottom edge pulled in one row for the command line (the path bar is
56
+ # reserved by {#top}).
57
+ #
58
+ # browser.bottom #=> terminal_bottom - 1
59
+ #
60
+ # @return [Integer]
40
61
 
41
62
  def bottom; super - 1 end
42
63
 
43
- ## Top offset plus one row for the path info bar.
64
+ ##
65
+ # Top edge pushed down one row for the path info bar.
66
+ #
67
+ # browser.top #=> constructor top + 1
68
+ #
69
+ # @return [Integer]
44
70
 
45
71
  def top; super + 1 end
46
72
 
@@ -53,6 +79,11 @@ module Typr
53
79
  # browser.cd "~" # => changes to home
54
80
  # browser.cd ".." # => parent directory
55
81
  # browser.cd 2 # => jump to history entry 2
82
+ #
83
+ # @param dir [String, Integer] target path or history index
84
+ # @param append [Boolean] scan without clearing (used by history)
85
+ # @param trim [Integer] path prefix trim for the +path+ column
86
+ # @return [false, nil] false when the directory cannot be opened
56
87
 
57
88
  def cd dir, append=false, trim=0
58
89
  return unless dir
@@ -176,10 +207,15 @@ module Typr
176
207
  end
177
208
 
178
209
  ##
179
- # Resolves hint characters typed on a row. Returns the matching row id
180
- # for fast navigation, otherwise falls through to Stack#send.
210
+ # Resolve hint characters typed on a row to a row id; a click on the
211
+ # info bar or command line dispatches that binding instead. Falls
212
+ # through to {Stack#send} for keys and navigation.
213
+ #
214
+ # browser.send "1" # => row id if hint "1" is visible
215
+ # browser.send ?s # => runs the sort command (a bound key)
181
216
  #
182
- # browser.send "1" # => row id if hint "1" is visible
217
+ # @param key [Typr::Mouse, String] mouse event, hint char, or key sequence
218
+ # @return [Integer, String, Symbol, nil] picked row id, bound command result, or super's response
183
219
 
184
220
  def send key
185
221
  if key.is_a?(Typr::Mouse) and key.press? and key.left?
@@ -200,19 +236,28 @@ module Typr
200
236
  else super end
201
237
  end
202
238
 
203
- ## Returns the current directory path as the confirm result.
239
+ ##
240
+ # Returns the current directory path as the confirm result.
241
+ #
242
+ # browser.confirm #=> "/tmp/"
243
+ #
244
+ # @return [String]
204
245
 
205
246
  def confirm; return @directory end
206
247
 
207
248
  ## Prompts to select rows by index. Returns chosen ids or nil.
208
249
  #
209
250
  # browser.mark # => [0, 2, 5] or nil
251
+ #
252
+ # @return [Array<Integer>, nil]
210
253
 
211
254
  def mark; return @user.ask( 'mark files': [self, :rows] ) end
212
255
 
213
256
  ## Toggles selection for every row within a "from..to" range.
214
257
  #
215
258
  # browser.mark_range # prompts for from/to, toggles selection
259
+ #
260
+ # @return [Array<Integer>] the toggled ids
216
261
 
217
262
  def mark_range; return ids[ ( eval @user.ask(
218
263
  'mark from': [self, :relative_row], 'to': [self, :relative_row]
@@ -234,24 +279,32 @@ module Typr
234
279
  ## Navigate up one directory.
235
280
  #
236
281
  # browser.back # equivalent to cd ".."
282
+ #
283
+ # @return [void]
237
284
 
238
285
  def back; cd ".." end
239
286
 
240
287
  ## Navigate to the root directory.
241
288
  #
242
289
  # browser.root # => cd "/"
290
+ #
291
+ # @return [void]
243
292
 
244
293
  def root; cd ?/ end
245
294
 
246
295
  ## Navigate to the home directory.
247
296
  #
248
297
  # browser.home # => cd "~"
298
+ #
299
+ # @return [void]
249
300
 
250
301
  def home; cd ?~ end
251
302
 
252
303
  ## Toggle visibility of hidden files (those starting with a dot).
253
304
  #
254
- # browser.toggle_hidden
305
+ # browser.toggle_hidden # rescans the directory
306
+ #
307
+ # @return [void]
255
308
 
256
309
  def toggle_hidden
257
310
  @show_hidden = !@show_hidden
@@ -259,6 +312,10 @@ module Typr
259
312
  end
260
313
 
261
314
  ## Display a help grid listing all keybindings.
315
+ #
316
+ # browser.help # => picked binding row id or nil
317
+ #
318
+ # @return [Integer, nil]
262
319
 
263
320
  def help; @user.help end
264
321
 
@@ -266,35 +323,46 @@ module Typr
266
323
  # Reset display, selection, and position state then re-sort.
267
324
  #
268
325
  # browser.reset_view
326
+ #
327
+ # @return [void]
269
328
 
270
329
  def reset_view; reset [:display, :selection]; cd @directory; sort end
271
330
 
272
331
  ## Open a filter popup on the chosen row/column and apply the input.
273
332
  #
274
333
  # browser.filter_view # prompts for filter text
334
+ #
335
+ # @return [void]
275
336
 
276
337
  def filter_view; add_filter *@user.ask(
277
338
  filter: [ popup( type: :"[f]ilter" ), :row, NAME, { column: 0 }], with: :line ) end
278
339
 
279
- ## Open live `/` search filtered on the name column.
340
+ ## Open live `/` narrowing filtered on the name column.
280
341
  #
281
- # browser.search # type to narrow, Escape restores, Enter commits
342
+ # browser.narrow # type to narrow, Escape restores, Enter commits
343
+ #
344
+ # @return [void]
282
345
 
283
- def search
346
+ def narrow
284
347
  @suppress_user = true
285
348
  pick :none, column: NAME
286
349
  @suppress_user = false
287
350
  end
351
+ alias search narrow
288
352
 
289
353
  ## Open a sort-picker popup to choose a column to sort by.
290
354
  #
291
355
  # browser.sort_view # pick a column header to sort
356
+ #
357
+ # @return [void]
292
358
 
293
359
  def sort_view; sort_by @user.ask( "sort by": [ popup( type: :"[s]ort" ), :row, nil, { column: 0 }] ) end
294
360
 
295
361
  ## Open a view-picker popup and switch to the selected view.
296
362
  #
297
363
  # browser.views # choose :simple, :compact, :stats, or :full
364
+ #
365
+ # @return [void]
298
366
 
299
367
  def views; change_view( popup( type: :"[v]iews", input: @views.keys ).pick( :row, column: 0 ) ) end
300
368
 
@@ -551,13 +619,13 @@ full: %i[ name size permissions executable printable owner group
551
619
  @user ||= Line.new( parent:self, left:->{@parent.left}, top:->{@parent.bottom+1},
552
620
  bindings: bindings_labels )
553
621
  else
554
- cmds = { "[h]elp" => :help, "[/]:search" => :search,
622
+ cmds = { "[h]elp" => :help, "[/]:narrow" => :narrow,
555
623
  "[f]ilter" => :filter_view, "[s]ort" => :sort_view, "[v]iews" => :views,
556
624
  "[r]eset" => :reset_view, "[m]ark" => :mark,
557
625
  "[M]ark_range" => :mark_range, "mark_[a]ll" => :mark_all,
558
626
  "[i]nvert_mark" => :mark_invert, "[~]:home" => :home,
559
627
  "[H]idden_files" => :toggle_hidden }
560
- plain = ["[h]elp", "[/]:search"] +
628
+ plain = ["[h]elp", "[/]:narrow"] +
561
629
  @colors[:actions].map { |name, _| "[" + name.to_s[0] + "]" + name.to_s[1..] } +
562
630
  %w[ [r]eset [m]ark [M]ark_range mark_[a]ll
563
631
  [i]nvert_mark [~]:home [H]idden_files [back]:.. [up] [down] [pageup] [pagedown] [home] [end] [esc]:quit [return]:confirm\ ]
data/lib/grid.rb CHANGED
@@ -9,16 +9,37 @@ module Typr
9
9
  # input: [ ["Alice", 30], ["Bob", 25] ],
10
10
  # header: [:name, :age],
11
11
  # format: [:max, :right]
12
- # )
13
- # grid.show
14
- #
12
+ # )
13
+ # grid.show
14
+ #
15
15
  # Column format tokens: +Integer+ (fixed width), +:min+ (auto), +:max+ (fill remaining).
16
16
  #
17
17
  # Built-in procs: +:filetree+, +:datetime+, +:magnitudes+, +:convert+.
18
18
 
19
19
  class Grid < Stack # Table
20
+ ##
21
+ # Display and transform state: +format+ per-column width tokens,
22
+ # +procs+ per-column processors, +filters+ the active [column, query]
23
+ # pairs, +data+ the rows (Array or Hash), and +map+ the visible row
24
+ # ids after filtering/sorting.
25
+ #
26
+ # grid.filters #=> [[0, "Ali"]]
27
+ # grid.map #=> [0, 2]
28
+
20
29
  attr_accessor :format, :procs, :filters, :data, :map
30
+ ##
31
+ # +align+ is per-column alignment, +rawsort+/+rawfilter+ flag columns
32
+ # to bypass their +procs+, and +reverse+ flips the sort direction.
33
+ #
34
+ # grid.reverse = true
35
+ # grid.rawsort[0] = true # sort raw values for column 0
36
+
21
37
  attr_accessor :align, :rawsort, :rawfilter, :reverse
38
+ ##
39
+ # Overrides the column render order (see {#sequence}).
40
+ #
41
+ # grid.sequence = [2, 0, 1]
42
+
22
43
  attr_writer :sequence
23
44
 
24
45
  ##
@@ -52,7 +73,8 @@ module Typr
52
73
  ##
53
74
  # Column render order. Defaults to 0..n-1; override with +sequence=+.
54
75
  #
55
- # grid.sequence #=> [0, 1, 2]
76
+ # grid.sequence #=> [0, 1, 2]
77
+ # grid.sequence = [2, 0, 1]
56
78
  #
57
79
  # @return [Array<Integer>]
58
80
 
@@ -115,12 +137,14 @@ module Typr
115
137
  end
116
138
 
117
139
  ##
118
- # Set column headers. Symbols are auto-defined as +UPCASE_I+ constants.
140
+ # Set column headers. Symbols are auto-defined as +UPCASE+ constants
141
+ # holding the column index.
119
142
  #
120
- # grid.header = [:name, :age]
121
- # # defines NAME_0 = "name", AGE_1 = "age"
143
+ # grid.header = ["name", "age"]
144
+ # Typr::NAME #=> 0
145
+ # Typr::AGE #=> 1
122
146
  #
123
- # @param h [Array<String, Symbol>]
147
+ # @param h [Array<String, Symbol>] header labels
124
148
 
125
149
  def header=( h ); super
126
150
  h.each_with_index{ |h,i|
@@ -240,18 +264,22 @@ module Typr
240
264
  def sorted_by name=false; (name and @sort) ? @header[@sort] : @sort end
241
265
 
242
266
  ##
243
- # Pick a row with live-filtering.
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).
244
272
  #
245
- # When column is given, `Grid` overrides `Stack#pick` to add interactive
246
- # text filtering of rows by the given column. Filter text is shown at the
247
- # bottom of the grid. Passes through to `super` for all other types or
248
- # when no column is given.
273
+ # With no +column+, passes through to {Stack#pick}.
249
274
  #
250
- # grid.pick :row, 0, column: :name
251
- # grid.pick :row, 0, column: :all # match any column
275
+ # grid.pick :row, 0, column: 0 # type to narrow, auto-returns at 1 match
276
+ # grid.pick :row, 0, column: :all # match any column
277
+ # grid.pick :none, column: 0 # narrowing only, Enter commits
252
278
  #
253
- # @param type [Symbol] type is ignored (always :row); passed to super otherwise
254
- # @param column [Symbol, String] column name to filter on, or :all (default nil)
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
255
283
 
256
284
  def pick type = :row, row=0, column: nil
257
285
  return super(type, row) unless column
@@ -308,12 +336,15 @@ module Typr
308
336
  end
309
337
 
310
338
  ##
311
- # Sort by column (toggle direction on repeat).
339
+ # Dispatch a mouse press on the header row to a column sort (clicking
340
+ # the header cell for a column calls +sort_by+); everything else falls
341
+ # through to {Stack#send}.
312
342
  #
313
- # grid.sort_by :age
314
- # grid.sort_by :age # reverses
343
+ # grid.send Typr::Mouse.new(0, 12, grid.top + 1, :press, 0) #=> :scroll
344
+ # grid.send Typr::KEY_DOWN # scrolls
315
345
  #
316
- # @param column [Integer, Symbol]
346
+ # @param key [Typr::Mouse, String] mouse event or key sequence
347
+ # @return [Symbol, Object] +:scroll+ on a header click, super's response otherwise
317
348
 
318
349
  def send key
319
350
  if key.is_a?(Typr::Mouse) and key.press? and key.left? and
@@ -328,6 +359,15 @@ module Typr
328
359
  super
329
360
  end
330
361
 
362
+ ##
363
+ # Sort by column (toggle direction on repeat).
364
+ #
365
+ # grid.sort_by :age
366
+ # grid.sort_by :age # reverses
367
+ #
368
+ # @param column [Integer, Symbol] column index or header-name symbol
369
+ # @return [void]
370
+
331
371
  def sort_by column
332
372
  column = @header.index( column.to_s ) if column.is_a? Symbol
333
373
  if @sort and (column == @sort) then @reverse = !@reverse
data/lib/line.rb CHANGED
@@ -12,6 +12,16 @@ module Typr
12
12
  # line.ask([ ["Name?", :line], ["Age?", :line] ])
13
13
 
14
14
  class Line < Space
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,
19
+ # +trim+ (:left/:right) the side clipped on overflow.
20
+ #
21
+ # line.default = "status: ok"
22
+ # line.prompt = "?"
23
+ # line.align = :left
24
+
15
25
  attr_accessor :default, :prompt, :bindings, :data, :align, :trim
16
26
 
17
27
  # Render +msg+ (String, Proc, or Space) on this line.
@@ -92,7 +102,10 @@ class Line < Space
92
102
  # Array - interactive pick via <widget>.pick(<type>); an optional third
93
103
  # element renders the chosen row's field into the line, and an
94
104
  # optional fourth element is passed as pick keyword arguments
95
- # (e.g. { column: 0 } to open the pick in / search mode)
105
+ # (e.g. { column: 0 } to open the pick in / search mode).
106
+ # Special case: [:line, { initial: "text" }] opens the inline
107
+ # editor via Typr.read_line prefilled with +initial+ instead
108
+ # of a widget pick.
96
109
  # :key - a single keypress via Typr.read_key
97
110
  # :line - interactive line editor via Typr.read_line at the cursor
98
111
  #
@@ -102,6 +115,7 @@ class Line < Space
102
115
  # line.ask(name: :line) #=> "Alice"
103
116
  # line.ask(keys: :key) #=> "a"
104
117
  # line.ask(open: [grid, :row, :name]) #=> 2 (picked row id)
118
+ # line.ask(nick: [:line, { initial: "bob" }]) #=> edited string
105
119
  # line.ask([ ["Name?", :line], ["Ok?", :key] ]) #=> ["Alice", "a"]
106
120
 
107
121
  def ask sentence
@@ -149,15 +163,25 @@ class Line < Space
149
163
  return ( answer.one? ? answer.first : answer )
150
164
  end
151
165
 
152
- # Prompt for a single command line at the cursor: +prompt+ rendered in the
153
- # line's default color, typed input in the answer color. When +completions+
154
- # is truthy, TAB matches the current word against the shell (commands,
155
- # options, files) and replaces it through the pick grid. Regions of the
156
- # typed input matching @colors[:regex] are colored as they are typed
157
- # (with the :shell scheme: the first word red, double-dash options blue).
166
+ ##
167
+ # Prompt for a single command line at the cursor: +prompt+ rendered in
168
+ # the line's default color, typed input in the answer color. TAB
169
+ # matches the current word against the shell (commands, options, files)
170
+ # and replaces it through the pick grid. Regions of the typed input
171
+ # matching @colors[:regex] are colored as they are typed (with the
172
+ # :shell scheme: the first word red, double-dash options blue).
173
+ #
174
+ # +history+ is an Array whose entries Up/Down walk; +prefixes+ lists
175
+ # command-start characters stripped before completion matching;
176
+ # +redraw+ repaints the caller's screen after a completion pick;
177
+ # +regex+ overrides @colors[:regex] for this prompt.
158
178
  #
159
179
  # line.cli "> ", initial: "cp %name"
160
180
  # #=> "cp source.txt" (or nil on Escape)
181
+ # line.cli "> ", history: @recent, prefixes: "$>"
182
+ #
183
+ # @param prompt [String] rendered before the typed input
184
+ # @return [String, nil] the accepted command, or nil on Escape
161
185
 
162
186
  def cli prompt, initial: '', redraw: nil, history: nil, prefixes: nil, regex: nil
163
187
  re = /\A[#{Regexp.escape(prefixes || '|+=!')}]/
@@ -197,6 +221,16 @@ class Line < Space
197
221
 
198
222
  public
199
223
 
224
+ ##
225
+ # Construct a Line. Position via the {Space} boundaries; +default+ sets
226
+ # the text shown after {#reset}, +bindings+ the keybinding labels.
227
+ #
228
+ # line = Typr::Line.new(top: -1, left: 0,
229
+ # default: "(f)ilter, (s)ort, [esc] quit")
230
+ #
231
+ # @param args [Hash] boundary and display options (see {Space#initialize})
232
+ # @return [Line]
233
+
200
234
  def initialize args
201
235
  @right= -1
202
236
  @align = :left
data/lib/space.rb CHANGED
@@ -16,7 +16,21 @@ class Space
16
16
  include Typr
17
17
 
18
18
  %w[left top right bottom].each{ |name| method = <<STR
19
- def %s_setting; @%s; end
19
+ ##
20
+ # Raw +%s+ value as passed to the constructor, before resolution
21
+ # (nil, Integer, Float, or Proc). Useful for introspection.
22
+ #
23
+ # space.%s_setting #=> 0
24
+ #
25
+ def %s_setting; @%s; end
26
+ ##
27
+ # Resolved +%s+ edge: Integer (absolute position), Float (fraction of
28
+ # the terminal), Proc (evaluated each layout pass), or a negative
29
+ # Integer (offset from the opposite edge, -1 = last). Defaults to the
30
+ # widget's natural extent when unset.
31
+ #
32
+ # space.%s #=> 4
33
+ #
20
34
  def %s
21
35
  max=Typr.#{ name[/left|right/] ? 'width' : 'height' }
22
36
  if !@%s;[max, #{ case name
@@ -30,9 +44,18 @@ else 'top+[(respond_to?(:rows) ? rows : 0), 1].max-1+(respond_to?(:headspace) ?
30
44
  else @%s end
31
45
  end
32
46
  STR
33
- eval( method % ([name]*11) ) }
47
+ eval( method % ([name]*15) ) }
48
+
49
+ ##
50
+ # Deep-convert String keys to Symbols so string-keyed config (e.g. YAML)
51
+ # is honored. Non-Hash values pass through untouched.
52
+ #
53
+ # space.symbolize "colors" => { "border" => :red }
54
+ # #=> {colors: {border: :red}}
55
+ #
56
+ # @param obj [Object] hash or value to convert
57
+ # @return [Object] converted copy, or the value itself when not a Hash
34
58
 
35
- # Deep-convert String keys to Symbols so string-keyed config (e.g. YAML) is honored.
36
59
  def symbolize obj
37
60
  case obj
38
61
  when Hash
@@ -41,12 +64,44 @@ eval( method % ([name]*11) ) }
41
64
  else obj end
42
65
  end
43
66
 
67
+ ##
68
+ # Viewport edges, readable and assignable. Each accepts an Integer
69
+ # (absolute position), a Float (fraction of the terminal), a Proc
70
+ # (evaluated each layout pass), or a negative Integer (offset from the
71
+ # opposite edge, -1 = last row/column); unset edges fall back to the
72
+ # widget's natural extent. +left_setting+ and friends return the raw
73
+ # constructor value before resolution.
74
+ #
75
+ # space.left = 0
76
+ # space.right = 0.5
77
+ # space.bottom = -1
78
+ # space.right_setting #=> 0.5
44
79
  attr_writer :left, :top, :right, :bottom
80
+ ##
81
+ # +margin+ is the left-edge padding string, +colors+ the per-part color
82
+ # hash, +interval+ the redraw period in seconds for {#start}, and
83
+ # +borders+ the raw edge characters (prefer {#border=}).
84
+ #
85
+ # space.margin = " "
86
+ # space.interval = 2
45
87
  attr_accessor :margin, :colors, :interval, :borders
46
88
 
89
+ ##
47
90
  # Viewport width in cells (right - left + 1).
91
+ #
92
+ # space.width #=> 80
93
+ #
94
+ # @return [Integer]
95
+
48
96
  def width; right - left + 1 end
97
+
98
+ ##
49
99
  # Viewport height in cells (bottom - top + 1).
100
+ #
101
+ # space.height #=> 24
102
+ #
103
+ # @return [Integer]
104
+
50
105
  def height; bottom - top + 1 end
51
106
 
52
107
  ##
@@ -60,7 +115,7 @@ eval( method % ([name]*11) ) }
60
115
  def data_at x, y; nil end
61
116
 
62
117
  ##
63
- # The trigger key for a binding label like "[h]elp" or "[/]:search", or nil
118
+ # The trigger key for a binding label like "[h]elp" or "[/]:narrow", or nil
64
119
  # for named keys ("[up]", "[esc]") which stay keyboard-only.
65
120
  #
66
121
  # line.extract_key "[R]ecurse" # => "R"
@@ -70,18 +125,48 @@ eval( method % ([name]*11) ) }
70
125
  key if key and key.size == 1
71
126
  end
72
127
 
128
+ ##
73
129
  # Redraw every +interval+ seconds from a background thread.
130
+ #
131
+ # space.interval = 1
132
+ # space.start #=> #<Thread>
133
+ # space.stop # terminates it
134
+ #
135
+ # @return [Thread]
136
+
74
137
  def start; @updater = Thread.new{ loop{show; sleep @interval }} end
75
- # Stop the background refresh thread.
138
+
139
+ ##
140
+ # Stop the background refresh thread started by {#start}.
141
+ #
142
+ # space.stop #=> #<Thread> (terminated)
143
+ #
144
+ # @return [Thread]
145
+
76
146
  def stop; @updater.terminate end
77
147
 
148
+ ##
78
149
  # Draw a border char at screen (x, y); skips nil and off-screen positions.
150
+ #
151
+ # space.draw_border 0, 0, "┌" #=> nil
152
+ #
153
+ # @param x [Integer] column (0-based)
154
+ # @param y [Integer] row (0-based)
155
+ # @param char [String, nil] character to draw
156
+ # @return [nil]
157
+
79
158
  def draw_border x,y,char
80
159
  draw move_code(x, y) + char unless not char or
81
160
  x < 0 or x > Typr.width or y < 0 or y > Typr.height
82
161
  end
83
162
 
163
+ ##
84
164
  # Draw the widget's border box (no-op when no borders are set).
165
+ #
166
+ # space.show #=> nil
167
+ #
168
+ # @return [nil]
169
+
85
170
  def show
86
171
  return if @borders.empty?
87
172
  color @colors[:border]
@@ -97,8 +182,19 @@ eval( method % ([name]*11) ) }
97
182
  draw_border(right + 1, bottom + 1, @borders[:bottom_right])
98
183
  end
99
184
 
100
- # Set border style: :light/:heavy/:double/:round, a char repeated across
101
- # the box, or nil for none.
185
+ ##
186
+ # Set border style: +:light+/:heavy+/:double+/:round+, a single char
187
+ # repeated across the box, an 8-char string (top, bottom, left, right,
188
+ # top-left, top-right, bottom-left, bottom-right), or nil for none.
189
+ #
190
+ # space.border = :round
191
+ # space.border = "─" # one char for every edge
192
+ # space.border = "──││┌┐└┘" # explicit edges, same as :light
193
+ # space.border = nil # remove the border
194
+ #
195
+ # @param border [Symbol, String, nil]
196
+ # @return [void]
197
+
102
198
  def border=(border)
103
199
  chars = case border
104
200
  when Symbol
@@ -111,8 +207,17 @@ eval( method % ([name]*11) ) }
111
207
  bottom_right ].map.with_index{ |part,id| [part.to_sym, chars[id]] }.to_h if chars
112
208
  end
113
209
 
210
+ ##
114
211
  # Build a widget from boundary (left/top/right/bottom), border, margin,
115
- # colors, interval, and keymap.
212
+ # colors, interval, and keymap. Every keyword becomes an instance
213
+ # variable (+border:+ is applied through {#border=}).
214
+ #
215
+ # box = Typr::Space.new(left: 0, top: 2, right: 0.5, bottom: -4,
216
+ # border: :round, colors: { border: :grey50 }, interval: 2)
217
+ #
218
+ # @param args [Hash] boundary and visual options (see the class example)
219
+ # @return [Space]
220
+
116
221
  def initialize args={}
117
222
  @max, @keymap, @colors, @margin = 0, {}, {}, ' '
118
223
  @left, @top, @interval, @borders = 0, 0, 1, {}