typr 1.5.1 → 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 +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +16 -10
- data/lib/browser.rb +59 -35
- data/lib/grid.rb +13 -7
- data/lib/line.rb +60 -3
- data/lib/space.rb +1 -1
- data/lib/stack.rb +2 -0
- data/lib/terminal.rb +123 -28
- data/lib/text.rb +25 -3
- data/typr.gemspec +2 -2
- metadata +2 -4
- data/lib/frontend.rb +0 -23
- data/lib/graphical.rb +0 -121
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3fc328e46b4b4d36e0d3f68eb0a50f8068934469ec5e9d5d84099755685732ad
|
|
4
|
+
data.tar.gz: 254c5a5842474b4fc550a0aff5fbaa772e961a6aca9a9a8f90ed1a89cc761e51
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eeae3cd67ce492706bc952e8422adea09d300f115928c074411c2589f5dae577ff4ad73e610e6a9b279007a93205e81fa9579c59d3f404f2778f52b06cb26e24
|
|
7
|
+
data.tar.gz: 735a2236dbd0868674f4727572f578237f34165ab04d683179dd45464918099719c422503b386915f2df4895f1d5228550e6ee9bc25a9bf607d650eff8e8153c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
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
|
+
|
|
3
27
|
## [v1.5.1] — 2026-09-11
|
|
4
28
|
|
|
5
29
|
## [v1.5.0] — 2026-09-05
|
|
@@ -24,7 +48,10 @@
|
|
|
24
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
|
|
25
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
|
|
26
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)
|
|
27
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
|
|
28
55
|
|
|
29
56
|
## [v1.4.0] — 2026-08-15
|
|
30
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
|
-
|
|
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
|
-
|
|
39
|
-
- **Graphical** (`require 'typr/graphical'`) — experimental: SDL2-based windows
|
|
38
|
+
### Frontends
|
|
40
39
|
|
|
41
|
-
|
|
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 "/"
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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|
|
|
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
|
|
120
|
-
#
|
|
121
|
-
# in
|
|
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" # =>
|
|
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
|
-
|
|
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;
|
|
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]+ | {
|
|
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] = {
|
|
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,
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
491
|
-
|
|
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][:
|
|
502
|
-
@colors[:actions][:
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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
|
|
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(
|
|
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
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
|
|
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,24 @@ 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
|
+
# 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
|
|
233
257
|
|
|
234
258
|
# Read the current background/foreground color pair.
|
|
235
259
|
def get_background; $color[1] end
|
|
@@ -260,6 +284,14 @@ module Typr
|
|
|
260
284
|
# input is unavailable.
|
|
261
285
|
|
|
262
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
|
|
263
295
|
read = ->(tty) do
|
|
264
296
|
str = tty.sysread 6
|
|
265
297
|
if str.start_with?("\e[M") and str.size < 6
|
|
@@ -303,6 +335,43 @@ module Typr
|
|
|
303
335
|
Mouse.new button, x, y, action, modifiers
|
|
304
336
|
end
|
|
305
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
|
+
|
|
306
375
|
## Interactive line editor used for string prompts (search, %str, ...).
|
|
307
376
|
#
|
|
308
377
|
# Renders +prompt+ and the query at (left, top) and edits it with
|
|
@@ -312,14 +381,24 @@ module Typr
|
|
|
312
381
|
# editor and becomes its return value. When stdin is not a tty, the
|
|
313
382
|
# query is read from a single line of piped input instead.
|
|
314
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
|
+
#
|
|
315
393
|
# Typr.read_line "/" do |key, query, cursor|
|
|
316
394
|
# filter query
|
|
317
395
|
# nil
|
|
318
396
|
# end
|
|
319
397
|
|
|
320
|
-
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
|
|
321
399
|
return $stdin.gets&.chomp unless $stdin.tty?
|
|
322
400
|
query, cursor = initial.dup, initial.length
|
|
401
|
+
hist_idx = nil
|
|
323
402
|
$>.print CURSOR_NORMAL
|
|
324
403
|
begin
|
|
325
404
|
loop do
|
|
@@ -327,7 +406,8 @@ module Typr
|
|
|
327
406
|
$>.print ERASE_LINE
|
|
328
407
|
before = text_width( prompt + query[0...cursor] )
|
|
329
408
|
offset = [ left + before - Typr.width, 0 ].max
|
|
330
|
-
|
|
409
|
+
display = prompt + paint( query, regex, prompt[/\e\[[0-9;]*m\z/].to_s )
|
|
410
|
+
$>.print slice_width( display, offset, Typr.width - left + 1 )
|
|
331
411
|
$>.print "\e[%i;%if" % [ top + 1, left + 1 + before - offset ]
|
|
332
412
|
key = read_key
|
|
333
413
|
return nil if key.nil? or key == KEY_ESCAPE
|
|
@@ -339,6 +419,24 @@ module Typr
|
|
|
339
419
|
when KEY_ALT_RIGHT, "\eC"; cursor = word_next query, cursor
|
|
340
420
|
when KEY_HOME; cursor = 0
|
|
341
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
|
|
342
440
|
when KEY_DC, "\e[3~"; query.slice!(cursor, 1) if cursor < query.length
|
|
343
441
|
when KEY_ALT_BACKSPACE
|
|
344
442
|
old = cursor
|
|
@@ -360,32 +458,29 @@ module Typr
|
|
|
360
458
|
result = block.call( key, query, cursor ) if block
|
|
361
459
|
return result unless result.nil?
|
|
362
460
|
if completions and key == KEY_TAB
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
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
|
|
387
470
|
end
|
|
388
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
|
|
389
484
|
end
|
|
390
485
|
end
|
|
391
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.
|
|
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
|
-
|
|
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
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.
|
|
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-
|
|
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
|