typr 1.6.1 → 1.8.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: 3e9446e4b4389a720e2bda6b8e95f7a7144eb5b731b72f325b80f4bdea2866ed
4
- data.tar.gz: 2c8d5867850febf6db6cebcee4acb6fde4b4c023007af4af54124f12f927e19f
3
+ metadata.gz: 02fb4a45192b9a6bc2b3a751033a71e058b2abcb33b77d8873c991a2d6031dd1
4
+ data.tar.gz: 878f286e0c40eb45b0b5c5216633bc0a9ff82c7f6bc03d18531a80090936e2af
5
5
  SHA512:
6
- metadata.gz: 670f5b22e356fd91507a3335038d228c06d8eef5cb404c76783c6dfa37126f073ad90253b062b26aedc81ef15c9fcb95fde0074ffff3b15dac5fd670fa53045b
7
- data.tar.gz: 8dd835d95fb86ac1a56c9390ac7a485ea17d5f5e70e917db4f18d5f6491517dfc31dc56cf07a13ffffed58ae3dc92d9f1a0600ed944c7e2ca9ecc0b67389ad94
6
+ metadata.gz: 921906f7edae965d3ea48ed62e8eae91a770ad63dcf7cc3f20bb8463c9bbef280bcca2d2577317e4dd6c71d48b3740630bfaedf2d8c617cc9878d01381eebb9e
7
+ data.tar.gz: d0db793255d8969a1181193dee6b06227efc2c0248fff99ca6407f6d5944ece37dc330e8cdabafa26229e98a299a576ebc245ea4e4ebd5a25661b20526d094cc
data/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## [vx.y.z] — __DATE__
4
4
 
