typr 1.5.1 → 1.6.1

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: f04e1f9234a97079a70786d6a427c7c6394775515c78a19feee8e17e3dc1370e
4
- data.tar.gz: b82771f5976e12fabbd56446d347ac8f49739e41d4dbe089b7ffc5738938dd17
3
+ metadata.gz: 3e9446e4b4389a720e2bda6b8e95f7a7144eb5b731b72f325b80f4bdea2866ed
4
+ data.tar.gz: 2c8d5867850febf6db6cebcee4acb6fde4b4c023007af4af54124f12f927e19f
5
5
  SHA512:
6
- metadata.gz: 99278addeb2d176281564448ddb5b34de23aebb1bac5a3e186c0a4735aafad0387274100bd38fa33018988d7672a7959c6c9791fd2bb8bf50c2e2545075552c6
7
- data.tar.gz: 9efd4720c0cc7c58f41acf31ca8a38827300127b72ffadaf9d18ebd53b8be0500f53de1386eed6255149f858ab3e5a943523ba79630cadf11eddf8c0f8a6e072
6
+ metadata.gz: 670f5b22e356fd91507a3335038d228c06d8eef5cb404c76783c6dfa37126f073ad90253b062b26aedc81ef15c9fcb95fde0074ffff3b15dac5fd670fa53045b
7
+ data.tar.gz: 8dd835d95fb86ac1a56c9390ac7a485ea17d5f5e70e917db4f18d5f6491517dfc31dc56cf07a13ffffed58ae3dc92d9f1a0600ed944c7e2ca9ecc0b67389ad94
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [vx.y.z] — __DATE__
4
+
5
+ ## [v1.6.1] — 2026-10-02
6
+
7
+ ### Fixed
8
+ - `Typr.paint` leaves pre-existing ANSI escapes byte-identical — the regex union matched escape bytes (`ESC`, `[`, `;`, digits) as ordinary text, shredding sequences already in the string; downstream clipping then dropped the orphaned `ESC` bytes and the bare sequence bodies printed as literal text (filecon help titles showed `[38;2;…m`). Paint now matches text runs between escapes only
9
+
10
+ ## [v1.6.0] — 2026-09-30
11
+
12
+ ### Added
13
+ - `regex:` color — every component's `colors:` hash accepts a `Regexp => color` map. Typed input (`read_line`/`Line#cli`) colors matching regions and restores the prompt's trailing color code; `Typr::Text` paints its whole buffer once at build (per logical row, keeping its `default` colors as the restore base) so rendering stays a pure emit. Additive `Typr.paint(str, regex, base)` helper; when patterns overlap the earlier key wins. Painted rows re-sanitize before append/rebuild, so a new paint never stacks escape codes
14
+ - Default `regex:` schemes — `Typr::Text` colors numbers cyan, colon-separated keys yellow, quoted strings green, bracketed groups magenta, and remaining punctuation grey60; `Line#cli` colors the first word red and `--options` blue. Both install only when `regex:` is absent, so a custom map replaces them and `regex: {}` turns them off. Colors may be any value `color_code` accepts, including 8-bit indices and RGB triplets
15
+ - `Line#cli` — prompts a single command line (default-colored prompt, answer-colored input) with TAB completion matched against the shell. The typed command tints with the default `regex:` scheme; completion candidates are plain strings; bash_completion completers are loaded dynamically on each match so per-command option sets (`ls`/`git`/...) appear rather than just `--help`
16
+ - `Typr.drain_input` — discards bytes already waiting on the input stream so input confirmed at one prompt cannot auto-answer the next (e.g. a double-tapped Enter confirming a destructive default)
17
+ - `Browser#printable` column — boolean that replaces a "binary" concept from the other side: a regular file is printable when the first 8KB contain no NUL byte and decode as valid UTF-8 (directories, links, and special files are false). It sits next to `executable` in the `full` view and the header auto-defines `Typr::PRINTABLE`
18
+
19
+ ### Changed
20
+ - Header bars render bold by default — `Stack#show` applies the bold attribute around the header row and resets it before the content rows, so Grid and Text headers are emphasized without affecting the body. `mode_code` now matches its attribute by symbol (the string lookup always missed and emitted nothing)
21
+ - Tabs in `Typr::Text` areas expand to `tabspace` spaces (default 2) instead of passing the raw byte through to the terminal's own tab stops, so width math and wrapping see the same cells as the rendered output
22
+ - `Typr::Text` regex paint restores only the `default` foreground after each matched span instead of also forcing the background to black, so Stack's per-row background (alternating stripes, search highlight, padding) stays intact for the rest of the line
23
+ - `color_code` stays callable with an explicit receiver — `module_function` kept its module copy alive for `paint` but made the instance copy private, which broke includers that called it as `object.color_code`; the instance method is public again (term.rb)
24
+ - The `executable` and `printable` boolean columns render as `yes`/`no` (a display proc replaces the raw true/false) and sort correctly — booleans lack a spaceship operator, so the comparator fell back to 0 and every row compared equal; sorting now runs on the proc'd `yes`/`no` strings. `Grid#process` no longer drops `false` cells, which would have left a `nil` in the layer map
25
+ - Tab completion moves out of `read_line` into the callers: `read_line` now takes `completions:` as a content-agnostic callable (`->(query, cursor) { candidates }`) and TAB merely matches the current word through its pick grid; `Line#ask` keeps its plain `:line` behaviour and no longer forwards a `completions` option
26
+ - `Grid#pick` with a column filter confirms the single remaining match instantly — when the live search narrows the list to one row it returns immediately and arms a 0.5s quiet window: overtyped keys are discarded at the next in-app read (`read_key`) and the tty queue is flushed before any subprocess handoff, so neither the picker nor a spawned editor sees them
27
+ - Command-line history can be navigated in `read_line` and `Line#cli`; `Line#cli` also accepts a command `prefixes:` parameter so completion strips the command prefix before matching the current word
28
+
29
+ ### Fixed
30
+ - Editing recalled command-line history no longer fails on frozen strings and restores the colored prompt when returning to the recalled entry
31
+
3
32
  ## [v1.5.1] — 2026-09-11
4
33
 
5
34
  ## [v1.5.0] — 2026-09-05
