ratatui_ruby 1.0.0 → 1.1.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/.builds/ruby-3.2.yml +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/AGENTS.md +3 -2
- data/CHANGELOG.md +33 -7
- data/Steepfile +1 -0
- data/doc/concepts/application_testing.md +5 -5
- data/doc/concepts/event_handling.md +1 -1
- data/doc/contributors/design/ruby_frontend.md +40 -12
- data/doc/contributors/design/rust_backend.md +13 -1
- data/doc/contributors/releasing.md +215 -0
- data/doc/contributors/todo/align/api_completeness_audit-finished.md +6 -0
- data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +1 -7
- data/doc/contributors/todo/align/term.md +351 -0
- data/doc/contributors/upstream_requests/paragraph_span_rects.md +259 -0
- data/doc/getting_started/quickstart.md +1 -1
- data/doc/getting_started/why.md +3 -3
- data/doc/images/app_external_editor.gif +0 -0
- data/doc/index.md +1 -6
- data/examples/app_external_editor/README.md +62 -0
- data/examples/app_external_editor/app.rb +344 -0
- data/examples/widget_list/app.rb +2 -4
- data/examples/widget_table/app.rb +8 -2
- data/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/ext/ratatui_ruby/src/events.rs +171 -203
- data/ext/ratatui_ruby/src/lib.rs +36 -0
- data/ext/ratatui_ruby/src/lib_header.rs +11 -0
- data/ext/ratatui_ruby/src/terminal/capabilities.rs +46 -0
- data/ext/ratatui_ruby/src/terminal/init.rs +92 -0
- data/ext/ratatui_ruby/src/terminal/mod.rs +12 -3
- data/ext/ratatui_ruby/src/terminal/queries.rs +15 -0
- data/ext/ratatui_ruby/src/terminal/query.rs +64 -2
- data/lib/ratatui_ruby/backend/window_size.rb +50 -0
- data/lib/ratatui_ruby/backend.rb +59 -0
- data/lib/ratatui_ruby/event/key/navigation.rb +10 -1
- data/lib/ratatui_ruby/event/key.rb +84 -0
- data/lib/ratatui_ruby/event/mouse.rb +95 -3
- data/lib/ratatui_ruby/event/resize.rb +45 -3
- data/lib/ratatui_ruby/layout/alignment.rb +91 -0
- data/lib/ratatui_ruby/layout/layout.rb +1 -2
- data/lib/ratatui_ruby/layout/size.rb +10 -3
- data/lib/ratatui_ruby/layout.rb +4 -0
- data/lib/ratatui_ruby/terminal/capabilities.rb +316 -0
- data/lib/ratatui_ruby/terminal/viewport.rb +1 -1
- data/lib/ratatui_ruby/terminal.rb +66 -0
- data/lib/ratatui_ruby/test_helper/global_state.rb +111 -0
- data/lib/ratatui_ruby/test_helper.rb +3 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby/widgets/table.rb +2 -2
- data/lib/ratatui_ruby.rb +25 -4
- data/sig/examples/app_external_editor/app.rbs +12 -0
- data/sig/generated/event_key_predicates.rbs +1348 -0
- data/sig/ratatui_ruby/backend/window_size.rbs +17 -0
- data/sig/ratatui_ruby/backend.rbs +12 -0
- data/sig/ratatui_ruby/event.rbs +7 -0
- data/sig/ratatui_ruby/layout/alignment.rbs +26 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -0
- data/sig/ratatui_ruby/terminal/capabilities.rbs +38 -0
- data/sig/ratatui_ruby/terminal/viewport.rbs +15 -1
- data/tasks/bump/bump_workflow.rb +49 -0
- data/tasks/bump/changelog.rb +57 -0
- data/tasks/bump/patch_release.rb +19 -0
- data/tasks/bump/release_branch.rb +17 -0
- data/tasks/bump/release_from_trunk.rb +49 -0
- data/tasks/bump/repository.rb +54 -0
- data/tasks/bump/ruby_gem.rb +6 -26
- data/tasks/bump/sem_ver.rb +4 -0
- data/tasks/bump/unreleased_section.rb +17 -0
- data/tasks/bump.rake +21 -11
- data/tasks/doc/documentation.rb +59 -0
- data/tasks/doc/link/file_url.rb +30 -0
- data/tasks/doc/link/relative_path.rb +61 -0
- data/tasks/doc/link/web_url.rb +55 -0
- data/tasks/doc/link.rb +52 -0
- data/tasks/doc/link_audit.rb +116 -0
- data/tasks/doc/problem.rb +40 -0
- data/tasks/doc/source_file.rb +93 -0
- data/tasks/doc.rake +18 -0
- data/tasks/rbs_predicates/predicate_catalog.rb +52 -0
- data/tasks/rbs_predicates/predicate_tests.rb +124 -0
- data/tasks/rbs_predicates/rbs_signature.rb +63 -0
- data/tasks/rbs_predicates.rake +31 -0
- data/tasks/test.rake +3 -0
- data/tasks/website/version.rb +23 -28
- metadata +38 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
//! during `draw()` callbacks return data from a pre-captured snapshot,
|
|
8
8
|
//! avoiding reentrancy issues with the terminal lock.
|
|
9
9
|
|
|
10
|
+
mod capabilities;
|
|
10
11
|
mod init;
|
|
11
12
|
mod mutations;
|
|
12
13
|
mod queries;
|
|
@@ -21,13 +22,21 @@ pub use storage::{
|
|
|
21
22
|
pub use wrapper::TerminalWrapper;
|
|
22
23
|
|
|
23
24
|
// Init/restore functions
|
|
24
|
-
pub use init::{
|
|
25
|
+
pub use init::{
|
|
26
|
+
get_terminal_size_instance, init_terminal, init_test_terminal, init_test_terminal_instance,
|
|
27
|
+
restore_terminal,
|
|
28
|
+
};
|
|
25
29
|
|
|
26
30
|
// Query functions
|
|
27
31
|
pub use queries::{
|
|
28
|
-
get_buffer_content, get_cell_at, get_cursor_position, get_terminal_area,
|
|
29
|
-
get_viewport_type,
|
|
32
|
+
frame_count, get_buffer_content, get_cell_at, get_cursor_position, get_terminal_area,
|
|
33
|
+
get_terminal_size, get_viewport_type,
|
|
30
34
|
};
|
|
31
35
|
|
|
32
36
|
// Mutation functions
|
|
33
37
|
pub use mutations::{insert_before, resize_terminal, set_cursor_position};
|
|
38
|
+
|
|
39
|
+
// Capability detection
|
|
40
|
+
pub use capabilities::{
|
|
41
|
+
available_color_count, force_color_output, supports_keyboard_enhancement, terminal_window_size,
|
|
42
|
+
};
|
|
@@ -214,3 +214,18 @@ fn modifiers_to_value(modifier: ratatui::style::Modifier) -> Value {
|
|
|
214
214
|
|
|
215
215
|
ary.as_value()
|
|
216
216
|
}
|
|
217
|
+
|
|
218
|
+
/// Returns the number of frames that have been drawn
|
|
219
|
+
pub fn frame_count() -> Result<usize, Error> {
|
|
220
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
221
|
+
|
|
222
|
+
crate::terminal::with_query(|q| Ok(q.frame_count())).unwrap_or_else(|| {
|
|
223
|
+
let module = ruby.define_module("RatatuiRuby").unwrap();
|
|
224
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
|
|
225
|
+
let error_class = error_base.const_get("Invariant").unwrap();
|
|
226
|
+
Err(Error::new(
|
|
227
|
+
error_class,
|
|
228
|
+
"Cannot query frame_count: terminal not initialized. Use RatatuiRuby.run or call init_terminal first.",
|
|
229
|
+
))
|
|
230
|
+
})
|
|
231
|
+
}
|
|
@@ -19,6 +19,7 @@ pub trait TerminalQuery {
|
|
|
19
19
|
fn is_test_mode(&self) -> bool;
|
|
20
20
|
fn cursor_position(&self) -> Option<(u16, u16)>;
|
|
21
21
|
fn cell_at(&self, x: u16, y: u16) -> Option<Cell>;
|
|
22
|
+
fn frame_count(&self) -> usize;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/// Snapshot of terminal state captured before draw.
|
|
@@ -30,6 +31,7 @@ pub struct DrawSnapshot {
|
|
|
30
31
|
pub is_test_mode: bool,
|
|
31
32
|
pub cursor_position: Option<(u16, u16)>,
|
|
32
33
|
pub buffer: Option<ratatui::buffer::Buffer>,
|
|
34
|
+
pub frame_count: usize,
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
impl DrawSnapshot {
|
|
@@ -38,18 +40,23 @@ impl DrawSnapshot {
|
|
|
38
40
|
match wrapper {
|
|
39
41
|
super::wrapper::TerminalWrapper::Crossterm(t) => {
|
|
40
42
|
let size = t.size().unwrap_or_default();
|
|
41
|
-
let
|
|
43
|
+
let frame = t.get_frame();
|
|
44
|
+
let viewport = frame.area();
|
|
45
|
+
let count = frame.count();
|
|
42
46
|
Self {
|
|
43
47
|
size: Rect::new(0, 0, size.width, size.height),
|
|
44
48
|
viewport_area: viewport,
|
|
45
49
|
is_test_mode: false,
|
|
46
50
|
cursor_position: None,
|
|
47
51
|
buffer: None,
|
|
52
|
+
frame_count: count,
|
|
48
53
|
}
|
|
49
54
|
}
|
|
50
55
|
super::wrapper::TerminalWrapper::Test(t) => {
|
|
51
56
|
let size = t.size().unwrap_or_default();
|
|
52
|
-
let
|
|
57
|
+
let frame = t.get_frame();
|
|
58
|
+
let viewport = frame.area();
|
|
59
|
+
let count = frame.count();
|
|
53
60
|
let cursor = t.get_cursor_position().ok().map(Into::into);
|
|
54
61
|
let buffer = t.backend().buffer().clone();
|
|
55
62
|
Self {
|
|
@@ -58,6 +65,7 @@ impl DrawSnapshot {
|
|
|
58
65
|
is_test_mode: true,
|
|
59
66
|
cursor_position: cursor,
|
|
60
67
|
buffer: Some(buffer),
|
|
68
|
+
frame_count: count,
|
|
61
69
|
}
|
|
62
70
|
}
|
|
63
71
|
}
|
|
@@ -80,6 +88,9 @@ impl TerminalQuery for DrawSnapshot {
|
|
|
80
88
|
fn cell_at(&self, x: u16, y: u16) -> Option<Cell> {
|
|
81
89
|
self.buffer.as_ref()?.cell((x, y)).cloned()
|
|
82
90
|
}
|
|
91
|
+
fn frame_count(&self) -> usize {
|
|
92
|
+
self.frame_count
|
|
93
|
+
}
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
/// Live queries against actual terminal (outside draw).
|
|
@@ -129,6 +140,12 @@ impl TerminalQuery for LiveTerminal<'_> {
|
|
|
129
140
|
super::wrapper::TerminalWrapper::Crossterm(_) => None,
|
|
130
141
|
}
|
|
131
142
|
}
|
|
143
|
+
fn frame_count(&self) -> usize {
|
|
144
|
+
match *self.0.borrow_mut() {
|
|
145
|
+
super::wrapper::TerminalWrapper::Crossterm(ref mut t) => t.get_frame().count(),
|
|
146
|
+
super::wrapper::TerminalWrapper::Test(ref mut t) => t.get_frame().count(),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
132
149
|
}
|
|
133
150
|
|
|
134
151
|
#[cfg(test)]
|
|
@@ -156,6 +173,9 @@ mod tests {
|
|
|
156
173
|
fn cell_at(&self, _x: u16, _y: u16) -> Option<Cell> {
|
|
157
174
|
None
|
|
158
175
|
}
|
|
176
|
+
fn frame_count(&self) -> usize {
|
|
177
|
+
0
|
|
178
|
+
}
|
|
159
179
|
}
|
|
160
180
|
|
|
161
181
|
#[test]
|
|
@@ -174,6 +194,7 @@ mod tests {
|
|
|
174
194
|
is_test_mode: false,
|
|
175
195
|
cursor_position: None,
|
|
176
196
|
buffer: None,
|
|
197
|
+
frame_count: 0,
|
|
177
198
|
};
|
|
178
199
|
assert_eq!(snapshot.size(), Rect::new(0, 0, 120, 40));
|
|
179
200
|
}
|
|
@@ -186,6 +207,7 @@ mod tests {
|
|
|
186
207
|
is_test_mode: false,
|
|
187
208
|
cursor_position: None,
|
|
188
209
|
buffer: None,
|
|
210
|
+
frame_count: 0,
|
|
189
211
|
};
|
|
190
212
|
// This should return the stored viewport_area, not Rect::default()
|
|
191
213
|
assert_eq!(snapshot.viewport_area(), Rect::new(5, 10, 80, 20));
|
|
@@ -199,6 +221,7 @@ mod tests {
|
|
|
199
221
|
is_test_mode: true, // TRUE!
|
|
200
222
|
cursor_position: None,
|
|
201
223
|
buffer: None,
|
|
224
|
+
frame_count: 0,
|
|
202
225
|
};
|
|
203
226
|
assert!(snapshot.is_test_mode());
|
|
204
227
|
}
|
|
@@ -211,6 +234,7 @@ mod tests {
|
|
|
211
234
|
is_test_mode: false,
|
|
212
235
|
cursor_position: Some((15, 20)), // Not None!
|
|
213
236
|
buffer: None,
|
|
237
|
+
frame_count: 0,
|
|
214
238
|
};
|
|
215
239
|
assert_eq!(snapshot.cursor_position(), Some((15, 20)));
|
|
216
240
|
}
|
|
@@ -229,6 +253,7 @@ mod tests {
|
|
|
229
253
|
is_test_mode: true,
|
|
230
254
|
cursor_position: None,
|
|
231
255
|
buffer: Some(buffer),
|
|
256
|
+
frame_count: 0,
|
|
232
257
|
};
|
|
233
258
|
|
|
234
259
|
let cell = snapshot.cell_at(5, 5);
|
|
@@ -335,4 +360,41 @@ mod tests {
|
|
|
335
360
|
assert_eq!(viewport.width, 80);
|
|
336
361
|
assert_eq!(viewport.height, 5);
|
|
337
362
|
}
|
|
363
|
+
|
|
364
|
+
#[test]
|
|
365
|
+
fn test_live_terminal_frame_count_increases_after_draw() {
|
|
366
|
+
use crate::terminal::TerminalWrapper;
|
|
367
|
+
use ratatui::{backend::TestBackend, Terminal};
|
|
368
|
+
|
|
369
|
+
let backend = TestBackend::new(80, 24);
|
|
370
|
+
let mut terminal = Terminal::new(backend).unwrap();
|
|
371
|
+
|
|
372
|
+
// Draw once to increment the counter
|
|
373
|
+
terminal.draw(|_frame| {}).unwrap();
|
|
374
|
+
|
|
375
|
+
let mut wrapper = TerminalWrapper::Test(terminal);
|
|
376
|
+
let live = super::LiveTerminal::new(&mut wrapper);
|
|
377
|
+
|
|
378
|
+
// Frame count should be 1 after one draw, NOT 0!
|
|
379
|
+
assert_eq!(live.frame_count(), 1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#[test]
|
|
383
|
+
fn test_live_terminal_frame_count_increases_with_multiple_draws() {
|
|
384
|
+
use crate::terminal::TerminalWrapper;
|
|
385
|
+
use ratatui::{backend::TestBackend, Terminal};
|
|
386
|
+
|
|
387
|
+
let backend = TestBackend::new(80, 24);
|
|
388
|
+
let mut terminal = Terminal::new(backend).unwrap();
|
|
389
|
+
|
|
390
|
+
// Draw twice
|
|
391
|
+
terminal.draw(|_frame| {}).unwrap();
|
|
392
|
+
terminal.draw(|_frame| {}).unwrap();
|
|
393
|
+
|
|
394
|
+
let mut wrapper = TerminalWrapper::Test(terminal);
|
|
395
|
+
let live = super::LiveTerminal::new(&mut wrapper);
|
|
396
|
+
|
|
397
|
+
// Frame count should be 2 after two draws!
|
|
398
|
+
assert_eq!(live.frame_count(), 2);
|
|
399
|
+
}
|
|
338
400
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module RatatuiRuby
|
|
9
|
+
module Backend
|
|
10
|
+
# Terminal window dimensions in characters and pixels.
|
|
11
|
+
#
|
|
12
|
+
# Some operations need both character grid size and pixel dimensions.
|
|
13
|
+
# Sixel graphics, image rendering, and precise layout calculations all
|
|
14
|
+
# benefit from knowing both measurements at once.
|
|
15
|
+
#
|
|
16
|
+
# This struct bundles both sizes together. It matches upstream Ratatui's
|
|
17
|
+
# <tt>backend::WindowSize</tt> struct exactly.
|
|
18
|
+
#
|
|
19
|
+
# Both fields are <tt>Layout::Size</tt> instances. This reuses the same
|
|
20
|
+
# type for character and pixel dimensions, matching upstream design.
|
|
21
|
+
#
|
|
22
|
+
# Note: Pixel dimensions may be zero on some systems. Unix marks these
|
|
23
|
+
# fields "unused" in TIOCGWINSZ. Windows does not implement them.
|
|
24
|
+
#
|
|
25
|
+
# === Example
|
|
26
|
+
#
|
|
27
|
+
#--
|
|
28
|
+
# SPDX-SnippetBegin
|
|
29
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
30
|
+
# SPDX-License-Identifier: MIT-0
|
|
31
|
+
#++
|
|
32
|
+
# ws = RatatuiRuby::Terminal.window_size
|
|
33
|
+
# if ws
|
|
34
|
+
# puts "#{ws.columns_rows.width}x#{ws.columns_rows.height} chars"
|
|
35
|
+
# puts "#{ws.pixels.width}x#{ws.pixels.height} pixels"
|
|
36
|
+
# end
|
|
37
|
+
#--
|
|
38
|
+
# SPDX-SnippetEnd
|
|
39
|
+
#++
|
|
40
|
+
class WindowSize < Data.define(:columns_rows, :pixels)
|
|
41
|
+
##
|
|
42
|
+
# :attr_reader: columns_rows
|
|
43
|
+
# Size of the window in characters (columns/rows) as <tt>Layout::Size</tt>.
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# :attr_reader: pixels
|
|
47
|
+
# Size of the window in pixels as <tt>Layout::Size</tt>.
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module RatatuiRuby
|
|
9
|
+
# Backend abstractions for terminal rendering.
|
|
10
|
+
#
|
|
11
|
+
# This module contains types related to terminal backend operations.
|
|
12
|
+
# It mirrors upstream Ratatui's <tt>backend</tt> module structure.
|
|
13
|
+
module Backend
|
|
14
|
+
class << self
|
|
15
|
+
# Queries terminal window size in characters and pixels.
|
|
16
|
+
#
|
|
17
|
+
# Some operations need both the character grid and pixel dimensions.
|
|
18
|
+
# Querying them separately wastes syscalls. Most backends fetch both
|
|
19
|
+
# at once anyway.
|
|
20
|
+
#
|
|
21
|
+
# This method queries crossterm for window dimensions. It returns a
|
|
22
|
+
# <tt>Backend::WindowSize</tt> with <tt>columns_rows</tt> and
|
|
23
|
+
# <tt>pixels</tt> fields, each as <tt>Layout::Size</tt> instances.
|
|
24
|
+
# Returns <tt>nil</tt> if the query fails.
|
|
25
|
+
#
|
|
26
|
+
# Note: Pixel dimensions may be zero on some systems. Unix marks
|
|
27
|
+
# these fields "unused" in TIOCGWINSZ. Windows does not implement them.
|
|
28
|
+
#
|
|
29
|
+
# === Example
|
|
30
|
+
#
|
|
31
|
+
#--
|
|
32
|
+
# SPDX-SnippetBegin
|
|
33
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
34
|
+
# SPDX-License-Identifier: MIT-0
|
|
35
|
+
#++
|
|
36
|
+
# ws = RatatuiRuby::Backend.window_size
|
|
37
|
+
# if ws
|
|
38
|
+
# puts "#{ws.columns_rows.width}x#{ws.columns_rows.height} chars"
|
|
39
|
+
# puts "#{ws.pixels.width}x#{ws.pixels.height} pixels"
|
|
40
|
+
# end
|
|
41
|
+
#--
|
|
42
|
+
# SPDX-SnippetEnd
|
|
43
|
+
#++
|
|
44
|
+
def window_size
|
|
45
|
+
window_size = Terminal._terminal_window_size
|
|
46
|
+
return nil unless window_size
|
|
47
|
+
columns, rows, px_width, px_height = window_size
|
|
48
|
+
WindowSize.new(
|
|
49
|
+
columns_rows: Layout::Size.new(width: columns, height: rows),
|
|
50
|
+
pixels: Layout::Size.new(width: px_width, height: px_height)
|
|
51
|
+
)
|
|
52
|
+
rescue
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
require_relative "backend/window_size"
|
|
@@ -39,9 +39,18 @@ module RatatuiRuby
|
|
|
39
39
|
return true if @code == "back_tab"
|
|
40
40
|
return true if @code == "tab" && @modifiers.include?("shift")
|
|
41
41
|
end
|
|
42
|
-
|
|
43
42
|
# DWIM: Check explicit aliases
|
|
44
43
|
navigation_aliases = {
|
|
44
|
+
# Arrow key aliases (disambiguate from Mouse#up? and Mouse#down?)
|
|
45
|
+
arrow_up: "up",
|
|
46
|
+
up_arrow: "up",
|
|
47
|
+
arrow_down: "down",
|
|
48
|
+
down_arrow: "down",
|
|
49
|
+
arrow_left: "left",
|
|
50
|
+
left_arrow: "left",
|
|
51
|
+
arrow_right: "right",
|
|
52
|
+
right_arrow: "right",
|
|
53
|
+
# Other navigation aliases
|
|
45
54
|
return: "enter",
|
|
46
55
|
back: "backspace",
|
|
47
56
|
del: "delete",
|
|
@@ -327,6 +327,60 @@ module RatatuiRuby
|
|
|
327
327
|
#++
|
|
328
328
|
# event.media_pause? # => true ONLY for media pause
|
|
329
329
|
# event.code == "pause" # => true ONLY for system pause
|
|
330
|
+
#
|
|
331
|
+
#--
|
|
332
|
+
# SPDX-SnippetEnd
|
|
333
|
+
#++
|
|
334
|
+
# === Arrow Key Aliases
|
|
335
|
+
#
|
|
336
|
+
# Arrow keys respond to <tt>arrow_up?</tt> and <tt>up_arrow?</tt> variants.
|
|
337
|
+
# This disambiguates from Mouse events, which also respond to <tt>up?</tt>
|
|
338
|
+
# and <tt>down?</tt>:
|
|
339
|
+
#
|
|
340
|
+
#--
|
|
341
|
+
# SPDX-SnippetBegin
|
|
342
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
343
|
+
# SPDX-License-Identifier: MIT-0
|
|
344
|
+
#++
|
|
345
|
+
# event.arrow_up? # => true for up arrow key
|
|
346
|
+
# event.up_arrow? # => true for up arrow key
|
|
347
|
+
# event.arrow_down? # => true for down arrow key
|
|
348
|
+
#
|
|
349
|
+
#--
|
|
350
|
+
# SPDX-SnippetEnd
|
|
351
|
+
#++
|
|
352
|
+
# === Key Prefix and Suffix
|
|
353
|
+
#
|
|
354
|
+
# Predicates accept <tt>key_</tt> prefix or <tt>_key</tt> suffix for explicit
|
|
355
|
+
# key event matching in mixed event contexts:
|
|
356
|
+
#
|
|
357
|
+
#--
|
|
358
|
+
# SPDX-SnippetBegin
|
|
359
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
360
|
+
# SPDX-License-Identifier: MIT-0
|
|
361
|
+
#++
|
|
362
|
+
# event.key_up? # => true for up arrow key
|
|
363
|
+
# event.key_q? # => true for "q" key
|
|
364
|
+
# event.q_key? # => true for "q" key
|
|
365
|
+
# event.enter_key? # => true for enter key
|
|
366
|
+
#
|
|
367
|
+
#--
|
|
368
|
+
# SPDX-SnippetEnd
|
|
369
|
+
#++
|
|
370
|
+
# === Capital Letters and Shift
|
|
371
|
+
#
|
|
372
|
+
# Capital letter predicates match shifted keys naturally. The terminal reports
|
|
373
|
+
# the produced character with shift in the modifiers:
|
|
374
|
+
#
|
|
375
|
+
#--
|
|
376
|
+
# SPDX-SnippetBegin
|
|
377
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
378
|
+
# SPDX-License-Identifier: MIT-0
|
|
379
|
+
#++
|
|
380
|
+
# event.G? # => true for code="G" modifiers=["shift"]
|
|
381
|
+
# event.shift_g? # => true for code="G" modifiers=["shift"]
|
|
382
|
+
# event.alt_B? # => true for code="B" modifiers=["alt", "shift"]
|
|
383
|
+
#
|
|
330
384
|
#--
|
|
331
385
|
# SPDX-SnippetEnd
|
|
332
386
|
#++
|
|
@@ -345,6 +399,36 @@ module RatatuiRuby
|
|
|
345
399
|
return true if match_navigation_dwim?(key_name, key_sym)
|
|
346
400
|
return true if match_system_dwim?(key_name, key_sym)
|
|
347
401
|
|
|
402
|
+
# DWIM: key_ prefix and _key suffix (disambiguate from mouse events)
|
|
403
|
+
# key_up? → up?, q_key? → q?, etc.
|
|
404
|
+
key_name = key_name.delete_prefix("key_").delete_suffix("_key")
|
|
405
|
+
|
|
406
|
+
# Fast path after prefix/suffix stripping
|
|
407
|
+
return true if self == key_name.to_sym
|
|
408
|
+
|
|
409
|
+
# DWIM: Single character codes match even with shift modifier present
|
|
410
|
+
# G? matches code="G" modifiers=["shift"], B? matches code="B" modifiers=["alt","shift"]
|
|
411
|
+
# @? matches code="@" modifiers=["shift"]
|
|
412
|
+
# The terminal reports the produced character, shift is implicit for these
|
|
413
|
+
if key_name.length == 1 && @code == key_name && @modifiers.include?("shift")
|
|
414
|
+
return true
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# DWIM: Uppercase in predicate implies shift, so alt_B? matches alt_shift_B
|
|
418
|
+
# Parse predicate to extract modifiers and final letter
|
|
419
|
+
if key_name.match?(/\A([a-z_]+_)?([A-Z])\z/)
|
|
420
|
+
pred_letter = key_name[-1]
|
|
421
|
+
pred_mods = key_name.chop.delete_suffix("_").split("_").reject(&:empty?)
|
|
422
|
+
expected_mods = (pred_mods + ["shift"]).sort
|
|
423
|
+
return true if @code == pred_letter && @modifiers == expected_mods
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# DWIM: Case-insensitive letter matching with modifiers
|
|
427
|
+
# shift_g? matches code="G" modifiers=["shift"]
|
|
428
|
+
if @code.length == 1 && @code.match?(/[A-Za-z]/) && (to_sym.to_s.downcase == key_name.downcase)
|
|
429
|
+
return true
|
|
430
|
+
end
|
|
431
|
+
|
|
348
432
|
# DWIM: Universal underscore-insensitivity
|
|
349
433
|
# Normalize both predicate and code by stripping underscores
|
|
350
434
|
normalized_predicate = key_name.delete("_")
|
|
@@ -110,6 +110,9 @@ module RatatuiRuby
|
|
|
110
110
|
@kind == "up"
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
alias mouse_down? down?
|
|
114
|
+
alias mouse_up? up?
|
|
115
|
+
|
|
113
116
|
# Returns true if mouse is being dragged.
|
|
114
117
|
def drag?
|
|
115
118
|
@kind == "drag"
|
|
@@ -137,6 +140,25 @@ module RatatuiRuby
|
|
|
137
140
|
@kind == "scroll_down"
|
|
138
141
|
end
|
|
139
142
|
|
|
143
|
+
# Returns true if event involves the left mouse button.
|
|
144
|
+
def left?
|
|
145
|
+
@button == "left"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns true if event involves the right mouse button.
|
|
149
|
+
def right?
|
|
150
|
+
@button == "right"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Returns true if event involves the middle mouse button.
|
|
154
|
+
def middle?
|
|
155
|
+
@button == "middle"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
alias left_button? left?
|
|
159
|
+
alias right_button? right?
|
|
160
|
+
alias middle_button? middle?
|
|
161
|
+
|
|
140
162
|
# Deconstructs the event for pattern matching.
|
|
141
163
|
#
|
|
142
164
|
#--
|
|
@@ -156,10 +178,80 @@ module RatatuiRuby
|
|
|
156
178
|
end
|
|
157
179
|
|
|
158
180
|
##
|
|
159
|
-
#
|
|
181
|
+
# Converts the event to a Symbol representation.
|
|
182
|
+
#
|
|
183
|
+
# The format varies by event type:
|
|
184
|
+
#
|
|
185
|
+
# [Left Button]
|
|
186
|
+
# <tt>:mouse_left_down</tt>, <tt>:mouse_left_up</tt>, <tt>:mouse_left_drag</tt>
|
|
187
|
+
# [Right Button]
|
|
188
|
+
# <tt>:mouse_right_down</tt>, <tt>:mouse_right_up</tt>, <tt>:mouse_right_drag</tt>
|
|
189
|
+
# [Middle Button]
|
|
190
|
+
# <tt>:mouse_middle_down</tt>, <tt>:mouse_middle_up</tt>, <tt>:mouse_middle_drag</tt>
|
|
191
|
+
# [Scroll]
|
|
192
|
+
# <tt>:scroll_up</tt>, <tt>:scroll_down</tt>
|
|
193
|
+
# [Move]
|
|
194
|
+
#--
|
|
195
|
+
# SPDX-SnippetBegin
|
|
196
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
197
|
+
# SPDX-License-Identifier: MIT-0
|
|
198
|
+
#++
|
|
199
|
+
# <tt>:mouse_moved</tt>
|
|
200
|
+
#
|
|
201
|
+
#--
|
|
202
|
+
# SPDX-SnippetEnd
|
|
203
|
+
#++
|
|
204
|
+
# === Example
|
|
205
|
+
#
|
|
206
|
+
#--
|
|
207
|
+
# SPDX-SnippetBegin
|
|
208
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
209
|
+
# SPDX-License-Identifier: MIT-0
|
|
210
|
+
#++
|
|
211
|
+
# event = Event::Mouse.new(kind: "down", x: 10, y: 5, button: "left")
|
|
212
|
+
# event.to_sym # => :mouse_left_down
|
|
213
|
+
#
|
|
214
|
+
# scroll = Event::Mouse.new(kind: "scroll_up", x: 0, y: 0, button: "none")
|
|
215
|
+
# scroll.to_sym # => :scroll_up
|
|
216
|
+
#--
|
|
217
|
+
# SPDX-SnippetEnd
|
|
218
|
+
#++
|
|
219
|
+
def to_sym
|
|
220
|
+
if @kind.start_with?("scroll")
|
|
221
|
+
@kind.to_sym
|
|
222
|
+
elsif @button == "none"
|
|
223
|
+
:"mouse_#{@kind}"
|
|
224
|
+
else
|
|
225
|
+
:"mouse_#{@button}_#{@kind}"
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
##
|
|
230
|
+
# Compares the event with another object.
|
|
231
|
+
#
|
|
232
|
+
# - If +other+ is a +Symbol+, compares against #to_sym.
|
|
233
|
+
# - If +other+ is a +Mouse+, compares as a value object.
|
|
234
|
+
# - Otherwise, returns +false+.
|
|
235
|
+
#
|
|
236
|
+
# === Example
|
|
237
|
+
#
|
|
238
|
+
#--
|
|
239
|
+
# SPDX-SnippetBegin
|
|
240
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
241
|
+
# SPDX-License-Identifier: MIT-0
|
|
242
|
+
#++
|
|
243
|
+
# if event == :mouse_left_down
|
|
244
|
+
# handle_click(event)
|
|
245
|
+
# end
|
|
246
|
+
#--
|
|
247
|
+
# SPDX-SnippetEnd
|
|
248
|
+
#++
|
|
160
249
|
def ==(other)
|
|
161
|
-
|
|
162
|
-
|
|
250
|
+
case other
|
|
251
|
+
when Symbol then to_sym == other
|
|
252
|
+
when Mouse then kind == other.kind && x == other.x && y == other.y && button == other.button && modifiers == other.modifiers
|
|
253
|
+
else false
|
|
254
|
+
end
|
|
163
255
|
end
|
|
164
256
|
end
|
|
165
257
|
end
|
|
@@ -104,10 +104,52 @@ module RatatuiRuby
|
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
##
|
|
107
|
-
#
|
|
107
|
+
# Converts the event to a Symbol representation.
|
|
108
|
+
#
|
|
109
|
+
# Always returns <tt>:resize</tt>.
|
|
110
|
+
#
|
|
111
|
+
# === Example
|
|
112
|
+
#
|
|
113
|
+
#--
|
|
114
|
+
# SPDX-SnippetBegin
|
|
115
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
116
|
+
# SPDX-License-Identifier: MIT-0
|
|
117
|
+
#++
|
|
118
|
+
# event = Event::Resize.new(width: 80, height: 24)
|
|
119
|
+
# event.to_sym # => :resize
|
|
120
|
+
#--
|
|
121
|
+
# SPDX-SnippetEnd
|
|
122
|
+
#++
|
|
123
|
+
def to_sym
|
|
124
|
+
:resize
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
##
|
|
128
|
+
# Compares the event with another object.
|
|
129
|
+
#
|
|
130
|
+
# - If +other+ is a +Symbol+, compares against #to_sym.
|
|
131
|
+
# - If +other+ is a +Resize+, compares as a value object.
|
|
132
|
+
# - Otherwise, returns +false+.
|
|
133
|
+
#
|
|
134
|
+
# === Example
|
|
135
|
+
#
|
|
136
|
+
#--
|
|
137
|
+
# SPDX-SnippetBegin
|
|
138
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
139
|
+
# SPDX-License-Identifier: MIT-0
|
|
140
|
+
#++
|
|
141
|
+
# if event == :resize
|
|
142
|
+
# handle_resize(event)
|
|
143
|
+
# end
|
|
144
|
+
#--
|
|
145
|
+
# SPDX-SnippetEnd
|
|
146
|
+
#++
|
|
108
147
|
def ==(other)
|
|
109
|
-
|
|
110
|
-
|
|
148
|
+
case other
|
|
149
|
+
when Symbol then to_sym == other
|
|
150
|
+
when Resize then width == other.width && height == other.height
|
|
151
|
+
else false
|
|
152
|
+
end
|
|
111
153
|
end
|
|
112
154
|
end
|
|
113
155
|
end
|