5
+ ## [v1.8.0] — 2026-10-11
6
+
7
+ ### Changed
8
+ - Sort indicator uses solid triangles (▲▼) instead of thin arrows, for a heavier mark at the same single-cell width
9
+
10
+ ### Fixed
11
+ - Text paints new spans once at entry and rebuilds only reflow: scheme and content colors survive resizes and appends without stacking or stripping
12
+ - Terminal self-test renders without scrolling and covers all named colors — status line uses raw output on the last row (`p` scrolled with inspect quotes and a newline), rows are capped to the visible area, the 256-ramp starts at a known position, the color list includes all `COLOR_MAP` names plus the full grey ramp, and the fg/bg steppers clamp to valid ids
13
+ - String headers fit inside minimal-width boxes — the box ignored the margin in the header width, so the border overwrote the last header character; single `:max` columns provably span the widget width
14
+ - Word jumps are documented as alt-arrow, not ctrl-arrow — README, the `read_line` doc comment, and the v1.2.0 entry called them ctrl-arrow while the bindings are `KEY_ALT_LEFT`/`KEY_ALT_RIGHT` (SGR modifier 3 = alt); the package's key modifier is alt everywhere
15
+ - Command-line redraw overwrites in place — `read_line` cleared the row on every keypress, flashing on cursor moves; each frame now pads to end-of-line with spaces instead, so shrinking content leaves no ghosts either
16
+ - Cursor stays hidden while the command line redraws — every keypress parked the visible cursor at the prompt between the home move and the edit-position move; each frame now hides it first and shows it at the edit position
17
+ - Mode table corrected to standard SGR order (`dim` restored, so `slow→5` and `invert→7`) with `:steady` at position 25 for blink-off; `mode_code` stays index-by-position
18
+
19
+ ## [v1.7.0] — 2026-10-02
20
+
21
+ ### Fixed
22
+ - Column hints in `pick :column` sit in the gap cell left of each column — `draw_hints` positioned them from the widget edge while rows render past the margin, so every hint landed one margin-width left of its column; overlaying the column starts overshot right instead
23
+ - Default bottom-bar commands work in a bare browser — the key labels were display-only, so `h / f s v r m M a i ~ H` did nothing; the default bindings now populate the keymap like explicit ones, and the pick loop discards command results instead of misreading them as picks
24
+ - `[d]irectories` left the default bar — directory history is a filecon feature; typr's unrelated `Browser#directories` row query is unchanged
25
+ - Explicit nil widget boundaries fall back to safe defaults — the unified clamp referenced the accessor itself, recursing until `SystemStackError` (and the bottom default assumed subclass-only methods); per-name fallbacks are restored inside the clamp
26
+
27
+ ### Changed
28
+ - Regex color schemes are named and opt-in — `regex:` accepts a Hash as before or a Symbol that `Typr.paint` resolves through `REGEX_SCHEMES` (`:generic` paints numbers, colon keys, quotes, brackets, and punctuation; `:shell` paints a command line's first word and `--options`); unknown symbols raise `ArgumentError`. `Text` and `Line` no longer install a default scheme, so `regex:` is empty by default and help pages, error popups, and unconfigured views render unpainted
29
+
5
30
  ## [v1.6.1] — 2026-10-02
6
31
 
7
32
  ### Fixed
@@ -89,7 +114,7 @@
89
114
  ## [v1.2.0] — 2026-08-02
90
115
 
91
116
  ### Added
92
- - `Typr.read_key` / `Typr.read_line` — the public terminal input API: `read_key` reads a raw key; `read_line` is the shared interactive line editor that renders a prompt + query at any position with arrow keys, ctrl-arrow word jumps, Home/End/Delete/Backspace, Escape aborts (`nil`), Enter confirms, and an optional block runs after each keypress
117
+ - `Typr.read_key` / `Typr.read_line` — the public terminal input API: `read_key` reads a raw key; `read_line` is the shared interactive line editor that renders a prompt + query at any position with arrow keys, alt-arrow word jumps, Home/End/Delete/Backspace, Escape aborts (`nil`), Enter confirms, and an optional block runs after each keypress
93
118
  - `Line#ask` strict question types — `Array` (pick from an object/column), `:key`, `:line`; the question and answer are rendered in `@colors[:question]`/`@colors[:answer]`
94
119
  - `Line#align` attribute (`:left`/`:right`) for text alignment in draw
95
120
  - `dark_*` named colors (`dark_red`, `dark_green`, `dark_yellow`, `dark_blue`, `dark_magenta`, `dark_cyan`) and a `COLOR_MAP` of popular named colors in `color_code`
data/README.md CHANGED
@@ -31,9 +31,9 @@ The default terminal frontend is pure ANSI on raw TTY input — **no ncurses**.
31
31
 
32
32
  ### Colors
33
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, …). A component-level `regex:` key maps `Regexp => color` to tint typed input (`Typr.read_line`/`Line#cli`) and the rows of `Typr::Text` areas; when two patterns match the same span the earlier key wins, so order overlapping patterns longest-first. Colors may be any value `color_code` accepts — named color, `greyN`, an 8-bit index, an RGB triplet like `[10, 200, 30]`, or a `[fg, bg]` pair. Header bars render bold by default.
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, …). A component-level `regex:` key maps `Regexp => color` to tint typed input (`Typr.read_line`/`Line#cli`) and the rows of `Typr::Text` areas, or names a scheme — `:generic` or `:shell`; when two patterns match the same span the earlier key wins, so order overlapping patterns longest-first. Colors may be any value `color_code` accepts — named color, `greyN`, an 8-bit index, an RGB triplet like `[10, 200, 30]`, or a `[fg, bg]` pair. Header bars render bold by default.
35
35
 
36
- `Typr::Text` paints numbers cyan, colon-separated keys yellow, quoted strings green, bracketed groups magenta, and remaining punctuation grey — override with `regex:`, disable with `regex: {}`. Each painted span restores only the `default` foreground, so the row's own background (alternating stripes, search highlight, padding) survives; `Line#cli` colors the typed command (first word red, `--options` blue); it no longer tints the completion grid. Set `regex: {}` to turn a scheme off.
36
+ Pass `regex: :generic` and `Typr::Text` paints numbers cyan, colon-separated keys yellow, quoted strings green, bracketed groups magenta, and remaining punctuation grey — override with a custom map, disable with `regex: {}` (the default: unpainted). Each painted span restores only the `default` foreground, so the row's own background (alternating stripes, search highlight, padding) survives; `Line#cli` colors the typed command with the `:shell` scheme (first word red, `--options` blue) when configured; it no longer tints the completion grid. Set `regex: {}` to turn a scheme off.
37
37
 