@@ -24,7 +53,10 @@
24
53
  - `reset_view` reloads the directory again — it calls `cd @directory` before clearing display/selection/position and re-sorting, so reset picks up new and removed files instead of only reshuffling the stale listing
25
54
  - Head path lookup — `@head.path_at(x, y)` maps a click to a target directory: the username column returns the resolved home directory, each `/`-separated segment of the directory cell returns that ancestor path (padding, action columns and array listings return nil); mouse `x` is adjusted from 1-based terminal columns so boundary clicks resolve to the cell under the cursor
26
55
  - `Mouse#alt?` — reports the SGRmeta bit, so callers can treat Alt+click like a right-click (e.g. opening a command list where right-click is hard to send, as on Termux touchscreens)
56
+ - User-definable base palette — `$palette` maps the eight basic color names to Integer/`[r,g,b]` values and `color_code` consults it first, so apps can override terminal colors from config (absent names keep terminal resolution)
27
57
  - `cd` survives unreadable directories — `chdir` and the listing are probed before any state changes, so `EACCES`/`ENOENT` (e.g. Android's `/data`) shows a warning and stays put instead of crashing on an emptied listing; returns `false` on failure
58
+ - Refresh keeps scroll position and hints — `cd` only resets `@start`/`@hints_start` when actually navigating somewhere new, and `reset_view` clears filters/selection without touching position (filters are cleared before the reload so the re-filter doesn't snap to top either)
59
+ - Enter picks the first result in search-filtered `Grid#pick` — confirming the filter line returns the top visible row (honoring cycled hints) instead of applying the filter and picking nothing; with no matches left the old commit-filter behavior (and nil return) is kept, so `Browser#search` still works
28
60
 
29
61
  ## [v1.4.0] — 2026-08-15
30
62
 
data/README.md CHANGED
@@ -31,14 +31,13 @@ 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, …).
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.
35
35
 
36
- ### Frontends
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.
37
37
 
38
- - **Terminal** (`require 'typr'`) — the default: raw terminal IO, no ncurses, works over SSH and on Termux
39
- - **Graphical** (`require 'typr/graphical'`) — experimental: SDL2-based windows
38
+ ### Frontends
40
39
 
41
- All components work identically across frontends.
40
+ The Terminal frontend (`require 'typr'`) is the default: raw terminal IO, no ncurses, works over SSH and on Termux.
42
41
 
43
42
  ## Installation
44
43
 
@@ -187,13 +186,12 @@ Clears the full screen or a single line (`:line`).
187
186
  Reads a single keypress in raw mode. Returns the key as a String (`KEY_ESCAPE`, `?q`, arrow escape sequences), a `Typr::Mouse` struct for mouse reports, or `nil` when input is unavailable.
188
187
 
189
188
  ### `Typr.read_line(prompt, ...)`
190
- The shared interactive line editor. Renders a prompt and query at any position with arrow keys, ctrl-arrow word jumps, Home/End/Delete/Backspace; Enter confirms, Escape aborts (`nil`). An optional block runs after each keypress and may short-circuit with a result:
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.
191
190
 
192
191
  ```ruby
193
- Typr.read_line "/" do |key, query, cursor|
194
- filter query
195
- nil
196
- end
192
+ Typr.read_line "/", completions: ->(query, cursor) {
193
+ songs.select { |s| s.start_with? query[0...cursor] }
194
+ }
197
195
  ```
198
196
 
199
197
  ### `Line#ask`
@@ -204,6 +202,13 @@ Strict question types:
204
202
 
205
203
  The question and answer are rendered in `@colors[:question]`/`@colors[:answer]`.
206
204
 
205
+ ### `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.
207
+
208
+ ```ruby
209
+ line.cli "> ", initial: "cp %name %dir", completions: true, redraw: -> { show }
210
+ ```
211
+
207
212
  ### Layout properties (all components)
208
213
  - `top`, `bottom`, `left`, `right` — placement. Integers for absolute offsets, floats (0-1) for relative sizing. Negative values position from the opposite edge. (`bottom: -1` = last row)
209
214
 
@@ -225,6 +230,7 @@ The question and answer are rendered in `@colors[:question]`/`@colors[:answer]`.
225
230
  | `input` | String or IO-like stream to display |
226
231
  | `header` | Header bar text |
227
232
  | `border` | Border style: `:round`, `" "`, etc. |
233
+ | `tabspace` | Tab width; tabs expand to this many spaces (default 2) |
228
234
  | `.search` | Search the text (`/`); `.search_next`/`.search_prev` (`n`/`p`) |
229
235
 
230
236
  ### Browser options
data/lib/browser.rb CHANGED
@@ -54,10 +54,11 @@ module Typr
54
54
  # browser.cd ".." # => parent directory
55
55
  # browser.cd 2 # => jump to history entry 2
56
56
 
57
- def cd dir, append=false
57
+ def cd dir, append=false, trim=0
58
58
  return unless dir
59
59
  data, missing = [],{}
60
60
  unless append
61
+ same = File.expand_path( dir ) == File.expand_path( @directory.to_s )
61
62
  begin
62
63
  path = File.expand_path( dir )
63
64
  Dir.chdir( path )
@@ -71,7 +72,8 @@ module Typr
71
72
  clear
72
73
  @directory = path
73
74
  else
74
- entries = Dir.new( @directory ).children
75
+ Dir.chdir( dir )
76
+ entries = Dir.new( dir ).children
75
77
  end
76
78
  data = entries.reject{|f|
77
79
  !@show_hidden and f[0] == ?. }.map { |file| scan_file file }
@@ -103,36 +105,44 @@ module Typr
103
105
  next unless file
104
106
  data[ missing[file] ][TYPE, 2] = mime.split( ?/ ) if missing[file] }
105
107
  end
106
- data.each { |row| row[0] = File.join(@directory, row[0]) }
108
+ data.each { |row|
109
+ row[0] = File.join(append ? dir : @directory, row[0])
110
+ row[PATH] = "./" + (File.dirname(row[0])[(trim > 1 ? trim + 1 : trim)..-1] || "")
111
+ }
107
112
  self << data
108
113
  sort
109
114
  filter
110
115
  unless append
111
116
  @directory += ?/ unless @directory == ?/
112
117
  change_view @view
113
- reset :position
118
+ reset :position unless same
114
119
  end
