typr 1.5.0 → 1.6.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: 6f464a51f6c8356a1571fcebe51bbe315428ec054d9b1b819753a688764ce347
4
- data.tar.gz: '0367917e4ce7b2f67c0605bb321f358aa81e17f27ab555ab558a6655b646dc9a'
3
+ metadata.gz: 3fc328e46b4b4d36e0d3f68eb0a50f8068934469ec5e9d5d84099755685732ad
4
+ data.tar.gz: 254c5a5842474b4fc550a0aff5fbaa772e961a6aca9a9a8f90ed1a89cc761e51
5
5
  SHA512:
6
- metadata.gz: 6f3f59d78bfc8831542c843de4eb51aa1507774b95436f97d9cf906f12945808f25c1add663945ad3d94ba8f33a543bc8f2cb960eac2adf995e6c6ec62fb715e
7
- data.tar.gz: bf0a175749a29714fa07e0e8124b72a031dd9606c7d077cdcf7f214dc81783d28a2cce6b0e265afbbee9155a82cef742916c0a4956522c3edf56b66b6c223bbd
6
+ metadata.gz: eeae3cd67ce492706bc952e8422adea09d300f115928c074411c2589f5dae577ff4ad73e610e6a9b279007a93205e81fa9579c59d3f404f2778f52b06cb26e24
7
+ data.tar.gz: 735a2236dbd0868674f4727572f578237f34165ab04d683179dd45464918099719c422503b386915f2df4895f1d5228550e6ee9bc25a9bf607d650eff8e8153c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [vx.y.z] — __DATE__
4
+
5
+ ## [v1.6.0] — 2026-09-30
6
+
7
+ ### Added
8
+ - `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
9
+ - 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
10
+ - `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`
11
+ - `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)
12
+ - `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`
13
+
14
+ ### Changed
15
+ - 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)
16
+ - 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
17
+ - `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
18
+ - `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)
19
+ - 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
20
+ - 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
21
+ - `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
22
+ - 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
23
+
24
+ ### Fixed
25
+ - Editing recalled command-line history no longer fails on frozen strings and restores the colored prompt when returning to the recalled entry
26
+
27
+ ## [v1.5.1] — 2026-09-11
28
+
3
29
  ## [v1.5.0] — 2026-09-05
4
30
 
5
31
  ### Added
@@ -21,6 +47,11 @@
21
47
  - `Text#build` wraps ANSI-colored content correctly — the capacity calculation uses display width instead of byte length, so escape sequences don't inflate the line count