38
38
  ### Frontends
39
39
 
@@ -114,8 +114,7 @@ Typr.init
114
114
 
115
115
  @browser = Typr::Browser.new(
116
116
  directory: "/tmp",
117
- right: -1, bottom: -1,
118
- colors: { hints: [:white, :black] }
117
+ right: -1, bottom: -1
119
118
  )
120
119
 
121
120
  selected = @browser.pick # interactive file picker, returns the absolute path
@@ -186,7 +185,7 @@ Clears the full screen or a single line (`:line`).
186
185
  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.
187
186
 
188
187
  ### `Typr.read_line(prompt, ...)`
189
- 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. Pass `completions:` — a callable `->(query, cursor) { candidates }` — and TAB matches the current word against `candidates` in a pick grid: a candidate is either a string or `[text, color-pair]` to tint its pick entry. Pass `regex:` — a Hash of `Regexp => color` — and matching regions of the query are colored as they are typed, restoring the prompt's trailing color code. The editor is agnostic to where candidates come from.
188
+ The shared interactive line editor. Renders a prompt and query at any position with arrow keys, alt-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. Pass `completions:` — a callable `->(query, cursor) { candidates }` — and TAB matches the current word against `candidates` in a pick grid: a candidate is either a string or `[text, color-pair]` to tint its pick entry. Pass `regex:` — a Hash of `Regexp => color`, or a scheme name (`:generic`, `:shell`) — and matching regions of the query are colored as they are typed, restoring the prompt's trailing color code. The editor is agnostic to where candidates come from.
190
189
 
191
190
  ```ruby
192
191
  Typr.read_line "/", completions: ->(query, cursor) {
@@ -203,7 +202,7 @@ Strict question types:
203
202
  The question and answer are rendered in `@colors[:question]`/`@colors[:answer]`.
204
203
 
205
204
  ### `Line#cli`
206
- 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. The typed command tints with `@colors[:regex]` — default 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). `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.
207
206
 
208
207
  ```ruby
209
208
  line.cli "> ", initial: "cp %name %dir", completions: true, redraw: -> { show }
data/example/browser CHANGED
@@ -4,35 +4,40 @@
4
4
  # Usage:
5
5
  # cd example && ruby browser
6
6
  # ruby browser /path/to/dir Start in a specific directory
7
- # ruby browser --select Start in pick mode (returns path)
8
7
  #
9
8
  # Navigates directories and displays files with size and type.
10
9
  # Pass a directory path as an argument to start there.
11
- # Pass --select to enter pick mode — selected file is printed to stdout on exit.
10
+ # The picked file path is printed to stdout on exit.
12
11
  #
13
12
  # Keys:
14
- # arrows / jk Navigate up/down
15
- # enter Open directory or pick file (in select mode)
16
- # / Filter files by name
17
- # esc Exit
13
+ # arrows Scroll up/down
14
+ # pgup/pgdn Page up/down
15
+ # home/end Jump to top/bottom
16
+ # tab Cycle hint characters
17
+ # 1-9,0 Jump to the hinted row (directories change into, files pick)
18
+ # enter Open directory or pick file (returns current directory)
19
+ # h Keybinding help
20
+ # / Live-search names
21
+ # f/s/v Filter/sort/views popups
22
+ # r/m/M/a/i Reset, mark, mark range, mark all, invert marks
23
+ # ~/H Home directory, toggle hidden files
24
+ # esc Exit
18
25
  #
19
- # When run with --select, the chosen file path is printed
20
- # to stdout after the TUI exits (piped to calling process).
26
+ # Click a row to open it; click a bottom-bar label to run that command.
21
27
 
22
28
  require_relative '../lib/typr.rb'
23
29
 
24
30
  class BrowserTest
25
31
  include Typr
26
32
  def initialize
27
- Typr.init
28
- $conf=$*.map{|arg| [arg.start_with?(/\w/), arg] }.to_h
29
- @browser = Browser.new( right: -1, bottom: -1,
30
- directory: $conf[false], colors: { hints: [:white, :black] } )
33
+ Typr.init
34
+ dirs = ARGV.reject{|arg| arg.start_with?('--') }
35
+ @browser = Browser.new( right: -1, bottom: -1, directory: dirs.first )
31
36
  end
