ratatui_ruby 1.3.0 → 1.4.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/ext/ratatui_ruby/Cargo.lock +2 -1
- data/ext/ratatui_ruby/Cargo.toml +2 -1
- data/ext/ratatui_ruby/src/events.rs +157 -18
- data/lib/ratatui_ruby/test_helper/snapshot.rb +58 -10
- data/lib/ratatui_ruby/test_helper/subprocess_timeout.rb +35 -0
- data/lib/ratatui_ruby/test_helper.rb +2 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- metadata +17 -270
- data/.builds/ruby-3.2.yml +0 -54
- data/.builds/ruby-3.3.yml +0 -54
- data/.builds/ruby-3.4.yml +0 -54
- data/.builds/ruby-4.0.0.yml +0 -54
- data/.pre-commit-config.yaml +0 -16
- data/.rubocop.yml +0 -10
- data/AGENTS.md +0 -147
- data/CHANGELOG.md +0 -771
- data/README.md +0 -187
- data/README.rdoc +0 -302
- data/Rakefile +0 -11
- data/Steepfile +0 -50
- data/doc/concepts/application_architecture.md +0 -321
- data/doc/concepts/application_testing.md +0 -193
- data/doc/concepts/async.md +0 -190
- data/doc/concepts/custom_widgets.md +0 -247
- data/doc/concepts/debugging.md +0 -401
- data/doc/concepts/event_handling.md +0 -162
- data/doc/concepts/interactive_design.md +0 -146
- data/doc/contributors/auditing/parity.md +0 -239
- data/doc/contributors/design/ruby_frontend.md +0 -448
- data/doc/contributors/design/rust_backend.md +0 -434
- data/doc/contributors/design.md +0 -11
- data/doc/contributors/developing_examples.md +0 -400
- data/doc/contributors/documentation_style.md +0 -121
- data/doc/contributors/index.md +0 -21
- data/doc/contributors/releasing.md +0 -215
- data/doc/contributors/todo/align/api_completeness_audit-finished.md +0 -381
- data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -200
- data/doc/contributors/todo/align/term.md +0 -351
- data/doc/contributors/todo/align/terminal.md +0 -647
- data/doc/contributors/todo/future_work.md +0 -169
- data/doc/contributors/upstream_requests/paragraph_span_rects.md +0 -259
- data/doc/contributors/upstream_requests/tab_rects.md +0 -173
- data/doc/contributors/upstream_requests/title_rects.md +0 -132
- data/doc/custom.css +0 -22
- data/doc/getting_started/quickstart.md +0 -291
- data/doc/getting_started/why.md +0 -93
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_cli_rich_moments.gif +0 -0
- data/doc/images/app_color_picker.png +0 -0
- data/doc/images/app_debugging_showcase.gif +0 -0
- data/doc/images/app_debugging_showcase.png +0 -0
- data/doc/images/app_external_editor.gif +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.png +0 -0
- data/doc/images/widget_block.png +0 -0
- data/doc/images/widget_box.png +0 -0
- data/doc/images/widget_calendar.png +0 -0
- data/doc/images/widget_canvas.png +0 -0
- data/doc/images/widget_cell.png +0 -0
- data/doc/images/widget_center.png +0 -0
- data/doc/images/widget_chart.png +0 -0
- data/doc/images/widget_gauge.png +0 -0
- data/doc/images/widget_layout_split.png +0 -0
- data/doc/images/widget_line_gauge.png +0 -0
- data/doc/images/widget_list.png +0 -0
- data/doc/images/widget_map.png +0 -0
- data/doc/images/widget_overlay.png +0 -0
- data/doc/images/widget_popup.png +0 -0
- data/doc/images/widget_ratatui_logo.png +0 -0
- data/doc/images/widget_ratatui_mascot.png +0 -0
- data/doc/images/widget_rect.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_scrollbar.png +0 -0
- data/doc/images/widget_sparkline.png +0 -0
- data/doc/images/widget_style_colors.png +0 -0
- data/doc/images/widget_table.png +0 -0
- data/doc/images/widget_tabs.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/index.md +0 -34
- data/doc/troubleshooting/async.md +0 -4
- data/doc/troubleshooting/terminal_limitations.md +0 -131
- data/doc/troubleshooting/tui_output.md +0 -197
- data/examples/app_all_events/README.md +0 -114
- data/examples/app_all_events/app.rb +0 -98
- data/examples/app_all_events/model/app_model.rb +0 -159
- data/examples/app_all_events/model/event_color_cycle.rb +0 -43
- data/examples/app_all_events/model/event_entry.rb +0 -94
- data/examples/app_all_events/model/msg.rb +0 -39
- data/examples/app_all_events/model/timestamp.rb +0 -56
- data/examples/app_all_events/update.rb +0 -75
- data/examples/app_all_events/view/app_view.rb +0 -80
- data/examples/app_all_events/view/controls_view.rb +0 -54
- data/examples/app_all_events/view/counts_view.rb +0 -61
- data/examples/app_all_events/view/live_view.rb +0 -72
- data/examples/app_all_events/view/log_view.rb +0 -57
- data/examples/app_all_events/view.rb +0 -9
- data/examples/app_cli_rich_moments/README.md +0 -81
- data/examples/app_cli_rich_moments/app.rb +0 -189
- data/examples/app_color_picker/README.md +0 -156
- data/examples/app_color_picker/app.rb +0 -76
- data/examples/app_color_picker/clipboard.rb +0 -86
- data/examples/app_color_picker/color.rb +0 -193
- data/examples/app_color_picker/controls.rb +0 -92
- data/examples/app_color_picker/copy_dialog.rb +0 -168
- data/examples/app_color_picker/export_pane.rb +0 -128
- data/examples/app_color_picker/harmony.rb +0 -58
- data/examples/app_color_picker/input.rb +0 -176
- data/examples/app_color_picker/main_container.rb +0 -180
- data/examples/app_color_picker/palette.rb +0 -111
- data/examples/app_debugging_showcase/README.md +0 -119
- data/examples/app_debugging_showcase/app.rb +0 -318
- data/examples/app_external_editor/README.md +0 -62
- data/examples/app_external_editor/app.rb +0 -344
- data/examples/app_login_form/README.md +0 -58
- data/examples/app_login_form/app.rb +0 -109
- data/examples/app_stateful_interaction/README.md +0 -35
- data/examples/app_stateful_interaction/app.rb +0 -328
- data/examples/timeout_demo.rb +0 -45
- data/examples/verify_quickstart_dsl/README.md +0 -55
- data/examples/verify_quickstart_dsl/app.rb +0 -49
- data/examples/verify_quickstart_layout/README.md +0 -77
- data/examples/verify_quickstart_layout/app.rb +0 -73
- data/examples/verify_quickstart_lifecycle/README.md +0 -68
- data/examples/verify_quickstart_lifecycle/app.rb +0 -62
- data/examples/verify_readme_usage/README.md +0 -49
- data/examples/verify_readme_usage/app.rb +0 -42
- data/examples/verify_website_managed/README.md +0 -48
- data/examples/verify_website_managed/app.rb +0 -36
- data/examples/verify_website_menu/README.md +0 -60
- data/examples/verify_website_menu/app.rb +0 -84
- data/examples/verify_website_spinner/README.md +0 -44
- data/examples/verify_website_spinner/app.rb +0 -34
- data/examples/widget_barchart/README.md +0 -58
- data/examples/widget_barchart/app.rb +0 -240
- data/examples/widget_block/README.md +0 -44
- data/examples/widget_block/app.rb +0 -258
- data/examples/widget_box/README.md +0 -54
- data/examples/widget_box/app.rb +0 -255
- data/examples/widget_calendar/README.md +0 -48
- data/examples/widget_calendar/app.rb +0 -115
- data/examples/widget_canvas/README.md +0 -31
- data/examples/widget_canvas/app.rb +0 -130
- data/examples/widget_cell/README.md +0 -45
- data/examples/widget_cell/app.rb +0 -112
- data/examples/widget_center/README.md +0 -33
- data/examples/widget_center/app.rb +0 -118
- data/examples/widget_chart/README.md +0 -50
- data/examples/widget_chart/app.rb +0 -220
- data/examples/widget_gauge/README.md +0 -50
- data/examples/widget_gauge/app.rb +0 -229
- data/examples/widget_layout_split/README.md +0 -53
- data/examples/widget_layout_split/app.rb +0 -260
- data/examples/widget_line_gauge/README.md +0 -50
- data/examples/widget_line_gauge/app.rb +0 -219
- data/examples/widget_list/README.md +0 -58
- data/examples/widget_list/app.rb +0 -382
- data/examples/widget_map/README.md +0 -48
- data/examples/widget_map/app.rb +0 -95
- data/examples/widget_overlay/README.md +0 -45
- data/examples/widget_overlay/app.rb +0 -250
- data/examples/widget_popup/README.md +0 -45
- data/examples/widget_popup/app.rb +0 -106
- data/examples/widget_ratatui_logo/README.md +0 -43
- data/examples/widget_ratatui_logo/app.rb +0 -104
- data/examples/widget_ratatui_mascot/README.md +0 -43
- data/examples/widget_ratatui_mascot/app.rb +0 -95
- data/examples/widget_rect/README.md +0 -53
- data/examples/widget_rect/app.rb +0 -222
- data/examples/widget_render/README.md +0 -46
- data/examples/widget_render/app.rb +0 -186
- data/examples/widget_render/app.rbs +0 -41
- data/examples/widget_rich_text/README.md +0 -44
- data/examples/widget_rich_text/app.rb +0 -193
- data/examples/widget_scroll_text/README.md +0 -46
- data/examples/widget_scroll_text/app.rb +0 -109
- data/examples/widget_scrollbar/README.md +0 -46
- data/examples/widget_scrollbar/app.rb +0 -155
- data/examples/widget_sparkline/README.md +0 -51
- data/examples/widget_sparkline/app.rb +0 -277
- data/examples/widget_style_colors/README.md +0 -43
- data/examples/widget_style_colors/app.rb +0 -83
- data/examples/widget_table/README.md +0 -57
- data/examples/widget_table/app.rb +0 -285
- data/examples/widget_tabs/README.md +0 -50
- data/examples/widget_tabs/app.rb +0 -183
- data/examples/widget_text_width/README.md +0 -44
- data/examples/widget_text_width/app.rb +0 -117
- data/migrate_to_buffer.rb +0 -145
- data/mise.toml +0 -8
- data/tasks/autodoc/examples.rb +0 -87
- data/tasks/autodoc/member.rb +0 -58
- data/tasks/autodoc/name.rb +0 -21
- data/tasks/autodoc.rake +0 -21
- data/tasks/bump/bump_workflow.rb +0 -49
- data/tasks/bump/cargo_lockfile.rb +0 -21
- data/tasks/bump/changelog.rb +0 -104
- data/tasks/bump/header.rb +0 -32
- data/tasks/bump/history.rb +0 -32
- data/tasks/bump/links.rb +0 -69
- data/tasks/bump/manifest.rb +0 -33
- data/tasks/bump/patch_release.rb +0 -19
- data/tasks/bump/release_branch.rb +0 -17
- data/tasks/bump/release_from_trunk.rb +0 -49
- data/tasks/bump/repository.rb +0 -54
- data/tasks/bump/ruby_gem.rb +0 -29
- data/tasks/bump/sem_ver.rb +0 -44
- data/tasks/bump/unreleased_section.rb +0 -73
- data/tasks/bump.rake +0 -61
- data/tasks/doc/documentation.rb +0 -59
- data/tasks/doc/link/file_url.rb +0 -30
- data/tasks/doc/link/relative_path.rb +0 -61
- data/tasks/doc/link/web_url.rb +0 -55
- data/tasks/doc/link.rb +0 -52
- data/tasks/doc/link_audit.rb +0 -116
- data/tasks/doc/problem.rb +0 -40
- data/tasks/doc/source_file.rb +0 -93
- data/tasks/doc.rake +0 -905
- data/tasks/example_viewer.html.erb +0 -172
- data/tasks/extension.rake +0 -14
- data/tasks/license/headers_md.rb +0 -223
- data/tasks/license/headers_rb.rb +0 -210
- data/tasks/license/license_utils.rb +0 -130
- data/tasks/license/snippets_md.rb +0 -315
- data/tasks/license/snippets_rdoc.rb +0 -150
- data/tasks/license.rake +0 -91
- data/tasks/lint.rake +0 -170
- data/tasks/rbs_predicates/predicate_catalog.rb +0 -52
- data/tasks/rbs_predicates/predicate_tests.rb +0 -124
- data/tasks/rbs_predicates/rbs_signature.rb +0 -63
- data/tasks/rbs_predicates.rake +0 -31
- data/tasks/rdoc_config.rb +0 -29
- data/tasks/resources/build.yml.erb +0 -60
- data/tasks/resources/index.html.erb +0 -141
- data/tasks/resources/rubies.yml +0 -7
- data/tasks/sourcehut.rake +0 -122
- data/tasks/steep.rake +0 -11
- data/tasks/terminal_preview/app_screenshot.rb +0 -45
- data/tasks/terminal_preview/crash_report.rb +0 -54
- data/tasks/terminal_preview/example_app.rb +0 -27
- data/tasks/terminal_preview/launcher_script.rb +0 -48
- data/tasks/terminal_preview/preview_collection.rb +0 -60
- data/tasks/terminal_preview/preview_timing.rb +0 -24
- data/tasks/terminal_preview/safety_confirmation.rb +0 -58
- data/tasks/terminal_preview/saved_screenshot.rb +0 -56
- data/tasks/terminal_preview/system_appearance.rb +0 -13
- data/tasks/terminal_preview/terminal_window.rb +0 -138
- data/tasks/terminal_preview/window_id.rb +0 -16
- data/tasks/terminal_preview.rake +0 -30
- data/tasks/test.rake +0 -36
- data/tasks/website/index_page.rb +0 -30
- data/tasks/website/version.rb +0 -122
- data/tasks/website/version_menu.rb +0 -68
- data/tasks/website/versioned_documentation.rb +0 -83
- data/tasks/website/website.rb +0 -53
- data/tasks/website.rake +0 -28
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e12c46f758cf25eb436da52bbdcbf46f5cd256b804cb3f67a2d6edeeb8d4062
|
|
4
|
+
data.tar.gz: c4aa2e4c6251a6a75b8ac57e3c7f6133c88066185ee5e9cd1633fbc459edf612
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dca9adc8ecef9e432c163b5b7b9e2488d42f9f1db98212a5c140a70bf21dc6c19fa687a97fb30d52ef9cadf8c5dd34a3aecbe471562ba5d9bfdb8c1ff85b99bf
|
|
7
|
+
data.tar.gz: 5f1e347a863736fae0ea9854a37402bd3115c311a228c23b24848113c23a21beeb97b5a33a9177611cd83c7e9d61ed6b4c4c9cf85c110e89f358e805f456465c
|
data/ext/ratatui_ruby/Cargo.lock
CHANGED
|
@@ -1059,12 +1059,13 @@ dependencies = [
|
|
|
1059
1059
|
|
|
1060
1060
|
[[package]]
|
|
1061
1061
|
name = "ratatui_ruby"
|
|
1062
|
-
version = "1.
|
|
1062
|
+
version = "1.4.0"
|
|
1063
1063
|
dependencies = [
|
|
1064
1064
|
"bumpalo",
|
|
1065
1065
|
"lazy_static",
|
|
1066
1066
|
"magnus",
|
|
1067
1067
|
"ratatui",
|
|
1068
|
+
"rb-sys",
|
|
1068
1069
|
"time",
|
|
1069
1070
|
"unicode-width 0.1.14",
|
|
1070
1071
|
]
|
data/ext/ratatui_ruby/Cargo.toml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
[package]
|
|
5
5
|
name = "ratatui_ruby"
|
|
6
|
-
version = "1.
|
|
6
|
+
version = "1.4.0"
|
|
7
7
|
edition = "2021"
|
|
8
8
|
|
|
9
9
|
[lib]
|
|
@@ -11,6 +11,7 @@ crate-type = ["cdylib", "staticlib"]
|
|
|
11
11
|
|
|
12
12
|
[dependencies]
|
|
13
13
|
magnus = "0.8.2"
|
|
14
|
+
rb-sys = "*"
|
|
14
15
|
ratatui = { version = "0.30", features = ["widget-calendar", "layout-cache", "unstable-rendered-line-info", "palette"] }
|
|
15
16
|
unicode-width = "0.1"
|
|
16
17
|
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use magnus::{Error, IntoValue, TryConvert, Value};
|
|
5
|
+
use rb_sys::rb_thread_call_without_gvl;
|
|
5
6
|
use std::cell::RefCell;
|
|
7
|
+
use std::os::raw::c_void;
|
|
6
8
|
|
|
7
9
|
/// Wrapper enum for test events - includes crossterm events and our Sync event.
|
|
8
10
|
#[derive(Debug, Clone)]
|
|
@@ -314,6 +316,92 @@ pub fn clear_events() {
|
|
|
314
316
|
EVENT_QUEUE.with(|q| q.borrow_mut().clear());
|
|
315
317
|
}
|
|
316
318
|
|
|
319
|
+
/// Result of polling crossterm from outside the GVL.
|
|
320
|
+
///
|
|
321
|
+
/// The blocking I/O (poll + read) happens without the Ruby GVL held,
|
|
322
|
+
/// so other Ruby threads can run concurrently. This enum carries the
|
|
323
|
+
/// result back across the GVL boundary for Ruby object construction.
|
|
324
|
+
#[derive(Debug)]
|
|
325
|
+
enum PollResult {
|
|
326
|
+
/// A crossterm event was read successfully.
|
|
327
|
+
Event(ratatui::crossterm::event::Event),
|
|
328
|
+
/// The timeout expired with no event available.
|
|
329
|
+
NoEvent,
|
|
330
|
+
/// An I/O error occurred during poll or read.
|
|
331
|
+
Error(String),
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// Data passed to the GVL-free polling callback.
|
|
335
|
+
///
|
|
336
|
+
/// The `timeout` field controls the poll behavior:
|
|
337
|
+
/// - `Some(duration)`: poll with a timeout, return `NoEvent` if it expires.
|
|
338
|
+
/// - `None`: block indefinitely until an event arrives.
|
|
339
|
+
struct PollData {
|
|
340
|
+
timeout: Option<std::time::Duration>,
|
|
341
|
+
result: PollResult,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/// Polls crossterm for events without the Ruby GVL held.
|
|
345
|
+
///
|
|
346
|
+
/// This is the work function passed to `rb_thread_call_without_gvl`.
|
|
347
|
+
/// It performs blocking I/O that would otherwise starve Ruby threads.
|
|
348
|
+
extern "C" fn poll_without_gvl(data: *mut c_void) -> *mut c_void {
|
|
349
|
+
// SAFETY: `data` is a valid pointer to `PollData`, passed by
|
|
350
|
+
// `poll_crossterm_without_gvl` via `rb_thread_call_without_gvl`. The
|
|
351
|
+
// pointer remains valid for the duration of this callback because the
|
|
352
|
+
// caller owns the `PollData` on the stack and waits for us to return.
|
|
353
|
+
let data = unsafe { &mut *data.cast::<PollData>() };
|
|
354
|
+
|
|
355
|
+
data.result = match data.timeout {
|
|
356
|
+
Some(duration) => match ratatui::crossterm::event::poll(duration) {
|
|
357
|
+
Ok(true) => match ratatui::crossterm::event::read() {
|
|
358
|
+
Ok(event) => PollResult::Event(event),
|
|
359
|
+
Err(e) => PollResult::Error(e.to_string()),
|
|
360
|
+
},
|
|
361
|
+
Ok(false) => PollResult::NoEvent,
|
|
362
|
+
Err(e) => PollResult::Error(e.to_string()),
|
|
363
|
+
},
|
|
364
|
+
None => match ratatui::crossterm::event::read() {
|
|
365
|
+
Ok(event) => PollResult::Event(event),
|
|
366
|
+
Err(e) => PollResult::Error(e.to_string()),
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
std::ptr::null_mut()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/// Polls crossterm with the GVL released, then converts the result
|
|
374
|
+
/// to a Ruby value after reacquiring the GVL.
|
|
375
|
+
fn poll_crossterm_without_gvl(
|
|
376
|
+
ruby: &magnus::Ruby,
|
|
377
|
+
timeout: Option<std::time::Duration>,
|
|
378
|
+
) -> Result<Value, Error> {
|
|
379
|
+
let mut data = PollData {
|
|
380
|
+
timeout,
|
|
381
|
+
result: PollResult::NoEvent,
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// SAFETY: `data` is a valid stack-local `PollData` whose lifetime
|
|
385
|
+
// spans this entire call. `poll_without_gvl` only reads/writes
|
|
386
|
+
// through the pointer while `rb_thread_call_without_gvl` blocks,
|
|
387
|
+
// and we don't access `data` again until that function returns.
|
|
388
|
+
unsafe {
|
|
389
|
+
rb_thread_call_without_gvl(
|
|
390
|
+
Some(poll_without_gvl),
|
|
391
|
+
(&raw mut data).cast::<c_void>(),
|
|
392
|
+
None,
|
|
393
|
+
std::ptr::null_mut(),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// GVL is now re-held — safe to create Ruby objects.
|
|
398
|
+
match data.result {
|
|
399
|
+
PollResult::Event(event) => handle_crossterm_event(event),
|
|
400
|
+
PollResult::NoEvent => Ok(ruby.qnil().into_value_with(ruby)),
|
|
401
|
+
PollResult::Error(msg) => Err(Error::new(ruby.exception_runtime_error(), msg)),
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
317
405
|
pub fn poll_event(ruby: &magnus::Ruby, timeout_val: Option<f64>) -> Result<Value, Error> {
|
|
318
406
|
let event = EVENT_QUEUE.with(|q| {
|
|
319
407
|
let mut queue = q.borrow_mut();
|
|
@@ -334,24 +422,8 @@ pub fn poll_event(ruby: &magnus::Ruby, timeout_val: Option<f64>) -> Result<Value
|
|
|
334
422
|
return Ok(ruby.qnil().into_value_with(ruby));
|
|
335
423
|
}
|
|
336
424
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
let duration = std::time::Duration::from_secs_f64(secs);
|
|
340
|
-
if ratatui::crossterm::event::poll(duration)
|
|
341
|
-
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?
|
|
342
|
-
{
|
|
343
|
-
let event = ratatui::crossterm::event::read()
|
|
344
|
-
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
345
|
-
handle_crossterm_event(event)
|
|
346
|
-
} else {
|
|
347
|
-
Ok(ruby.qnil().into_value_with(ruby))
|
|
348
|
-
}
|
|
349
|
-
} else {
|
|
350
|
-
// Blocking: wait indefinitely for an event
|
|
351
|
-
let event = ratatui::crossterm::event::read()
|
|
352
|
-
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
353
|
-
handle_crossterm_event(event)
|
|
354
|
-
}
|
|
425
|
+
let timeout = timeout_val.map(std::time::Duration::from_secs_f64);
|
|
426
|
+
poll_crossterm_without_gvl(ruby, timeout)
|
|
355
427
|
}
|
|
356
428
|
|
|
357
429
|
fn handle_test_event(event: TestEvent) -> Result<Value, Error> {
|
|
@@ -559,3 +631,70 @@ fn handle_focus_event(event_type: &str) -> Result<Value, Error> {
|
|
|
559
631
|
hash.aset(ruby.to_symbol("type"), ruby.to_symbol(event_type))?;
|
|
560
632
|
Ok(hash.into_value_with(&ruby))
|
|
561
633
|
}
|
|
634
|
+
|
|
635
|
+
#[cfg(test)]
|
|
636
|
+
mod tests {
|
|
637
|
+
use super::*;
|
|
638
|
+
|
|
639
|
+
#[test]
|
|
640
|
+
fn poll_data_defaults_to_no_event() {
|
|
641
|
+
let data = PollData {
|
|
642
|
+
timeout: Some(std::time::Duration::from_millis(0)),
|
|
643
|
+
result: PollResult::NoEvent,
|
|
644
|
+
};
|
|
645
|
+
assert!(matches!(data.result, PollResult::NoEvent));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
#[test]
|
|
649
|
+
fn poll_data_accepts_none_timeout_for_indefinite_blocking() {
|
|
650
|
+
let data = PollData {
|
|
651
|
+
timeout: None,
|
|
652
|
+
result: PollResult::NoEvent,
|
|
653
|
+
};
|
|
654
|
+
assert!(data.timeout.is_none());
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
#[test]
|
|
658
|
+
fn poll_result_error_preserves_message() {
|
|
659
|
+
let result = PollResult::Error("connection reset".to_string());
|
|
660
|
+
match result {
|
|
661
|
+
PollResult::Error(msg) => assert_eq!(msg, "connection reset"),
|
|
662
|
+
_ => panic!("Expected PollResult::Error"),
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
#[test]
|
|
667
|
+
fn poll_result_event_wraps_crossterm_event() {
|
|
668
|
+
let key_event =
|
|
669
|
+
ratatui::crossterm::event::Event::Key(ratatui::crossterm::event::KeyEvent::new(
|
|
670
|
+
ratatui::crossterm::event::KeyCode::Char('q'),
|
|
671
|
+
ratatui::crossterm::event::KeyModifiers::empty(),
|
|
672
|
+
));
|
|
673
|
+
let result = PollResult::Event(key_event.clone());
|
|
674
|
+
match result {
|
|
675
|
+
PollResult::Event(e) => assert_eq!(format!("{e:?}"), format!("{key_event:?}")),
|
|
676
|
+
_ => panic!("Expected PollResult::Event"),
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
#[test]
|
|
681
|
+
fn poll_without_gvl_returns_no_event_on_zero_timeout() {
|
|
682
|
+
// With a zero-duration timeout, poll returns immediately with no event.
|
|
683
|
+
// In headless environments (CI), crossterm may return an Error because
|
|
684
|
+
// there is no terminal to read from — that's also a valid outcome.
|
|
685
|
+
let mut data = PollData {
|
|
686
|
+
timeout: Some(std::time::Duration::from_millis(0)),
|
|
687
|
+
result: PollResult::Error("sentinel — should be overwritten".to_string()),
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
poll_without_gvl(&mut data as *mut PollData as *mut c_void);
|
|
691
|
+
|
|
692
|
+
// The callback must have overwritten the sentinel value.
|
|
693
|
+
match &data.result {
|
|
694
|
+
PollResult::Error(msg) if msg == "sentinel — should be overwritten" => {
|
|
695
|
+
panic!("poll_without_gvl did not write to data.result")
|
|
696
|
+
}
|
|
697
|
+
_ => {} // NoEvent, Event, or a *different* Error are all fine.
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
@@ -99,7 +99,62 @@ module RatatuiRuby
|
|
|
99
99
|
#--
|
|
100
100
|
# SPDX-SnippetEnd
|
|
101
101
|
#++
|
|
102
|
+
#
|
|
103
|
+
# === Class-Wide Normalization
|
|
104
|
+
#
|
|
105
|
+
# When every test in a class faces the same dynamic content, override
|
|
106
|
+
# <tt>normalize_snapshots</tt> instead of repeating the block. Return a callable
|
|
107
|
+
# (or Array of callables) that transforms lines. The hook runs before any per-call
|
|
108
|
+
# block, so the two compose naturally. See <tt>normalize_snapshots</tt> for details.
|
|
102
109
|
module Snapshot
|
|
110
|
+
# Override this method to normalize all snapshots in a test class.
|
|
111
|
+
#
|
|
112
|
+
# Snapshot assertions compare screen content against stored files. Dynamic content
|
|
113
|
+
# (timestamps, temp paths, random IDs) breaks those comparisons. Passing a normalization
|
|
114
|
+
# block to each assertion fixes one test but creates duplication when every test in a
|
|
115
|
+
# class faces the same dynamic content.
|
|
116
|
+
#
|
|
117
|
+
# Override <tt>normalize_snapshots</tt> in your test class to define class-wide
|
|
118
|
+
# normalization. Accept an Array of Strings (lines) and return the transformed Array.
|
|
119
|
+
#
|
|
120
|
+
# The hook runs before any per-call normalization block, so the two compose naturally:
|
|
121
|
+
# the hook handles class-wide concerns and the block handles one-off masking.
|
|
122
|
+
#
|
|
123
|
+
# Returns <tt>lines</tt> unchanged by default (no normalization).
|
|
124
|
+
#
|
|
125
|
+
# === Example
|
|
126
|
+
#
|
|
127
|
+
#--
|
|
128
|
+
# SPDX-SnippetBegin
|
|
129
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
130
|
+
# SPDX-License-Identifier: MIT-0
|
|
131
|
+
#++
|
|
132
|
+
# class TestFileExplorer < Minitest::Test
|
|
133
|
+
# include RatatuiRuby::TestHelper
|
|
134
|
+
#
|
|
135
|
+
# private def normalize_snapshots(lines)
|
|
136
|
+
# lines.map { |l| l.gsub(Dir.pwd, "STABLE_PATH") }
|
|
137
|
+
# end
|
|
138
|
+
#
|
|
139
|
+
# def test_initial_render
|
|
140
|
+
# # normalize_snapshots runs automatically — no block needed
|
|
141
|
+
# assert_snapshots("initial_render")
|
|
142
|
+
# end
|
|
143
|
+
#
|
|
144
|
+
# def test_after_scroll
|
|
145
|
+
# # Per-call block composes with the hook (hook runs first)
|
|
146
|
+
# assert_snapshots("after_scroll") do |lines|
|
|
147
|
+
# lines.map { |l| l.gsub(/\d{2}:\d{2}/, "XX:XX") }
|
|
148
|
+
# end
|
|
149
|
+
# end
|
|
150
|
+
# end
|
|
151
|
+
#
|
|
152
|
+
#--
|
|
153
|
+
# SPDX-SnippetEnd
|
|
154
|
+
#++
|
|
155
|
+
private def normalize_snapshots(lines)
|
|
156
|
+
lines
|
|
157
|
+
end
|
|
103
158
|
##
|
|
104
159
|
# Asserts that the current screen content matches a stored plain text snapshot.
|
|
105
160
|
#
|
|
@@ -181,7 +236,7 @@ module RatatuiRuby
|
|
|
181
236
|
# Ensure your render logic is deterministic by seeding random number generators and stubbing
|
|
182
237
|
# time where necessary.
|
|
183
238
|
def assert_screen_matches(expected, msg = nil)
|
|
184
|
-
actual_lines = buffer_content
|
|
239
|
+
actual_lines = normalize_snapshots(buffer_content)
|
|
185
240
|
|
|
186
241
|
if block_given?
|
|
187
242
|
actual_lines = yield(actual_lines)
|
|
@@ -203,9 +258,6 @@ module RatatuiRuby
|
|
|
203
258
|
|
|
204
259
|
# Write with explicit mode to ensure clean write
|
|
205
260
|
File.write(snapshot_path, content_to_write, mode: "w")
|
|
206
|
-
|
|
207
|
-
# Flush filesystem buffers to ensure durability
|
|
208
|
-
File.open(snapshot_path, "r", &:fsync) if File.exist?(snapshot_path)
|
|
209
261
|
rescue => e
|
|
210
262
|
warn "Failed to write snapshot #{snapshot_path}: #{e.message}"
|
|
211
263
|
raise
|
|
@@ -269,12 +321,11 @@ module RatatuiRuby
|
|
|
269
321
|
|
|
270
322
|
actual_content = _render_buffer_with_ansi
|
|
271
323
|
|
|
324
|
+
lines = normalize_snapshots(actual_content.split("\n"))
|
|
272
325
|
if block_given?
|
|
273
|
-
lines = actual_content.split("\n")
|
|
274
|
-
# Yield lines to user block for modification (e.g. masking IDs/Times)
|
|
275
326
|
lines = yield(lines)
|
|
276
|
-
actual_content = "#{lines.join("\n")}\n"
|
|
277
327
|
end
|
|
328
|
+
actual_content = "#{lines.join("\n")}\n"
|
|
278
329
|
|
|
279
330
|
update_snapshots = ENV["UPDATE_SNAPSHOTS"] == "1" || ENV["UPDATE_SNAPSHOTS"] == "true"
|
|
280
331
|
|
|
@@ -287,9 +338,6 @@ module RatatuiRuby
|
|
|
287
338
|
|
|
288
339
|
# Write with explicit mode to ensure clean write
|
|
289
340
|
File.write(snapshot_path, actual_content, mode: "w")
|
|
290
|
-
|
|
291
|
-
# Flush filesystem buffers to ensure durability
|
|
292
|
-
File.open(snapshot_path, "r", &:fsync) if File.exist?(snapshot_path)
|
|
293
341
|
rescue => e
|
|
294
342
|
warn "Failed to write rich snapshot #{snapshot_path}: #{e.message}"
|
|
295
343
|
raise
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "timeout"
|
|
9
|
+
|
|
10
|
+
module RatatuiRuby
|
|
11
|
+
module TestHelper
|
|
12
|
+
##
|
|
13
|
+
# Portable subprocess timeout helper.
|
|
14
|
+
module SubprocessTimeout
|
|
15
|
+
private def popen_with_timeout(env, cmd, timeout: 2)
|
|
16
|
+
output = +""
|
|
17
|
+
IO.popen(env, cmd, err: [:child, :out]) do |io|
|
|
18
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
19
|
+
loop do
|
|
20
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
21
|
+
break if remaining <= 0
|
|
22
|
+
break unless io.wait_readable(remaining)
|
|
23
|
+
|
|
24
|
+
chunk = io.read_nonblock(4096, exception: false)
|
|
25
|
+
break if chunk.nil? || chunk == :wait_readable
|
|
26
|
+
|
|
27
|
+
output << chunk
|
|
28
|
+
end
|
|
29
|
+
Process.kill("KILL", io.pid) rescue nil # rubocop:disable Style/RescueModifier
|
|
30
|
+
end
|
|
31
|
+
output.strip
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -12,6 +12,7 @@ require_relative "test_helper/event_injection"
|
|
|
12
12
|
require_relative "test_helper/style_assertions"
|
|
13
13
|
require_relative "test_helper/test_doubles"
|
|
14
14
|
require_relative "test_helper/global_state"
|
|
15
|
+
require_relative "test_helper/subprocess_timeout"
|
|
15
16
|
|
|
16
17
|
module RatatuiRuby
|
|
17
18
|
##
|
|
@@ -109,5 +110,6 @@ module RatatuiRuby
|
|
|
109
110
|
include StyleAssertions
|
|
110
111
|
include TestDoubles
|
|
111
112
|
include GlobalState
|
|
113
|
+
include SubprocessTimeout
|
|
112
114
|
end
|
|
113
115
|
end
|
data/lib/ratatui_ruby/version.rb
CHANGED