ratatui_ruby 0.1.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 +7 -0
- data/.build.yml +34 -0
- data/.pre-commit-config.yaml +9 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +119 -0
- data/CHANGELOG.md +15 -0
- data/CODE_OF_CONDUCT.md +30 -0
- data/CONTRIBUTING.md +40 -0
- data/LICENSE +15 -0
- data/LICENSES/AGPL-3.0-or-later.txt +661 -0
- data/LICENSES/BSD-2-Clause.txt +9 -0
- data/LICENSES/CC-BY-SA-4.0.txt +427 -0
- data/LICENSES/CC0-1.0.txt +121 -0
- data/LICENSES/MIT.txt +21 -0
- data/README.md +86 -0
- data/REUSE.toml +17 -0
- data/Rakefile +108 -0
- data/docs/application_testing.md +96 -0
- data/docs/contributors/design/ruby_frontend.md +100 -0
- data/docs/contributors/design/rust_backend.md +61 -0
- data/docs/contributors/design.md +11 -0
- data/docs/contributors/index.md +16 -0
- data/docs/images/examples-analytics.rb.png +0 -0
- data/docs/images/examples-box_demo.rb.png +0 -0
- data/docs/images/examples-dashboard.rb.png +0 -0
- data/docs/images/examples-login_form.rb.png +0 -0
- data/docs/images/examples-map_demo.rb.png +0 -0
- data/docs/images/examples-mouse_events.rb.png +0 -0
- data/docs/images/examples-scrollbar_demo.rb.png +0 -0
- data/docs/images/examples-stock_ticker.rb.png +0 -0
- data/docs/images/examples-system_monitor.rb.png +0 -0
- data/docs/index.md +18 -0
- data/docs/quickstart.md +126 -0
- data/examples/analytics.rb +87 -0
- data/examples/box_demo.rb +71 -0
- data/examples/dashboard.rb +72 -0
- data/examples/login_form.rb +114 -0
- data/examples/map_demo.rb +58 -0
- data/examples/mouse_events.rb +95 -0
- data/examples/scrollbar_demo.rb +75 -0
- data/examples/stock_ticker.rb +85 -0
- data/examples/system_monitor.rb +93 -0
- data/examples/test_analytics.rb +65 -0
- data/examples/test_box_demo.rb +38 -0
- data/examples/test_dashboard.rb +38 -0
- data/examples/test_login_form.rb +63 -0
- data/examples/test_map_demo.rb +100 -0
- data/examples/test_stock_ticker.rb +39 -0
- data/examples/test_system_monitor.rb +40 -0
- data/ext/ratatui_ruby/.cargo/config.toml +8 -0
- data/ext/ratatui_ruby/.gitignore +4 -0
- data/ext/ratatui_ruby/Cargo.lock +698 -0
- data/ext/ratatui_ruby/Cargo.toml +16 -0
- data/ext/ratatui_ruby/extconf.rb +12 -0
- data/ext/ratatui_ruby/src/events.rs +279 -0
- data/ext/ratatui_ruby/src/lib.rs +105 -0
- data/ext/ratatui_ruby/src/rendering.rs +31 -0
- data/ext/ratatui_ruby/src/style.rs +149 -0
- data/ext/ratatui_ruby/src/terminal.rs +131 -0
- data/ext/ratatui_ruby/src/widgets/barchart.rs +73 -0
- data/ext/ratatui_ruby/src/widgets/block.rs +12 -0
- data/ext/ratatui_ruby/src/widgets/canvas.rs +146 -0
- data/ext/ratatui_ruby/src/widgets/center.rs +81 -0
- data/ext/ratatui_ruby/src/widgets/cursor.rs +29 -0
- data/ext/ratatui_ruby/src/widgets/gauge.rs +50 -0
- data/ext/ratatui_ruby/src/widgets/layout.rs +82 -0
- data/ext/ratatui_ruby/src/widgets/linechart.rs +154 -0
- data/ext/ratatui_ruby/src/widgets/list.rs +62 -0
- data/ext/ratatui_ruby/src/widgets/mod.rs +18 -0
- data/ext/ratatui_ruby/src/widgets/overlay.rs +20 -0
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +56 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +68 -0
- data/ext/ratatui_ruby/src/widgets/sparkline.rs +59 -0
- data/ext/ratatui_ruby/src/widgets/table.rs +117 -0
- data/ext/ratatui_ruby/src/widgets/tabs.rs +51 -0
- data/lib/ratatui_ruby/output.rb +7 -0
- data/lib/ratatui_ruby/schema/bar_chart.rb +28 -0
- data/lib/ratatui_ruby/schema/block.rb +23 -0
- data/lib/ratatui_ruby/schema/canvas.rb +62 -0
- data/lib/ratatui_ruby/schema/center.rb +19 -0
- data/lib/ratatui_ruby/schema/constraint.rb +33 -0
- data/lib/ratatui_ruby/schema/cursor.rb +17 -0
- data/lib/ratatui_ruby/schema/gauge.rb +24 -0
- data/lib/ratatui_ruby/schema/layout.rb +22 -0
- data/lib/ratatui_ruby/schema/line_chart.rb +41 -0
- data/lib/ratatui_ruby/schema/list.rb +22 -0
- data/lib/ratatui_ruby/schema/overlay.rb +15 -0
- data/lib/ratatui_ruby/schema/paragraph.rb +37 -0
- data/lib/ratatui_ruby/schema/scrollbar.rb +33 -0
- data/lib/ratatui_ruby/schema/sparkline.rb +24 -0
- data/lib/ratatui_ruby/schema/style.rb +31 -0
- data/lib/ratatui_ruby/schema/table.rb +24 -0
- data/lib/ratatui_ruby/schema/tabs.rb +22 -0
- data/lib/ratatui_ruby/test_helper.rb +75 -0
- data/lib/ratatui_ruby/version.rb +10 -0
- data/lib/ratatui_ruby.rb +87 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +16 -0
- data/sig/ratatui_ruby/schema/bar_chart.rbs +14 -0
- data/sig/ratatui_ruby/schema/block.rbs +11 -0
- data/sig/ratatui_ruby/schema/canvas.rbs +62 -0
- data/sig/ratatui_ruby/schema/center.rbs +11 -0
- data/sig/ratatui_ruby/schema/constraint.rbs +13 -0
- data/sig/ratatui_ruby/schema/cursor.rbs +10 -0
- data/sig/ratatui_ruby/schema/gauge.rbs +13 -0
- data/sig/ratatui_ruby/schema/layout.rbs +11 -0
- data/sig/ratatui_ruby/schema/line_chart.rbs +20 -0
- data/sig/ratatui_ruby/schema/list.rbs +11 -0
- data/sig/ratatui_ruby/schema/overlay.rbs +9 -0
- data/sig/ratatui_ruby/schema/paragraph.rbs +11 -0
- data/sig/ratatui_ruby/schema/scrollbar.rbs +20 -0
- data/sig/ratatui_ruby/schema/sparkline.rbs +12 -0
- data/sig/ratatui_ruby/schema/style.rbs +13 -0
- data/sig/ratatui_ruby/schema/table.rbs +13 -0
- data/sig/ratatui_ruby/schema/tabs.rbs +11 -0
- data/sig/ratatui_ruby/test_helper.rbs +11 -0
- data/sig/ratatui_ruby/version.rbs +6 -0
- data/vendor/goodcop/base.yml +1047 -0
- metadata +196 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
require "bundler/gem_tasks"
|
|
7
|
+
require "minitest/test_task"
|
|
8
|
+
|
|
9
|
+
# Ruby tests are handled by test:ruby
|
|
10
|
+
# Cargo tests are handled by test:rust
|
|
11
|
+
|
|
12
|
+
require "rake/extensiontask"
|
|
13
|
+
|
|
14
|
+
spec = Gem::Specification.load("ratatui_ruby.gemspec")
|
|
15
|
+
Rake::ExtensionTask.new("ratatui_ruby", spec) do |ext|
|
|
16
|
+
ext.lib_dir = "lib/ratatui_ruby"
|
|
17
|
+
ext.ext_dir = "ext/ratatui_ruby"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The :compile task is now provided by rake-compiler
|
|
21
|
+
|
|
22
|
+
require "rubocop/rake_task"
|
|
23
|
+
|
|
24
|
+
RuboCop::RakeTask.new
|
|
25
|
+
|
|
26
|
+
require "rdoc/task"
|
|
27
|
+
|
|
28
|
+
RDoc::Task.new do |rdoc|
|
|
29
|
+
rdoc.rdoc_dir = "doc"
|
|
30
|
+
rdoc.main = "README.md"
|
|
31
|
+
rdoc.rdoc_files.include("**/*.md", "**/*.rdoc", "lib/**/*.rb", "exe/**/*")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Rake::Task[:rdoc].enhance do
|
|
35
|
+
FileUtils.mkdir_p "doc/docs/images"
|
|
36
|
+
FileUtils.cp_r FileList["docs/images/*.png"], "doc/docs/images"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Rake::Task[:rerdoc].enhance do
|
|
40
|
+
FileUtils.mkdir_p "doc/docs/images"
|
|
41
|
+
FileUtils.cp_r FileList["docs/images/*.png"], "doc/docs/images"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
require "rubycritic/rake_task"
|
|
45
|
+
|
|
46
|
+
RubyCritic::RakeTask.new do |task|
|
|
47
|
+
task.options = "--no-browser"
|
|
48
|
+
task.paths = FileList.new.include("exe/**/*.rb", "lib/**/*.rb", "sig/**/*.rbs")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
require "inch/rake"
|
|
52
|
+
|
|
53
|
+
Inch::Rake::Suggest.new("doc:suggest", "exe/**/*.rb", "lib/**/*.rb", "sig/**/*.rbs") do |suggest|
|
|
54
|
+
suggest.args << ""
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
namespace :cargo do
|
|
58
|
+
desc "Run cargo fmt"
|
|
59
|
+
task :fmt do
|
|
60
|
+
sh "cd ext/ratatui_ruby && cargo fmt --all -- --check"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
desc "Run cargo clippy"
|
|
64
|
+
task :clippy do
|
|
65
|
+
sh "cd ext/ratatui_ruby && cargo clippy -- -D warnings"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
desc "Run cargo tests"
|
|
69
|
+
task :test do
|
|
70
|
+
sh "cd ext/ratatui_ruby && cargo test"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
namespace :reuse do
|
|
75
|
+
desc "Run the REUSE Tool to confirm REUSE compliance"
|
|
76
|
+
task :lint do
|
|
77
|
+
sh "pipx run reuse lint"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
task(:reuse) { Rake::Task["reuse:lint"].invoke }
|
|
81
|
+
|
|
82
|
+
namespace :lint do
|
|
83
|
+
multitask docs: %i[rubycritic rdoc:coverage reuse:lint]
|
|
84
|
+
multitask code: %i[rubocop rubycritic cargo:fmt cargo:clippy cargo:test]
|
|
85
|
+
multitask licenses: %i[reuse:lint]
|
|
86
|
+
multitask all: %i[docs code licenses]
|
|
87
|
+
end
|
|
88
|
+
task(:lint) { Rake::Task["lint:all"].invoke }
|
|
89
|
+
|
|
90
|
+
# Clear the default test task created by Minitest::TestTask
|
|
91
|
+
Rake::Task["test"].clear
|
|
92
|
+
|
|
93
|
+
desc "Run all tests (Ruby and Rust)"
|
|
94
|
+
task test: %w[test:ruby test:rust]
|
|
95
|
+
|
|
96
|
+
namespace :test do
|
|
97
|
+
desc "Run Rust tests"
|
|
98
|
+
task :rust do
|
|
99
|
+
Rake::Task["cargo:test"].invoke
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Create a specific Minitest task for Ruby tests
|
|
103
|
+
Minitest::TestTask.create(:ruby) do |t|
|
|
104
|
+
t.test_globs = ["test/**/test_*.rb", "examples/**/test_*.rb"]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
multitask default: %i[test lint]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
# Application Testing Guide
|
|
6
|
+
|
|
7
|
+
This guide explains how to test your RatatuiRuby applications using the provided `RatatuiRuby::TestHelper`.
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
RatatuiRuby includes a `TestHelper` module designed to simplify unit testing of TUI applications. It allows you to:
|
|
12
|
+
|
|
13
|
+
- Initialize a virtual "test terminal" with specific dimensions.
|
|
14
|
+
|
|
15
|
+
- Capture the rendered output (the "buffer") to assert against expected text.
|
|
16
|
+
|
|
17
|
+
- Inspect the cursor position.
|
|
18
|
+
|
|
19
|
+
- Simulate user input (using `inject_event`).
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
First, require the test helper in your test file or `test_helper.rb`:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require "ratatui_ruby/test_helper"
|
|
27
|
+
require "minitest/autorun" # or your preferred test framework
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then, include the module in your test class:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
class MyApplicationTest < Minitest::Test
|
|
34
|
+
include RatatuiRuby::TestHelper
|
|
35
|
+
# ...
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Basic Usage
|
|
40
|
+
|
|
41
|
+
### `with_test_terminal`
|
|
42
|
+
|
|
43
|
+
Wrap your test assertions in `with_test_terminal`. This sets up a temporary, in-memory backend for Ratatui to draw to, instead of the real terminal. It automatically cleans up afterwards.
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
def test_rendering
|
|
47
|
+
# Create a 80x24 terminal
|
|
48
|
+
with_test_terminal(80, 24) do
|
|
49
|
+
# 1. Instantiate your app/component
|
|
50
|
+
widget = RatatuiRuby::Paragraph.new(text: "Hello World")
|
|
51
|
+
|
|
52
|
+
# 2. Render it
|
|
53
|
+
RatatuiRuby.draw(widget)
|
|
54
|
+
|
|
55
|
+
# 3. Assert on the output
|
|
56
|
+
assert_includes buffer_content[0], "Hello World"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `buffer_content`
|
|
62
|
+
|
|
63
|
+
Returns the current state of the terminal as an Array of Strings. Useful for verifying that specific text appears where you expect it.
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
rows = buffer_content
|
|
67
|
+
assert_equal "Title", rows[0].strip
|
|
68
|
+
assert_match /Results: \d+/, rows[2]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `cursor_position`
|
|
72
|
+
|
|
73
|
+
Returns the current cursor coordinates as `{ x: Integer, y: Integer }`. Useful for forms or ensuring focus is correct.
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
pos = cursor_position
|
|
77
|
+
assert_equal 5, pos[:x]
|
|
78
|
+
assert_equal 2, pos[:y]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `inject_event`
|
|
82
|
+
|
|
83
|
+
Injects a mock event into the event queue. This is the preferred way to simulate user input instead of stubbing `poll_event`.
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# Simulate 'q' key press
|
|
87
|
+
inject_event("key", { code: "q" })
|
|
88
|
+
|
|
89
|
+
# Now poll_event will return the 'q' key event
|
|
90
|
+
event = RatatuiRuby.poll_event
|
|
91
|
+
assert_equal "q", event[:code]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Example
|
|
95
|
+
|
|
96
|
+
Be sure to check out the [examples directory](../examples/) in the repository, which contains several fully tested example applications showcasing these patterns.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Ruby Frontend Design (`ratatui_ruby`)
|
|
7
|
+
|
|
8
|
+
This document describes the design philosophy and structure of the Ruby layer in `ratatui_ruby`.
|
|
9
|
+
|
|
10
|
+
## Core Philosophy: Data-Driven UI
|
|
11
|
+
|
|
12
|
+
The Ruby frontend is designed as a **thin, declarative layer** over the Rust backend. It uses an **Immediate Mode** paradigm where the user constructs a tree of pure data objects every frame to represent the desired UI state.
|
|
13
|
+
|
|
14
|
+
### 1. View Tree as Data
|
|
15
|
+
|
|
16
|
+
Unlike traditional OO GUI toolkits (like Qt or Swing) where widgets are retained objects with internal state, `ratatui_ruby` widgets are immutable value objects.
|
|
17
|
+
|
|
18
|
+
* Implemented using Ruby 3.2+ `Data` classes.
|
|
19
|
+
* Located in `lib/ratatui_ruby/schema/`.
|
|
20
|
+
* These objects act as a Schema or Interface Definition Language (IDL) between Ruby and Rust.
|
|
21
|
+
|
|
22
|
+
**Example:**
|
|
23
|
+
```ruby
|
|
24
|
+
# This is just a piece of data, not a "live" widget
|
|
25
|
+
paragraph = RatatuiRuby::Paragraph.new(
|
|
26
|
+
text: "Hello World",
|
|
27
|
+
style: RatatuiRuby::Style.new(fg: :red),
|
|
28
|
+
block: nil
|
|
29
|
+
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. Immediate Mode Rendering
|
|
33
|
+
|
|
34
|
+
The application loop typically looks like this:
|
|
35
|
+
|
|
36
|
+
1. **Poll Event**: Ruby asks Rust for the next event.
|
|
37
|
+
2. **Update State**: Ruby application code updates its own domain state (e.g., `counter += 1`).
|
|
38
|
+
3. **Render**: Ruby constructs a fresh View Tree based on the current domain state and passes the root node to `RatatuiRuby.draw`.
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
loop do
|
|
42
|
+
# 1. & 2. Handle events and update state
|
|
43
|
+
event = RatatuiRuby.poll_event
|
|
44
|
+
break if event[:type] == :key && event[:code] == "esc"
|
|
45
|
+
|
|
46
|
+
# 3. Construct View Tree
|
|
47
|
+
ui = RatatuiRuby::Paragraph.new(text: "Time: #{Time.now}")
|
|
48
|
+
|
|
49
|
+
# 4. Draw
|
|
50
|
+
RatatuiRuby.draw(ui)
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. No render logic in Ruby
|
|
55
|
+
|
|
56
|
+
The Ruby classes in `lib/ratatui_ruby/schema/` should **not** contain rendering logic. They are strictly for structural definition and validation. All rendering logic resides in the Rust extension (`ext/ratatui_ruby/`), which walks this Ruby object tree and produces Ratatui primitives.
|
|
57
|
+
|
|
58
|
+
## Adding a New Widget
|
|
59
|
+
|
|
60
|
+
To add a new widget to the Ruby frontend:
|
|
61
|
+
|
|
62
|
+
1. Define the class in `lib/ratatui_ruby/schema/`.
|
|
63
|
+
2. Use `Data.define`.
|
|
64
|
+
3. Ensure attribute names match what the Rust rendering logic expects (see `ext/ratatui_ruby/src/widgets/`).
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# lib/ratatui_ruby/schema/my_widget.rb
|
|
68
|
+
module RatatuiRuby
|
|
69
|
+
# A widget that does something specific.
|
|
70
|
+
#
|
|
71
|
+
# [some_property] The description of the property.
|
|
72
|
+
# [style] The style to apply.
|
|
73
|
+
# [block] Optional block widget.
|
|
74
|
+
class MyWidget < Data.define(:some_property, :style, :block)
|
|
75
|
+
# Creates a new MyWidget.
|
|
76
|
+
#
|
|
77
|
+
# [some_property] The description of the property.
|
|
78
|
+
# [style] The style to apply.
|
|
79
|
+
# [block] Optional block widget.
|
|
80
|
+
def initialize(some_property:, style: nil, block: nil)
|
|
81
|
+
super
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
And define the types in the corresponding `.rbs` file:
|
|
88
|
+
|
|
89
|
+
```rbs
|
|
90
|
+
# sig/ratatui_ruby/schema/my_widget.rbs
|
|
91
|
+
module RatatuiRuby
|
|
92
|
+
class MyWidget < Data
|
|
93
|
+
attr_reader some_property: String
|
|
94
|
+
attr_reader style: Style?
|
|
95
|
+
attr_reader block: Block?
|
|
96
|
+
|
|
97
|
+
def self.new: (some_property: String, ?style: Style?, ?block: Block?) -> MyWidget
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Rust Backend Design (`ratatui_ruby` extension)
|
|
7
|
+
|
|
8
|
+
This document describes the internal architecture of the `ratatui_ruby` Rust extension.
|
|
9
|
+
|
|
10
|
+
## Architecture Guidelines
|
|
11
|
+
|
|
12
|
+
The project follows a **Structured Design** approach, separating concerns into modules to improve cohesiveness and testability.
|
|
13
|
+
|
|
14
|
+
### Core Principles
|
|
15
|
+
|
|
16
|
+
1. **Single Generic Renderer**: The backend implements a single generic renderer that accepts a Ruby `Value` representing the root of the view tree.
|
|
17
|
+
2. **No Custom Rust Structs for UI**: Do not define custom Rust structs that mirror Ruby UI components. Instead, extract data directly from Ruby objects using `funcall`.
|
|
18
|
+
3. **Dynamic Dispatch**: Use `value.class().name()` (e.g., `"RatatuiRuby::Paragraph"`) to dynamically dispatch rendering logic to the appropriate widget module.
|
|
19
|
+
4. **Immediate Mode**: The renderer traverses the Ruby object tree every frame and rebuilds the Ratatui widget tree on the fly.
|
|
20
|
+
|
|
21
|
+
### Module Structure
|
|
22
|
+
|
|
23
|
+
The Rust extension is located in `ext/ratatui_ruby/src/` and is organized as follows:
|
|
24
|
+
|
|
25
|
+
* **`lib.rs`**: The entry point for the compiled extension. It defines the Ruby module structure using `magnus` and exports public functions (`init_terminal`, `draw`, `poll_event`). It wires together the submodules.
|
|
26
|
+
* **`terminal.rs`**: Encapsulates the global `TERMINAL` state (mutex-wrapped `CrosstermBackend`). It provides functions to initialize and restore the terminal to raw mode.
|
|
27
|
+
* **`events.rs`**: Handles keyboard input polling and mapping Crossterm events to Ruby hashes.
|
|
28
|
+
* **`style.rs`**: Provides pure functions for parsing styling information (Colors, Styles, Blocks) from Ruby values.
|
|
29
|
+
* **`rendering.rs`**: The central dispatcher for the render loop. It takes the top-level Ruby View Tree node and recursively delegates to specific widget implementations based on the Ruby class name.
|
|
30
|
+
* **`widgets/`**: A directory containing individual modules for each Ratatui widget (e.g., `paragraph.rs`, `list.rs`).
|
|
31
|
+
|
|
32
|
+
### Adding a New Widget
|
|
33
|
+
|
|
34
|
+
To add a new widget:
|
|
35
|
+
|
|
36
|
+
1. Create a new file `src/widgets/my_widget.rs`.
|
|
37
|
+
2. Implement a public `render` function:
|
|
38
|
+
```rust
|
|
39
|
+
/// Renders the widget to the given area.
|
|
40
|
+
///
|
|
41
|
+
/// # Arguments
|
|
42
|
+
///
|
|
43
|
+
/// * `frame` - The Ratatui frame to render to.
|
|
44
|
+
/// * `area` - The rectangular area within the frame to draw the widget.
|
|
45
|
+
/// * `node` - The Ruby object (Value) containing the widget's properties.
|
|
46
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error>
|
|
47
|
+
```
|
|
48
|
+
3. Inside `render`:
|
|
49
|
+
* Extract properties from the `node` (Ruby value) using `.funcall("method_name", ())?`.
|
|
50
|
+
* Construct the Ratatui widget.
|
|
51
|
+
* Render it using `frame.render_widget`.
|
|
52
|
+
4. Register the module in `src/widgets/mod.rs`.
|
|
53
|
+
5. Add a dispatch arm in `src/rendering.rs` matching the Ruby class name (e.g., `RatatuiRuby::MyWidget`).
|
|
54
|
+
|
|
55
|
+
### Testing Strategy
|
|
56
|
+
|
|
57
|
+
* **Unit Tests (`cargo test`)**:
|
|
58
|
+
* **Logic**: Test pure logic like `parse_color` in `style.rs` without needing a terminal or Ruby VM if possible (though `magnus::Value` usually requires it).
|
|
59
|
+
* **Rendering**: Verify that widgets render *something* to a buffer. Ratatui's `TestBackend` or `Buffer` can be used to assert that cells are filled.
|
|
60
|
+
* **Integration Tests (`rake test`)**:
|
|
61
|
+
* Run Ruby scripts that exercise the full stack. Verify no crashes and expected return values.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Design Documentation
|
|
7
|
+
|
|
8
|
+
This directory contains detailed design documents for the `ratatui_ruby` project.
|
|
9
|
+
|
|
10
|
+
* [Rust Backend Design](design/rust_backend.md): Details on the internal architecture of the Rust extension (`ext/ratatui_ruby`), including module structure, rendering pipeline, and widget implementation guide.
|
|
11
|
+
* [Ruby Frontend Design](design/ruby_frontend.md): Explains the Data-Driven UI, Immediate Mode paradigm, and the View Tree structure.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
# **ratatui_ruby** Contributors’ Documentation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Documentation for Contributors
|
|
9
|
+
|
|
10
|
+
- [Contributing Guidelines](../../CONTRIBUTING.md)
|
|
11
|
+
- [The Design of **ratatui_ruby**](./design.md)
|
|
12
|
+
|
|
13
|
+
## Documentation for Users
|
|
14
|
+
|
|
15
|
+
- [README](../../README.md)
|
|
16
|
+
- [More Documentation for Users](../index.md)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/docs/index.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
# **ratatui_ruby** Documentation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Documentation for Users
|
|
9
|
+
|
|
10
|
+
- [README](../README.md)
|
|
11
|
+
- [Quickstart](./quickstart.md)
|
|
12
|
+
- [Testing Your Application](./application_testing.md)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Documentation for Contributors
|
|
16
|
+
|
|
17
|
+
- [Contributing Guidelines](../CONTRIBUTING.md)
|
|
18
|
+
- [More Documentation for Contributors](./contributors/index.md)
|
data/docs/quickstart.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
# Quickstart
|
|
6
|
+
|
|
7
|
+
Welcome to **ratatui_ruby**! This guide will help you get up and running with your first Terminal User Interface in Ruby.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add this line to your application's Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem 'ratatui_ruby'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
And then execute:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or install it yourself as:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gem install ratatui_ruby
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Basic Application
|
|
30
|
+
|
|
31
|
+
Here is a "Hello World" application that demonstrates the core lifecycle of a **ratatui_ruby** app.
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require "ratatui_ruby"
|
|
35
|
+
|
|
36
|
+
# 1. Initialize the terminal
|
|
37
|
+
RatatuiRuby.init_terminal
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
# The Main Loop
|
|
41
|
+
loop do
|
|
42
|
+
# 2. Create your UI (Immediate Mode)
|
|
43
|
+
# We define a Paragraph widget inside a Block with a title and borders.
|
|
44
|
+
view = RatatuiRuby::Paragraph.new(
|
|
45
|
+
text: "Hello, Ratatui! Press 'q' to quit.",
|
|
46
|
+
block: RatatuiRuby::Block.new(
|
|
47
|
+
title: "My First App",
|
|
48
|
+
borders: [:all],
|
|
49
|
+
border_style: "cyan"
|
|
50
|
+
),
|
|
51
|
+
alignment: :center
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 3. Draw the UI
|
|
55
|
+
RatatuiRuby.draw(view)
|
|
56
|
+
|
|
57
|
+
# 4. Poll for events
|
|
58
|
+
event = RatatuiRuby.poll_event
|
|
59
|
+
if event && event[:type] == :key && event[:code] == "q"
|
|
60
|
+
break
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
ensure
|
|
64
|
+
# 5. Restore the terminal to its original state
|
|
65
|
+
RatatuiRuby.restore_terminal
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### How it works
|
|
70
|
+
|
|
71
|
+
1. **`RatatuiRuby.init_terminal`**: Enters raw mode and switches to the alternate screen.
|
|
72
|
+
2. **Immediate Mode UI**: On every iteration of the loop, you describe what the UI should look like by creating `Data` objects (like `Paragraph` and `Block`).
|
|
73
|
+
3. **`RatatuiRuby.draw(view)`**: The Ruby UI tree is passed to the Rust backend, which renders it to the terminal.
|
|
74
|
+
4. **`RatatuiRuby.poll_event`**: Checks for keyboard, mouse, or resize events.
|
|
75
|
+
5. **`RatatuiRuby.restore_terminal`**: Crucial for leaving raw mode and returning the user to their shell properly. Always wrap your loop in a `begin...ensure` block to guarantee this runs.
|
|
76
|
+
6. **`sleep 0.05`**: In a real app, you'd want to control your frame rate to avoid consuming 100% CPU.
|
|
77
|
+
|
|
78
|
+
## Examples
|
|
79
|
+
|
|
80
|
+
To see more complex layouts and widget usage, check out the `examples/` directory in the repository.
|
|
81
|
+
|
|
82
|
+
### [Analytics](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/analytics.rb)
|
|
83
|
+
Demonstrates the use of `Tabs` and `BarChart` widgets with a simple data-switching mechanism.
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
### [Box Demo](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/box_demo.rb)
|
|
88
|
+
A simple demonstration of `Block` and `Paragraph` widgets, reacting to arrow key presses to change colors.
|
|
89
|
+
|
|
90
|
+

|
|
91
|
+
|
|
92
|
+
### [Dashboard](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/dashboard.rb)
|
|
93
|
+
Uses `Layout`, `List`, and `Paragraph` to create a classic sidebar-and-content interface.
|
|
94
|
+
|
|
95
|
+

|
|
96
|
+
|
|
97
|
+
### [Login Form](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/login_form.rb)
|
|
98
|
+
Shows how to use `Overlay`, `Center`, and `Cursor` to build a modal login form with text input.
|
|
99
|
+
|
|
100
|
+

|
|
101
|
+
|
|
102
|
+
### [Map Demo](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/map_demo.rb)
|
|
103
|
+
Exhibits the `Canvas` widget's power, rendering a world map along with animated circles and lines.
|
|
104
|
+
|
|
105
|
+

|
|
106
|
+
|
|
107
|
+
### [Mouse Events](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/mouse_events.rb)
|
|
108
|
+
Detailed plumbing of mouse events, including clicks, drags, and movement tracking.
|
|
109
|
+
|
|
110
|
+

|
|
111
|
+
|
|
112
|
+
### [Scrollbar Demo](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/scrollbar_demo.rb)
|
|
113
|
+
A simple example of integrating the `Scrollbar` widget and handling mouse wheel events for scrolling.
|
|
114
|
+
|
|
115
|
+

|
|
116
|
+
|
|
117
|
+
### [Stock Ticker](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/stock_ticker.rb)
|
|
118
|
+
Utilizes `Sparkline` and `LineChart` widgets to visualize real-time (simulated) data.
|
|
119
|
+
|
|
120
|
+

|
|
121
|
+
|
|
122
|
+
### [System Monitor](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/system_monitor.rb)
|
|
123
|
+
Combines `Table` and `Gauge` widgets in a vertical layout to create a functional system overview.
|
|
124
|
+
|
|
125
|
+

|
|
126
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
require_relative "../lib/ratatui_ruby"
|
|
7
|
+
|
|
8
|
+
# Analytics Dashboard Example
|
|
9
|
+
# Demonstrates Tabs and BarChart widgets.
|
|
10
|
+
|
|
11
|
+
class AnalyticsApp
|
|
12
|
+
def initialize
|
|
13
|
+
@selected_tab = 0
|
|
14
|
+
@tabs = ["Revenue", "Traffic", "Errors"]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
RatatuiRuby.init_terminal
|
|
19
|
+
begin
|
|
20
|
+
loop do
|
|
21
|
+
render
|
|
22
|
+
break if handle_input == :quit
|
|
23
|
+
sleep 0.05
|
|
24
|
+
end
|
|
25
|
+
ensure
|
|
26
|
+
RatatuiRuby.restore_terminal
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def render
|
|
31
|
+
# Data for different tabs
|
|
32
|
+
data = case @selected_tab
|
|
33
|
+
when 0 # Revenue
|
|
34
|
+
{ "Q1" => 50, "Q2" => 80 }
|
|
35
|
+
when 1 # Traffic
|
|
36
|
+
{ "Mon" => 120, "Tue" => 150 }
|
|
37
|
+
when 2 # Errors
|
|
38
|
+
{ "DB" => 5, "UI" => 2 }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
style = case @selected_tab
|
|
42
|
+
when 0 then RatatuiRuby::Style.new(fg: "green")
|
|
43
|
+
when 1 then RatatuiRuby::Style.new(fg: "blue")
|
|
44
|
+
when 2 then RatatuiRuby::Style.new(fg: "red")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Build the UI
|
|
48
|
+
ui = RatatuiRuby::Layout.new(
|
|
49
|
+
direction: :vertical,
|
|
50
|
+
constraints: [
|
|
51
|
+
RatatuiRuby::Constraint.new(type: :length, value: 3),
|
|
52
|
+
RatatuiRuby::Constraint.new(type: :min, value: 0),
|
|
53
|
+
],
|
|
54
|
+
children: [
|
|
55
|
+
RatatuiRuby::Tabs.new(
|
|
56
|
+
titles: @tabs,
|
|
57
|
+
selected_index: @selected_tab,
|
|
58
|
+
block: RatatuiRuby::Block.new(title: "Views", borders: [:all])
|
|
59
|
+
),
|
|
60
|
+
RatatuiRuby::BarChart.new(
|
|
61
|
+
data:,
|
|
62
|
+
bar_width: 10,
|
|
63
|
+
style:,
|
|
64
|
+
block: RatatuiRuby::Block.new(title: "Analytics: #{@tabs[@selected_tab]}", borders: [:all])
|
|
65
|
+
),
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
RatatuiRuby.draw(ui)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def handle_input
|
|
73
|
+
event = RatatuiRuby.poll_event
|
|
74
|
+
if event
|
|
75
|
+
case event[:code]
|
|
76
|
+
when "q"
|
|
77
|
+
:quit
|
|
78
|
+
when "right"
|
|
79
|
+
@selected_tab = (@selected_tab + 1) % @tabs.size
|
|
80
|
+
when "left"
|
|
81
|
+
@selected_tab = (@selected_tab - 1) % @tabs.size
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
AnalyticsApp.new.run if __FILE__ == $0
|