32
37
 
33
38
  def run
34
39
  Typr.clear
35
- $file = @browser.pick $conf[true]
40
+ $file = @browser.pick(:file)
36
41
  end
37
42
  end
38
43
 
@@ -40,6 +45,5 @@ begin
40
45
  BrowserTest.new.run
41
46
  ensure
42
47
  Typr.exit
43
- puts $file
44
- end
45
-
48
+ puts $file if $file
49
+ end
data/example/grid CHANGED
@@ -38,10 +38,11 @@ class GridTest
38
38
  @user = Line.new( top: -1, default:
39
39
  "(s)ort, (f)ilter, (r)eset, select (R)ows/(C)olumns/(F)ields, [esc] quit")
40
40
  @head = Line.new( interval: 1,
41
- default: proc{ draw " %s, sort:%s filter:%s selected:%s" %
41
+ default: proc{ draw " %s, sort:%s filter:%s" %
42
42
  [ Time.now.strftime("%D %T"),
43
43
  (@grid.header[@grid.sorted_by] if @grid.sorted_by),
44
- @grid.filters.join, @grid.selected ] } )
44
+ @grid.filters.join ] } )
45
+ @head.reset
45
46
  end
46
47
 
47
48
  def run
data/example/text CHANGED
@@ -26,9 +26,7 @@
26
26
  require_relative '../lib/typr.rb'
27
27
 
28
28
  class TextTest
29
- # extend Typr
30
29
  include Typr
31
- # Typr.frontend :terminal
32
30
  def initialize
33
31
  Typr.init
