typr 1.2.0 → 1.3.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 +8 -0
- data/README.md +64 -33
- data/lib/browser.rb +23 -16
- data/lib/space.rb +11 -0
- data/lib/stack.rb +25 -1
- data/lib/terminal.rb +102 -14
- data/lib/text.rb +7 -0
- data/lib/typr.rb +3 -3
- data/share/terminfo +691 -0
- data/typr.gemspec +4 -3
- metadata +2 -2
- data/lib/curses.rb +0 -124
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cdcaef4b1bc2c40e6fab6bd7d1462ddf19aeca03babcdbb9be74629551230647
|
|
4
|
+
data.tar.gz: b4e927b5237f27e2366ed15057054b33d9f070ef5a50c234533699182c7228ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 889fd403e345e89dc6777c02c6736d0865b7266b16157e13e4cf2469b9a3664d68f4fe672d1f9b5b6d72316d697c2c8e20e1f920853ae495014c9fb049fed6cd
|
|
7
|
+
data.tar.gz: 662df0d3ef61d964cd8a03a1cd1f7caa7fb14a15d87c22afa3a2cdf9115f4780b944446ebcdc9f538c33ee80753e48c362fea7f716d2ef7fca2070f07fe5483f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v1.3.0] — 2026-08-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Mouse support — `Typr.init`/`Typr.exit` enable/disable mouse reporting (buttons + SGR coordinates, `CSI ? 1000 h/l` + `CSI ? 1006 h/l`); `read_key` decodes SGR and legacy X10 reports into a `Typr::Mouse` struct (`button`, `x`, `y`, `action`, `modifiers`) with `press?`/`release?`/`wheel?`/`left?`/`middle?`/`right?` predicates
|
|
7
|
+
- `Stack#hit` — maps a 1-based terminal row/col to the data row id under the cursor (nil for header/margins/outside)
|
|
8
|
+
- Click-to-act in pickers — a left click on a row in `Stack#pick`/`Browser#pick` acts like pressing its hint digit (selects the row; in `Browser` cd's into directories and returns file paths)
|
|
9
|
+
- Wheel-to-scroll in pickers — mouse wheel up/down scrolls the listing in `Stack#pick`/`Browser#pick`
|
|
10
|
+
|
|
3
11
|
## [v1.2.0] — 2026-08-02
|
|
4
12
|
|
|
5
13
|
### Added
|
data/README.md
CHANGED
|
@@ -1,29 +1,44 @@
|
|
|
1
1
|
# typr — Terminal UI Toolkit
|
|
2
2
|
|
|
3
|
-
typr is a Ruby library for building interactive
|
|
3
|
+
typr is a Ruby library for building interactive, keyboard-driven terminal user interfaces. It is built from data-object based components: grids for arrays, a pager for text, a file picker, and lines for user interaction — each of which renders itself, handles its own keymap, and can be combined into full applications.
|
|
4
|
+
|
|
5
|
+
typr is designed for speed: every interactive choice is one keystroke away. Selectable rows and columns carry number hints, popups open in `/` search mode so long lists can be distilled down and picked from in a couple of keystrokes. It is the toolkit behind the [Filecon](https://codeberg.org/typr/filecon) file manager.
|
|
6
|
+
|
|
7
|
+
typr's terminal layer is self-contained: it ships a precompiled terminfo key table and a MIME-type database inside the gem, so it has no ncurses dependency and works on any raw, ANSI-capable terminal — including Android/Termux.
|
|
4
8
|
|
|
5
9
|
## Features
|
|
6
10
|
|
|
7
|
-
###
|
|
11
|
+
### Components
|
|
8
12
|
|
|
9
|
-
- **Grid** — sortable, filterable table with formatted columns, colors, and row selection
|
|
10
|
-
- **Text** — scrollable text viewer with word/
|
|
11
|
-
- **Browser** — file-system directory browser with MIME type
|
|
12
|
-
- **
|
|
13
|
-
- **Line** — interactive prompt/input with colored questions and answers
|
|
13
|
+
- **Grid** — sortable, filterable table with formatted columns, per-cell colors, and row selection. Columns can be `:min`/`:max`/fixed-width, with built-in processors for dates, file sizes, and file trees. `Grid#pick column:` adds live `/` search
|
|
14
|
+
- **Text** — scrollable text viewer with word/row picking, `/` search with `n`/`p` navigation and match highlighting, and borders
|
|
15
|
+
- **Browser** — file-system directory browser with MIME-type detection (bundled database, optional magic detection), per-type coloring, and an interactive picker
|
|
16
|
+
- **Line** — interactive prompt/input with colored questions and answers, configurable bindings, and strict `ask` question types (`Array`, `:key`, `:line`)
|
|
14
17
|
|
|
15
|
-
###
|
|
18
|
+
### Keyboard interaction
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
Navigation uses **hint numbers** displayed next to selectable items — rows in grids, words in text, and other elements. Press the number to select that item directly. When more items are visible than fit in one set of hints (1–9, 0), press **Tab** to cycle through hint groups. Arrow keys, Page Up/Down, Home/End work as expected, and all keys are dispatched through a configurable keymap.
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
- **Graphical** (`require 'typr/graphical'`) — SDL2-based, for desktop GUI windows
|
|
22
|
+
**Live `/` search** is built into listings and pickers: popups open in search mode, so you start typing to distill the list down to the matches, then press a number hint to pick from the few remaining results. `Text` adds less-style search (`/`, `n`, `p`) with smart-case matching and wrap-around.
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
### Mouse support
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
typr reports and decodes mouse input. `Typr.init` enables mouse reporting and `Typr.exit` disables it; `Typr.read_key` decodes SGR and legacy X10 reports into a `Typr::Mouse` struct (`button`, `x`, `y`, `action`, `modifiers`) with `press?`/`release?`/`wheel?`/`left?`/`middle?`/`right?` predicates. Pickers accept the mouse directly: a left click acts like pressing the row's hint digit, and the wheel scrolls the listing.
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
### Terminal layer
|
|
29
|
+
|
|
30
|
+
The default terminal frontend is pure ANSI on raw TTY input — **no ncurses**. Key sequences and keypad escapes are precompiled into a bundled terminfo table (`share/terminfo`), and MIME types come from a bundled database (`share/mimetypes`); both ship with the gem, so typr runs anywhere a raw, ANSI-capable terminal exists. The only runtime dependency is `unicode-display_width`.
|
|
31
|
+
|
|
32
|
+
### Colors
|
|
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, …).
|
|
35
|
+
|
|
36
|
+
### Frontends
|
|
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
|
|
40
|
+
|
|
41
|
+
All components work identically across frontends.
|
|
27
42
|
|
|
28
43
|
## Installation
|
|
29
44
|
|
|
@@ -34,7 +49,7 @@ gem install typr
|
|
|
34
49
|
Or in your Gemfile:
|
|
35
50
|
|
|
36
51
|
```ruby
|
|
37
|
-
gem 'typr', '~> 1.
|
|
52
|
+
gem 'typr', '~> 1.2'
|
|
38
53
|
```
|
|
39
54
|
|
|
40
55
|
## Usage
|
|
@@ -59,8 +74,8 @@ Typr.init
|
|
|
59
74
|
|
|
60
75
|
Typr.clear
|
|
61
76
|
@grid.show
|
|
62
|
-
key = Typr.
|
|
63
|
-
@grid.reset
|
|
77
|
+
key = Typr.read_key # reads a single keypress
|
|
78
|
+
@grid.reset # reset grid to default view
|
|
64
79
|
Typr.exit
|
|
65
80
|
```
|
|
66
81
|
|
|
@@ -82,7 +97,7 @@ Typr.init
|
|
|
82
97
|
Typr.clear
|
|
83
98
|
loop do
|
|
84
99
|
@text.show
|
|
85
|
-
key = Typr.
|
|
100
|
+
key = Typr.read_key
|
|
86
101
|
case key
|
|
87
102
|
when ?q then break
|
|
88
103
|
else @text.send(key) # spacebar scrolls, j/k move up/down, etc.
|
|
@@ -104,7 +119,7 @@ Typr.init
|
|
|
104
119
|
colors: { hints: [:white, :black] }
|
|
105
120
|
)
|
|
106
121
|
|
|
107
|
-
selected = @browser.pick
|
|
122
|
+
selected = @browser.pick # interactive file picker, returns the absolute path
|
|
108
123
|
puts "You selected: #{selected}"
|
|
109
124
|
Typr.exit
|
|
110
125
|
```
|
|
@@ -140,7 +155,7 @@ class MyApp
|
|
|
140
155
|
loop do
|
|
141
156
|
@grid.show
|
|
142
157
|
@prompt.show
|
|
143
|
-
key = Typr.
|
|
158
|
+
key = Typr.read_key
|
|
144
159
|
case key
|
|
145
160
|
when ?s then @grid.sort_by(@prompt.ask("sort column": [@grid, :column]))
|
|
146
161
|
when ?f then @grid.add_filter(*@prompt.ask("filter column": [@grid, :column], with: :line))
|
|
@@ -163,31 +178,46 @@ end
|
|
|
163
178
|
## API Reference
|
|
164
179
|
|
|
165
180
|
### `Typr.init` / `Typr.exit`
|
|
166
|
-
Initialize and tear down the terminal UI. Always call these at the start and end of your program.
|
|
181
|
+
Initialize and tear down the terminal UI (hides the cursor, enables key-mode and mouse reporting; restores them on exit). Always call these at the start and end of your program.
|
|
167
182
|
|
|
168
183
|
### `Typr.clear([scope])`
|
|
169
184
|
Clears the full screen or a single line (`:line`).
|
|
170
185
|
|
|
171
|
-
### `Typr.
|
|
172
|
-
Reads
|
|
173
|
-
|
|
174
|
-
|
|
186
|
+
### `Typr.read_key`
|
|
187
|
+
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
|
+
|
|
189
|
+
### `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:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
Typr.read_line "/" do |key, query, cursor|
|
|
194
|
+
filter query
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### `Line#ask`
|
|
200
|
+
Strict question types:
|
|
201
|
+
- `Array` — pick a row from an object/column (hint-driven)
|
|
202
|
+
- `:key` — read a single key
|
|
203
|
+
- `:line` — read a line of text
|
|
204
|
+
|
|
205
|
+
The question and answer are rendered in `@colors[:question]`/`@colors[:answer]`.
|
|
175
206
|
|
|
176
|
-
### Layout properties (all
|
|
207
|
+
### Layout properties (all components)
|
|
177
208
|
- `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)
|
|
178
209
|
|
|
179
210
|
### Grid options
|
|
180
211
|
| Option | Description |
|
|
181
212
|
|---|---|
|
|
182
213
|
| `input` | Array of arrays (data rows) |
|
|
183
|
-
| `header` | Column titles array |
|
|
214
|
+
| `header` | Column titles array (symbols define `Typr::NAME`-style constants) |
|
|
184
215
|
| `format` | Per-column format: `:min`, `:max`, or a number for fixed width |
|
|
216
|
+
| `procs` | Per-column processors (built-ins: `:datetime`, `:magnitudes`, `:filetree`, `:convert`) |
|
|
185
217
|
| `colors` | `{ columns: [:yellow, :cyan], header: [:white, :black] }` etc. |
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
|
|
|
189
|
-
| `.sort_by` | Sort the grid on a given column |
|
|
190
|
-
| `.select_rows / select_columns` | Multi-select via index picker |
|
|
218
|
+
| `.sort_by` | Sort the grid on a given column (toggles direction) |
|
|
219
|
+
| `.add_filter` | Add a substring/regex filter on a column (or `:all`) |
|
|
220
|
+
| `.pick :row, column:` | Interactive row picker with live `/` search and number hints |
|
|
191
221
|
|
|
192
222
|
### Text options
|
|
193
223
|
| Option | Description |
|
|
@@ -195,12 +225,13 @@ Reads input from the terminal. Types:
|
|
|
195
225
|
| `input` | String or IO-like stream to display |
|
|
196
226
|
| `header` | Header bar text |
|
|
197
227
|
| `border` | Border style: `:round`, `" "`, etc. |
|
|
228
|
+
| `.search` | Search the text (`/`); `.search_next`/`.search_prev` (`n`/`p`) |
|
|
198
229
|
|
|
199
230
|
### Browser options
|
|
200
231
|
| Option | Description |
|
|
201
232
|
|---|---|
|
|
202
233
|
| `directory` | Root directory to start browsing |
|
|
203
|
-
| `.pick(
|
|
234
|
+
| `.pick([type])` | Interactive file picker, returns the absolute path; `type` filters by MIME family (`"image"`, `"audio"`, `"video"`, `"text"`, `"application"`, `"inode"`) |
|
|
204
235
|
|
|
205
236
|
## License
|
|
206
237
|
|
data/lib/browser.rb
CHANGED
|
@@ -216,12 +216,7 @@ module Typr
|
|
|
216
216
|
def help; @user.help end
|
|
217
217
|
|
|
218
218
|
##
|
|
219
|
-
#
|
|
220
|
-
# via +cd(path, true)+. Sorts by name with the filetree processor.
|
|
221
|
-
#
|
|
222
|
-
# browser.recurse # flattens tree into leaf names
|
|
223
|
-
|
|
224
|
-
## Reset display, selection, and position state then re-sort.
|
|
219
|
+
# Reset display, selection, and position state then re-sort.
|
|
225
220
|
#
|
|
226
221
|
# browser.reset_view
|
|
227
222
|
|
|
@@ -231,11 +226,13 @@ module Typr
|
|
|
231
226
|
#
|
|
232
227
|
# browser.filter_view # prompts for filter text
|
|
233
228
|
|
|
234
|
-
def search; pick :none, 0, column: NAME end
|
|
235
|
-
|
|
236
229
|
def filter_view; add_filter *@user.ask(
|
|
237
230
|
filter: [ popup( type: :"[f]ilter" ), :row, NAME], with: :line ) end
|
|
238
231
|
|
|
232
|
+
## Open live `/` search filtered on the name column.
|
|
233
|
+
#
|
|
234
|
+
# browser.search # type to narrow, Escape restores, Enter commits
|
|
235
|
+
|
|
239
236
|
def search
|
|
240
237
|
@suppress_user = true
|
|
241
238
|
pick :none, column: NAME
|
|
@@ -262,12 +259,14 @@ module Typr
|
|
|
262
259
|
|
|
263
260
|
##
|
|
264
261
|
# Interactive file/directory picker. Blocks until a selection or Escape.
|
|
265
|
-
# Return on a file
|
|
266
|
-
#
|
|
267
|
-
#
|
|
262
|
+
# Return on a file yields its raw absolute path; multi-select joins the
|
|
263
|
+
# chosen paths double-quoted with spaces. Enter on a directory triggers
|
|
264
|
+
# cd. +type+ optionally filters by MIME family (audio, video, image,
|
|
265
|
+
# text, application, inode). Mouse: left-click a row to pick it; wheel
|
|
266
|
+
# up/down scrolls the listing.
|
|
268
267
|
#
|
|
269
|
-
# browser.pick # =>
|
|
270
|
-
# browser.pick 'image' # =>
|
|
268
|
+
# browser.pick # => /home/user/file.txt
|
|
269
|
+
# browser.pick 'image' # => /path/to/photo.png (images only)
|
|
271
270
|
# browser.pick nil # => anything, same as 'file'
|
|
272
271
|
|
|
273
272
|
def pick type='file', **kw
|
|
@@ -284,7 +283,15 @@ module Typr
|
|
|
284
283
|
reset :display
|
|
285
284
|
return
|
|
286
285
|
end
|
|
287
|
-
|
|
286
|
+
if key.is_a?(Typr::Mouse)
|
|
287
|
+
if key.wheel? and key.press?
|
|
288
|
+
send( key.wheel_up? ? @keymap[:up] : @keymap[:down] )
|
|
289
|
+
next
|
|
290
|
+
elsif key.press? and key.left? and choice = hit( key.y, key.x )
|
|
291
|
+
else next end
|
|
292
|
+
else
|
|
293
|
+
next unless choice = send( key )
|
|
294
|
+
end
|
|
288
295
|
case choice
|
|
289
296
|
when Array; choice = choice.map{ |id| @data[id][NAME] }.join('" "')
|
|
290
297
|
when Integer; file = @data[ choice ]
|
|
@@ -303,7 +310,7 @@ module Typr
|
|
|
303
310
|
# browser.show # shows browser with info bar
|
|
304
311
|
|
|
305
312
|
def show; super
|
|
306
|
-
user = ENV["USER"] || Etc.getlogin
|
|
313
|
+
user = Etc.getpwuid(Process.euid).name rescue ENV["USER"] || Etc.getlogin
|
|
307
314
|
@head.colors[:columns][0] = user == "root" ? [:red, :black] : [:green, :black]
|
|
308
315
|
@head.reset :format
|
|
309
316
|
@head.show
|
|
@@ -440,7 +447,7 @@ module Typr
|
|
|
440
447
|
@parent = self
|
|
441
448
|
@head ||= Grid.new( parent:self, left: ->{@parent.left}, margin: '',
|
|
442
449
|
right: ->{@parent.width + @parent.left - 1}, top: ->{@parent.top-1},
|
|
443
|
-
input: [ [->{ENV["USER"] || Etc.getlogin}, ->{@parent.directory},
|
|
450
|
+
input: [ [->{Etc.getpwuid(Process.euid).name rescue ENV["USER"] || Etc.getlogin}, ->{@parent.directory},
|
|
444
451
|
->{@parent.filter_display},
|
|
445
452
|
->{ (s = @parent.sorted_by(true)) &&
|
|
446
453
|
"#{s} #{@parent.reverse ? "\u2193" : "\u2191"}"},
|
data/lib/space.rb
CHANGED
|
@@ -29,6 +29,7 @@ include Typr
|
|
|
29
29
|
STR
|
|
30
30
|
eval( method % ([name]*11) ) }
|
|
31
31
|
|
|
32
|
+
# Deep-convert String keys to Symbols so string-keyed config (e.g. YAML) is honored.
|
|
32
33
|
def symbolize obj
|
|
33
34
|
case obj
|
|
34
35
|
when Hash
|
|
@@ -40,17 +41,23 @@ eval( method % ([name]*11) ) }
|
|
|
40
41
|
attr_writer :left, :top, :right, :bottom
|
|
41
42
|
attr_accessor :margin, :colors, :interval, :borders
|
|
42
43
|
|
|
44
|
+
# Viewport width in cells (right - left + 1).
|
|
43
45
|
def width; right - left + 1 end
|
|
46
|
+
# Viewport height in cells (bottom - top + 1).
|
|
44
47
|
def height; bottom - top + 1 end
|
|
45
48
|
|
|
49
|
+
# Redraw every +interval+ seconds from a background thread.
|
|
46
50
|
def start; @updater = Thread.new{ loop{show; sleep @interval }} end
|
|
51
|
+
# Stop the background refresh thread.
|
|
47
52
|
def stop; @updater.terminate end
|
|
48
53
|
|
|
54
|
+
# Draw a border char at screen (x, y); skips nil and off-screen positions.
|
|
49
55
|
def draw_border x,y,char
|
|
50
56
|
draw move_code(x, y) + char unless not char or
|
|
51
57
|
x < 0 or x > Typr.width or y < 0 or y > Typr.height
|
|
52
58
|
end
|
|
53
59
|
|
|
60
|
+
# Draw the widget's border box (no-op when no borders are set).
|
|
54
61
|
def show
|
|
55
62
|
return if @borders.empty?
|
|
56
63
|
color @colors[:border]
|
|
@@ -66,6 +73,8 @@ eval( method % ([name]*11) ) }
|
|
|
66
73
|
draw_border(right + 1, bottom + 1, @borders[:bottom_right])
|
|
67
74
|
end
|
|
68
75
|
|
|
76
|
+
# Set border style: :light/:heavy/:double/:round, a char repeated across
|
|
77
|
+
# the box, or nil for none.
|
|
69
78
|
def border=(border)
|
|
70
79
|
chars = case border
|
|
71
80
|
when Symbol
|
|
@@ -78,6 +87,8 @@ eval( method % ([name]*11) ) }
|
|
|
78
87
|
bottom_right ].map.with_index{ |part,id| [part.to_sym, chars[id]] }.to_h if chars
|
|
79
88
|
end
|
|
80
89
|
|
|
90
|
+
# Build a widget from boundary (left/top/right/bottom), border, margin,
|
|
91
|
+
# colors, interval, and keymap.
|
|
81
92
|
def initialize args={}
|
|
82
93
|
@max, @keymap, @colors, @margin = 0, {}, {}, ' '
|
|
83
94
|
@left, @top, @interval, @borders = 0, 0, 1, {}
|
data/lib/stack.rb
CHANGED
|
@@ -38,6 +38,20 @@ module Typr
|
|
|
38
38
|
|
|
39
39
|
def headspace; @header ? 1 : 0 end
|
|
40
40
|
|
|
41
|
+
##
|
|
42
|
+
# Map a mouse click at 1-based terminal (row, col) to a data row id on
|
|
43
|
+
# the current page, or nil when the click lands outside the widget (border,
|
|
44
|
+
# header, margins). Row ids match what hint presses return from `pick`.
|
|
45
|
+
#
|
|
46
|
+
# stack.hit 3, 5 # => 0 (row 3, col 5 hits data row 0)
|
|
47
|
+
|
|
48
|
+
def hit row, col
|
|
49
|
+
return unless row and col
|
|
50
|
+
rel = row - 1 - top - headspace
|
|
51
|
+
return unless rel.between?(0, height - 1) and col.between?(left, right)
|
|
52
|
+
page.to_a[rel]
|
|
53
|
+
end
|
|
54
|
+
|
|
41
55
|
##
|
|
42
56
|
# The usable height after subtracting any header space.
|
|
43
57
|
#
|
|
@@ -193,6 +207,8 @@ module Typr
|
|
|
193
207
|
# stack.pick # single row picker
|
|
194
208
|
# stack.pick :column, 3 # pick a column in row 3
|
|
195
209
|
# stack.pick "rows" # multi-select rows
|
|
210
|
+
#
|
|
211
|
+
# Mouse: left-click a row to pick it; wheel up/down scrolls the page.
|
|
196
212
|
|
|
197
213
|
def pick type = :row, row=0 #, key=nil
|
|
198
214
|
type = type.to_s
|
|
@@ -207,7 +223,15 @@ module Typr
|
|
|
207
223
|
draw_hints type, row unless type['none']
|
|
208
224
|
limit = type['row'] ? height : positions(row).count
|
|
209
225
|
key = Typr.read_key
|
|
210
|
-
if key.is_a?(
|
|
226
|
+
if key.is_a?(Typr::Mouse)
|
|
227
|
+
if key.wheel? and key.press?
|
|
228
|
+
send( key.wheel_up? ? @keymap[:up] : @keymap[:down] )
|
|
229
|
+
next
|
|
230
|
+
elsif key.press? and key.left? and type['row'] and
|
|
231
|
+
value = hit( key.y, key.x )
|
|
232
|
+
relative = value if type['relative_']
|
|
233
|
+
end
|
|
234
|
+
elsif key.is_a?(String) and value =
|
|
211
235
|
@hints[0..limit-@hints_start-1].index(key)
|
|
212
236
|
value += @hints_start
|
|
213
237
|
relative = value + @start if type['relative_']
|
data/lib/terminal.rb
CHANGED
|
@@ -10,22 +10,70 @@ module Typr
|
|
|
10
10
|
# Raw /dev/tty used for key input; falls back to $stdin when unavailable.
|
|
11
11
|
INPUT = (IO.new IO.sysopen("/dev/tty", "r")) rescue $stdin
|
|
12
12
|
|
|
13
|
-
# KEY_*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
# KEY_* escapes and keypad sequences are precompiled into share/terminfo by
|
|
14
|
+
# bin/build_terminfo, so the runtime needs no ncurses/infocmp (e.g. Termux).
|
|
15
|
+
TERMINFO = begin
|
|
16
|
+
eval File.read(File.expand_path("../share/terminfo", __dir__))
|
|
17
|
+
rescue StandardError
|
|
18
|
+
{}
|
|
19
|
+
end
|
|
20
|
+
# Known-good fallback so KEY_* are always defined, even for TERM=dumb.
|
|
21
|
+
DEFAULT_KEYS = { "key_up" => "\eOA", "key_down" => "\eOB", "key_left" => "\eOD",
|
|
22
|
+
"key_right" => "\eOC", "key_home" => "\eOH", "key_end" => "\eOF",
|
|
23
|
+
"key_backspace" => "\x7f", "key_dc" => "\e[3~", "key_npage" => "\e[6~",
|
|
24
|
+
"key_ppage" => "\e[5~", "carriage_return" => "\r",
|
|
25
|
+
"keypad_xmit" => "\e[?1h\e=", "keypad_local" => "\e[?1l\e>",
|
|
26
|
+
"cursor_invisible" => "\e[?25l", "cursor_normal" => "\e[?25h",
|
|
27
|
+
"clear_screen" => "\e[H\e[2J", "user7" => "\e[6n" }
|
|
28
|
+
term = ENV["TERM"].to_s
|
|
29
|
+
entry = nil
|
|
30
|
+
[ term, term.sub(/-.*/, ""), "xterm" ].each do |name|
|
|
31
|
+
found = TERMINFO[name]
|
|
32
|
+
if found && found.any? { |cap, _| cap.start_with?("key_") }
|
|
33
|
+
entry = found
|
|
34
|
+
break
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
entry ||= {}
|
|
38
|
+
# Fill gaps with safe ANSI defaults, but never fabricate keypad modes:
|
|
39
|
+
# those must match the terminal exactly or arrows get misread.
|
|
40
|
+
DEFAULT_KEYS.each do |cap, seq|
|
|
41
|
+
next if cap.start_with?("keypad_") && !entry[cap]
|
|
42
|
+
const_set cap.upcase, entry[cap] || seq
|
|
43
|
+
end
|
|
20
44
|
|
|
21
45
|
# Common key aliases and the line-erase escape sequence.
|
|
22
46
|
ERASE_LINE = "\e[K"
|
|
47
|
+
# Reset foreground/background to the terminal defaults.
|
|
48
|
+
ORIG_COLORS = "\e[39;49m"
|
|
23
49
|
KEY_ESCAPE = "\e"
|
|
24
50
|
KEY_RETURN = CARRIAGE_RETURN
|
|
25
51
|
KEY_TAB = "\t"
|
|
26
52
|
KEY_PAGEDOWN = KEY_NPAGE
|
|
27
53
|
KEY_PAGEUP = KEY_PPAGE
|
|
28
54
|
|
|
55
|
+
# Enable/disable mouse reporting. Universal private modes, independent of
|
|
56
|
+
# terminfo: 1000 = button press/release/wheel, 1006 = SGR coordinates.
|
|
57
|
+
MOUSE_ON = "\e[?1000h\e[?1006h"
|
|
58
|
+
MOUSE_OFF = "\e[?1000l\e[?1006l"
|
|
59
|
+
|
|
60
|
+
# A mouse event decoded from the input stream. +x+ / +y+ are 1-based
|
|
61
|
+
# terminal coordinates; +button+ is 0=left, 1=middle, 2=right, or 4-7 for
|
|
62
|
+
# the wheel (up/down/left/right); +modifiers+ holds the SGR shift/alt/ctrl
|
|
63
|
+
# bits. Non-widget code can test `key.is_a?(Typr::Mouse)`.
|
|
64
|
+
Mouse = Struct.new(:button, :x, :y, :action, :modifiers) do
|
|
65
|
+
def press?; action == :press end
|
|
66
|
+
def release?; action == :release end
|
|
67
|
+
def wheel?; button.between?(4, 7) end
|
|
68
|
+
def wheel_up?; button == 4 end
|
|
69
|
+
def wheel_down?; button == 5 end
|
|
70
|
+
def left?; button == 0 end
|
|
71
|
+
def middle?; button == 1 end
|
|
72
|
+
def right?; button == 2 end
|
|
73
|
+
def to_s; "%s button=%i x=%i y=%i%s" % [ action, button, x, y,
|
|
74
|
+
( modifiers > 0 ? " modifiers=#{modifiers}" : "" ) ] end
|
|
75
|
+
end
|
|
76
|
+
|
|
29
77
|
# Text attributes (reset, bold, italic, ...) and named 8/256-color tables.
|
|
30
78
|
MODES = %i[ reset bold italic underline slow fast invert ]
|
|
31
79
|
COLORS = %i[ black red green yellow blue magenta cyan white ]
|
|
@@ -196,11 +244,47 @@ module Typr
|
|
|
196
244
|
## Reads a single keypress in raw mode.
|
|
197
245
|
#
|
|
198
246
|
# Returns the key sequence as a String (e.g. "a" or "\e[A" for up-arrow),
|
|
199
|
-
# or nil when stdin is not a tty or
|
|
247
|
+
# or a Typr::Mouse for a mouse report, or nil when stdin is not a tty or
|
|
248
|
+
# input is unavailable.
|
|
200
249
|
|
|
201
250
|
def self.read_key
|
|
202
|
-
|
|
203
|
-
|
|
251
|
+
read = ->(tty) do
|
|
252
|
+
str = tty.sysread 6
|
|
253
|
+
if str.start_with?("\e[<")
|
|
254
|
+
# SGR mouse reports (CSI < b ; x ; y M/m) can exceed six bytes.
|
|
255
|
+
str << tty.sysread(1) until str[-1].ord >= 0x40
|
|
256
|
+
elsif str.start_with?("\e[M") and str.size < 6
|
|
257
|
+
str << tty.sysread(6 - str.size)
|
|
258
|
+
end
|
|
259
|
+
str
|
|
260
|
+
end
|
|
261
|
+
raw = (INPUT.raw{ |tty| read.call tty } rescue nil)
|
|
262
|
+
return unless raw
|
|
263
|
+
decode_mouse(raw) || raw
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
## Decode a raw input string into a {Typr::Mouse} event, or nil when +raw+
|
|
267
|
+
# is not a mouse report. Handles both SGR (CSI < b ; x ; y M/m) and the
|
|
268
|
+
# legacy X10 (CSI M + three bytes) encodings.
|
|
269
|
+
|
|
270
|
+
def self.decode_mouse raw
|
|
271
|
+
if (m = raw.match(/\A\e\[<(\d+);(\d+);(\d+)([Mm])\z/))
|
|
272
|
+
code, x, y, final = m[1].to_i, m[2].to_i, m[3].to_i, m[4]
|
|
273
|
+
elsif raw.start_with?("\e[M")
|
|
274
|
+
code, x, y = raw.getbyte(3) - 32, raw.getbyte(4) - 32, raw.getbyte(5) - 32
|
|
275
|
+
final = code == 3 ? ?m : ?M
|
|
276
|
+
else
|
|
277
|
+
return nil
|
|
278
|
+
end
|
|
279
|
+
modifiers = code & 28
|
|
280
|
+
base = code & ~28
|
|
281
|
+
motion = (base & 32) != 0
|
|
282
|
+
wheel = base >= 64
|
|
283
|
+
button = wheel ? base - 64 + 4 : base & 3
|
|
284
|
+
action = if motion then :motion
|
|
285
|
+
elsif wheel then final == ?m ? :release : :press
|
|
286
|
+
else ( final == ?m or code == 3 ) ? :release : :press end
|
|
287
|
+
Mouse.new button, x, y, action, modifiers
|
|
204
288
|
end
|
|
205
289
|
|
|
206
290
|
## Interactive line editor used for string prompts (search, %str, ...).
|
|
@@ -303,21 +387,25 @@ module Typr
|
|
|
303
387
|
def self.on_resize &block
|
|
304
388
|
trap(:WINCH, &block)
|
|
305
389
|
end
|
|
306
|
-
# Enter interactive mode: seed the default colors, hide the cursor
|
|
307
|
-
#
|
|
390
|
+
# Enter interactive mode: seed the default colors, hide the cursor, enable
|
|
391
|
+
# key-mode (application) escapes and mouse reporting when stdin is a tty.
|
|
308
392
|
def self.init default=[ :white, :black ]
|
|
309
393
|
$default = default.dup
|
|
310
394
|
$color = $default.dup
|
|
311
395
|
if $stdin.tty?
|
|
312
396
|
print CURSOR_INVISIBLE
|
|
313
|
-
|
|
397
|
+
print KEYPAD_XMIT if defined?(KEYPAD_XMIT)
|
|
398
|
+
print MOUSE_ON
|
|
314
399
|
end
|
|
315
400
|
end
|
|
316
|
-
# Restore the terminal: show the cursor, reset colors
|
|
401
|
+
# Restore the terminal: show the cursor, reset keypad/colors, disable mouse
|
|
402
|
+
# reporting and clear.
|
|
317
403
|
def self.exit;
|
|
318
404
|
extend self
|
|
319
405
|
if $stdin.tty?
|
|
320
406
|
$>.print CURSOR_NORMAL;
|
|
407
|
+
$>.print KEYPAD_LOCAL if defined?(KEYPAD_LOCAL)
|
|
408
|
+
$>.print MOUSE_OFF
|
|
321
409
|
$>.print ORIG_COLORS; color; clear
|
|
322
410
|
end
|
|
323
411
|
end
|
data/lib/text.rb
CHANGED
|
@@ -191,6 +191,8 @@ module Typr
|
|
|
191
191
|
self << @input
|
|
192
192
|
end
|
|
193
193
|
|
|
194
|
+
# Reset state: +:search+ clears the pattern and highlights; +:all+ resets
|
|
195
|
+
# position, selection, and search; else falls through to {Stack#reset}.
|
|
194
196
|
def reset type=:all
|
|
195
197
|
case type
|
|
196
198
|
when :search; highlight_clear
|
|
@@ -201,10 +203,13 @@ module Typr
|
|
|
201
203
|
|
|
202
204
|
private
|
|
203
205
|
|
|
206
|
+
# Re-apply search for a live query (empty clears the highlights).
|
|
204
207
|
def live_search query
|
|
205
208
|
query.empty? ? highlight_clear : search(query)
|
|
206
209
|
end
|
|
207
210
|
|
|
211
|
+
# Move to the next/previous match from the current position; returns the
|
|
212
|
+
# matched line id or nil when there are none.
|
|
208
213
|
def step
|
|
209
214
|
matches = @search[:matches]
|
|
210
215
|
return nil if matches.empty?
|
|
@@ -221,6 +226,8 @@ module Typr
|
|
|
221
226
|
return id
|
|
222
227
|
end
|
|
223
228
|
|
|
229
|
+
# Interactive `/` prompt via Typr.read_line; Escape restores the previous
|
|
230
|
+
# search state, Enter searches (empty restores the last pattern).
|
|
224
231
|
def search_prompt
|
|
225
232
|
query = @search[:pattern].is_a?(String) ? @search[:pattern].dup : ''
|
|
226
233
|
saved = { re: @re, pattern: @search[:pattern],
|
data/lib/typr.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# typr is a Ruby library for building interactive terminal-based user interfaces.
|
|
2
2
|
# It provides layout primitives, widgets, and event-driven interaction.
|
|
3
3
|
#
|
|
4
|
-
# ==
|
|
4
|
+
# == Classes
|
|
5
5
|
#
|
|
6
6
|
# * Typr::Grid - Sortable, filterable table with formatted columns and row selection
|
|
7
7
|
# * Typr::Text - Scrollable text viewer with word/line picking and search
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
#
|
|
13
13
|
# == Terminal Control
|
|
14
14
|
#
|
|
15
|
-
# Typr.init # Initialize terminal (hide cursor, enable key mode)
|
|
15
|
+
# Typr.init # Initialize terminal (hide cursor, enable key mode and mouse reporting)
|
|
16
16
|
# Typr.clear # Clear entire screen
|
|
17
17
|
# Typr.clear :line # Clear current line only
|
|
18
18
|
# key = Typr.read_key # Read a single keypress
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
#
|
|
22
22
|
# == Layout Properties
|
|
23
23
|
#
|
|
24
|
-
# All
|
|
24
|
+
# All classes accept +left+, +top+, +right+, +bottom+ as:
|
|
25
25
|
# Integer - absolute position
|
|
26
26
|
# Float - fraction of terminal (0.0..1.0)
|
|
27
27
|
# Proc - evaluated each layout pass
|