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,40 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
use magnus::Value;
|
|
6
|
+
use ratatui::{
|
|
7
|
+
buffer::Buffer,
|
|
8
|
+
layout::Rect,
|
|
9
|
+
widgets::{RatatuiLogo, RatatuiLogoSize, Widget},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
pub fn render(buffer: &mut Buffer, area: Rect, _node: Value) {
|
|
13
|
+
// RatatuiLogo does not support custom styling (it has fixed colors).
|
|
14
|
+
// It requires a size argument.
|
|
15
|
+
let widget = RatatuiLogo::new(RatatuiLogoSize::Small);
|
|
16
|
+
widget.render(area, buffer);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[cfg(test)]
|
|
20
|
+
mod tests {
|
|
21
|
+
use super::*;
|
|
22
|
+
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
|
23
|
+
|
|
24
|
+
#[test]
|
|
25
|
+
fn test_render() {
|
|
26
|
+
let mut buffer = Buffer::empty(Rect::new(0, 0, 50, 20));
|
|
27
|
+
let widget = RatatuiLogo::new(RatatuiLogoSize::Small);
|
|
28
|
+
widget.render(Rect::new(0, 0, 50, 20), &mut buffer);
|
|
29
|
+
|
|
30
|
+
let content = buffer
|
|
31
|
+
.content()
|
|
32
|
+
.iter()
|
|
33
|
+
.map(|c| c.symbol())
|
|
34
|
+
.collect::<String>();
|
|
35
|
+
|
|
36
|
+
// The logo uses block characters for rendering
|
|
37
|
+
assert!(content.contains('█'));
|
|
38
|
+
assert!(!content.trim().is_empty());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
use crate::style::parse_block;
|
|
6
|
+
use bumpalo::Bump;
|
|
7
|
+
use magnus::{prelude::*, Error, Value};
|
|
8
|
+
use ratatui::{
|
|
9
|
+
buffer::Buffer,
|
|
10
|
+
layout::Rect,
|
|
11
|
+
widgets::{RatatuiMascot, Widget},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
pub fn render_ratatui_mascot(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
15
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
16
|
+
|
|
17
|
+
let mut inner_area = area;
|
|
18
|
+
|
|
19
|
+
if !block_val.is_nil() {
|
|
20
|
+
let bump = Bump::new();
|
|
21
|
+
let block = parse_block(block_val, &bump)?;
|
|
22
|
+
inner_area = block.inner(area);
|
|
23
|
+
block.render(area, buffer);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let widget = RatatuiMascot::new();
|
|
27
|
+
widget.render(inner_area, buffer);
|
|
28
|
+
Ok(())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[cfg(test)]
|
|
32
|
+
mod tests {
|
|
33
|
+
use super::*;
|
|
34
|
+
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
|
35
|
+
|
|
36
|
+
#[test]
|
|
37
|
+
fn test_render() {
|
|
38
|
+
let mut buffer = Buffer::empty(Rect::new(0, 0, 50, 20));
|
|
39
|
+
let widget = RatatuiMascot::new();
|
|
40
|
+
widget.render(Rect::new(0, 0, 50, 20), &mut buffer);
|
|
41
|
+
|
|
42
|
+
let content = buffer
|
|
43
|
+
.content()
|
|
44
|
+
.iter()
|
|
45
|
+
.map(|c| c.symbol())
|
|
46
|
+
.collect::<String>();
|
|
47
|
+
|
|
48
|
+
// The mascot uses block drawing characters
|
|
49
|
+
assert!(
|
|
50
|
+
content.contains("█"),
|
|
51
|
+
"Mascot rendering should contain block characters"
|
|
52
|
+
);
|
|
53
|
+
assert!(!content.trim().is_empty());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
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;
|
|
5
|
+
use crate::widgets::scrollbar_state::RubyScrollbarState;
|
|
6
|
+
use bumpalo::Bump;
|
|
7
|
+
use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
|
|
8
|
+
use ratatui::{
|
|
9
|
+
buffer::Buffer,
|
|
10
|
+
layout::Rect,
|
|
11
|
+
widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
15
|
+
let content_length: usize = node.funcall("content_length", ())?;
|
|
16
|
+
let position: usize = node.funcall("position", ())?;
|
|
17
|
+
let orientation_sym: Symbol = node.funcall("orientation", ())?;
|
|
18
|
+
|
|
19
|
+
let thumb_symbol_val: Value = node.funcall("thumb_symbol", ())?;
|
|
20
|
+
let thumb_style_val: Value = node.funcall("thumb_style", ())?;
|
|
21
|
+
let track_symbol_val: Value = node.funcall("track_symbol", ())?;
|
|
22
|
+
let track_style_val: Value = node.funcall("track_style", ())?;
|
|
23
|
+
let begin_symbol_val: Value = node.funcall("begin_symbol", ())?;
|
|
24
|
+
let begin_style_val: Value = node.funcall("begin_style", ())?;
|
|
25
|
+
let end_symbol_val: Value = node.funcall("end_symbol", ())?;
|
|
26
|
+
let end_style_val: Value = node.funcall("end_style", ())?;
|
|
27
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
28
|
+
|
|
29
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
30
|
+
|
|
31
|
+
let mut state = ScrollbarState::new(content_length).position(position);
|
|
32
|
+
let mut scrollbar = Scrollbar::default();
|
|
33
|
+
|
|
34
|
+
scrollbar = match orientation_sym.to_string().as_str() {
|
|
35
|
+
"vertical_left" => scrollbar.orientation(ScrollbarOrientation::VerticalLeft),
|
|
36
|
+
"horizontal_bottom" | "horizontal" => {
|
|
37
|
+
scrollbar.orientation(ScrollbarOrientation::HorizontalBottom)
|
|
38
|
+
}
|
|
39
|
+
"horizontal_top" => scrollbar.orientation(ScrollbarOrientation::HorizontalTop),
|
|
40
|
+
_ => scrollbar.orientation(ScrollbarOrientation::VerticalRight),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Hoisted strings to extend lifetime
|
|
44
|
+
let thumb_str: String;
|
|
45
|
+
let track_str: String;
|
|
46
|
+
let begin_str: String;
|
|
47
|
+
let end_str: String;
|
|
48
|
+
|
|
49
|
+
if !thumb_symbol_val.is_nil() {
|
|
50
|
+
thumb_str = thumb_symbol_val.funcall("to_s", ())?;
|
|
51
|
+
scrollbar = scrollbar.thumb_symbol(&thumb_str);
|
|
52
|
+
}
|
|
53
|
+
if !thumb_style_val.is_nil() {
|
|
54
|
+
scrollbar = scrollbar.thumb_style(crate::style::parse_style(thumb_style_val)?);
|
|
55
|
+
}
|
|
56
|
+
if !track_symbol_val.is_nil() {
|
|
57
|
+
track_str = track_symbol_val.funcall("to_s", ())?;
|
|
58
|
+
scrollbar = scrollbar.track_symbol(Some(&track_str));
|
|
59
|
+
}
|
|
60
|
+
if !track_style_val.is_nil() {
|
|
61
|
+
scrollbar = scrollbar.track_style(crate::style::parse_style(track_style_val)?);
|
|
62
|
+
}
|
|
63
|
+
if !begin_symbol_val.is_nil() {
|
|
64
|
+
begin_str = begin_symbol_val.funcall("to_s", ())?;
|
|
65
|
+
scrollbar = scrollbar.begin_symbol(Some(&begin_str));
|
|
66
|
+
}
|
|
67
|
+
if !begin_style_val.is_nil() {
|
|
68
|
+
scrollbar = scrollbar.begin_style(crate::style::parse_style(begin_style_val)?);
|
|
69
|
+
}
|
|
70
|
+
if !end_symbol_val.is_nil() {
|
|
71
|
+
end_str = end_symbol_val.funcall("to_s", ())?;
|
|
72
|
+
scrollbar = scrollbar.end_symbol(Some(&end_str));
|
|
73
|
+
}
|
|
74
|
+
if !end_style_val.is_nil() {
|
|
75
|
+
scrollbar = scrollbar.end_style(crate::style::parse_style(end_style_val)?);
|
|
76
|
+
}
|
|
77
|
+
if !style_val.is_nil() {
|
|
78
|
+
scrollbar = scrollbar.style(crate::style::parse_style(style_val)?);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if block_val.is_nil() {
|
|
82
|
+
StatefulWidget::render(scrollbar, area, buffer, &mut state);
|
|
83
|
+
} else {
|
|
84
|
+
let bump = Bump::new();
|
|
85
|
+
let block = parse_block(block_val, &bump)?;
|
|
86
|
+
let inner_area = block.inner(area);
|
|
87
|
+
block.render(area, buffer);
|
|
88
|
+
StatefulWidget::render(scrollbar, inner_area, buffer, &mut state);
|
|
89
|
+
}
|
|
90
|
+
Ok(())
|
|
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
|
+
buffer: &mut Buffer,
|
|
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
|
+
StatefulWidget::render(scrollbar, area, buffer, &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
|
+
block.render(area, buffer);
|
|
177
|
+
StatefulWidget::render(scrollbar, inner_area, buffer, &mut inner_state);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
Ok(())
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#[cfg(test)]
|
|
185
|
+
mod tests {
|
|
186
|
+
use super::*;
|
|
187
|
+
use ratatui::buffer::Buffer;
|
|
188
|
+
use ratatui::widgets::StatefulWidget;
|
|
189
|
+
|
|
190
|
+
#[test]
|
|
191
|
+
fn test_scrollbar_vertical_render() {
|
|
192
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 1, 5));
|
|
193
|
+
let mut state = ScrollbarState::new(10).position(2);
|
|
194
|
+
let scrollbar = Scrollbar::default().orientation(ScrollbarOrientation::VerticalRight);
|
|
195
|
+
|
|
196
|
+
// Note: Scrollbar is stateful
|
|
197
|
+
scrollbar.render(Rect::new(0, 0, 1, 5), &mut buf, &mut state);
|
|
198
|
+
|
|
199
|
+
// Vertical scrollbar should render something in the first column
|
|
200
|
+
assert!(buf.content().iter().any(|c| c.symbol() != " "));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[test]
|
|
204
|
+
fn test_scrollbar_horizontal_render() {
|
|
205
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
|
|
206
|
+
let mut state = ScrollbarState::new(10).position(2);
|
|
207
|
+
let scrollbar = Scrollbar::default().orientation(ScrollbarOrientation::HorizontalBottom);
|
|
208
|
+
|
|
209
|
+
scrollbar.render(Rect::new(0, 0, 5, 1), &mut buf, &mut state);
|
|
210
|
+
|
|
211
|
+
// Horizontal scrollbar should render something in the first row
|
|
212
|
+
assert!(buf.content().iter().any(|c| c.symbol() != " "));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use crate::style::{parse_bar_set, parse_block, parse_style};
|
|
5
|
+
use bumpalo::Bump;
|
|
6
|
+
use magnus::{prelude::*, Error, RString, Value};
|
|
7
|
+
use ratatui::buffer::Buffer;
|
|
8
|
+
use ratatui::{layout::Rect, widgets::RenderDirection, widgets::Sparkline, widgets::Widget};
|
|
9
|
+
|
|
10
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
11
|
+
let bump = Bump::new();
|
|
12
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
13
|
+
let data_val: magnus::RArray = node.funcall("data", ())?;
|
|
14
|
+
let max_val: Value = node.funcall("max", ())?;
|
|
15
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
16
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
17
|
+
let direction_val: Value = node.funcall("direction", ())?;
|
|
18
|
+
let absent_value_symbol_val: Value = node.funcall("absent_value_symbol", ())?;
|
|
19
|
+
let absent_value_style_val: Value = node.funcall("absent_value_style", ())?;
|
|
20
|
+
let bar_set_val: Value = node.funcall("bar_set", ())?;
|
|
21
|
+
|
|
22
|
+
let mut data_vec = Vec::new();
|
|
23
|
+
for i in 0..data_val.len() {
|
|
24
|
+
let index = isize::try_from(i)
|
|
25
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
26
|
+
let val: Value = data_val.entry(index)?;
|
|
27
|
+
if val.is_nil() {
|
|
28
|
+
data_vec.push(None);
|
|
29
|
+
} else {
|
|
30
|
+
let num: u64 = u64::try_convert(val)?;
|
|
31
|
+
data_vec.push(Some(num));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let mut sparkline = Sparkline::default().data(&data_vec);
|
|
36
|
+
|
|
37
|
+
if !max_val.is_nil() {
|
|
38
|
+
let max: u64 = u64::try_convert(max_val)?;
|
|
39
|
+
sparkline = sparkline.max(max);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if !style_val.is_nil() {
|
|
43
|
+
sparkline = sparkline.style(parse_style(style_val)?);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if !block_val.is_nil() {
|
|
47
|
+
sparkline = sparkline.block(parse_block(block_val, &bump)?);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if !direction_val.is_nil() {
|
|
51
|
+
let direction_sym: RString = direction_val.funcall("to_s", ())?;
|
|
52
|
+
let direction_str = direction_sym.to_string()?;
|
|
53
|
+
let direction = match direction_str.as_str() {
|
|
54
|
+
"right_to_left" => RenderDirection::RightToLeft,
|
|
55
|
+
_ => RenderDirection::LeftToRight,
|
|
56
|
+
};
|
|
57
|
+
sparkline = sparkline.direction(direction);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if !absent_value_symbol_val.is_nil() {
|
|
61
|
+
let symbol_str: String = String::try_convert(absent_value_symbol_val)?;
|
|
62
|
+
// Only use the first character if multiple are provided
|
|
63
|
+
if let Some(first_char) = symbol_str.chars().next() {
|
|
64
|
+
sparkline = sparkline.absent_value_symbol(first_char);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if !absent_value_style_val.is_nil() {
|
|
69
|
+
sparkline = sparkline.absent_value_style(parse_style(absent_value_style_val)?);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if !bar_set_val.is_nil() {
|
|
73
|
+
sparkline = sparkline.bar_set(parse_bar_set(bar_set_val, &bump)?);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
sparkline.render(area, buffer);
|
|
77
|
+
Ok(())
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[cfg(test)]
|
|
81
|
+
mod tests {
|
|
82
|
+
use super::*;
|
|
83
|
+
use ratatui::buffer::Buffer;
|
|
84
|
+
use ratatui::widgets::{Sparkline, Widget};
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn test_sparkline_rendering() {
|
|
88
|
+
let data = vec![1, 2, 3, 4];
|
|
89
|
+
let sparkline = Sparkline::default().data(&data);
|
|
90
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
|
|
91
|
+
sparkline.render(Rect::new(0, 0, 4, 1), &mut buf);
|
|
92
|
+
// Should have sparkline rendered (non-space characters)
|
|
93
|
+
assert!(buf.content().iter().any(|c| c.symbol() != " "));
|
|
94
|
+
// In sparkline, higher values generally result in different bar symbols
|
|
95
|
+
// but verifying exact symbols might be fragile across ratatui versions.
|
|
96
|
+
// At least we know it should have rendered 4 bars for 4 data points.
|
|
97
|
+
let bars = buf.content().iter().filter(|c| c.symbol() != " ").count();
|
|
98
|
+
assert_eq!(bars, 4);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[test]
|
|
102
|
+
fn test_sparkline_absent_value_symbol() {
|
|
103
|
+
// Data with absent (None) and present values: [Some(5), None, Some(8), None]
|
|
104
|
+
let data = vec![Some(5), None, Some(8), None];
|
|
105
|
+
let sparkline = Sparkline::default().data(&data).absent_value_symbol("-");
|
|
106
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
|
|
107
|
+
sparkline.render(Rect::new(0, 0, 4, 1), &mut buf);
|
|
108
|
+
|
|
109
|
+
// Collect all rendered symbols
|
|
110
|
+
let symbols: Vec<&str> = buf.content().iter().map(|c| c.symbol()).collect();
|
|
111
|
+
|
|
112
|
+
// Check that we have 4 cells rendered
|
|
113
|
+
assert_eq!(
|
|
114
|
+
symbols.len(),
|
|
115
|
+
4,
|
|
116
|
+
"Should have 4 cells rendered for 4 data points"
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Absent values (None) should render as "-"
|
|
120
|
+
assert_eq!(symbols[1], "-", "Second value (None) should render as dash");
|
|
121
|
+
assert_eq!(symbols[3], "-", "Fourth value (None) should render as dash");
|
|
122
|
+
|
|
123
|
+
// Present values should not be dashes
|
|
124
|
+
assert_ne!(symbols[0], "-", "First value (Some(5)) should not be dash");
|
|
125
|
+
assert_ne!(symbols[2], "-", "Third value (Some(8)) should not be dash");
|
|
126
|
+
}
|
|
127
|
+
}
|