34
32
  @text = Text.new(
@@ -47,7 +45,7 @@ class TextTest
47
45
  loop do
48
46
  @text.show
49
47
  @user.show
50
- case key = Typr.read( :key )
48
+ case key = Typr.read_key
51
49
  when KEY_ESCAPE; exit
52
50
  when ?/; @text.search
53
51
  when ?w; @user.ask( 'pick word': [@text, :field] )
data/lib/browser.rb CHANGED
@@ -340,6 +340,7 @@ module Typr
340
340
  choice = send key
341
341
  next if choice == :scroll or choice == :outside
342
342
  else
343
+ if @command_keys.include? key then send key; next end
343
344
  next unless choice = send( key )
344
345
  end
345
346
  next unless choice
@@ -502,7 +503,7 @@ full: %i[ name size permissions executable printable owner group
502
503
  input: [ [->{Etc.getpwuid(Process.euid).name rescue ENV["USER"] || Etc.getlogin}, ->{@parent.directory},
503
504
  ->{@parent.filter_display},
504
505
  ->{ (s = @parent.sorted_by(true)) &&
505
- "#{s} #{@parent.reverse ? "\u2193" : "\u2191"}"},
506
+ "#{s} #{@parent.reverse ? "\u25BC" : "\u25B2"}"},
506
507
  ->{@parent.view} ] ], format: [:min, :max, :min, :min, :min],
507
508
  colors: { columns: [@colors[:default], @colors[:default],
508
509
  @colors[:actions][:filter], @colors[:actions][:sort],
@@ -529,6 +530,15 @@ full: %i[ name size permissions executable printable owner group
529
530
  ( pre + ( pre.end_with?( "/" ) ? "" : text[pre.length..-1][/\A[^\/]*/] ) ).sub( %r{(?<=.)/$}, "" )
530
531
  end
531
532
 
533
+ @command_keys = []
534
+ wire = ->(label, cmd) {
535
+ key = extract_key(label)
536
+ next unless key
537
+ method = cmd.is_a?(Symbol) ? cmd :
538
+ (cmd.start_with?('!') ? cmd[1..-1].to_sym : cmd.to_sym)
539
+ @keymap[method] = key
540
+ @command_keys << key if key.size == 1
541
+ }
532
542
  if args[:bindings]
533
543
  bindings_labels = []
534
544
  args[:bindings].each do |label, cmd|
@@ -536,20 +546,27 @@ full: %i[ name size permissions executable printable owner group
536
546
  colored = @colors[:actions][name] ?
537
547
  color_code( @colors[:actions][name] ) + label + color_code(@colors[:default]) : label
538
548
  bindings_labels << colored
539
- key = extract_key(label)
540
- next unless key
541
- method = cmd.start_with?('!') ? cmd[1..-1].to_sym : cmd.to_sym
542
- @keymap[method] = key
549
+ wire.(label, cmd)
543
550
  end
544
551
  @user ||= Line.new( parent:self, left:->{@parent.left}, top:->{@parent.bottom+1},
545
552
  bindings: bindings_labels )
546
553
  else
547
- @user ||= Line.new( parent:self, left:->{@parent.left},top:->{@parent.bottom+1},
548
- bindings: %w[ [h]elp [/]:search ] + @colors[:actions].map{|name,color|
549
- label = "[" + name.to_s[0] + "]" + name.to_s[1..]
550
- color_code( color ) + label + color_code(@colors[:default]) } +
551
- %w[ [r]eset [d]irectories [m]ark [M]ark_range mark_[a]ll
552
- [i]nvert_mark [~]:home [H]idden_files [back]:.. [up] [down] [pageup] [pagedown] [home] [end] [esc]:quit [return]:confirm\ ])
554
+ cmds = { "[h]elp" => :help, "[/]:search" => :search,
555
+ "[f]ilter" => :filter_view, "[s]ort" => :sort_view, "[v]iews" => :views,
556
+ "[r]eset" => :reset_view, "[m]ark" => :mark,
557
+ "[M]ark_range" => :mark_range, "mark_[a]ll" => :mark_all,
558
+ "[i]nvert_mark" => :mark_invert, "[~]:home" => :home,
559
+ "[H]idden_files" => :toggle_hidden }
560
+ plain = ["[h]elp", "[/]:search"] +
561
+ @colors[:actions].map { |name, _| "[" + name.to_s[0] + "]" + name.to_s[1..] } +
562
+ %w[ [r]eset [m]ark [M]ark_range mark_[a]ll
563
+ [i]nvert_mark [~]:home [H]idden_files [back]:.. [up] [down] [pageup] [pagedown] [home] [end] [esc]:quit [return]:confirm\ ]
564
+ plain.each { |label| wire.(label, cmds[label]) if cmds[label] }
565
+ @user ||= Line.new( parent:self, left:->{@parent.left}, top:->{@parent.bottom+1},
566
+ bindings: plain.map { |label|
567
+ name = label.gsub( /[\[\]]/, '' ).downcase.to_sym
568
+ @colors[:actions][name] ?
569
+ color_code( @colors[:actions][name] ) + label + color_code(@colors[:default]) : label } )
553
570
  end
554
571
  cd @directory
555
572
  end
data/lib/grid.rb CHANGED
@@ -411,7 +411,7 @@ module Typr
411
411
  gaps = @margin.size + @separator.size * (columns - 1)
412
412
  sum = @widths.sum
413
413
  @max = sum + gaps - 1
414
- @max = [ @max, @header.length ].max if @header.is_a?(String)
414
+ @max = [ @max, @header.length + @margin.size ].max if @header.is_a?(String)
415
415
  space = width - gaps
416
416
  if (extra = sum - space ) > 0
417
417
  extra.times{ |i| @widths[ @widths.index(@widths.max) ] -= 1 }
@@ -434,7 +434,7 @@ module Typr
434
434
  def print row=nil
435
435
  return super unless row
436
436
  header = ( row == :header )
437
- return draw @header[0..width-1].ljust(width) if
437
+ return draw @header[0...(width - @margin.size)].ljust(width - @margin.size) if
438
438
  header and @header.is_a? String
439
439
  return unless row_data = header ? @header : @data[row]
440
440
  fields = row_data.dup
data/lib/line.rb CHANGED
@@ -153,19 +153,19 @@ class Line < Space
153
153
  # line's default color, typed input in the answer color. When +completions+
154
154
  # is truthy, TAB matches the current word against the shell (commands,
155
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: the
157
- # first word red (the command), double-dash options blue.
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).
158
158
  #
159
159
  # line.cli "> ", initial: "cp %name"
160
160
  # #=> "cp source.txt" (or nil on Escape)
161
161
 
162
- def cli prompt, initial: '', redraw: nil, history: nil, prefixes: nil
162
+ def cli prompt, initial: '', redraw: nil, history: nil, prefixes: nil, regex: nil
163
163
  re = /\A[#{Regexp.escape(prefixes || '|+=!')}]/
164
164
  source = ->(query) {
165
165
  bash_candidates(query.sub(re, '')).split("\n").reject(&:empty?).uniq }
166
166
  colored = color_code(@colors[:default]).to_s + prompt + color_code(@colors[:answer])
167
167
  Typr.read_line(colored, left: left, top: top, initial: initial,
168
- completions: source, redraw: redraw, regex: @colors[:regex], history: history)
168
+ completions: source, redraw: redraw, regex: regex || @colors[:regex], history: history)
169
169
  end
170
170
 
171
171
  private
@@ -204,9 +204,7 @@ class Line < Space
204
204
  @prompt = ?>
205
205
  super
206
206
  @bottom = @top
207
- regex = { /(?:^|\s)--[^\s]+/ => :blue, /\A\S+/ => :red }
208
207
  @colors = { question: :green, answer: :yellow }.merge @colors
209
- @colors[:regex] = regex if @colors[:regex].nil?
210
208
  reset
211
209
  end
212
210
  end
data/lib/space.rb CHANGED
@@ -19,8 +19,11 @@ include Typr
19
19
  def %s_setting; @%s; end
20
20
  def %s
21
21
  max=Typr.#{ name[/left|right/] ? 'width' : 'height' }
22
- if !@%s;[max, #{name=='right' ? 'left+@max' :
23
- 'top+rows-1+headspace'}].min
22
+ if !@%s;[max, #{ case name
23
+ when 'left' then '0'
24
+ when 'top' then '0'
25
+ when 'right' then 'left+@max'
26
+ else 'top+[(respond_to?(:rows) ? rows : 0), 1].max-1+(respond_to?(:headspace) ? headspace : 0)' end }].min
24
27
  elsif @%s.is_a? Proc; @%s.call
25
28
  elsif @%s.is_a? Float; (max * @%s).round
26
29
  elsif @%s.negative?; max+@%s+1
data/lib/stack.rb CHANGED
@@ -101,7 +101,7 @@ module Typr
101
101
  color @colors[ :hints ]
102
102
  if type.to_s[ /column/ ]
103
103
  positions( row_id )[ @hints_start..-1 ].each_with_index{ |pos, idx|
104
- move( left + pos, top + row_id )
104
+ move( [left + @margin.size + pos - 1, left].max, top + row_id )
105
105
  draw @hints[idx] }
106
106
  else
107
107
  bottom = top + headspace + [height, rows].min - 1
data/lib/terminal.rb CHANGED
@@ -86,7 +86,8 @@ module Typr
86
86
  end
87
87
 
88
88
  # Text attributes (reset, bold, italic, ...) and named 8/256-color tables.
89
- MODES = %i[ reset bold italic underline slow fast invert ]
89
+ MODES = %i[ reset bold dim italic underline slow fast invert ]
90
+ MODES[25] = :steady
90
91
  COLORS = %i[ black red green yellow blue magenta cyan white ]
91
92
  COLOR_MAP = {
92
93
  brown: 130, orange: 208, lime: 118, pink: 218,
@@ -239,6 +240,24 @@ module Typr
239
240
  module_function :color_code
240
241
  public :color_code
241
242
 
243
+ # Named regex color schemes, accepted anywhere a +regex:+ map is (Text,
244
+ # Line, read_line, paint). +:generic+ paints numbers, colon keys, quoted
245
+ # strings, bracket groups, and punctuation; +:shell+ paints a command
246
+ # line's first word and double-dash options.
247
+ REGEX_SCHEMES = {
248
+ generic: {
249
+ /\[[^\]]*\]|\([^)]*\)|\{[^}]*\}/ => :magenta,
250
+ /".*?"|'.*?'/ => :green,
251
+ /\b\d+(?:\.\d+)?\b/ => :cyan,
252
+ /\b[\w.-]+:/ => :yellow,
253
+ /[^\w\s]/ => :grey60
254
+ },
255
+ shell: {
256
+ /(?:^|\s)--[^\s]+/ => :blue,
257
+ /\A\S+/ => :red
258
+ }
259
+ }
260
+
242
261
  # Returns +str+ with every region matched by the keys of +regex+ wrapped in
