ratatui_ruby 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/lib/ratatui_ruby/version.rb +1 -1
- metadata +1 -232
- data/.builds/ruby-3.2.yml +0 -54
- data/.builds/ruby-3.3.yml +0 -54
- data/.builds/ruby-3.4.yml +0 -54
- data/.builds/ruby-4.0.0.yml +0 -54
- data/.pre-commit-config.yaml +0 -16
- data/.rubocop.yml +0 -10
- data/AGENTS.md +0 -146
- data/CHANGELOG.md +0 -710
- data/README.md +0 -187
- data/README.rdoc +0 -302
- data/Rakefile +0 -11
- data/Steepfile +0 -49
- data/doc/concepts/application_architecture.md +0 -321
- data/doc/concepts/application_testing.md +0 -193
- data/doc/concepts/async.md +0 -190
- data/doc/concepts/custom_widgets.md +0 -247
- data/doc/concepts/debugging.md +0 -401
- data/doc/concepts/event_handling.md +0 -162
- data/doc/concepts/interactive_design.md +0 -146
- data/doc/contributors/auditing/parity.md +0 -239
- data/doc/contributors/design/ruby_frontend.md +0 -420
- data/doc/contributors/design/rust_backend.md +0 -422
- data/doc/contributors/design.md +0 -11
- data/doc/contributors/developing_examples.md +0 -400
- data/doc/contributors/documentation_style.md +0 -121
- data/doc/contributors/index.md +0 -21
- data/doc/contributors/todo/align/api_completeness_audit-finished.md +0 -375
- data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -206
- data/doc/contributors/todo/align/terminal.md +0 -647
- data/doc/contributors/todo/future_work.md +0 -169
- data/doc/contributors/upstream_requests/tab_rects.md +0 -173
- data/doc/contributors/upstream_requests/title_rects.md +0 -132
- data/doc/custom.css +0 -22
- data/doc/getting_started/quickstart.md +0 -291
- data/doc/getting_started/why.md +0 -93
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_cli_rich_moments.gif +0 -0
- data/doc/images/app_color_picker.png +0 -0
- data/doc/images/app_debugging_showcase.gif +0 -0
- data/doc/images/app_debugging_showcase.png +0 -0
- data/doc/images/app_login_form.png +0 -0
- data/doc/images/app_stateful_interaction.png +0 -0
- data/doc/images/verify_quickstart_dsl.png +0 -0
- data/doc/images/verify_quickstart_layout.png +0 -0
- data/doc/images/verify_quickstart_lifecycle.png +0 -0
- data/doc/images/verify_readme_usage.png +0 -0
- data/doc/images/widget_barchart.png +0 -0
- data/doc/images/widget_block.png +0 -0
- data/doc/images/widget_box.png +0 -0
- data/doc/images/widget_calendar.png +0 -0
- data/doc/images/widget_canvas.png +0 -0
- data/doc/images/widget_cell.png +0 -0
- data/doc/images/widget_center.png +0 -0
- data/doc/images/widget_chart.png +0 -0
- data/doc/images/widget_gauge.png +0 -0
- data/doc/images/widget_layout_split.png +0 -0
- data/doc/images/widget_line_gauge.png +0 -0
- data/doc/images/widget_list.png +0 -0
- data/doc/images/widget_map.png +0 -0
- data/doc/images/widget_overlay.png +0 -0
- data/doc/images/widget_popup.png +0 -0
- data/doc/images/widget_ratatui_logo.png +0 -0
- data/doc/images/widget_ratatui_mascot.png +0 -0
- data/doc/images/widget_rect.png +0 -0
- data/doc/images/widget_render.png +0 -0
- data/doc/images/widget_rich_text.png +0 -0
- data/doc/images/widget_scroll_text.png +0 -0
- data/doc/images/widget_scrollbar.png +0 -0
- data/doc/images/widget_sparkline.png +0 -0
- data/doc/images/widget_style_colors.png +0 -0
- data/doc/images/widget_table.png +0 -0
- data/doc/images/widget_tabs.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/index.md +0 -39
- data/doc/troubleshooting/async.md +0 -4
- data/doc/troubleshooting/terminal_limitations.md +0 -131
- data/doc/troubleshooting/tui_output.md +0 -197
- data/examples/app_all_events/README.md +0 -114
- data/examples/app_all_events/app.rb +0 -98
- data/examples/app_all_events/model/app_model.rb +0 -159
- data/examples/app_all_events/model/event_color_cycle.rb +0 -43
- data/examples/app_all_events/model/event_entry.rb +0 -94
- data/examples/app_all_events/model/msg.rb +0 -39
- data/examples/app_all_events/model/timestamp.rb +0 -56
- data/examples/app_all_events/update.rb +0 -75
- data/examples/app_all_events/view/app_view.rb +0 -80
- data/examples/app_all_events/view/controls_view.rb +0 -54
- data/examples/app_all_events/view/counts_view.rb +0 -61
- data/examples/app_all_events/view/live_view.rb +0 -72
- data/examples/app_all_events/view/log_view.rb +0 -57
- data/examples/app_all_events/view.rb +0 -9
- data/examples/app_cli_rich_moments/README.md +0 -81
- data/examples/app_cli_rich_moments/app.rb +0 -189
- data/examples/app_color_picker/README.md +0 -156
- data/examples/app_color_picker/app.rb +0 -76
- data/examples/app_color_picker/clipboard.rb +0 -86
- data/examples/app_color_picker/color.rb +0 -193
- data/examples/app_color_picker/controls.rb +0 -92
- data/examples/app_color_picker/copy_dialog.rb +0 -168
- data/examples/app_color_picker/export_pane.rb +0 -128
- data/examples/app_color_picker/harmony.rb +0 -58
- data/examples/app_color_picker/input.rb +0 -176
- data/examples/app_color_picker/main_container.rb +0 -180
- data/examples/app_color_picker/palette.rb +0 -111
- data/examples/app_debugging_showcase/README.md +0 -119
- data/examples/app_debugging_showcase/app.rb +0 -318
- data/examples/app_login_form/README.md +0 -58
- data/examples/app_login_form/app.rb +0 -109
- data/examples/app_stateful_interaction/README.md +0 -35
- data/examples/app_stateful_interaction/app.rb +0 -328
- data/examples/timeout_demo.rb +0 -45
- data/examples/verify_quickstart_dsl/README.md +0 -55
- data/examples/verify_quickstart_dsl/app.rb +0 -49
- data/examples/verify_quickstart_layout/README.md +0 -77
- data/examples/verify_quickstart_layout/app.rb +0 -73
- data/examples/verify_quickstart_lifecycle/README.md +0 -68
- data/examples/verify_quickstart_lifecycle/app.rb +0 -62
- data/examples/verify_readme_usage/README.md +0 -49
- data/examples/verify_readme_usage/app.rb +0 -42
- data/examples/verify_website_managed/README.md +0 -48
- data/examples/verify_website_managed/app.rb +0 -36
- data/examples/verify_website_menu/README.md +0 -60
- data/examples/verify_website_menu/app.rb +0 -84
- data/examples/verify_website_spinner/README.md +0 -44
- data/examples/verify_website_spinner/app.rb +0 -34
- data/examples/widget_barchart/README.md +0 -58
- data/examples/widget_barchart/app.rb +0 -240
- data/examples/widget_block/README.md +0 -44
- data/examples/widget_block/app.rb +0 -258
- data/examples/widget_box/README.md +0 -54
- data/examples/widget_box/app.rb +0 -255
- data/examples/widget_calendar/README.md +0 -48
- data/examples/widget_calendar/app.rb +0 -115
- data/examples/widget_canvas/README.md +0 -31
- data/examples/widget_canvas/app.rb +0 -130
- data/examples/widget_cell/README.md +0 -45
- data/examples/widget_cell/app.rb +0 -112
- data/examples/widget_center/README.md +0 -33
- data/examples/widget_center/app.rb +0 -118
- data/examples/widget_chart/README.md +0 -50
- data/examples/widget_chart/app.rb +0 -220
- data/examples/widget_gauge/README.md +0 -50
- data/examples/widget_gauge/app.rb +0 -229
- data/examples/widget_layout_split/README.md +0 -53
- data/examples/widget_layout_split/app.rb +0 -260
- data/examples/widget_line_gauge/README.md +0 -50
- data/examples/widget_line_gauge/app.rb +0 -219
- data/examples/widget_list/README.md +0 -58
- data/examples/widget_list/app.rb +0 -384
- data/examples/widget_map/README.md +0 -48
- data/examples/widget_map/app.rb +0 -95
- data/examples/widget_overlay/README.md +0 -45
- data/examples/widget_overlay/app.rb +0 -250
- data/examples/widget_popup/README.md +0 -45
- data/examples/widget_popup/app.rb +0 -106
- data/examples/widget_ratatui_logo/README.md +0 -43
- data/examples/widget_ratatui_logo/app.rb +0 -104
- data/examples/widget_ratatui_mascot/README.md +0 -43
- data/examples/widget_ratatui_mascot/app.rb +0 -95
- data/examples/widget_rect/README.md +0 -53
- data/examples/widget_rect/app.rb +0 -222
- data/examples/widget_render/README.md +0 -46
- data/examples/widget_render/app.rb +0 -186
- data/examples/widget_render/app.rbs +0 -41
- data/examples/widget_rich_text/README.md +0 -44
- data/examples/widget_rich_text/app.rb +0 -193
- data/examples/widget_scroll_text/README.md +0 -46
- data/examples/widget_scroll_text/app.rb +0 -109
- data/examples/widget_scrollbar/README.md +0 -46
- data/examples/widget_scrollbar/app.rb +0 -155
- data/examples/widget_sparkline/README.md +0 -51
- data/examples/widget_sparkline/app.rb +0 -277
- data/examples/widget_style_colors/README.md +0 -43
- data/examples/widget_style_colors/app.rb +0 -83
- data/examples/widget_table/README.md +0 -57
- data/examples/widget_table/app.rb +0 -279
- data/examples/widget_tabs/README.md +0 -50
- data/examples/widget_tabs/app.rb +0 -183
- data/examples/widget_text_width/README.md +0 -44
- data/examples/widget_text_width/app.rb +0 -117
- data/migrate_to_buffer.rb +0 -145
- data/mise.toml +0 -8
- data/tasks/autodoc/examples.rb +0 -87
- data/tasks/autodoc/member.rb +0 -58
- data/tasks/autodoc/name.rb +0 -21
- data/tasks/autodoc.rake +0 -21
- data/tasks/bump/cargo_lockfile.rb +0 -21
- data/tasks/bump/changelog.rb +0 -47
- data/tasks/bump/header.rb +0 -32
- data/tasks/bump/history.rb +0 -32
- data/tasks/bump/links.rb +0 -69
- data/tasks/bump/manifest.rb +0 -33
- data/tasks/bump/ruby_gem.rb +0 -49
- data/tasks/bump/sem_ver.rb +0 -40
- data/tasks/bump/unreleased_section.rb +0 -56
- data/tasks/bump.rake +0 -51
- data/tasks/doc.rake +0 -887
- data/tasks/example_viewer.html.erb +0 -172
- data/tasks/extension.rake +0 -14
- data/tasks/license/headers_md.rb +0 -223
- data/tasks/license/headers_rb.rb +0 -210
- data/tasks/license/license_utils.rb +0 -130
- data/tasks/license/snippets_md.rb +0 -315
- data/tasks/license/snippets_rdoc.rb +0 -150
- data/tasks/license.rake +0 -91
- data/tasks/lint.rake +0 -170
- data/tasks/rdoc_config.rb +0 -29
- data/tasks/resources/build.yml.erb +0 -60
- data/tasks/resources/index.html.erb +0 -141
- data/tasks/resources/rubies.yml +0 -7
- data/tasks/sourcehut.rake +0 -110
- data/tasks/steep.rake +0 -11
- data/tasks/terminal_preview/app_screenshot.rb +0 -45
- data/tasks/terminal_preview/crash_report.rb +0 -54
- data/tasks/terminal_preview/example_app.rb +0 -27
- data/tasks/terminal_preview/launcher_script.rb +0 -48
- data/tasks/terminal_preview/preview_collection.rb +0 -60
- data/tasks/terminal_preview/preview_timing.rb +0 -24
- data/tasks/terminal_preview/safety_confirmation.rb +0 -58
- data/tasks/terminal_preview/saved_screenshot.rb +0 -56
- data/tasks/terminal_preview/system_appearance.rb +0 -13
- data/tasks/terminal_preview/terminal_window.rb +0 -138
- data/tasks/terminal_preview/window_id.rb +0 -16
- data/tasks/terminal_preview.rake +0 -30
- data/tasks/test.rake +0 -33
- data/tasks/website/index_page.rb +0 -30
- data/tasks/website/version.rb +0 -127
- data/tasks/website/version_menu.rb +0 -68
- data/tasks/website/versioned_documentation.rb +0 -83
- data/tasks/website/website.rb +0 -53
- data/tasks/website.rake +0 -28
|
@@ -1,647 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
-
|
|
4
|
-
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
5
|
-
-->
|
|
6
|
-
|
|
7
|
-
# Terminal Object Design Proposals
|
|
8
|
-
|
|
9
|
-
## Executive Summary
|
|
10
|
-
|
|
11
|
-
The current ratatui_ruby architecture abstracts Terminal functionality behind module-level methods (`RatatuiRuby.draw`, `RatatuiRuby.poll_event`) and the `TUI` facade class. This document proposes a design for introducing a proper `Terminal` object that aligns with upstream Ratatui's architecture while maintaining Ruby idioms, the established Mullet Architecture, and full backward compatibility.
|
|
12
|
-
|
|
13
|
-
## Background
|
|
14
|
-
|
|
15
|
-
### Current Architecture
|
|
16
|
-
|
|
17
|
-
**Module-Level API** (`RatatuiRuby` module):
|
|
18
|
-
- `init_terminal`, `restore_terminal`, `run`
|
|
19
|
-
- `draw`, `poll_event`, `get_cell_at`
|
|
20
|
-
- `get_viewport_area`, `get_terminal_size`
|
|
21
|
-
- `insert_before` (for inline viewports)
|
|
22
|
-
- `cursor_position`, `cursor_position=`
|
|
23
|
-
|
|
24
|
-
**Rust Implementation**:
|
|
25
|
-
- Global `TERMINAL` singleton wrapped in `Mutex<Option<TerminalWrapper>>`
|
|
26
|
-
- `TerminalWrapper` enum supporting `Crossterm` and `Test` backends
|
|
27
|
-
- Direct FFI methods exposed to Ruby
|
|
28
|
-
|
|
29
|
-
**Upstream Ratatui Terminal struct** provides:
|
|
30
|
-
- `new(backend)`, `with_options(backend, options)` — construction
|
|
31
|
-
- `draw(callback)` — frame rendering
|
|
32
|
-
- `get_frame()` — direct frame access
|
|
33
|
-
- `hide_cursor()`, `show_cursor()`, `get_cursor_position()`, `set_cursor_position()` — cursor control
|
|
34
|
-
- `clear()` — screen clearing
|
|
35
|
-
- `resize(area)`, `autoresize()` — size management
|
|
36
|
-
- `insert_before(height, draw_fn)` — inline viewport insertion
|
|
37
|
-
- `backend()`, `backend_mut()` — backend access
|
|
38
|
-
- `current_buffer_mut()` — buffer inspection
|
|
39
|
-
- `flush()`, `swap_buffers()` — low-level rendering control
|
|
40
|
-
|
|
41
|
-
### Design Principles (from ruby_frontend.md)
|
|
42
|
-
|
|
43
|
-
1. **Ratatui Alignment**: Ruby namespace mirrors Rust module hierarchy
|
|
44
|
-
2. **Two-Layer Architecture (Mullet)**:
|
|
45
|
-
- Layer 1: Explicit schema classes (`RatatuiRuby::Widgets::*`)
|
|
46
|
-
- Layer 2: Ergonomic DSL (`TUI` facade)
|
|
47
|
-
3. **Explicit Over Magic**: No runtime metaprogramming
|
|
48
|
-
4. **Data-Driven UI**: Immediate mode, immutable data structures
|
|
49
|
-
5. **Separation of Configuration and Status**: Widgets (input) vs State (output)
|
|
50
|
-
6. **No Render Logic in Ruby**: Ruby defines data, Rust renders
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
## Proposal 1: Terminal Class with Instance-Based API (Full Upstream Port)
|
|
55
|
-
|
|
56
|
-
### Overview
|
|
57
|
-
Create a proper `RatatuiRuby::Terminal` class that mirrors the upstream Ratatui Terminal struct. Maintains full backward compatibility via singleton delegation pattern.
|
|
58
|
-
|
|
59
|
-
### Design Philosophy: Rust-Side Object Construction
|
|
60
|
-
|
|
61
|
-
**Proposal 1 design decision**: FFI methods return fully-constructed Ruby objects, not raw primitives or hashes.
|
|
62
|
-
|
|
63
|
-
Using Magnus, Rust can instantiate Ruby classes directly:
|
|
64
|
-
- `_poll_event_instance(timeout)` returns `Event::Key`, `Event::Mouse`, `Event::None`, etc.
|
|
65
|
-
- `_viewport_area_instance()` returns `Layout::Rect`
|
|
66
|
-
- `_get_cell_at_instance(x, y)` returns `Buffer::Cell`
|
|
67
|
-
|
|
68
|
-
This keeps Terminal methods as **thin delegates** to Rust, with all object construction logic centralized in the FFI layer. Ruby code becomes cleaner and less error-prone.
|
|
69
|
-
|
|
70
|
-
**Current pattern** (returns hash):
|
|
71
|
-
<!-- SPDX-SnippetBegin -->
|
|
72
|
-
<!--
|
|
73
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
74
|
-
SPDX-License-Identifier: MIT-0
|
|
75
|
-
-->
|
|
76
|
-
```ruby
|
|
77
|
-
def poll_event(timeout: 0.016)
|
|
78
|
-
raw = _poll_event_instance(timeout)
|
|
79
|
-
return Event::None.new.freeze if raw.nil?
|
|
80
|
-
case raw[:type]
|
|
81
|
-
when :key then Event::Key.new(code: raw[:code], ...)
|
|
82
|
-
# ... 30 lines of parsing
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
```
|
|
86
|
-
<!-- SPDX-SnippetEnd -->
|
|
87
|
-
|
|
88
|
-
**Proposal 1 pattern** (returns object):
|
|
89
|
-
<!-- SPDX-SnippetBegin -->
|
|
90
|
-
<!--
|
|
91
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
92
|
-
SPDX-License-Identifier: MIT-0
|
|
93
|
-
-->
|
|
94
|
-
```ruby
|
|
95
|
-
def poll_event(timeout: 0.016)
|
|
96
|
-
_poll_event_instance(timeout) # Rust instantiates Event::Key, etc.
|
|
97
|
-
end
|
|
98
|
-
```
|
|
99
|
-
<!-- SPDX-SnippetEnd -->
|
|
100
|
-
|
|
101
|
-
### Implementation
|
|
102
|
-
|
|
103
|
-
<!-- SPDX-SnippetBegin -->
|
|
104
|
-
<!--
|
|
105
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
106
|
-
SPDX-License-Identifier: MIT-0
|
|
107
|
-
-->
|
|
108
|
-
```ruby
|
|
109
|
-
# lib/ratatui_ruby/terminal.rb
|
|
110
|
-
module RatatuiRuby
|
|
111
|
-
class Terminal
|
|
112
|
-
# Construction
|
|
113
|
-
def initialize(viewport: :fullscreen, height: nil)
|
|
114
|
-
@viewport = resolve_viewport(viewport, height)
|
|
115
|
-
_init_terminal_instance(@viewport.type.to_s, @viewport.height)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# Core rendering (thin delegate)
|
|
119
|
-
def draw(&block)
|
|
120
|
-
_draw_instance(&block)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Event polling (Rust returns Event objects)
|
|
124
|
-
def poll_event(timeout: 0.016)
|
|
125
|
-
_poll_event_instance(timeout)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# Cursor control
|
|
129
|
-
def hide_cursor
|
|
130
|
-
_hide_cursor_instance
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def show_cursor
|
|
134
|
-
_show_cursor_instance
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Cursor position (Rust returns Layout::Position)
|
|
138
|
-
def cursor_position
|
|
139
|
-
_get_cursor_position_instance
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def cursor_position=(position)
|
|
143
|
-
if position.is_a?(Array)
|
|
144
|
-
x, y = position
|
|
145
|
-
else
|
|
146
|
-
x, y = position.x, position.y
|
|
147
|
-
end
|
|
148
|
-
_set_cursor_position_instance(x, y)
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# Viewport operations
|
|
152
|
-
def insert_before(height, widget = nil, &block)
|
|
153
|
-
content = widget || block&.call
|
|
154
|
-
_insert_before_instance(height, content)
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# Viewport queries (Rust returns Layout::Rect)
|
|
158
|
-
def viewport_area
|
|
159
|
-
_get_viewport_area_instance
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def terminal_size
|
|
163
|
-
_get_terminal_size_instance
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Screen management
|
|
167
|
-
def clear
|
|
168
|
-
_clear_instance
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def resize(width, height)
|
|
172
|
-
_resize_instance(width, height)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def autoresize
|
|
176
|
-
_autoresize_instance
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Buffer inspection (Rust returns Buffer::Cell)
|
|
180
|
-
def get_cell_at(x, y)
|
|
181
|
-
_get_cell_at_instance(x, y)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Low-level rendering control
|
|
185
|
-
def flush
|
|
186
|
-
_flush_instance
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def swap_buffers
|
|
190
|
-
_swap_buffers_instance
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# Backend access (advanced)
|
|
194
|
-
def backend
|
|
195
|
-
_backend_instance
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def backend_mut
|
|
199
|
-
_backend_mut_instance
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
# Lifecycle
|
|
203
|
-
def restore
|
|
204
|
-
_restore_terminal_instance
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
private
|
|
208
|
-
|
|
209
|
-
def resolve_viewport(viewport, height)
|
|
210
|
-
case viewport
|
|
211
|
-
when nil, :fullscreen then Terminal::Viewport.fullscreen
|
|
212
|
-
when :inline then Terminal::Viewport.inline(height || 8)
|
|
213
|
-
when Terminal::Viewport then viewport
|
|
214
|
-
else raise ArgumentError, "Unknown viewport: #{viewport.inspect}"
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
# Module-level singleton for backward compatibility
|
|
220
|
-
@default_terminal = nil
|
|
221
|
-
|
|
222
|
-
class << self
|
|
223
|
-
private
|
|
224
|
-
|
|
225
|
-
def default_terminal
|
|
226
|
-
@default_terminal ||= Terminal.new
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
# Existing module methods delegate to singleton (BACKWARD COMPATIBLE)
|
|
231
|
-
def self.draw(&block)
|
|
232
|
-
default_terminal.draw(&block)
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def self.poll_event(timeout: 0.016)
|
|
236
|
-
default_terminal.poll_event(timeout:)
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def self.get_cell_at(x, y)
|
|
240
|
-
default_terminal.get_cell_at(x, y)
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def self.get_viewport_area
|
|
244
|
-
default_terminal.viewport_area
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def self.get_terminal_size
|
|
248
|
-
default_terminal.terminal_size
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def self.insert_before(height, widget = nil, &block)
|
|
252
|
-
default_terminal.insert_before(height, widget, &block)
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# ... (all other module methods delegate similarly)
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
# TUI facade delegates to the Terminal instance passed to it
|
|
259
|
-
class TUI
|
|
260
|
-
def initialize(terminal)
|
|
261
|
-
@terminal = terminal
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
# Expose the terminal instance
|
|
265
|
-
attr_reader :terminal
|
|
266
|
-
|
|
267
|
-
# Delegate core operations to terminal
|
|
268
|
-
def draw(&block)
|
|
269
|
-
@terminal.draw(&block)
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def poll_event(timeout: 0.016)
|
|
273
|
-
@terminal.poll_event(timeout:)
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
# ... (all existing TUI factory methods remain unchanged)
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
# run creates a Terminal and yields a TUI wrapping it
|
|
280
|
-
def self.run(viewport: :fullscreen, height: nil, &block)
|
|
281
|
-
terminal = Terminal.new(viewport:, height:)
|
|
282
|
-
tui = TUI.new(terminal)
|
|
283
|
-
yield tui
|
|
284
|
-
ensure
|
|
285
|
-
terminal.restore
|
|
286
|
-
end
|
|
287
|
-
```
|
|
288
|
-
<!-- SPDX-SnippetEnd -->
|
|
289
|
-
|
|
290
|
-
### Usage: Three APIs (Deprecation Plan)
|
|
291
|
-
|
|
292
|
-
<!-- SPDX-SnippetBegin -->
|
|
293
|
-
<!--
|
|
294
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
295
|
-
SPDX-License-Identifier: MIT-0
|
|
296
|
-
-->
|
|
297
|
-
```ruby
|
|
298
|
-
# API 1: Beginner-friendly TUI facade (PERMANENT)
|
|
299
|
-
# This is the recommended API for most users
|
|
300
|
-
RatatuiRuby.run do |tui|
|
|
301
|
-
tui.draw { |frame| ... }
|
|
302
|
-
event = tui.poll_event
|
|
303
|
-
|
|
304
|
-
# Access the underlying Terminal instance
|
|
305
|
-
puts tui.terminal.viewport_type # :inline or :fullscreen
|
|
306
|
-
puts tui.terminal.width # terminal width
|
|
307
|
-
tui.terminal.clear if some_condition
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
# API 2: Direct module methods (DEPRECATED, remove before v1.0.0)
|
|
311
|
-
# This API should never have existed and will be removed
|
|
312
|
-
RatatuiRuby.init_terminal
|
|
313
|
-
RatatuiRuby.draw { |frame| ... }
|
|
314
|
-
event = RatatuiRuby.poll_event
|
|
315
|
-
RatatuiRuby.restore_terminal
|
|
316
|
-
|
|
317
|
-
# API 3: Explicit Terminal instance (PERMANENT)
|
|
318
|
-
# This is aligned with upstream Ratatui and provides explicit resource management
|
|
319
|
-
terminal = RatatuiRuby::Terminal.new(viewport: :inline, height: 10)
|
|
320
|
-
terminal.draw { |frame| ... }
|
|
321
|
-
event = terminal.poll_event
|
|
322
|
-
terminal.restore
|
|
323
|
-
```
|
|
324
|
-
<!-- SPDX-SnippetEnd -->
|
|
325
|
-
|
|
326
|
-
**Migration strategy**:
|
|
327
|
-
- **API 1** (`RatatuiRuby.run { |tui| }`) remains the primary, beginner-friendly interface
|
|
328
|
-
- **API 3** (`Terminal.new`) is the advanced, upstream-aligned interface for explicit control
|
|
329
|
-
- **API 2** (module methods like `RatatuiRuby.draw`) exists only for backward compatibility during transition
|
|
330
|
-
- Add deprecation warnings immediately after implementing Terminal
|
|
331
|
-
- Remove completely before v1.0.0 release
|
|
332
|
-
|
|
333
|
-
### Rust Changes
|
|
334
|
-
|
|
335
|
-
The Rust implementation needs significant refactoring to support instance-based terminals:
|
|
336
|
-
|
|
337
|
-
**Current Architecture**:
|
|
338
|
-
<!-- SPDX-SnippetBegin -->
|
|
339
|
-
<!--
|
|
340
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
341
|
-
SPDX-License-Identifier: MIT-0
|
|
342
|
-
-->
|
|
343
|
-
```rust
|
|
344
|
-
pub static TERMINAL: Mutex<Option<TerminalWrapper>> = Mutex::new(None);
|
|
345
|
-
```
|
|
346
|
-
<!-- SPDX-SnippetEnd -->
|
|
347
|
-
|
|
348
|
-
**New Architecture**:
|
|
349
|
-
<!-- SPDX-SnippetBegin -->
|
|
350
|
-
<!--
|
|
351
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
352
|
-
SPDX-License-Identifier: MIT-0
|
|
353
|
-
-->
|
|
354
|
-
```rust
|
|
355
|
-
// Instance tracking
|
|
356
|
-
static TERMINAL_INSTANCES: Mutex<HashMap<u64, TerminalWrapper>> = Mutex::new(HashMap::new());
|
|
357
|
-
static NEXT_TERMINAL_ID: AtomicU64 = AtomicU64::new(0);
|
|
358
|
-
|
|
359
|
-
pub fn init_terminal_instance(viewport_type: String, height: Option<u16>) -> Result<u64, Error> {
|
|
360
|
-
let id = NEXT_TERMINAL_ID.fetch_add(1, Ordering::SeqCst);
|
|
361
|
-
let terminal = create_terminal(viewport_type, height)?;
|
|
362
|
-
|
|
363
|
-
let mut instances = TERMINAL_INSTANCES.lock().unwrap();
|
|
364
|
-
instances.insert(id, terminal);
|
|
365
|
-
Ok(id)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
pub fn draw_instance(terminal_id: u64, callback: Value) -> Result<(), Error> {
|
|
369
|
-
let mut instances = TERMINAL_INSTANCES.lock().unwrap();
|
|
370
|
-
let terminal = instances.get_mut(&terminal_id)
|
|
371
|
-
.ok_or_else(|| Error::new(error_class, "Terminal instance not found"))?;
|
|
372
|
-
|
|
373
|
-
match terminal {
|
|
374
|
-
TerminalWrapper::Crossterm(term) => term.draw(|frame| { ... }),
|
|
375
|
-
TerminalWrapper::Test(term) => term.draw(|frame| { ... }),
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
```
|
|
379
|
-
<!-- SPDX-SnippetEnd -->
|
|
380
|
-
|
|
381
|
-
**Module-level methods maintain singleton**:
|
|
382
|
-
<!-- SPDX-SnippetBegin -->
|
|
383
|
-
<!--
|
|
384
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
385
|
-
SPDX-License-Identifier: MIT-0
|
|
386
|
-
-->
|
|
387
|
-
```rust
|
|
388
|
-
static DEFAULT_TERMINAL_ID: Mutex<Option<u64>> = Mutex::new(None);
|
|
389
|
-
|
|
390
|
-
pub fn init_terminal(viewport_type: String, height: Option<u16>) -> Result<(), Error> {
|
|
391
|
-
let id = init_terminal_instance(viewport_type, height)?;
|
|
392
|
-
*DEFAULT_TERMINAL_ID.lock().unwrap() = Some(id);
|
|
393
|
-
Ok(())
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
pub fn draw(callback: Value) -> Result<(), Error> {
|
|
397
|
-
let default_id = DEFAULT_TERMINAL_ID.lock().unwrap()
|
|
398
|
-
.ok_or_else(|| Error::new(error_class, "No default terminal initialized"))?;
|
|
399
|
-
draw_instance(default_id, callback)
|
|
400
|
-
}
|
|
401
|
-
```
|
|
402
|
-
<!-- SPDX-SnippetEnd -->
|
|
403
|
-
|
|
404
|
-
**Rust-side object instantiation** (Magnus):
|
|
405
|
-
<!-- SPDX-SnippetBegin -->
|
|
406
|
-
<!--
|
|
407
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
408
|
-
SPDX-License-Identifier: MIT-0
|
|
409
|
-
-->
|
|
410
|
-
```rust
|
|
411
|
-
pub fn poll_event_instance(terminal_id: u64, timeout: Option<f64>) -> Result<Value, Error> {
|
|
412
|
-
let ruby = magnus::Ruby::get().unwrap();
|
|
413
|
-
let module = ruby.define_module("RatatuiRuby")?;
|
|
414
|
-
let event_module = module.const_get::<_, RModule>("Event")?;
|
|
415
|
-
|
|
416
|
-
match crossterm::event::poll(timeout_duration)? {
|
|
417
|
-
true => match crossterm::event::read()? {
|
|
418
|
-
CrosstermEvent::Key(key_event) => {
|
|
419
|
-
let key_class = event_module.const_get::<_, RClass>("Key")?;
|
|
420
|
-
key_class.new_instance((
|
|
421
|
-
("code", extract_key_code(key_event.code)),
|
|
422
|
-
("modifiers", extract_modifiers(key_event.modifiers)),
|
|
423
|
-
("kind", extract_kind(key_event.kind)),
|
|
424
|
-
))?.freeze()
|
|
425
|
-
},
|
|
426
|
-
CrosstermEvent::Mouse(mouse_event) => {
|
|
427
|
-
let mouse_class = event_module.const_get::<_, RClass>("Mouse")?;
|
|
428
|
-
mouse_class.new_instance((
|
|
429
|
-
("kind", extract_mouse_kind(mouse_event.kind)),
|
|
430
|
-
("x", mouse_event.column),
|
|
431
|
-
("y", mouse_event.row),
|
|
432
|
-
("button", extract_mouse_button(mouse_event.kind)),
|
|
433
|
-
("modifiers", extract_modifiers(mouse_event.modifiers)),
|
|
434
|
-
))?.freeze()
|
|
435
|
-
},
|
|
436
|
-
// ... other event types
|
|
437
|
-
},
|
|
438
|
-
false => {
|
|
439
|
-
let none_class = event_module.const_get::<_, RClass>("None")?;
|
|
440
|
-
none_class.new_instance(())?.freeze()
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Similar pattern for Layout::Rect, Buffer::Cell, etc.
|
|
446
|
-
pub fn get_viewport_area_instance(terminal_id: u64) -> Result<Value, Error> {
|
|
447
|
-
let ruby = magnus::Ruby::get().unwrap();
|
|
448
|
-
let module = ruby.define_module("RatatuiRuby")?;
|
|
449
|
-
let layout_module = module.const_get::<_, RModule>("Layout")?;
|
|
450
|
-
let rect_class = layout_module.const_get::<_, RClass>("Rect")?;
|
|
451
|
-
|
|
452
|
-
let area = get_terminal_viewport_area(terminal_id)?;
|
|
453
|
-
rect_class.new_instance((
|
|
454
|
-
("x", area.x),
|
|
455
|
-
("y", area.y),
|
|
456
|
-
("width", area.width),
|
|
457
|
-
("height", area.height),
|
|
458
|
-
))
|
|
459
|
-
}
|
|
460
|
-
```
|
|
461
|
-
<!-- SPDX-SnippetEnd -->
|
|
462
|
-
|
|
463
|
-
### Pros
|
|
464
|
-
|
|
465
|
-
✅ **Full alignment with upstream Ratatui** — Terminal is a first-class object
|
|
466
|
-
✅ **Explicit ownership model** — terminal is a managed resource
|
|
467
|
-
✅ **Supports multiple terminals** (future: testing, headless)
|
|
468
|
-
✅ **Clear lifecycle** — `new`, `draw`, `restore`
|
|
469
|
-
✅ **Non-breaking** — all existing APIs continue to work via singleton delegation
|
|
470
|
-
✅ **Three API tiers** — beginner (TUI), intermediate (module), advanced (instance)
|
|
471
|
-
✅ **MVU-compatible** — `Command.tui(->(tui) { tui.terminal.clear })` provides escape hatch for terminal operations while maintaining Update purity
|
|
472
|
-
|
|
473
|
-
### Cons
|
|
474
|
-
|
|
475
|
-
❌ **Significant Rust refactoring** — requires instance tracking, ID management, and both instance-based FFI methods (`_draw_instance`) and singleton-delegating methods (`_draw`). The global `TERMINAL` mutex must be replaced with an instance registry (`HashMap<u64, TerminalWrapper>`), adding complexity to every terminal operation.
|
|
476
|
-
|
|
477
|
-
---
|
|
478
|
-
|
|
479
|
-
## Verification Plan
|
|
480
|
-
|
|
481
|
-
### Unit Tests
|
|
482
|
-
|
|
483
|
-
<!-- SPDX-SnippetBegin -->
|
|
484
|
-
<!--
|
|
485
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
486
|
-
SPDX-License-Identifier: MIT-0
|
|
487
|
-
-->
|
|
488
|
-
```ruby
|
|
489
|
-
# test/test_terminal.rb
|
|
490
|
-
class TestTerminal < Minitest::Test
|
|
491
|
-
def setup
|
|
492
|
-
RatatuiRuby.init_test_terminal(80, 24, "fullscreen")
|
|
493
|
-
end
|
|
494
|
-
|
|
495
|
-
def teardown
|
|
496
|
-
RatatuiRuby.restore_terminal
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
def test_terminal_instance_creation
|
|
500
|
-
terminal = RatatuiRuby::Terminal.new
|
|
501
|
-
assert_instance_of RatatuiRuby::Terminal, terminal
|
|
502
|
-
end
|
|
503
|
-
|
|
504
|
-
def test_terminal_size
|
|
505
|
-
terminal = RatatuiRuby::Terminal.new
|
|
506
|
-
size = terminal.terminal_size
|
|
507
|
-
assert_equal 80, size.width
|
|
508
|
-
assert_equal 24, size.height
|
|
509
|
-
end
|
|
510
|
-
|
|
511
|
-
def test_viewport_type_fullscreen
|
|
512
|
-
terminal = RatatuiRuby::Terminal.new
|
|
513
|
-
assert_equal :fullscreen, terminal.viewport_type
|
|
514
|
-
end
|
|
515
|
-
|
|
516
|
-
def test_tui_terminal_access
|
|
517
|
-
RatatuiRuby.run do |tui|
|
|
518
|
-
assert_instance_of RatatuiRuby::Terminal, tui.terminal
|
|
519
|
-
end
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
def test_backward_compatible_module_methods
|
|
523
|
-
# Module methods still work via singleton delegation
|
|
524
|
-
RatatuiRuby.draw { |frame| frame.render_widget(...) }
|
|
525
|
-
event = RatatuiRuby.poll_event
|
|
526
|
-
assert_instance_of RatatuiRuby::Event::None, event
|
|
527
|
-
end
|
|
528
|
-
end
|
|
529
|
-
|
|
530
|
-
# test/test_terminal_inline.rb
|
|
531
|
-
class TestTerminalInline < Minitest::Test
|
|
532
|
-
def setup
|
|
533
|
-
RatatuiRuby.init_test_terminal(80, 24, "inline", 8)
|
|
534
|
-
end
|
|
535
|
-
|
|
536
|
-
def teardown
|
|
537
|
-
RatatuiRuby.restore_terminal
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
def test_inline_viewport
|
|
541
|
-
terminal = RatatuiRuby::Terminal.new(viewport: :inline, height: 8)
|
|
542
|
-
assert_equal :inline, terminal.viewport_type
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
def test_insert_before_works_inline
|
|
546
|
-
terminal = RatatuiRuby::Terminal.new(viewport: :inline, height: 8)
|
|
547
|
-
terminal.insert_before(1, RatatuiRuby::Widgets::Paragraph.new(text: "test"))
|
|
548
|
-
# Should not raise
|
|
549
|
-
end
|
|
550
|
-
end
|
|
551
|
-
```
|
|
552
|
-
<!-- SPDX-SnippetEnd -->
|
|
553
|
-
|
|
554
|
-
### Integration Tests
|
|
555
|
-
|
|
556
|
-
<!-- SPDX-SnippetBegin -->
|
|
557
|
-
<!--
|
|
558
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
559
|
-
SPDX-License-Identifier: MIT-0
|
|
560
|
-
-->
|
|
561
|
-
```ruby
|
|
562
|
-
# examples/terminal_instance_demo.rb
|
|
563
|
-
require "ratatui_ruby"
|
|
564
|
-
|
|
565
|
-
# API 3: Direct Terminal instance
|
|
566
|
-
terminal = RatatuiRuby::Terminal.new(viewport: :inline, height: 10)
|
|
567
|
-
|
|
568
|
-
terminal.draw do |frame|
|
|
569
|
-
info = <<~INFO
|
|
570
|
-
Terminal Information:
|
|
571
|
-
- Type: #{terminal.viewport_type}
|
|
572
|
-
- Size: #{terminal.width}x#{terminal.height}
|
|
573
|
-
|
|
574
|
-
Press 'q' to quit
|
|
575
|
-
INFO
|
|
576
|
-
|
|
577
|
-
paragraph = RatatuiRuby::Widgets::Paragraph.new(text: info)
|
|
578
|
-
frame.render_widget(paragraph, frame.area)
|
|
579
|
-
end
|
|
580
|
-
|
|
581
|
-
# Insert log above viewport
|
|
582
|
-
terminal.insert_before(1, RatatuiRuby::Widgets::Paragraph.new(text: "[LOG] Started"))
|
|
583
|
-
|
|
584
|
-
loop do
|
|
585
|
-
event = terminal.poll_event
|
|
586
|
-
break if event.key? && event.code == "q"
|
|
587
|
-
end
|
|
588
|
-
|
|
589
|
-
terminal.restore
|
|
590
|
-
```
|
|
591
|
-
<!-- SPDX-SnippetEnd -->
|
|
592
|
-
|
|
593
|
-
### Manual Testing Checklist
|
|
594
|
-
|
|
595
|
-
- [ ] `Terminal.new` creates new instance
|
|
596
|
-
- [ ] `terminal.width` and `terminal.height` match expected dimensions
|
|
597
|
-
- [ ] `terminal.viewport_type` returns correct mode
|
|
598
|
-
- [ ] `tui.terminal` provides access from TUI facade
|
|
599
|
-
- [ ] `terminal.insert_before` works in inline mode
|
|
600
|
-
- [ ] `terminal.poll_event` returns Event objects
|
|
601
|
-
- [ ] `terminal.cursor_position` returns Position object
|
|
602
|
-
- [ ] Module methods (`RatatuiRuby.draw`) still work (backward compat)
|
|
603
|
-
- [ ] Deprecation warnings appear for module methods
|
|
604
|
-
|
|
605
|
-
---
|
|
606
|
-
|
|
607
|
-
## Future Enhancements
|
|
608
|
-
|
|
609
|
-
### Command.tui Integration
|
|
610
|
-
|
|
611
|
-
Implement the MVU escape hatch:
|
|
612
|
-
|
|
613
|
-
<!-- SPDX-SnippetBegin -->
|
|
614
|
-
<!--
|
|
615
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
616
|
-
SPDX-License-Identifier: MIT-0
|
|
617
|
-
-->
|
|
618
|
-
```ruby
|
|
619
|
-
module RatatuiRuby
|
|
620
|
-
module Tea
|
|
621
|
-
module Command
|
|
622
|
-
def self.tui(callable)
|
|
623
|
-
TuiCommand.new(callable)
|
|
624
|
-
end
|
|
625
|
-
|
|
626
|
-
class TuiCommand < Data.define(:callable)
|
|
627
|
-
def execute(tui)
|
|
628
|
-
callable.call(tui)
|
|
629
|
-
nil
|
|
630
|
-
end
|
|
631
|
-
end
|
|
632
|
-
end
|
|
633
|
-
end
|
|
634
|
-
end
|
|
635
|
-
|
|
636
|
-
# Usage in Update
|
|
637
|
-
def update(model, message)
|
|
638
|
-
case message
|
|
639
|
-
when clear_requested
|
|
640
|
-
[model, Command.tui(->(tui) { tui.terminal.clear })]
|
|
641
|
-
end
|
|
642
|
-
end
|
|
643
|
-
```
|
|
644
|
-
<!-- SPDX-SnippetEnd -->
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|