rubytui 1.2.5 → 1.2.7
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/README.md +16 -4
- data/lib/rubytui/app.rb +5 -2
- data/lib/rubytui/backend.rb +13 -0
- data/lib/rubytui/backends/ansi_backend.rb +48 -8
- data/lib/rubytui/buffer.rb +4 -2
- data/lib/rubytui/event.rb +5 -2
- data/lib/rubytui/input/key.rb +5 -0
- data/lib/rubytui/input/parser.rb +183 -60
- data/lib/rubytui/input/reader.rb +9 -10
- data/lib/rubytui/version.rb +1 -1
- data/lib/rubytui/widgets/async_image.rb +4 -2
- data/lib/rubytui/widgets/block.rb +3 -2
- data/lib/rubytui/widgets/canvas.rb +8 -3
- data/lib/rubytui/widgets/chart.rb +50 -41
- data/lib/rubytui/widgets/gauge.rb +9 -6
- data/lib/rubytui/widgets/input_field.rb +49 -14
- data/lib/rubytui/widgets/paragraph.rb +7 -6
- data/lib/rubytui.rb +25 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79cdd36b58280934698cfd3e90ff14ba85665e495826dfbbc350efa038177f06
|
|
4
|
+
data.tar.gz: a4304020e612c2d330cfd9aae0284e3780c7bed6c6b054e913f1e4330a38aa97
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f56ab22ff3594c7f6c634017342e24ddb16fcaa59d76c3e3dc28b4e6be5d47142009f24d8e96f7135e291e0acb0051a838de0b8e7a248e5a33f9751520d8a82f
|
|
7
|
+
data.tar.gz: b3c031a1efcc1bc437f787bf88ea388a9ba7c3717b5bc57f739af2f3c245405fac6cc0048937768dcd716e670cb61ea01176df07b5e760e09fff68d2dc69806a
|
data/README.md
CHANGED
|
@@ -198,13 +198,25 @@ end
|
|
|
198
198
|
|
|
199
199
|
### Keyboard
|
|
200
200
|
|
|
201
|
-
A `KeyEvent` has a `code`, a `char` and modifier predicates (`ctrl?`, `alt?`, `shift?`).
|
|
201
|
+
A `KeyEvent` has a `code`, a `char` and modifier predicates (`ctrl?`, `alt?`, `shift?`, `super?`).
|
|
202
202
|
|
|
203
203
|
- `code` is one of the symbols in `RubyTUI::Input::Key`: `CHAR`, `ENTER`, `ESCAPE`, `BACKSPACE`, `DELETE`, `TAB`, `BACKTAB` (Shift+Tab), `UP`, `DOWN`, `LEFT`, `RIGHT`, `HOME`, `END_KEY`, `PAGE_UP`, `PAGE_DOWN`, `INSERT` and `F1` to `F12`.
|
|
204
|
-
- For `CHAR` events, `char` holds the character. Ctrl+letter arrives as the lowercase letter with `ctrl?` true, except Ctrl+H, Ctrl+I and Ctrl+M, which the terminal sends as Backspace, Tab and Enter. Alt+key arrives with `alt?` true.
|
|
205
|
-
- Arrow keys, Home and
|
|
204
|
+
- For `CHAR` events, `char` holds the character. Ctrl+letter arrives as the lowercase letter with `ctrl?` true, except Ctrl+H, Ctrl+I and Ctrl+M, which the terminal sends as Backspace, Tab and Enter. Ctrl+Space arrives as `" "` with `ctrl?` true. Alt+key arrives with `alt?` true, including Alt+Backspace, Alt+Enter, Alt+Tab and Alt+Escape as those keys.
|
|
205
|
+
- Arrow keys, Home, End, Page Up, Page Down, Insert, Delete and the function keys also report Shift, Alt and Ctrl modifiers.
|
|
206
206
|
- A lone Escape key press is reported after a 50 ms wait that separates it from escape sequences.
|
|
207
207
|
- In raw mode, Ctrl+C does not send `SIGINT`; it arrives as a key event, so handle it if your application should quit on it.
|
|
208
|
+
- On macOS, Option types characters (Option+A gives `å`) unless the terminal is set to send it as Alt: `macos-option-as-alt` in Ghostty, Option key "Esc+" in iTerm2's profile, "Use Option as Meta key" in Terminal.
|
|
209
|
+
|
|
210
|
+
#### Kitty keyboard protocol
|
|
211
|
+
|
|
212
|
+
`RubyTUI.run(kitty_keyboard: true)` (also `RubyTUI.init` and `App#run!`) asks the terminal to report keys with the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), and switches it back off when the block exits or raises. On terminals that support it, such as Ghostty, iTerm2, kitty, Alacritty and foot:
|
|
213
|
+
|
|
214
|
+
- Ctrl+Shift+letter arrives with `ctrl?` and `shift?` true, and `char` is the shifted character (`"A"`) when the terminal reports it, otherwise the lowercase letter.
|
|
215
|
+
- Ctrl+I, Ctrl+M, Ctrl+H and Ctrl+[ arrive as Ctrl+letter, not as Tab, Enter, Backspace and Escape.
|
|
216
|
+
- Ctrl and Alt combine with digits and punctuation, and Cmd combinations that the terminal does not use itself arrive with `super?` true.
|
|
217
|
+
- Escape is reported without the 50 ms wait.
|
|
218
|
+
|
|
219
|
+
Plain and Shift-ed text, Enter, Tab and Backspace arrive as before, as do all keys on terminals without the protocol, such as macOS Terminal. Shift+Space is not reported separately.
|
|
208
220
|
|
|
209
221
|
```ruby
|
|
210
222
|
require "rubytui"
|
|
@@ -243,7 +255,7 @@ end
|
|
|
243
255
|
|
|
244
256
|
### Text input
|
|
245
257
|
|
|
246
|
-
`InputState#handle_key(event)` applies a key event to the input and returns `true` if it consumed the event. Printable characters are inserted; Backspace, Delete, Left, Right, Home and End edit and move the cursor; Ctrl+A and Ctrl+E move to the start and end; Ctrl+K and Ctrl+U delete to the end and to the start; Ctrl+W deletes the previous word. Other keys, such as Enter, Tab and Escape, are left to your application.
|
|
258
|
+
`InputState#handle_key(event)` applies a key event to the input and returns `true` if it consumed the event. Printable characters are inserted; Backspace, Delete, Left, Right, Home and End edit and move the cursor, treating a letter with its combining marks or an emoji sequence as one character; Ctrl+A and Ctrl+E move to the start and end; Ctrl+K and Ctrl+U delete to the end and to the start; Ctrl+W deletes the previous word. Other keys, such as Enter, Tab and Escape, are left to your application.
|
|
247
259
|
|
|
248
260
|
```ruby
|
|
249
261
|
state = RubyTUI::Widgets::InputState.new
|
data/lib/rubytui/app.rb
CHANGED
|
@@ -134,9 +134,12 @@ module RubyTUI
|
|
|
134
134
|
# milliseconds (default: 50)
|
|
135
135
|
# @param output [IO] output stream (default: $stdout)
|
|
136
136
|
# @param input [IO] input stream (default: $stdin)
|
|
137
|
+
# @param kitty_keyboard [Boolean] report keys with the kitty keyboard
|
|
138
|
+
# protocol where the terminal supports it (see {RubyTUI.run}) (default: false)
|
|
137
139
|
# @return [void]
|
|
138
|
-
def run!(mouse: false, tick_ms: 50, output: $stdout, input: $stdin)
|
|
139
|
-
RubyTUI.run(output: output, input: input, mouse: mouse
|
|
140
|
+
def run!(mouse: false, tick_ms: 50, output: $stdout, input: $stdin, kitty_keyboard: false)
|
|
141
|
+
RubyTUI.run(output: output, input: input, mouse: mouse,
|
|
142
|
+
kitty_keyboard: kitty_keyboard) do |terminal, reader|
|
|
140
143
|
model = init
|
|
141
144
|
on_start(terminal, reader)
|
|
142
145
|
|
data/lib/rubytui/backend.rb
CHANGED
|
@@ -116,6 +116,19 @@ module RubyTUI
|
|
|
116
116
|
raise NotImplementedError
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
# Turn on the kitty keyboard protocol (see {AnsiBackend#enable_kitty_keyboard}).
|
|
120
|
+
# No-op by default.
|
|
121
|
+
# @return [void]
|
|
122
|
+
def enable_kitty_keyboard
|
|
123
|
+
# no-op by default
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Turn off what {#enable_kitty_keyboard} turned on. No-op by default.
|
|
127
|
+
# @return [void]
|
|
128
|
+
def disable_kitty_keyboard
|
|
129
|
+
# no-op by default
|
|
130
|
+
end
|
|
131
|
+
|
|
119
132
|
# Draw an image using the Kitty graphics protocol.
|
|
120
133
|
# No-op by default; backends that support images override it.
|
|
121
134
|
# @param placement [Hash{Symbol => Object}] image placement data with keys
|
|
@@ -7,7 +7,8 @@ module RubyTUI
|
|
|
7
7
|
#
|
|
8
8
|
# Output accumulates in an internal string buffer and is written to the
|
|
9
9
|
# output IO only by {#flush}. {#enter_alternate_screen},
|
|
10
|
-
# {#leave_alternate_screen}, {#enable_mouse}, {#disable_mouse}
|
|
10
|
+
# {#leave_alternate_screen}, {#enable_mouse}, {#disable_mouse},
|
|
11
|
+
# {#enable_kitty_keyboard}, {#disable_kitty_keyboard} and
|
|
11
12
|
# {#clear_all_images!} flush immediately.
|
|
12
13
|
#
|
|
13
14
|
# @see RubyTUI.init
|
|
@@ -17,6 +18,10 @@ module RubyTUI
|
|
|
17
18
|
# Control Sequence Introducer (<tt>ESC [</tt>) that prefixes escape sequences.
|
|
18
19
|
CSI = "\e["
|
|
19
20
|
|
|
21
|
+
# Kitty keyboard protocol flags pushed by {#enable_kitty_keyboard}:
|
|
22
|
+
# disambiguate escape codes (1) and report alternate keys (4).
|
|
23
|
+
KITTY_KEYBOARD_FLAGS = 0b101
|
|
24
|
+
|
|
20
25
|
# @param output [IO] stream escape sequences are written to (default: $stdout)
|
|
21
26
|
# @param input [IO] terminal input used for raw mode and cell-size queries
|
|
22
27
|
# (default: $stdin)
|
|
@@ -144,6 +149,40 @@ module RubyTUI
|
|
|
144
149
|
flush
|
|
145
150
|
end
|
|
146
151
|
|
|
152
|
+
# Push {KITTY_KEYBOARD_FLAGS} onto the terminal's kitty keyboard mode
|
|
153
|
+
# stack (<tt>CSI > 5 u</tt>) and flush. Keys the legacy encoding cannot
|
|
154
|
+
# tell apart then arrive as <tt>CSI u</tt> sequences, which {Input::Parser}
|
|
155
|
+
# decodes: Esc, and Ctrl, Alt or Super (Cmd) with a key. Plain and
|
|
156
|
+
# Shift-ed text, Enter, Tab and Backspace are sent as before. A terminal
|
|
157
|
+
# without the protocol keeps sending keys as before. Does nothing if
|
|
158
|
+
# already on.
|
|
159
|
+
#
|
|
160
|
+
# Call it after {#enter_alternate_screen}, as each screen has its own stack.
|
|
161
|
+
#
|
|
162
|
+
# @return [void]
|
|
163
|
+
def enable_kitty_keyboard
|
|
164
|
+
return if @kitty_keyboard
|
|
165
|
+
|
|
166
|
+
@buffer << "#{CSI}>#{KITTY_KEYBOARD_FLAGS}u"
|
|
167
|
+
@kitty_keyboard = true
|
|
168
|
+
flush
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Pop the entry pushed by {#enable_kitty_keyboard} (<tt>CSI < u</tt>),
|
|
172
|
+
# restoring the terminal's previous keyboard mode, and flush. Does
|
|
173
|
+
# nothing if it is not on.
|
|
174
|
+
#
|
|
175
|
+
# Call it before {#leave_alternate_screen}.
|
|
176
|
+
#
|
|
177
|
+
# @return [void]
|
|
178
|
+
def disable_kitty_keyboard
|
|
179
|
+
return unless @kitty_keyboard
|
|
180
|
+
|
|
181
|
+
@buffer << "#{CSI}<u"
|
|
182
|
+
@kitty_keyboard = false
|
|
183
|
+
flush
|
|
184
|
+
end
|
|
185
|
+
|
|
147
186
|
# Put the input into raw mode, saving its terminal state for {#disable_raw_mode}.
|
|
148
187
|
#
|
|
149
188
|
# Save the termios state of the SAME fd we are about to raw! — a backtick
|
|
@@ -390,18 +429,19 @@ module RubyTUI
|
|
|
390
429
|
codes << (style.bg ? style.bg.to_bg_ansi : "49")
|
|
391
430
|
end
|
|
392
431
|
|
|
393
|
-
# Modifiers added
|
|
432
|
+
# Modifiers removed, before any are added: SGR 22 turns off both bold
|
|
433
|
+
# and dim, so it must not follow a 1 or 2 meant to stay on
|
|
434
|
+
removed = prev.modifiers & ~style.modifiers
|
|
435
|
+
off = Modifier::SGR_OFF.filter_map { |flag, code| code if Modifier.contains?(removed, flag) }.uniq
|
|
436
|
+
codes.concat(off)
|
|
437
|
+
|
|
438
|
+
# Modifiers added, plus any bold/dim the new style keeps that 22 cleared
|
|
394
439
|
added = style.modifiers & ~prev.modifiers
|
|
440
|
+
added |= style.modifiers & (Modifier::BOLD | Modifier::DIM) if off.include?("22")
|
|
395
441
|
Modifier::SGR_ON.each do |flag, code|
|
|
396
442
|
codes << code if Modifier.contains?(added, flag)
|
|
397
443
|
end
|
|
398
444
|
|
|
399
|
-
# Modifiers removed
|
|
400
|
-
removed = prev.modifiers & ~style.modifiers
|
|
401
|
-
Modifier::SGR_OFF.each do |flag, code|
|
|
402
|
-
codes << code if Modifier.contains?(removed, flag)
|
|
403
|
-
end
|
|
404
|
-
|
|
405
445
|
@buffer << "#{CSI}#{codes.join(";")}m" unless codes.empty?
|
|
406
446
|
@prev_style = style
|
|
407
447
|
end
|
data/lib/rubytui/buffer.rb
CHANGED
|
@@ -97,6 +97,7 @@ module RubyTUI
|
|
|
97
97
|
# Iterates by grapheme cluster so that combining sequences (Devanagari
|
|
98
98
|
# matras/virama, emoji ZWJ, Latin diacritics) are stored as a single
|
|
99
99
|
# cell symbol. Wide clusters (2-column) mark the next cell as a spacer.
|
|
100
|
+
# Control characters (zero width, e.g. a tab or newline) are skipped.
|
|
100
101
|
# Clips at buffer boundaries.
|
|
101
102
|
#
|
|
102
103
|
# @param x [Integer] starting column
|
|
@@ -114,6 +115,7 @@ module RubyTUI
|
|
|
114
115
|
break if col >= @area.right
|
|
115
116
|
|
|
116
117
|
w = Unicode.cluster_width(gc)
|
|
118
|
+
next if w.zero?
|
|
117
119
|
|
|
118
120
|
if col >= @area.x
|
|
119
121
|
idx = index(col, y)
|
|
@@ -278,12 +280,12 @@ module RubyTUI
|
|
|
278
280
|
# puts buf.to_ansi
|
|
279
281
|
def to_ansi
|
|
280
282
|
csi = "\e["
|
|
281
|
-
result = String.new(capacity: @area.area * 2)
|
|
283
|
+
result = String.new(capacity: @area.area * 2, encoding: Encoding::UTF_8)
|
|
282
284
|
prev_style = nil
|
|
283
285
|
|
|
284
286
|
@area.height.times do |row|
|
|
285
287
|
# Build the row content
|
|
286
|
-
row_buf = String.new(capacity: @area.width * 2)
|
|
288
|
+
row_buf = String.new(capacity: @area.width * 2, encoding: Encoding::UTF_8)
|
|
287
289
|
col = 0
|
|
288
290
|
display_col = 0 # track actual terminal column for cursor correction
|
|
289
291
|
|
data/lib/rubytui/event.rb
CHANGED
|
@@ -19,8 +19,8 @@ module RubyTUI
|
|
|
19
19
|
# (e.g. {Input::Key::ENTER}, {Input::Key::CHAR})
|
|
20
20
|
# @param char [String, nil] the character, for {Input::Key::CHAR} events; a
|
|
21
21
|
# UTF-8 String when produced by {Input::Parser} (default: nil)
|
|
22
|
-
# @param modifiers [Integer] bitmask of {Input::Key::SHIFT}, {Input::Key::ALT}
|
|
23
|
-
# {Input::Key::CTRL} (default: {Input::Key::NONE})
|
|
22
|
+
# @param modifiers [Integer] bitmask of {Input::Key::SHIFT}, {Input::Key::ALT},
|
|
23
|
+
# {Input::Key::CTRL} and {Input::Key::SUPER} (default: {Input::Key::NONE})
|
|
24
24
|
def initialize(code:, char: nil, modifiers: Input::Key::NONE)
|
|
25
25
|
@code = code
|
|
26
26
|
@char = char
|
|
@@ -33,6 +33,8 @@ module RubyTUI
|
|
|
33
33
|
def alt? = (@modifiers & Input::Key::ALT) != 0
|
|
34
34
|
# @return [Boolean] true if the {Input::Key::SHIFT} flag is set
|
|
35
35
|
def shift? = (@modifiers & Input::Key::SHIFT) != 0
|
|
36
|
+
# @return [Boolean] true if the {Input::Key::SUPER} flag (Cmd on macOS) is set
|
|
37
|
+
def super? = (@modifiers & Input::Key::SUPER) != 0
|
|
36
38
|
|
|
37
39
|
# Value equality: same code, character and modifiers.
|
|
38
40
|
#
|
|
@@ -53,6 +55,7 @@ module RubyTUI
|
|
|
53
55
|
parts << "Ctrl" if ctrl?
|
|
54
56
|
parts << "Alt" if alt?
|
|
55
57
|
parts << "Shift" if shift?
|
|
58
|
+
parts << "Super" if super?
|
|
56
59
|
parts << (@char || @code.to_s)
|
|
57
60
|
"KeyEvent(#{parts.join("+")})"
|
|
58
61
|
end
|
data/lib/rubytui/input/key.rb
CHANGED
|
@@ -86,6 +86,11 @@ module RubyTUI
|
|
|
86
86
|
ALT = 0b010
|
|
87
87
|
# Ctrl modifier bit.
|
|
88
88
|
CTRL = 0b100
|
|
89
|
+
# Super modifier bit: Cmd on macOS, the Windows key elsewhere. Reported
|
|
90
|
+
# only by terminals that send it in a modifier parameter, such as with
|
|
91
|
+
# the kitty keyboard protocol (see {RubyTUI.run}); xterm sends Meta in
|
|
92
|
+
# this bit.
|
|
93
|
+
SUPER = 0b1000
|
|
89
94
|
|
|
90
95
|
# @!endgroup
|
|
91
96
|
end
|
data/lib/rubytui/input/parser.rb
CHANGED
|
@@ -5,13 +5,32 @@ module RubyTUI
|
|
|
5
5
|
# Decodes raw terminal input into {KeyEvent} and {MouseEvent} objects.
|
|
6
6
|
#
|
|
7
7
|
# Recognised input: printable ASCII, Enter, Tab, Backspace, Ctrl+letter,
|
|
8
|
+
# Ctrl+Space and Ctrl+\ ] ^ _ (bytes 0 and 28-31),
|
|
8
9
|
# single UTF-8 characters (as raw bytes, as read by {Reader}, or as a UTF-8
|
|
9
|
-
# String), ESC, Alt+
|
|
10
|
+
# String), ESC, Alt+key (ESC followed by one character or control byte,
|
|
11
|
+
# e.g. <tt>ESC DEL</tt> is Alt+Backspace and <tt>ESC ESC</tt> Alt+Escape),
|
|
10
12
|
# CSI and SS3 key sequences (arrows, Home/End, Insert/Delete, Page Up/Down,
|
|
11
|
-
# Back-tab, F1-F12,
|
|
13
|
+
# Back-tab, F1-F12, each with an optional xterm modifier parameter such as
|
|
14
|
+
# <tt>ESC [ 1 ; 5 A</tt> or <tt>ESC [ 5 ; 5 ~</tt>),
|
|
12
15
|
# the same key sequences preceded by ESC (Alt+key as sent by rxvt-style
|
|
13
16
|
# terminals, e.g. <tt>ESC ESC [ A</tt> is Alt+Up),
|
|
14
|
-
# and SGR and legacy mouse reports.
|
|
17
|
+
# and SGR and legacy mouse reports (buttons 8-11 give no event).
|
|
18
|
+
#
|
|
19
|
+
# Keys sent as <tt>CSI code ; modifiers u</tt> (the kitty keyboard
|
|
20
|
+
# protocol, see {RubyTUI.run}) or <tt>CSI 27 ; modifiers ; code ~</tt>
|
|
21
|
+
# (xterm modifyOtherKeys) are recognised whether or not rubytui asked
|
|
22
|
+
# for them. The key code is a Unicode code point, so the {KeyEvent} is a
|
|
23
|
+
# character key, or Enter, Tab, Escape or Backspace for codes 13, 9, 27
|
|
24
|
+
# and 127, or a keypad key reported as the key it stands for (keypad
|
|
25
|
+
# Enter as Enter, keypad 1 as "1"). The character is the shifted key or
|
|
26
|
+
# the key's text when the sequence carries one, otherwise the key code
|
|
27
|
+
# itself, so Ctrl+Shift+A is <tt>char "a"</tt> with Ctrl and Shift
|
|
28
|
+
# unless the terminal also sends the shifted key "A". Shift+Tab is
|
|
29
|
+
# {Input::Key::BACKTAB} without Shift. Key releases, modifier and lock
|
|
30
|
+
# keys pressed on their own, F13 and above, and media keys give no event.
|
|
31
|
+
#
|
|
32
|
+
# In every modifier parameter the Shift, Alt, Ctrl and Super (Cmd) bits
|
|
33
|
+
# are reported; Hyper, Meta, Caps Lock and Num Lock are ignored.
|
|
15
34
|
#
|
|
16
35
|
# String sequences that terminals send as replies are recognised and
|
|
17
36
|
# produce no event: APC (<tt>ESC _</tt>, e.g. Kitty graphics responses),
|
|
@@ -19,7 +38,8 @@ module RubyTUI
|
|
|
19
38
|
# (<tt>ESC ^</tt>), each ended by ST (ESC and a backslash) or BEL. Because these
|
|
20
39
|
# introducers start a string, Alt+P, Alt+X, Alt+], Alt+^ and Alt+_ are
|
|
21
40
|
# reported only when the input ends with the string they start still
|
|
22
|
-
# unfinished (see
|
|
41
|
+
# unfinished, or an ESC that does not start ST follows them (see
|
|
42
|
+
# {#parse_prefix}).
|
|
23
43
|
#
|
|
24
44
|
# The +char+ of every {KeyEvent} it returns is a UTF-8 String.
|
|
25
45
|
#
|
|
@@ -37,6 +57,24 @@ module RubyTUI
|
|
|
37
57
|
STRING_INTRODUCERS = [0x50, 0x58, 0x5D, 0x5E, 0x5F].freeze
|
|
38
58
|
private_constant :STRING_INTRODUCERS
|
|
39
59
|
|
|
60
|
+
# Modifier bits reported in a KeyEvent; they match the protocol bits
|
|
61
|
+
MODIFIER_MASK = Key::SHIFT | Key::ALT | Key::CTRL | Key::SUPER
|
|
62
|
+
private_constant :MODIFIER_MASK
|
|
63
|
+
|
|
64
|
+
# Key codes in CSI u and modifyOtherKeys sequences that are not
|
|
65
|
+
# characters: C0 keys and the kitty keypad keys (57399-57426), as a key
|
|
66
|
+
# code Symbol or the character the key types
|
|
67
|
+
CODE_KEYS = {
|
|
68
|
+
9 => Key::TAB, 13 => Key::ENTER, 27 => Key::ESCAPE, 127 => Key::BACKSPACE,
|
|
69
|
+
**(0..9).to_h { |digit| [57399 + digit, digit.to_s] },
|
|
70
|
+
57409 => ".", 57410 => "/", 57411 => "*", 57412 => "-", 57413 => "+",
|
|
71
|
+
57414 => Key::ENTER, 57415 => "=", 57416 => ",",
|
|
72
|
+
57417 => Key::LEFT, 57418 => Key::RIGHT, 57419 => Key::UP, 57420 => Key::DOWN,
|
|
73
|
+
57421 => Key::PAGE_UP, 57422 => Key::PAGE_DOWN, 57423 => Key::HOME,
|
|
74
|
+
57424 => Key::END_KEY, 57425 => Key::INSERT, 57426 => Key::DELETE
|
|
75
|
+
}.freeze
|
|
76
|
+
private_constant :CODE_KEYS
|
|
77
|
+
|
|
40
78
|
# Parse raw bytes into an Event.
|
|
41
79
|
#
|
|
42
80
|
# Input in any encoding is decoded byte-wise; a character is returned as
|
|
@@ -69,28 +107,30 @@ module RubyTUI
|
|
|
69
107
|
# still measured, so the caller can drop them and carry on.
|
|
70
108
|
#
|
|
71
109
|
# A CSI sequence (<tt>ESC [</tt>) is at most 256 bytes long. If no
|
|
72
|
-
# final byte comes within its first 256 bytes,
|
|
73
|
-
#
|
|
74
|
-
#
|
|
110
|
+
# final byte comes within its first 256 bytes, or a byte that cannot
|
|
111
|
+
# be part of a CSI sequence comes first, it was typed: only
|
|
112
|
+
# <tt>ESC [</tt> is taken, as Alt+[, and the bytes after it are left to
|
|
113
|
+
# be decoded as input of their own; this does not depend on +partial+.
|
|
75
114
|
#
|
|
76
115
|
# A terminal string sequence (APC, OSC, DCS, SOS, PM) runs up to and
|
|
77
|
-
# including its ST or BEL terminator and gives no event.
|
|
78
|
-
# it that does not start ST
|
|
79
|
-
#
|
|
116
|
+
# including its ST or BEL terminator and gives no event. Replies hold
|
|
117
|
+
# no other ESC, so an ESC inside it that does not start ST means it was
|
|
118
|
+
# typed: only its introducer is taken, as Alt+character (2 bytes, e.g.
|
|
119
|
+
# <tt>ESC P</tt> is Alt+P), and the bytes after it are left to be
|
|
120
|
+
# decoded as input of their own. There is no length limit ({Reader}
|
|
121
|
+
# applies one).
|
|
80
122
|
#
|
|
81
123
|
# @param bytes [String] buffered raw input
|
|
82
124
|
# @param partial [Boolean] true if more input may follow +bytes+: an
|
|
83
125
|
# incomplete sequence at the front (such as a lone ESC, an unfinished
|
|
84
|
-
# string sequence, or ESC ESC that may start Alt+key) is then
|
|
85
|
-
# unconsumed. When false, it is decoded as it stands: ESC ESC is
|
|
86
|
-
# plain ESC key; an unfinished string sequence
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
# Alt+O, and a cut-off UTF-8 character, mouse report or longer CSI
|
|
93
|
-
# sequence gives no event) (default: false)
|
|
126
|
+
# CSI or string sequence, or ESC ESC that may start Alt+key) is then
|
|
127
|
+
# left unconsumed. When false, it is decoded as it stands: ESC ESC is
|
|
128
|
+
# a plain ESC key; an unfinished CSI or string sequence is taken to be
|
|
129
|
+
# typed, so only its introducer is taken, as Alt+[ or Alt+character
|
|
130
|
+
# (2 bytes), and the bytes after it are left to be decoded as input of
|
|
131
|
+
# their own; any other unfinished sequence is taken whole (a lone ESC
|
|
132
|
+
# is the ESC key, <tt>ESC O</tt> is Alt+O, and a cut-off UTF-8
|
|
133
|
+
# character gives no event) (default: false)
|
|
94
134
|
# @return [Array(Event, Integer)] a two-element array: the event (nil if
|
|
95
135
|
# the sequence was not recognised or is a string sequence) and the
|
|
96
136
|
# number of bytes it took up. The count is 0 only when +bytes+ is
|
|
@@ -128,10 +168,11 @@ module RubyTUI
|
|
|
128
168
|
|
|
129
169
|
# Length to take for a sequence cut short by the end of input: all of
|
|
130
170
|
# it, except that ESC ESC starts with a plain ESC key and an unfinished
|
|
131
|
-
# string sequence with its introducer (Alt+character).
|
|
171
|
+
# CSI or string sequence with its introducer (Alt+[ or Alt+character).
|
|
132
172
|
def incomplete_length(bytes)
|
|
133
173
|
return 1 if bytes.start_with?("\e\e")
|
|
134
|
-
return 2 if bytes.getbyte(0) == 0x1B &&
|
|
174
|
+
return 2 if bytes.getbyte(0) == 0x1B &&
|
|
175
|
+
(bytes.getbyte(1) == 0x5B || STRING_INTRODUCERS.include?(bytes.getbyte(1)))
|
|
135
176
|
|
|
136
177
|
bytes.bytesize
|
|
137
178
|
end
|
|
@@ -163,13 +204,13 @@ module RubyTUI
|
|
|
163
204
|
end
|
|
164
205
|
|
|
165
206
|
# CSI: parameter/intermediate bytes 0x20-0x3F, then a final byte 0x40-0x7E.
|
|
166
|
-
# Any other byte
|
|
167
|
-
#
|
|
207
|
+
# Any other byte, or no final byte within MAX_CSI_BYTES, means it was
|
|
208
|
+
# typed: ESC [ alone (Alt+[).
|
|
168
209
|
def csi_length(bytes)
|
|
169
210
|
(2...[bytes.bytesize, MAX_CSI_BYTES].min).each do |i|
|
|
170
211
|
b = bytes.getbyte(i)
|
|
171
212
|
return i + 1 if b.between?(0x40, 0x7E)
|
|
172
|
-
return
|
|
213
|
+
return 2 unless b.between?(0x20, 0x3F)
|
|
173
214
|
end
|
|
174
215
|
bytes.bytesize >= MAX_CSI_BYTES ? 2 : nil
|
|
175
216
|
end
|
|
@@ -187,8 +228,9 @@ module RubyTUI
|
|
|
187
228
|
parse(bytes.byteslice(0, length + 1)).is_a?(KeyEvent) ? length + 1 : 1
|
|
188
229
|
end
|
|
189
230
|
|
|
190
|
-
# String sequence: ends after BEL or ST (ESC \)
|
|
191
|
-
#
|
|
231
|
+
# String sequence: ends after BEL or ST (ESC \); nil while neither has
|
|
232
|
+
# come. Terminal replies hold no other ESC, so an ESC followed by any
|
|
233
|
+
# other byte means the introducer was typed: 2 (Alt+character).
|
|
192
234
|
def string_length(bytes)
|
|
193
235
|
stop = bytes.index(/[\a\e]/, 2)
|
|
194
236
|
return nil if stop.nil?
|
|
@@ -197,7 +239,7 @@ module RubyTUI
|
|
|
197
239
|
following = bytes.getbyte(stop + 1)
|
|
198
240
|
return nil if following.nil? # ESC at the end: may be the start of ST
|
|
199
241
|
|
|
200
|
-
following == 0x5C ? stop + 2 :
|
|
242
|
+
following == 0x5C ? stop + 2 : 2
|
|
201
243
|
end
|
|
202
244
|
|
|
203
245
|
# Length of the UTF-8 character starting at +start+; an invalid sequence
|
|
@@ -235,10 +277,15 @@ module RubyTUI
|
|
|
235
277
|
KeyEvent.new(code: Key::BACKSPACE)
|
|
236
278
|
when 8 # BS (backspace on some terminals)
|
|
237
279
|
KeyEvent.new(code: Key::BACKSPACE)
|
|
280
|
+
when 0 # Ctrl+Space
|
|
281
|
+
KeyEvent.new(code: Key::CHAR, char: " ", modifiers: Key::CTRL)
|
|
238
282
|
when 1..26
|
|
239
283
|
# Ctrl+A through Ctrl+Z
|
|
240
284
|
char = (ord + 96).chr(Encoding::UTF_8) # Convert to lowercase letter
|
|
241
285
|
KeyEvent.new(code: Key::CHAR, char: char, modifiers: Key::CTRL)
|
|
286
|
+
when 28..31
|
|
287
|
+
# Ctrl+\ ] ^ _
|
|
288
|
+
KeyEvent.new(code: Key::CHAR, char: (ord + 64).chr(Encoding::UTF_8), modifiers: Key::CTRL)
|
|
242
289
|
when 32..126
|
|
243
290
|
# Printable ASCII
|
|
244
291
|
KeyEvent.new(code: Key::CHAR, char: utf8_char(byte))
|
|
@@ -254,13 +301,10 @@ module RubyTUI
|
|
|
254
301
|
# SGR mouse: ESC [ < Cb ; Cx ; Cy M/m
|
|
255
302
|
when /\A\e\[<(\d+);(\d+);(\d+)([Mm])\z/
|
|
256
303
|
parse_sgr_mouse($1.to_i, $2.to_i, $3.to_i, $4)
|
|
257
|
-
# CSI sequences: ESC [
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
parse_csi_tilde($1.to_i)
|
|
262
|
-
when /\A\e\[1;(\d+)([A-Za-z])\z/
|
|
263
|
-
parse_csi_modified($2, $1.to_i)
|
|
304
|
+
# CSI key sequences: ESC [ params final, params being ;-separated
|
|
305
|
+
# fields of :-separated numbers
|
|
306
|
+
when /\A\e\[([\d;:]*)([A-Za-z~])\z/
|
|
307
|
+
parse_csi_key($1, $2)
|
|
264
308
|
# Normal mouse: ESC [ M Cb Cx Cy (legacy, 3 raw bytes after M)
|
|
265
309
|
when /\A\e\[M(.{3})\z/m
|
|
266
310
|
parse_normal_mouse($1)
|
|
@@ -273,16 +317,93 @@ module RubyTUI
|
|
|
273
317
|
if key.is_a?(KeyEvent)
|
|
274
318
|
KeyEvent.new(code: key.code, char: key.char, modifiers: key.modifiers | Key::ALT)
|
|
275
319
|
end
|
|
276
|
-
# Alt+
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
320
|
+
# Alt+key: ESC + one (possibly multi-byte) character, control byte
|
|
321
|
+
# (ESC DEL is Alt+Backspace) or ESC (Alt+Escape)
|
|
322
|
+
when /\A\e(.+)\z/m
|
|
323
|
+
key = if $1 == "\e"
|
|
324
|
+
KeyEvent.new(code: Key::ESCAPE)
|
|
325
|
+
elsif !$1.start_with?("\e")
|
|
326
|
+
parse($1)
|
|
327
|
+
end
|
|
328
|
+
with_modifiers(key, Key::ALT)
|
|
280
329
|
else
|
|
281
330
|
nil
|
|
282
331
|
end
|
|
283
332
|
end
|
|
284
333
|
|
|
285
|
-
# CSI
|
|
334
|
+
# CSI key sequence. +params+ is the parameter string, +final+ the final
|
|
335
|
+
# byte. The second field is the modifier parameter, with an optional
|
|
336
|
+
# event type (1 press, 2 repeat, 3 release) after a colon.
|
|
337
|
+
def parse_csi_key(params, final)
|
|
338
|
+
fields = params.split(";", -1).map { |field| field.split(":", -1) }
|
|
339
|
+
number = fields.dig(0, 0)
|
|
340
|
+
mod_code, event_type = fields[1]
|
|
341
|
+
return nil if event_type == "3" # key release
|
|
342
|
+
|
|
343
|
+
modifiers = decode_modifiers(mod_code.to_s.empty? ? 1 : mod_code.to_i)
|
|
344
|
+
case final
|
|
345
|
+
when "u"
|
|
346
|
+
parse_csi_u(fields, modifiers)
|
|
347
|
+
when "~"
|
|
348
|
+
if number == "27" && fields.size == 3 # xterm modifyOtherKeys
|
|
349
|
+
parse_key_code(fields[2][0].to_i, modifiers)
|
|
350
|
+
else
|
|
351
|
+
with_modifiers(parse_csi_tilde(number.to_i), modifiers)
|
|
352
|
+
end
|
|
353
|
+
else
|
|
354
|
+
# The number is 1, or omitted when there are no modifiers
|
|
355
|
+
return nil unless [nil, "", "1"].include?(number)
|
|
356
|
+
|
|
357
|
+
with_modifiers(parse_csi_letter(final), modifiers)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# CSI u: ESC [ code[:shifted[:base]] ; modifiers ; text u, where text is
|
|
362
|
+
# :-separated code points. The base layout key is not used.
|
|
363
|
+
def parse_csi_u(fields, modifiers)
|
|
364
|
+
code, shifted = fields[0]
|
|
365
|
+
text = fields[2]&.map { |cp| codepoint_char(cp.to_i) }
|
|
366
|
+
if text&.all?
|
|
367
|
+
KeyEvent.new(code: Key::CHAR, char: text.join, modifiers: modifiers)
|
|
368
|
+
else
|
|
369
|
+
parse_key_code(code.to_i, modifiers, shifted.to_s.empty? ? nil : shifted.to_i)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# KeyEvent for a key code (and the shifted key, if sent) from a CSI u or
|
|
374
|
+
# modifyOtherKeys sequence, or nil if it has no key.
|
|
375
|
+
def parse_key_code(code, modifiers, shifted = nil)
|
|
376
|
+
if code == 9 && (modifiers & Key::SHIFT) != 0
|
|
377
|
+
return KeyEvent.new(code: Key::BACKTAB, modifiers: modifiers & ~Key::SHIFT)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
case (key = CODE_KEYS[code])
|
|
381
|
+
when Symbol then KeyEvent.new(code: key, modifiers: modifiers)
|
|
382
|
+
when String then KeyEvent.new(code: Key::CHAR, char: key, modifiers: modifiers)
|
|
383
|
+
else
|
|
384
|
+
char = codepoint_char(shifted || code)
|
|
385
|
+
char && KeyEvent.new(code: Key::CHAR, char: char, modifiers: modifiers)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# +cp+ as a UTF-8 String, or nil if it is not a printable character:
|
|
390
|
+
# a C0 or C1 control code, DEL, a surrogate, past U+10FFFF, or in the
|
|
391
|
+
# private use area U+E000-U+F8FF, where the kitty keyboard protocol puts
|
|
392
|
+
# functional keys.
|
|
393
|
+
def codepoint_char(cp)
|
|
394
|
+
return nil if cp < 0x20 || cp.between?(0x7F, 0x9F) || cp.between?(0xD800, 0xDFFF) ||
|
|
395
|
+
cp.between?(0xE000, 0xF8FF) || cp > 0x10FFFF
|
|
396
|
+
|
|
397
|
+
cp.chr(Encoding::UTF_8)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# +event+ with +modifiers+ added, or nil if +event+ is nil
|
|
401
|
+
def with_modifiers(event, modifiers)
|
|
402
|
+
event && KeyEvent.new(code: event.code, char: event.char, modifiers: event.modifiers | modifiers)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# CSI letter: ESC [ X, or ESC [ 1 ; MOD X. CSI R is a cursor position
|
|
406
|
+
# report, not F3.
|
|
286
407
|
def parse_csi_letter(letter)
|
|
287
408
|
case letter
|
|
288
409
|
when "A" then KeyEvent.new(code: Key::UP)
|
|
@@ -292,17 +413,26 @@ module RubyTUI
|
|
|
292
413
|
when "H" then KeyEvent.new(code: Key::HOME)
|
|
293
414
|
when "F" then KeyEvent.new(code: Key::END_KEY)
|
|
294
415
|
when "Z" then KeyEvent.new(code: Key::BACKTAB)
|
|
416
|
+
when "P" then KeyEvent.new(code: Key::F1)
|
|
417
|
+
when "Q" then KeyEvent.new(code: Key::F2)
|
|
418
|
+
when "S" then KeyEvent.new(code: Key::F4)
|
|
295
419
|
else nil
|
|
296
420
|
end
|
|
297
421
|
end
|
|
298
422
|
|
|
299
|
-
# CSI tilde sequences: ESC [ N ~
|
|
423
|
+
# CSI tilde sequences: ESC [ N ~ or ESC [ N ; MOD ~
|
|
300
424
|
def parse_csi_tilde(n)
|
|
301
425
|
case n
|
|
426
|
+
when 1, 7 then KeyEvent.new(code: Key::HOME)
|
|
302
427
|
when 2 then KeyEvent.new(code: Key::INSERT)
|
|
303
428
|
when 3 then KeyEvent.new(code: Key::DELETE)
|
|
429
|
+
when 4, 8 then KeyEvent.new(code: Key::END_KEY)
|
|
304
430
|
when 5 then KeyEvent.new(code: Key::PAGE_UP)
|
|
305
431
|
when 6 then KeyEvent.new(code: Key::PAGE_DOWN)
|
|
432
|
+
when 11 then KeyEvent.new(code: Key::F1)
|
|
433
|
+
when 12 then KeyEvent.new(code: Key::F2)
|
|
434
|
+
when 13 then KeyEvent.new(code: Key::F3)
|
|
435
|
+
when 14 then KeyEvent.new(code: Key::F4)
|
|
306
436
|
when 15 then KeyEvent.new(code: Key::F5)
|
|
307
437
|
when 17 then KeyEvent.new(code: Key::F6)
|
|
308
438
|
when 18 then KeyEvent.new(code: Key::F7)
|
|
@@ -315,15 +445,6 @@ module RubyTUI
|
|
|
315
445
|
end
|
|
316
446
|
end
|
|
317
447
|
|
|
318
|
-
# CSI modified: ESC [ 1 ; MOD X (e.g., Shift+Up = ESC[1;2A)
|
|
319
|
-
def parse_csi_modified(letter, mod_code)
|
|
320
|
-
modifiers = decode_modifiers(mod_code)
|
|
321
|
-
base = parse_csi_letter(letter)
|
|
322
|
-
return nil unless base
|
|
323
|
-
|
|
324
|
-
KeyEvent.new(code: base.code, char: base.char, modifiers: modifiers)
|
|
325
|
-
end
|
|
326
|
-
|
|
327
448
|
# SS3 sequences: ESC O X (function keys on some terminals)
|
|
328
449
|
def parse_ss3(letter)
|
|
329
450
|
case letter
|
|
@@ -337,20 +458,19 @@ module RubyTUI
|
|
|
337
458
|
end
|
|
338
459
|
end
|
|
339
460
|
|
|
340
|
-
# Decode
|
|
341
|
-
#
|
|
461
|
+
# Decode a modifier parameter: 1 plus a bit field of Shift (1), Alt (2),
|
|
462
|
+
# Ctrl (4) and Super (8), e.g. 2=Shift, 5=Ctrl, 6=Shift+Ctrl, 9=Super.
|
|
463
|
+
# Higher bits are dropped; 0 counts as 1 (no modifiers).
|
|
342
464
|
def decode_modifiers(code)
|
|
343
|
-
|
|
344
|
-
mods = Key::NONE
|
|
345
|
-
mods |= Key::SHIFT if (val & 0b001) != 0
|
|
346
|
-
mods |= Key::ALT if (val & 0b010) != 0
|
|
347
|
-
mods |= Key::CTRL if (val & 0b100) != 0
|
|
348
|
-
mods
|
|
465
|
+
[code - 1, 0].max & MODIFIER_MASK
|
|
349
466
|
end
|
|
350
467
|
|
|
351
468
|
# SGR extended mouse mode: ESC [ < Cb ; Cx ; Cy M (press) or m (release)
|
|
352
|
-
# Cb encodes button + modifiers + motion flag
|
|
469
|
+
# Cb encodes button + modifiers + motion flag. Buttons 8-11 (Cb 128
|
|
470
|
+
# and up) give no event.
|
|
353
471
|
def parse_sgr_mouse(cb, cx, cy, terminator)
|
|
472
|
+
return nil if (cb & 128) != 0
|
|
473
|
+
|
|
354
474
|
# Coordinates are 1-based in SGR mode
|
|
355
475
|
x = cx - 1
|
|
356
476
|
y = cy - 1
|
|
@@ -392,9 +512,12 @@ module RubyTUI
|
|
|
392
512
|
MouseEvent.new(button: button, action: action, x: x, y: y, modifiers: mods)
|
|
393
513
|
end
|
|
394
514
|
|
|
395
|
-
# Legacy normal mouse: ESC [ M Cb Cx Cy (raw bytes, 32-based encoding)
|
|
515
|
+
# Legacy normal mouse: ESC [ M Cb Cx Cy (raw bytes, 32-based encoding).
|
|
516
|
+
# Buttons 8-11 (Cb 128 and up) give no event.
|
|
396
517
|
def parse_normal_mouse(raw)
|
|
397
518
|
cb = raw.getbyte(0) - 32
|
|
519
|
+
return nil if cb.negative? || (cb & 128) != 0
|
|
520
|
+
|
|
398
521
|
x = raw.getbyte(1) - 33
|
|
399
522
|
y = raw.getbyte(2) - 33
|
|
400
523
|
|
data/lib/rubytui/input/reader.rb
CHANGED
|
@@ -21,14 +21,12 @@ module RubyTUI
|
|
|
21
21
|
# String sequences that terminals write as replies (APC such as Kitty
|
|
22
22
|
# graphics responses, OSC, DCS, SOS, PM; see {Parser}) are read and
|
|
23
23
|
# discarded without an event when their terminator (ST or BEL) arrives.
|
|
24
|
-
# An ESC inside one that does not start ST ends it just before that ESC,
|
|
25
|
-
# also without an event.
|
|
26
24
|
#
|
|
27
|
-
# If
|
|
28
|
-
#
|
|
29
|
-
# returned as an Alt key (<tt>ESC P</tt> as Alt+P,
|
|
30
|
-
# Alt+], Alt+^ and Alt+_) and every byte after it, a
|
|
31
|
-
# included, is decoded as normal input.
|
|
25
|
+
# If an ESC that does not start ST comes first, or no byte arrives for
|
|
26
|
+
# {ESC_TIMEOUT_MS}, or the input ends, the sequence was typed, not sent:
|
|
27
|
+
# its introducer is returned as an Alt key (<tt>ESC P</tt> as Alt+P,
|
|
28
|
+
# likewise Alt+X, Alt+], Alt+^ and Alt+_) and every byte after it, a
|
|
29
|
+
# final lone ESC included, is decoded as normal input.
|
|
32
30
|
#
|
|
33
31
|
# If one reaches 4096 bytes with no terminator while bytes keep
|
|
34
32
|
# arriving, it is taken to be a reply: those bytes are discarded, and so
|
|
@@ -38,9 +36,10 @@ module RubyTUI
|
|
|
38
36
|
# ends; an ESC not yet discarded at that point is decoded as normal
|
|
39
37
|
# input.
|
|
40
38
|
#
|
|
41
|
-
# A CSI sequence with no final byte in its first 256 bytes
|
|
42
|
-
#
|
|
43
|
-
#
|
|
39
|
+
# A CSI sequence with no final byte in its first 256 bytes, or cut off
|
|
40
|
+
# by a pause of {ESC_TIMEOUT_MS}, the end of input or a byte that cannot
|
|
41
|
+
# continue it, was typed: it is returned as Alt+[, and the bytes after
|
|
42
|
+
# <tt>ESC [</tt> are decoded as normal input (see {Parser#parse_prefix}).
|
|
44
43
|
#
|
|
45
44
|
# When the input is a TTY that reports its size (+winsize+), {#poll}
|
|
46
45
|
# also returns a {ResizeEvent} after the terminal size changes.
|
data/lib/rubytui/version.rb
CHANGED
|
@@ -208,9 +208,11 @@ module RubyTUI
|
|
|
208
208
|
|
|
209
209
|
return if render_area.empty?
|
|
210
210
|
|
|
211
|
-
|
|
211
|
+
# Read the state and image together, as the loading thread sets both
|
|
212
|
+
state, image_widget = @mutex.synchronize { [@state, @image_widget] }
|
|
213
|
+
case state
|
|
212
214
|
when STATE_READY
|
|
213
|
-
|
|
215
|
+
image_widget&.render(render_area, buf)
|
|
214
216
|
when STATE_LOADING
|
|
215
217
|
render_placeholder(render_area, buf, @placeholder || "Loading...")
|
|
216
218
|
when STATE_ERROR
|
|
@@ -238,6 +238,7 @@ module RubyTUI
|
|
|
238
238
|
end
|
|
239
239
|
end
|
|
240
240
|
|
|
241
|
+
# A new frozen [top, right, bottom, left] Array, never the caller's
|
|
241
242
|
def normalize_padding(padding)
|
|
242
243
|
case padding
|
|
243
244
|
when nil
|
|
@@ -248,12 +249,12 @@ module RubyTUI
|
|
|
248
249
|
case padding.length
|
|
249
250
|
when 1 then [padding[0]] * 4
|
|
250
251
|
when 2 then [padding[0], padding[1], padding[0], padding[1]]
|
|
251
|
-
when 4 then padding
|
|
252
|
+
when 4 then padding.dup
|
|
252
253
|
else [0, 0, 0, 0]
|
|
253
254
|
end
|
|
254
255
|
else
|
|
255
256
|
[0, 0, 0, 0]
|
|
256
|
-
end
|
|
257
|
+
end.freeze
|
|
257
258
|
end
|
|
258
259
|
end
|
|
259
260
|
end
|
|
@@ -40,10 +40,12 @@ module RubyTUI
|
|
|
40
40
|
|
|
41
41
|
# @param x_bounds [Array(Numeric, Numeric), nil] <tt>[min, max]</tt> data range
|
|
42
42
|
# mapped to the canvas width (default: nil, computed from the x coordinates
|
|
43
|
-
# of points and lines)
|
|
43
|
+
# of points and lines). For given or computed bounds, an equal min and max
|
|
44
|
+
# are widened to <tt>max = min + 1</tt>
|
|
44
45
|
# @param y_bounds [Array(Numeric, Numeric), nil] <tt>[min, max]</tt> data range
|
|
45
46
|
# mapped to the canvas height, max at the top (default: nil, computed from
|
|
46
|
-
# the y coordinates of points and lines
|
|
47
|
+
# the y coordinates of points and lines; equal bounds are widened as for
|
|
48
|
+
# +x_bounds+)
|
|
47
49
|
# @param style [Style] style for labels added without a style (default: {Style::DEFAULT})
|
|
48
50
|
# @param marker_style [Style, nil] style for dots drawn without a style
|
|
49
51
|
# (default: white foreground)
|
|
@@ -186,7 +188,10 @@ module RubyTUI
|
|
|
186
188
|
# axis: 0 for x, 1 for y (index into points and into each line's endpoints)
|
|
187
189
|
def compute_bounds(bounds, canvas_size, axis)
|
|
188
190
|
if bounds
|
|
189
|
-
|
|
191
|
+
mn = bounds[0].to_f
|
|
192
|
+
mx = bounds[1].to_f
|
|
193
|
+
mx = mn + 1 if mx == mn
|
|
194
|
+
[mn, mx]
|
|
190
195
|
else
|
|
191
196
|
coords = @points.map { |p| p[axis] } + @lines.flat_map { |l| [l[axis], l[axis + 2]] }
|
|
192
197
|
return [0.0, canvas_size.to_f] if coords.empty?
|
|
@@ -29,19 +29,27 @@ module RubyTUI
|
|
|
29
29
|
# @param datasets [Array<Dataset>] series to plot; datasets with fewer than
|
|
30
30
|
# 2 points are skipped (default: [])
|
|
31
31
|
# @param x_axis [Array(String, Numeric, Numeric), nil] <tt>[label, min, max]</tt>;
|
|
32
|
-
# +min
|
|
32
|
+
# +min+ and +max+ may each be nil for a bound computed from the data (see
|
|
33
|
+
# below). The bottom row of
|
|
33
34
|
# the area is always reserved for the x axis line and its min/max values;
|
|
34
35
|
# when set, the label is also drawn centered on it, over the values. The
|
|
35
36
|
# label and min value are truncated to the plot width (whole grapheme
|
|
36
|
-
# clusters), and the max value is omitted
|
|
37
|
-
# value (default: nil, bounds computed from
|
|
37
|
+
# clusters), and the max value is omitted unless at least one column
|
|
38
|
+
# separates it from the min value (default: nil, bounds computed from
|
|
39
|
+
# the data and no label)
|
|
38
40
|
# @param y_axis [Array(String, Numeric, Numeric), nil] <tt>[label, min, max]</tt>;
|
|
39
|
-
# +min
|
|
40
|
-
# min/max values are drawn left of a vertical axis line and the
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
41
|
+
# +min+ and +max+ may each be nil for a bound computed from the data. When
|
|
42
|
+
# set, the min/max values are drawn left of a vertical axis line and the
|
|
43
|
+
# label's first grapheme cluster at mid-height, if it fits the label
|
|
44
|
+
# column. A plot (the area minus the x axis row) 1 row tall shows only
|
|
45
|
+
# the max value, and the label is drawn only when the plot is at least
|
|
46
|
+
# 3 rows tall. The label column is as wide as the wider of the min/max values
|
|
47
|
+
# drawn, i.e. of the final bounds (default: nil, no y axis; bounds
|
|
48
|
+
# computed from the data).
|
|
49
|
+
#
|
|
50
|
+
# For both axes, bounds with equal min and max are widened to
|
|
51
|
+
# <tt>max = min + 1</tt>, and a bound computed from the data that would
|
|
52
|
+
# reach or pass the explicit other bound is set 1 beyond it.
|
|
45
53
|
# @param style [Style, nil] base style of the area inside +block+: when it is
|
|
46
54
|
# not {Style::DEFAULT} it is patched onto every cell, and the axis lines,
|
|
47
55
|
# labels and plotted dots are drawn with this style patched ({Style#patch})
|
|
@@ -143,32 +151,29 @@ module RubyTUI
|
|
|
143
151
|
[format_num(y_min).length, format_num(y_max).length].max
|
|
144
152
|
end
|
|
145
153
|
|
|
146
|
-
def compute_x_bounds
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
mn =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
154
|
+
def compute_x_bounds = compute_bounds(@x_axis, 0)
|
|
155
|
+
def compute_y_bounds = compute_bounds(@y_axis, 1)
|
|
156
|
+
|
|
157
|
+
# [min, max] for an axis (<tt>[label, min, max]</tt> or nil): each
|
|
158
|
+
# explicit bound where set, otherwise the data's along coordinate
|
|
159
|
+
# +index+ (0 x, 1 y). Equal bounds are widened to max = min + 1; a
|
|
160
|
+
# computed bound that reaches or passes the explicit one is set 1
|
|
161
|
+
# beyond it.
|
|
162
|
+
def compute_bounds(axis, index)
|
|
163
|
+
_, mn, mx = axis
|
|
164
|
+
values = @datasets.flat_map { |ds| ds.data.map { |point| point[index] } }
|
|
165
|
+
if mn.nil? && mx.nil?
|
|
166
|
+
return [0.0, 1.0] if values.empty?
|
|
167
|
+
|
|
168
|
+
mn = values.min.to_f
|
|
169
|
+
mx = values.max.to_f
|
|
170
|
+
elsif mn.nil?
|
|
171
|
+
mn = values.min&.to_f || mx - 1
|
|
172
|
+
mn = mx - 1 if mn >= mx
|
|
173
|
+
elsif mx.nil?
|
|
174
|
+
mx = values.max&.to_f || mn + 1
|
|
175
|
+
mx = mn + 1 if mx <= mn
|
|
165
176
|
end
|
|
166
|
-
|
|
167
|
-
all_y = @datasets.flat_map { |ds| ds.data.map { |_, y| y } }
|
|
168
|
-
return [0.0, 1.0] if all_y.empty?
|
|
169
|
-
|
|
170
|
-
mn = all_y.min.to_f
|
|
171
|
-
mx = all_y.max.to_f
|
|
172
177
|
mx = mn + 1 if mx == mn
|
|
173
178
|
[mn, mx]
|
|
174
179
|
end
|
|
@@ -183,15 +188,19 @@ module RubyTUI
|
|
|
183
188
|
buf[chart_area.x - 1, y]&.set("\u2502", @axis_style) if chart_area.x > render_area.x
|
|
184
189
|
end
|
|
185
190
|
|
|
186
|
-
# Y labels at top and bottom
|
|
191
|
+
# Y labels at top and bottom; a 1-row plot has room for the max only
|
|
187
192
|
y_label_x = render_area.x
|
|
188
193
|
buf.set_string(y_label_x, chart_area.y, format_num(y_max), @label_style)
|
|
189
|
-
|
|
194
|
+
if chart_area.height > 1
|
|
195
|
+
buf.set_string(y_label_x, chart_area.bottom - 1, format_num(y_min), @label_style)
|
|
196
|
+
end
|
|
190
197
|
|
|
191
|
-
# Y axis label (vertically centered
|
|
192
|
-
|
|
198
|
+
# Y axis label (vertically centered): its first grapheme cluster, if it
|
|
199
|
+
# fits the label column and the plot has a row between the values
|
|
200
|
+
if label && !label.empty? && chart_area.height > 2
|
|
193
201
|
mid_y = chart_area.y + chart_area.height / 2
|
|
194
|
-
|
|
202
|
+
first = Unicode.truncate_to_width(label.grapheme_clusters.first, chart_area.x - 1 - render_area.x)
|
|
203
|
+
buf.set_string(render_area.x, mid_y, first, @label_style)
|
|
195
204
|
end
|
|
196
205
|
end
|
|
197
206
|
|
|
@@ -209,12 +218,12 @@ module RubyTUI
|
|
|
209
218
|
end
|
|
210
219
|
|
|
211
220
|
# X labels at left and right, truncated to the plot width; the max label
|
|
212
|
-
# is omitted
|
|
221
|
+
# is omitted unless at least one column separates it from the min label
|
|
213
222
|
min_str = Unicode.truncate_to_width(format_num(x_min), chart_area.width)
|
|
214
223
|
max_str = format_num(x_max)
|
|
215
224
|
buf.set_string(chart_area.x, x_axis_y, min_str, @label_style)
|
|
216
225
|
max_x = chart_area.right - Unicode.string_width(max_str)
|
|
217
|
-
if max_x
|
|
226
|
+
if max_x > chart_area.x + Unicode.string_width(min_str)
|
|
218
227
|
buf.set_string(max_x, x_axis_y, max_str, @label_style)
|
|
219
228
|
end
|
|
220
229
|
|
|
@@ -120,17 +120,20 @@ module RubyTUI
|
|
|
120
120
|
# On empty portion: gauge color as fg, default bg
|
|
121
121
|
empty_label_style = @label_style || Style.new.fg(gauge_fg).bold
|
|
122
122
|
|
|
123
|
+
# One cell per grapheme cluster; control characters are skipped
|
|
123
124
|
cx = label_x
|
|
124
|
-
label_text.
|
|
125
|
-
|
|
125
|
+
label_text.grapheme_clusters.each do |gc|
|
|
126
|
+
gc_w = Unicode.cluster_width(gc)
|
|
127
|
+
next if gc_w.zero?
|
|
128
|
+
|
|
126
129
|
col_offset = cx - render_area.x
|
|
127
130
|
char_style = col_offset < boundary ? filled_label_style : empty_label_style
|
|
128
|
-
buf[cx, label_y]&.set(
|
|
129
|
-
# Mark spacer for wide
|
|
130
|
-
if
|
|
131
|
+
buf[cx, label_y]&.set(gc, char_style)
|
|
132
|
+
# Mark spacer for wide clusters
|
|
133
|
+
if gc_w == 2 && cx + 1 < render_area.right
|
|
131
134
|
buf[cx + 1, label_y]&.set("", char_style)
|
|
132
135
|
end
|
|
133
|
-
cx +=
|
|
136
|
+
cx += gc_w
|
|
134
137
|
end
|
|
135
138
|
end
|
|
136
139
|
end
|
|
@@ -26,48 +26,66 @@ module RubyTUI
|
|
|
26
26
|
|
|
27
27
|
# Insert a character at the cursor position
|
|
28
28
|
#
|
|
29
|
+
# A cursor past the end (or before the start) of the text is first moved
|
|
30
|
+
# to the end (or start).
|
|
31
|
+
#
|
|
29
32
|
# @param char [String] character (or string) to insert; the cursor advances by its length
|
|
30
33
|
# @return [void]
|
|
31
34
|
def insert(char)
|
|
35
|
+
@cursor = @cursor.clamp(0, @text.length)
|
|
32
36
|
@text.insert(@cursor, char)
|
|
33
37
|
@cursor += char.length
|
|
34
38
|
end
|
|
35
39
|
|
|
36
|
-
# Delete
|
|
40
|
+
# Delete the grapheme cluster before the cursor (backspace)
|
|
37
41
|
#
|
|
38
|
-
#
|
|
42
|
+
# A letter with its combining marks, or an emoji sequence, is deleted as
|
|
43
|
+
# a whole, as is a cluster the cursor is inside; the cursor moves to the
|
|
44
|
+
# cluster's start. Does nothing when the cursor is at the start. A cursor
|
|
45
|
+
# past the end of the text moves back one position.
|
|
39
46
|
#
|
|
40
47
|
# @return [void]
|
|
41
48
|
def backspace
|
|
42
|
-
return if @cursor
|
|
49
|
+
return if @cursor <= 0
|
|
50
|
+
return @cursor -= 1 if @cursor > @text.length
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
@
|
|
52
|
+
range = cluster_at(@cursor - 1)
|
|
53
|
+
@text.slice!(range)
|
|
54
|
+
@cursor = range.begin
|
|
46
55
|
end
|
|
47
56
|
|
|
48
|
-
# Delete
|
|
57
|
+
# Delete the grapheme cluster at the cursor (delete key)
|
|
49
58
|
#
|
|
59
|
+
# The whole cluster is deleted; a cursor inside it moves to its start.
|
|
50
60
|
# Does nothing when the cursor is at the end.
|
|
51
61
|
#
|
|
52
62
|
# @return [void]
|
|
53
63
|
def delete
|
|
54
|
-
return if @cursor >= @text.length
|
|
64
|
+
return if @cursor >= @text.length || @cursor.negative?
|
|
55
65
|
|
|
56
|
-
|
|
66
|
+
range = cluster_at(@cursor)
|
|
67
|
+
@text.slice!(range)
|
|
68
|
+
@cursor = range.begin
|
|
57
69
|
end
|
|
58
70
|
|
|
59
|
-
# Move cursor left
|
|
71
|
+
# Move cursor left, to the start of the previous grapheme cluster
|
|
60
72
|
#
|
|
61
73
|
# @return [void]
|
|
62
74
|
def move_left
|
|
63
|
-
@cursor =
|
|
75
|
+
@cursor = if @cursor <= 0 then 0
|
|
76
|
+
elsif @cursor > @text.length then @cursor - 1
|
|
77
|
+
else cluster_at(@cursor - 1).begin
|
|
78
|
+
end
|
|
64
79
|
end
|
|
65
80
|
|
|
66
|
-
# Move cursor right
|
|
81
|
+
# Move cursor right, past the grapheme cluster at the cursor
|
|
67
82
|
#
|
|
68
83
|
# @return [void]
|
|
69
84
|
def move_right
|
|
70
|
-
@cursor =
|
|
85
|
+
@cursor = if @cursor >= @text.length then @text.length
|
|
86
|
+
elsif @cursor.negative? then 0
|
|
87
|
+
else cluster_at(@cursor).end
|
|
88
|
+
end
|
|
71
89
|
end
|
|
72
90
|
|
|
73
91
|
# Move cursor to start
|
|
@@ -131,10 +149,11 @@ module RubyTUI
|
|
|
131
149
|
|
|
132
150
|
# Handle a key event, returns true if the event was consumed
|
|
133
151
|
#
|
|
134
|
-
# Character keys without Ctrl insert <tt>event.char</tt>. Backspace, Delete,
|
|
152
|
+
# Character keys without Ctrl or Super insert <tt>event.char</tt>. Backspace, Delete,
|
|
135
153
|
# Left, Right, Home and End edit or move. Ctrl-A / Ctrl-E move to start / end,
|
|
136
154
|
# Ctrl-K / Ctrl-U delete to end / start, Ctrl-W deletes the previous word.
|
|
137
|
-
# Other Ctrl combinations, other keys and
|
|
155
|
+
# Other Ctrl combinations, Super (Cmd) combinations, other keys and
|
|
156
|
+
# non-{KeyEvent} events are not consumed.
|
|
138
157
|
#
|
|
139
158
|
# @param event [Event] the input event
|
|
140
159
|
# @return [Boolean] true if the event was consumed
|
|
@@ -152,6 +171,8 @@ module RubyTUI
|
|
|
152
171
|
when "w" then delete_word_back; true
|
|
153
172
|
else false
|
|
154
173
|
end
|
|
174
|
+
elsif event.super?
|
|
175
|
+
false
|
|
155
176
|
else
|
|
156
177
|
insert(event.char) if event.char
|
|
157
178
|
true
|
|
@@ -166,6 +187,20 @@ module RubyTUI
|
|
|
166
187
|
false
|
|
167
188
|
end
|
|
168
189
|
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
# Character range of the grapheme cluster that contains character
|
|
194
|
+
# +index+ (0 <= index < text length)
|
|
195
|
+
def cluster_at(index)
|
|
196
|
+
start = 0
|
|
197
|
+
@text.grapheme_clusters.each do |gc|
|
|
198
|
+
stop = start + gc.length
|
|
199
|
+
return start...stop if index < stop
|
|
200
|
+
|
|
201
|
+
start = stop
|
|
202
|
+
end
|
|
203
|
+
end
|
|
169
204
|
end
|
|
170
205
|
|
|
171
206
|
# InputField renders a single-line text input with cursor.
|
|
@@ -26,10 +26,11 @@ module RubyTUI
|
|
|
26
26
|
# into lines on newlines, a Span is split the same way into one-span Lines
|
|
27
27
|
# that keep its {Span#style}, +nil+ means no text, other objects are
|
|
28
28
|
# converted with +to_s+ (default: nil)
|
|
29
|
-
# @param style [Style] base style of the text. Each span is drawn with this
|
|
29
|
+
# @param style [Style, nil] base style of the text. Each span is drawn with this
|
|
30
30
|
# style patched ({Style#patch}) with {Text#style}, then {Line#style}, then
|
|
31
31
|
# {Span#style}, so later layers win. When it is not {Style::DEFAULT} it is
|
|
32
|
-
# also patched onto every cell of the area
|
|
32
|
+
# also patched onto every cell of the area. +nil+ is treated as
|
|
33
|
+
# {Style::DEFAULT} (default: {Style::DEFAULT})
|
|
33
34
|
# @param alignment [Symbol] +:left+ (default), +:center+ or +:right+
|
|
34
35
|
# @param wrap [Symbol] {WRAP_WORD} (default), {WRAP_CHAR} or {WRAP_NONE}. Wrapped
|
|
35
36
|
# lines take the style of the original line's first span and keep the
|
|
@@ -43,7 +44,7 @@ module RubyTUI
|
|
|
43
44
|
block: nil # optional Block to render inside
|
|
44
45
|
)
|
|
45
46
|
@text = normalize_text(text)
|
|
46
|
-
@style = style
|
|
47
|
+
@style = style || Style::DEFAULT
|
|
47
48
|
@alignment = alignment
|
|
48
49
|
@wrap = wrap
|
|
49
50
|
@block = block
|
|
@@ -137,7 +138,7 @@ module RubyTUI
|
|
|
137
138
|
|
|
138
139
|
words = plain.split(/(\s+)/)
|
|
139
140
|
wrapped = []
|
|
140
|
-
current = String.new
|
|
141
|
+
current = String.new(encoding: plain.encoding)
|
|
141
142
|
current_width = 0
|
|
142
143
|
|
|
143
144
|
words.each do |word|
|
|
@@ -165,7 +166,7 @@ module RubyTUI
|
|
|
165
166
|
line_style = line.spans.first&.style || Style::DEFAULT
|
|
166
167
|
|
|
167
168
|
wrapped = []
|
|
168
|
-
current = String.new
|
|
169
|
+
current = String.new(encoding: plain.encoding)
|
|
169
170
|
current_width = 0
|
|
170
171
|
|
|
171
172
|
# Break only between grapheme clusters, so conjuncts and ZWJ sequences stay whole
|
|
@@ -173,7 +174,7 @@ module RubyTUI
|
|
|
173
174
|
gc_width = Unicode.cluster_width(gc)
|
|
174
175
|
if current_width + gc_width > max_width
|
|
175
176
|
wrapped << Line.new([Span.new(current, line_style)], line.style) unless current.empty?
|
|
176
|
-
current = String.new
|
|
177
|
+
current = String.new(encoding: plain.encoding)
|
|
177
178
|
current_width = 0
|
|
178
179
|
end
|
|
179
180
|
current << gc
|
data/lib/rubytui.rb
CHANGED
|
@@ -104,10 +104,13 @@ module RubyTUI
|
|
|
104
104
|
# @param input [IO] input stream (default: $stdin)
|
|
105
105
|
# @param mouse [Boolean] enable mouse event reporting (default: false)
|
|
106
106
|
# @param alternate_screen [Boolean] enter alternate screen buffer (default: true)
|
|
107
|
+
# @param kitty_keyboard [Boolean] turn on the kitty keyboard protocol
|
|
108
|
+
# (see {.run}) (default: false)
|
|
107
109
|
# @return [Array(Terminal, Input::Reader)] terminal and reader pair
|
|
108
110
|
# @see .run
|
|
109
111
|
# @see .restore
|
|
110
|
-
def init(output: $stdout, input: $stdin, mouse: false, alternate_screen: true
|
|
112
|
+
def init(output: $stdout, input: $stdin, mouse: false, alternate_screen: true,
|
|
113
|
+
kitty_keyboard: false)
|
|
111
114
|
backend = AnsiBackend.new(output: output, input: input)
|
|
112
115
|
terminal = Terminal.new(backend: backend)
|
|
113
116
|
reader = Input::Reader.new(input: input)
|
|
@@ -115,6 +118,7 @@ module RubyTUI
|
|
|
115
118
|
backend.enable_raw_mode
|
|
116
119
|
backend.hide_cursor
|
|
117
120
|
terminal.enter_alternate_screen if alternate_screen
|
|
121
|
+
backend.enable_kitty_keyboard if kitty_keyboard
|
|
118
122
|
backend.enable_mouse if mouse
|
|
119
123
|
backend.flush
|
|
120
124
|
|
|
@@ -123,7 +127,8 @@ module RubyTUI
|
|
|
123
127
|
|
|
124
128
|
# Restore terminal to normal state.
|
|
125
129
|
#
|
|
126
|
-
# Disables mouse, shows cursor,
|
|
130
|
+
# Disables mouse, shows cursor, turns off the kitty keyboard protocol if
|
|
131
|
+
# {.init} turned it on, leaves alternate screen, and disables raw mode.
|
|
127
132
|
#
|
|
128
133
|
# @param terminal [Terminal] the terminal to restore
|
|
129
134
|
# @param alternate_screen [Boolean] whether alternate screen was used (must match {.init})
|
|
@@ -132,6 +137,7 @@ module RubyTUI
|
|
|
132
137
|
def restore(terminal, alternate_screen: true)
|
|
133
138
|
terminal.backend.disable_mouse
|
|
134
139
|
terminal.backend.show_cursor
|
|
140
|
+
terminal.backend.disable_kitty_keyboard
|
|
135
141
|
terminal.leave_alternate_screen if alternate_screen
|
|
136
142
|
terminal.backend.disable_raw_mode
|
|
137
143
|
terminal.backend.flush
|
|
@@ -147,6 +153,19 @@ module RubyTUI
|
|
|
147
153
|
# @param mouse [Boolean] enable mouse event reporting (default: false)
|
|
148
154
|
# @param alternate_screen [Boolean] use alternate screen buffer (default: true).
|
|
149
155
|
# Set to +false+ to keep output in terminal scrollback after exit.
|
|
156
|
+
# @param kitty_keyboard [Boolean] ask the terminal to report keys with the
|
|
157
|
+
# kitty keyboard protocol (default: false). On terminals that support it
|
|
158
|
+
# (kitty, Ghostty, iTerm2, Alacritty, foot; WezTerm with its
|
|
159
|
+
# +enable_kitty_keyboard+ setting), Ctrl+Shift+letter,
|
|
160
|
+
# Ctrl or Alt with digits and punctuation, and Super (Cmd) combinations
|
|
161
|
+
# that the terminal passes on are reported, and Ctrl+I, Ctrl+M, Ctrl+H
|
|
162
|
+
# and Ctrl+[ arrive as Ctrl+letter instead of Tab, Enter, Backspace and
|
|
163
|
+
# Escape. The Escape key arrives as a sequence of its own, without the
|
|
164
|
+
# wait that separates a lone ESC byte from an escape sequence. Plain and
|
|
165
|
+
# Shift-ed text, Enter, Tab and Backspace are unchanged, as are all keys
|
|
166
|
+
# on terminals without the protocol (such as macOS Terminal). The mode
|
|
167
|
+
# is turned off again when the block exits or raises.
|
|
168
|
+
# See {AnsiBackend#enable_kitty_keyboard}.
|
|
150
169
|
# @yield [terminal, reader] the initialized terminal and input reader
|
|
151
170
|
# @yieldparam terminal [Terminal]
|
|
152
171
|
# @yieldparam reader [Input::Reader]
|
|
@@ -165,9 +184,11 @@ module RubyTUI
|
|
|
165
184
|
# RubyTUI.run(alternate_screen: false) do |terminal, reader|
|
|
166
185
|
# terminal.draw { |frame| frame.buffer.set_string(0, 0, "Stays visible") }
|
|
167
186
|
# end
|
|
168
|
-
def run(output: $stdout, input: $stdin, mouse: false, alternate_screen: true
|
|
187
|
+
def run(output: $stdout, input: $stdin, mouse: false, alternate_screen: true,
|
|
188
|
+
kitty_keyboard: false)
|
|
169
189
|
terminal, reader = init(output: output, input: input, mouse: mouse,
|
|
170
|
-
alternate_screen: alternate_screen
|
|
190
|
+
alternate_screen: alternate_screen,
|
|
191
|
+
kitty_keyboard: kitty_keyboard)
|
|
171
192
|
yield terminal, reader
|
|
172
193
|
ensure
|
|
173
194
|
restore(terminal, alternate_screen: alternate_screen) if terminal
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubytui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- CoCL
|
|
@@ -84,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
84
84
|
- !ruby/object:Gem::Version
|
|
85
85
|
version: '0'
|
|
86
86
|
requirements: []
|
|
87
|
-
rubygems_version: 4.0.
|
|
87
|
+
rubygems_version: 4.0.23
|
|
88
88
|
specification_version: 4
|
|
89
89
|
summary: Zero-dependency terminal user interface library
|
|
90
90
|
test_files: []
|