243
262
  # the associated color, restoring +base+ after each region. Non-matching
244
263
  # text passes through untouched. +regex+ may be nil or empty (no-op), and
@@ -254,6 +273,10 @@ module Typr
254
273
  #
255
274
  # Typr.paint "fix WARN now", { /WARN/ => :yellow }, "\e[39;49m"
256
275
  def self.paint str, regex, base
276
+ if regex.is_a? Symbol
277
+ regex = REGEX_SCHEMES.fetch(regex) {
278
+ raise ArgumentError, "unknown regex scheme: #{regex.inspect}" }
279
+ end
257
280
  return str if regex.nil? or regex.empty?
258
281
  union = Regexp.union( regex.keys )
259
282
  ansi = /\e(?:\[[0-9;?]*[ -\/]*[@-~]|\][^\a\e]*(?:\a|\e\\)|[()=><0A])|\e/
@@ -385,7 +408,7 @@ module Typr
385
408
  ## Interactive line editor used for string prompts (search, %str, ...).
386
409
  #
387
410
  # Renders +prompt+ and the query at (left, top) and edits it with
388
- # arrow keys, ctrl-arrow word jumps, home/end, delete and backspace.
411
+ # arrow keys, alt-arrow word jumps, home/end, delete and backspace.
389
412
  # Returns the query on enter, nil on escape. A block may inspect each