22
48
  - `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
23
49
  - 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
50
+ - `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)
51
+ - 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)
52
+ - `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
53
+ - 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)
54
+ - 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
24
55
 
25
56
  ## [v1.4.0] — 2026-08-15
26
57
 
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,15 +54,28 @@ 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 )
62
+ begin
63
+ path = File.expand_path( dir )
64
+ Dir.chdir( path )
65
+ entries = Dir.new( path ).children
66
+ rescue SystemCallError
67
+ @user.show "cannot open directory: #{dir}"
68
+ Typr.read_key
69
+ return false
70
+ end
61
71
  @colors[:fields] = {}
62
72
  clear
63
- Dir.chdir( @directory = File.expand_path( dir ) )
73
+ @directory = path
74
+ else
75
+ Dir.chdir( dir )
76
+ entries = Dir.new( dir ).children
64
77
  end
65
- data = Dir.new(@directory).children.reject{|f|
78
+ data = entries.reject{|f|
66
79
  !@show_hidden and f[0] == ?. }.map { |file| scan_file file }
67
80
 
68
81
  data.each_with_index{ |row, id|
@@ -92,36 +105,44 @@ module Typr
92
105
  next unless file
93
106
  data[ missing[file] ][TYPE, 2] = mime.split( ?/ ) if missing[file] }
94
107
  end
95
- 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
+ }
96
112
  self << data
97
113
  sort
98
114
  filter
99
115
  unless append
100
116
  @directory += ?/ unless @directory == ?/
101
117
  change_view @view
102
- reset :position
118
+ reset :position unless same
103
119
  end
104
120
  return
105
121
  end
106
122
 
107
123
  ##
108
- # Build a row for a single directory entry from filesystem metadata only.
109
- # MIME type detection (extension database and +file --mime-type+) happens
110
- # in a batch pass inside {#cd}; type/subtype are left as +"?"+ here.
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.
111
129
  #
112
- # browser.scan_file "image.png" # => 12-column row
130
+ # browser.scan_file "image.png" # => 14-column row
113
131
 
114
132
  def scan_file file
115
133
  target = nil
116
134
  if File.symlink? file
117
135
  target = File.readlink( file ) rescue $!.to_s
118
- 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)
119
137
  end
120
138
  stat = File.stat(file) rescue nil
121
- return [file, "", "????", false, "?", "?", 0, 0, 0, 0, "?", "?", nil] unless stat
122
- [ 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|
123
144
  "rwx".chars.map.with_index{|c,i| d.to_i[2-i]==0 ? ?- : c } }.join,
124
- ( File.executable?(file) and stat.file? ), stat.uid, stat.gid, stat.size,
145
+ ( File.executable?(file) and stat.file? ), printable, stat.uid, stat.gid, stat.size,
125
146
  stat.atime.to_i, stat.mtime.to_i, stat.ctime.to_i, "?", "?", nil ]
126
147
  end
127
148
 
@@ -148,10 +169,7 @@ module Typr
148
169
  return unless view
149
170
  view = @views.keys[ view ] if view.is_a? Integer
150
171
  @sequence = @views[ view ].map{|title| @header.index title.to_s}
151
- if @show_path
152
- @data.each { |row| row[PATH] = File.dirname(row[NAME]) } if @data.any?
153
- @sequence.insert(@sequence.index(NAME) || 0, PATH) unless @sequence.include?(PATH)
154
- end
172
+ @sequence.insert(0, PATH) if @show_path
155
173
  reset :format
156
174
  @view = view
157
175
  return
@@ -249,7 +267,7 @@ module Typr
249
267
  #
250
268
  # browser.reset_view
251
269
 
252
- def reset_view; cd @directory; reset [:display, :selection, :position]; sort end
270
+ def reset_view; reset [:display, :selection]; cd @directory; sort end
253
271
 
254
272
  ## Open a filter popup on the chosen row/column and apply the input.
255
273
  #
@@ -404,7 +422,7 @@ module Typr
404
422
  top: top, align: [:right], input:@header,
405
423
  left: left + ( @positions[args[:type]] || 0 ) -
406
424
  ( args[:input] || @header ).map( &:size ).max - 1,
407
- colors:{ columns: [ @colors[:actions][args[:type]] ] } }.merge args )
425
+ colors:{ columns: [ @colors[:actions][args[:type].to_s.gsub(/[\[\]]/, '').downcase.to_sym] ] } }.merge args )
408
426
  end
409
427
 
410
428
  ##
@@ -415,7 +433,7 @@ module Typr
415
433
  # +directory+ | +"~"+ | Starting directory to browse
416
434
  # +mimetype_db+ | true | Load MIME from file extensions
417
435
  # +mimetype_magic+ | true | Use +file --mime-type+ for unknowns
418
- # +colors[:actions]+ | { "[f]ilter": :red, ... } | Status bar item colors
436
+ # +colors[:actions]+ | { filter: :red, ... } | Status bar item colors
419
437
  # +colors[:dir_history]+ | :yellow | History picker column color
420
438
  # +colors[:types]+ | (see below) | Per MIME-type foreground color
421
439
  #
@@ -434,7 +452,7 @@ module Typr
434
452
  @left ||= 0
435
453
  @top ||= 0
436
454
  @directory ||= ?~
437
- @colors[:actions] = { "[f]ilter": :red, "[s]ort": :blue, "[v]iews": :green
455
+ @colors[:actions] = { filter: :red, sort: :blue, views: :green
438
456
  }.merge @colors[:actions] || {}
439
457
  @colors[ :types ] = {
440
458
  image: :blue,
@@ -449,13 +467,11 @@ module Typr
449
467
  'x-msdownload': :red, 'x-pie-executable': :red
450
468
  }.merge( @colors[ :types ] || {} )
451
469
 
452
- @keymap = { back: KEY_BACKSPACE, confirm: KEY_RETURN, help: ?h,
453
- home: ?~, views: ?v, sort_view: ?s, search: ?/,
454
- mark: ?m, mark_range: ?M, mark_all: ?a, mark_invert: ?i,
455
- filter_view: ?f, reset_view: ?r,
456
- 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
457
473
 
458
- self.header = %w[ name target permissions executable owner group size
474
+ self.header = %w[ name target permissions executable printable owner group size
459
475
  accessed modified created type subtype path ]
460
476
 
461
477
  @format[NAME] = :max
@@ -463,9 +479,11 @@ module Typr
463
479
  @ignore[NAME] = @ignore[TARGET] = true
464
480
  @hints = "1234567890"
465
481
  [NAME, SIZE, ACCESSED, MODIFIED, CREATED].each{|col| @rawsort[col] = true}
482
+ @rawsort[PRINTABLE] = @rawsort[EXECUTABLE] = false
466
483
 
467
484
  @procs[ACCESSED] = @procs[MODIFIED] = @procs[CREATED] = :datetime
468
485
  @procs[SIZE] = :magnitudes
486
+ @procs[EXECUTABLE] = @procs[PRINTABLE] = ->(v) { v ? "yes" : "no" }
469
487
  @procs[OWNER] = Proc.new{|uid| Etc.getpwuid(uid).name rescue uid }
470
488
  @procs[GROUP] = Proc.new{|gid| Etc.getgrgid(gid).name rescue gid }
471
489
 
@@ -476,8 +494,8 @@ module Typr
476
494
  @views = { simple: %i[ name ],
477
495
  compact: %i[ name size type],
478
496
  stats: %i[ name size permissions owner group modified ],
479
- full: %i[ name size permissions executable owner group
480
- 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
481
499
  @parent = self
482
500
  @head ||= Grid.new( parent:self, left: ->{@parent.left}, margin: '',
483
501
  right: ->{@parent.width + @parent.left - 1}, top: ->{@parent.top-1},
@@ -487,8 +505,8 @@ module Typr
487
505
  "#{s} #{@parent.reverse ? "\u2193" : "\u2191"}"},
488
506
  ->{@parent.view} ] ], format: [:min, :max, :min, :min, :min],
489
507
  colors: { columns: [@colors[:default], @colors[:default],
490
- @colors[:actions][:"[f]ilter"], @colors[:actions][:"[s]ort"],
491
- @colors[:actions][:"[v]iews"]] } )
508
+ @colors[:actions][:filter], @colors[:actions][:sort],
509
+ @colors[:actions][:views]] } )
492
510
  @head.define_singleton_method( :data_at ) do |x, y|
493
511
  return unless x and y and y.between?( top + 1, bottom + 1 ) and
494
512
  x.between?( left, right )
@@ -511,11 +529,28 @@ module Typr
511
529
  ( pre + ( pre.end_with?( "/" ) ? "" : text[pre.length..-1][/\A[^\/]*/] ) ).sub( %r{(?<=.)/$}, "" )
512
530
  end
513
531
 
514
- @user ||= Line.new( parent:self, left:->{@parent.left},top:->{@parent.bottom+1},
515
- bindings: %w[ [h]elp [/]:search ] + @colors[:actions].map{|name,color|
516
- color_code( color ) + name.to_s + color_code(@colors[:default]) } +
517
- %w[ [r]eset [d]irectories [m]ark [M]ark_range mark_[a]ll
518
- [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
519
554
  cd @directory
520
555
  end
521
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
 
@@ -76,6 +80,7 @@ module Typr
76
80
  def left?; button == 0 end
77
81
  def middle?; button == 1 end
78
82
  def right?; button == 2 end
83
+ def alt?; ( modifiers & 8 ) != 0 end
79
84
  def to_s; "%s button=%i x=%i y=%i%s" % [ action, button, x, y,
80
85
  ( modifiers > 0 ? " modifiers=#{modifiers}" : "" ) ] end
81
86
  end
@@ -207,7 +212,7 @@ module Typr
207
212
  # Apply a text attribute (+mode+); mode_code returns the escape sequence
208
213
  # without writing it.
209
214
  def mode name; draw mode_code(name) end
210
- 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
211
216
 
212
217
  # Build the ANSI code for +color+ (a Symbol name, greyN, Integer palette id,
213
218
  # or [r,g,b] / [fg,bg] Array), optionally as a background; when false the
@@ -216,7 +221,9 @@ module Typr
216
221
  color = color.to_sym if color.is_a? String
217
222
  case color
218
223
  when Symbol;
219
- 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"
220
227
  elsif color[/^gr[ae]y\d{,2}$/]
221
228
  "\e[%i;5;%im" % [bg ? 48 : 38, 232 + (color[/\d+/].to_f/100*23).to_i]
222
229
  elsif id = COLOR_MAP[color]
@@ -229,6 +236,24 @@ module Typr
229
236
  end
230
237
  end
231
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
+ # Typr.paint "fix WARN now", { /WARN/ => :yellow }, "\e[39;49m"
251
+ def self.paint str, regex, base
252
+ return str if regex.nil? or regex.empty?
253
+ str.gsub( Regexp.union( regex.keys ) ) { |m|
254
+ color = regex[ regex.keys.find{ |k| k === m } ]
255
+ color_code( color ).to_s + m + base }
256
+ end
232
257
 
233
258
  # Read the current background/foreground color pair.
234
259
  def get_background; $color[1] end
@@ -259,6 +284,14 @@ module Typr
259
284
  # input is unavailable.
260
285
 
261
286
  def self.read_key
287
+ if quiet_window?
288
+ loop do
289
+ remaining = @suppress_until - Process.clock_gettime(Process::CLOCK_MONOTONIC)
290
+ break if remaining <= 0
291
+ break unless IO.select([INPUT], nil, nil, remaining)
292
+ begin INPUT.read_nonblock(1024) rescue break end
293
+ end
294
+ end
262
295
  read = ->(tty) do
263
296
  str = tty.sysread 6
264
297
  if str.start_with?("\e[M") and str.size < 6
@@ -302,6 +335,43 @@ module Typr
302
335
  Mouse.new button, x, y, action, modifiers
303
336
  end
304
337
 
338
+ ## Discard any bytes already waiting on the input stream, so input confirmed
339
+ # at a previous prompt cannot auto-answer the next one (e.g. a double-tapped
340
+ # Enter confirming a destructive default). Best-effort: never raises.
341
+ #
342
+ # Typr.drain_input # => nil
343
+
344
+ def self.drain_input
345
+ while IO.select([INPUT], nil, nil, 0)
346
+ begin
347
+ INPUT.read_nonblock(1024)
348
+ rescue IO::WaitReadable
349
+ break
350
+ end
351
+ end
352
+ nil
353
+ rescue StandardError
354
+ nil
355
+ end
356
+
357
+ # Post-auto-confirm input guard: keys within OVERTYPE_QUIET seconds of an
358
+ # auto-selected pick are discarded — in-app via +read_key+, and before a
359
+ # subprocess handoff via +flush_overtype+.
360
+ OVERTYPE_QUIET = 0.5
361
+
362
+ def self.suppress_keys timeout=OVERTYPE_QUIET
363
+ @suppress_until = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
364
+ end
365
+
366
+ def self.quiet_window?
367
+ @suppress_until and $stdin.tty? and
368
+ @suppress_until > Process.clock_gettime(Process::CLOCK_MONOTONIC)
369
+ end
370
+
371
+ def self.flush_overtype
372
+ drain_input if quiet_window?
373
+ end
374
+
305
375
  ## Interactive line editor used for string prompts (search, %str, ...).
306
376
  #
307
377
  # Renders +prompt+ and the query at (left, top) and edits it with
@@ -311,14 +381,24 @@ module Typr
311
381
  # editor and becomes its return value. When stdin is not a tty, the
312
382
  # query is read from a single line of piped input instead.
313
383
  #
384
+ # +completions+ may be a callable matching the current word to a list
385
+ # of candidates; TAB then replaces the word through a pick grid. Each
386
+ # candidate is either a plain String or [text, color-pair] to tint the
387
+ # pick entry. The callable's content is arbitrary - shells, tags, ...
388
+ #
389
+ # +regex+ is a Hash of Regexp => color (see {.paint}) applied to the
390
+ # query as it is rendered; matched regions restore the prompt's trailing
391
+ # color code.
392
+ #
314
393
  # Typr.read_line "/" do |key, query, cursor|
315
394
  # filter query
316
395
  # nil
317
396
  # end
318
397
 
319
- def self.read_line prompt='', left: 0, top: 0, initial: '', completions: nil, redraw: nil, &block
398
+ def self.read_line prompt='', left: 0, top: 0, initial: '', completions: nil, redraw: nil, regex: nil, history: nil, &block
320
399
  return $stdin.gets&.chomp unless $stdin.tty?
321
400
  query, cursor = initial.dup, initial.length
401
+ hist_idx = nil
322
402
  $>.print CURSOR_NORMAL
323
403
  begin
324
404
  loop do
@@ -326,7 +406,8 @@ module Typr
326
406
  $>.print ERASE_LINE
327
407
  before = text_width( prompt + query[0...cursor] )
328
408
  offset = [ left + before - Typr.width, 0 ].max
329
- $>.print slice_width( prompt + query, offset, Typr.width - left + 1 )
409
+ display = prompt + paint( query, regex, prompt[/\e\[[0-9;]*m\z/].to_s )
410
+ $>.print slice_width( display, offset, Typr.width - left + 1 )
330
411
  $>.print "\e[%i;%if" % [ top + 1, left + 1 + before - offset ]
331
412
  key = read_key
332
413
  return nil if key.nil? or key == KEY_ESCAPE
@@ -338,6 +419,24 @@ module Typr
338
419
  when KEY_ALT_RIGHT, "\eC"; cursor = word_next query, cursor
339
420
  when KEY_HOME; cursor = 0
340
421
  when KEY_END; cursor = query.length
422
+ when KEY_UP, "\eOA";
423
+ if history&.any?
424
+ hist_idx = [((hist_idx || -1) + 1), history.size - 1].min
425
+ query = history[hist_idx].dup
426
+ cursor = query.length
427
+ end
428
+ when KEY_DOWN, "\eOB";
429
+ if history&.any? && hist_idx
430
+ hist_idx -= 1
431
+ if hist_idx < 0
432
+ query = +''
433
+ cursor = 0
434
+ hist_idx = nil
435
+ else
436
+ query = history[hist_idx].dup
437
+ cursor = query.length
438
+ end
439
+ end
341
440
  when KEY_DC, "\e[3~"; query.slice!(cursor, 1) if cursor < query.length
342
441
  when KEY_ALT_BACKSPACE
343
442
  old = cursor
@@ -359,32 +458,29 @@ module Typr
359
458
  result = block.call( key, query, cursor ) if block
360
459
  return result unless result.nil?
361
460
  if completions and key == KEY_TAB
362
- raw = query[0...cursor]
363
- cmd = raw[/\w+/]
364
- if cmd
365
- items = IO.popen([{ 'COMP_LINE' => raw, 'COMP_POINT' => raw.bytesize.to_s },
366
- 'bash', '-c', <<~SH], err: '/dev/null', &:read)
367
- _init_completion(){ cur=${COMP_WORDS[COMP_CWORD]}; prev=${COMP_WORDS[COMP_CWORD-1]}; }
368
- source /usr/share/bash-completion/completions/#{cmd} 2>/dev/null || exit
369
- COMP_WORDS=($COMP_LINE) COMP_CWORD=$(( ${#COMP_WORDS[@]} - 1 ))
370
- Fn=$(complete -p #{cmd} 2>/dev/null | awk '{print $(NF-1)}')
371
- eval "$Fn" 2>/dev/null && printf '%s\\n' "${COMPREPLY[@]}"
372
- SH
373
- items = items.split("\n").reject(&:empty?).uniq
374
- if items.any?
375
- grid = Grid.new(left: left, top: [top - items.size - 3, 0].max,
376
- input: items.map{ |s| [s] }, border: :light,
377
- colors: { columns: [[:blue, :default]] })
378
- picked = grid.pick(:row, column: 0)
379
- redraw&.call
380
- $>.print CURSOR_NORMAL
381
- if picked.is_a?(Integer)
382
- word_start = cursor
383
- word_start -= 1 while word_start > 0 and query[word_start - 1] != ' '
384
- query[word_start...cursor] = items[picked]
385
- cursor = word_start + items[picked].length
461
+ items = Array(completions.call( query[0...cursor] ))
462
+ if items.any?
463
+ fields = {}
464
+ input = items.each_with_index.map do |item, index|
465
+ if item.is_a?(Array)
466
+ fields[[index, 0]] = item[1]
467
+ item[0]
468
+ else
469
+ item
386
470
  end
387
471
  end
472
+ grid = Grid.new(left: left, top: [top - input.size - 3, 0].max,
473
+ input: input.map{ |s| [s] }, border: :light,
474
+ colors: { columns: [[:blue, :default]], fields: fields })
475
+ picked = grid.pick(:row, column: 0)
476
+ redraw&.call
477
+ $>.print CURSOR_NORMAL
478
+ if picked.is_a?(Integer)
479
+ word_start = cursor
480
+ word_start -= 1 while word_start > 0 and query[word_start - 1] != ' '
481
+ query[word_start...cursor] = input[picked]
482
+ cursor = word_start + input[picked].length
483
+ end
388
484
  end
389
485
  end
390
486
  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.0"
6
- s.date = "2026-07-25"
5
+ s.version = "1.6.0"
6
+ s.date = "2026-09-30"
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.0
4
+ version: 1.6.0
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-09-30 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