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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45627e7b77b76ba56a7cb5bb3b0cb578d282a26782c6702abdc87893d89e14a9
4
- data.tar.gz: b401cd48eb576970a74c8efc7bf22dba20a9bbd4d4378ff85b4a3f45797a3e31
3
+ metadata.gz: 69c2adadc9306ebeb02ffefa11e56abe7db2f0ef1c361a2edaf1cf5a3f9b9fee
4
+ data.tar.gz: c8e07b3361a46597f957fa6a4ade8e9eef426fa85a0df8650f7188a306384d5a
5
5
  SHA512:
6
- metadata.gz: 8ef1ed4f7c6c35df19e7955fd9fc752e5b73da316380fe21c9927b3f5c5fbc3d959948821b0c795572edf58cd43627be65922452f93dfa51dd54f93e1a2b164b
7
- data.tar.gz: 13c4cb8c1de48f10920128527222f4ae94db9ac98f0ec1b3b975b3edd0a221859f352a2ddc570b0d955f566a393341413ea2e0ed9ea4e1e12f596973bf657723
6
+ metadata.gz: eb439fe49d032bf2d7221b4771b2b6b0156d9b5373d32fe6710ff0077046263fc5e719c1d91d1ed271b3cb58c18cf9f31ef6d366daed59b6f4413ac66db82447
7
+ data.tar.gz: dc395bd562a70d0bc8a9d6eafa4ca0b488ef3ce450a3f3d65b125550ed616c1516b2b5301bb0b634028433f90d4306885ba8f756542241fb9d5e0c4fa25d6644
data/.builds/ruby-3.2.yml CHANGED
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.10.3.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-1.0.0.pre.beta.2.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
data/.builds/ruby-3.3.yml CHANGED
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.10.3.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-1.0.0.pre.beta.2.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
data/.builds/ruby-3.4.yml CHANGED
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.10.3.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-1.0.0.pre.beta.2.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.10.3.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-1.0.0.pre.beta.2.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
data/.rubocop.yml CHANGED
@@ -5,4 +5,6 @@ inherit_from:
5
5
  - ./vendor/goodcop/base.yml
6
6
 
7
7
  AllCops:
8
- TargetRubyVersion: "3.3"
8
+ TargetRubyVersion: "3.3"
9
+ Exclude:
10
+ - 'examples/verify_website_menu/app.rb'
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 **ALPHA**. The API may change between minor versions.
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: :black, bg: :green) : nil
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
- choices = ["Production", "Staging", "Development"]
13
- index = 0
14
-
15
- RatatuiRuby.run(viewport: :inline, height: 5) do |tui|
16
- loop do
17
- tui.draw do |frame|
18
- items = choices.map.with_index do |c, i|
19
- prefix = (i == index) ? "● " : "○ "
20
- "#{prefix}#{c}"
21
- end
22
- widget = tui.paragraph(
23
- text: items.join("\n"),
24
- block: tui.block(
25
- borders: :all,
26
- title: "Select Environment",
27
- titles: [{ content: "↑/↓ Enter | Ctrl+C: Cancel", position: :bottom, alignment: :right }]
28
- )
29
- )
30
- frame.render_widget(widget, frame.area)
31
- end
32
-
33
- case tui.poll_event
34
- in { type: :key, code: "up" }
35
- index = (index - 1) % choices.size
36
- in { type: :key, code: "down" }
37
- index = (index + 1) % choices.size
38
- in { type: :key, code: "enter" }
39
- area = tui.viewport_area
40
- RatatuiRuby.cursor_position = [0, area.y + area.height]
41
- break
42
- in { type: :key, code: "c", modifiers: ["ctrl"] }
43
- area = tui.viewport_area
44
- RatatuiRuby.cursor_position = [0, area.y + area.height]
45
- index = nil
46
- break
47
- else nil
48
- end
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
- end
51
-
52
- puts
53
- if index
54
- puts "Deploying to #{choices[index]}..."
55
- else
56
- puts "Cancelled."
57
- end
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, message, color) = tui.draw do |frame|
25
- frame.render_widget(tui.paragraph(text: message, fg: color), frame.area)
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 + 1)
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; puts
34
+ Spinner.new.main
@@ -1059,7 +1059,7 @@ dependencies = [
1059
1059
 
1060
1060
  [[package]]
1061
1061
  name = "ratatui_ruby"
1062
- version = "0.10.3"
1062
+ version = "1.0.0-beta.2"
1063
1063
  dependencies = [
1064
1064
  "bumpalo",
1065
1065
  "lazy_static",
@@ -3,7 +3,7 @@
3
3
 
4
4
  [package]
5
5
  name = "ratatui_ruby"
6
- version = "0.10.3"
6
+ version = "1.0.0-beta.2"
7
7
  edition = "2021"
8
8
 
9
9
  [lib]
@@ -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
@@ -8,5 +8,5 @@
8
8
  module RatatuiRuby
9
9
  # The version of the ratatui_ruby gem.
10
10
  # See https://semver.org/spec/v2.0.0.html
11
- VERSION = "0.10.3"
11
+ VERSION = "1.0.0-beta.2"
12
12
  end
@@ -11,12 +11,15 @@ class SemVer
11
11
 
12
12
  def self.parse(string)
13
13
  require "rubygems"
14
- segments = Gem::Version.new(string).segments.fill(0, 3).first(3)
15
- new(segments)
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
- gem_filename = "#{spec.name}-#{version}.gem"
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.10.3
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: ratatui_ruby is a wrapper for the Ratatui Rust crate <https://ratatui.rs>.
97
- It allows you to cook up Terminal User Interfaces in Ruby.
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