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,58 +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
|
-
# A single color variant with label and styling information.
|
|
9
|
-
#
|
|
10
|
-
# Color palettes need to show individual colors with labels (Main, Shade, Tint,
|
|
11
|
-
# Complement). Bundling a color's hex code, text color, and frame color together
|
|
12
|
-
# is natural—they're always used as a set.
|
|
13
|
-
#
|
|
14
|
-
# This value object pairs a color with its metadata and rendering styles.
|
|
15
|
-
#
|
|
16
|
-
# Use it to represent colors in a palette or harmony.
|
|
17
|
-
#
|
|
18
|
-
# === Attributes
|
|
19
|
-
#
|
|
20
|
-
# [label] String label for this color variant
|
|
21
|
-
# [hex] String hex color code
|
|
22
|
-
# [text_color] Symbol (:white or :black) for readable text
|
|
23
|
-
# [frame_color] String background color for the swatch frame
|
|
24
|
-
#
|
|
25
|
-
# === Example
|
|
26
|
-
#
|
|
27
|
-
# harmony = Harmony.new(
|
|
28
|
-
# label: "Main",
|
|
29
|
-
# hex: "#FF0000",
|
|
30
|
-
# text_color: :white,
|
|
31
|
-
# frame_color: "#000000"
|
|
32
|
-
# )
|
|
33
|
-
Harmony = Data.define(:label, :hex, :text_color, :frame_color) do
|
|
34
|
-
# Renders a 4-line color swatch for display in a TUI Block.
|
|
35
|
-
#
|
|
36
|
-
# Produces a visual representation: a 7-character-wide box with the color
|
|
37
|
-
# centered and the hex code below.
|
|
38
|
-
#
|
|
39
|
-
# [tui] Session or TUI factory object
|
|
40
|
-
#
|
|
41
|
-
# === Example
|
|
42
|
-
#
|
|
43
|
-
# harmony = Harmony.new(...)
|
|
44
|
-
# lines = harmony.color_swatch_lines(tui)
|
|
45
|
-
# # => [TextLine, TextLine, TextLine, TextLine]
|
|
46
|
-
def color_swatch_lines(tui)
|
|
47
|
-
[
|
|
48
|
-
tui.text_line(spans: Array.new(7) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) }),
|
|
49
|
-
tui.text_line(spans: [
|
|
50
|
-
*Array.new(3) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) },
|
|
51
|
-
tui.text_span(content: " ", style: tui.style(bg: hex, fg: text_color)),
|
|
52
|
-
*Array.new(3) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) },
|
|
53
|
-
]),
|
|
54
|
-
tui.text_line(spans: Array.new(7) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) }),
|
|
55
|
-
tui.text_line(spans: [tui.text_span(content: hex, style: tui.style(fg: :white))]),
|
|
56
|
-
]
|
|
57
|
-
end
|
|
58
|
-
end
|
|
@@ -1,176 +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
|
-
require_relative "color"
|
|
9
|
-
|
|
10
|
-
# A self-contained text input component for color entry.
|
|
11
|
-
#
|
|
12
|
-
# Users type color values. They make mistakes—typos, invalid formats. The app
|
|
13
|
-
# needs to validate their input and show helpful error messages.
|
|
14
|
-
#
|
|
15
|
-
# This component encapsulates rendering, state, and event handling. It draws
|
|
16
|
-
# itself into the provided area, caches that area for hit testing, and handles
|
|
17
|
-
# keyboard events internally.
|
|
18
|
-
#
|
|
19
|
-
# === Component Contract
|
|
20
|
-
#
|
|
21
|
-
# - `render(tui, frame, area)`: Draws the input field; stores `area` for hit testing
|
|
22
|
-
# - `handle_event(event) -> Symbol | nil`: Returns `:consumed`, `:submitted`, or `nil`
|
|
23
|
-
#
|
|
24
|
-
# === Example
|
|
25
|
-
#
|
|
26
|
-
# input = Input.new
|
|
27
|
-
# input.render(tui, frame, area)
|
|
28
|
-
#
|
|
29
|
-
# result = input.handle_event(event)
|
|
30
|
-
# case result
|
|
31
|
-
# when :submitted
|
|
32
|
-
# palette.update_color(input.parsed_color)
|
|
33
|
-
# end
|
|
34
|
-
class Input
|
|
35
|
-
PRINTABLE_PATTERN = /[\w#,().\s%]/
|
|
36
|
-
|
|
37
|
-
# Creates a new Input with an optional initial value.
|
|
38
|
-
#
|
|
39
|
-
# [initial_value] String initial color input (default: <tt>"#F96302"</tt>)
|
|
40
|
-
def initialize(initial_value = "#F96302")
|
|
41
|
-
@value = initial_value
|
|
42
|
-
@error = ""
|
|
43
|
-
@parsed_color = nil
|
|
44
|
-
@area = nil
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Current input string.
|
|
48
|
-
attr_reader :value
|
|
49
|
-
|
|
50
|
-
# Error message from the last failed parse, or empty string.
|
|
51
|
-
attr_reader :error
|
|
52
|
-
|
|
53
|
-
# The last successfully parsed Color, or nil.
|
|
54
|
-
attr_reader :parsed_color
|
|
55
|
-
|
|
56
|
-
# The cached render area, for hit testing.
|
|
57
|
-
attr_reader :area
|
|
58
|
-
|
|
59
|
-
# Clears the current error message.
|
|
60
|
-
def clear_error
|
|
61
|
-
@error = ""
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Renders the input widget into the given area.
|
|
65
|
-
#
|
|
66
|
-
# Caches `area` for hit testing. Shows the current input value and positions
|
|
67
|
-
# the terminal's blinking cursor at the end of the text using
|
|
68
|
-
# `frame.set_cursor_position`. Displays the error message in red if set.
|
|
69
|
-
#
|
|
70
|
-
# [tui] Session or TUI factory object
|
|
71
|
-
# [frame] Frame object from RatatuiRuby.draw block
|
|
72
|
-
# [area] Rect area to draw into
|
|
73
|
-
#
|
|
74
|
-
# === Example
|
|
75
|
-
#
|
|
76
|
-
# input.render(tui, frame, input_area)
|
|
77
|
-
def render(tui, frame, area)
|
|
78
|
-
@area = area
|
|
79
|
-
widget = build_widget(tui)
|
|
80
|
-
frame.render_widget(widget, area)
|
|
81
|
-
|
|
82
|
-
# Position real blinking cursor at end of input text
|
|
83
|
-
cursor_x, cursor_y = cursor_position_in(area)
|
|
84
|
-
frame.set_cursor_position(cursor_x, cursor_y)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Processes a keyboard event and updates internal state.
|
|
88
|
-
#
|
|
89
|
-
# Returns:
|
|
90
|
-
# - `:submitted` when Enter is pressed (caller should read `parsed_color`)
|
|
91
|
-
# - `:consumed` when the event was handled (typing, backspace)
|
|
92
|
-
# - `nil` when the event was ignored
|
|
93
|
-
#
|
|
94
|
-
# [event] Event from RatatuiRuby.poll_event
|
|
95
|
-
#
|
|
96
|
-
# === Example
|
|
97
|
-
#
|
|
98
|
-
# result = input.handle_event(event)
|
|
99
|
-
# if result == :submitted
|
|
100
|
-
# palette.update_color(input.parsed_color)
|
|
101
|
-
# end
|
|
102
|
-
def handle_event(event)
|
|
103
|
-
case event
|
|
104
|
-
in { type: :key, code: "enter" }
|
|
105
|
-
parse
|
|
106
|
-
:submitted
|
|
107
|
-
in { type: :key, code: "backspace" }
|
|
108
|
-
delete_char
|
|
109
|
-
:consumed
|
|
110
|
-
in { type: :paste, content: }
|
|
111
|
-
set(content)
|
|
112
|
-
parse
|
|
113
|
-
:submitted
|
|
114
|
-
in { type: :key, code: code }
|
|
115
|
-
append_char(code)
|
|
116
|
-
:consumed
|
|
117
|
-
else
|
|
118
|
-
nil
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
private def append_char(char)
|
|
123
|
-
@value += char if char.length == 1 && char.match?(PRINTABLE_PATTERN)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
private def delete_char
|
|
127
|
-
@value = @value[0...-1]
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
private def set(text)
|
|
131
|
-
@value = text
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
private def parse
|
|
135
|
-
color = Color.parse(@value)
|
|
136
|
-
if color
|
|
137
|
-
clear_error
|
|
138
|
-
@parsed_color = color
|
|
139
|
-
else
|
|
140
|
-
@error = "Invalid color format. Try: #ff0000, rgb(255,0,0), red"
|
|
141
|
-
@parsed_color = nil
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
private def build_widget(tui)
|
|
146
|
-
input_lines = [
|
|
147
|
-
tui.text_line(spans: [
|
|
148
|
-
tui.text_span(content: @value),
|
|
149
|
-
]),
|
|
150
|
-
]
|
|
151
|
-
|
|
152
|
-
unless @error.empty?
|
|
153
|
-
input_lines << tui.text_line(spans: [
|
|
154
|
-
tui.text_span(content: @error, style: tui.style(fg: :red)),
|
|
155
|
-
])
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
tui.block(
|
|
159
|
-
title: "Color Input",
|
|
160
|
-
borders: [:all],
|
|
161
|
-
children: [
|
|
162
|
-
tui.paragraph(text: input_lines),
|
|
163
|
-
]
|
|
164
|
-
)
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Calculates cursor position within the input area.
|
|
168
|
-
#
|
|
169
|
-
# Accounts for block border (1 cell) and current text length.
|
|
170
|
-
private def cursor_position_in(area)
|
|
171
|
-
# Border takes 1 cell on left, cursor goes after last character
|
|
172
|
-
x = area.x + 1 + @value.length
|
|
173
|
-
y = area.y + 1 # First line inside border
|
|
174
|
-
[x, y]
|
|
175
|
-
end
|
|
176
|
-
end
|
|
@@ -1,180 +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
|
-
require_relative "input"
|
|
9
|
-
require_relative "palette"
|
|
10
|
-
require_relative "export_pane"
|
|
11
|
-
require_relative "controls"
|
|
12
|
-
require_relative "clipboard"
|
|
13
|
-
require_relative "copy_dialog"
|
|
14
|
-
|
|
15
|
-
# The root container that owns all child components and orchestrates the UI.
|
|
16
|
-
#
|
|
17
|
-
# Building a complete color picker UI involves layout calculation, widget
|
|
18
|
-
# composition, event routing, and cross-component communication. The Container
|
|
19
|
-
# pattern centralizes this orchestration while keeping components decoupled.
|
|
20
|
-
#
|
|
21
|
-
# This container:
|
|
22
|
-
# - **Layout Phase**: Calculates Rects using tui.layout_split
|
|
23
|
-
# - **Delegation Phase**: Calls child.render(tui, frame, area) for each component
|
|
24
|
-
# - **Event Routing (Chain of Responsibility)**: Delegates events front-to-back
|
|
25
|
-
# - **Mediator Pattern**: Manages cross-component communication via symbolic signals
|
|
26
|
-
#
|
|
27
|
-
# === Component Contract
|
|
28
|
-
#
|
|
29
|
-
# - `render(tui, frame, area)`: Lays out and renders all children
|
|
30
|
-
# - `handle_event(event) -> Symbol | nil`: Routes events to children
|
|
31
|
-
# - `tick`: Delegates lifecycle updates (clipboard timer)
|
|
32
|
-
#
|
|
33
|
-
# === Example
|
|
34
|
-
#
|
|
35
|
-
# container = MainContainer.new(tui)
|
|
36
|
-
# container.render(tui, frame, frame.area)
|
|
37
|
-
# result = container.handle_event(event)
|
|
38
|
-
# container.tick
|
|
39
|
-
class MainContainer
|
|
40
|
-
def initialize(tui)
|
|
41
|
-
@tui = tui
|
|
42
|
-
@input = Input.new
|
|
43
|
-
@palette = Palette.new(@input.parsed_color)
|
|
44
|
-
@export_pane = ExportPane.new
|
|
45
|
-
@controls = Controls.new
|
|
46
|
-
@clipboard = Clipboard.new
|
|
47
|
-
@dialog = CopyDialog.new(@clipboard)
|
|
48
|
-
|
|
49
|
-
# Parse initial color
|
|
50
|
-
initial_result = simulate_initial_parse
|
|
51
|
-
@palette.update_color(initial_result) if initial_result
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Renders all child components into the given area.
|
|
55
|
-
#
|
|
56
|
-
# Calculates layout once per frame. Delegates rendering to each component.
|
|
57
|
-
# Renders the dialog overlay last for z-ordering.
|
|
58
|
-
#
|
|
59
|
-
# [tui] Session or TUI factory object
|
|
60
|
-
# [frame] Frame object from RatatuiRuby.draw block
|
|
61
|
-
# [area] Rect area to draw into
|
|
62
|
-
#
|
|
63
|
-
# === Example
|
|
64
|
-
#
|
|
65
|
-
# tui.draw { |frame| container.render(tui, frame, frame.area) }
|
|
66
|
-
def render(tui, frame, area)
|
|
67
|
-
# Layout Phase: calculate all areas
|
|
68
|
-
input_area, rest = tui.layout_split(
|
|
69
|
-
area,
|
|
70
|
-
direction: :vertical,
|
|
71
|
-
constraints: [
|
|
72
|
-
tui.constraint_length(3),
|
|
73
|
-
tui.constraint_fill(1),
|
|
74
|
-
]
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
color_area, control_area = tui.layout_split(
|
|
78
|
-
rest,
|
|
79
|
-
direction: :vertical,
|
|
80
|
-
constraints: [
|
|
81
|
-
tui.constraint_length(14),
|
|
82
|
-
tui.constraint_fill(1),
|
|
83
|
-
]
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
harmony_area, export_area = tui.layout_split(
|
|
87
|
-
color_area,
|
|
88
|
-
direction: :vertical,
|
|
89
|
-
constraints: [
|
|
90
|
-
tui.constraint_length(7),
|
|
91
|
-
tui.constraint_fill(1),
|
|
92
|
-
]
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
# Delegation Phase: render each component
|
|
96
|
-
@input.render(tui, frame, input_area)
|
|
97
|
-
@palette.render(tui, frame, harmony_area)
|
|
98
|
-
@export_pane.render(tui, frame, export_area, palette: @palette)
|
|
99
|
-
@controls.render(tui, frame, control_area, clipboard: @clipboard)
|
|
100
|
-
|
|
101
|
-
# Overlay Logic: dialog rendered last for z-ordering
|
|
102
|
-
if @dialog.active?
|
|
103
|
-
dialog_area = calculate_center_area(area, 40, 8)
|
|
104
|
-
frame.render_widget(tui.clear, area)
|
|
105
|
-
@dialog.render(tui, frame, dialog_area)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Routes events to child components in visual order (front-to-back).
|
|
110
|
-
#
|
|
111
|
-
# Implements Chain of Responsibility:
|
|
112
|
-
# 1. If dialog is active, offer it the event first
|
|
113
|
-
# 2. Then Input, ExportPane (which may trigger dialog)
|
|
114
|
-
# 3. Mediator pattern: interprets symbolic signals for cross-component effects
|
|
115
|
-
#
|
|
116
|
-
# Returns:
|
|
117
|
-
# - `:consumed` when any component handled the event
|
|
118
|
-
# - `nil` when no component handled the event
|
|
119
|
-
#
|
|
120
|
-
# [event] Event from RatatuiRuby.poll_event
|
|
121
|
-
#
|
|
122
|
-
# === Example
|
|
123
|
-
#
|
|
124
|
-
# result = container.handle_event(event)
|
|
125
|
-
def handle_event(event)
|
|
126
|
-
# Clear input error when not in dialog mode
|
|
127
|
-
@input.clear_error unless @dialog.active?
|
|
128
|
-
|
|
129
|
-
# Front-to-back: dialog has priority when active
|
|
130
|
-
if @dialog.active?
|
|
131
|
-
result = @dialog.handle_event(event)
|
|
132
|
-
return :consumed if result == :consumed
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Input component
|
|
136
|
-
result = @input.handle_event(event)
|
|
137
|
-
case result
|
|
138
|
-
when :submitted
|
|
139
|
-
# Mediator: sync Input -> Palette
|
|
140
|
-
@palette.update_color(@input.parsed_color)
|
|
141
|
-
return :consumed
|
|
142
|
-
when :consumed
|
|
143
|
-
return :consumed
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# ExportPane: may request copy dialog
|
|
147
|
-
result = @export_pane.handle_event(event)
|
|
148
|
-
if result == :copy_requested && @palette.main
|
|
149
|
-
@dialog.open(@palette.main.hex)
|
|
150
|
-
return :consumed
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Palette and Controls are display-only
|
|
154
|
-
nil
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# Delegates lifecycle tick to time-sensitive components.
|
|
158
|
-
#
|
|
159
|
-
# Currently handles clipboard feedback timer.
|
|
160
|
-
#
|
|
161
|
-
# === Example
|
|
162
|
-
#
|
|
163
|
-
# container.tick
|
|
164
|
-
def tick
|
|
165
|
-
@controls.tick(@clipboard)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
private def calculate_center_area(parent_area, width, height)
|
|
169
|
-
x = (parent_area.width - width) / 2
|
|
170
|
-
y = (parent_area.height - height) / 2
|
|
171
|
-
@tui.rect(x:, y:, width:, height:)
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# Simulates the initial parse that happens when the app starts.
|
|
175
|
-
# Input is initialized with a default color, so we need to parse it.
|
|
176
|
-
private def simulate_initial_parse
|
|
177
|
-
require_relative "color"
|
|
178
|
-
Color.parse(@input.value)
|
|
179
|
-
end
|
|
180
|
-
end
|
|
@@ -1,111 +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
|
-
require_relative "color"
|
|
9
|
-
|
|
10
|
-
# A self-contained component displaying a color palette with harmonies.
|
|
11
|
-
#
|
|
12
|
-
# Color pickers need to show related colors: shades, tints, complements. This
|
|
13
|
-
# component owns a primary color and renders its harmonies.
|
|
14
|
-
#
|
|
15
|
-
# === Component Contract
|
|
16
|
-
#
|
|
17
|
-
# - `render(tui, frame, area)`: Draws the harmony blocks; stores `area`
|
|
18
|
-
# - `handle_event(event) -> nil`: Display-only, always returns nil
|
|
19
|
-
# - `update_color(color)`: Updates the primary color (called by MainContainer)
|
|
20
|
-
#
|
|
21
|
-
# === Example
|
|
22
|
-
#
|
|
23
|
-
# palette = Palette.new
|
|
24
|
-
# palette.update_color(Color.parse("#FF0000"))
|
|
25
|
-
# palette.render(tui, frame, palette_area)
|
|
26
|
-
class Palette
|
|
27
|
-
def initialize(primary_color = nil)
|
|
28
|
-
@primary = primary_color
|
|
29
|
-
@area = nil
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# The cached render area.
|
|
33
|
-
attr_reader :area
|
|
34
|
-
|
|
35
|
-
# The primary (main) color, or nil if no color is set.
|
|
36
|
-
#
|
|
37
|
-
# === Example
|
|
38
|
-
#
|
|
39
|
-
# palette.main.hex # => "#FF0000"
|
|
40
|
-
def main
|
|
41
|
-
@primary
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Updates the primary color.
|
|
45
|
-
#
|
|
46
|
-
# Called by the MainContainer when Input submits a new color.
|
|
47
|
-
#
|
|
48
|
-
# [color] Color object or nil
|
|
49
|
-
def update_color(color)
|
|
50
|
-
@primary = color
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# All harmonies: main, shade, tint, complement, split 1, split 2, split-complement.
|
|
54
|
-
#
|
|
55
|
-
# Returns an empty array if no primary color is set.
|
|
56
|
-
def all
|
|
57
|
-
return [] if @primary.nil?
|
|
58
|
-
|
|
59
|
-
@primary.harmonies
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Renders the palette into the given area.
|
|
63
|
-
#
|
|
64
|
-
# Shows all harmony blocks in a horizontal layout. If no color is set,
|
|
65
|
-
# displays a placeholder message.
|
|
66
|
-
#
|
|
67
|
-
# [tui] Session or TUI factory object
|
|
68
|
-
# [frame] Frame object from RatatuiRuby.draw block
|
|
69
|
-
# [area] Rect area to draw into
|
|
70
|
-
#
|
|
71
|
-
# === Example
|
|
72
|
-
#
|
|
73
|
-
# palette.render(tui, frame, palette_area)
|
|
74
|
-
def render(tui, frame, area)
|
|
75
|
-
@area = area
|
|
76
|
-
widget = build_widget(tui)
|
|
77
|
-
frame.render_widget(widget, area)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Display-only component; always returns nil.
|
|
81
|
-
def handle_event(_event)
|
|
82
|
-
nil
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
private def build_widget(tui)
|
|
86
|
-
if @primary.nil?
|
|
87
|
-
tui.paragraph(text: "No color selected")
|
|
88
|
-
else
|
|
89
|
-
blocks = as_blocks(tui)
|
|
90
|
-
tui.layout(
|
|
91
|
-
direction: :horizontal,
|
|
92
|
-
constraints: Array.new(blocks.size) { tui.constraint_fill(1) },
|
|
93
|
-
children: blocks
|
|
94
|
-
)
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
private def as_blocks(tui)
|
|
99
|
-
return [] if @primary.nil?
|
|
100
|
-
|
|
101
|
-
all.map do |harmony|
|
|
102
|
-
tui.block(
|
|
103
|
-
title: harmony.label,
|
|
104
|
-
borders: [:all],
|
|
105
|
-
children: [
|
|
106
|
-
tui.paragraph(text: harmony.color_swatch_lines(tui)),
|
|
107
|
-
]
|
|
108
|
-
)
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
-
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
-
-->
|
|
5
|
-
|
|
6
|
-
# Debugging Showcase
|
|
7
|
-
|
|
8
|
-
[](app.rb)
|
|
9
|
-
|
|
10
|
-
Interactive demonstration of RatatuiRuby's debugging features.
|
|
11
|
-
|
|
12
|
-
For comprehensive documentation, see the [Debugging Guide](../../doc/concepts/debugging.md).
|
|
13
|
-
|
|
14
|
-
## What This Example Does
|
|
15
|
-
|
|
16
|
-
This app lets you trigger each debugging feature with a hotkey. Test your setup before encountering a real bug.
|
|
17
|
-
|
|
18
|
-
| Key | Action |
|
|
19
|
-
|-----|--------|
|
|
20
|
-
| `d` | Enable `debug_mode!` programmatically |
|
|
21
|
-
| `p` | Trigger a Rust panic |
|
|
22
|
-
| `t` | Trigger a Rust `TypeError` |
|
|
23
|
-
| `b` | Refresh debug status |
|
|
24
|
-
| `q` | Quit |
|
|
25
|
-
|
|
26
|
-
## Library Features Showcased
|
|
27
|
-
|
|
28
|
-
Reading this code will teach you how to:
|
|
29
|
-
|
|
30
|
-
* **Enable Remote Debugging**: Call `RatatuiRuby.debug_mode!` and receive the socket path
|
|
31
|
-
* **Detect Debug State**: Query `Debug.enabled?`, `Debug.rust_backtrace_enabled?`, and `Debug.remote_debugging_mode`
|
|
32
|
-
* **Trigger Test Panics**: Use `Debug.test_panic!` to verify Rust backtrace visibility
|
|
33
|
-
* **See Improved Error Messages**: Bypass DWIM coercion to see `TypeError` messages with value context
|
|
34
|
-
|
|
35
|
-
**Note:** Rust backtraces only appear for panics (`[p]`). Exceptions (`[t]`) show Ruby backtraces with improved error messages from Rust.
|
|
36
|
-
|
|
37
|
-
## Environment Variables
|
|
38
|
-
|
|
39
|
-
The example behaves differently depending on which environment variables you set.
|
|
40
|
-
|
|
41
|
-
### No Environment Variables
|
|
42
|
-
|
|
43
|
-
<!-- SPDX-SnippetBegin -->
|
|
44
|
-
<!--
|
|
45
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
46
|
-
SPDX-License-Identifier: MIT-0
|
|
47
|
-
-->
|
|
48
|
-
```bash
|
|
49
|
-
ruby examples/app_debugging_showcase/app.rb
|
|
50
|
-
```
|
|
51
|
-
<!-- SPDX-SnippetEnd -->
|
|
52
|
-
|
|
53
|
-
The TUI starts normally. Rust backtraces are disabled. Press `[d]` to enable remote debugging mid-session.
|
|
54
|
-
|
|
55
|
-
### `RUST_BACKTRACE=1`
|
|
56
|
-
|
|
57
|
-
<!-- SPDX-SnippetBegin -->
|
|
58
|
-
<!--
|
|
59
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
60
|
-
SPDX-License-Identifier: MIT-0
|
|
61
|
-
-->
|
|
62
|
-
```bash
|
|
63
|
-
RUST_BACKTRACE=1 ruby examples/app_debugging_showcase/app.rb
|
|
64
|
-
```
|
|
65
|
-
<!-- SPDX-SnippetEnd -->
|
|
66
|
-
|
|
67
|
-
Enables Rust backtraces. Press `[p]` to see the full Rust stack trace with file names and line numbers.
|
|
68
|
-
|
|
69
|
-
### `RR_DEBUG=1`
|
|
70
|
-
|
|
71
|
-
<!-- SPDX-SnippetBegin -->
|
|
72
|
-
<!--
|
|
73
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
74
|
-
SPDX-License-Identifier: MIT-0
|
|
75
|
-
-->
|
|
76
|
-
```bash
|
|
77
|
-
RR_DEBUG=1 ruby examples/app_debugging_showcase/app.rb
|
|
78
|
-
```
|
|
79
|
-
<!-- SPDX-SnippetEnd -->
|
|
80
|
-
|
|
81
|
-
Full debug mode at startup. The debugger opens a socket and **waits for a connection** before the TUI starts. You'll see the socket path printed before the app runs.
|
|
82
|
-
|
|
83
|
-
In another terminal:
|
|
84
|
-
|
|
85
|
-
<!-- SPDX-SnippetBegin -->
|
|
86
|
-
<!--
|
|
87
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
88
|
-
SPDX-License-Identifier: MIT-0
|
|
89
|
-
-->
|
|
90
|
-
```bash
|
|
91
|
-
rdbg --attach
|
|
92
|
-
```
|
|
93
|
-
<!-- SPDX-SnippetEnd -->
|
|
94
|
-
|
|
95
|
-
When you attach, you'll see "Stop by SIGURG" — that's normal! SIGURG is how the debug gem signals the app to pause. Type `continue` to resume.
|
|
96
|
-
|
|
97
|
-
This uses the [ruby/debug](https://github.com/ruby/debug?tab=readme-ov-file#readme) gem for remote debugging.
|
|
98
|
-
|
|
99
|
-
### When to Use Which
|
|
100
|
-
|
|
101
|
-
`RR_DEBUG=1` includes Rust backtraces automatically (it calls `enable_rust_backtrace!` internally).
|
|
102
|
-
|
|
103
|
-
Use `RUST_BACKTRACE=1` alone when you want backtraces but **don't** want the debugger to stop and wait for a connection at startup.
|
|
104
|
-
|
|
105
|
-
## What Problems Does This Solve?
|
|
106
|
-
|
|
107
|
-
### "Is my Rust backtrace setup working?"
|
|
108
|
-
|
|
109
|
-
Press `[p]`. If you see a stack trace with file names and line numbers, you're good. If not, you need `RUST_BACKTRACE=1`.
|
|
110
|
-
|
|
111
|
-
### "How do I attach a debugger to a TUI?"
|
|
112
|
-
|
|
113
|
-
TUIs run in raw mode — you can't type into a REPL. Press `[d]` to enable remote debugging. The socket path appears in the UI. Run `rdbg --attach` from another terminal.
|
|
114
|
-
|
|
115
|
-
### "What does a Rust TypeError look like?"
|
|
116
|
-
|
|
117
|
-
Press `[t]`. The error message shows the expected type and the actual value you passed (e.g., `expected array for rows, got 42`). Note: This shows Ruby's backtrace — only panics (`[p]`) show Rust backtraces.
|
|
118
|
-
|
|
119
|
-
[Read the source code →](app.rb)
|