ratatui_ruby 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +54 -0
- data/.builds/ruby-3.3.yml +54 -0
- data/.builds/ruby-3.4.yml +54 -0
- data/.builds/ruby-4.0.0.yml +54 -0
- data/.pre-commit-config.yaml +16 -0
- data/.rubocop.yml +10 -0
- data/AGENTS.md +147 -0
- data/CHANGELOG.md +771 -0
- data/README.md +187 -0
- data/README.rdoc +302 -0
- data/Rakefile +11 -0
- data/Steepfile +50 -0
- data/doc/concepts/application_architecture.md +321 -0
- data/doc/concepts/application_testing.md +193 -0
- data/doc/concepts/async.md +190 -0
- data/doc/concepts/custom_widgets.md +247 -0
- data/doc/concepts/debugging.md +401 -0
- data/doc/concepts/event_handling.md +162 -0
- data/doc/concepts/interactive_design.md +146 -0
- data/doc/contributors/auditing/parity.md +239 -0
- data/doc/contributors/design/ruby_frontend.md +448 -0
- data/doc/contributors/design/rust_backend.md +434 -0
- data/doc/contributors/design.md +11 -0
- data/doc/contributors/developing_examples.md +400 -0
- data/doc/contributors/documentation_style.md +121 -0
- data/doc/contributors/index.md +21 -0
- data/doc/contributors/releasing.md +215 -0
- data/doc/contributors/todo/align/api_completeness_audit-finished.md +381 -0
- data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +200 -0
- data/doc/contributors/todo/align/term.md +351 -0
- data/doc/contributors/todo/align/terminal.md +647 -0
- data/doc/contributors/todo/future_work.md +169 -0
- data/doc/contributors/upstream_requests/paragraph_span_rects.md +259 -0
- data/doc/contributors/upstream_requests/tab_rects.md +173 -0
- data/doc/contributors/upstream_requests/title_rects.md +132 -0
- data/doc/custom.css +22 -0
- data/doc/getting_started/quickstart.md +291 -0
- data/doc/getting_started/why.md +93 -0
- 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 +34 -0
- data/doc/troubleshooting/async.md +4 -0
- data/doc/troubleshooting/terminal_limitations.md +131 -0
- data/doc/troubleshooting/tui_output.md +197 -0
- data/examples/app_all_events/README.md +114 -0
- data/examples/app_all_events/app.rb +98 -0
- data/examples/app_all_events/model/app_model.rb +159 -0
- data/examples/app_all_events/model/event_color_cycle.rb +43 -0
- data/examples/app_all_events/model/event_entry.rb +94 -0
- data/examples/app_all_events/model/msg.rb +39 -0
- data/examples/app_all_events/model/timestamp.rb +56 -0
- data/examples/app_all_events/update.rb +75 -0
- data/examples/app_all_events/view/app_view.rb +80 -0
- data/examples/app_all_events/view/controls_view.rb +54 -0
- data/examples/app_all_events/view/counts_view.rb +61 -0
- data/examples/app_all_events/view/live_view.rb +72 -0
- data/examples/app_all_events/view/log_view.rb +57 -0
- data/examples/app_all_events/view.rb +9 -0
- data/examples/app_cli_rich_moments/README.md +81 -0
- data/examples/app_cli_rich_moments/app.rb +189 -0
- data/examples/app_color_picker/README.md +156 -0
- data/examples/app_color_picker/app.rb +76 -0
- data/examples/app_color_picker/clipboard.rb +86 -0
- data/examples/app_color_picker/color.rb +193 -0
- data/examples/app_color_picker/controls.rb +92 -0
- data/examples/app_color_picker/copy_dialog.rb +168 -0
- data/examples/app_color_picker/export_pane.rb +128 -0
- data/examples/app_color_picker/harmony.rb +58 -0
- data/examples/app_color_picker/input.rb +176 -0
- data/examples/app_color_picker/main_container.rb +180 -0
- data/examples/app_color_picker/palette.rb +111 -0
- data/examples/app_debugging_showcase/README.md +119 -0
- data/examples/app_debugging_showcase/app.rb +318 -0
- data/examples/app_external_editor/README.md +62 -0
- data/examples/app_external_editor/app.rb +344 -0
- data/examples/app_login_form/README.md +58 -0
- data/examples/app_login_form/app.rb +109 -0
- data/examples/app_stateful_interaction/README.md +35 -0
- data/examples/app_stateful_interaction/app.rb +328 -0
- data/examples/timeout_demo.rb +45 -0
- data/examples/verify_quickstart_dsl/README.md +55 -0
- data/examples/verify_quickstart_dsl/app.rb +49 -0
- data/examples/verify_quickstart_layout/README.md +77 -0
- data/examples/verify_quickstart_layout/app.rb +73 -0
- data/examples/verify_quickstart_lifecycle/README.md +68 -0
- data/examples/verify_quickstart_lifecycle/app.rb +62 -0
- data/examples/verify_readme_usage/README.md +49 -0
- data/examples/verify_readme_usage/app.rb +42 -0
- data/examples/verify_website_managed/README.md +48 -0
- data/examples/verify_website_managed/app.rb +36 -0
- data/examples/verify_website_menu/README.md +60 -0
- data/examples/verify_website_menu/app.rb +84 -0
- data/examples/verify_website_spinner/README.md +44 -0
- data/examples/verify_website_spinner/app.rb +34 -0
- data/examples/widget_barchart/README.md +58 -0
- data/examples/widget_barchart/app.rb +240 -0
- data/examples/widget_block/README.md +44 -0
- data/examples/widget_block/app.rb +258 -0
- data/examples/widget_box/README.md +54 -0
- data/examples/widget_box/app.rb +255 -0
- data/examples/widget_calendar/README.md +48 -0
- data/examples/widget_calendar/app.rb +115 -0
- data/examples/widget_canvas/README.md +31 -0
- data/examples/widget_canvas/app.rb +130 -0
- data/examples/widget_cell/README.md +45 -0
- data/examples/widget_cell/app.rb +112 -0
- data/examples/widget_center/README.md +33 -0
- data/examples/widget_center/app.rb +118 -0
- data/examples/widget_chart/README.md +50 -0
- data/examples/widget_chart/app.rb +220 -0
- data/examples/widget_gauge/README.md +50 -0
- data/examples/widget_gauge/app.rb +229 -0
- data/examples/widget_layout_split/README.md +53 -0
- data/examples/widget_layout_split/app.rb +260 -0
- data/examples/widget_line_gauge/README.md +50 -0
- data/examples/widget_line_gauge/app.rb +219 -0
- data/examples/widget_list/README.md +58 -0
- data/examples/widget_list/app.rb +382 -0
- data/examples/widget_map/README.md +48 -0
- data/examples/widget_map/app.rb +95 -0
- data/examples/widget_overlay/README.md +45 -0
- data/examples/widget_overlay/app.rb +250 -0
- data/examples/widget_popup/README.md +45 -0
- data/examples/widget_popup/app.rb +106 -0
- data/examples/widget_ratatui_logo/README.md +43 -0
- data/examples/widget_ratatui_logo/app.rb +104 -0
- data/examples/widget_ratatui_mascot/README.md +43 -0
- data/examples/widget_ratatui_mascot/app.rb +95 -0
- data/examples/widget_rect/README.md +53 -0
- data/examples/widget_rect/app.rb +222 -0
- data/examples/widget_render/README.md +46 -0
- data/examples/widget_render/app.rb +186 -0
- data/examples/widget_render/app.rbs +41 -0
- data/examples/widget_rich_text/README.md +44 -0
- data/examples/widget_rich_text/app.rb +193 -0
- data/examples/widget_scroll_text/README.md +46 -0
- data/examples/widget_scroll_text/app.rb +109 -0
- data/examples/widget_scrollbar/README.md +46 -0
- data/examples/widget_scrollbar/app.rb +155 -0
- data/examples/widget_sparkline/README.md +51 -0
- data/examples/widget_sparkline/app.rb +277 -0
- data/examples/widget_style_colors/README.md +43 -0
- data/examples/widget_style_colors/app.rb +83 -0
- data/examples/widget_table/README.md +57 -0
- data/examples/widget_table/app.rb +285 -0
- data/examples/widget_tabs/README.md +50 -0
- data/examples/widget_tabs/app.rb +183 -0
- data/examples/widget_text_width/README.md +44 -0
- data/examples/widget_text_width/app.rb +117 -0
- data/ext/ratatui_ruby/Cargo.lock +1 -2
- data/ext/ratatui_ruby/Cargo.toml +1 -2
- data/ext/ratatui_ruby/src/events.rs +18 -157
- data/lib/ratatui_ruby/event/focus_gained.rb +50 -0
- data/lib/ratatui_ruby/event/focus_lost.rb +51 -0
- data/lib/ratatui_ruby/event/key/dwim.rb +301 -0
- data/lib/ratatui_ruby/event/key/modifier.rb +2 -0
- data/lib/ratatui_ruby/event/key.rb +9 -0
- data/lib/ratatui_ruby/event/mouse.rb +33 -0
- data/lib/ratatui_ruby/event/paste.rb +25 -0
- data/lib/ratatui_ruby/event/resize.rb +65 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- data/migrate_to_buffer.rb +145 -0
- data/mise.toml +8 -0
- data/sig/ratatui_ruby/event.rbs +97 -0
- data/tasks/autodoc/examples.rb +87 -0
- data/tasks/autodoc/member.rb +58 -0
- data/tasks/autodoc/name.rb +21 -0
- data/tasks/autodoc.rake +21 -0
- data/tasks/bump/bump_workflow.rb +49 -0
- data/tasks/bump/cargo_lockfile.rb +21 -0
- data/tasks/bump/changelog.rb +104 -0
- data/tasks/bump/header.rb +32 -0
- data/tasks/bump/history.rb +32 -0
- data/tasks/bump/links.rb +69 -0
- data/tasks/bump/manifest.rb +33 -0
- data/tasks/bump/patch_release.rb +19 -0
- data/tasks/bump/release_branch.rb +17 -0
- data/tasks/bump/release_from_trunk.rb +49 -0
- data/tasks/bump/repository.rb +54 -0
- data/tasks/bump/ruby_gem.rb +29 -0
- data/tasks/bump/sem_ver.rb +44 -0
- data/tasks/bump/unreleased_section.rb +73 -0
- data/tasks/bump.rake +61 -0
- data/tasks/doc/documentation.rb +59 -0
- data/tasks/doc/link/file_url.rb +30 -0
- data/tasks/doc/link/relative_path.rb +61 -0
- data/tasks/doc/link/web_url.rb +55 -0
- data/tasks/doc/link.rb +52 -0
- data/tasks/doc/link_audit.rb +116 -0
- data/tasks/doc/problem.rb +40 -0
- data/tasks/doc/source_file.rb +93 -0
- data/tasks/doc.rake +905 -0
- data/tasks/example_viewer.html.erb +172 -0
- data/tasks/extension.rake +14 -0
- data/tasks/license/headers_md.rb +223 -0
- data/tasks/license/headers_rb.rb +210 -0
- data/tasks/license/license_utils.rb +130 -0
- data/tasks/license/snippets_md.rb +315 -0
- data/tasks/license/snippets_rdoc.rb +150 -0
- data/tasks/license.rake +91 -0
- data/tasks/lint.rake +170 -0
- data/tasks/rbs_predicates/predicate_catalog.rb +52 -0
- data/tasks/rbs_predicates/predicate_tests.rb +124 -0
- data/tasks/rbs_predicates/rbs_signature.rb +63 -0
- data/tasks/rbs_predicates.rake +31 -0
- data/tasks/rdoc_config.rb +29 -0
- data/tasks/resources/build.yml.erb +60 -0
- data/tasks/resources/index.html.erb +141 -0
- data/tasks/resources/rubies.yml +7 -0
- data/tasks/sourcehut.rake +122 -0
- data/tasks/steep.rake +11 -0
- data/tasks/terminal_preview/app_screenshot.rb +45 -0
- data/tasks/terminal_preview/crash_report.rb +54 -0
- data/tasks/terminal_preview/example_app.rb +27 -0
- data/tasks/terminal_preview/launcher_script.rb +48 -0
- data/tasks/terminal_preview/preview_collection.rb +60 -0
- data/tasks/terminal_preview/preview_timing.rb +24 -0
- data/tasks/terminal_preview/safety_confirmation.rb +58 -0
- data/tasks/terminal_preview/saved_screenshot.rb +56 -0
- data/tasks/terminal_preview/system_appearance.rb +13 -0
- data/tasks/terminal_preview/terminal_window.rb +138 -0
- data/tasks/terminal_preview/window_id.rb +16 -0
- data/tasks/terminal_preview.rake +30 -0
- data/tasks/test.rake +36 -0
- data/tasks/website/index_page.rb +30 -0
- data/tasks/website/version.rb +122 -0
- data/tasks/website/version_menu.rb +68 -0
- data/tasks/website/versioned_documentation.rb +83 -0
- data/tasks/website/website.rb +53 -0
- data/tasks/website.rake +28 -0
- metadata +256 -1
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.3.0"
|
|
7
7
|
edition = "2021"
|
|
8
8
|
|
|
9
9
|
[lib]
|
|
@@ -11,7 +11,6 @@ crate-type = ["cdylib", "staticlib"]
|
|
|
11
11
|
|
|
12
12
|
[dependencies]
|
|
13
13
|
magnus = "0.8.2"
|
|
14
|
-
rb-sys = "*"
|
|
15
14
|
ratatui = { version = "0.30", features = ["widget-calendar", "layout-cache", "unstable-rendered-line-info", "palette"] }
|
|
16
15
|
unicode-width = "0.1"
|
|
17
16
|
|
|
@@ -2,9 +2,7 @@
|
|
|
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;
|
|
6
5
|
use std::cell::RefCell;
|
|
7
|
-
use std::os::raw::c_void;
|
|
8
6
|
|
|
9
7
|
/// Wrapper enum for test events - includes crossterm events and our Sync event.
|
|
10
8
|
#[derive(Debug, Clone)]
|
|
@@ -316,92 +314,6 @@ pub fn clear_events() {
|
|
|
316
314
|
EVENT_QUEUE.with(|q| q.borrow_mut().clear());
|
|
317
315
|
}
|
|
318
316
|
|
|
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
|
-
|
|
405
317
|
pub fn poll_event(ruby: &magnus::Ruby, timeout_val: Option<f64>) -> Result<Value, Error> {
|
|
406
318
|
let event = EVENT_QUEUE.with(|q| {
|
|
407
319
|
let mut queue = q.borrow_mut();
|
|
@@ -422,8 +334,24 @@ pub fn poll_event(ruby: &magnus::Ruby, timeout_val: Option<f64>) -> Result<Value
|
|
|
422
334
|
return Ok(ruby.qnil().into_value_with(ruby));
|
|
423
335
|
}
|
|
424
336
|
|
|
425
|
-
let
|
|
426
|
-
|
|
337
|
+
if let Some(secs) = timeout_val {
|
|
338
|
+
// Timed poll: wait up to the specified duration
|
|
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
|
+
}
|
|
427
355
|
}
|
|
428
356
|
|
|
429
357
|
fn handle_test_event(event: TestEvent) -> Result<Value, Error> {
|
|
@@ -631,70 +559,3 @@ fn handle_focus_event(event_type: &str) -> Result<Value, Error> {
|
|
|
631
559
|
hash.aset(ruby.to_symbol("type"), ruby.to_symbol(event_type))?;
|
|
632
560
|
Ok(hash.into_value_with(&ruby))
|
|
633
561
|
}
|
|
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
|
-
}
|
|
@@ -70,6 +70,56 @@ module RatatuiRuby
|
|
|
70
70
|
def ==(other)
|
|
71
71
|
other.is_a?(FocusGained)
|
|
72
72
|
end
|
|
73
|
+
|
|
74
|
+
# =========================================================================
|
|
75
|
+
# DWIM Predicates
|
|
76
|
+
# =========================================================================
|
|
77
|
+
|
|
78
|
+
# Returns true. The terminal window is now in focus.
|
|
79
|
+
#
|
|
80
|
+
# event.focus? # => true
|
|
81
|
+
def focus?
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
alias focused? focus?
|
|
85
|
+
|
|
86
|
+
# Returns true. The application gained focus.
|
|
87
|
+
#
|
|
88
|
+
# event.gained? # => true
|
|
89
|
+
def gained?
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns false. This is not a focus lost event.
|
|
94
|
+
#
|
|
95
|
+
# event.lost? # => false
|
|
96
|
+
def lost?
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns false. Blur is the opposite of focus gained.
|
|
101
|
+
#
|
|
102
|
+
# event.blur? # => false
|
|
103
|
+
def blur?
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
alias blurred? blur?
|
|
107
|
+
|
|
108
|
+
# Returns true. The application is active.
|
|
109
|
+
#
|
|
110
|
+
# event.active? # => true
|
|
111
|
+
def active?
|
|
112
|
+
true
|
|
113
|
+
end
|
|
114
|
+
alias foreground? active?
|
|
115
|
+
|
|
116
|
+
# Returns false. The application is not inactive.
|
|
117
|
+
#
|
|
118
|
+
# event.inactive? # => false
|
|
119
|
+
def inactive?
|
|
120
|
+
false
|
|
121
|
+
end
|
|
122
|
+
alias background? inactive?
|
|
73
123
|
end
|
|
74
124
|
end
|
|
75
125
|
end
|
|
@@ -71,6 +71,57 @@ module RatatuiRuby
|
|
|
71
71
|
def ==(other)
|
|
72
72
|
other.is_a?(FocusLost)
|
|
73
73
|
end
|
|
74
|
+
|
|
75
|
+
# =========================================================================
|
|
76
|
+
# DWIM Predicates
|
|
77
|
+
# =========================================================================
|
|
78
|
+
|
|
79
|
+
# Returns true. The terminal has lost focus (blur).
|
|
80
|
+
#
|
|
81
|
+
# event.blur? # => true
|
|
82
|
+
def blur?
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
alias blurred? blur?
|
|
86
|
+
|
|
87
|
+
# Returns true. The application lost focus.
|
|
88
|
+
#
|
|
89
|
+
# event.lost? # => true
|
|
90
|
+
def lost?
|
|
91
|
+
true
|
|
92
|
+
end
|
|
93
|
+
alias unfocused? lost?
|
|
94
|
+
|
|
95
|
+
# Returns false. This is not a focus gained event.
|
|
96
|
+
#
|
|
97
|
+
# event.focus? # => false
|
|
98
|
+
def focus?
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
alias focused? focus?
|
|
102
|
+
|
|
103
|
+
# Returns false. This is not a gained event.
|
|
104
|
+
#
|
|
105
|
+
# event.gained? # => false
|
|
106
|
+
def gained?
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Returns true. The application is inactive.
|
|
111
|
+
#
|
|
112
|
+
# event.inactive? # => true
|
|
113
|
+
def inactive?
|
|
114
|
+
true
|
|
115
|
+
end
|
|
116
|
+
alias background? inactive?
|
|
117
|
+
|
|
118
|
+
# Returns false. The application is not active.
|
|
119
|
+
#
|
|
120
|
+
# event.active? # => false
|
|
121
|
+
def active?
|
|
122
|
+
false
|
|
123
|
+
end
|
|
124
|
+
alias foreground? active?
|
|
74
125
|
end
|
|
75
126
|
end
|
|
76
127
|
end
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module RatatuiRuby
|
|
9
|
+
class Event
|
|
10
|
+
class Key < Event
|
|
11
|
+
# DWIM predicates for common key patterns.
|
|
12
|
+
#
|
|
13
|
+
# These predicates anticipate what developers intuitively try. Space bars,
|
|
14
|
+
# character categories, Unix signals, and Vim-style navigation.
|
|
15
|
+
module Dwim
|
|
16
|
+
# Returns true if the key is a space character.
|
|
17
|
+
#
|
|
18
|
+
# event.space? # => true for " "
|
|
19
|
+
def space?
|
|
20
|
+
@code == " " && @modifiers.empty?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
alias spacebar? space?
|
|
24
|
+
|
|
25
|
+
# Returns true if the key is Enter. Alias for carriage return.
|
|
26
|
+
#
|
|
27
|
+
# event.cr? # => true for enter
|
|
28
|
+
def cr?
|
|
29
|
+
@code == "enter" && @modifiers.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
alias carriagereturn? cr?
|
|
33
|
+
alias linefeed? cr?
|
|
34
|
+
alias newline? cr?
|
|
35
|
+
alias lf? cr?
|
|
36
|
+
|
|
37
|
+
# Returns true if the key is a single letter (a-z, A-Z).
|
|
38
|
+
#
|
|
39
|
+
# Event::Key.new(code: "a").letter? # => true
|
|
40
|
+
def letter?
|
|
41
|
+
@code.length == 1 && @code.match?(/\A[A-Za-z]\z/)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns true if the key is a digit (0-9).
|
|
45
|
+
#
|
|
46
|
+
# Event::Key.new(code: "5").digit? # => true
|
|
47
|
+
def digit?
|
|
48
|
+
@code.length == 1 && @code.match?(/\A[0-9]\z/)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns true if the key is alphanumeric.
|
|
52
|
+
#
|
|
53
|
+
# Event::Key.new(code: "a").alphanumeric? # => true
|
|
54
|
+
def alphanumeric?
|
|
55
|
+
letter? || digit?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns true if the key is punctuation.
|
|
59
|
+
#
|
|
60
|
+
# Event::Key.new(code: "@", modifiers: ["shift"]).punctuation? # => true
|
|
61
|
+
def punctuation?
|
|
62
|
+
return false unless @code.length == 1
|
|
63
|
+
!letter? && !digit? && !whitespace?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns true if the key is whitespace (space, enter, tab).
|
|
67
|
+
#
|
|
68
|
+
# Event::Key.new(code: " ").whitespace? # => true
|
|
69
|
+
def whitespace?
|
|
70
|
+
@code == " " || @code == "enter" || @code == "tab"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns true for interrupt (Ctrl+C).
|
|
74
|
+
#
|
|
75
|
+
# event.interrupt? # => true for Ctrl+C
|
|
76
|
+
def interrupt?
|
|
77
|
+
@code == "c" && @modifiers == ["ctrl"]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns true for end-of-file (Ctrl+D).
|
|
81
|
+
#
|
|
82
|
+
# event.eof? # => true for Ctrl+D
|
|
83
|
+
def eof?
|
|
84
|
+
@code == "d" && @modifiers == ["ctrl"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns true for cancel (Esc or Ctrl+C).
|
|
88
|
+
#
|
|
89
|
+
# event.cancel? # => true for Esc or Ctrl+C
|
|
90
|
+
def cancel?
|
|
91
|
+
(@code == "esc" && @modifiers.empty?) || interrupt?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns true for SIGINT (Ctrl+C).
|
|
95
|
+
#
|
|
96
|
+
# event.sigint? # => true for Ctrl+C
|
|
97
|
+
def sigint?
|
|
98
|
+
interrupt?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
alias int? sigint?
|
|
102
|
+
|
|
103
|
+
# Returns true for SIGTSTP (Ctrl+Z) - suspend/stop.
|
|
104
|
+
#
|
|
105
|
+
# event.suspend? # => true for Ctrl+Z
|
|
106
|
+
def suspend?
|
|
107
|
+
@code == "z" && @modifiers == ["ctrl"]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
alias sigtstp? suspend?
|
|
111
|
+
alias tstp? suspend?
|
|
112
|
+
|
|
113
|
+
# Returns true for SIGQUIT (Ctrl+\).
|
|
114
|
+
#
|
|
115
|
+
# event.quit? # => true for Ctrl+\
|
|
116
|
+
def quit?
|
|
117
|
+
@code == "\\" && @modifiers == ["ctrl"]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
alias sigquit? quit?
|
|
121
|
+
|
|
122
|
+
NAVIGATION_KEYS = %w[up down left right home end page_up page_down].freeze # :nodoc:
|
|
123
|
+
|
|
124
|
+
# Returns true if key is a navigation key.
|
|
125
|
+
#
|
|
126
|
+
# Event::Key.new(code: "up").navigation? # => true
|
|
127
|
+
def navigation?
|
|
128
|
+
NAVIGATION_KEYS.include?(@code) && @modifiers.empty?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
ARROW_KEYS = %w[up down left right].freeze # :nodoc:
|
|
132
|
+
|
|
133
|
+
# Returns true if key is an arrow key.
|
|
134
|
+
#
|
|
135
|
+
# Event::Key.new(code: "up").arrow? # => true
|
|
136
|
+
def arrow?
|
|
137
|
+
ARROW_KEYS.include?(@code) && @modifiers.empty?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
VIM_MOVEMENT_KEYS = %w[h j k l w b g G].freeze # :nodoc:
|
|
141
|
+
|
|
142
|
+
# Returns true if key is a Vim movement key.
|
|
143
|
+
#
|
|
144
|
+
# Event::Key.new(code: "j").vim? # => true
|
|
145
|
+
def vim?
|
|
146
|
+
return true if VIM_MOVEMENT_KEYS.include?(@code) && @modifiers.empty?
|
|
147
|
+
@code == "G" && @modifiers == ["shift"]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Returns true for Vim left (h).
|
|
151
|
+
def vim_left?
|
|
152
|
+
@code == "h" && @modifiers.empty?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Returns true for Vim down (j).
|
|
156
|
+
def vim_down?
|
|
157
|
+
@code == "j" && @modifiers.empty?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Returns true for Vim up (k).
|
|
161
|
+
def vim_up?
|
|
162
|
+
@code == "k" && @modifiers.empty?
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Returns true for Vim right (l).
|
|
166
|
+
def vim_right?
|
|
167
|
+
@code == "l" && @modifiers.empty?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Returns true for Vim word forward (w).
|
|
171
|
+
def vim_word_forward?
|
|
172
|
+
@code == "w" && @modifiers.empty?
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Returns true for Vim word backward (b).
|
|
176
|
+
def vim_word_backward?
|
|
177
|
+
@code == "b" && @modifiers.empty?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Returns true for Vim go to top (gg pattern, here just g).
|
|
181
|
+
def vim_top?
|
|
182
|
+
@code == "g" && @modifiers.empty?
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Returns true for Vim go to bottom (G).
|
|
186
|
+
def vim_bottom?
|
|
187
|
+
@code == "G" && @modifiers == ["shift"]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Punctuation name predicates - generated at load time for performance.
|
|
191
|
+
# Maps intuitive names to their symbol characters.
|
|
192
|
+
# :nodoc:
|
|
193
|
+
PUNCTUATION_NAMES = {
|
|
194
|
+
# Navigation shortcuts (the original use case!)
|
|
195
|
+
tilde: "~",
|
|
196
|
+
slash: "/",
|
|
197
|
+
forwardslash: "/",
|
|
198
|
+
backslash: "\\",
|
|
199
|
+
|
|
200
|
+
# Common punctuation
|
|
201
|
+
comma: ",",
|
|
202
|
+
period: ".",
|
|
203
|
+
dot: ".",
|
|
204
|
+
colon: ":",
|
|
205
|
+
semicolon: ";",
|
|
206
|
+
|
|
207
|
+
# Question and exclamation
|
|
208
|
+
question: "?",
|
|
209
|
+
questionmark: "?",
|
|
210
|
+
exclamation: "!",
|
|
211
|
+
exclamationmark: "!",
|
|
212
|
+
exclamationpoint: "!",
|
|
213
|
+
bang: "!",
|
|
214
|
+
|
|
215
|
+
# Programming symbols
|
|
216
|
+
at: "@",
|
|
217
|
+
atsign: "@",
|
|
218
|
+
hash: "#",
|
|
219
|
+
pound: "#",
|
|
220
|
+
numbersign: "#",
|
|
221
|
+
dollar: "$",
|
|
222
|
+
dollarsign: "$",
|
|
223
|
+
percent: "%",
|
|
224
|
+
caret: "^",
|
|
225
|
+
circumflex: "^",
|
|
226
|
+
ampersand: "&",
|
|
227
|
+
asterisk: "*",
|
|
228
|
+
star: "*",
|
|
229
|
+
|
|
230
|
+
# Arithmetic and comparison
|
|
231
|
+
underscore: "_",
|
|
232
|
+
hyphen: "-",
|
|
233
|
+
dash: "-",
|
|
234
|
+
minus: "-",
|
|
235
|
+
plus: "+",
|
|
236
|
+
equals: "=",
|
|
237
|
+
equalsign: "=",
|
|
238
|
+
pipe: "|",
|
|
239
|
+
bar: "|",
|
|
240
|
+
lessthan: "<",
|
|
241
|
+
lt: "<",
|
|
242
|
+
greaterthan: ">",
|
|
243
|
+
gt: ">",
|
|
244
|
+
|
|
245
|
+
# Brackets and parens
|
|
246
|
+
lparen: "(",
|
|
247
|
+
leftparen: "(",
|
|
248
|
+
openparen: "(",
|
|
249
|
+
leftparenthesis: "(",
|
|
250
|
+
openparenthesis: "(",
|
|
251
|
+
rparen: ")",
|
|
252
|
+
rightparen: ")",
|
|
253
|
+
closeparen: ")",
|
|
254
|
+
rightparenthesis: ")",
|
|
255
|
+
closeparenthesis: ")",
|
|
256
|
+
lbracket: "[",
|
|
257
|
+
leftbracket: "[",
|
|
258
|
+
openbracket: "[",
|
|
259
|
+
leftsquarebracket: "[",
|
|
260
|
+
opensquarebracket: "[",
|
|
261
|
+
rbracket: "]",
|
|
262
|
+
rightbracket: "]",
|
|
263
|
+
closebracket: "]",
|
|
264
|
+
rightsquarebracket: "]",
|
|
265
|
+
closesquarebracket: "]",
|
|
266
|
+
lbrace: "{",
|
|
267
|
+
leftbrace: "{",
|
|
268
|
+
openbrace: "{",
|
|
269
|
+
leftcurlybrace: "{",
|
|
270
|
+
opencurlybrace: "{",
|
|
271
|
+
rbrace: "}",
|
|
272
|
+
rightbrace: "}",
|
|
273
|
+
closebrace: "}",
|
|
274
|
+
rightcurlybrace: "}",
|
|
275
|
+
closecurlybrace: "}",
|
|
276
|
+
|
|
277
|
+
# Quotes
|
|
278
|
+
backtick: "`",
|
|
279
|
+
grave: "`",
|
|
280
|
+
singlequote: "'",
|
|
281
|
+
apostrophe: "'",
|
|
282
|
+
doublequote: "\"",
|
|
283
|
+
}.freeze
|
|
284
|
+
|
|
285
|
+
# Generate predicate methods at load time (faster than method_missing)
|
|
286
|
+
PUNCTUATION_NAMES.each do |name, char|
|
|
287
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
288
|
+
def #{name}?
|
|
289
|
+
@code == #{char.inspect}
|
|
290
|
+
end
|
|
291
|
+
RUBY
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# quote? matches both single and double quotes
|
|
295
|
+
def quote?
|
|
296
|
+
@code == "'" || @code == "\""
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#++
|
|
7
7
|
|
|
8
8
|
require_relative "key/character"
|
|
9
|
+
require_relative "key/dwim"
|
|
9
10
|
require_relative "key/media"
|
|
10
11
|
require_relative "key/modifier"
|
|
11
12
|
require_relative "key/navigation"
|
|
@@ -88,6 +89,7 @@ module RatatuiRuby
|
|
|
88
89
|
# These keys will not work in Terminal.app, iTerm2, or GNOME Terminal.
|
|
89
90
|
class Key < Event
|
|
90
91
|
include Character
|
|
92
|
+
include Dwim
|
|
91
93
|
include Media
|
|
92
94
|
include Modifier
|
|
93
95
|
include Navigation
|
|
@@ -435,6 +437,13 @@ module RatatuiRuby
|
|
|
435
437
|
normalized_code = @code.delete("_")
|
|
436
438
|
return true if normalized_predicate == normalized_code && @modifiers.empty?
|
|
437
439
|
|
|
440
|
+
# DWIM: Underscore variants delegate to existing methods
|
|
441
|
+
# space_bar? → spacebar? → space?, sig_int? → sigint?
|
|
442
|
+
normalized_method = :"#{normalized_predicate}?"
|
|
443
|
+
if normalized_method != name && respond_to?(normalized_method)
|
|
444
|
+
return public_send(normalized_method)
|
|
445
|
+
end
|
|
446
|
+
|
|
438
447
|
false
|
|
439
448
|
else
|
|
440
449
|
super
|