ratatui_ruby 1.4.0-x86_64-linux
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 +7 -0
- data/LICENSE +15 -0
- data/LICENSES/AGPL-3.0-or-later.txt +661 -0
- data/LICENSES/CC-BY-SA-4.0.txt +427 -0
- data/LICENSES/CC0-1.0.txt +121 -0
- data/LICENSES/LGPL-3.0-or-later.txt +304 -0
- data/LICENSES/MIT-0.txt +16 -0
- data/LICENSES/MIT.txt +21 -0
- data/REUSE.toml +42 -0
- data/exe/.gitkeep +0 -0
- data/ext/ratatui_ruby/.cargo/config.toml +13 -0
- data/ext/ratatui_ruby/.gitignore +4 -0
- data/ext/ratatui_ruby/Cargo.lock +1737 -0
- data/ext/ratatui_ruby/Cargo.toml +24 -0
- data/ext/ratatui_ruby/clippy.toml +7 -0
- data/ext/ratatui_ruby/extconf.rb +21 -0
- data/ext/ratatui_ruby/src/color.rs +82 -0
- data/ext/ratatui_ruby/src/errors.rs +28 -0
- data/ext/ratatui_ruby/src/events.rs +700 -0
- data/ext/ratatui_ruby/src/frame.rs +241 -0
- data/ext/ratatui_ruby/src/lib.rs +343 -0
- data/ext/ratatui_ruby/src/lib_header.rs +11 -0
- data/ext/ratatui_ruby/src/rendering.rs +158 -0
- data/ext/ratatui_ruby/src/string_width.rs +101 -0
- data/ext/ratatui_ruby/src/style.rs +469 -0
- data/ext/ratatui_ruby/src/terminal/capabilities.rs +46 -0
- data/ext/ratatui_ruby/src/terminal/init.rs +233 -0
- data/ext/ratatui_ruby/src/terminal/mod.rs +42 -0
- data/ext/ratatui_ruby/src/terminal/mutations.rs +158 -0
- data/ext/ratatui_ruby/src/terminal/queries.rs +231 -0
- data/ext/ratatui_ruby/src/terminal/query.rs +400 -0
- data/ext/ratatui_ruby/src/terminal/storage.rs +109 -0
- data/ext/ratatui_ruby/src/terminal/wrapper.rs +16 -0
- data/ext/ratatui_ruby/src/text.rs +225 -0
- data/ext/ratatui_ruby/src/widgets/barchart.rs +169 -0
- data/ext/ratatui_ruby/src/widgets/block.rs +41 -0
- data/ext/ratatui_ruby/src/widgets/calendar.rs +84 -0
- data/ext/ratatui_ruby/src/widgets/canvas.rs +183 -0
- data/ext/ratatui_ruby/src/widgets/center.rs +79 -0
- data/ext/ratatui_ruby/src/widgets/chart.rs +222 -0
- data/ext/ratatui_ruby/src/widgets/clear.rs +39 -0
- data/ext/ratatui_ruby/src/widgets/cursor.rs +32 -0
- data/ext/ratatui_ruby/src/widgets/gauge.rs +65 -0
- data/ext/ratatui_ruby/src/widgets/layout.rs +379 -0
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +100 -0
- data/ext/ratatui_ruby/src/widgets/list.rs +378 -0
- data/ext/ratatui_ruby/src/widgets/list_state.rs +173 -0
- data/ext/ratatui_ruby/src/widgets/mod.rs +26 -0
- data/ext/ratatui_ruby/src/widgets/overlay.rs +24 -0
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +87 -0
- data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +40 -0
- data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +55 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +214 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
- data/ext/ratatui_ruby/src/widgets/sparkline.rs +127 -0
- data/ext/ratatui_ruby/src/widgets/table.rs +415 -0
- data/ext/ratatui_ruby/src/widgets/table_state.rs +203 -0
- data/ext/ratatui_ruby/src/widgets/tabs.rs +194 -0
- data/lib/ratatui_ruby/backend/window_size.rb +50 -0
- data/lib/ratatui_ruby/backend.rb +59 -0
- data/lib/ratatui_ruby/buffer/cell.rb +212 -0
- data/lib/ratatui_ruby/buffer.rb +149 -0
- data/lib/ratatui_ruby/cell.rb +208 -0
- data/lib/ratatui_ruby/debug.rb +215 -0
- data/lib/ratatui_ruby/draw.rb +63 -0
- data/lib/ratatui_ruby/event/focus_gained.rb +125 -0
- data/lib/ratatui_ruby/event/focus_lost.rb +127 -0
- data/lib/ratatui_ruby/event/key/character.rb +53 -0
- data/lib/ratatui_ruby/event/key/dwim.rb +301 -0
- data/lib/ratatui_ruby/event/key/media.rb +46 -0
- data/lib/ratatui_ruby/event/key/modifier.rb +107 -0
- data/lib/ratatui_ruby/event/key/navigation.rb +72 -0
- data/lib/ratatui_ruby/event/key/system.rb +47 -0
- data/lib/ratatui_ruby/event/key.rb +479 -0
- data/lib/ratatui_ruby/event/mouse.rb +291 -0
- data/lib/ratatui_ruby/event/none.rb +53 -0
- data/lib/ratatui_ruby/event/paste.rb +130 -0
- data/lib/ratatui_ruby/event/resize.rb +221 -0
- data/lib/ratatui_ruby/event/sync.rb +52 -0
- data/lib/ratatui_ruby/event.rb +163 -0
- data/lib/ratatui_ruby/frame.rb +257 -0
- data/lib/ratatui_ruby/labs/a11y.rb +182 -0
- data/lib/ratatui_ruby/labs/frame_a11y_capture.rb +50 -0
- data/lib/ratatui_ruby/labs.rb +47 -0
- data/lib/ratatui_ruby/layout/alignment.rb +91 -0
- data/lib/ratatui_ruby/layout/constraint.rb +337 -0
- data/lib/ratatui_ruby/layout/layout.rb +258 -0
- data/lib/ratatui_ruby/layout/position.rb +81 -0
- data/lib/ratatui_ruby/layout/rect.rb +733 -0
- data/lib/ratatui_ruby/layout/size.rb +62 -0
- data/lib/ratatui_ruby/layout.rb +29 -0
- data/lib/ratatui_ruby/list_state.rb +201 -0
- data/lib/ratatui_ruby/output_guard.rb +171 -0
- data/lib/ratatui_ruby/ratatui_ruby.so +0 -0
- data/lib/ratatui_ruby/scrollbar_state.rb +122 -0
- data/lib/ratatui_ruby/style/color.rb +149 -0
- data/lib/ratatui_ruby/style/style.rb +147 -0
- data/lib/ratatui_ruby/style.rb +19 -0
- data/lib/ratatui_ruby/symbols.rb +435 -0
- data/lib/ratatui_ruby/synthetic_events.rb +106 -0
- data/lib/ratatui_ruby/table_state.rb +251 -0
- data/lib/ratatui_ruby/terminal/capabilities.rb +316 -0
- data/lib/ratatui_ruby/terminal/viewport.rb +80 -0
- data/lib/ratatui_ruby/terminal.rb +66 -0
- data/lib/ratatui_ruby/terminal_lifecycle.rb +303 -0
- data/lib/ratatui_ruby/terminal_lifecycle.rb.bak +197 -0
- data/lib/ratatui_ruby/test_helper/event_injection.rb +241 -0
- data/lib/ratatui_ruby/test_helper/global_state.rb +111 -0
- data/lib/ratatui_ruby/test_helper/snapshot.rb +568 -0
- data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.ansi +24 -0
- data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.txt +24 -0
- data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.ansi +5 -0
- data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.txt +5 -0
- data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.ansi +24 -0
- data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.txt +24 -0
- data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.ansi +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.txt +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.ansi +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.txt +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.ansi +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.txt +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.ansi +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.txt +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.ansi +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.txt +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/my_snapshot.txt +1 -0
- data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.ansi +10 -0
- data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.txt +10 -0
- data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.ansi +10 -0
- data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.txt +10 -0
- data/lib/ratatui_ruby/test_helper/style_assertions.rb +449 -0
- data/lib/ratatui_ruby/test_helper/subprocess_timeout.rb +35 -0
- data/lib/ratatui_ruby/test_helper/terminal.rb +187 -0
- data/lib/ratatui_ruby/test_helper/test_doubles.rb +86 -0
- data/lib/ratatui_ruby/test_helper.rb +115 -0
- data/lib/ratatui_ruby/text/line.rb +245 -0
- data/lib/ratatui_ruby/text/span.rb +158 -0
- data/lib/ratatui_ruby/text.rb +99 -0
- data/lib/ratatui_ruby/tui/buffer_factories.rb +22 -0
- data/lib/ratatui_ruby/tui/canvas_factories.rb +149 -0
- data/lib/ratatui_ruby/tui/core.rb +67 -0
- data/lib/ratatui_ruby/tui/layout_factories.rb +153 -0
- data/lib/ratatui_ruby/tui/state_factories.rb +77 -0
- data/lib/ratatui_ruby/tui/style_factories.rb +22 -0
- data/lib/ratatui_ruby/tui/text_factories.rb +86 -0
- data/lib/ratatui_ruby/tui/widget_factories.rb +272 -0
- data/lib/ratatui_ruby/tui.rb +106 -0
- data/lib/ratatui_ruby/version.rb +12 -0
- data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +51 -0
- data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +29 -0
- data/lib/ratatui_ruby/widgets/bar_chart.rb +308 -0
- data/lib/ratatui_ruby/widgets/block.rb +266 -0
- data/lib/ratatui_ruby/widgets/calendar.rb +88 -0
- data/lib/ratatui_ruby/widgets/canvas.rb +297 -0
- data/lib/ratatui_ruby/widgets/cell.rb +59 -0
- data/lib/ratatui_ruby/widgets/center.rb +71 -0
- data/lib/ratatui_ruby/widgets/chart.rb +172 -0
- data/lib/ratatui_ruby/widgets/clear.rb +66 -0
- data/lib/ratatui_ruby/widgets/coerceable_widget.rb +77 -0
- data/lib/ratatui_ruby/widgets/cursor.rb +54 -0
- data/lib/ratatui_ruby/widgets/gauge.rb +146 -0
- data/lib/ratatui_ruby/widgets/line_gauge.rb +158 -0
- data/lib/ratatui_ruby/widgets/list.rb +252 -0
- data/lib/ratatui_ruby/widgets/list_item.rb +55 -0
- data/lib/ratatui_ruby/widgets/overlay.rb +55 -0
- data/lib/ratatui_ruby/widgets/paragraph.rb +113 -0
- data/lib/ratatui_ruby/widgets/ratatui_logo.rb +35 -0
- data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +40 -0
- data/lib/ratatui_ruby/widgets/row.rb +123 -0
- data/lib/ratatui_ruby/widgets/scrollbar.rb +147 -0
- data/lib/ratatui_ruby/widgets/shape/label.rb +80 -0
- data/lib/ratatui_ruby/widgets/sparkline.rb +153 -0
- data/lib/ratatui_ruby/widgets/table.rb +213 -0
- data/lib/ratatui_ruby/widgets/tabs.rb +91 -0
- data/lib/ratatui_ruby/widgets.rb +43 -0
- data/lib/ratatui_ruby.rb +555 -0
- data/sig/examples/app_all_events/app.rbs +11 -0
- data/sig/examples/app_all_events/model/app_model.rbs +23 -0
- data/sig/examples/app_all_events/model/event_entry.rbs +23 -0
- data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
- data/sig/examples/app_all_events/view/app_view.rbs +8 -0
- data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
- data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
- data/sig/examples/app_all_events/view/live_view.rbs +6 -0
- data/sig/examples/app_all_events/view/log_view.rbs +6 -0
- data/sig/examples/app_all_events/view.rbs +14 -0
- data/sig/examples/app_cli_rich_moments/app.rbs +12 -0
- data/sig/examples/app_color_picker/app.rbs +17 -0
- data/sig/examples/app_external_editor/app.rbs +12 -0
- data/sig/examples/app_login_form/app.rbs +11 -0
- data/sig/examples/app_stateful_interaction/app.rbs +39 -0
- data/sig/examples/verify_quickstart_dsl/app.rbs +17 -0
- data/sig/examples/verify_quickstart_lifecycle/app.rbs +17 -0
- data/sig/examples/verify_readme_usage/app.rbs +17 -0
- data/sig/examples/widget_block_demo/app.rbs +38 -0
- data/sig/examples/widget_box_demo/app.rbs +17 -0
- data/sig/examples/widget_calendar_demo/app.rbs +17 -0
- data/sig/examples/widget_cell_demo/app.rbs +17 -0
- data/sig/examples/widget_chart_demo/app.rbs +17 -0
- data/sig/examples/widget_gauge_demo/app.rbs +17 -0
- data/sig/examples/widget_layout_split/app.rbs +16 -0
- data/sig/examples/widget_line_gauge_demo/app.rbs +17 -0
- data/sig/examples/widget_list_demo/app.rbs +17 -0
- data/sig/examples/widget_map_demo/app.rbs +17 -0
- data/sig/examples/widget_popup_demo/app.rbs +17 -0
- data/sig/examples/widget_ratatui_logo_demo/app.rbs +17 -0
- data/sig/examples/widget_ratatui_mascot_demo/app.rbs +17 -0
- data/sig/examples/widget_rect/app.rbs +18 -0
- data/sig/examples/widget_render/app.rbs +16 -0
- data/sig/examples/widget_rich_text/app.rbs +17 -0
- data/sig/examples/widget_scroll_text/app.rbs +17 -0
- data/sig/examples/widget_scrollbar_demo/app.rbs +17 -0
- data/sig/examples/widget_sparkline_demo/app.rbs +16 -0
- data/sig/examples/widget_style_colors/app.rbs +20 -0
- data/sig/examples/widget_table_demo/app.rbs +17 -0
- data/sig/examples/widget_text_width/app.rbs +16 -0
- data/sig/generated/event_key_predicates.rbs +1348 -0
- data/sig/manifest.yaml +5 -0
- data/sig/patches/data.rbs +26 -0
- data/sig/patches/debugger__.rbs +8 -0
- data/sig/ratatui_ruby/backend/window_size.rbs +17 -0
- data/sig/ratatui_ruby/backend.rbs +12 -0
- data/sig/ratatui_ruby/buffer/cell.rbs +46 -0
- data/sig/ratatui_ruby/buffer.rbs +18 -0
- data/sig/ratatui_ruby/cell.rbs +44 -0
- data/sig/ratatui_ruby/clear.rbs +18 -0
- data/sig/ratatui_ruby/constraint.rbs +26 -0
- data/sig/ratatui_ruby/debug.rbs +45 -0
- data/sig/ratatui_ruby/draw.rbs +30 -0
- data/sig/ratatui_ruby/event.rbs +249 -0
- data/sig/ratatui_ruby/frame.rbs +23 -0
- data/sig/ratatui_ruby/interfaces.rbs +25 -0
- data/sig/ratatui_ruby/labs.rbs +90 -0
- data/sig/ratatui_ruby/layout/alignment.rbs +26 -0
- data/sig/ratatui_ruby/layout/constraint.rbs +39 -0
- data/sig/ratatui_ruby/layout/layout.rbs +45 -0
- data/sig/ratatui_ruby/layout/position.rbs +18 -0
- data/sig/ratatui_ruby/layout/rect.rbs +64 -0
- data/sig/ratatui_ruby/layout/size.rbs +18 -0
- data/sig/ratatui_ruby/list_state.rbs +23 -0
- data/sig/ratatui_ruby/output_guard.rbs +23 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +113 -0
- data/sig/ratatui_ruby/rect.rbs +17 -0
- data/sig/ratatui_ruby/scrollbar_state.rbs +24 -0
- data/sig/ratatui_ruby/session.rbs +93 -0
- data/sig/ratatui_ruby/style/color.rbs +22 -0
- data/sig/ratatui_ruby/style/style.rbs +29 -0
- data/sig/ratatui_ruby/symbols.rbs +141 -0
- data/sig/ratatui_ruby/synthetic_events.rbs +24 -0
- data/sig/ratatui_ruby/table_state.rbs +27 -0
- data/sig/ratatui_ruby/terminal/capabilities.rbs +38 -0
- data/sig/ratatui_ruby/terminal/viewport.rbs +33 -0
- data/sig/ratatui_ruby/terminal_lifecycle.rbs +39 -0
- data/sig/ratatui_ruby/test_helper/event_injection.rbs +22 -0
- data/sig/ratatui_ruby/test_helper/snapshot.rbs +37 -0
- data/sig/ratatui_ruby/test_helper/style_assertions.rbs +77 -0
- data/sig/ratatui_ruby/test_helper/terminal.rbs +20 -0
- data/sig/ratatui_ruby/test_helper/test_doubles.rbs +32 -0
- data/sig/ratatui_ruby/test_helper.rbs +18 -0
- data/sig/ratatui_ruby/text/line.rbs +27 -0
- data/sig/ratatui_ruby/text/span.rbs +23 -0
- data/sig/ratatui_ruby/text.rbs +12 -0
- data/sig/ratatui_ruby/tui/buffer_factories.rbs +16 -0
- data/sig/ratatui_ruby/tui/canvas_factories.rbs +38 -0
- data/sig/ratatui_ruby/tui/core.rbs +23 -0
- data/sig/ratatui_ruby/tui/layout_factories.rbs +39 -0
- data/sig/ratatui_ruby/tui/state_factories.rbs +23 -0
- data/sig/ratatui_ruby/tui/style_factories.rbs +18 -0
- data/sig/ratatui_ruby/tui/text_factories.rbs +23 -0
- data/sig/ratatui_ruby/tui/widget_factories.rbs +138 -0
- data/sig/ratatui_ruby/tui.rbs +25 -0
- data/sig/ratatui_ruby/version.rbs +12 -0
- data/sig/ratatui_ruby/widgets/bar_chart.rbs +95 -0
- data/sig/ratatui_ruby/widgets/block.rbs +51 -0
- data/sig/ratatui_ruby/widgets/calendar.rbs +45 -0
- data/sig/ratatui_ruby/widgets/canvas.rbs +95 -0
- data/sig/ratatui_ruby/widgets/chart.rbs +91 -0
- data/sig/ratatui_ruby/widgets/coerceable_widget.rbs +26 -0
- data/sig/ratatui_ruby/widgets/gauge.rbs +44 -0
- data/sig/ratatui_ruby/widgets/line_gauge.rbs +48 -0
- data/sig/ratatui_ruby/widgets/list.rbs +63 -0
- data/sig/ratatui_ruby/widgets/misc.rbs +158 -0
- data/sig/ratatui_ruby/widgets/paragraph.rbs +45 -0
- data/sig/ratatui_ruby/widgets/row.rbs +43 -0
- data/sig/ratatui_ruby/widgets/scrollbar.rbs +53 -0
- data/sig/ratatui_ruby/widgets/shape/label.rbs +37 -0
- data/sig/ratatui_ruby/widgets/sparkline.rbs +45 -0
- data/sig/ratatui_ruby/widgets/table.rbs +78 -0
- data/sig/ratatui_ruby/widgets/tabs.rbs +44 -0
- data/sig/ratatui_ruby/widgets.rbs +16 -0
- data/vendor/goodcop/base.yml +1047 -0
- metadata +729 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use crate::errors::type_error_with_context;
|
|
5
|
+
use crate::style::{parse_block, parse_style};
|
|
6
|
+
use crate::text::{parse_line, parse_span};
|
|
7
|
+
use crate::widgets::list_state::RubyListState;
|
|
8
|
+
use bumpalo::Bump;
|
|
9
|
+
use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
|
|
10
|
+
use ratatui::buffer::Buffer;
|
|
11
|
+
use ratatui::{
|
|
12
|
+
layout::Rect,
|
|
13
|
+
text::Line,
|
|
14
|
+
widgets::{HighlightSpacing, List, ListItem, ListState, StatefulWidget},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
18
|
+
let bump = Bump::new();
|
|
19
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
20
|
+
let items_val: Value = node.funcall("items", ())?;
|
|
21
|
+
let items_array = magnus::RArray::from_value(items_val)
|
|
22
|
+
.ok_or_else(|| type_error_with_context(&ruby, "expected array for items", items_val))?;
|
|
23
|
+
let selected_index_val: Value = node.funcall("selected_index", ())?;
|
|
24
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
25
|
+
let highlight_style_val: Value = node.funcall("highlight_style", ())?;
|
|
26
|
+
let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
|
|
27
|
+
let repeat_highlight_symbol_val: Value = node.funcall("repeat_highlight_symbol", ())?;
|
|
28
|
+
let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
|
|
29
|
+
let direction_val: Value = node.funcall("direction", ())?;
|
|
30
|
+
let scroll_padding_val: Value = node.funcall("scroll_padding", ())?;
|
|
31
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
32
|
+
|
|
33
|
+
let mut items: Vec<ListItem> = Vec::new();
|
|
34
|
+
for i in 0..items_array.len() {
|
|
35
|
+
let index = isize::try_from(i)
|
|
36
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
37
|
+
let item_val: Value = items_array.entry(index)?;
|
|
38
|
+
let item = parse_list_item(item_val)?;
|
|
39
|
+
items.push(item);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let symbol: String = if highlight_symbol_val.is_nil() {
|
|
43
|
+
String::new()
|
|
44
|
+
} else {
|
|
45
|
+
String::try_convert(highlight_symbol_val)?
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let mut state = ListState::default();
|
|
49
|
+
if !selected_index_val.is_nil() {
|
|
50
|
+
let index: usize = selected_index_val.funcall("to_int", ())?;
|
|
51
|
+
state.select(Some(index));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let offset_val: Value = node.funcall("offset", ())?;
|
|
55
|
+
if !offset_val.is_nil() {
|
|
56
|
+
let offset: usize = offset_val.funcall("to_int", ())?;
|
|
57
|
+
*state.offset_mut() = offset;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let mut list = List::new(items);
|
|
61
|
+
|
|
62
|
+
let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
|
|
63
|
+
"always" => HighlightSpacing::Always,
|
|
64
|
+
"never" => HighlightSpacing::Never,
|
|
65
|
+
_ => HighlightSpacing::WhenSelected,
|
|
66
|
+
};
|
|
67
|
+
list = list.highlight_spacing(highlight_spacing);
|
|
68
|
+
|
|
69
|
+
if !highlight_symbol_val.is_nil() {
|
|
70
|
+
list = list.highlight_symbol(Line::from(symbol));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if !repeat_highlight_symbol_val.is_nil() {
|
|
74
|
+
let repeat: bool = TryConvert::try_convert(repeat_highlight_symbol_val)?;
|
|
75
|
+
list = list.repeat_highlight_symbol(repeat);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if !direction_val.is_nil() {
|
|
79
|
+
let direction_sym: magnus::Symbol = TryConvert::try_convert(direction_val)?;
|
|
80
|
+
let direction_str = direction_sym.name().unwrap();
|
|
81
|
+
match direction_str.as_ref() {
|
|
82
|
+
"top_to_bottom" => list = list.direction(ratatui::widgets::ListDirection::TopToBottom),
|
|
83
|
+
"bottom_to_top" => list = list.direction(ratatui::widgets::ListDirection::BottomToTop),
|
|
84
|
+
_ => {
|
|
85
|
+
return Err(Error::new(
|
|
86
|
+
ruby.exception_arg_error(),
|
|
87
|
+
"direction must be :top_to_bottom or :bottom_to_top",
|
|
88
|
+
))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if !scroll_padding_val.is_nil() {
|
|
94
|
+
let padding: usize = TryConvert::try_convert(scroll_padding_val)?;
|
|
95
|
+
list = list.scroll_padding(padding);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if !style_val.is_nil() {
|
|
99
|
+
list = list.style(parse_style(style_val)?);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if !highlight_style_val.is_nil() {
|
|
103
|
+
list = list.highlight_style(parse_style(highlight_style_val)?);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if !block_val.is_nil() {
|
|
107
|
+
list = list.block(parse_block(block_val, &bump)?);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
StatefulWidget::render(list, area, buffer, &mut state);
|
|
111
|
+
Ok(())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Renders a List with an external state object.
|
|
115
|
+
///
|
|
116
|
+
/// This function ignores `selected_index` and `offset` from the widget.
|
|
117
|
+
/// The State object is the single source of truth for selection and scroll position.
|
|
118
|
+
pub fn render_stateful(
|
|
119
|
+
buffer: &mut Buffer,
|
|
120
|
+
area: Rect,
|
|
121
|
+
node: Value,
|
|
122
|
+
state_wrapper: Value,
|
|
123
|
+
) -> Result<(), Error> {
|
|
124
|
+
let bump = Bump::new();
|
|
125
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
126
|
+
|
|
127
|
+
// Extract the RubyListState wrapper
|
|
128
|
+
let state: &RubyListState = TryConvert::try_convert(state_wrapper)?;
|
|
129
|
+
|
|
130
|
+
// Build items
|
|
131
|
+
let items_val: Value = node.funcall("items", ())?;
|
|
132
|
+
let items_array = magnus::RArray::from_value(items_val)
|
|
133
|
+
.ok_or_else(|| type_error_with_context(&ruby, "expected array for items", items_val))?;
|
|
134
|
+
|
|
135
|
+
let mut items: Vec<ListItem> = Vec::new();
|
|
136
|
+
for i in 0..items_array.len() {
|
|
137
|
+
let index = isize::try_from(i)
|
|
138
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
139
|
+
let item_val: Value = items_array.entry(index)?;
|
|
140
|
+
let item = parse_list_item(item_val)?;
|
|
141
|
+
items.push(item);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Build widget (ignoring selected_index and offset — State is truth)
|
|
145
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
146
|
+
let highlight_style_val: Value = node.funcall("highlight_style", ())?;
|
|
147
|
+
let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
|
|
148
|
+
let repeat_highlight_symbol_val: Value = node.funcall("repeat_highlight_symbol", ())?;
|
|
149
|
+
let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
|
|
150
|
+
let direction_val: Value = node.funcall("direction", ())?;
|
|
151
|
+
let scroll_padding_val: Value = node.funcall("scroll_padding", ())?;
|
|
152
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
153
|
+
|
|
154
|
+
let symbol: String = if highlight_symbol_val.is_nil() {
|
|
155
|
+
String::new()
|
|
156
|
+
} else {
|
|
157
|
+
String::try_convert(highlight_symbol_val)?
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
let mut list = List::new(items);
|
|
161
|
+
|
|
162
|
+
let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
|
|
163
|
+
"always" => HighlightSpacing::Always,
|
|
164
|
+
"never" => HighlightSpacing::Never,
|
|
165
|
+
_ => HighlightSpacing::WhenSelected,
|
|
166
|
+
};
|
|
167
|
+
list = list.highlight_spacing(highlight_spacing);
|
|
168
|
+
|
|
169
|
+
if !highlight_symbol_val.is_nil() {
|
|
170
|
+
list = list.highlight_symbol(Line::from(symbol));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if !repeat_highlight_symbol_val.is_nil() {
|
|
174
|
+
let repeat: bool = TryConvert::try_convert(repeat_highlight_symbol_val)?;
|
|
175
|
+
list = list.repeat_highlight_symbol(repeat);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if !direction_val.is_nil() {
|
|
179
|
+
let direction_sym: magnus::Symbol = TryConvert::try_convert(direction_val)?;
|
|
180
|
+
let direction_str = direction_sym.name().unwrap();
|
|
181
|
+
match direction_str.as_ref() {
|
|
182
|
+
"top_to_bottom" => list = list.direction(ratatui::widgets::ListDirection::TopToBottom),
|
|
183
|
+
"bottom_to_top" => list = list.direction(ratatui::widgets::ListDirection::BottomToTop),
|
|
184
|
+
_ => {
|
|
185
|
+
return Err(Error::new(
|
|
186
|
+
ruby.exception_arg_error(),
|
|
187
|
+
"direction must be :top_to_bottom or :bottom_to_top",
|
|
188
|
+
))
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if !scroll_padding_val.is_nil() {
|
|
194
|
+
let padding: usize = TryConvert::try_convert(scroll_padding_val)?;
|
|
195
|
+
list = list.scroll_padding(padding);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if !style_val.is_nil() {
|
|
199
|
+
list = list.style(parse_style(style_val)?);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if !highlight_style_val.is_nil() {
|
|
203
|
+
list = list.highlight_style(parse_style(highlight_style_val)?);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if !block_val.is_nil() {
|
|
207
|
+
list = list.block(parse_block(block_val, &bump)?);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Borrow the inner ListState, render, and release the borrow immediately
|
|
211
|
+
{
|
|
212
|
+
let mut inner_state = state.borrow_mut();
|
|
213
|
+
StatefulWidget::render(list, area, buffer, &mut inner_state);
|
|
214
|
+
}
|
|
215
|
+
// Borrow is now released
|
|
216
|
+
|
|
217
|
+
Ok(())
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Parses a Ruby list item into a ratatui `ListItem`.
|
|
221
|
+
///
|
|
222
|
+
/// Accepts:
|
|
223
|
+
/// - `String`: Plain text item
|
|
224
|
+
/// - `Text::Span`: A single styled fragment
|
|
225
|
+
/// - `Text::Line`: A line composed of multiple spans
|
|
226
|
+
/// - `RatatuiRuby::ListItem`: A `ListItem` object with content and optional style
|
|
227
|
+
fn parse_list_item(value: Value) -> Result<ListItem<'static>, Error> {
|
|
228
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
229
|
+
|
|
230
|
+
// Check if it's a RatatuiRuby::ListItem
|
|
231
|
+
if let Ok(class_obj) = value.funcall::<_, _, Value>("class", ()) {
|
|
232
|
+
if let Ok(class_name) = class_obj.funcall::<_, _, String>("name", ()) {
|
|
233
|
+
if class_name.contains("ListItem") {
|
|
234
|
+
// Extract content and style from the ListItem
|
|
235
|
+
let content_val: Value = value.funcall("content", ())?;
|
|
236
|
+
let style_val: Value = value.funcall("style", ())?;
|
|
237
|
+
|
|
238
|
+
// Parse content as a Line
|
|
239
|
+
let line = if let Ok(s) = String::try_convert(content_val) {
|
|
240
|
+
Line::from(s)
|
|
241
|
+
} else if let Ok(line) = parse_line(content_val) {
|
|
242
|
+
line
|
|
243
|
+
} else if let Ok(span) = parse_span(content_val) {
|
|
244
|
+
Line::from(vec![span])
|
|
245
|
+
} else {
|
|
246
|
+
Line::from("")
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Parse and apply style if present
|
|
250
|
+
let mut item = ListItem::new(line);
|
|
251
|
+
if !style_val.is_nil() {
|
|
252
|
+
item = item.style(parse_style(style_val)?);
|
|
253
|
+
}
|
|
254
|
+
return Ok(item);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Try as String
|
|
260
|
+
if let Ok(s) = String::try_convert(value) {
|
|
261
|
+
return Ok(ListItem::new(Line::from(s)));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Try as Line
|
|
265
|
+
if let Ok(line) = parse_line(value) {
|
|
266
|
+
return Ok(ListItem::new(line));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Try as Span
|
|
270
|
+
if let Ok(span) = parse_span(value) {
|
|
271
|
+
return Ok(ListItem::new(Line::from(vec![span])));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
Err(type_error_with_context(
|
|
275
|
+
&ruby,
|
|
276
|
+
"expected String, Text::Span, Text::Line, or ListItem",
|
|
277
|
+
value,
|
|
278
|
+
))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#[cfg(test)]
|
|
282
|
+
mod tests {
|
|
283
|
+
use super::*;
|
|
284
|
+
use ratatui::buffer::Buffer;
|
|
285
|
+
use ratatui::widgets::List;
|
|
286
|
+
|
|
287
|
+
#[test]
|
|
288
|
+
fn test_list_rendering() {
|
|
289
|
+
let items = vec!["Item 1", "Item 2"];
|
|
290
|
+
let list = List::new(items)
|
|
291
|
+
.highlight_symbol(Line::from(">> "))
|
|
292
|
+
.style(ratatui::style::Style::default().fg(ratatui::style::Color::White))
|
|
293
|
+
.highlight_style(
|
|
294
|
+
ratatui::style::Style::default()
|
|
295
|
+
.fg(ratatui::style::Color::Yellow)
|
|
296
|
+
.add_modifier(ratatui::style::Modifier::BOLD),
|
|
297
|
+
);
|
|
298
|
+
let mut state = ListState::default();
|
|
299
|
+
state.select(Some(1));
|
|
300
|
+
|
|
301
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 2));
|
|
302
|
+
StatefulWidget::render(list, Rect::new(0, 0, 10, 2), &mut buf, &mut state);
|
|
303
|
+
|
|
304
|
+
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
305
|
+
assert!(content.contains("Item 1"));
|
|
306
|
+
assert!(content.contains(">> Item 2"));
|
|
307
|
+
|
|
308
|
+
// Check colors
|
|
309
|
+
assert_eq!(buf.cell((0, 0)).unwrap().fg, ratatui::style::Color::White);
|
|
310
|
+
assert_eq!(buf.cell((0, 1)).unwrap().fg, ratatui::style::Color::Yellow);
|
|
311
|
+
assert!(buf
|
|
312
|
+
.cell((0, 1))
|
|
313
|
+
.unwrap()
|
|
314
|
+
.modifier
|
|
315
|
+
.contains(ratatui::style::Modifier::BOLD));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#[test]
|
|
319
|
+
fn test_repeat_highlight_symbol() {
|
|
320
|
+
let items = vec!["Item 1", "Item 2"];
|
|
321
|
+
let list_without_repeat = List::new(items.clone()).highlight_symbol(Line::from(">> "));
|
|
322
|
+
let list_with_repeat = List::new(items)
|
|
323
|
+
.highlight_symbol(Line::from(">> "))
|
|
324
|
+
.repeat_highlight_symbol(true);
|
|
325
|
+
|
|
326
|
+
let mut state = ListState::default();
|
|
327
|
+
state.select(Some(0));
|
|
328
|
+
|
|
329
|
+
let mut buf1 = Buffer::empty(Rect::new(0, 0, 10, 2));
|
|
330
|
+
StatefulWidget::render(
|
|
331
|
+
list_without_repeat,
|
|
332
|
+
Rect::new(0, 0, 10, 2),
|
|
333
|
+
&mut buf1,
|
|
334
|
+
&mut state,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
let mut buf2 = Buffer::empty(Rect::new(0, 0, 10, 2));
|
|
338
|
+
StatefulWidget::render(
|
|
339
|
+
list_with_repeat,
|
|
340
|
+
Rect::new(0, 0, 10, 2),
|
|
341
|
+
&mut buf2,
|
|
342
|
+
&mut state,
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// Both should render, but the behavior might differ based on content width
|
|
346
|
+
let content1 = buf1
|
|
347
|
+
.content()
|
|
348
|
+
.iter()
|
|
349
|
+
.map(|c| c.symbol())
|
|
350
|
+
.collect::<String>();
|
|
351
|
+
let content2 = buf2
|
|
352
|
+
.content()
|
|
353
|
+
.iter()
|
|
354
|
+
.map(|c| c.symbol())
|
|
355
|
+
.collect::<String>();
|
|
356
|
+
assert!(!content1.is_empty());
|
|
357
|
+
assert!(!content2.is_empty());
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#[test]
|
|
361
|
+
fn test_scroll_padding() {
|
|
362
|
+
let items = vec!["Item 1", "Item 2", "Item 3", "Item 4"];
|
|
363
|
+
let list = List::new(items)
|
|
364
|
+
.scroll_padding(1)
|
|
365
|
+
.highlight_symbol(Line::from(">> "));
|
|
366
|
+
|
|
367
|
+
let mut state = ListState::default();
|
|
368
|
+
state.select(Some(1));
|
|
369
|
+
|
|
370
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 4));
|
|
371
|
+
StatefulWidget::render(list, Rect::new(0, 0, 15, 4), &mut buf, &mut state);
|
|
372
|
+
|
|
373
|
+
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
374
|
+
// With scroll padding, it should render but the exact behavior is handled by ratatui
|
|
375
|
+
assert!(!content.is_empty());
|
|
376
|
+
assert!(content.contains("Item"));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
//! `ListState` wrapper for exposing Ratatui's `ListState` to Ruby.
|
|
5
|
+
//!
|
|
6
|
+
//! This module provides `RubyListState`, a Magnus-wrapped struct that holds
|
|
7
|
+
//! a `RefCell<ListState>` 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_index`, `offset`)
|
|
13
|
+
//! are ignored in stateful mode.
|
|
14
|
+
//!
|
|
15
|
+
//! # Safety
|
|
16
|
+
//!
|
|
17
|
+
//! The `RefCell` is borrowed only during the `render_stateful_widget` call.
|
|
18
|
+
//! The borrow is released immediately after to avoid double-borrow panics
|
|
19
|
+
//! if a user inspects state inside a custom widget's render method.
|
|
20
|
+
|
|
21
|
+
use magnus::{function, method, prelude::*, Error, Module, Ruby};
|
|
22
|
+
use ratatui::widgets::ListState;
|
|
23
|
+
use std::cell::RefCell;
|
|
24
|
+
|
|
25
|
+
/// A wrapper around Ratatui's `ListState` exposed to Ruby.
|
|
26
|
+
///
|
|
27
|
+
/// This struct uses `RefCell` for interior mutability, allowing the state
|
|
28
|
+
/// to be updated during rendering while remaining accessible from Ruby.
|
|
29
|
+
#[magnus::wrap(class = "RatatuiRuby::ListState")]
|
|
30
|
+
pub struct RubyListState {
|
|
31
|
+
inner: RefCell<ListState>,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
impl RubyListState {
|
|
35
|
+
/// Creates a new `RubyListState` with optional initial selection.
|
|
36
|
+
///
|
|
37
|
+
/// # Arguments
|
|
38
|
+
///
|
|
39
|
+
/// * `selected` - Optional initial selection index
|
|
40
|
+
pub fn new(selected: Option<usize>) -> Self {
|
|
41
|
+
let mut state = ListState::default();
|
|
42
|
+
if let Some(idx) = selected {
|
|
43
|
+
state.select(Some(idx));
|
|
44
|
+
}
|
|
45
|
+
Self {
|
|
46
|
+
inner: RefCell::new(state),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Sets the selected index.
|
|
51
|
+
///
|
|
52
|
+
/// Pass `nil` to deselect.
|
|
53
|
+
pub fn select(&self, index: Option<usize>) {
|
|
54
|
+
self.inner.borrow_mut().select(index);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Returns the currently selected index, or `nil` if nothing is selected.
|
|
58
|
+
pub fn selected(&self) -> Option<usize> {
|
|
59
|
+
self.inner.borrow().selected()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Returns the current scroll offset.
|
|
63
|
+
///
|
|
64
|
+
/// This is the critical read-back method. After `render_stateful_widget`,
|
|
65
|
+
/// this returns the scroll position calculated by Ratatui to keep the
|
|
66
|
+
/// selection visible.
|
|
67
|
+
pub fn offset(&self) -> usize {
|
|
68
|
+
self.inner.borrow().offset()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Scrolls down by the given number of items.
|
|
72
|
+
pub fn scroll_down_by(&self, amount: u16) {
|
|
73
|
+
self.inner.borrow_mut().scroll_down_by(amount);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Scrolls up by the given number of items.
|
|
77
|
+
pub fn scroll_up_by(&self, amount: u16) {
|
|
78
|
+
self.inner.borrow_mut().scroll_up_by(amount);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Selects the next item or the first one if no item is selected.
|
|
82
|
+
///
|
|
83
|
+
/// Note: until the list is rendered, the number of items is not known, so the index
|
|
84
|
+
/// is set to 0 and will be corrected when the list is rendered.
|
|
85
|
+
pub fn select_next(&self) {
|
|
86
|
+
self.inner.borrow_mut().select_next();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Selects the previous item or the last one if no item is selected.
|
|
90
|
+
///
|
|
91
|
+
/// Note: until the list is rendered, the number of items is not known, so the index
|
|
92
|
+
/// is set to `usize::MAX` and will be corrected when the list is rendered.
|
|
93
|
+
pub fn select_previous(&self) {
|
|
94
|
+
self.inner.borrow_mut().select_previous();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Selects the first item.
|
|
98
|
+
pub fn select_first(&self) {
|
|
99
|
+
self.inner.borrow_mut().select_first();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Selects the last item.
|
|
103
|
+
///
|
|
104
|
+
/// Note: until the list is rendered, the number of items is not known, so the index
|
|
105
|
+
/// is set to `usize::MAX` and will be corrected when the list is rendered.
|
|
106
|
+
pub fn select_last(&self) {
|
|
107
|
+
self.inner.borrow_mut().select_last();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Borrows the inner `ListState` mutably for rendering.
|
|
111
|
+
///
|
|
112
|
+
/// # Safety
|
|
113
|
+
///
|
|
114
|
+
/// The caller must ensure the borrow is released before returning
|
|
115
|
+
/// control to Ruby to avoid double-borrow panics.
|
|
116
|
+
pub fn borrow_mut(&self) -> std::cell::RefMut<'_, ListState> {
|
|
117
|
+
self.inner.borrow_mut()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Registers the `ListState` class with Ruby.
|
|
122
|
+
pub fn register(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
|
|
123
|
+
let class = module.define_class("ListState", ruby.class_object())?;
|
|
124
|
+
class.define_singleton_method("new", function!(RubyListState::new, 1))?;
|
|
125
|
+
class.define_method("select", method!(RubyListState::select, 1))?;
|
|
126
|
+
class.define_method("selected", method!(RubyListState::selected, 0))?;
|
|
127
|
+
class.define_method("offset", method!(RubyListState::offset, 0))?;
|
|
128
|
+
class.define_method("scroll_down_by", method!(RubyListState::scroll_down_by, 1))?;
|
|
129
|
+
class.define_method("scroll_up_by", method!(RubyListState::scroll_up_by, 1))?;
|
|
130
|
+
class.define_method("select_next", method!(RubyListState::select_next, 0))?;
|
|
131
|
+
class.define_method(
|
|
132
|
+
"select_previous",
|
|
133
|
+
method!(RubyListState::select_previous, 0),
|
|
134
|
+
)?;
|
|
135
|
+
class.define_method("select_first", method!(RubyListState::select_first, 0))?;
|
|
136
|
+
class.define_method("select_last", method!(RubyListState::select_last, 0))?;
|
|
137
|
+
Ok(())
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[cfg(test)]
|
|
141
|
+
mod tests {
|
|
142
|
+
use super::*;
|
|
143
|
+
|
|
144
|
+
#[test]
|
|
145
|
+
fn test_new_with_no_selection() {
|
|
146
|
+
let state = RubyListState::new(None);
|
|
147
|
+
assert_eq!(state.selected(), None);
|
|
148
|
+
assert_eq!(state.offset(), 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[test]
|
|
152
|
+
fn test_new_with_selection() {
|
|
153
|
+
let state = RubyListState::new(Some(5));
|
|
154
|
+
assert_eq!(state.selected(), Some(5));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[test]
|
|
158
|
+
fn test_select_and_deselect() {
|
|
159
|
+
let state = RubyListState::new(None);
|
|
160
|
+
state.select(Some(3));
|
|
161
|
+
assert_eq!(state.selected(), Some(3));
|
|
162
|
+
state.select(None);
|
|
163
|
+
assert_eq!(state.selected(), None);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#[test]
|
|
167
|
+
fn test_scroll_operations() {
|
|
168
|
+
let state = RubyListState::new(None);
|
|
169
|
+
state.scroll_down_by(5);
|
|
170
|
+
// Note: scroll operations affect offset, but the exact behavior
|
|
171
|
+
// depends on the list size which is determined during rendering
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
pub mod barchart;
|
|
5
|
+
pub mod block;
|
|
6
|
+
pub mod calendar;
|
|
7
|
+
pub mod canvas;
|
|
8
|
+
pub mod center;
|
|
9
|
+
pub mod chart;
|
|
10
|
+
pub mod clear;
|
|
11
|
+
pub mod cursor;
|
|
12
|
+
pub mod gauge;
|
|
13
|
+
pub mod layout;
|
|
14
|
+
pub mod line_gauge;
|
|
15
|
+
pub mod list;
|
|
16
|
+
pub mod list_state;
|
|
17
|
+
pub mod overlay;
|
|
18
|
+
pub mod paragraph;
|
|
19
|
+
pub mod ratatui_logo;
|
|
20
|
+
pub mod ratatui_mascot;
|
|
21
|
+
pub mod scrollbar;
|
|
22
|
+
pub mod scrollbar_state;
|
|
23
|
+
pub mod sparkline;
|
|
24
|
+
pub mod table;
|
|
25
|
+
pub mod table_state;
|
|
26
|
+
pub mod tabs;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use crate::errors::type_error_with_context;
|
|
5
|
+
use crate::rendering::render_node;
|
|
6
|
+
use magnus::{prelude::*, Error, Value};
|
|
7
|
+
use ratatui::{buffer::Buffer, layout::Rect};
|
|
8
|
+
|
|
9
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
10
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
11
|
+
let layers_val: Value = node.funcall("layers", ())?;
|
|
12
|
+
let layers_array = magnus::RArray::from_value(layers_val)
|
|
13
|
+
.ok_or_else(|| type_error_with_context(&ruby, "expected array for layers", layers_val))?;
|
|
14
|
+
|
|
15
|
+
for i in 0..layers_array.len() {
|
|
16
|
+
let index = isize::try_from(i)
|
|
17
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
18
|
+
let layer: Value = layers_array.entry(index)?;
|
|
19
|
+
if let Err(e) = render_node(buffer, area, layer) {
|
|
20
|
+
eprintln!("Error rendering overlay layer {i}: {e:?}");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
Ok(())
|
|
24
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use crate::style::{parse_block, parse_style};
|
|
5
|
+
use bumpalo::Bump;
|
|
6
|
+
use magnus::{prelude::*, Error, Symbol, Value};
|
|
7
|
+
use ratatui::buffer::Buffer;
|
|
8
|
+
use ratatui::{
|
|
9
|
+
layout::{HorizontalAlignment, Rect},
|
|
10
|
+
widgets::{Paragraph, Widget, Wrap},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
use crate::text::parse_text;
|
|
14
|
+
|
|
15
|
+
pub fn create_paragraph(node: Value, bump: &Bump) -> Result<Paragraph<'_>, Error> {
|
|
16
|
+
let text_val: Value = node.funcall("text", ())?;
|
|
17
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
18
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
19
|
+
let wrap: bool = node.funcall("wrap", ())?;
|
|
20
|
+
let alignment_opt: Option<Symbol> = node.funcall("alignment", ())?;
|
|
21
|
+
let scroll_val: Value = node.funcall("scroll", ())?;
|
|
22
|
+
|
|
23
|
+
let lines = parse_text(text_val)?;
|
|
24
|
+
let style = parse_style(style_val)?;
|
|
25
|
+
let mut paragraph = Paragraph::new(lines).style(style);
|
|
26
|
+
|
|
27
|
+
if !block_val.is_nil() {
|
|
28
|
+
paragraph = paragraph.block(parse_block(block_val, bump)?);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if wrap {
|
|
32
|
+
paragraph = paragraph.wrap(Wrap { trim: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if let Some(alignment) = alignment_opt {
|
|
36
|
+
match alignment.to_string().as_str() {
|
|
37
|
+
"center" => paragraph = paragraph.alignment(HorizontalAlignment::Center),
|
|
38
|
+
"right" => paragraph = paragraph.alignment(HorizontalAlignment::Right),
|
|
39
|
+
_ => {}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if !scroll_val.is_nil() {
|
|
44
|
+
let scroll_array: Vec<u16> = Vec::<u16>::try_convert(scroll_val)?;
|
|
45
|
+
if scroll_array.len() >= 2 {
|
|
46
|
+
paragraph = paragraph.scroll((scroll_array[0], scroll_array[1]));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
Ok(paragraph)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
54
|
+
let bump = Bump::new();
|
|
55
|
+
let paragraph = create_paragraph(node, &bump)?;
|
|
56
|
+
Widget::render(paragraph, area, buffer);
|
|
57
|
+
Ok(())
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub fn line_count(node: Value, width: u16) -> Result<usize, Error> {
|
|
61
|
+
let bump = Bump::new();
|
|
62
|
+
let paragraph = create_paragraph(node, &bump)?;
|
|
63
|
+
Ok(paragraph.line_count(width))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pub fn line_width(node: Value) -> Result<usize, Error> {
|
|
67
|
+
let bump = Bump::new();
|
|
68
|
+
let paragraph = create_paragraph(node, &bump)?;
|
|
69
|
+
Ok(paragraph.line_width())
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[cfg(test)]
|
|
73
|
+
mod tests {
|
|
74
|
+
use super::*;
|
|
75
|
+
use ratatui::buffer::Buffer;
|
|
76
|
+
|
|
77
|
+
#[test]
|
|
78
|
+
fn test_paragraph_rendering() {
|
|
79
|
+
let p = Paragraph::new("test content").alignment(HorizontalAlignment::Center);
|
|
80
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
|
|
81
|
+
p.render(Rect::new(0, 0, 20, 1), &mut buf);
|
|
82
|
+
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
83
|
+
assert!(content.contains("test content"));
|
|
84
|
+
// Check for centered alignment (should have leading spaces)
|
|
85
|
+
assert!(content.starts_with(' '));
|
|
86
|
+
}
|
|
87
|
+
}
|