115
120
  return
116
121
  end
117
122
 
118
123
  ##
119
- # Build a row for a single directory entry from filesystem metadata only.
120
- # MIME type detection (extension database and +file --mime-type+) happens
121
- # in a batch pass inside {#cd}; type/subtype are left as +"?"+ here.
124
+ # Build a row for a single directory entry from filesystem metadata plus a
125
+ # cheap content probe for the +printable+ flag (no NUL byte and valid
126
+ # UTF-8 in the first 8KB). MIME type detection (extension database and
127
+ # +file --mime-type+) happens in a batch pass inside {#cd}; type/subtype
128
+ # are left as +"?"+ here.
122
129
  #
123
- # browser.scan_file "image.png" # => 12-column row
130
+ # browser.scan_file "image.png" # => 14-column row
124
131
 
125
132
  def scan_file file
126
133
  target = nil
127
134
  if File.symlink? file
128
135
  target = File.readlink( file ) rescue $!.to_s
129
- return [file, target, "????", false, "?", "?", 0, 0, 0, 0, "?", "?", nil] unless File.exist?(file)
136
+ return [file, target, "????", false, false, "?", "?", 0, 0, 0, 0, "?", "?", nil] unless File.exist?(file)
130
137
  end
131
138
  stat = File.stat(file) rescue nil
132
- return [file, "", "????", false, "?", "?", 0, 0, 0, 0, "?", "?", nil] unless stat
133
- [ file, target, stat.mode.to_s(8)[-3..-1].chars.map{|d|
139
+ return [file, "", "????", false, false, "?", "?", 0, 0, 0, 0, "?", "?", nil] unless stat
140
+ chunk = stat.file? ? ( File.open( file, "rb"){ |f| f.read 8192 } rescue nil ) : nil
141
+ printable = !!chunk && !chunk.include?("\x00") &&
142
+ chunk.dup.force_encoding( Encoding::UTF_8 ).valid_encoding?
143
+ [ file, target, stat.mode.to_s(8)[-3..-1].chars.map{|d|
134
144
  "rwx".chars.map.with_index{|c,i| d.to_i[2-i]==0 ? ?- : c } }.join,
135
- ( File.executable?(file) and stat.file? ), stat.uid, stat.gid, stat.size,
145
+ ( File.executable?(file) and stat.file? ), printable, stat.uid, stat.gid, stat.size,
136
146
  stat.atime.to_i, stat.mtime.to_i, stat.ctime.to_i, "?", "?", nil ]
137
147
  end
138
148
 
@@ -159,10 +169,7 @@ module Typr
159
169
  return unless view
160
170
  view = @views.keys[ view ] if view.is_a? Integer
161
171
  @sequence = @views[ view ].map{|title| @header.index title.to_s}
162
- if @show_path
163
- @data.each { |row| row[PATH] = File.dirname(row[NAME]) } if @data.any?
164
- @sequence.insert(@sequence.index(NAME) || 0, PATH) unless @sequence.include?(PATH)
165
- end
172
+ @sequence.insert(0, PATH) if @show_path
166
173
  reset :format
167
174
  @view = view
168
175
  return
@@ -260,7 +267,7 @@ module Typr
260
267
  #
261
268
  # browser.reset_view
262
269
 
263
- def reset_view; cd @directory; reset [:display, :selection, :position]; sort end
270
+ def reset_view; reset [:display, :selection]; cd @directory; sort end
264
271
 
265
272
  ## Open a filter popup on the chosen row/column and apply the input.
266
273
  #
@@ -415,7 +422,7 @@ module Typr
415
422
  top: top, align: [:right], input:@header,
416
423
  left: left + ( @positions[args[:type]] || 0 ) -
417
424
  ( args[:input] || @header ).map( &:size ).max - 1,
418
- colors:{ columns: [ @colors[:actions][args[:type]] ] } }.merge args )
425
+ colors:{ columns: [ @colors[:actions][args[:type].to_s.gsub(/[\[\]]/, '').downcase.to_sym] ] } }.merge args )
419
426
  end
420
427
 
421
428
  ##
@@ -426,7 +433,7 @@ module Typr
426
433
  # +directory+ | +"~"+ | Starting directory to browse
427
434
  # +mimetype_db+ | true | Load MIME from file extensions
428
435
  # +mimetype_magic+ | true | Use +file --mime-type+ for unknowns
429
- # +colors[:actions]+ | { "[f]ilter": :red, ... } | Status bar item colors
436
+ # +colors[:actions]+ | { filter: :red, ... } | Status bar item colors
430
437
  # +colors[:dir_history]+ | :yellow | History picker column color
431
438
  # +colors[:types]+ | (see below) | Per MIME-type foreground color
432
439
  #
@@ -445,7 +452,7 @@ module Typr
445
452
  @left ||= 0
446
453
  @top ||= 0
447
454
  @directory ||= ?~