390
413
  # keypress after it is applied; a non-nil return short-circuits the
391
414
  # editor and becomes its return value. When stdin is not a tty, the
@@ -412,13 +435,16 @@ module Typr
412
435
  $>.print CURSOR_NORMAL
413
436
  begin
414
437
  loop do
438
+ $>.print CURSOR_INVISIBLE
415
439
  $>.print "\e[%i;%if" % [ top + 1, left + 1 ]
416
- $>.print ERASE_LINE
417
440
  before = text_width( prompt + query[0...cursor] )
418
441
  offset = [ left + before - Typr.width, 0 ].max
419
442
  display = prompt + paint( query, regex, prompt[/\e\[[0-9;]*m\z/].to_s )
420
- $>.print slice_width( display, offset, Typr.width - left + 1 )
443
+ line = slice_width( display, offset, Typr.width - left + 1 )
444
+ $>.print line
445
+ $>.print ' ' * (Typr.width - left + 1 - text_width(line))
421
446
  $>.print "\e[%i;%if" % [ top + 1, left + 1 + before - offset ]
447
+ $>.print CURSOR_NORMAL
422
448
  key = read_key
423
449
  return nil if key.nil? or key == KEY_ESCAPE
424
450
  return query if key == KEY_RETURN or key == "\n"
@@ -597,14 +623,15 @@ if __FILE__ == $0
597
623
  begin
598
624
  include Typr
599
625
  Typr.init
600
- colors = COLORS + (1..9).map{|i| "grey#{i*10}" }
626
+ colors = COLORS + COLOR_MAP.keys.map(&:to_s) + (0..10).map{|i| "grey#{i*10}" }
601
627
  chars = (33..126).map(&:chr)
602
628
  slice = chars.count / colors.count + 1
603
629
  characters = chars.each_slice( slice ).map(&:join)
604
630
  key,fg,bg = "",150,0
605
631
  Typr.clear
632
+ rows = [colors.count, Typr.height - 4].min
606
633
  loop do
607
- colors.each_with_index{ |color,y|
634
+ colors.first(rows).each_with_index{ |color,y|
608
635
  characters.each_with_index{ |chars,x|
609
636
  foreground color.to_sym;
610
637
  background colors[x].to_sym;
@@ -612,20 +639,21 @@ begin
612
639
  draw chars
613
640
  }
614
641
  }
642
+ move 0, rows
615
643
  (0...256).each{ |i| background i; draw ' ' }
616
644
 
617
645
  move 0, Typr.height
618
646
  color [:white, :black]
619
- p "KEY: %s / %s" % [key, key]
647
+ draw "KEY: %s / %s" % [key, key]
620
648
  move 15, Typr.height
621
649
  color [fg,bg]
622
650
  print " %s / %s " % [fg,bg]
623
651
  key = Typr.read_key
624
652
  case key
625
- when KEY_UP; fg+=1
626
- when KEY_DOWN; fg-=1
627
- when KEY_LEFT; bg-=1
628
- when KEY_RIGHT; bg+=1
653
+ when KEY_UP; fg = [fg + 1, 255].min
654
+ when KEY_DOWN; fg = [fg - 1, 0].max
655
+ when KEY_LEFT; bg = [bg - 1, 0].max
656
+ when KEY_RIGHT; bg = [bg + 1, 255].max
629
657
  when KEY_ESCAPE; exit
630
658
  end
631
659
  end
data/lib/text.rb CHANGED
@@ -33,13 +33,19 @@ module Typr
33
33
  # text << "appended line" # alias for build
34
34
 
35
35
  def build input=nil
36
- ( input, @data, @lines = sanitize(@data), "", [0] ) if rebuild = !input
36
+ ( input, @data, @lines = @data, "", [0] ) if rebuild = !input
37
37
  return self if rebuild && input == ""
38
- rows = @lines[0...-1].each_index.map{ |i| sanitize(@data[@lines[i]...@lines[i+1]]) }
38
+ rows = @lines[0...-1].each_index.map{ |i| @data[@lines[i]...@lines[i+1]] }
39
39
  @data = rows.join
40
40
  off = [ 0 ]; rows.each{ |row| off << off.last + row.length }
41
41
  @lines = off
42
42
  str = input.respond_to?(:read) ? input.read : input.to_s
43
+ if !rebuild and (r = @colors[:regex]) and r != {}
44
+ base = color_code( @colors[:default][0] ).to_s
45
+ # Paint new spans once, whole (multiline matches included); rebuilds
46
+ # below only reflow, so paint can neither stack nor need stripping.
47
+ str = Typr.paint(str, r, base)
48
+ end
43
49
  str = str.gsub("\t", " " * @tabspace)
44
50
  @tail = ""
45
51
  pos = 0
@@ -85,14 +91,6 @@ module Typr
85
91
  end
86
92
  end
87
93
  @lines << @data.length unless @lines.last == @data.length
88
- if (r = @colors[:regex]) and !r.empty?
89
- base = color_code( @colors[:default][0] ).to_s
90
- painted = @lines[0...-1].each_index.map{ |i|
91
- Typr.paint @data[@lines[i]...@lines[i+1]], r, base }
92
- @data = painted.join
93
- off = [ 0 ]; painted.each{ |line| off << off.last + line.length }
94
- @lines = off
95
- end
96
94
  highlight if @re
97
95
  end
98
96
  alias :<< :build
@@ -211,15 +209,7 @@ module Typr
211
209
  @data, @lines, @tail = "", [0], ""
212
210
  @search = { pattern: nil, dir: :forward, last: nil, matches: [] }
213
211
  super args
214
- regex = {
215
- /\[[^\]]*\]|\([^)]*\)|\{[^}]*\}/ => :magenta,
216
- /".*?"|'.*?'/ => :green,
217
- /\b\d+(?:\.\d+)?\b/ => :cyan,
218
- /\b[\w.-]+:/ => :yellow,
219
- /[^\w\s]/ => :grey60
220
- }
221
- @colors = { header: :yellow, search: :grey20 }.merge( @colors || {} )
222
- @colors[:regex] = regex if @colors[:regex].nil?
212
+ @colors = { header: :yellow, search: :grey20 }.merge( @colors || {} )
223
213
  @keymap = { search: ?/, search_next: ?n, search_prev: ?p }.merge @keymap
224
214
  @lastwidth = width
225
215
  self << @input
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.6.1"
6
- s.date = "2026-10-02"
5
+ s.version = "1.8.0"
6
+ s.date = "2026-10-11"
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.6.1
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kilian Reitmayr
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-02 00:00:00.000000000 Z
10
+ date: 2026-10-11 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: unicode-display_width