ratatui_ruby 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.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 +6 -0
- data/CHANGELOG.md +44 -7
- data/README.md +11 -4
- data/REUSE.toml +2 -7
- data/doc/application_architecture.md +84 -10
- data/doc/application_testing.md +75 -29
- data/doc/contributors/design/ruby_frontend.md +39 -3
- data/doc/contributors/design/rust_backend.md +1 -0
- data/doc/contributors/developing_examples.md +129 -44
- data/doc/contributors/examples_audit/p1_high.md +21 -0
- data/doc/contributors/examples_audit/p2_moderate.md +81 -0
- data/doc/contributors/examples_audit.md +41 -0
- data/doc/event_handling.md +11 -3
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_color_picker.png +0 -0
- data/doc/images/app_login_form.png +0 -0
- data/doc/images/app_stateful_interaction.png +0 -0
- data/doc/images/verify_quickstart_dsl.png +0 -0
- data/doc/images/verify_quickstart_layout.png +0 -0
- data/doc/images/verify_quickstart_lifecycle.png +0 -0
- data/doc/images/verify_readme_usage.png +0 -0
- data/doc/images/widget_barchart_demo.png +0 -0
- data/doc/images/widget_block_demo.png +0 -0
- data/doc/images/widget_canvas_demo.png +0 -0
- data/doc/images/widget_cell_demo.png +0 -0
- data/doc/images/widget_center_demo.png +0 -0
- data/doc/images/widget_chart_demo.png +0 -0
- data/doc/images/widget_list_demo.png +0 -0
- data/doc/images/widget_overlay_demo.png +0 -0
- data/doc/images/widget_render.png +0 -0
- data/doc/images/widget_rich_text.png +0 -0
- data/doc/images/widget_scroll_text.png +0 -0
- data/doc/images/widget_sparkline_demo.png +0 -0
- data/doc/images/widget_table_demo.png +0 -0
- data/doc/images/widget_tabs_demo.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/quickstart.md +69 -76
- data/doc/terminal_limitations.md +92 -0
- data/examples/app_all_events/README.md +45 -27
- data/examples/app_all_events/app.rb +38 -35
- data/examples/app_all_events/model/app_model.rb +157 -0
- data/examples/app_all_events/model/event_entry.rb +17 -0
- data/examples/app_all_events/model/msg.rb +37 -0
- data/examples/app_all_events/update.rb +73 -0
- data/examples/app_all_events/view/app_view.rb +8 -8
- data/examples/app_all_events/view/controls_view.rb +8 -6
- data/examples/app_all_events/view/counts_view.rb +12 -8
- data/examples/app_all_events/view/live_view.rb +8 -7
- data/examples/app_all_events/view/log_view.rb +10 -15
- data/examples/app_color_picker/README.md +84 -44
- data/examples/app_color_picker/app.rb +24 -62
- data/examples/app_color_picker/controls.rb +90 -0
- data/examples/app_color_picker/copy_dialog.rb +45 -49
- data/examples/app_color_picker/export_pane.rb +126 -0
- data/examples/app_color_picker/input.rb +99 -67
- data/examples/app_color_picker/main_container.rb +178 -0
- data/examples/app_color_picker/palette.rb +55 -26
- data/examples/app_login_form/README.md +47 -0
- data/examples/app_login_form/app.rb +2 -3
- data/examples/app_stateful_interaction/README.md +31 -0
- data/examples/app_stateful_interaction/app.rb +272 -0
- data/examples/timeout_demo.rb +43 -0
- data/examples/verify_quickstart_dsl/README.md +48 -0
- data/examples/verify_quickstart_dsl/app.rb +2 -0
- data/examples/verify_quickstart_layout/README.md +71 -0
- data/examples/verify_quickstart_layout/app.rb +2 -0
- data/examples/verify_quickstart_lifecycle/README.md +56 -0
- data/examples/verify_quickstart_lifecycle/app.rb +8 -2
- data/examples/verify_readme_usage/README.md +43 -0
- data/examples/verify_readme_usage/app.rb +8 -2
- data/examples/widget_barchart_demo/README.md +49 -0
- data/examples/widget_barchart_demo/app.rb +5 -5
- data/examples/widget_block_demo/README.md +34 -0
- data/examples/widget_block_demo/app.rb +256 -0
- data/examples/widget_box_demo/README.md +45 -0
- data/examples/widget_calendar_demo/README.md +39 -0
- data/examples/widget_canvas_demo/README.md +27 -0
- data/examples/widget_canvas_demo/app.rb +123 -0
- data/examples/widget_cell_demo/README.md +36 -0
- data/examples/widget_cell_demo/app.rb +31 -24
- data/examples/widget_center_demo/README.md +29 -0
- data/examples/widget_center_demo/app.rb +116 -0
- data/examples/widget_chart_demo/README.md +41 -0
- data/examples/widget_chart_demo/app.rb +7 -2
- data/examples/widget_gauge_demo/README.md +41 -0
- data/examples/widget_layout_split/README.md +44 -0
- data/examples/widget_line_gauge_demo/README.md +41 -0
- data/examples/widget_list_demo/README.md +49 -0
- data/examples/widget_list_demo/app.rb +91 -107
- data/examples/widget_map_demo/README.md +39 -0
- data/examples/{app_map_demo → widget_map_demo}/app.rb +2 -2
- data/examples/widget_overlay_demo/app.rb +248 -0
- data/examples/widget_popup_demo/README.md +36 -0
- data/examples/widget_ratatui_logo_demo/README.md +34 -0
- data/examples/widget_ratatui_mascot_demo/README.md +34 -0
- data/examples/widget_rect/README.md +38 -0
- data/examples/widget_render/README.md +37 -0
- data/examples/widget_rich_text/README.md +35 -0
- data/examples/widget_rich_text/app.rb +62 -33
- data/examples/widget_scroll_text/README.md +37 -0
- data/examples/widget_scroll_text/app.rb +0 -1
- data/examples/widget_scrollbar_demo/README.md +37 -0
- data/examples/widget_sparkline_demo/README.md +42 -0
- data/examples/widget_sparkline_demo/app.rb +4 -3
- data/examples/widget_style_colors/README.md +34 -0
- data/examples/widget_table_demo/README.md +48 -0
- data/examples/{app_table_select → widget_table_demo}/app.rb +46 -8
- data/examples/widget_tabs_demo/README.md +41 -0
- data/examples/widget_tabs_demo/app.rb +15 -1
- data/examples/widget_text_width/README.md +35 -0
- data/examples/widget_text_width/app.rb +106 -0
- data/exe/.gitkeep +0 -0
- data/ext/ratatui_ruby/Cargo.lock +11 -4
- data/ext/ratatui_ruby/Cargo.toml +2 -1
- data/ext/ratatui_ruby/src/events.rs +238 -26
- data/ext/ratatui_ruby/src/frame.rs +113 -1
- data/ext/ratatui_ruby/src/lib.rs +34 -4
- data/ext/ratatui_ruby/src/string_width.rs +101 -0
- data/ext/ratatui_ruby/src/terminal.rs +39 -15
- data/ext/ratatui_ruby/src/text.rs +1 -1
- data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
- data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/list.rs +179 -3
- data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
- data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +93 -1
- data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
- data/ext/ratatui_ruby/src/widgets/table.rs +113 -1
- data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
- data/lib/ratatui_ruby/cell.rb +4 -4
- data/lib/ratatui_ruby/event/key/character.rb +35 -0
- data/lib/ratatui_ruby/event/key/media.rb +44 -0
- data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
- data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
- data/lib/ratatui_ruby/event/key/system.rb +45 -0
- data/lib/ratatui_ruby/event/key.rb +111 -51
- data/lib/ratatui_ruby/event/mouse.rb +3 -3
- data/lib/ratatui_ruby/event/paste.rb +1 -1
- data/lib/ratatui_ruby/frame.rb +96 -0
- data/lib/ratatui_ruby/list_state.rb +88 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
- data/lib/ratatui_ruby/schema/cursor.rb +5 -0
- data/lib/ratatui_ruby/schema/gauge.rb +3 -1
- data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
- data/lib/ratatui_ruby/schema/list.rb +25 -4
- data/lib/ratatui_ruby/schema/list_item.rb +41 -0
- data/lib/ratatui_ruby/schema/rect.rb +43 -0
- data/lib/ratatui_ruby/schema/style.rb +24 -4
- data/lib/ratatui_ruby/schema/table.rb +21 -3
- data/lib/ratatui_ruby/schema/text.rb +69 -1
- data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
- data/lib/ratatui_ruby/session/autodoc.rb +65 -0
- data/lib/ratatui_ruby/session.rb +22 -7
- data/lib/ratatui_ruby/table_state.rb +90 -0
- data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
- data/lib/ratatui_ruby/test_helper/snapshot.rb +390 -0
- data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
- data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
- data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
- data/lib/ratatui_ruby/test_helper.rb +65 -358
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +42 -19
- data/sig/examples/app_stateful_interaction/app.rbs +33 -0
- data/sig/examples/widget_block_demo/app.rbs +32 -0
- data/sig/examples/{app_map_demo → widget_map_demo}/app.rbs +2 -2
- data/sig/examples/{app_table_select → widget_table_demo}/app.rbs +2 -2
- data/sig/examples/{widget_table_flex → widget_text_width}/app.rbs +2 -3
- data/sig/ratatui_ruby/event.rbs +11 -1
- data/sig/ratatui_ruby/frame.rbs +2 -0
- data/sig/ratatui_ruby/list_state.rbs +13 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -2
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
- data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
- data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
- data/sig/ratatui_ruby/schema/list.rbs +4 -2
- data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
- data/sig/ratatui_ruby/schema/rect.rbs +3 -0
- data/sig/ratatui_ruby/schema/style.rbs +3 -3
- data/sig/ratatui_ruby/schema/table.rbs +3 -1
- data/sig/ratatui_ruby/schema/text.rbs +8 -6
- data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
- data/sig/ratatui_ruby/session.rbs +13 -0
- data/sig/ratatui_ruby/table_state.rbs +15 -0
- data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
- data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
- data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
- data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
- data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
- data/sig/ratatui_ruby/test_helper.rbs +5 -4
- data/tasks/autodoc/examples.rb +79 -0
- data/tasks/autodoc/inventory.rb +9 -7
- data/tasks/autodoc.rake +11 -5
- data/tasks/bump/changelog.rb +3 -3
- data/tasks/bump/links.rb +67 -0
- data/tasks/sourcehut.rake +61 -21
- data/tasks/terminal_preview/app_screenshot.rb +13 -3
- data/tasks/terminal_preview/saved_screenshot.rb +4 -3
- metadata +111 -37
- data/doc/images/app_table_select.png +0 -0
- data/doc/images/widget_block_padding.png +0 -0
- data/doc/images/widget_block_titles.png +0 -0
- data/doc/images/widget_list_styles.png +0 -0
- data/examples/app_all_events/model/events.rb +0 -180
- data/examples/app_all_events/model/highlight.rb +0 -57
- data/examples/app_all_events/test/snapshots/after_focus_lost.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_focus_regained.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_key_a.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_mouse_click.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_multiple_events.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_paste.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_right_click.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/initial_state.txt +0 -24
- data/examples/app_all_events/view_state.rb +0 -42
- data/examples/app_color_picker/scene.rb +0 -201
- data/examples/widget_block_padding/app.rb +0 -67
- data/examples/widget_block_titles/app.rb +0 -69
- data/examples/widget_list_styles/app.rb +0 -141
- data/examples/widget_table_flex/app.rb +0 -95
- data/sig/examples/widget_block_padding/app.rbs +0 -11
- data/sig/examples/widget_block_titles/app.rbs +0 -11
- data/sig/examples/widget_list_styles/app.rbs +0 -11
- data/tasks/bump/comparison_links.rb +0 -41
- /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use crate::style::parse_block;
|
|
5
|
+
use crate::widgets::scrollbar_state::RubyScrollbarState;
|
|
5
6
|
use bumpalo::Bump;
|
|
6
|
-
use magnus::{prelude::*, Error, Symbol, Value};
|
|
7
|
+
use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
|
|
7
8
|
use ratatui::{
|
|
8
9
|
layout::Rect,
|
|
9
10
|
widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState},
|
|
@@ -89,6 +90,97 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
89
90
|
Ok(())
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
/// Renders a Scrollbar with an external state object.
|
|
94
|
+
///
|
|
95
|
+
/// The State object is the single source of truth for position and `content_length`.
|
|
96
|
+
/// Widget properties (`position`, `content_length`) are ignored.
|
|
97
|
+
pub fn render_stateful(
|
|
98
|
+
frame: &mut Frame,
|
|
99
|
+
area: Rect,
|
|
100
|
+
node: Value,
|
|
101
|
+
state_wrapper: Value,
|
|
102
|
+
) -> Result<(), Error> {
|
|
103
|
+
// Extract the RubyScrollbarState wrapper
|
|
104
|
+
let state: &RubyScrollbarState = TryConvert::try_convert(state_wrapper)?;
|
|
105
|
+
|
|
106
|
+
let orientation_sym: Symbol = node.funcall("orientation", ())?;
|
|
107
|
+
let thumb_symbol_val: Value = node.funcall("thumb_symbol", ())?;
|
|
108
|
+
let thumb_style_val: Value = node.funcall("thumb_style", ())?;
|
|
109
|
+
let track_symbol_val: Value = node.funcall("track_symbol", ())?;
|
|
110
|
+
let track_style_val: Value = node.funcall("track_style", ())?;
|
|
111
|
+
let begin_symbol_val: Value = node.funcall("begin_symbol", ())?;
|
|
112
|
+
let begin_style_val: Value = node.funcall("begin_style", ())?;
|
|
113
|
+
let end_symbol_val: Value = node.funcall("end_symbol", ())?;
|
|
114
|
+
let end_style_val: Value = node.funcall("end_style", ())?;
|
|
115
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
116
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
117
|
+
|
|
118
|
+
let mut scrollbar = Scrollbar::default();
|
|
119
|
+
|
|
120
|
+
scrollbar = match orientation_sym.to_string().as_str() {
|
|
121
|
+
"vertical_left" => scrollbar.orientation(ScrollbarOrientation::VerticalLeft),
|
|
122
|
+
"horizontal_bottom" | "horizontal" => {
|
|
123
|
+
scrollbar.orientation(ScrollbarOrientation::HorizontalBottom)
|
|
124
|
+
}
|
|
125
|
+
"horizontal_top" => scrollbar.orientation(ScrollbarOrientation::HorizontalTop),
|
|
126
|
+
_ => scrollbar.orientation(ScrollbarOrientation::VerticalRight),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Hoisted strings to extend lifetime
|
|
130
|
+
let thumb_str: String;
|
|
131
|
+
let track_str: String;
|
|
132
|
+
let begin_str: String;
|
|
133
|
+
let end_str: String;
|
|
134
|
+
|
|
135
|
+
if !thumb_symbol_val.is_nil() {
|
|
136
|
+
thumb_str = thumb_symbol_val.funcall("to_s", ())?;
|
|
137
|
+
scrollbar = scrollbar.thumb_symbol(&thumb_str);
|
|
138
|
+
}
|
|
139
|
+
if !thumb_style_val.is_nil() {
|
|
140
|
+
scrollbar = scrollbar.thumb_style(crate::style::parse_style(thumb_style_val)?);
|
|
141
|
+
}
|
|
142
|
+
if !track_symbol_val.is_nil() {
|
|
143
|
+
track_str = track_symbol_val.funcall("to_s", ())?;
|
|
144
|
+
scrollbar = scrollbar.track_symbol(Some(&track_str));
|
|
145
|
+
}
|
|
146
|
+
if !track_style_val.is_nil() {
|
|
147
|
+
scrollbar = scrollbar.track_style(crate::style::parse_style(track_style_val)?);
|
|
148
|
+
}
|
|
149
|
+
if !begin_symbol_val.is_nil() {
|
|
150
|
+
begin_str = begin_symbol_val.funcall("to_s", ())?;
|
|
151
|
+
scrollbar = scrollbar.begin_symbol(Some(&begin_str));
|
|
152
|
+
}
|
|
153
|
+
if !begin_style_val.is_nil() {
|
|
154
|
+
scrollbar = scrollbar.begin_style(crate::style::parse_style(begin_style_val)?);
|
|
155
|
+
}
|
|
156
|
+
if !end_symbol_val.is_nil() {
|
|
157
|
+
end_str = end_symbol_val.funcall("to_s", ())?;
|
|
158
|
+
scrollbar = scrollbar.end_symbol(Some(&end_str));
|
|
159
|
+
}
|
|
160
|
+
if !end_style_val.is_nil() {
|
|
161
|
+
scrollbar = scrollbar.end_style(crate::style::parse_style(end_style_val)?);
|
|
162
|
+
}
|
|
163
|
+
if !style_val.is_nil() {
|
|
164
|
+
scrollbar = scrollbar.style(crate::style::parse_style(style_val)?);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Borrow the inner ScrollbarState, render, and release the borrow immediately
|
|
168
|
+
{
|
|
169
|
+
let mut inner_state = state.borrow_mut();
|
|
170
|
+
if block_val.is_nil() {
|
|
171
|
+
frame.render_stateful_widget(scrollbar, area, &mut inner_state);
|
|
172
|
+
} else {
|
|
173
|
+
let bump = Bump::new();
|
|
174
|
+
let block = parse_block(block_val, &bump)?;
|
|
175
|
+
let inner_area = block.inner(area);
|
|
176
|
+
frame.render_widget(block, area);
|
|
177
|
+
frame.render_stateful_widget(scrollbar, inner_area, &mut inner_state);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
Ok(())
|
|
182
|
+
}
|
|
183
|
+
|
|
92
184
|
#[cfg(test)]
|
|
93
185
|
mod tests {
|
|
94
186
|
use super::*;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
//! `ScrollbarState` wrapper for exposing Ratatui's `ScrollbarState` to Ruby.
|
|
5
|
+
//!
|
|
6
|
+
//! This module provides `RubyScrollbarState`, a Magnus-wrapped struct that holds
|
|
7
|
+
//! a `RefCell<ScrollbarState>` for interior mutability during stateful rendering.
|
|
8
|
+
|
|
9
|
+
use magnus::{function, method, prelude::*, Error, Module, Ruby};
|
|
10
|
+
use ratatui::widgets::ScrollbarState;
|
|
11
|
+
use std::cell::RefCell;
|
|
12
|
+
|
|
13
|
+
/// A wrapper around Ratatui's `ScrollbarState` exposed to Ruby.
|
|
14
|
+
///
|
|
15
|
+
/// Ratatui's `ScrollbarState` doesn't expose getters for `position`, `content_length`,
|
|
16
|
+
/// or `viewport_content_length`. We track these values internally.
|
|
17
|
+
#[magnus::wrap(class = "RatatuiRuby::ScrollbarState")]
|
|
18
|
+
pub struct RubyScrollbarState {
|
|
19
|
+
inner: RefCell<ScrollbarState>,
|
|
20
|
+
/// We store these values ourselves since Ratatui's `ScrollbarState`
|
|
21
|
+
/// doesn't expose getters for them.
|
|
22
|
+
position_val: RefCell<usize>,
|
|
23
|
+
content_len: RefCell<usize>,
|
|
24
|
+
viewport_len: RefCell<usize>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl RubyScrollbarState {
|
|
28
|
+
/// Creates a new `RubyScrollbarState` with the given content length.
|
|
29
|
+
pub fn new(content_length: usize) -> Self {
|
|
30
|
+
Self {
|
|
31
|
+
inner: RefCell::new(ScrollbarState::new(content_length)),
|
|
32
|
+
position_val: RefCell::new(0),
|
|
33
|
+
content_len: RefCell::new(content_length),
|
|
34
|
+
viewport_len: RefCell::new(0),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Returns the current scroll position.
|
|
39
|
+
pub fn position(&self) -> usize {
|
|
40
|
+
*self.position_val.borrow()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Sets the current scroll position.
|
|
44
|
+
pub fn set_position(&self, position: usize) {
|
|
45
|
+
*self.position_val.borrow_mut() = position;
|
|
46
|
+
let mut state = self.inner.borrow_mut();
|
|
47
|
+
*state = state.position(position);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Returns the total content length.
|
|
51
|
+
pub fn content_length(&self) -> usize {
|
|
52
|
+
*self.content_len.borrow()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Sets the total content length.
|
|
56
|
+
pub fn set_content_length(&self, length: usize) {
|
|
57
|
+
*self.content_len.borrow_mut() = length;
|
|
58
|
+
let mut state = self.inner.borrow_mut();
|
|
59
|
+
*state = state.content_length(length);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Returns the viewport content length.
|
|
63
|
+
pub fn viewport_content_length(&self) -> usize {
|
|
64
|
+
*self.viewport_len.borrow()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Sets the viewport content length.
|
|
68
|
+
pub fn set_viewport_content_length(&self, length: usize) {
|
|
69
|
+
*self.viewport_len.borrow_mut() = length;
|
|
70
|
+
let mut state = self.inner.borrow_mut();
|
|
71
|
+
*state = state.viewport_content_length(length);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Scrolls to the first position.
|
|
75
|
+
pub fn first(&self) {
|
|
76
|
+
*self.position_val.borrow_mut() = 0;
|
|
77
|
+
self.inner.borrow_mut().first();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Scrolls to the last position.
|
|
81
|
+
pub fn last(&self) {
|
|
82
|
+
let content_len = *self.content_len.borrow();
|
|
83
|
+
let new_pos = content_len.saturating_sub(1);
|
|
84
|
+
*self.position_val.borrow_mut() = new_pos;
|
|
85
|
+
self.inner.borrow_mut().last();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Scrolls to the next position.
|
|
89
|
+
pub fn next(&self) {
|
|
90
|
+
let content_len = *self.content_len.borrow();
|
|
91
|
+
let current = *self.position_val.borrow();
|
|
92
|
+
let new_pos = (current + 1).min(content_len.saturating_sub(1));
|
|
93
|
+
*self.position_val.borrow_mut() = new_pos;
|
|
94
|
+
self.inner.borrow_mut().next();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Scrolls to the previous position.
|
|
98
|
+
pub fn prev(&self) {
|
|
99
|
+
let current = *self.position_val.borrow();
|
|
100
|
+
let new_pos = current.saturating_sub(1);
|
|
101
|
+
*self.position_val.borrow_mut() = new_pos;
|
|
102
|
+
self.inner.borrow_mut().prev();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Borrows the inner `ScrollbarState` mutably for rendering.
|
|
106
|
+
pub fn borrow_mut(&self) -> std::cell::RefMut<'_, ScrollbarState> {
|
|
107
|
+
self.inner.borrow_mut()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Registers the `ScrollbarState` class with Ruby.
|
|
112
|
+
pub fn register(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
|
|
113
|
+
let class = module.define_class("ScrollbarState", ruby.class_object())?;
|
|
114
|
+
class.define_singleton_method("new", function!(RubyScrollbarState::new, 1))?;
|
|
115
|
+
class.define_method("position", method!(RubyScrollbarState::position, 0))?;
|
|
116
|
+
class.define_method("position=", method!(RubyScrollbarState::set_position, 1))?;
|
|
117
|
+
class.define_method(
|
|
118
|
+
"content_length",
|
|
119
|
+
method!(RubyScrollbarState::content_length, 0),
|
|
120
|
+
)?;
|
|
121
|
+
class.define_method(
|
|
122
|
+
"content_length=",
|
|
123
|
+
method!(RubyScrollbarState::set_content_length, 1),
|
|
124
|
+
)?;
|
|
125
|
+
class.define_method(
|
|
126
|
+
"viewport_content_length",
|
|
127
|
+
method!(RubyScrollbarState::viewport_content_length, 0),
|
|
128
|
+
)?;
|
|
129
|
+
class.define_method(
|
|
130
|
+
"viewport_content_length=",
|
|
131
|
+
method!(RubyScrollbarState::set_viewport_content_length, 1),
|
|
132
|
+
)?;
|
|
133
|
+
class.define_method("first", method!(RubyScrollbarState::first, 0))?;
|
|
134
|
+
class.define_method("last", method!(RubyScrollbarState::last, 0))?;
|
|
135
|
+
class.define_method("next", method!(RubyScrollbarState::next, 0))?;
|
|
136
|
+
class.define_method("prev", method!(RubyScrollbarState::prev, 0))?;
|
|
137
|
+
Ok(())
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[cfg(test)]
|
|
141
|
+
mod tests {
|
|
142
|
+
use super::*;
|
|
143
|
+
|
|
144
|
+
#[test]
|
|
145
|
+
fn test_new_with_content_length() {
|
|
146
|
+
let state = RubyScrollbarState::new(100);
|
|
147
|
+
assert_eq!(state.content_length(), 100);
|
|
148
|
+
assert_eq!(state.position(), 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[test]
|
|
152
|
+
fn test_position_navigation() {
|
|
153
|
+
let state = RubyScrollbarState::new(10);
|
|
154
|
+
state.next();
|
|
155
|
+
assert_eq!(state.position(), 1);
|
|
156
|
+
state.prev();
|
|
157
|
+
assert_eq!(state.position(), 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[test]
|
|
161
|
+
fn test_first_and_last() {
|
|
162
|
+
let state = RubyScrollbarState::new(10);
|
|
163
|
+
state.set_position(5);
|
|
164
|
+
state.first();
|
|
165
|
+
assert_eq!(state.position(), 0);
|
|
166
|
+
state.last();
|
|
167
|
+
assert_eq!(state.position(), 9);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use crate::style::{parse_block, parse_style};
|
|
5
|
+
use crate::widgets::table_state::RubyTableState;
|
|
5
6
|
use bumpalo::Bump;
|
|
6
|
-
use magnus::{prelude::*, Error, Symbol, Value};
|
|
7
|
+
use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
|
|
7
8
|
use ratatui::{
|
|
8
9
|
layout::{Constraint, Flex, Rect},
|
|
9
10
|
widgets::{Cell, HighlightSpacing, Row, Table, TableState},
|
|
@@ -110,10 +111,121 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
110
111
|
state.select_column(Some(index));
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
let offset_val: Value = node.funcall("offset", ())?;
|
|
115
|
+
if !offset_val.is_nil() {
|
|
116
|
+
let offset: usize = offset_val.funcall("to_int", ())?;
|
|
117
|
+
*state.offset_mut() = offset;
|
|
118
|
+
}
|
|
119
|
+
|
|
113
120
|
frame.render_stateful_widget(table, area, &mut state);
|
|
114
121
|
Ok(())
|
|
115
122
|
}
|
|
116
123
|
|
|
124
|
+
/// Renders a Table with an external state object.
|
|
125
|
+
///
|
|
126
|
+
/// This function ignores `selected_row`, `selected_column`, and `offset` from the widget.
|
|
127
|
+
/// The State object is the single source of truth for selection and scroll position.
|
|
128
|
+
pub fn render_stateful(
|
|
129
|
+
frame: &mut Frame,
|
|
130
|
+
area: Rect,
|
|
131
|
+
node: Value,
|
|
132
|
+
state_wrapper: Value,
|
|
133
|
+
) -> Result<(), Error> {
|
|
134
|
+
let bump = Bump::new();
|
|
135
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
136
|
+
|
|
137
|
+
// Extract the RubyTableState wrapper
|
|
138
|
+
let state: &RubyTableState = TryConvert::try_convert(state_wrapper)?;
|
|
139
|
+
|
|
140
|
+
// Parse rows
|
|
141
|
+
let rows_value: Value = node.funcall("rows", ())?;
|
|
142
|
+
let rows_array = magnus::RArray::from_value(rows_value)
|
|
143
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for rows"))?;
|
|
144
|
+
let widths_val: Value = node.funcall("widths", ())?;
|
|
145
|
+
let widths_array = magnus::RArray::from_value(widths_val)
|
|
146
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for widths"))?;
|
|
147
|
+
|
|
148
|
+
let mut rows = Vec::new();
|
|
149
|
+
for i in 0..rows_array.len() {
|
|
150
|
+
let index = isize::try_from(i)
|
|
151
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
152
|
+
let row_val: Value = rows_array.entry(index)?;
|
|
153
|
+
rows.push(parse_row(row_val)?);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let constraints = parse_constraints(widths_array)?;
|
|
157
|
+
|
|
158
|
+
// Build table (ignoring selected_row, selected_column, offset — State is truth)
|
|
159
|
+
let header_val: Value = node.funcall("header", ())?;
|
|
160
|
+
let footer_val: Value = node.funcall("footer", ())?;
|
|
161
|
+
let highlight_style_val: Value = node.funcall("highlight_style", ())?;
|
|
162
|
+
let column_highlight_style_val: Value = node.funcall("column_highlight_style", ())?;
|
|
163
|
+
let cell_highlight_style_val: Value = node.funcall("cell_highlight_style", ())?;
|
|
164
|
+
let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
|
|
165
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
166
|
+
let flex_sym: Symbol = node.funcall("flex", ())?;
|
|
167
|
+
let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
|
|
168
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
169
|
+
let column_spacing_val: Value = node.funcall("column_spacing", ())?;
|
|
170
|
+
|
|
171
|
+
let flex = match flex_sym.to_string().as_str() {
|
|
172
|
+
"start" => Flex::Start,
|
|
173
|
+
"center" => Flex::Center,
|
|
174
|
+
"end" => Flex::End,
|
|
175
|
+
"space_between" => Flex::SpaceBetween,
|
|
176
|
+
"space_around" => Flex::SpaceAround,
|
|
177
|
+
"space_evenly" => Flex::SpaceEvenly,
|
|
178
|
+
_ => Flex::Legacy,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
let mut table = Table::new(rows, constraints).flex(flex);
|
|
182
|
+
|
|
183
|
+
let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
|
|
184
|
+
"always" => HighlightSpacing::Always,
|
|
185
|
+
"never" => HighlightSpacing::Never,
|
|
186
|
+
_ => HighlightSpacing::WhenSelected,
|
|
187
|
+
};
|
|
188
|
+
table = table.highlight_spacing(highlight_spacing);
|
|
189
|
+
|
|
190
|
+
if !header_val.is_nil() {
|
|
191
|
+
table = table.header(parse_row(header_val)?);
|
|
192
|
+
}
|
|
193
|
+
if !footer_val.is_nil() {
|
|
194
|
+
table = table.footer(parse_row(footer_val)?);
|
|
195
|
+
}
|
|
196
|
+
if !block_val.is_nil() {
|
|
197
|
+
table = table.block(parse_block(block_val, &bump)?);
|
|
198
|
+
}
|
|
199
|
+
if !highlight_style_val.is_nil() {
|
|
200
|
+
table = table.row_highlight_style(parse_style(highlight_style_val)?);
|
|
201
|
+
}
|
|
202
|
+
if !column_highlight_style_val.is_nil() {
|
|
203
|
+
table = table.column_highlight_style(parse_style(column_highlight_style_val)?);
|
|
204
|
+
}
|
|
205
|
+
if !cell_highlight_style_val.is_nil() {
|
|
206
|
+
table = table.cell_highlight_style(parse_style(cell_highlight_style_val)?);
|
|
207
|
+
}
|
|
208
|
+
if !highlight_symbol_val.is_nil() {
|
|
209
|
+
let symbol: String = highlight_symbol_val.funcall("to_s", ())?;
|
|
210
|
+
table = table.highlight_symbol(symbol);
|
|
211
|
+
}
|
|
212
|
+
if !style_val.is_nil() {
|
|
213
|
+
table = table.style(parse_style(style_val)?);
|
|
214
|
+
}
|
|
215
|
+
if !column_spacing_val.is_nil() {
|
|
216
|
+
let spacing: u16 = column_spacing_val.funcall("to_int", ())?;
|
|
217
|
+
table = table.column_spacing(spacing);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Borrow the inner TableState, render, and release the borrow immediately
|
|
221
|
+
{
|
|
222
|
+
let mut inner_state = state.borrow_mut();
|
|
223
|
+
frame.render_stateful_widget(table, area, &mut inner_state);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
Ok(())
|
|
227
|
+
}
|
|
228
|
+
|
|
117
229
|
fn parse_row(row_val: Value) -> Result<Row<'static>, Error> {
|
|
118
230
|
let ruby = magnus::Ruby::get().unwrap();
|
|
119
231
|
let row_array = magnus::RArray::from_value(row_val)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
//! `TableState` wrapper for exposing Ratatui's `TableState` to Ruby.
|
|
5
|
+
//!
|
|
6
|
+
//! This module provides `RubyTableState`, a Magnus-wrapped struct that holds
|
|
7
|
+
//! a `RefCell<TableState>` for interior mutability during stateful rendering.
|
|
8
|
+
//!
|
|
9
|
+
//! # Design
|
|
10
|
+
//!
|
|
11
|
+
//! When using `render_stateful_widget`, the State object is the single source
|
|
12
|
+
//! of truth for selection and offset. Widget properties (`selected_row`,
|
|
13
|
+
//! `selected_column`, `offset`) are ignored in stateful mode.
|
|
14
|
+
|
|
15
|
+
use magnus::{function, method, prelude::*, Error, Module, Ruby};
|
|
16
|
+
use ratatui::widgets::TableState;
|
|
17
|
+
use std::cell::RefCell;
|
|
18
|
+
|
|
19
|
+
/// A wrapper around Ratatui's `TableState` exposed to Ruby.
|
|
20
|
+
#[magnus::wrap(class = "RatatuiRuby::TableState")]
|
|
21
|
+
pub struct RubyTableState {
|
|
22
|
+
inner: RefCell<TableState>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl RubyTableState {
|
|
26
|
+
/// Creates a new `RubyTableState` with optional initial selection.
|
|
27
|
+
pub fn new(selected: Option<usize>) -> Self {
|
|
28
|
+
let mut state = TableState::default();
|
|
29
|
+
if let Some(idx) = selected {
|
|
30
|
+
state.select(Some(idx));
|
|
31
|
+
}
|
|
32
|
+
Self {
|
|
33
|
+
inner: RefCell::new(state),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Sets the selected row index.
|
|
38
|
+
pub fn select(&self, index: Option<usize>) {
|
|
39
|
+
self.inner.borrow_mut().select(index);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Returns the currently selected row index.
|
|
43
|
+
pub fn selected(&self) -> Option<usize> {
|
|
44
|
+
self.inner.borrow().selected()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Sets the selected column index.
|
|
48
|
+
pub fn select_column(&self, index: Option<usize>) {
|
|
49
|
+
self.inner.borrow_mut().select_column(index);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Returns the currently selected column index.
|
|
53
|
+
pub fn selected_column(&self) -> Option<usize> {
|
|
54
|
+
self.inner.borrow().selected_column()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Returns the current scroll offset.
|
|
58
|
+
pub fn offset(&self) -> usize {
|
|
59
|
+
self.inner.borrow().offset()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Scrolls down by the given number of rows.
|
|
63
|
+
pub fn scroll_down_by(&self, amount: u16) {
|
|
64
|
+
self.inner.borrow_mut().scroll_down_by(amount);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Scrolls up by the given number of rows.
|
|
68
|
+
pub fn scroll_up_by(&self, amount: u16) {
|
|
69
|
+
self.inner.borrow_mut().scroll_up_by(amount);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Borrows the inner `TableState` mutably for rendering.
|
|
73
|
+
pub fn borrow_mut(&self) -> std::cell::RefMut<'_, TableState> {
|
|
74
|
+
self.inner.borrow_mut()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Registers the `TableState` class with Ruby.
|
|
79
|
+
pub fn register(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
|
|
80
|
+
let class = module.define_class("TableState", ruby.class_object())?;
|
|
81
|
+
class.define_singleton_method("new", function!(RubyTableState::new, 1))?;
|
|
82
|
+
class.define_method("select", method!(RubyTableState::select, 1))?;
|
|
83
|
+
class.define_method("selected", method!(RubyTableState::selected, 0))?;
|
|
84
|
+
class.define_method("select_column", method!(RubyTableState::select_column, 1))?;
|
|
85
|
+
class.define_method(
|
|
86
|
+
"selected_column",
|
|
87
|
+
method!(RubyTableState::selected_column, 0),
|
|
88
|
+
)?;
|
|
89
|
+
class.define_method("offset", method!(RubyTableState::offset, 0))?;
|
|
90
|
+
class.define_method("scroll_down_by", method!(RubyTableState::scroll_down_by, 1))?;
|
|
91
|
+
class.define_method("scroll_up_by", method!(RubyTableState::scroll_up_by, 1))?;
|
|
92
|
+
Ok(())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[cfg(test)]
|
|
96
|
+
mod tests {
|
|
97
|
+
use super::*;
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn test_new_with_no_selection() {
|
|
101
|
+
let state = RubyTableState::new(None);
|
|
102
|
+
assert_eq!(state.selected(), None);
|
|
103
|
+
assert_eq!(state.selected_column(), None);
|
|
104
|
+
assert_eq!(state.offset(), 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn test_new_with_selection() {
|
|
109
|
+
let state = RubyTableState::new(Some(3));
|
|
110
|
+
assert_eq!(state.selected(), Some(3));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[test]
|
|
114
|
+
fn test_column_selection() {
|
|
115
|
+
let state = RubyTableState::new(None);
|
|
116
|
+
state.select_column(Some(2));
|
|
117
|
+
assert_eq!(state.selected_column(), Some(2));
|
|
118
|
+
state.select_column(None);
|
|
119
|
+
assert_eq!(state.selected_column(), None);
|
|
120
|
+
}
|
|
121
|
+
}
|
data/lib/ratatui_ruby/cell.rb
CHANGED
|
@@ -82,10 +82,10 @@ module RatatuiRuby
|
|
|
82
82
|
# [bg] Symbol or String (nullable).
|
|
83
83
|
# [modifiers] Array of Strings.
|
|
84
84
|
def initialize(symbol: nil, char: nil, fg: nil, bg: nil, modifiers: [])
|
|
85
|
-
@symbol = symbol || char || " "
|
|
86
|
-
@fg = fg
|
|
87
|
-
@bg = bg
|
|
88
|
-
@modifiers = modifiers.freeze
|
|
85
|
+
@symbol = (symbol || char || " ").freeze
|
|
86
|
+
@fg = fg&.freeze
|
|
87
|
+
@bg = bg&.freeze
|
|
88
|
+
@modifiers = modifiers.map(&:freeze).freeze
|
|
89
89
|
freeze
|
|
90
90
|
end
|
|
91
91
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
class Event
|
|
8
|
+
class Key < Event
|
|
9
|
+
# Methods for handling printable characters.
|
|
10
|
+
module Character
|
|
11
|
+
# Returns true if the key represents a single printable character.
|
|
12
|
+
#
|
|
13
|
+
# RatatuiRuby::Event::Key.new(code: "a").text? # => true
|
|
14
|
+
# RatatuiRuby::Event::Key.new(code: "enter").text? # => false
|
|
15
|
+
# RatatuiRuby::Event::Key.new(code: "space").text? # => false ("space" is not 1 char, " " is)
|
|
16
|
+
def text?
|
|
17
|
+
@code.length == 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the key as a printable character (if applicable).
|
|
21
|
+
#
|
|
22
|
+
# [Printable Characters]
|
|
23
|
+
# Returns the character itself (e.g., <tt>"a"</tt>, <tt>"1"</tt>, <tt>" "</tt>).
|
|
24
|
+
# [Special Keys]
|
|
25
|
+
# Returns <tt>nil</tt> (e.g., <tt>"enter"</tt>, <tt>"up"</tt>, <tt>"f1"</tt>).
|
|
26
|
+
#
|
|
27
|
+
# RatatuiRuby::Event::Key.new(code: "a").char # => "a"
|
|
28
|
+
# RatatuiRuby::Event::Key.new(code: "enter").char # => nil
|
|
29
|
+
def char
|
|
30
|
+
text? ? @code : nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
class Event
|
|
8
|
+
class Key < Event
|
|
9
|
+
# Methods and logic for media keys.
|
|
10
|
+
module Media
|
|
11
|
+
# Returns true if this is a media key.
|
|
12
|
+
#
|
|
13
|
+
# Media keys include: play, pause, stop, track controls, volume controls.
|
|
14
|
+
# These are only available in terminals supporting the Kitty keyboard protocol.
|
|
15
|
+
#
|
|
16
|
+
# event.media? # => true for media_play, media_pause, etc.
|
|
17
|
+
def media?
|
|
18
|
+
@kind == :media
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Handles media-specific DWIM logic for method_missing.
|
|
22
|
+
private def match_media_dwim?(key_name)
|
|
23
|
+
return false unless @kind == :media
|
|
24
|
+
|
|
25
|
+
# Allow unprefixed predicate
|
|
26
|
+
# e.g., pause? returns true for media_pause
|
|
27
|
+
if @code.start_with?("media_")
|
|
28
|
+
base_code = @code.delete_prefix("media_")
|
|
29
|
+
return true if key_name == base_code
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Bidirectional media overlaps
|
|
33
|
+
# e.g., play? and pause? both match media_play_pause
|
|
34
|
+
return true if @code == "media_play_pause" && (key_name == "play" || key_name == "pause")
|
|
35
|
+
|
|
36
|
+
# e.g., play_pause? matches media_play or media_pause
|
|
37
|
+
return true if key_name == "play_pause" && (@code == "media_play" || @code == "media_pause")
|
|
38
|
+
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|