448
- @colors[:actions] = { "[f]ilter": :red, "[s]ort": :blue, "[v]iews": :green
455
+ @colors[:actions] = { filter: :red, sort: :blue, views: :green
449
456
  }.merge @colors[:actions] || {}
450
457
  @colors[ :types ] = {
451
458
  image: :blue,
@@ -460,13 +467,11 @@ module Typr
460
467
  'x-msdownload': :red, 'x-pie-executable': :red
461
468
  }.merge( @colors[ :types ] || {} )
462
469
 
463
- @keymap = { back: KEY_BACKSPACE, confirm: KEY_RETURN, help: ?h,
464
- home: ?~, views: ?v, sort_view: ?s, search: ?/,
465
- mark: ?m, mark_range: ?M, mark_all: ?a, mark_invert: ?i,
466
- filter_view: ?f, reset_view: ?r,
467
- toggle_hidden: ?H}.merge @keymap
470
+ @keymap = { back: KEY_BACKSPACE, confirm: KEY_RETURN,
471
+ page_down: KEY_PAGEDOWN, page_up: KEY_PAGEUP, up: KEY_UP,
472
+ down: KEY_DOWN, to_top: KEY_HOME, to_bottom: KEY_END }.merge @keymap
468
473
 
469
- self.header = %w[ name target permissions executable owner group size
474
+ self.header = %w[ name target permissions executable printable owner group size
470
475
  accessed modified created type subtype path ]
471
476
 
472
477
  @format[NAME] = :max
@@ -474,9 +479,11 @@ module Typr
474
479
  @ignore[NAME] = @ignore[TARGET] = true
475
480
  @hints = "1234567890"
476
481
  [NAME, SIZE, ACCESSED, MODIFIED, CREATED].each{|col| @rawsort[col] = true}
482
+ @rawsort[PRINTABLE] = @rawsort[EXECUTABLE] = false
477
483
 
478
484
  @procs[ACCESSED] = @procs[MODIFIED] = @procs[CREATED] = :datetime
479
485
  @procs[SIZE] = :magnitudes
486
+ @procs[EXECUTABLE] = @procs[PRINTABLE] = ->(v) { v ? "yes" : "no" }
480
487
  @procs[OWNER] = Proc.new{|uid| Etc.getpwuid(uid).name rescue uid }
481
488
  @procs[GROUP] = Proc.new{|gid| Etc.getgrgid(gid).name rescue gid }
482
489
 
@@ -487,8 +494,8 @@ module Typr
487
494
  @views = { simple: %i[ name ],
488
495
  compact: %i[ name size type],
489
496
  stats: %i[ name size permissions owner group modified ],
490
- full: %i[ name size permissions executable owner group
491
- accessed modified created type subtype ] }.merge @views
497
+ full: %i[ name size permissions executable printable owner group
498
+ accessed modified created type subtype ] }.merge @views
492
499
  @parent = self
493
500
  @head ||= Grid.new( parent:self, left: ->{@parent.left}, margin: '',
494
501
  right: ->{@parent.width + @parent.left - 1}, top: ->{@parent.top-1},
@@ -498,8 +505,8 @@ module Typr
498
505
  "#{s} #{@parent.reverse ? "\u2193" : "\u2191"}"},
499
506
  ->{@parent.view} ] ], format: [:min, :max, :min, :min, :min],
500
507
  colors: { columns: [@colors[:default], @colors[:default],
501
- @colors[:actions][:"[f]ilter"], @colors[:actions][:"[s]ort"],
502
- @colors[:actions][:"[v]iews"]] } )
508
+ @colors[:actions][:filter], @colors[:actions][:sort],
509
+ @colors[:actions][:views]] } )
503
510
  @head.define_singleton_method( :data_at ) do |x, y|
504
511
  return unless x and y and y.between?( top + 1, bottom + 1 ) and
505
512
  x.between?( left, right )
@@ -522,11 +529,28 @@ module Typr
522
529
  ( pre + ( pre.end_with?( "/" ) ? "" : text[pre.length..-1][/\A[^\/]*/] ) ).sub( %r{(?<=.)/$}, "" )
523
530
  end
524
531
 
525
- @user ||= Line.new( parent:self, left:->{@parent.left},top:->{@parent.bottom+1},
526
- bindings: %w[ [h]elp [/]:search ] + @colors[:actions].map{|name,color|
527
- color_code( color ) + name.to_s + color_code(@colors[:default]) } +
528
- %w[ [r]eset [d]irectories [m]ark [M]ark_range mark_[a]ll
529
- [i]nvert_mark [~]:home [H]idden_files [back]:.. [up] [down] [pageup] [pagedown] [home] [end] [esc]:quit [return]:confirm\ ])
532
+ if args[:bindings]
533
+ bindings_labels = []
534
+ args[:bindings].each do |label, cmd|
535
+ name = label.gsub( /[\[\]]/, '' ).downcase.to_sym
536
+ colored = @colors[:actions][name] ?
537
+ color_code( @colors[:actions][name] ) + label + color_code(@colors[:default]) : label
538
+ 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
543
+ end
544
+ @user ||= Line.new( parent:self, left:->{@parent.left}, top:->{@parent.bottom+1},
545
+ bindings: bindings_labels )
546
+ 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\ ])
553
+ end
530
554
  cd @directory
531
555
  end
532
556
  end
data/lib/grid.rb CHANGED
@@ -169,11 +169,10 @@ module Typr
169
169
  @layer ||= @data.class.new
170
170
  return if @procs.empty?
171
171
  @layer[row] = @procs.map{ |col,proc|
172
- proc = @functions[proc] if proc.is_a? Symbol
173
172
  cell = @data[row][col]
174
- next unless cell
175
- [ col, proc.lambda? ? proc.call( cell ) : proc.call( cell, row, col ) ] unless
176
- cell.is_a?(Proc) }.to_h
173
+ next if cell.nil? or cell.is_a?(Proc)
174
+ proc = @functions[proc] if proc.is_a? Symbol
175
+ [ col, proc.lambda? ? proc.call( cell ) : proc.call( cell, row, col ) ] }.to_h
177
176
  end
178
177
 
179
178
  ##
@@ -256,6 +255,7 @@ module Typr
256
255
 
257
256
  def pick type = :row, row=0, column: nil
258
257
  return super(type, row) unless column
258
+ auto = !type.to_s['none']
259
259
 
260
260
  saved = [@filters.dup, @map, @start]
261
261
  base = @map || ids
@@ -286,6 +286,7 @@ module Typr
286
286
  @filters = [[column, query]]
287
287
  @map = base.select{ |id| check id }
288
288
  @start = 0
289
+ return @map.first if auto and @map.one?
289
290
  end
290
291
  show
291
292
  draw_hints
@@ -293,10 +294,15 @@ module Typr
293
294
  }
294
295
  case result
295
296
  when nil, :outside; @filters, @map, @start = saved
296
- when Integer; return result
297
+ when Integer; Typr.suppress_keys if auto; return result
297
298
  else
298
- @filters = result.empty? ? saved[0] : saved[0] + [[column, result]]
299
- filter :all
299
+ picked = page.to_a[@hints_start]
300
+ if picked; Typr.suppress_keys if auto; return picked end
301
+ return begin
302
+ @filters = result.empty? ? saved[0] : saved[0] + [[column, result]]
303
+ filter :all
304
+ nil
305
+ end
300
306
  end
301
307
  return
302
308
  end
data/lib/line.rb CHANGED
@@ -123,10 +123,17 @@ class Line < Space
123
123
  draw prepare( colored, width, @align, @trim )
124
124
  color( @colors[:answer] )
125
125
  case object
