ratatui_ruby 0.10.3 → 1.0.0.pre.beta.2
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 +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/.rubocop.yml +3 -1
- data/CHANGELOG.md +17 -0
- data/README.md +2 -2
- data/README.rdoc +302 -0
- data/REUSE.toml +5 -0
- data/examples/app_all_events/view/live_view.rb +1 -1
- data/examples/verify_website_menu/app.rb +73 -46
- data/examples/verify_website_spinner/app.rb +5 -4
- data/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/ext/ratatui_ruby/src/widgets/table_state.rs +27 -0
- data/lib/ratatui_ruby/table_state.rb +100 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- data/tasks/bump/sem_ver.rb +8 -4
- data/tasks/lint.rake +3 -1
- data/tasks/sourcehut.rake +26 -1
- metadata +305 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69c2adadc9306ebeb02ffefa11e56abe7db2f0ef1c361a2edaf1cf5a3f9b9fee
|
|
4
|
+
data.tar.gz: c8e07b3361a46597f957fa6a4ade8e9eef426fa85a0df8650f7188a306384d5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eb439fe49d032bf2d7221b4771b2b6b0156d9b5373d32fe6710ff0077046263fc5e719c1d91d1ed271b3cb58c18cf9f31ef6d366daed59b6f4413ac66db82447
|
|
7
|
+
data.tar.gz: dc395bd562a70d0bc8a9d6eafa4ca0b488ef3ce450a3f3d65b125550ed616c1516b2b5301bb0b634028433f90d4306885ba8f756542241fb9d5e0c4fa25d6644
|
data/.builds/ruby-3.2.yml
CHANGED
data/.builds/ruby-3.3.yml
CHANGED
data/.builds/ruby-3.4.yml
CHANGED
data/.builds/ruby-4.0.0.yml
CHANGED
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,21 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
18
18
|
|
|
19
19
|
### Removed
|
|
20
20
|
|
|
21
|
+
## [1.0.0-beta.2] - 2026-01-20
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **TableState Row Navigation Methods**: Added missing row navigation methods (`select_next`, `select_previous`, `select_first`, `select_last`) that should have been included alongside the column navigation methods added in v0.10.0. These methods match `ListState`'s navigation API and are used in the `app_stateful_interaction` example.
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
|
|
33
|
+
## [1.0.0-beta.1] - 2026-01-20
|
|
34
|
+
This release is functionally equivalent to v0.10.3. The version bump signals the beginning of the 1.0 release series.
|
|
35
|
+
|
|
21
36
|
## [0.10.3] - 2026-01-16
|
|
22
37
|
|
|
23
38
|
### Added
|
|
@@ -667,6 +682,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
667
682
|
- **Testing Support**: Included `RatatuiRuby::TestHelper` and RSpec integration to make testing your TUI applications possible.
|
|
668
683
|
|
|
669
684
|
[Unreleased]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/HEAD
|
|
685
|
+
[1.0.0-beta.2]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v1.0.0-beta.2
|
|
686
|
+
[1.0.0-beta.1]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v1.0.0-beta.1
|
|
670
687
|
[0.10.3]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.10.3
|
|
671
688
|
[0.10.2]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.10.2
|
|
672
689
|
[0.10.1]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.10.1
|
data/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Mailing List: Announcements](https://img.shields.io/badge/mailing_list-announcem
|
|
|
21
21
|
**ratatui_ruby** is a community wrapper that is not affiliated with [the Ratatui team](https://github.com/orgs/ratatui/people).
|
|
22
22
|
|
|
23
23
|
> [!WARNING]
|
|
24
|
-
> **ratatui_ruby** is currently in **
|
|
24
|
+
> **ratatui_ruby** is currently in **BETA**. Please report any bugs you find!
|
|
25
25
|
|
|
26
26
|
**[Why RatatuiRuby?](./doc/getting_started/why.md)** — Native Rust performance, zero runtime overhead, and Ruby's expressiveness. [See how we compare](./doc/getting_started/why.md) to CharmRuby, raw Rust, and Go.
|
|
27
27
|
|
|
@@ -91,7 +91,7 @@ Or install it yourself with:
|
|
|
91
91
|
SPDX-License-Identifier: MIT-0
|
|
92
92
|
-->
|
|
93
93
|
```bash
|
|
94
|
-
gem install ratatui_ruby
|
|
94
|
+
gem install ratatui_ruby --pre
|
|
95
95
|
```
|
|
96
96
|
<!-- SPDX-SnippetEnd -->
|
|
97
97
|
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
|
|
2
|
+
== Terminal UIs, the Ruby Way
|
|
3
|
+
|
|
4
|
+
RatatuiRuby[https://rubygems.org/gems/ratatui_ruby] is a RubyGem built on
|
|
5
|
+
Ratatui[https://ratatui.rs], a leading TUI library written in
|
|
6
|
+
Rust[https://rust-lang.org]. You get native performance with the joy of Ruby.
|
|
7
|
+
|
|
8
|
+
gem install ratatui_ruby --pre
|
|
9
|
+
|
|
10
|
+
{rdoc-image:https://ratatui-ruby.dev/hero.gif}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_cli_rich_moments/README_md.html]
|
|
11
|
+
|
|
12
|
+
=== Rich Moments
|
|
13
|
+
|
|
14
|
+
Add a spinner, a progress bar, or an inline menu to your CLI script. No
|
|
15
|
+
full-screen takeover. Your terminal history stays intact.
|
|
16
|
+
|
|
17
|
+
==== Inline Viewports
|
|
18
|
+
|
|
19
|
+
Standard TUIs erase themselves on exit. Your carefully formatted CLI output
|
|
20
|
+
disappears. Users lose their scrollback.
|
|
21
|
+
|
|
22
|
+
<b>Inline viewports</b> solve this. They occupy a fixed number of lines, render
|
|
23
|
+
rich UI, then leave the output in place when done.
|
|
24
|
+
|
|
25
|
+
Perfect for spinners, menus, progress indicators—any brief moment of richness.
|
|
26
|
+
|
|
27
|
+
require "ratatui_ruby"
|
|
28
|
+
|
|
29
|
+
RatatuiRuby.run(viewport: :inline, height: 1) do |tui|
|
|
30
|
+
until connected?
|
|
31
|
+
status = tui.paragraph(text: "\#{spin} Connecting...")
|
|
32
|
+
tui.draw { |frame| frame.render_widget(status, frame.area) }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
=== Build Something Real
|
|
37
|
+
|
|
38
|
+
Full-screen applications with {keyboard and mouse input}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_all_events/README_md.html]. The managed loop
|
|
39
|
+
sets up the terminal and restores it on exit, even after crashes.
|
|
40
|
+
|
|
41
|
+
RatatuiRuby.run do |tui|
|
|
42
|
+
loop do
|
|
43
|
+
tui.draw do |frame|
|
|
44
|
+
frame.render_widget(
|
|
45
|
+
tui.paragraph(text: "Hello, RatatuiRuby!", alignment: :center),
|
|
46
|
+
frame.area
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
case tui.poll_event
|
|
51
|
+
in { type: :key, code: "q" } then break
|
|
52
|
+
else nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
==== Widgets included:
|
|
58
|
+
|
|
59
|
+
[Layout]
|
|
60
|
+
{Block}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_block/README_md.html],
|
|
61
|
+
{Center}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_center/README_md.html],
|
|
62
|
+
{Clear (Popup, Modal)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_popup/README_md.html],
|
|
63
|
+
{Layout (Split, Grid)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_layout_split/README_md.html],
|
|
64
|
+
{Overlay}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_overlay/README_md.html]
|
|
65
|
+
[Data]
|
|
66
|
+
{Bar Chart}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_barchart/README_md.html],
|
|
67
|
+
{Chart}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_chart/README_md.html],
|
|
68
|
+
{Gauge}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_gauge/README_md.html],
|
|
69
|
+
{Line Gauge}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_line_gauge/README_md.html],
|
|
70
|
+
{Sparkline}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_sparkline/README_md.html],
|
|
71
|
+
{Table}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_table/README_md.html]
|
|
72
|
+
[Text]
|
|
73
|
+
{Cell}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_cell/README_md.html],
|
|
74
|
+
{List}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_list/README_md.html],
|
|
75
|
+
{Rich Text (Line, Span)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_rich_text/README_md.html],
|
|
76
|
+
{Scrollbar (Scroll)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_scrollbar/README_md.html],
|
|
77
|
+
{Tabs}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_tabs/README_md.html]
|
|
78
|
+
[Graphics]
|
|
79
|
+
{Calendar}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_calendar/README_md.html],
|
|
80
|
+
{Canvas}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_canvas/README_md.html],
|
|
81
|
+
{Map (World Map)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_map/README_md.html]
|
|
82
|
+
|
|
83
|
+
Need something else? {Build custom widgets}[https://www.ratatui-ruby.dev/docs/v0.10/doc/concepts/custom_widgets_md.html] in Ruby!
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
=== Testing Built In
|
|
89
|
+
|
|
90
|
+
TUI testing is tedious. You need a headless terminal, event injection,
|
|
91
|
+
snapshot comparisons, and style assertions. RatatuiRuby bundles all of it.
|
|
92
|
+
|
|
93
|
+
require "ratatui_ruby/test_helper"
|
|
94
|
+
|
|
95
|
+
class TestColorPicker < Minitest::Test
|
|
96
|
+
include RatatuiRuby::TestHelper
|
|
97
|
+
|
|
98
|
+
def test_swatch_widget
|
|
99
|
+
with_test_terminal(10, 3) do
|
|
100
|
+
RatatuiRuby.draw do |frame|
|
|
101
|
+
frame.render_widget(Swatch.new(:red), frame.area)
|
|
102
|
+
end
|
|
103
|
+
assert_cell_style 2, 1, char: "█", bg: :red
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
==== What's inside:
|
|
109
|
+
|
|
110
|
+
- <b>Headless terminal</b> — No real TTY needed
|
|
111
|
+
- <b>Snapshots</b> — Plain text and rich (ANSI colors)
|
|
112
|
+
- <b>Event injection</b> — Keys, mouse, paste, resize
|
|
113
|
+
- <b>Style assertions</b> — Color, bold, underline at any cell
|
|
114
|
+
- <b>Test doubles</b> — Mock frames and stub rects
|
|
115
|
+
- <b>UPDATE_SNAPSHOTS=1</b> — Regenerate baselines in one command
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
==== Inline Menu Example
|
|
121
|
+
|
|
122
|
+
require "ratatui_ruby"
|
|
123
|
+
|
|
124
|
+
# This example renders an inline menu. Arrow keys select, enter confirms.
|
|
125
|
+
# The menu appears in-place, preserving scrollback. When the user chooses,
|
|
126
|
+
# the TUI closes and the script continues with the selected value.
|
|
127
|
+
class RadioMenu
|
|
128
|
+
CHOICES = ["Production", "Staging", "Development"] # ASCII strings are universally supported.
|
|
129
|
+
PREFIXES = { active: "●", inactive: "○" } # Some terminals may not support Unicode.
|
|
130
|
+
CONTROLS = "↑/↓: Select | Enter: Choose | Ctrl+C: Cancel" # Let users know what keys you handle.
|
|
131
|
+
TITLES = ["Select Environment", # The default title position is top left.
|
|
132
|
+
{ content: CONTROLS, # Multiple titles can save space.
|
|
133
|
+
position: :bottom, # Titles go on the top or bottom,
|
|
134
|
+
alignment: :right }] # aligned left, right, or center
|
|
135
|
+
|
|
136
|
+
def call # This method blocks until a choice is made.
|
|
137
|
+
RatatuiRuby.run(viewport: :inline, height: 5) do |tui| # RatauiRuby.run manages the terminal.
|
|
138
|
+
@tui = tui # The TUI instance is safe to store.
|
|
139
|
+
show_menu until chosen? # You can use any loop keyword you like.
|
|
140
|
+
end # `run` won't return until your block does,
|
|
141
|
+
RadioMenu::CHOICES[@choice] # so you can use it synchronously.
|
|
142
|
+
end
|
|
143
|
+
# Classes like RadioMenu are convenient for
|
|
144
|
+
private # CLI authors to offer "rich moments."
|
|
145
|
+
|
|
146
|
+
def show_menu = @tui.draw do |frame| # RatatuiRuby gives you low-level access.
|
|
147
|
+
widget = @tui.paragraph( # But the TUI facade makes it easy to use.
|
|
148
|
+
text: menu_items, # Text can be spans, lines, or paragraphs.
|
|
149
|
+
block: @tui.block(borders: :all, titles: TITLES) # Blocks give you boxes and titles, and hold
|
|
150
|
+
) # one or more widgets. We only use one here,
|
|
151
|
+
frame.render_widget(widget, frame.area) # but "area" lets you compose sub-views.
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def chosen? # You are responsible for handling input.
|
|
155
|
+
interaction = @tui.poll_event # Every frame, you receive an event object:
|
|
156
|
+
return choose if interaction.enter? # Key, Mouse, Resize, Paste, FocusGained,
|
|
157
|
+
# FocusLost, or None objects. They come with
|
|
158
|
+
move_by(-1) if interaction.up? # predicates, support pattern matching, and
|
|
159
|
+
move_by(1) if interaction.down? # can be inspected for properties directly.
|
|
160
|
+
quit! if interaction.ctrl_c? # Your application must handle every input,
|
|
161
|
+
false # even interrupts and other exit patterns.
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def choose # Here, the loop is about to exit, and the
|
|
165
|
+
prepare_next_line # block will return. The inline viewport
|
|
166
|
+
@choice # will be torn down and the terminal will
|
|
167
|
+
end # be restored, but you are responsible for
|
|
168
|
+
# positioning the cursor.
|
|
169
|
+
def prepare_next_line # To ensure the next output is on a new
|
|
170
|
+
area = @tui.viewport_area # line, query the viewport area and move
|
|
171
|
+
RatatuiRuby.cursor_position = [0, area.y + area.height] # the cursor to the start of the last line.
|
|
172
|
+
puts # Then print a newline.
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def quit! # All of your familiar Ruby control flow
|
|
176
|
+
prepare_next_line # keywords work as expected, so we can
|
|
177
|
+
exit 0 # use them to leave the TUI.
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def move_by(line_count) # You are in full control of your UX, so
|
|
181
|
+
@choice = (@choice + line_count) % CHOICES.size # you can implement any logic you need:
|
|
182
|
+
end # Would you "wrap around" here, or not?
|
|
183
|
+
#
|
|
184
|
+
def menu_items = CHOICES.map.with_index do |choice, i| # Notably, RatatuiRuby has no concept of
|
|
185
|
+
"\#{prefix_for(i)} \#{choice}" # "menus" or "radio buttons". You are in
|
|
186
|
+
end # full control, but it also means you must
|
|
187
|
+
def prefix_for(choice_index) # implement the logic yourself. For larger
|
|
188
|
+
return PREFIXES[:active] if choice_index == @choice # applications, consider using Rooibos,
|
|
189
|
+
PREFIXES[:inactive] # an MVU framework built with RatatuiRuby.
|
|
190
|
+
end # Or, use the upcoming ratatui-ruby-kit,
|
|
191
|
+
# our object-oriented component library.
|
|
192
|
+
def initialize = @choice = 0 # However, those are both optional, and
|
|
193
|
+
end # designed for full-screen Terminal UIs.
|
|
194
|
+
# RatatuiRuby will always give you the most
|
|
195
|
+
choice = RadioMenu.new.call # control, and is enough for "rich CLI
|
|
196
|
+
puts "You chose \#{choice}!" # moments" like this one.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
=== Full App Solutions
|
|
201
|
+
|
|
202
|
+
RatatuiRuby renders. For complex applications, add a framework that manages
|
|
203
|
+
state and composition.
|
|
204
|
+
|
|
205
|
+
==== Rooibos[https://git.sr.ht/~kerrick/rooibos] (Framework)
|
|
206
|
+
|
|
207
|
+
Model-View-Update architecture. Inspired by Elm, Bubble Tea, and React +
|
|
208
|
+
Redux. Your UI is a pure function of state.
|
|
209
|
+
|
|
210
|
+
- Functional programming with MVU
|
|
211
|
+
- Commands work off the main thread
|
|
212
|
+
- Messages, not callbacks, drive updates
|
|
213
|
+
|
|
214
|
+
==== {Kit}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-3-the-object-path--kit] (Coming Soon)
|
|
215
|
+
|
|
216
|
+
Component-based architecture. Encapsulate state, input handling, and
|
|
217
|
+
rendering in reusable pieces.
|
|
218
|
+
|
|
219
|
+
- OOP with stateful components
|
|
220
|
+
- Separate UI state from domain logic
|
|
221
|
+
- Built-in focus management & click handling
|
|
222
|
+
|
|
223
|
+
Both use the same widget library and rendering engine. Pick the paradigm
|
|
224
|
+
that fits your brain.
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
=== Why RatatuiRuby?
|
|
230
|
+
|
|
231
|
+
Ruby deserves world-class terminal user interfaces. TUI developers deserve
|
|
232
|
+
a world-class language.
|
|
233
|
+
|
|
234
|
+
RatatuiRuby wraps Rust's Ratatui via native extension. The Rust library
|
|
235
|
+
handles rendering. Your Ruby code handles design.
|
|
236
|
+
|
|
237
|
+
>>>
|
|
238
|
+
"Text UIs are seeing a renaissance with many new TUI libraries popping up.
|
|
239
|
+
The Ratatui bindings have proven to be full featured and stable."
|
|
240
|
+
|
|
241
|
+
— {Mike Perham}[https://www.mikeperham.com/], creator of
|
|
242
|
+
Sidekiq[https://sidekiq.org/] and Faktory[https://contribsys.com/faktory/]
|
|
243
|
+
|
|
244
|
+
==== Why Rust? Why Ruby?
|
|
245
|
+
|
|
246
|
+
Rust excels at low-level rendering. Ruby excels at expressing domain logic
|
|
247
|
+
and UI. RatatuiRuby puts each language where it performs best.
|
|
248
|
+
|
|
249
|
+
==== Versus CharmRuby
|
|
250
|
+
|
|
251
|
+
CharmRuby[https://charm-ruby.dev/] wraps Charm's Go libraries. Both projects
|
|
252
|
+
give Ruby developers TUI options.
|
|
253
|
+
|
|
254
|
+
[Integration]
|
|
255
|
+
CharmRuby: Two runtimes, one process.
|
|
256
|
+
RatatuiRuby: Native extension in Rust.
|
|
257
|
+
[Runtime]
|
|
258
|
+
CharmRuby: Go + Ruby (competing).
|
|
259
|
+
RatatuiRuby: Ruby (Rust has no runtime).
|
|
260
|
+
[Memory]
|
|
261
|
+
CharmRuby: Two uncoordinated GCs.
|
|
262
|
+
RatatuiRuby: One Garbage Collector.
|
|
263
|
+
[Style]
|
|
264
|
+
CharmRuby: The Elm Architecture (TEA).
|
|
265
|
+
RatatuiRuby: TEA, OOP, or Imperative.
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
=== Links
|
|
271
|
+
|
|
272
|
+
[Get Started]
|
|
273
|
+
{Quickstart}[https://www.ratatui-ruby.dev/docs/v0.10/doc/getting_started/quickstart_md.html],
|
|
274
|
+
{Examples}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_cli_rich_moments/README_md.html],
|
|
275
|
+
{API Reference}[https://www.ratatui-ruby.dev/docs/v0.10/],
|
|
276
|
+
{Guides}[https://www.ratatui-ruby.dev/docs/v0.10/doc/index_md.html]
|
|
277
|
+
[Ecosystem]
|
|
278
|
+
Rooibos[https://git.sr.ht/~kerrick/rooibos],
|
|
279
|
+
{Kit}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-3-the-object-path--kit] (Planned),
|
|
280
|
+
{Framework}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-5-the-framework] (Planned),
|
|
281
|
+
{UI Widgets}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-6-licensing] (Planned)
|
|
282
|
+
[Community]
|
|
283
|
+
{Discuss and Chat}[https://lists.sr.ht/~kerrick/ratatui_ruby-discuss],
|
|
284
|
+
{Announcements}[https://lists.sr.ht/~kerrick/ratatui_ruby-announce],
|
|
285
|
+
{Development}[https://lists.sr.ht/~kerrick/ratatui_ruby-devel],
|
|
286
|
+
{Bug Tracker}[https://todo.sr.ht/~kerrick/ratatui_ruby]
|
|
287
|
+
[Contribute]
|
|
288
|
+
{Contributing Guide}[https://man.sr.ht/~kerrick/ratatui_ruby/contributing.md],
|
|
289
|
+
{Code of Conduct}[https://man.sr.ht/~kerrick/ratatui_ruby/code_of_conduct.md],
|
|
290
|
+
{Project History}[https://man.sr.ht/~kerrick/ratatui_ruby/history/index.md],
|
|
291
|
+
{Pull Requests}[https://lists.sr.ht/~kerrick/ratatui_ruby-devel/patches]
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
[Website] https://www.ratatui-ruby.dev
|
|
297
|
+
[Source] https://git.sr.ht/~kerrick/ratatui_ruby
|
|
298
|
+
[RubyGems] https://rubygems.org/gems/ratatui_ruby
|
|
299
|
+
[Upstream] https://ratatui.rs
|
|
300
|
+
[Build Status] https://builds.sr.ht/~kerrick/ratatui_ruby
|
|
301
|
+
|
|
302
|
+
© 2026 Kerrick Long · Library: LGPL-3.0-or-later · Website: CC-BY-NC-ND-4.0 · Snippets: MIT-0
|
data/REUSE.toml
CHANGED
|
@@ -5,6 +5,11 @@ path = 'Gemfile.lock'
|
|
|
5
5
|
SPDX-FileCopyrightText = "2025 Kerrick Long <me@kerricklong.com>"
|
|
6
6
|
SPDX-License-Identifier = "CC0-1.0"
|
|
7
7
|
|
|
8
|
+
[[annotations]]
|
|
9
|
+
path = 'README.rdoc'
|
|
10
|
+
SPDX-FileCopyrightText = "2025 Kerrick Long <me@kerricklong.com>"
|
|
11
|
+
SPDX-License-Identifier = "CC-BY-SA-4.0"
|
|
12
|
+
|
|
8
13
|
|
|
9
14
|
[[annotations]]
|
|
10
15
|
path = 'ext/ratatui_ruby/Cargo.lock'
|
|
@@ -49,7 +49,7 @@ class View::Live
|
|
|
49
49
|
desc_str = event_data ? event_data[:description] : "—"
|
|
50
50
|
|
|
51
51
|
is_lit = model.lit?(type)
|
|
52
|
-
row_style = is_lit ? tui.style(fg: :
|
|
52
|
+
row_style = is_lit ? tui.style(fg: :green, modifiers: [:reversed]) : nil
|
|
53
53
|
|
|
54
54
|
rows << tui.text_line(spans: [
|
|
55
55
|
tui.text_span(content: class_str.ljust(9), style: row_style || tui.style(fg: :cyan)),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# rubocop:disable all
|
|
2
3
|
|
|
3
4
|
#--
|
|
4
5
|
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
@@ -6,52 +7,78 @@
|
|
|
6
7
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
8
|
#++
|
|
8
9
|
|
|
9
|
-
# Test 2: Inline menu example (fixed)
|
|
10
10
|
require "ratatui_ruby"
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
12
|
+
# This example renders an inline menu. Arrow keys select, enter confirms.
|
|
13
|
+
# The menu appears in-place, preserving scrollback. When the user chooses,
|
|
14
|
+
# the TUI closes and the script continues with the selected value.
|
|
15
|
+
class RadioMenu
|
|
16
|
+
CHOICES = ["Production", "Staging", "Development"] # ASCII strings are universally supported.
|
|
17
|
+
PREFIXES = { active: "●", inactive: "○" } # Some terminals may not support Unicode.
|
|
18
|
+
CONTROLS = "↑/↓: Select | Enter: Choose | Ctrl+C: Cancel" # Let users know what keys you handle.
|
|
19
|
+
TITLES = ["Select Environment", # The default title position is top left.
|
|
20
|
+
{ content: CONTROLS, # Multiple titles can save space.
|
|
21
|
+
position: :bottom, # Titles go on the top or bottom,
|
|
22
|
+
alignment: :right }] # aligned left, right, or center
|
|
23
|
+
|
|
24
|
+
def call # This method blocks until a choice is made.
|
|
25
|
+
RatatuiRuby.run(viewport: :inline, height: 5) do |tui| # RatauiRuby.run manages the terminal.
|
|
26
|
+
@tui = tui # The TUI instance is safe to store.
|
|
27
|
+
show_menu until chosen? # You can use any loop keyword you like.
|
|
28
|
+
end # `run` won't return until your block does,
|
|
29
|
+
RadioMenu::CHOICES[@choice] # so you can use it synchronously.
|
|
30
|
+
end
|
|
31
|
+
# Classes like RadioMenu are convenient for
|
|
32
|
+
private # CLI authors to offer "rich moments."
|
|
33
|
+
|
|
34
|
+
def show_menu = @tui.draw do |frame| # RatatuiRuby gives you low-level access.
|
|
35
|
+
widget = @tui.paragraph( # But the TUI facade makes it easy to use.
|
|
36
|
+
text: menu_items, # Text can be spans, lines, or paragraphs.
|
|
37
|
+
block: @tui.block(borders: :all, titles: TITLES) # Blocks give you boxes and titles, and hold
|
|
38
|
+
) # one or more widgets. We only use one here,
|
|
39
|
+
frame.render_widget(widget, frame.area) # but "area" lets you compose sub-views.
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def chosen? # You are responsible for handling input.
|
|
43
|
+
interaction = @tui.poll_event # Every frame, you receive an event object:
|
|
44
|
+
return choose if interaction.enter? # Key, Mouse, Resize, Paste, FocusGained,
|
|
45
|
+
# FocusLost, or None objects. They come with
|
|
46
|
+
move_by(-1) if interaction.up? # predicates, support pattern matching, and
|
|
47
|
+
move_by(1) if interaction.down? # can be inspected for properties directly.
|
|
48
|
+
quit! if interaction.ctrl_c? # Your application must handle every input,
|
|
49
|
+
false # even interrupts and other exit patterns.
|
|
49
50
|
end
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
|
|
52
|
+
def choose # Here, the loop is about to exit, and the
|
|
53
|
+
prepare_next_line # block will return. The inline viewport
|
|
54
|
+
@choice # will be torn down and the terminal will
|
|
55
|
+
end # be restored, but you are responsible for
|
|
56
|
+
# positioning the cursor.
|
|
57
|
+
def prepare_next_line # To ensure the next output is on a new
|
|
58
|
+
area = @tui.viewport_area # line, query the viewport area and move
|
|
59
|
+
RatatuiRuby.cursor_position = [0, area.y + area.height] # the cursor to the start of the last line.
|
|
60
|
+
puts # Then print a newline.
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def quit! # All of your familiar Ruby control flow
|
|
64
|
+
prepare_next_line # keywords work as expected, so we can
|
|
65
|
+
exit 0 # use them to leave the TUI.
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def move_by(line_count) # You are in full control of your UX, so
|
|
69
|
+
@choice = (@choice + line_count) % CHOICES.size # you can implement any logic you need:
|
|
70
|
+
end # Would you "wrap around" here, or not?
|
|
71
|
+
#
|
|
72
|
+
def menu_items = CHOICES.map.with_index do |choice, i| # Notably, RatatuiRuby has no concept of
|
|
73
|
+
"#{prefix_for(i)} #{choice}" # "menus" or "radio buttons". You are in
|
|
74
|
+
end # full control, but it also means you must
|
|
75
|
+
def prefix_for(choice_index) # implement the logic yourself. For larger
|
|
76
|
+
return PREFIXES[:active] if choice_index == @choice # applications, consider using Rooibos,
|
|
77
|
+
PREFIXES[:inactive] # an MVU framework built with RatatuiRuby.
|
|
78
|
+
end # Or, use the upcoming ratatui-ruby-kit,
|
|
79
|
+
# our object-oriented component library.
|
|
80
|
+
def initialize = @choice = 0 # However, those are both optional, and
|
|
81
|
+
end # designed for full-screen Terminal UIs.
|
|
82
|
+
# RatatuiRuby will always give you the most
|
|
83
|
+
choice = RadioMenu.new.call # control, and is enough for "rich CLI
|
|
84
|
+
puts "You chose #{choice}!" # moments" like this one.
|
|
@@ -21,13 +21,14 @@ class Spinner
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def ending(tui,
|
|
25
|
-
frame.render_widget(tui.paragraph(text
|
|
24
|
+
def ending(tui, text, fg) = tui.draw do |frame|
|
|
25
|
+
frame.render_widget(tui.paragraph(text:, fg:), frame.area)
|
|
26
|
+
puts # Prepare a new line for the shell prompt
|
|
26
27
|
end
|
|
27
28
|
|
|
28
|
-
def initialize = (@frame, @finish = 0, Time.now +
|
|
29
|
+
def initialize = (@frame, @finish = 0, Time.now + 2)
|
|
29
30
|
def connected? = Time.now >= @finish # Simulate work
|
|
30
31
|
def spin = SPINNER[(@frame += 1) % SPINNER.length]
|
|
31
32
|
SPINNER = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏]
|
|
32
33
|
end
|
|
33
|
-
Spinner.new.main
|
|
34
|
+
Spinner.new.main
|
data/ext/ratatui_ruby/Cargo.lock
CHANGED
data/ext/ratatui_ruby/Cargo.toml
CHANGED
|
@@ -95,6 +95,26 @@ impl RubyTableState {
|
|
|
95
95
|
self.inner.borrow_mut().select_last_column();
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/// Selects the next row or the first one if no row is selected.
|
|
99
|
+
pub fn select_next(&self) {
|
|
100
|
+
self.inner.borrow_mut().select_next();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Selects the previous row or the last one if no row is selected.
|
|
104
|
+
pub fn select_previous(&self) {
|
|
105
|
+
self.inner.borrow_mut().select_previous();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Selects the first row.
|
|
109
|
+
pub fn select_first(&self) {
|
|
110
|
+
self.inner.borrow_mut().select_first();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Selects the last row.
|
|
114
|
+
pub fn select_last(&self) {
|
|
115
|
+
self.inner.borrow_mut().select_last();
|
|
116
|
+
}
|
|
117
|
+
|
|
98
118
|
/// Creates a new `RubyTableState` with a selected cell (row, column).
|
|
99
119
|
pub fn with_selected_cell(cell: Option<(usize, usize)>) -> Self {
|
|
100
120
|
let state = TableState::default().with_selected_cell(cell);
|
|
@@ -144,6 +164,13 @@ pub fn register(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
|
|
|
144
164
|
class.define_method("offset", method!(RubyTableState::offset, 0))?;
|
|
145
165
|
class.define_method("scroll_down_by", method!(RubyTableState::scroll_down_by, 1))?;
|
|
146
166
|
class.define_method("scroll_up_by", method!(RubyTableState::scroll_up_by, 1))?;
|
|
167
|
+
class.define_method("select_next", method!(RubyTableState::select_next, 0))?;
|
|
168
|
+
class.define_method(
|
|
169
|
+
"select_previous",
|
|
170
|
+
method!(RubyTableState::select_previous, 0),
|
|
171
|
+
)?;
|
|
172
|
+
class.define_method("select_first", method!(RubyTableState::select_first, 0))?;
|
|
173
|
+
class.define_method("select_last", method!(RubyTableState::select_last, 0))?;
|
|
147
174
|
Ok(())
|
|
148
175
|
}
|
|
149
176
|
|
|
@@ -138,6 +138,106 @@ module RatatuiRuby
|
|
|
138
138
|
#
|
|
139
139
|
# (Native method implemented in Rust)
|
|
140
140
|
|
|
141
|
+
##
|
|
142
|
+
# :method: select_next
|
|
143
|
+
# :call-seq: select_next() -> nil
|
|
144
|
+
#
|
|
145
|
+
# Moves selection to the next row. Selects first row if nothing selected.
|
|
146
|
+
#
|
|
147
|
+
# === Optimistic Indexing
|
|
148
|
+
#
|
|
149
|
+
# Increments the index immediately, even past table bounds. The renderer
|
|
150
|
+
# clamps to valid range on draw. Reading <tt>selected</tt> between this
|
|
151
|
+
# call and render may return an out-of-bounds value.
|
|
152
|
+
#
|
|
153
|
+
# To detect actual selection changes, check bounds first:
|
|
154
|
+
#
|
|
155
|
+
#--
|
|
156
|
+
# SPDX-SnippetBegin
|
|
157
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
158
|
+
# SPDX-License-Identifier: MIT-0
|
|
159
|
+
#++
|
|
160
|
+
# max_index = rows.size - 1
|
|
161
|
+
# return if (state.selected || 0) >= max_index
|
|
162
|
+
# state.select_next
|
|
163
|
+
#
|
|
164
|
+
#--
|
|
165
|
+
# SPDX-SnippetEnd
|
|
166
|
+
#++
|
|
167
|
+
# (Native method implemented in Rust)
|
|
168
|
+
|
|
169
|
+
##
|
|
170
|
+
# :method: select_previous
|
|
171
|
+
# :call-seq: select_previous() -> nil
|
|
172
|
+
#
|
|
173
|
+
# Moves selection to the previous row. Selects last row if nothing selected.
|
|
174
|
+
#
|
|
175
|
+
# === Optimistic Indexing
|
|
176
|
+
#
|
|
177
|
+
# At index 0, does nothing. With no selection, sets index to maximum value;
|
|
178
|
+
# the renderer clamps to actual last row on draw.
|
|
179
|
+
#
|
|
180
|
+
# To detect actual selection changes, check bounds first:
|
|
181
|
+
#
|
|
182
|
+
#--
|
|
183
|
+
# SPDX-SnippetBegin
|
|
184
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
185
|
+
# SPDX-License-Identifier: MIT-0
|
|
186
|
+
#++
|
|
187
|
+
# return if (state.selected || 0) <= 0
|
|
188
|
+
# state.select_previous
|
|
189
|
+
#
|
|
190
|
+
#--
|
|
191
|
+
# SPDX-SnippetEnd
|
|
192
|
+
#++
|
|
193
|
+
# (Native method implemented in Rust)
|
|
194
|
+
|
|
195
|
+
##
|
|
196
|
+
# :method: select_first
|
|
197
|
+
# :call-seq: select_first() -> nil
|
|
198
|
+
#
|
|
199
|
+
# Jumps selection to the first row (index 0).
|
|
200
|
+
#
|
|
201
|
+
# To detect actual selection changes:
|
|
202
|
+
#
|
|
203
|
+
#--
|
|
204
|
+
# SPDX-SnippetBegin
|
|
205
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
206
|
+
# SPDX-License-Identifier: MIT-0
|
|
207
|
+
#++
|
|
208
|
+
# return if (state.selected || 0) == 0
|
|
209
|
+
# state.select_first
|
|
210
|
+
#
|
|
211
|
+
#--
|
|
212
|
+
# SPDX-SnippetEnd
|
|
213
|
+
#++
|
|
214
|
+
# (Native method implemented in Rust)
|
|
215
|
+
|
|
216
|
+
##
|
|
217
|
+
# :method: select_last
|
|
218
|
+
# :call-seq: select_last() -> nil
|
|
219
|
+
#
|
|
220
|
+
# Jumps selection to the last row.
|
|
221
|
+
#
|
|
222
|
+
# === Optimistic Indexing
|
|
223
|
+
#
|
|
224
|
+
# Sets index to maximum possible value. The renderer clamps to actual last
|
|
225
|
+
# row on draw. To get or check the real last index, track row count:
|
|
226
|
+
#
|
|
227
|
+
#--
|
|
228
|
+
# SPDX-SnippetBegin
|
|
229
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
230
|
+
# SPDX-License-Identifier: MIT-0
|
|
231
|
+
#++
|
|
232
|
+
# max_index = rows.size - 1
|
|
233
|
+
# return if (state.selected || 0) == max_index
|
|
234
|
+
# state.select(max_index)
|
|
235
|
+
#
|
|
236
|
+
#--
|
|
237
|
+
# SPDX-SnippetEnd
|
|
238
|
+
#++
|
|
239
|
+
# (Native method implemented in Rust)
|
|
240
|
+
|
|
141
241
|
##
|
|
142
242
|
# :singleton-method: with_selected_cell
|
|
143
243
|
# :call-seq: with_selected_cell(cell) -> TableState
|
data/lib/ratatui_ruby/version.rb
CHANGED
data/tasks/bump/sem_ver.rb
CHANGED
|
@@ -11,12 +11,15 @@ class SemVer
|
|
|
11
11
|
|
|
12
12
|
def self.parse(string)
|
|
13
13
|
require "rubygems"
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
# Extract prerelease suffix (e.g., "-beta.1", "-alpha.2", "-rc.1")
|
|
15
|
+
base, prerelease = string.split("-", 2)
|
|
16
|
+
segments = Gem::Version.new(base).segments.fill(0, 3).first(3)
|
|
17
|
+
new(segments, prerelease:)
|
|
16
18
|
end
|
|
17
19
|
|
|
18
|
-
def initialize(segments)
|
|
20
|
+
def initialize(segments, prerelease: nil)
|
|
19
21
|
@segments = segments
|
|
22
|
+
@prerelease = prerelease
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
def next(segment)
|
|
@@ -31,6 +34,7 @@ class SemVer
|
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
def to_s
|
|
34
|
-
@segments.join(".")
|
|
37
|
+
base = @segments.join(".")
|
|
38
|
+
@prerelease ? "#{base}-#{@prerelease}" : base
|
|
35
39
|
end
|
|
36
40
|
end
|
data/tasks/lint.rake
CHANGED
|
@@ -85,8 +85,10 @@ namespace :reuse do
|
|
|
85
85
|
desc "Normalize Ruby files: frozen_string_literal at top, SPDX in #--/#++ block"
|
|
86
86
|
task :normalize_ruby do
|
|
87
87
|
ruby_extensions = %w[rb rake gemspec].freeze
|
|
88
|
+
# Exclude intentionally-formatted files
|
|
89
|
+
excluded_files = %w[examples/verify_website_menu/app.rb].freeze
|
|
88
90
|
ruby_files = Dir.glob("**/*.{#{ruby_extensions.join(',')}}")
|
|
89
|
-
.reject { |f| f.start_with?("vendor/", "tmp/", ".") }
|
|
91
|
+
.reject { |f| f.start_with?("vendor/", "tmp/", ".") || excluded_files.include?(f) }
|
|
90
92
|
|
|
91
93
|
fixed_count = 0
|
|
92
94
|
ruby_files.each do |file|
|
data/tasks/sourcehut.rake
CHANGED
|
@@ -25,7 +25,11 @@ namespace :sourcehut do
|
|
|
25
25
|
version_content = File.read("lib/ratatui_ruby/version.rb")
|
|
26
26
|
version = version_content.match(/VERSION = "(.+?)"/)[1]
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
# Normalize version using Gem::Version - RubyGems converts hyphens
|
|
29
|
+
# (e.g., "1.0.0-beta.1" -> "1.0.0.pre.beta.1")
|
|
30
|
+
normalized_version = Gem::Version.new(version).to_s
|
|
31
|
+
|
|
32
|
+
gem_filename = "#{spec.name}-#{normalized_version}.gem"
|
|
29
33
|
|
|
30
34
|
rubies = YAML.load_file("tasks/resources/rubies.yml")
|
|
31
35
|
|
|
@@ -80,6 +84,27 @@ end
|
|
|
80
84
|
|
|
81
85
|
if Rake::Task.task_defined?("release")
|
|
82
86
|
Rake::Task["release"].enhance do
|
|
87
|
+
# Replace Bundler's normalized tag with semver-style for prerelease versions.
|
|
88
|
+
# Bundler creates tags using normalized Gem::Version (e.g., v1.0.0.pre.beta.1).
|
|
89
|
+
# Semver uses hyphens (e.g., v1.0.0-beta.1). We want only the semver tag.
|
|
90
|
+
version_content = File.read("lib/ratatui_ruby/version.rb")
|
|
91
|
+
version = version_content.match(/VERSION = "(.+?)"/)[1]
|
|
92
|
+
|
|
93
|
+
if version.include?("-")
|
|
94
|
+
normalized_tag = "v#{Gem::Version.new(version)}"
|
|
95
|
+
semver_tag = "v#{version}"
|
|
96
|
+
|
|
97
|
+
if normalized_tag != semver_tag
|
|
98
|
+
puts "Replacing normalized tag #{normalized_tag} with semver tag #{semver_tag}..."
|
|
99
|
+
# Delete the normalized tag locally and remotely
|
|
100
|
+
sh "git tag -d #{normalized_tag}"
|
|
101
|
+
sh "git push origin :refs/tags/#{normalized_tag}"
|
|
102
|
+
# Create the semver tag pointing to the same commit
|
|
103
|
+
sh "git tag #{semver_tag} HEAD"
|
|
104
|
+
sh "git push origin #{semver_tag}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
83
108
|
Rake::Task["sourcehut:update_stable"].invoke
|
|
84
109
|
end
|
|
85
110
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ratatui_ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0.pre.beta.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kerrick Long
|
|
@@ -93,8 +93,309 @@ dependencies:
|
|
|
93
93
|
- - ">="
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '1.0'
|
|
96
|
-
description:
|
|
97
|
-
|
|
96
|
+
description: |2-
|
|
97
|
+
|
|
98
|
+
== Terminal UIs, the Ruby Way
|
|
99
|
+
|
|
100
|
+
RatatuiRuby[https://rubygems.org/gems/ratatui_ruby] is a RubyGem built on
|
|
101
|
+
Ratatui[https://ratatui.rs], a leading TUI library written in
|
|
102
|
+
Rust[https://rust-lang.org]. You get native performance with the joy of Ruby.
|
|
103
|
+
|
|
104
|
+
gem install ratatui_ruby --pre
|
|
105
|
+
|
|
106
|
+
{rdoc-image:https://ratatui-ruby.dev/hero.gif}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_cli_rich_moments/README_md.html]
|
|
107
|
+
|
|
108
|
+
=== Rich Moments
|
|
109
|
+
|
|
110
|
+
Add a spinner, a progress bar, or an inline menu to your CLI script. No
|
|
111
|
+
full-screen takeover. Your terminal history stays intact.
|
|
112
|
+
|
|
113
|
+
==== Inline Viewports
|
|
114
|
+
|
|
115
|
+
Standard TUIs erase themselves on exit. Your carefully formatted CLI output
|
|
116
|
+
disappears. Users lose their scrollback.
|
|
117
|
+
|
|
118
|
+
<b>Inline viewports</b> solve this. They occupy a fixed number of lines, render
|
|
119
|
+
rich UI, then leave the output in place when done.
|
|
120
|
+
|
|
121
|
+
Perfect for spinners, menus, progress indicators—any brief moment of richness.
|
|
122
|
+
|
|
123
|
+
require "ratatui_ruby"
|
|
124
|
+
|
|
125
|
+
RatatuiRuby.run(viewport: :inline, height: 1) do |tui|
|
|
126
|
+
until connected?
|
|
127
|
+
status = tui.paragraph(text: "\#{spin} Connecting...")
|
|
128
|
+
tui.draw { |frame| frame.render_widget(status, frame.area) }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
=== Build Something Real
|
|
133
|
+
|
|
134
|
+
Full-screen applications with {keyboard and mouse input}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_all_events/README_md.html]. The managed loop
|
|
135
|
+
sets up the terminal and restores it on exit, even after crashes.
|
|
136
|
+
|
|
137
|
+
RatatuiRuby.run do |tui|
|
|
138
|
+
loop do
|
|
139
|
+
tui.draw do |frame|
|
|
140
|
+
frame.render_widget(
|
|
141
|
+
tui.paragraph(text: "Hello, RatatuiRuby!", alignment: :center),
|
|
142
|
+
frame.area
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
case tui.poll_event
|
|
147
|
+
in { type: :key, code: "q" } then break
|
|
148
|
+
else nil
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
==== Widgets included:
|
|
154
|
+
|
|
155
|
+
[Layout]
|
|
156
|
+
{Block}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_block/README_md.html],
|
|
157
|
+
{Center}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_center/README_md.html],
|
|
158
|
+
{Clear (Popup, Modal)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_popup/README_md.html],
|
|
159
|
+
{Layout (Split, Grid)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_layout_split/README_md.html],
|
|
160
|
+
{Overlay}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_overlay/README_md.html]
|
|
161
|
+
[Data]
|
|
162
|
+
{Bar Chart}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_barchart/README_md.html],
|
|
163
|
+
{Chart}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_chart/README_md.html],
|
|
164
|
+
{Gauge}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_gauge/README_md.html],
|
|
165
|
+
{Line Gauge}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_line_gauge/README_md.html],
|
|
166
|
+
{Sparkline}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_sparkline/README_md.html],
|
|
167
|
+
{Table}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_table/README_md.html]
|
|
168
|
+
[Text]
|
|
169
|
+
{Cell}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_cell/README_md.html],
|
|
170
|
+
{List}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_list/README_md.html],
|
|
171
|
+
{Rich Text (Line, Span)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_rich_text/README_md.html],
|
|
172
|
+
{Scrollbar (Scroll)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_scrollbar/README_md.html],
|
|
173
|
+
{Tabs}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_tabs/README_md.html]
|
|
174
|
+
[Graphics]
|
|
175
|
+
{Calendar}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_calendar/README_md.html],
|
|
176
|
+
{Canvas}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_canvas/README_md.html],
|
|
177
|
+
{Map (World Map)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_map/README_md.html]
|
|
178
|
+
|
|
179
|
+
Need something else? {Build custom widgets}[https://www.ratatui-ruby.dev/docs/v0.10/doc/concepts/custom_widgets_md.html] in Ruby!
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
=== Testing Built In
|
|
185
|
+
|
|
186
|
+
TUI testing is tedious. You need a headless terminal, event injection,
|
|
187
|
+
snapshot comparisons, and style assertions. RatatuiRuby bundles all of it.
|
|
188
|
+
|
|
189
|
+
require "ratatui_ruby/test_helper"
|
|
190
|
+
|
|
191
|
+
class TestColorPicker < Minitest::Test
|
|
192
|
+
include RatatuiRuby::TestHelper
|
|
193
|
+
|
|
194
|
+
def test_swatch_widget
|
|
195
|
+
with_test_terminal(10, 3) do
|
|
196
|
+
RatatuiRuby.draw do |frame|
|
|
197
|
+
frame.render_widget(Swatch.new(:red), frame.area)
|
|
198
|
+
end
|
|
199
|
+
assert_cell_style 2, 1, char: "█", bg: :red
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
==== What's inside:
|
|
205
|
+
|
|
206
|
+
- <b>Headless terminal</b> — No real TTY needed
|
|
207
|
+
- <b>Snapshots</b> — Plain text and rich (ANSI colors)
|
|
208
|
+
- <b>Event injection</b> — Keys, mouse, paste, resize
|
|
209
|
+
- <b>Style assertions</b> — Color, bold, underline at any cell
|
|
210
|
+
- <b>Test doubles</b> — Mock frames and stub rects
|
|
211
|
+
- <b>UPDATE_SNAPSHOTS=1</b> — Regenerate baselines in one command
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
==== Inline Menu Example
|
|
217
|
+
|
|
218
|
+
require "ratatui_ruby"
|
|
219
|
+
|
|
220
|
+
# This example renders an inline menu. Arrow keys select, enter confirms.
|
|
221
|
+
# The menu appears in-place, preserving scrollback. When the user chooses,
|
|
222
|
+
# the TUI closes and the script continues with the selected value.
|
|
223
|
+
class RadioMenu
|
|
224
|
+
CHOICES = ["Production", "Staging", "Development"] # ASCII strings are universally supported.
|
|
225
|
+
PREFIXES = { active: "●", inactive: "○" } # Some terminals may not support Unicode.
|
|
226
|
+
CONTROLS = "↑/↓: Select | Enter: Choose | Ctrl+C: Cancel" # Let users know what keys you handle.
|
|
227
|
+
TITLES = ["Select Environment", # The default title position is top left.
|
|
228
|
+
{ content: CONTROLS, # Multiple titles can save space.
|
|
229
|
+
position: :bottom, # Titles go on the top or bottom,
|
|
230
|
+
alignment: :right }] # aligned left, right, or center
|
|
231
|
+
|
|
232
|
+
def call # This method blocks until a choice is made.
|
|
233
|
+
RatatuiRuby.run(viewport: :inline, height: 5) do |tui| # RatauiRuby.run manages the terminal.
|
|
234
|
+
@tui = tui # The TUI instance is safe to store.
|
|
235
|
+
show_menu until chosen? # You can use any loop keyword you like.
|
|
236
|
+
end # `run` won't return until your block does,
|
|
237
|
+
RadioMenu::CHOICES[@choice] # so you can use it synchronously.
|
|
238
|
+
end
|
|
239
|
+
# Classes like RadioMenu are convenient for
|
|
240
|
+
private # CLI authors to offer "rich moments."
|
|
241
|
+
|
|
242
|
+
def show_menu = @tui.draw do |frame| # RatatuiRuby gives you low-level access.
|
|
243
|
+
widget = @tui.paragraph( # But the TUI facade makes it easy to use.
|
|
244
|
+
text: menu_items, # Text can be spans, lines, or paragraphs.
|
|
245
|
+
block: @tui.block(borders: :all, titles: TITLES) # Blocks give you boxes and titles, and hold
|
|
246
|
+
) # one or more widgets. We only use one here,
|
|
247
|
+
frame.render_widget(widget, frame.area) # but "area" lets you compose sub-views.
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def chosen? # You are responsible for handling input.
|
|
251
|
+
interaction = @tui.poll_event # Every frame, you receive an event object:
|
|
252
|
+
return choose if interaction.enter? # Key, Mouse, Resize, Paste, FocusGained,
|
|
253
|
+
# FocusLost, or None objects. They come with
|
|
254
|
+
move_by(-1) if interaction.up? # predicates, support pattern matching, and
|
|
255
|
+
move_by(1) if interaction.down? # can be inspected for properties directly.
|
|
256
|
+
quit! if interaction.ctrl_c? # Your application must handle every input,
|
|
257
|
+
false # even interrupts and other exit patterns.
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def choose # Here, the loop is about to exit, and the
|
|
261
|
+
prepare_next_line # block will return. The inline viewport
|
|
262
|
+
@choice # will be torn down and the terminal will
|
|
263
|
+
end # be restored, but you are responsible for
|
|
264
|
+
# positioning the cursor.
|
|
265
|
+
def prepare_next_line # To ensure the next output is on a new
|
|
266
|
+
area = @tui.viewport_area # line, query the viewport area and move
|
|
267
|
+
RatatuiRuby.cursor_position = [0, area.y + area.height] # the cursor to the start of the last line.
|
|
268
|
+
puts # Then print a newline.
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def quit! # All of your familiar Ruby control flow
|
|
272
|
+
prepare_next_line # keywords work as expected, so we can
|
|
273
|
+
exit 0 # use them to leave the TUI.
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def move_by(line_count) # You are in full control of your UX, so
|
|
277
|
+
@choice = (@choice + line_count) % CHOICES.size # you can implement any logic you need:
|
|
278
|
+
end # Would you "wrap around" here, or not?
|
|
279
|
+
#
|
|
280
|
+
def menu_items = CHOICES.map.with_index do |choice, i| # Notably, RatatuiRuby has no concept of
|
|
281
|
+
"\#{prefix_for(i)} \#{choice}" # "menus" or "radio buttons". You are in
|
|
282
|
+
end # full control, but it also means you must
|
|
283
|
+
def prefix_for(choice_index) # implement the logic yourself. For larger
|
|
284
|
+
return PREFIXES[:active] if choice_index == @choice # applications, consider using Rooibos,
|
|
285
|
+
PREFIXES[:inactive] # an MVU framework built with RatatuiRuby.
|
|
286
|
+
end # Or, use the upcoming ratatui-ruby-kit,
|
|
287
|
+
# our object-oriented component library.
|
|
288
|
+
def initialize = @choice = 0 # However, those are both optional, and
|
|
289
|
+
end # designed for full-screen Terminal UIs.
|
|
290
|
+
# RatatuiRuby will always give you the most
|
|
291
|
+
choice = RadioMenu.new.call # control, and is enough for "rich CLI
|
|
292
|
+
puts "You chose \#{choice}!" # moments" like this one.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
=== Full App Solutions
|
|
297
|
+
|
|
298
|
+
RatatuiRuby renders. For complex applications, add a framework that manages
|
|
299
|
+
state and composition.
|
|
300
|
+
|
|
301
|
+
==== Rooibos[https://git.sr.ht/~kerrick/rooibos] (Framework)
|
|
302
|
+
|
|
303
|
+
Model-View-Update architecture. Inspired by Elm, Bubble Tea, and React +
|
|
304
|
+
Redux. Your UI is a pure function of state.
|
|
305
|
+
|
|
306
|
+
- Functional programming with MVU
|
|
307
|
+
- Commands work off the main thread
|
|
308
|
+
- Messages, not callbacks, drive updates
|
|
309
|
+
|
|
310
|
+
==== {Kit}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-3-the-object-path--kit] (Coming Soon)
|
|
311
|
+
|
|
312
|
+
Component-based architecture. Encapsulate state, input handling, and
|
|
313
|
+
rendering in reusable pieces.
|
|
314
|
+
|
|
315
|
+
- OOP with stateful components
|
|
316
|
+
- Separate UI state from domain logic
|
|
317
|
+
- Built-in focus management & click handling
|
|
318
|
+
|
|
319
|
+
Both use the same widget library and rendering engine. Pick the paradigm
|
|
320
|
+
that fits your brain.
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
=== Why RatatuiRuby?
|
|
326
|
+
|
|
327
|
+
Ruby deserves world-class terminal user interfaces. TUI developers deserve
|
|
328
|
+
a world-class language.
|
|
329
|
+
|
|
330
|
+
RatatuiRuby wraps Rust's Ratatui via native extension. The Rust library
|
|
331
|
+
handles rendering. Your Ruby code handles design.
|
|
332
|
+
|
|
333
|
+
>>>
|
|
334
|
+
"Text UIs are seeing a renaissance with many new TUI libraries popping up.
|
|
335
|
+
The Ratatui bindings have proven to be full featured and stable."
|
|
336
|
+
|
|
337
|
+
— {Mike Perham}[https://www.mikeperham.com/], creator of
|
|
338
|
+
Sidekiq[https://sidekiq.org/] and Faktory[https://contribsys.com/faktory/]
|
|
339
|
+
|
|
340
|
+
==== Why Rust? Why Ruby?
|
|
341
|
+
|
|
342
|
+
Rust excels at low-level rendering. Ruby excels at expressing domain logic
|
|
343
|
+
and UI. RatatuiRuby puts each language where it performs best.
|
|
344
|
+
|
|
345
|
+
==== Versus CharmRuby
|
|
346
|
+
|
|
347
|
+
CharmRuby[https://charm-ruby.dev/] wraps Charm's Go libraries. Both projects
|
|
348
|
+
give Ruby developers TUI options.
|
|
349
|
+
|
|
350
|
+
[Integration]
|
|
351
|
+
CharmRuby: Two runtimes, one process.
|
|
352
|
+
RatatuiRuby: Native extension in Rust.
|
|
353
|
+
[Runtime]
|
|
354
|
+
CharmRuby: Go + Ruby (competing).
|
|
355
|
+
RatatuiRuby: Ruby (Rust has no runtime).
|
|
356
|
+
[Memory]
|
|
357
|
+
CharmRuby: Two uncoordinated GCs.
|
|
358
|
+
RatatuiRuby: One Garbage Collector.
|
|
359
|
+
[Style]
|
|
360
|
+
CharmRuby: The Elm Architecture (TEA).
|
|
361
|
+
RatatuiRuby: TEA, OOP, or Imperative.
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
=== Links
|
|
367
|
+
|
|
368
|
+
[Get Started]
|
|
369
|
+
{Quickstart}[https://www.ratatui-ruby.dev/docs/v0.10/doc/getting_started/quickstart_md.html],
|
|
370
|
+
{Examples}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_cli_rich_moments/README_md.html],
|
|
371
|
+
{API Reference}[https://www.ratatui-ruby.dev/docs/v0.10/],
|
|
372
|
+
{Guides}[https://www.ratatui-ruby.dev/docs/v0.10/doc/index_md.html]
|
|
373
|
+
[Ecosystem]
|
|
374
|
+
Rooibos[https://git.sr.ht/~kerrick/rooibos],
|
|
375
|
+
{Kit}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-3-the-object-path--kit] (Planned),
|
|
376
|
+
{Framework}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-5-the-framework] (Planned),
|
|
377
|
+
{UI Widgets}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-6-licensing] (Planned)
|
|
378
|
+
[Community]
|
|
379
|
+
{Discuss and Chat}[https://lists.sr.ht/~kerrick/ratatui_ruby-discuss],
|
|
380
|
+
{Announcements}[https://lists.sr.ht/~kerrick/ratatui_ruby-announce],
|
|
381
|
+
{Development}[https://lists.sr.ht/~kerrick/ratatui_ruby-devel],
|
|
382
|
+
{Bug Tracker}[https://todo.sr.ht/~kerrick/ratatui_ruby]
|
|
383
|
+
[Contribute]
|
|
384
|
+
{Contributing Guide}[https://man.sr.ht/~kerrick/ratatui_ruby/contributing.md],
|
|
385
|
+
{Code of Conduct}[https://man.sr.ht/~kerrick/ratatui_ruby/code_of_conduct.md],
|
|
386
|
+
{Project History}[https://man.sr.ht/~kerrick/ratatui_ruby/history/index.md],
|
|
387
|
+
{Pull Requests}[https://lists.sr.ht/~kerrick/ratatui_ruby-devel/patches]
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
[Website] https://www.ratatui-ruby.dev
|
|
393
|
+
[Source] https://git.sr.ht/~kerrick/ratatui_ruby
|
|
394
|
+
[RubyGems] https://rubygems.org/gems/ratatui_ruby
|
|
395
|
+
[Upstream] https://ratatui.rs
|
|
396
|
+
[Build Status] https://builds.sr.ht/~kerrick/ratatui_ruby
|
|
397
|
+
|
|
398
|
+
© 2026 Kerrick Long · Library: LGPL-3.0-or-later · Website: CC-BY-NC-ND-4.0 · Snippets: MIT-0
|
|
98
399
|
email:
|
|
99
400
|
- me@kerricklong.com
|
|
100
401
|
executables:
|
|
@@ -119,6 +420,7 @@ files:
|
|
|
119
420
|
- LICENSES/MIT-0.txt
|
|
120
421
|
- LICENSES/MIT.txt
|
|
121
422
|
- README.md
|
|
423
|
+
- README.rdoc
|
|
122
424
|
- REUSE.toml
|
|
123
425
|
- Rakefile
|
|
124
426
|
- Steepfile
|