ratatui_ruby 1.1.0 → 1.1.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 -255
- 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 -147
- data/CHANGELOG.md +0 -736
- data/README.md +0 -187
- data/README.rdoc +0 -302
- data/Rakefile +0 -11
- data/Steepfile +0 -50
- 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 -448
- data/doc/contributors/design/rust_backend.md +0 -434
- 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/releasing.md +0 -215
- data/doc/contributors/todo/align/api_completeness_audit-finished.md +0 -381
- data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -200
- data/doc/contributors/todo/align/term.md +0 -351
- data/doc/contributors/todo/align/terminal.md +0 -647
- data/doc/contributors/todo/future_work.md +0 -169
- data/doc/contributors/upstream_requests/paragraph_span_rects.md +0 -259
- 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_external_editor.gif +0 -0
- data/doc/images/app_login_form.png +0 -0
- data/doc/images/app_stateful_interaction.png +0 -0
- data/doc/images/verify_quickstart_dsl.png +0 -0
- data/doc/images/verify_quickstart_layout.png +0 -0
- data/doc/images/verify_quickstart_lifecycle.png +0 -0
- data/doc/images/verify_readme_usage.png +0 -0
- data/doc/images/widget_barchart.png +0 -0
- data/doc/images/widget_block.png +0 -0
- data/doc/images/widget_box.png +0 -0
- data/doc/images/widget_calendar.png +0 -0
- data/doc/images/widget_canvas.png +0 -0
- data/doc/images/widget_cell.png +0 -0
- data/doc/images/widget_center.png +0 -0
- data/doc/images/widget_chart.png +0 -0
- data/doc/images/widget_gauge.png +0 -0
- data/doc/images/widget_layout_split.png +0 -0
- data/doc/images/widget_line_gauge.png +0 -0
- data/doc/images/widget_list.png +0 -0
- data/doc/images/widget_map.png +0 -0
- data/doc/images/widget_overlay.png +0 -0
- data/doc/images/widget_popup.png +0 -0
- data/doc/images/widget_ratatui_logo.png +0 -0
- data/doc/images/widget_ratatui_mascot.png +0 -0
- data/doc/images/widget_rect.png +0 -0
- data/doc/images/widget_render.png +0 -0
- data/doc/images/widget_rich_text.png +0 -0
- data/doc/images/widget_scroll_text.png +0 -0
- data/doc/images/widget_scrollbar.png +0 -0
- data/doc/images/widget_sparkline.png +0 -0
- data/doc/images/widget_style_colors.png +0 -0
- data/doc/images/widget_table.png +0 -0
- data/doc/images/widget_tabs.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/index.md +0 -34
- 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_external_editor/README.md +0 -62
- data/examples/app_external_editor/app.rb +0 -344
- 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 -382
- 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 -285
- 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/bump_workflow.rb +0 -49
- data/tasks/bump/cargo_lockfile.rb +0 -21
- data/tasks/bump/changelog.rb +0 -104
- 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/patch_release.rb +0 -19
- data/tasks/bump/release_branch.rb +0 -17
- data/tasks/bump/release_from_trunk.rb +0 -49
- data/tasks/bump/repository.rb +0 -54
- data/tasks/bump/ruby_gem.rb +0 -29
- data/tasks/bump/sem_ver.rb +0 -44
- data/tasks/bump/unreleased_section.rb +0 -73
- data/tasks/bump.rake +0 -61
- data/tasks/doc/documentation.rb +0 -59
- data/tasks/doc/link/file_url.rb +0 -30
- data/tasks/doc/link/relative_path.rb +0 -61
- data/tasks/doc/link/web_url.rb +0 -55
- data/tasks/doc/link.rb +0 -52
- data/tasks/doc/link_audit.rb +0 -116
- data/tasks/doc/problem.rb +0 -40
- data/tasks/doc/source_file.rb +0 -93
- data/tasks/doc.rake +0 -905
- 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/rbs_predicates/predicate_catalog.rb +0 -52
- data/tasks/rbs_predicates/predicate_tests.rb +0 -124
- data/tasks/rbs_predicates/rbs_signature.rb +0 -63
- data/tasks/rbs_predicates.rake +0 -31
- 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 -36
- data/tasks/website/index_page.rb +0 -30
- data/tasks/website/version.rb +0 -122
- 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,344 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
#--
|
|
4
|
-
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
-
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
-
#++
|
|
7
|
-
|
|
8
|
-
$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
|
|
9
|
-
require "ratatui_ruby"
|
|
10
|
-
require "tempfile"
|
|
11
|
-
|
|
12
|
-
# Disable experimental warnings for line_count
|
|
13
|
-
RatatuiRuby.experimental_warnings = false
|
|
14
|
-
|
|
15
|
-
# External Editor Example
|
|
16
|
-
#
|
|
17
|
-
# Demonstrates full lifecycle control for temporarily exiting the TUI
|
|
18
|
-
# to invoke an external editor, then re-entering—like editing a commit
|
|
19
|
-
# message in lazygit.
|
|
20
|
-
#
|
|
21
|
-
# This example uses the low-level API (init_terminal/restore_terminal)
|
|
22
|
-
# instead of RatatuiRuby.run, which is required when you need to
|
|
23
|
-
# repeatedly enter and exit raw mode during a session.
|
|
24
|
-
#
|
|
25
|
-
# Features:
|
|
26
|
-
# - `e` edits this example's README.md
|
|
27
|
-
# - `s` edits a scratch file; saved content appears in a split pane
|
|
28
|
-
# - Focus-aware scrolling with visual indicators
|
|
29
|
-
# - Mouse scroll on hover
|
|
30
|
-
class AppExternalEditor
|
|
31
|
-
README_PATH = File.expand_path("README.md", __dir__)
|
|
32
|
-
|
|
33
|
-
def initialize
|
|
34
|
-
@tui = RatatuiRuby::TUI.new
|
|
35
|
-
@readme_scroll = 0
|
|
36
|
-
@scratch_scroll = 0
|
|
37
|
-
@readme_content = File.read(README_PATH)
|
|
38
|
-
@scratch = Tempfile.new(["scratch", ".md"])
|
|
39
|
-
@scratch_content = nil
|
|
40
|
-
@focus = :readme
|
|
41
|
-
|
|
42
|
-
# Cached geometry (set during calculate_layout)
|
|
43
|
-
@readme_area = nil
|
|
44
|
-
@scratch_area = nil
|
|
45
|
-
@readme_page_height = 10
|
|
46
|
-
@scratch_page_height = 10
|
|
47
|
-
@readme_line_count = 0
|
|
48
|
-
@scratch_line_count = 0
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def run
|
|
52
|
-
loop do
|
|
53
|
-
action = tui_session
|
|
54
|
-
case action
|
|
55
|
-
when :quit then break
|
|
56
|
-
when :edit_readme then edit_file(README_PATH) { reload_readme }
|
|
57
|
-
when :edit_scratch then edit_file(@scratch.path) { reload_scratch }
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
ensure
|
|
61
|
-
@scratch.unlink
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
private def tui_session
|
|
65
|
-
RatatuiRuby.init_terminal
|
|
66
|
-
begin
|
|
67
|
-
loop do
|
|
68
|
-
@tui.draw do |frame|
|
|
69
|
-
calculate_layout(frame.area) # Phase 1: Geometry (once per frame)
|
|
70
|
-
render(frame) # Phase 2: Draw
|
|
71
|
-
end
|
|
72
|
-
action = handle_input # Phase 3: Input (uses cached geometry)
|
|
73
|
-
return action if action # Early return triggers ensure block
|
|
74
|
-
end
|
|
75
|
-
ensure
|
|
76
|
-
RatatuiRuby.restore_terminal # Always runs, even on early return
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Open an external editor on the given file.
|
|
81
|
-
#
|
|
82
|
-
# Note: The terminal is already restored when this method is called!
|
|
83
|
-
# The tui_session method's ensure block handles restore_terminal before
|
|
84
|
-
# we get here. After the editor closes, the run loop calls tui_session
|
|
85
|
-
# again which does init_terminal. This pattern avoids explicit
|
|
86
|
-
# save/restore calls at every handoff point.
|
|
87
|
-
private def edit_file(path)
|
|
88
|
-
editor = ENV.fetch("EDITOR", "vi")
|
|
89
|
-
system(editor, path)
|
|
90
|
-
yield if block_given?
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Phase 1: Calculate layout and cache all geometry
|
|
94
|
-
private def calculate_layout(area)
|
|
95
|
-
content_area, @controls_area = @tui.layout_split(
|
|
96
|
-
area,
|
|
97
|
-
direction: :vertical,
|
|
98
|
-
constraints: [
|
|
99
|
-
@tui.constraint_fill(1),
|
|
100
|
-
@tui.constraint_length(3),
|
|
101
|
-
]
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
if split_view?
|
|
105
|
-
@readme_area, @scratch_area = @tui.layout_split(
|
|
106
|
-
content_area,
|
|
107
|
-
direction: :horizontal,
|
|
108
|
-
constraints: [
|
|
109
|
-
@tui.constraint_percentage(50),
|
|
110
|
-
@tui.constraint_percentage(50),
|
|
111
|
-
]
|
|
112
|
-
)
|
|
113
|
-
else
|
|
114
|
-
@readme_area = content_area
|
|
115
|
-
@scratch_area = nil
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# Calculate wrapped line counts and page heights
|
|
119
|
-
calculate_readme_geometry
|
|
120
|
-
calculate_scratch_geometry if split_view?
|
|
121
|
-
|
|
122
|
-
# Clamp scroll positions now that we have accurate geometry
|
|
123
|
-
clamp_scroll_positions
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
private def calculate_readme_geometry
|
|
127
|
-
block = @tui.block(borders: [:all])
|
|
128
|
-
inner = block.inner(@readme_area)
|
|
129
|
-
@readme_page_height = inner.height
|
|
130
|
-
|
|
131
|
-
paragraph = @tui.paragraph(text: @readme_content, wrap: true, block:)
|
|
132
|
-
@readme_line_count = paragraph.line_count(inner.width)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
private def calculate_scratch_geometry
|
|
136
|
-
return unless @scratch_area
|
|
137
|
-
|
|
138
|
-
block = @tui.block(borders: [:all])
|
|
139
|
-
inner = block.inner(@scratch_area)
|
|
140
|
-
@scratch_page_height = inner.height
|
|
141
|
-
|
|
142
|
-
paragraph = @tui.paragraph(text: @scratch_content || "", wrap: true, block:)
|
|
143
|
-
@scratch_line_count = paragraph.line_count(inner.width)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
private def clamp_scroll_positions
|
|
147
|
-
@readme_scroll = @readme_scroll.clamp(0, max_readme_scroll)
|
|
148
|
-
@scratch_scroll = @scratch_scroll.clamp(0, max_scratch_scroll) if split_view?
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# Phase 2: Render using cached geometry
|
|
152
|
-
private def render(frame)
|
|
153
|
-
render_readme(frame)
|
|
154
|
-
render_scratch(frame) if split_view?
|
|
155
|
-
render_controls(frame)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
private def split_view? = @scratch_content && !@scratch_content.strip.empty?
|
|
159
|
-
|
|
160
|
-
private def render_readme(frame)
|
|
161
|
-
focused = @focus == :readme
|
|
162
|
-
paragraph = @tui.paragraph(
|
|
163
|
-
text: @readme_content,
|
|
164
|
-
scroll: [@readme_scroll, 0],
|
|
165
|
-
wrap: true,
|
|
166
|
-
block: @tui.block(
|
|
167
|
-
title: "README.md (#{@readme_scroll + 1}-#{[@readme_scroll + @readme_page_height, @readme_line_count].min}/#{@readme_line_count})",
|
|
168
|
-
borders: [:all],
|
|
169
|
-
border_style: focused ? { fg: "cyan" } : { fg: "dark_gray" }
|
|
170
|
-
)
|
|
171
|
-
)
|
|
172
|
-
frame.render_widget(paragraph, @readme_area)
|
|
173
|
-
|
|
174
|
-
# Stateful scrollbar with viewport_content_length
|
|
175
|
-
# content_length = max_scroll + 1, so position=max_scroll => content_length-1 => thumb at bottom
|
|
176
|
-
scrollbar = @tui.scrollbar(content_length: 0, position: 0, orientation: :vertical, track_symbol: nil)
|
|
177
|
-
state = RatatuiRuby::ScrollbarState.new(max_readme_scroll + 1)
|
|
178
|
-
state.position = @readme_scroll
|
|
179
|
-
state.viewport_content_length = @readme_page_height
|
|
180
|
-
frame.render_stateful_widget(scrollbar, @readme_area, state)
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
private def render_scratch(frame)
|
|
184
|
-
focused = @focus == :scratch
|
|
185
|
-
paragraph = @tui.paragraph(
|
|
186
|
-
text: @scratch_content,
|
|
187
|
-
scroll: [@scratch_scroll, 0],
|
|
188
|
-
wrap: true,
|
|
189
|
-
block: @tui.block(
|
|
190
|
-
title: "Scratch (#{@scratch_scroll + 1}-#{[@scratch_scroll + @scratch_page_height, @scratch_line_count].min}/#{@scratch_line_count})",
|
|
191
|
-
borders: [:all],
|
|
192
|
-
border_style: focused ? { fg: "yellow" } : { fg: "dark_gray" }
|
|
193
|
-
)
|
|
194
|
-
)
|
|
195
|
-
frame.render_widget(paragraph, @scratch_area)
|
|
196
|
-
|
|
197
|
-
# Stateful scrollbar with viewport_content_length
|
|
198
|
-
scrollbar = @tui.scrollbar(content_length: 0, position: 0, orientation: :vertical, track_symbol: nil)
|
|
199
|
-
state = RatatuiRuby::ScrollbarState.new(max_scratch_scroll + 1)
|
|
200
|
-
state.position = @scratch_scroll
|
|
201
|
-
state.viewport_content_length = @scratch_page_height
|
|
202
|
-
frame.render_stateful_widget(scrollbar, @scratch_area, state)
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
private def render_controls(frame)
|
|
206
|
-
spans = [
|
|
207
|
-
@tui.text_span(content: "↑/↓", style: hotkey_style),
|
|
208
|
-
@tui.text_span(content: ": Scroll "),
|
|
209
|
-
@tui.text_span(content: "e", style: hotkey_style),
|
|
210
|
-
@tui.text_span(content: ": Edit README "),
|
|
211
|
-
@tui.text_span(content: "s", style: hotkey_style),
|
|
212
|
-
@tui.text_span(content: ": Edit Scratch "),
|
|
213
|
-
]
|
|
214
|
-
if split_view?
|
|
215
|
-
spans += [
|
|
216
|
-
@tui.text_span(content: "Tab", style: hotkey_style),
|
|
217
|
-
@tui.text_span(content: ": Focus "),
|
|
218
|
-
]
|
|
219
|
-
end
|
|
220
|
-
spans += [
|
|
221
|
-
@tui.text_span(content: "q", style: hotkey_style),
|
|
222
|
-
@tui.text_span(content: ": Quit"),
|
|
223
|
-
]
|
|
224
|
-
|
|
225
|
-
paragraph = @tui.paragraph(
|
|
226
|
-
text: [@tui.text_line(spans:)],
|
|
227
|
-
alignment: :center,
|
|
228
|
-
block: @tui.block(title: "Controls", borders: [:all])
|
|
229
|
-
)
|
|
230
|
-
frame.render_widget(paragraph, @controls_area)
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
private def hotkey_style = @tui.style(modifiers: [:bold, :underlined])
|
|
234
|
-
|
|
235
|
-
# Phase 3: Handle input using cached geometry
|
|
236
|
-
private def handle_input
|
|
237
|
-
case @tui.poll_event
|
|
238
|
-
# Quit
|
|
239
|
-
in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
|
|
240
|
-
:quit
|
|
241
|
-
|
|
242
|
-
# Edit actions
|
|
243
|
-
in { type: :key, code: "e" }
|
|
244
|
-
:edit_readme
|
|
245
|
-
in { type: :key, code: "s" }
|
|
246
|
-
:edit_scratch
|
|
247
|
-
|
|
248
|
-
# Focus switching (only in split view)
|
|
249
|
-
in { type: :key, code: "tab" } | { type: :key, code: "backtab" } |
|
|
250
|
-
{ type: :key, code: "left" } | { type: :key, code: "right" } |
|
|
251
|
-
{ type: :key, code: "h" } | { type: :key, code: "l" } if split_view?
|
|
252
|
-
switch_focus
|
|
253
|
-
nil
|
|
254
|
-
|
|
255
|
-
# Keyboard scrolling
|
|
256
|
-
in { type: :key, code: "up" } | { type: :key, code: "k" }
|
|
257
|
-
scroll_up
|
|
258
|
-
nil
|
|
259
|
-
in { type: :key, code: "down" } | { type: :key, code: "j" }
|
|
260
|
-
scroll_down
|
|
261
|
-
nil
|
|
262
|
-
in { type: :key, code: "page_up" }
|
|
263
|
-
scroll_up(page_height)
|
|
264
|
-
nil
|
|
265
|
-
in { type: :key, code: "page_down" }
|
|
266
|
-
scroll_down(page_height)
|
|
267
|
-
nil
|
|
268
|
-
in { type: :key, code: "home" }
|
|
269
|
-
scroll_to(0)
|
|
270
|
-
nil
|
|
271
|
-
in { type: :key, code: "end" }
|
|
272
|
-
scroll_to(max_scroll)
|
|
273
|
-
nil
|
|
274
|
-
|
|
275
|
-
# Mouse scroll - hit test to determine target pane
|
|
276
|
-
in { type: :mouse, kind: "scroll_up", x:, y: }
|
|
277
|
-
pane = pane_at(x, y)
|
|
278
|
-
scroll_pane_up(pane) if pane
|
|
279
|
-
nil
|
|
280
|
-
in { type: :mouse, kind: "scroll_down", x:, y: }
|
|
281
|
-
pane = pane_at(x, y)
|
|
282
|
-
scroll_pane_down(pane) if pane
|
|
283
|
-
nil
|
|
284
|
-
|
|
285
|
-
# Mouse click to focus (only in split view)
|
|
286
|
-
in { type: :mouse, kind: "down", x:, y: } if split_view?
|
|
287
|
-
pane = pane_at(x, y)
|
|
288
|
-
@focus = pane if pane
|
|
289
|
-
nil
|
|
290
|
-
|
|
291
|
-
else
|
|
292
|
-
nil
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
private def pane_at(x, y)
|
|
297
|
-
if @scratch_area&.contains?(x, y)
|
|
298
|
-
:scratch
|
|
299
|
-
elsif @readme_area&.contains?(x, y)
|
|
300
|
-
:readme
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
private def switch_focus
|
|
305
|
-
@focus = (@focus == :readme) ? :scratch : :readme
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
private def scroll_up(n = 1) = scroll_pane_up(@focus, n)
|
|
309
|
-
private def scroll_down(n = 1) = scroll_pane_down(@focus, n)
|
|
310
|
-
|
|
311
|
-
private def scroll_pane_up(pane, n = 1)
|
|
312
|
-
if pane == :readme
|
|
313
|
-
@readme_scroll = [@readme_scroll - n, 0].max
|
|
314
|
-
else
|
|
315
|
-
@scratch_scroll = [@scratch_scroll - n, 0].max
|
|
316
|
-
end
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
private def scroll_pane_down(pane, n = 1)
|
|
320
|
-
if pane == :readme
|
|
321
|
-
@readme_scroll = [@readme_scroll + n, max_readme_scroll].min
|
|
322
|
-
else
|
|
323
|
-
@scratch_scroll = [@scratch_scroll + n, max_scratch_scroll].min
|
|
324
|
-
end
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
private def scroll_to(y)
|
|
328
|
-
if @focus == :readme
|
|
329
|
-
@readme_scroll = y.clamp(0, max_readme_scroll)
|
|
330
|
-
else
|
|
331
|
-
@scratch_scroll = y.clamp(0, max_scratch_scroll)
|
|
332
|
-
end
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
private def max_scroll = (@focus == :readme) ? max_readme_scroll : max_scratch_scroll
|
|
336
|
-
private def max_readme_scroll = [@readme_line_count - @readme_page_height, 0].max
|
|
337
|
-
private def max_scratch_scroll = [@scratch_line_count - @scratch_page_height, 0].max
|
|
338
|
-
private def page_height = (@focus == :readme) ? @readme_page_height : @scratch_page_height
|
|
339
|
-
|
|
340
|
-
private def reload_readme = @readme_content = File.read(README_PATH)
|
|
341
|
-
private def reload_scratch = @scratch_content = File.read(@scratch.path)
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
AppExternalEditor.new.run if __FILE__ == $PROGRAM_NAME
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
-
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
-
-->
|
|
5
|
-
|
|
6
|
-
# Login Form Example
|
|
7
|
-
|
|
8
|
-
[](app.rb)
|
|
9
|
-
|
|
10
|
-
Demonstrates how to create a modal overlay for user input.
|
|
11
|
-
|
|
12
|
-
Many applications need to block interaction with the main UI while collecting specific information, like a login prompt or confirmation dialog. Managing the z-index and input focus for these overlays can be tricky.
|
|
13
|
-
|
|
14
|
-
This example solves this by using the `Overlay` widget to stack a centered popup on top of a base layer, conditionally rendering the popup based on state.
|
|
15
|
-
|
|
16
|
-
## Features Demonstrated
|
|
17
|
-
|
|
18
|
-
- **Overlays:** Stacking widgets on top of each other using `tui.overlay`.
|
|
19
|
-
- **Centering:** Positioning a widget in the center of the screen using `tui.center`.
|
|
20
|
-
- **State Management:** Switching between "Base" and "Popup" views.
|
|
21
|
-
- **Input Handling:** Capturing text input and handling specific keys (Enter, Esc) to trigger state changes.
|
|
22
|
-
- **Cursor Positioning:** Manually calculating cursor position within a `Paragraph`.
|
|
23
|
-
|
|
24
|
-
## Hotkeys
|
|
25
|
-
|
|
26
|
-
### Form Mode
|
|
27
|
-
- **Text Input**: Type to enter username (supports all characters including 'q').
|
|
28
|
-
- **Backspace**: Deletes the last character.
|
|
29
|
-
- **Enter**: Submits the form and opens the success popup.
|
|
30
|
-
- **Esc**: Quits the application.
|
|
31
|
-
- **Ctrl+C**: Quits the application.
|
|
32
|
-
|
|
33
|
-
### Popup Mode
|
|
34
|
-
- **q**: Closes the popup and quits the application.
|
|
35
|
-
- **Ctrl+C**: Quits the application.
|
|
36
|
-
|
|
37
|
-
## Usage
|
|
38
|
-
|
|
39
|
-
<!-- SPDX-SnippetBegin -->
|
|
40
|
-
<!--
|
|
41
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
42
|
-
SPDX-License-Identifier: MIT-0
|
|
43
|
-
-->
|
|
44
|
-
```bash
|
|
45
|
-
ruby examples/app_login_form/app.rb
|
|
46
|
-
```
|
|
47
|
-
<!-- SPDX-SnippetEnd -->
|
|
48
|
-
|
|
49
|
-
## Learning Outcomes
|
|
50
|
-
|
|
51
|
-
Use this example if you need to...
|
|
52
|
-
|
|
53
|
-
- Create a modal dialog or popup.
|
|
54
|
-
- Center a widget on the screen (vertically and horizontally).
|
|
55
|
-
- Implement a simple text input field with cursor management.
|
|
56
|
-
- layer widgets using the `Overlay` widget.
|
|
57
|
-
|
|
58
|
-
[Read the source code →](app.rb)
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
#--
|
|
4
|
-
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
-
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
-
#++
|
|
7
|
-
|
|
8
|
-
$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
|
|
9
|
-
require "ratatui_ruby"
|
|
10
|
-
|
|
11
|
-
class AppLoginForm
|
|
12
|
-
PREFIX = "Enter Username: [ "
|
|
13
|
-
SUFFIX = " ]"
|
|
14
|
-
|
|
15
|
-
def initialize
|
|
16
|
-
@username = ""
|
|
17
|
-
@show_popup = false
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def run
|
|
21
|
-
RatatuiRuby.run do |tui|
|
|
22
|
-
@tui = tui
|
|
23
|
-
loop do
|
|
24
|
-
render
|
|
25
|
-
break if handle_input == :quit
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
private def render
|
|
31
|
-
# 1. Base Layer Construction
|
|
32
|
-
# We want a cursor relative to the paragraph.
|
|
33
|
-
# So we wrap Paragraph and Cursor in an Overlay, and put that Overlay in a Center.
|
|
34
|
-
|
|
35
|
-
# Calculate cursor position
|
|
36
|
-
# Border takes 1 cell.
|
|
37
|
-
# Cursor X = 1 (border) + PREFIX.length + username.length
|
|
38
|
-
# Cursor Y = 1 (border + line 0)
|
|
39
|
-
cursor_x = 1 + PREFIX.length + @username.length
|
|
40
|
-
cursor_y = 1
|
|
41
|
-
|
|
42
|
-
# The content of the base form
|
|
43
|
-
form_content = @tui.overlay(layers: [
|
|
44
|
-
@tui.paragraph(
|
|
45
|
-
text: "#{PREFIX}#{@username}#{SUFFIX}",
|
|
46
|
-
block: @tui.block(borders: :all, title: "Login Form"),
|
|
47
|
-
alignment: :left
|
|
48
|
-
),
|
|
49
|
-
@tui.cursor(x: cursor_x, y: cursor_y),
|
|
50
|
-
])
|
|
51
|
-
|
|
52
|
-
# Center the form on screen
|
|
53
|
-
base_layer = @tui.center(
|
|
54
|
-
child: form_content,
|
|
55
|
-
width_percent: 50,
|
|
56
|
-
height_percent: 20
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
# 2. Popup Layer Construction
|
|
60
|
-
final_view = if @show_popup
|
|
61
|
-
popup_message = @tui.center(
|
|
62
|
-
child: @tui.paragraph(
|
|
63
|
-
text: "Login Successful!\nPress 'q' to quit.",
|
|
64
|
-
style: @tui.style(fg: :green, bg: :black),
|
|
65
|
-
block: @tui.block(borders: :all),
|
|
66
|
-
alignment: :center,
|
|
67
|
-
wrap: true
|
|
68
|
-
),
|
|
69
|
-
width_percent: 30,
|
|
70
|
-
height_percent: 20
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
# Render Base Layer (background) THEN Popup Layer
|
|
74
|
-
@tui.overlay(layers: [base_layer, popup_message])
|
|
75
|
-
else
|
|
76
|
-
base_layer
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# 3. Draw
|
|
80
|
-
@tui.draw do |frame|
|
|
81
|
-
frame.render_widget(final_view, frame.area)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
private def handle_input
|
|
86
|
-
case @tui.poll_event
|
|
87
|
-
in { type: :key, code: "c", modifiers: ["ctrl"] }
|
|
88
|
-
:quit
|
|
89
|
-
in { type: :key, code: "q" } if @show_popup
|
|
90
|
-
:quit
|
|
91
|
-
in { type: :key, code: "enter" }
|
|
92
|
-
@show_popup ||= true
|
|
93
|
-
nil
|
|
94
|
-
in { type: :key, code: "backspace" }
|
|
95
|
-
@username.chop! unless @show_popup
|
|
96
|
-
nil
|
|
97
|
-
in { type: :key, code: "esc" }
|
|
98
|
-
:quit unless @show_popup
|
|
99
|
-
in { type: :key, code:, modifiers: [] }
|
|
100
|
-
# Simple text input (single character, no modifiers)
|
|
101
|
-
@username += code if !@show_popup && code.length == 1
|
|
102
|
-
nil
|
|
103
|
-
else
|
|
104
|
-
nil
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
AppLoginForm.new.run if __FILE__ == $0
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
-
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
-
-->
|
|
5
|
-
|
|
6
|
-
# Stateful Interaction Example
|
|
7
|
-
|
|
8
|
-
[](app.rb)
|
|
9
|
-
|
|
10
|
-
This example demonstrates High-Fidelity Interaction using **Stateful Widget Rendering**.
|
|
11
|
-
|
|
12
|
-
It showcases a "Database Viewer" layout where:
|
|
13
|
-
1. **Selection Persistence:** `ListState` and `TableState` objects persist across frames, maintaining selection without manual index tracking variables.
|
|
14
|
-
2. **Offset Read-back:** The application reads `state.offset` *after* rendering to know exactly which items were visible on screen.
|
|
15
|
-
3. **Mouse Interaction:** Using the read-back offset, we can calculate exactly which row was clicked, even when the specific item wasn't drawn at that absolute Y position due to scrolling.
|
|
16
|
-
|
|
17
|
-
## Key Concept: The "Read-back" Loop
|
|
18
|
-
|
|
19
|
-
Standard immediate-mode interaction often requires you to re-calculate layout logic to determine what was clicked.
|
|
20
|
-
|
|
21
|
-
In `ratatui_ruby`'s Stateful Rendering:
|
|
22
|
-
1. **Update**: You modify `state` (e.g., `state.select(1)`).
|
|
23
|
-
2. **Render**: You pass `state` to `render_stateful_widget`. Ratatui's Rust backend calculates layout and **updates** `state.offset` in-place if scrolling happened.
|
|
24
|
-
3. **Interact**: On the next event loop, you use `state.offset` to correctly map mouse coordinates to data indices.
|
|
25
|
-
|
|
26
|
-
## Hotkeys
|
|
27
|
-
|
|
28
|
-
| Key | Action |
|
|
29
|
-
| --- | --- |
|
|
30
|
-
| `↑` / `↓` | Scroll the active pane |
|
|
31
|
-
| `Tab` / `←` / `→` | Switch active pane (List vs Table) |
|
|
32
|
-
| `Mouse Click` | Select the clicked row (Works with scrolling!) |
|
|
33
|
-
| `q` | Quit |
|
|
34
|
-
|
|
35
|
-
[Read the source code →](app.rb)
|