126
- when Array then answer << object.first.pick( object[1], **(object[3] || {}) )
126
+ when Array
127
+ if object.first == :line
128
+ opts = object[1] || {}
129
+ answer << Typr.read_line(colored + color_code(@colors[:answer]),
130
+ left: left, top: top, initial: opts[:initial] || "")
131
+ else
132
+ answer << object.first.pick(object[1], **(object[3] || {}))
133
+ end
127
134
  when :key then answer << Typr.read_key
128
- when :line then answer << Typr.read_line( colored + color_code( @colors[:answer] ),
129
- left: left, top: top )
135
+ when :line then answer << Typr.read_line(colored + color_code(@colors[:answer]),
136
+ left: left, top: top)
130
137
  end
131
138
  return if !answer.last or answer.last == @keymap[:exit]
132
139
  text = ( object.is_a?(Array) and object[2] ) ?
@@ -142,6 +149,54 @@ class Line < Space
142
149
  return ( answer.one? ? answer.first : answer )
143
150
  end
144
151
 
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: the
157
+ # first word red (the command), double-dash options blue.
158
+ #
159
+ # line.cli "> ", initial: "cp %name"
160
+ # #=> "cp source.txt" (or nil on Escape)
161
+
162
+ def cli prompt, initial: '', redraw: nil, history: nil, prefixes: nil
163
+ re = /\A[#{Regexp.escape(prefixes || '|+=!')}]/
164
+ source = ->(query) {
165
+ bash_candidates(query.sub(re, '')).split("\n").reject(&:empty?).uniq }
166
+ colored = color_code(@colors[:default]).to_s + prompt + color_code(@colors[:answer])
167
+ Typr.read_line(colored, left: left, top: top, initial: initial,
168
+ completions: source, redraw: redraw, regex: @colors[:regex], history: history)
169
+ end
170
+
171
+ private
172
+
173
+ def bash_candidates part
174
+ IO.popen([{ 'COMP_LINE' => part, 'COMP_POINT' => part.bytesize.to_s },
175
+ 'bash', '-ic', <<~SH], err: '/dev/null', &:read)
176
+ set +m; shopt -s expand_aliases
177
+ export PS1= PS2= PS3= PS4= PROMPT_COMMAND=
178
+ read -r -a COMP_WORDS <<< "$COMP_LINE"
179
+ [ "$COMP_LINE" != "${COMP_LINE% }" ] && COMP_WORDS+=( "" )
180
+ COMP_CWORD=$(( ${#COMP_WORDS[@]} - 1 ))
181
+ [ "$COMP_CWORD" -lt 0 ] && COMP_CWORD=0
182
+ cur=${COMP_WORDS[COMP_CWORD]}
183
+ if declare -F __load_completion >/dev/null 2>&1; then
184
+ __load_completion "${COMP_WORDS[0]}" 2>/dev/null
185
+ elif declare -F _completion_loader >/dev/null 2>&1; then
186
+ _completion_loader "${COMP_WORDS[0]}" 2>/dev/null
187
+ fi
188
+ Fn=$(complete -p "${COMP_WORDS[0]}" 2>/dev/null | awk '{print $(NF-1)}')
189
+ type "$Fn" >/dev/null 2>&1 && "$Fn" "${COMP_WORDS[0]}" "$cur" 2>/dev/null
190
+ if [ -z "${COMPREPLY[*]}" ]; then
191
+ if [ "$COMP_CWORD" -eq 0 ]; then compgen -A command -A alias -A file -- "$cur"
192
+ else compgen -A file -- "$cur"; fi
193
+ else printf '%s\n' "${COMPREPLY[@]}"
194
+ fi
195
+ SH
196
+ end
197
+
198
+ public
199
+
145
200
  def initialize args
146
201
  @right= -1
147
202
  @align = :left
@@ -149,7 +204,9 @@ class Line < Space
149
204
  @prompt = ?>
150
205
  super
151
206
  @bottom = @top
207
+ regex = { /(?:^|\s)--[^\s]+/ => :blue, /\A\S+/ => :red }
152
208
  @colors = { question: :green, answer: :yellow }.merge @colors
209
+ @colors[:regex] = regex if @colors[:regex].nil?
153
210
  reset
154
211
  end
155
212
  end
data/lib/space.rb CHANGED
@@ -117,7 +117,7 @@ eval( method % ([name]*11) ) }
117
117
  instance_variable_set ?@ + name.to_s, value }
118
118
  @colors = symbolize( @colors ) if @colors.is_a? Hash
119
119
  @keymap = { exit: KEY_ESCAPE }.merge @keymap
120
- @colors = { default: [:white, :black], border: [:white, :black] }.merge(@colors)
120
+ @colors = { default: [:white, :black], border: [:white, :black], regex: nil }.merge(@colors)
121
121
  borders = @borders.dup
122
122
  self.border = @border
123
123
  @borders.merge! borders
data/lib/stack.rb CHANGED
@@ -127,7 +127,9 @@ module Typr
127
127
  color @colors[ :header ]
128
128
  move left,top
129
129
  draw @margin
130
+ mode :bold
130
131
  print :header
132
+ mode :reset
131
133
  end
132
134
  list = page.to_a
133
135
  list[ height-1 ] = nil if list.length < height
data/lib/terminal.rb CHANGED
@@ -7,6 +7,10 @@ require 'unicode/display_width'
7
7
 
8
8
  module Typr
9
9
 
10
+ # User palette overriding the eight basic colors (black red green yellow
11
+ # blue magenta cyan white); absent names fall back to terminal resolution.
12
+ $palette = {}
13
+
10
14
  # Raw /dev/tty used for key input; falls back to $stdin when unavailable.
11
15
  INPUT = (IO.new IO.sysopen("/dev/tty", "r")) rescue $stdin
12
16
 
@@ -208,7 +212,7 @@ module Typr
208
212
  # Apply a text attribute (+mode+); mode_code returns the escape sequence
209
213
  # without writing it.
210
214
  def mode name; draw mode_code(name) end
211
- def mode_code name; if id = MODES.index(name.to_s) then "\e[#{ id }m" end end
215
+ def mode_code name; if id = MODES.index(name.to_s.to_sym) then "\e[#{ id }m" end end
212
216
 
213
217
  # Build the ANSI code for +color+ (a Symbol name, greyN, Integer palette id,
214
218
  # or [r,g,b] / [fg,bg] Array), optionally as a background; when false the
@@ -217,7 +221,9 @@ module Typr
217
221
  color = color.to_sym if color.is_a? String
218
222
  case color
219
223
  when Symbol;
220
- if id = COLORS.index(color); "\e[#{ id + (bg ? 40 : 30) }m"
224
+ if (spec = $palette[color]) and (spec.is_a?(Integer) or spec.is_a?(Array))
225
+ color_code(spec, bg)
226
+ elsif id = COLORS.index(color); "\e[#{ id + (bg ? 40 : 30) }m"
221
227
  elsif color[/^gr[ae]y\d{,2}$/]
222
228
  "\e[%i;5;%im" % [bg ? 48 : 38, 232 + (color[/\d+/].to_f/100*23).to_i]
223
229
  elsif id = COLOR_MAP[color]
@@ -230,6 +236,34 @@ module Typr
230
236
  end
231
237
  end
232
238
  end
239
+ module_function :color_code
240
+ public :color_code
241
+
242
+ # Returns +str+ with every region matched by the keys of +regex+ wrapped in
243
+ # the associated color, restoring +base+ after each region. Non-matching
244
+ # text passes through untouched. +regex+ may be nil or empty (no-op), and
245
+ # +base+ may be empty (color then persists to the end of the string).
246
+ #
247
+ # When two patterns match the same span, the earlier key in the Hash wins,
248
+ # so order overlapping keys longest-first.
249
+ #
250
+ # Pre-existing ANSI sequences pass through byte-identical: only the plain
251
+ # text between them is matched, so painting never shreds escapes that are
252
+ # already in the string (and a later clip cannot orphan their bodies as
253
+ # literal text).
254
+ #
255
+ # Typr.paint "fix WARN now", { /WARN/ => :yellow }, "\e[39;49m"
256
+ def self.paint str, regex, base
257
+ return str if regex.nil? or regex.empty?
258
+ union = Regexp.union( regex.keys )
259
+ ansi = /\e(?:\[[0-9;?]*[ -\/]*[@-~]|\][^\a\e]*(?:\a|\e\\)|[()=><0A])|\e/
260
+ str.scan( /#{ansi}|[^\e]+/ ).map { |run|
261
+ run.start_with?( "\e" ) ? run :
262
+ run.gsub( union ) { |m|
263
+ color = regex[ regex.keys.find{ |k| k === m } ]
264
+ color_code( color ).to_s + m + base }
265
+ }.join
266
+ end
233
267
 
234
268
  # Read the current background/foreground color pair.
235
269
  def get_background; $color[1] end
@@ -260,6 +294,14 @@ module Typr
260
294
  # input is unavailable.
261
295
 
262
296
  def self.read_key
297
+ if quiet_window?
298
+ loop do
299
+ remaining = @suppress_until - Process.clock_gettime(Process::CLOCK_MONOTONIC)
300
+ break if remaining <= 0
301
+ break unless IO.select([INPUT], nil, nil, remaining)
302
+ begin INPUT.read_nonblock(1024) rescue break end
303
+ end
304
+ end
263
305
  read = ->(tty) do
264
306
  str = tty.sysread 6
265
307
  if str.start_with?("\e[M") and str.size < 6
@@ -303,6 +345,43 @@ module Typr
303
345
  Mouse.new button, x, y, action, modifiers
304
346
  end
305
347
 
348
+ ## Discard any bytes already waiting on the input stream, so input confirmed
349
+ # at a previous prompt cannot auto-answer the next one (e.g. a double-tapped
350
+ # Enter confirming a destructive default). Best-effort: never raises.
351
+ #
352
+ # Typr.drain_input # => nil
353
+
354
+ def self.drain_input
355
+ while IO.select([INPUT], nil, nil, 0)
356
+ begin
357
+ INPUT.read_nonblock(1024)
358
+ rescue IO::WaitReadable
359
+ break
360
+ end
361
+ end
362
+ nil
363
+ rescue StandardError
364
+ nil
365
+ end
366
+
367
+ # Post-auto-confirm input guard: keys within OVERTYPE_QUIET seconds of an
368
+ # auto-selected pick are discarded — in-app via +read_key+, and before a
369
+ # subprocess handoff via +flush_overtype+.
370
+ OVERTYPE_QUIET = 0.5
371
+
372
+ def self.suppress_keys timeout=OVERTYPE_QUIET
373
+ @suppress_until = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
374
+ end
375
+
376
+ def self.quiet_window?
377
+ @suppress_until and $stdin.tty? and
378
+ @suppress_until > Process.clock_gettime(Process::CLOCK_MONOTONIC)
379
+ end
380
+
381
+ def self.flush_overtype
382
+ drain_input if quiet_window?
383
+ end
384
+
306
385
  ## Interactive line editor used for string prompts (search, %str, ...).
307
386
  #
308
387
  # Renders +prompt+ and the query at (left, top) and edits it with
@@ -312,14 +391,24 @@ module Typr
312
391
  # editor and becomes its return value. When stdin is not a tty, the
313
392
  # query is read from a single line of piped input instead.
314
393
  #
394
+ # +completions+ may be a callable matching the current word to a list
395
+ # of candidates; TAB then replaces the word through a pick grid. Each
396
+ # candidate is either a plain String or [text, color-pair] to tint the
397
+ # pick entry. The callable's content is arbitrary - shells, tags, ...
398
+ #
399
+ # +regex+ is a Hash of Regexp => color (see {.paint}) applied to the
400
+ # query as it is rendered; matched regions restore the prompt's trailing
401
+ # color code.
402
+ #
315
403
  # Typr.read_line "/" do |key, query, cursor|
316
404
  # filter query
317
405
  # nil
318
406
  # end
319
407
 
320
- def self.read_line prompt='', left: 0, top: 0, initial: '', completions: nil, redraw: nil, &block
408
+ def self.read_line prompt='', left: 0, top: 0, initial: '', completions: nil, redraw: nil, regex: nil, history: nil, &block
321
409
  return $stdin.gets&.chomp unless $stdin.tty?
322
410
  query, cursor = initial.dup, initial.length
411
+ hist_idx = nil
323
412
  $>.print CURSOR_NORMAL
324
413
  begin
325
414
  loop do
@@ -327,7 +416,8 @@ module Typr
327
416
  $>.print ERASE_LINE
328
417
  before = text_width( prompt + query[0...cursor] )
329
418
  offset = [ left + before - Typr.width, 0 ].max
330
- $>.print slice_width( prompt + query, offset, Typr.width - left + 1 )
419
+ display = prompt + paint( query, regex, prompt[/\e\[[0-9;]*m\z/].to_s )
420
+ $>.print slice_width( display, offset, Typr.width - left + 1 )
331
421
  $>.print "\e[%i;%if" % [ top + 1, left + 1 + before - offset ]
332
422
  key = read_key
333
423
  return nil if key.nil? or key == KEY_ESCAPE
@@ -339,6 +429,24 @@ module Typr
339
429
  when KEY_ALT_RIGHT, "\eC"; cursor = word_next query, cursor
340
430
  when KEY_HOME; cursor = 0
341
431
  when KEY_END; cursor = query.length
432
+ when KEY_UP, "\eOA";
433
+ if history&.any?
434
+ hist_idx = [((hist_idx || -1) + 1), history.size - 1].min
435
+ query = history[hist_idx].dup
436
+ cursor = query.length
437
+ end
438
+ when KEY_DOWN, "\eOB";
439
+ if history&.any? && hist_idx
440
+ hist_idx -= 1
441
+ if hist_idx < 0
442
+ query = +''
443
+ cursor = 0
444
+ hist_idx = nil
445
+ else
446
+ query = history[hist_idx].dup
447
+ cursor = query.length
448
+ end
449
+ end
342
450
  when KEY_DC, "\e[3~"; query.slice!(cursor, 1) if cursor < query.length
343
451
  when KEY_ALT_BACKSPACE
344
452
  old = cursor
@@ -360,32 +468,29 @@ module Typr
360
468
  result = block.call( key, query, cursor ) if block
361
469
  return result unless result.nil?
362
470
  if completions and key == KEY_TAB
363
- raw = query[0...cursor]
364
- cmd = raw[/\w+/]
365
- if cmd
366
- items = IO.popen([{ 'COMP_LINE' => raw, 'COMP_POINT' => raw.bytesize.to_s },
367
- 'bash', '-c', <<~SH], err: '/dev/null', &:read)
368
- _init_completion(){ cur=${COMP_WORDS[COMP_CWORD]}; prev=${COMP_WORDS[COMP_CWORD-1]}; }
369
- source /usr/share/bash-completion/completions/#{cmd} 2>/dev/null || exit
370
- COMP_WORDS=($COMP_LINE) COMP_CWORD=$(( ${#COMP_WORDS[@]} - 1 ))
371
- Fn=$(complete -p #{cmd} 2>/dev/null | awk '{print $(NF-1)}')
372
- eval "$Fn" 2>/dev/null && printf '%s\\n' "${COMPREPLY[@]}"
373
- SH
374
- items = items.split("\n").reject(&:empty?).uniq
375
- if items.any?
376
- grid = Grid.new(left: left, top: [top - items.size - 3, 0].max,
377
- input: items.map{ |s| [s] }, border: :light,
378
- colors: { columns: [[:blue, :default]] })
379
- picked = grid.pick(:row, column: 0)
380
- redraw&.call
381
- $>.print CURSOR_NORMAL
382
- if picked.is_a?(Integer)
383
- word_start = cursor
384
- word_start -= 1 while word_start > 0 and query[word_start - 1] != ' '
385
- query[word_start...cursor] = items[picked]
386
- cursor = word_start + items[picked].length
471
+ items = Array(completions.call( query[0...cursor] ))
472
+ if items.any?
473
+ fields = {}
474
+ input = items.each_with_index.map do |item, index|
475
+ if item.is_a?(Array)
476
+ fields[[index, 0]] = item[1]
477
+ item[0]
478
+ else
479
+ item
387
480
  end
388
481
  end
482
+ grid = Grid.new(left: left, top: [top - input.size - 3, 0].max,
483
+ input: input.map{ |s| [s] }, border: :light,
484
+ colors: { columns: [[:blue, :default]], fields: fields })
485
+ picked = grid.pick(:row, column: 0)
486
+ redraw&.call
487
+ $>.print CURSOR_NORMAL
488
+ if picked.is_a?(Integer)
489
+ word_start = cursor
490
+ word_start -= 1 while word_start > 0 and query[word_start - 1] != ' '
491
+ query[word_start...cursor] = input[picked]
492
+ cursor = word_start + input[picked].length
493
+ end
389
494
  end
390
495
  end
391
496
  end
data/lib/text.rb CHANGED
@@ -33,9 +33,14 @@ module Typr
33
33
  # text << "appended line" # alias for build
34
34
 
35
35
  def build input=nil
36
- ( input, @data, @lines = @data, "", [0] ) if rebuild = !input
36
+ ( input, @data, @lines = sanitize(@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]]) }
39
+ @data = rows.join
40
+ off = [ 0 ]; rows.each{ |row| off << off.last + row.length }
41
+ @lines = off
38
42
  str = input.respond_to?(:read) ? input.read : input.to_s
43
+ str = str.gsub("\t", " " * @tabspace)
39
44
  @tail = ""
40
45
  pos = 0
41
46
  while pos < str.size || !@tail.empty?
@@ -80,6 +85,14 @@ module Typr
80
85
  end
81
86
  end
82
87
  @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
83
96
  highlight if @re
84
97
  end
85
98
  alias :<< :build
@@ -109,7 +122,8 @@ module Typr
109
122
  # text.positions(2) # => [0, 6, 10, ...]
110
123
 
111
124
  def positions row
112
- [0] + @page[row-1].chars.map.with_index{|c,i| i+1 if c == @separator}.compact
125
+ [0] + sanitize(@page[row-1]).chars.
126
+ map.with_index{ |c, i| i + 1 if c == @separator }.compact
113
127
  end
114
128
 
115
129
  # Full draw cycle: rebuilds on resize, computes visible lines,
@@ -197,7 +211,15 @@ module Typr
197
211
  @data, @lines, @tail = "", [0], ""
198
212
  @search = { pattern: nil, dir: :forward, last: nil, matches: [] }
199
213
  super args
200
- @colors = { header: :yellow, search: :grey20 }.merge( @colors || {} )
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?
201
223
  @keymap = { search: ?/, search_next: ?n, search_prev: ?p }.merge @keymap
202
224
  @lastwidth = width
203
225
  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.5.1"
6
- s.date = "2026-07-25"
5
+ s.version = "1.6.1"
6
+ s.date = "2026-10-02"
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.5.1
4
+ version: 1.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kilian Reitmayr
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-07-25 00:00:00.000000000 Z
10
+ date: 2026-10-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: unicode-display_width
@@ -37,8 +37,6 @@ files:
37
37
  - example/text
38
38
  - example/top500.csv
39
39
  - lib/browser.rb
40
- - lib/frontend.rb
41
- - lib/graphical.rb
42
40
  - lib/grid.rb
43
41
  - lib/line.rb
44
42
  - lib/space.rb
data/lib/frontend.rb DELETED
@@ -1,23 +0,0 @@
1
- module Visuals
2
-
3
- COLORS = {
4
- black: [ 0, 0, 0],
5
- red: [ 180, 100, 100],
6
- green: [ 100, 180, 100],
7
- yellow: [ 180, 180, 100],
8
- blue: [ 100, 100, 180],
9
- magenta: [ 180, 100, 180],
10
- cyan: [ 100, 180, 180],
11
- white: [ 255, 255, 255],
12
- grey: [ 158, 158, 158],
13
- dark_red: [ 90, 30, 30],
14
- dark_green: [ 30, 90, 30],
15
- dark_yellow: [ 90, 80, 30],
16
- dark_blue: [ 30, 30, 90],
17
- dark_magenta: [ 90, 30, 90],
18
- dark_cyan: [ 30, 80, 80] }
19
-
20
- COLORS.merge! 1.upto(9).map{ |i|
21
- ["grey#{ i*10 }".to_sym, [i*25]*3 ] }.to_h
22
-
23
- end
data/lib/graphical.rb DELETED
@@ -1,121 +0,0 @@
1
- require 'sdl3'
2
- require_relative 'frontend'
3
-
4
- module Visuals
5
- include SDL2
6
-
7
- SDL2.init(SDL2::INIT_VIDEO)
8
- TTF.init
9
- WINDOW = Window.create("visuals",
10
- Window::POS_CENTERED,
11
- Window::POS_CENTERED,
12
- 1324, 768, 0)
13
- GO = WINDOW.create_renderer(-1,
14
- Renderer::Flags::ACCELERATED|Renderer::Flags::TARGETTEXTURE)
15
- Key.constants.each{ |c| c = c.to_s
16
- eval 'KEY_'+c.upcase+'=Key::'+c }
17
-
18
- def self.width; WINDOW.size[0] / $charsize[1] end
19
- def self.height; WINDOW.size[1] / $charsize[0] end
20
- def self.row; $row end
21
- def self.column; $col end
22
- def self.x; $x end
23
- def self.y; $y end
24
-
25
- def color_for c
26
- return unless c
27
- c = COLORS[c] if c.is_a?( Symbol )
28
- c.map{ |c| (c/1000.0*256).to_i }
29
- end
30
-
31
- def get_background; $color[1] end
32
- def get_foreground; $color[0] end
33
- def background color; return unless color
34
- $color[1] = color_for( color ) end
35
- def foreground color;
36
- $color[0] = (color ? color_for( color ) : $default[0]) end
37
-
38
- def color c
39
- c = [c] unless c.is_a? Array and c.count == 2
40
- foreground c[0]
41
- background c[1]
42
- end
43
-
44
- def move col, row
45
- $x = ($col = col) * $charsize[1]
46
- $y = ($row = row) * $charsize[0]
47
- end
48
-
49
- def clear type = :screen
50
- GO.draw_color = $default[1]
51
- case type
52
- when :screen; GO.fill_rect Rect.new(0, 0, *WINDOW.size)
53
- when :line; GO.fill_rect Rect.new(
54
- Visuals.x, Visuals.y, WINDOW.size[0], Visuals.y+$charsize[0])
55
- when :down; GO.fill_rect Rect.new(
56
- 0, Visuals.y, *WINDOW.size)
57
- end
58
- end
59
-
60
- def draw( str )
61
- return unless str
62
- if get_background
63
- GO.draw_color = get_background
64
- GO.fill_rect Rect.new($x, $y, str.size*$charsize[1], $charsize[0])
65
- end
66
- text = str.rstrip
67
- unless text.empty?
68
- image = $font.render_blended( text, get_foreground )
69
- GO.copy GO.create_texture_from( image ), nil,
70
- Rect.new($x, $y, image.w, $charsize[0])
71
- end
72
- move $col+str.size, $row
73
- end
74
-
75
- def refresh; GO.present end
76
-
77
- def read input
78
- case input
79
- when :event then e = Event.poll until e
80
- when :key then
81
- e = Event.poll until e.is_a? Event::KeyDown
82
- return e.sym.between?(32, 126) ? e.sym.chr : e.sym
83
- when :line
84
- str = ''
85
- loop do
86
- e = Event.poll
87
- next unless e.is_a? Event::KeyDown
88
- return nil if e.sym == KEY_ESCAPE
89
- return str if e.sym == KEY_RETURN
90
- str << e.sym.chr if e.sym.between? 32, 126
91
- end
92
- end
93
- end
94
-
95
- def self.exit
96
- end
97
-
98
- def self.init args={}
99
- $default = [
100
- color_for( args[:foreground] || :grey80 ),
101
- color_for( args[:background] || :grey10 ) ]
102
- $color = $default.dup
103
- $charsize = args[:charsize] || [18,11]
104
- $font = TTF.open( args[:font] || "/usr/share/fonts/X11/TTF/VeraMono.ttf", $charsize[0] )
105
- move 0, 0
106
- end
107
-
108
- end
109
-
110
- if __FILE__ == $0
111
- include Visuals
112
- Visuals.init font:ARGV[0]
113
- characters = (32..126).map(&:chr).join
114
- loop do
115
- clear
116
- COLORS.keys.each_with_index{ |c,i|
117
- foreground c; move 0,i; draw characters }
118
- refresh
119
- exit if read(:key) == KEY_ESCAPE
120
- end
121
- end