ratatui_ruby 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/AGENTS.md +6 -0
- data/CHANGELOG.md +44 -7
- data/README.md +11 -4
- data/REUSE.toml +2 -7
- data/doc/application_architecture.md +84 -10
- data/doc/application_testing.md +75 -29
- data/doc/contributors/design/ruby_frontend.md +39 -3
- data/doc/contributors/design/rust_backend.md +1 -0
- data/doc/contributors/developing_examples.md +129 -44
- data/doc/contributors/examples_audit/p1_high.md +21 -0
- data/doc/contributors/examples_audit/p2_moderate.md +81 -0
- data/doc/contributors/examples_audit.md +41 -0
- data/doc/event_handling.md +11 -3
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_color_picker.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_demo.png +0 -0
- data/doc/images/widget_block_demo.png +0 -0
- data/doc/images/widget_canvas_demo.png +0 -0
- data/doc/images/widget_cell_demo.png +0 -0
- data/doc/images/widget_center_demo.png +0 -0
- data/doc/images/widget_chart_demo.png +0 -0
- data/doc/images/widget_list_demo.png +0 -0
- data/doc/images/widget_overlay_demo.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_sparkline_demo.png +0 -0
- data/doc/images/widget_table_demo.png +0 -0
- data/doc/images/widget_tabs_demo.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/quickstart.md +69 -76
- data/doc/terminal_limitations.md +92 -0
- data/examples/app_all_events/README.md +45 -27
- data/examples/app_all_events/app.rb +38 -35
- data/examples/app_all_events/model/app_model.rb +157 -0
- data/examples/app_all_events/model/event_entry.rb +17 -0
- data/examples/app_all_events/model/msg.rb +37 -0
- data/examples/app_all_events/update.rb +73 -0
- data/examples/app_all_events/view/app_view.rb +8 -8
- data/examples/app_all_events/view/controls_view.rb +8 -6
- data/examples/app_all_events/view/counts_view.rb +12 -8
- data/examples/app_all_events/view/live_view.rb +8 -7
- data/examples/app_all_events/view/log_view.rb +10 -15
- data/examples/app_color_picker/README.md +84 -44
- data/examples/app_color_picker/app.rb +24 -62
- data/examples/app_color_picker/controls.rb +90 -0
- data/examples/app_color_picker/copy_dialog.rb +45 -49
- data/examples/app_color_picker/export_pane.rb +126 -0
- data/examples/app_color_picker/input.rb +99 -67
- data/examples/app_color_picker/main_container.rb +178 -0
- data/examples/app_color_picker/palette.rb +55 -26
- data/examples/app_login_form/README.md +47 -0
- data/examples/app_login_form/app.rb +2 -3
- data/examples/app_stateful_interaction/README.md +31 -0
- data/examples/app_stateful_interaction/app.rb +272 -0
- data/examples/timeout_demo.rb +43 -0
- data/examples/verify_quickstart_dsl/README.md +48 -0
- data/examples/verify_quickstart_dsl/app.rb +2 -0
- data/examples/verify_quickstart_layout/README.md +71 -0
- data/examples/verify_quickstart_layout/app.rb +2 -0
- data/examples/verify_quickstart_lifecycle/README.md +56 -0
- data/examples/verify_quickstart_lifecycle/app.rb +8 -2
- data/examples/verify_readme_usage/README.md +43 -0
- data/examples/verify_readme_usage/app.rb +8 -2
- data/examples/widget_barchart_demo/README.md +49 -0
- data/examples/widget_barchart_demo/app.rb +5 -5
- data/examples/widget_block_demo/README.md +34 -0
- data/examples/widget_block_demo/app.rb +256 -0
- data/examples/widget_box_demo/README.md +45 -0
- data/examples/widget_calendar_demo/README.md +39 -0
- data/examples/widget_canvas_demo/README.md +27 -0
- data/examples/widget_canvas_demo/app.rb +123 -0
- data/examples/widget_cell_demo/README.md +36 -0
- data/examples/widget_cell_demo/app.rb +31 -24
- data/examples/widget_center_demo/README.md +29 -0
- data/examples/widget_center_demo/app.rb +116 -0
- data/examples/widget_chart_demo/README.md +41 -0
- data/examples/widget_chart_demo/app.rb +7 -2
- data/examples/widget_gauge_demo/README.md +41 -0
- data/examples/widget_layout_split/README.md +44 -0
- data/examples/widget_line_gauge_demo/README.md +41 -0
- data/examples/widget_list_demo/README.md +49 -0
- data/examples/widget_list_demo/app.rb +91 -107
- data/examples/widget_map_demo/README.md +39 -0
- data/examples/{app_map_demo → widget_map_demo}/app.rb +2 -2
- data/examples/widget_overlay_demo/app.rb +248 -0
- data/examples/widget_popup_demo/README.md +36 -0
- data/examples/widget_ratatui_logo_demo/README.md +34 -0
- data/examples/widget_ratatui_mascot_demo/README.md +34 -0
- data/examples/widget_rect/README.md +38 -0
- data/examples/widget_render/README.md +37 -0
- data/examples/widget_rich_text/README.md +35 -0
- data/examples/widget_rich_text/app.rb +62 -33
- data/examples/widget_scroll_text/README.md +37 -0
- data/examples/widget_scroll_text/app.rb +0 -1
- data/examples/widget_scrollbar_demo/README.md +37 -0
- data/examples/widget_sparkline_demo/README.md +42 -0
- data/examples/widget_sparkline_demo/app.rb +4 -3
- data/examples/widget_style_colors/README.md +34 -0
- data/examples/widget_table_demo/README.md +48 -0
- data/examples/{app_table_select → widget_table_demo}/app.rb +46 -8
- data/examples/widget_tabs_demo/README.md +41 -0
- data/examples/widget_tabs_demo/app.rb +15 -1
- data/examples/widget_text_width/README.md +35 -0
- data/examples/widget_text_width/app.rb +106 -0
- data/exe/.gitkeep +0 -0
- data/ext/ratatui_ruby/Cargo.lock +11 -4
- data/ext/ratatui_ruby/Cargo.toml +2 -1
- data/ext/ratatui_ruby/src/events.rs +238 -26
- data/ext/ratatui_ruby/src/frame.rs +113 -1
- data/ext/ratatui_ruby/src/lib.rs +34 -4
- data/ext/ratatui_ruby/src/string_width.rs +101 -0
- data/ext/ratatui_ruby/src/terminal.rs +39 -15
- data/ext/ratatui_ruby/src/text.rs +1 -1
- data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
- data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/list.rs +179 -3
- data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
- data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +93 -1
- data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
- data/ext/ratatui_ruby/src/widgets/table.rs +113 -1
- data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
- data/lib/ratatui_ruby/cell.rb +4 -4
- data/lib/ratatui_ruby/event/key/character.rb +35 -0
- data/lib/ratatui_ruby/event/key/media.rb +44 -0
- data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
- data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
- data/lib/ratatui_ruby/event/key/system.rb +45 -0
- data/lib/ratatui_ruby/event/key.rb +111 -51
- data/lib/ratatui_ruby/event/mouse.rb +3 -3
- data/lib/ratatui_ruby/event/paste.rb +1 -1
- data/lib/ratatui_ruby/frame.rb +96 -0
- data/lib/ratatui_ruby/list_state.rb +88 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
- data/lib/ratatui_ruby/schema/cursor.rb +5 -0
- data/lib/ratatui_ruby/schema/gauge.rb +3 -1
- data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
- data/lib/ratatui_ruby/schema/list.rb +25 -4
- data/lib/ratatui_ruby/schema/list_item.rb +41 -0
- data/lib/ratatui_ruby/schema/rect.rb +43 -0
- data/lib/ratatui_ruby/schema/style.rb +24 -4
- data/lib/ratatui_ruby/schema/table.rb +21 -3
- data/lib/ratatui_ruby/schema/text.rb +69 -1
- data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
- data/lib/ratatui_ruby/session/autodoc.rb +65 -0
- data/lib/ratatui_ruby/session.rb +22 -7
- data/lib/ratatui_ruby/table_state.rb +90 -0
- data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
- data/lib/ratatui_ruby/test_helper/snapshot.rb +390 -0
- data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
- data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
- data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
- data/lib/ratatui_ruby/test_helper.rb +65 -358
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +42 -19
- data/sig/examples/app_stateful_interaction/app.rbs +33 -0
- data/sig/examples/widget_block_demo/app.rbs +32 -0
- data/sig/examples/{app_map_demo → widget_map_demo}/app.rbs +2 -2
- data/sig/examples/{app_table_select → widget_table_demo}/app.rbs +2 -2
- data/sig/examples/{widget_table_flex → widget_text_width}/app.rbs +2 -3
- data/sig/ratatui_ruby/event.rbs +11 -1
- data/sig/ratatui_ruby/frame.rbs +2 -0
- data/sig/ratatui_ruby/list_state.rbs +13 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -2
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
- data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
- data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
- data/sig/ratatui_ruby/schema/list.rbs +4 -2
- data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
- data/sig/ratatui_ruby/schema/rect.rbs +3 -0
- data/sig/ratatui_ruby/schema/style.rbs +3 -3
- data/sig/ratatui_ruby/schema/table.rbs +3 -1
- data/sig/ratatui_ruby/schema/text.rbs +8 -6
- data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
- data/sig/ratatui_ruby/session.rbs +13 -0
- data/sig/ratatui_ruby/table_state.rbs +15 -0
- data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
- data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
- data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
- data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
- data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
- data/sig/ratatui_ruby/test_helper.rbs +5 -4
- data/tasks/autodoc/examples.rb +79 -0
- data/tasks/autodoc/inventory.rb +9 -7
- data/tasks/autodoc.rake +11 -5
- data/tasks/bump/changelog.rb +3 -3
- data/tasks/bump/links.rb +67 -0
- data/tasks/sourcehut.rake +61 -21
- data/tasks/terminal_preview/app_screenshot.rb +13 -3
- data/tasks/terminal_preview/saved_screenshot.rb +4 -3
- metadata +111 -37
- data/doc/images/app_table_select.png +0 -0
- data/doc/images/widget_block_padding.png +0 -0
- data/doc/images/widget_block_titles.png +0 -0
- data/doc/images/widget_list_styles.png +0 -0
- data/examples/app_all_events/model/events.rb +0 -180
- data/examples/app_all_events/model/highlight.rb +0 -57
- data/examples/app_all_events/test/snapshots/after_focus_lost.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_focus_regained.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_key_a.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_mouse_click.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_multiple_events.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_paste.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_right_click.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/initial_state.txt +0 -24
- data/examples/app_all_events/view_state.rb +0 -42
- data/examples/app_color_picker/scene.rb +0 -201
- data/examples/widget_block_padding/app.rb +0 -67
- data/examples/widget_block_titles/app.rb +0 -69
- data/examples/widget_list_styles/app.rb +0 -141
- data/examples/widget_table_flex/app.rb +0 -95
- data/sig/examples/widget_block_padding/app.rbs +0 -11
- data/sig/examples/widget_block_titles/app.rbs +0 -11
- data/sig/examples/widget_list_styles/app.rbs +0 -11
- data/tasks/bump/comparison_links.rb +0 -41
- /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
|
@@ -8,62 +8,102 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
|
|
8
8
|
This example demonstrates how to build a **Feature-Rich Interactive Application** using `ratatui_ruby`.
|
|
9
9
|
|
|
10
10
|
It goes beyond simple widgets to show a complete, real-world architecture for handling:
|
|
11
|
-
- **Complex State Management** (Input validation,
|
|
11
|
+
- **Complex State Management** (Input validation, clipboard interaction)
|
|
12
12
|
- **Mouse Interaction & Hit Testing**
|
|
13
13
|
- **Dynamic Layouts**
|
|
14
14
|
- **Modal Dialogs**
|
|
15
15
|
|
|
16
|
-
## Architecture: The "
|
|
16
|
+
## Architecture: The "Proto-Kit" Pattern (Component-Based)
|
|
17
17
|
|
|
18
|
-
This app uses a
|
|
18
|
+
This app uses a **Strict Component-Based Architecture** where every UI element encapsulates its own **Rendering**, **State**, and **Event Handling**.
|
|
19
19
|
|
|
20
|
-
###
|
|
21
|
-
The main `App` class (`app.rb`) acts as the Controller. It:
|
|
22
|
-
- Holds the source of truth (the State).
|
|
23
|
-
- Runs the Event Loop.
|
|
24
|
-
- Routes input events to the appropriate handler.
|
|
25
|
-
- Initializes the `Scene`.
|
|
20
|
+
### The Component Contract
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
The `Scene` class (`scene.rb`) acts as the primary View. Unlike simple examples where the render logic is in the `App` class, here the **Scene owns the Layout**.
|
|
29
|
-
- **Composition**: It takes purely logical objects (`Palette`, `Input`) and decides how to present them.
|
|
30
|
-
- **Hit Testing**: Crucially, the Scene **caches layout rectangles** (like `@export_area_rect`) during the render pass so the Controller knows *where* things are to handle clicks later.
|
|
22
|
+
Every component implements this duck-type interface:
|
|
31
23
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
```ruby
|
|
25
|
+
# Renders the component into the given area
|
|
26
|
+
# Caches `area` for hit testing
|
|
27
|
+
def render(tui, frame, area)
|
|
28
|
+
@area = area
|
|
29
|
+
# ... render using frame.render_widget
|
|
30
|
+
end
|
|
38
31
|
|
|
39
|
-
|
|
32
|
+
# Processes events; returns a symbolic signal or nil
|
|
33
|
+
def handle_event(event) -> Symbol | nil
|
|
34
|
+
# Returns :consumed, :submitted, :copy_requested, etc.
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Optional: time-based updates
|
|
38
|
+
def tick
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 1. The MainContainer (Orchestrator)
|
|
43
|
+
|
|
44
|
+
The `MainContainer` class (`main_container.rb`) owns all child components and orchestrates the UI:
|
|
45
|
+
|
|
46
|
+
- **Layout Phase:** Calculates `Rect`s using `tui.layout_split`.
|
|
47
|
+
- **Delegation Phase:** Calls `child.render(tui, frame, child_area)` for each component.
|
|
48
|
+
- **Event Routing (Chain of Responsibility):** Delegates events front-to-back. The modal dialog gets priority when active.
|
|
49
|
+
- **Mediator Pattern:** Interprets symbolic signals (`:submitted`, `:copy_requested`) to coordinate cross-component effects.
|
|
50
|
+
|
|
51
|
+
### 2. Self-Contained Components
|
|
52
|
+
|
|
53
|
+
Each UI element is a self-contained component:
|
|
54
|
+
|
|
55
|
+
- **`Input`**: Text entry with validation. Returns `:submitted` when Enter is pressed.
|
|
56
|
+
- **`Palette`**: Displays color harmonies. Accepts `update_color` from the container.
|
|
57
|
+
- **`ExportPane`**: Shows HEX/RGB/HSL formats. Returns `:copy_requested` when clicked.
|
|
58
|
+
- **`Controls`**: Displays keyboard shortcuts. Has a `tick` lifecycle for clipboard feedback.
|
|
59
|
+
- **`CopyDialog`**: Modal confirmation dialog. Returns `:consumed` when handling events.
|
|
60
|
+
|
|
61
|
+
### 3. The App (Minimal Runner)
|
|
62
|
+
|
|
63
|
+
The `App` class (`app.rb`) is a thin runner:
|
|
64
|
+
- Creates the `MainContainer`.
|
|
65
|
+
- Runs the main loop: `tick` → `render` → `poll` → `handle_event`.
|
|
66
|
+
- Checks for quit events.
|
|
40
67
|
|
|
41
68
|
## Key Features Showcased
|
|
42
69
|
|
|
43
|
-
### 🖱️
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
70
|
+
### 🖱️ Encapsulated Hit Testing
|
|
71
|
+
|
|
72
|
+
Components cache their render area (`@area`) during `render`. In `handle_event`, they check `@area&.contains?(x, y)` to detect clicks. The container never calculates coordinates—hit testing is fully encapsulated.
|
|
73
|
+
|
|
74
|
+
### 🔲 Modal Dialogs via Chain of Responsibility
|
|
75
|
+
|
|
76
|
+
When `CopyDialog` is active, the `MainContainer` offers it events first. If it returns `:consumed`, event propagation stops. This creates modal behavior without explicit flags in the app.
|
|
77
|
+
|
|
78
|
+
### 📡 Symbolic Signals (Mediator Pattern)
|
|
79
|
+
|
|
80
|
+
Components return semantic symbols instead of just `:consumed`:
|
|
81
|
+
- `Input` returns `:submitted` when the user presses Enter.
|
|
82
|
+
- `ExportPane` returns `:copy_requested` when clicked.
|
|
83
|
+
|
|
84
|
+
The `MainContainer` interprets these signals to coordinate cross-component communication:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
result = @input.handle_event(event)
|
|
88
|
+
case result
|
|
89
|
+
when :submitted
|
|
90
|
+
@palette.update_color(@input.parsed_color)
|
|
91
|
+
return :consumed
|
|
92
|
+
end
|
|
93
|
+
```
|
|
47
94
|
|
|
48
|
-
###
|
|
49
|
-
See `CopyDialog`.
|
|
50
|
-
The app implements a modal overlay that intercepts input.
|
|
51
|
-
- **Pattern**: The `App` checks `if dialog.active?`. If true, it routes events *only* to the dialog, effectively "blocking" the main UI.
|
|
95
|
+
### ⏱️ Lifecycle Hooks (`tick`)
|
|
52
96
|
|
|
53
|
-
|
|
54
|
-
- **Dynamic Constraints**: Layouts that adapt to content.
|
|
55
|
-
- **Visual Feedback**:
|
|
56
|
-
- Input fields turn red on error.
|
|
57
|
-
- Clipboard messages fade out over time (`Clipboard#tick`).
|
|
58
|
-
- Text colors automatically adjust for contrast (Black text on light backgrounds, White on dark).
|
|
97
|
+
Components can have time-based updates. `Controls#tick` delegates to `Clipboard#tick` to decrement the feedback timer.
|
|
59
98
|
|
|
60
|
-
## Problem Solving: What
|
|
99
|
+
## Problem Solving: What You Can Learn
|
|
61
100
|
|
|
62
101
|
Read this example if you are trying to solve:
|
|
63
|
-
1. **"How do I structure a larger app?"**
|
|
64
|
-
2. **"How do I handle mouse clicks?"**
|
|
65
|
-
3. **"How do I make a popup?"**
|
|
66
|
-
4. **"How do I
|
|
102
|
+
1. **"How do I structure a larger app?"** → Use the Component Contract and a Container for orchestration.
|
|
103
|
+
2. **"How do I handle mouse clicks?"** → Cache `@area` during render; check `contains?` in `handle_event`.
|
|
104
|
+
3. **"How do I make a popup?"** → Use Chain of Responsibility: the active modal gets events first.
|
|
105
|
+
4. **"How do I coordinate between components?"** → Use symbolic signals and the Mediator pattern.
|
|
106
|
+
5. **"How do I validate input?"** → Encapsulate validation inside the `Input` component.
|
|
67
107
|
|
|
68
108
|
## Usage
|
|
69
109
|
|
|
@@ -79,16 +119,16 @@ ruby examples/app_color_picker/app.rb
|
|
|
79
119
|
|
|
80
120
|
Complex applications require structured state habits. This Color Picker and the [App All Events](../app_all_events/README.md) example demonstrate two different approaches.
|
|
81
121
|
|
|
82
|
-
### The Tool Approach (Color Picker)
|
|
122
|
+
### The Tool Approach (Color Picker - Proto-Kit)
|
|
83
123
|
|
|
84
|
-
Tools require interaction. Users click buttons and drag sliders.
|
|
124
|
+
Tools require interaction. Users click buttons and drag sliders. Components need to know where they exist on screen for hit testing. The Container orchestrates cross-component effects.
|
|
85
125
|
|
|
86
|
-
This example uses
|
|
126
|
+
This example uses the **Proto-Kit (Component-Based)** pattern. Each component owns its own state, rendering, and event handling. The Container routes events and mediates communication.
|
|
87
127
|
|
|
88
128
|
Use this pattern for forms, editors, and mouse-driven tools.
|
|
89
129
|
|
|
90
|
-
### The Dashboard Approach (AppAllEvents)
|
|
130
|
+
### The Dashboard Approach (AppAllEvents - Proto-TEA)
|
|
91
131
|
|
|
92
|
-
Dashboards display data. They rarely require complex mouse interaction.
|
|
132
|
+
Dashboards display data. They rarely require complex mouse interaction. Proto-TEA (Model-View-Update) works best there. State is immutable. Logic is pure. Updates are predictable. This simplifies testing.
|
|
93
133
|
|
|
94
134
|
Use that pattern for logs, monitors, and data viewers.
|
|
@@ -7,11 +7,7 @@ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
|
|
|
7
7
|
$LOAD_PATH.unshift File.expand_path(__dir__)
|
|
8
8
|
|
|
9
9
|
require "ratatui_ruby"
|
|
10
|
-
require_relative "
|
|
11
|
-
require_relative "palette"
|
|
12
|
-
require_relative "clipboard"
|
|
13
|
-
require_relative "copy_dialog"
|
|
14
|
-
require_relative "scene"
|
|
10
|
+
require_relative "main_container"
|
|
15
11
|
|
|
16
12
|
# A terminal-based color picker application.
|
|
17
13
|
#
|
|
@@ -21,26 +17,27 @@ require_relative "scene"
|
|
|
21
17
|
# This application solves the problem by providing an interactive interface. It parses hex strings,
|
|
22
18
|
# generates palettes, and displays them visually in the terminal.
|
|
23
19
|
#
|
|
24
|
-
#
|
|
20
|
+
# === Architecture
|
|
21
|
+
#
|
|
22
|
+
# This example uses the Proto-Kit (Component-Based) pattern:
|
|
23
|
+
# - **Components**: Self-contained UI elements with `render`, `handle_event`, and optional `tick`
|
|
24
|
+
# - **Container**: Owns layout, delegates to children, routes events via Chain of Responsibility
|
|
25
|
+
# - **Mediator**: Container interprets symbolic signals (`:consumed`, `:submitted`) for cross-component effects
|
|
25
26
|
#
|
|
26
27
|
# === Examples
|
|
27
28
|
#
|
|
28
29
|
# AppColorPicker.new.run
|
|
29
30
|
#
|
|
30
31
|
class AppColorPicker
|
|
31
|
-
# Creates a new <tt>AppColorPicker</tt> instance
|
|
32
|
+
# Creates a new <tt>AppColorPicker</tt> instance.
|
|
32
33
|
def initialize
|
|
33
|
-
@
|
|
34
|
-
@palette = Palette.new(@input.parse)
|
|
35
|
-
@clipboard = Clipboard.new
|
|
36
|
-
@dialog = CopyDialog.new(@clipboard)
|
|
37
|
-
@scene = nil
|
|
34
|
+
@container = nil
|
|
38
35
|
end
|
|
39
36
|
|
|
40
37
|
# Starts the terminal session and enters the main event loop.
|
|
41
38
|
#
|
|
42
|
-
# This method initializes the terminal,
|
|
43
|
-
#
|
|
39
|
+
# This method initializes the terminal, creates the MainContainer, and runs
|
|
40
|
+
# the event loop until the user quits.
|
|
44
41
|
#
|
|
45
42
|
# === Example
|
|
46
43
|
#
|
|
@@ -49,62 +46,27 @@ class AppColorPicker
|
|
|
49
46
|
#
|
|
50
47
|
def run
|
|
51
48
|
RatatuiRuby.run do |tui|
|
|
52
|
-
@
|
|
53
|
-
loop do
|
|
54
|
-
render(tui)
|
|
55
|
-
result = handle_input(tui)
|
|
56
|
-
break if result == :quit
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private def render(tui)
|
|
62
|
-
@clipboard.tick
|
|
63
|
-
tui.draw do |frame|
|
|
64
|
-
@scene.render(frame, input: @input, palette: @palette, clipboard: @clipboard, dialog: @dialog)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
49
|
+
@container = MainContainer.new(tui)
|
|
67
50
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
51
|
+
loop do
|
|
52
|
+
@container.tick
|
|
53
|
+
tui.draw { |frame| @container.render(tui, frame, frame.area) }
|
|
71
54
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
else
|
|
75
|
-
handle_main_input(event)
|
|
76
|
-
end
|
|
77
|
-
end
|
|
55
|
+
event = tui.poll_event
|
|
56
|
+
break if quit_event?(event)
|
|
78
57
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
case event
|
|
82
|
-
in { type: :key, code: "q" } | { type: :key, code: "esc" } | { type: :key, code: "c", modifiers: ["ctrl"] }
|
|
83
|
-
:quit
|
|
84
|
-
else
|
|
85
|
-
result
|
|
58
|
+
@container.handle_event(event)
|
|
59
|
+
end
|
|
86
60
|
end
|
|
87
61
|
end
|
|
88
62
|
|
|
89
|
-
private def
|
|
63
|
+
private def quit_event?(event)
|
|
90
64
|
case event
|
|
91
|
-
in { type: :key, code: "q" } | { type: :key, code: "esc" } |
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
@palette = Palette.new(@input.parse)
|
|
95
|
-
in { type: :key, code: "backspace" }
|
|
96
|
-
@input.delete_char
|
|
97
|
-
in { type: :paste, content: }
|
|
98
|
-
@input.set(content)
|
|
99
|
-
@palette = Palette.new(@input.parse)
|
|
100
|
-
in { type: :key, code: code }
|
|
101
|
-
@input.append_char(code)
|
|
102
|
-
in { type: :mouse, kind: "down", button: "left", x:, y: }
|
|
103
|
-
if @scene && @scene.export_rect&.contains?(x, y) && @palette.main
|
|
104
|
-
@dialog.open(@palette.main.hex)
|
|
105
|
-
end
|
|
65
|
+
in { type: :key, code: "q" } | { type: :key, code: "esc" } |
|
|
66
|
+
{ type: :key, code: "c", modifiers: [/ctrl/] }
|
|
67
|
+
true
|
|
106
68
|
else
|
|
107
|
-
|
|
69
|
+
false
|
|
108
70
|
end
|
|
109
71
|
end
|
|
110
72
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
# A display-only component showing keyboard shortcuts and clipboard feedback.
|
|
7
|
+
#
|
|
8
|
+
# Users need to know what keys are available. They also need feedback when
|
|
9
|
+
# they copy a color. This component renders the controls section.
|
|
10
|
+
#
|
|
11
|
+
# === Component Contract
|
|
12
|
+
#
|
|
13
|
+
# - `render(tui, frame, area, clipboard:)`: Draws the controls; stores `area`
|
|
14
|
+
# - `handle_event(event) -> nil`: Display-only, always returns nil
|
|
15
|
+
# - `tick`: Delegates to clipboard for time-based feedback updates
|
|
16
|
+
#
|
|
17
|
+
# === Example
|
|
18
|
+
#
|
|
19
|
+
# controls = Controls.new
|
|
20
|
+
# controls.render(tui, frame, area, clipboard: clipboard)
|
|
21
|
+
# controls.tick(clipboard)
|
|
22
|
+
class Controls
|
|
23
|
+
def initialize
|
|
24
|
+
@area = nil
|
|
25
|
+
@hotkey_style = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The cached render area.
|
|
29
|
+
attr_reader :area
|
|
30
|
+
|
|
31
|
+
# Renders the controls section into the given area.
|
|
32
|
+
#
|
|
33
|
+
# Shows keyboard shortcuts and clipboard feedback message if one is active.
|
|
34
|
+
#
|
|
35
|
+
# [tui] Session or TUI factory object
|
|
36
|
+
# [frame] Frame object from RatatuiRuby.draw block
|
|
37
|
+
# [area] Rect area to draw into
|
|
38
|
+
# [clipboard] Clipboard object for feedback message
|
|
39
|
+
#
|
|
40
|
+
# === Example
|
|
41
|
+
#
|
|
42
|
+
# controls.render(tui, frame, control_area, clipboard: clipboard)
|
|
43
|
+
def render(tui, frame, area, clipboard:)
|
|
44
|
+
@area = area
|
|
45
|
+
@hotkey_style ||= tui.style(modifiers: [:bold, :underlined])
|
|
46
|
+
widget = build_widget(tui, clipboard)
|
|
47
|
+
frame.render_widget(widget, area)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Display-only component; always returns nil.
|
|
51
|
+
def handle_event(_event)
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Delegates tick to the clipboard for time-based updates.
|
|
56
|
+
#
|
|
57
|
+
# [clipboard] Clipboard object to tick
|
|
58
|
+
def tick(clipboard)
|
|
59
|
+
clipboard.tick
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private def build_widget(tui, clipboard)
|
|
63
|
+
control_lines = [
|
|
64
|
+
tui.text_line(spans: [
|
|
65
|
+
tui.text_span(content: "a-z/0-9", style: @hotkey_style),
|
|
66
|
+
tui.text_span(content: ": Type "),
|
|
67
|
+
tui.text_span(content: "enter", style: @hotkey_style),
|
|
68
|
+
tui.text_span(content: ": Parse "),
|
|
69
|
+
tui.text_span(content: "bksp", style: @hotkey_style),
|
|
70
|
+
tui.text_span(content: ": Erase "),
|
|
71
|
+
tui.text_span(content: "esc", style: @hotkey_style),
|
|
72
|
+
tui.text_span(content: ": Quit"),
|
|
73
|
+
]),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
unless clipboard.message.empty?
|
|
77
|
+
control_lines << tui.text_line(spans: [
|
|
78
|
+
tui.text_span(content: clipboard.message, style: tui.style(fg: :green, modifiers: [:bold])),
|
|
79
|
+
])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
tui.block(
|
|
83
|
+
title: "Controls",
|
|
84
|
+
borders: [:all],
|
|
85
|
+
children: [
|
|
86
|
+
tui.paragraph(text: control_lines),
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -5,39 +5,41 @@
|
|
|
5
5
|
|
|
6
6
|
require_relative "clipboard"
|
|
7
7
|
|
|
8
|
-
# A
|
|
8
|
+
# A self-contained modal dialog component for copying text to the clipboard.
|
|
9
9
|
#
|
|
10
10
|
# Users click on content they want to copy. The app needs to confirm: "Are you
|
|
11
|
-
# sure?"
|
|
12
|
-
#
|
|
11
|
+
# sure?" This component owns dialog state, renders itself, and handles keyboard
|
|
12
|
+
# input.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
# to keyboard input. It delegates clipboard operations to a Clipboard.
|
|
14
|
+
# === Component Contract
|
|
16
15
|
#
|
|
17
|
-
#
|
|
16
|
+
# - `render(tui, frame, area)`: Draws the dialog; stores `area`
|
|
17
|
+
# - `handle_event(event) -> Symbol | nil`: Returns `:consumed` when handled
|
|
18
|
+
# - `open(text)`: Opens the dialog with the text to copy
|
|
19
|
+
# - `close`: Closes the dialog
|
|
20
|
+
# - `active?`: True if the dialog is visible
|
|
18
21
|
#
|
|
19
22
|
# === Example
|
|
20
23
|
#
|
|
21
|
-
# clipboard = Clipboard.new
|
|
22
24
|
# dialog = CopyDialog.new(clipboard)
|
|
23
|
-
#
|
|
24
|
-
# # Open the dialog
|
|
25
25
|
# dialog.open("#FF0000")
|
|
26
|
-
# dialog.active? # => true
|
|
27
26
|
#
|
|
28
|
-
#
|
|
29
|
-
# result
|
|
27
|
+
# result = dialog.handle_event(event)
|
|
28
|
+
# # result == :consumed when dialog handled the event
|
|
30
29
|
#
|
|
31
|
-
#
|
|
32
|
-
# widget = dialog.render(tui, area)
|
|
30
|
+
# dialog.render(tui, frame, center_area)
|
|
33
31
|
class CopyDialog
|
|
34
32
|
def initialize(clipboard)
|
|
35
33
|
@clipboard = clipboard
|
|
36
34
|
@text = ""
|
|
37
35
|
@selected = :yes
|
|
38
36
|
@active = false
|
|
37
|
+
@area = nil
|
|
39
38
|
end
|
|
40
39
|
|
|
40
|
+
# The cached render area.
|
|
41
|
+
attr_reader :area
|
|
42
|
+
|
|
41
43
|
# Opens the dialog with text to copy.
|
|
42
44
|
#
|
|
43
45
|
# Initializes selection to <tt>:yes</tt> and sets active to true.
|
|
@@ -48,7 +50,6 @@ class CopyDialog
|
|
|
48
50
|
#
|
|
49
51
|
# dialog.open("#FF0000")
|
|
50
52
|
# dialog.active? # => true
|
|
51
|
-
# dialog.text # => "#FF0000"
|
|
52
53
|
def open(text)
|
|
53
54
|
@text = text
|
|
54
55
|
@selected = :yes
|
|
@@ -61,73 +62,68 @@ class CopyDialog
|
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
# True if the dialog is currently open and visible.
|
|
65
|
+
def active?
|
|
66
|
+
@active
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Renders the dialog into the given area.
|
|
70
|
+
#
|
|
71
|
+
# Shows the text to copy, Yes/No buttons with current selection highlighted,
|
|
72
|
+
# and keyboard instructions.
|
|
73
|
+
#
|
|
74
|
+
# [tui] Session or TUI factory object
|
|
75
|
+
# [frame] Frame object from RatatuiRuby.draw block
|
|
76
|
+
# [area] Rect area to draw into
|
|
64
77
|
#
|
|
65
78
|
# === Example
|
|
66
79
|
#
|
|
67
|
-
# dialog.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@active
|
|
80
|
+
# dialog.render(tui, frame, center_area)
|
|
81
|
+
def render(tui, frame, area)
|
|
82
|
+
@area = area
|
|
83
|
+
widget = build_widget(tui)
|
|
84
|
+
frame.render_widget(widget, area)
|
|
73
85
|
end
|
|
74
86
|
|
|
75
87
|
# Processes a keyboard event and updates selection or closes the dialog.
|
|
76
88
|
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
89
|
+
# Returns:
|
|
90
|
+
# - `:consumed` when the event was handled
|
|
91
|
+
# - `nil` when the event was ignored or dialog is inactive
|
|
80
92
|
#
|
|
81
|
-
# [event]
|
|
93
|
+
# [event] Event from RatatuiRuby.poll_event
|
|
82
94
|
#
|
|
83
95
|
# === Example
|
|
84
96
|
#
|
|
85
|
-
# dialog.
|
|
86
|
-
|
|
87
|
-
# dialog.handle_input({ type: :key, code: "enter" })
|
|
88
|
-
# dialog.active? # => false
|
|
89
|
-
def handle_input(event)
|
|
97
|
+
# result = dialog.handle_event(event)
|
|
98
|
+
def handle_event(event)
|
|
90
99
|
return nil unless @active
|
|
91
100
|
|
|
92
101
|
case event
|
|
93
102
|
in { type: :key, code: "left" } | { type: :key, code: "h" }
|
|
94
103
|
@selected = :yes
|
|
95
|
-
|
|
104
|
+
:consumed
|
|
96
105
|
in { type: :key, code: "right" } | { type: :key, code: "l" }
|
|
97
106
|
@selected = :no
|
|
98
|
-
|
|
107
|
+
:consumed
|
|
99
108
|
in { type: :key, code: "enter" }
|
|
100
109
|
if @selected == :yes
|
|
101
110
|
@clipboard.copy(@text)
|
|
102
111
|
end
|
|
103
112
|
@active = false
|
|
104
|
-
|
|
113
|
+
:consumed
|
|
105
114
|
in { type: :key, code: "y" }
|
|
106
115
|
@clipboard.copy(@text)
|
|
107
116
|
@active = false
|
|
108
|
-
|
|
117
|
+
:consumed
|
|
109
118
|
in { type: :key, code: "n" }
|
|
110
119
|
@active = false
|
|
111
|
-
|
|
120
|
+
:consumed
|
|
112
121
|
else
|
|
113
122
|
nil
|
|
114
123
|
end
|
|
115
124
|
end
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
#
|
|
119
|
-
# Shows the text to copy, Yes/No buttons with current selection highlighted,
|
|
120
|
-
# and keyboard instructions. Renders only when active.
|
|
121
|
-
#
|
|
122
|
-
# [tui] Session or TUI factory object
|
|
123
|
-
# [area] Rect area for the dialog
|
|
124
|
-
#
|
|
125
|
-
# === Example
|
|
126
|
-
#
|
|
127
|
-
# dialog.open("#FF0000")
|
|
128
|
-
# widget = dialog.render(tui, center_area)
|
|
129
|
-
# frame.render_widget(widget, center_area)
|
|
130
|
-
def render(tui, area)
|
|
126
|
+
private def build_widget(tui)
|
|
131
127
|
yes_style = if @selected == :yes
|
|
132
128
|
tui.style(bg: :cyan, fg: :black, modifiers: [:bold])
|
|
133
129
|
else
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
# A self-contained component displaying export formats for a color.
|
|
7
|
+
#
|
|
8
|
+
# Users need to copy color values in different formats (HEX, RGB, HSL).
|
|
9
|
+
# This component renders the export section and detects clicks on itself.
|
|
10
|
+
#
|
|
11
|
+
# === Component Contract
|
|
12
|
+
#
|
|
13
|
+
# - `render(tui, frame, area, palette:)`: Draws the export formats; stores `area` for hit testing
|
|
14
|
+
# - `handle_event(event) -> Symbol | nil`: Returns `:copy_requested` when clicked
|
|
15
|
+
#
|
|
16
|
+
# === Example
|
|
17
|
+
#
|
|
18
|
+
# export_pane = ExportPane.new
|
|
19
|
+
# export_pane.render(tui, frame, area, palette: palette)
|
|
20
|
+
#
|
|
21
|
+
# result = export_pane.handle_event(event)
|
|
22
|
+
# if result == :copy_requested && palette.main
|
|
23
|
+
# dialog.open(palette.main.hex)
|
|
24
|
+
# end
|
|
25
|
+
class ExportPane
|
|
26
|
+
def initialize
|
|
27
|
+
@area = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The cached render area, for hit testing.
|
|
31
|
+
attr_reader :area
|
|
32
|
+
|
|
33
|
+
# Renders the export formats section into the given area.
|
|
34
|
+
#
|
|
35
|
+
# Shows HEX, RGB, and HSL values for the current color. If no color is set,
|
|
36
|
+
# displays a placeholder message.
|
|
37
|
+
#
|
|
38
|
+
# [tui] Session or TUI factory object
|
|
39
|
+
# [frame] Frame object from RatatuiRuby.draw block
|
|
40
|
+
# [area] Rect area to draw into
|
|
41
|
+
# [palette] Palette object containing the color to display
|
|
42
|
+
#
|
|
43
|
+
# === Example
|
|
44
|
+
#
|
|
45
|
+
# export_pane.render(tui, frame, export_area, palette: palette)
|
|
46
|
+
def render(tui, frame, area, palette:)
|
|
47
|
+
@area = area
|
|
48
|
+
widget = build_widget(tui, palette)
|
|
49
|
+
frame.render_widget(widget, area)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Processes a mouse event and returns a signal if clicked.
|
|
53
|
+
#
|
|
54
|
+
# Returns:
|
|
55
|
+
# - `:copy_requested` when the pane is clicked (caller should open copy dialog)
|
|
56
|
+
# - `nil` when the event was ignored or outside the area
|
|
57
|
+
#
|
|
58
|
+
# [event] Event from RatatuiRuby.poll_event
|
|
59
|
+
#
|
|
60
|
+
# === Example
|
|
61
|
+
#
|
|
62
|
+
# result = export_pane.handle_event(event)
|
|
63
|
+
# if result == :copy_requested
|
|
64
|
+
# dialog.open(palette.main.hex)
|
|
65
|
+
# end
|
|
66
|
+
def handle_event(event)
|
|
67
|
+
case event
|
|
68
|
+
in { type: :mouse, kind: "down", button: "left", x:, y: }
|
|
69
|
+
if @area&.contains?(x, y)
|
|
70
|
+
:copy_requested
|
|
71
|
+
end
|
|
72
|
+
else
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private def build_widget(tui, palette)
|
|
78
|
+
if palette.main.nil?
|
|
79
|
+
tui.block(
|
|
80
|
+
title: "Export Formats",
|
|
81
|
+
borders: [:all],
|
|
82
|
+
children: [
|
|
83
|
+
tui.paragraph(
|
|
84
|
+
text: tui.text_line(spans: [
|
|
85
|
+
tui.text_span(content: "Enter a color to see formats"),
|
|
86
|
+
])
|
|
87
|
+
),
|
|
88
|
+
]
|
|
89
|
+
)
|
|
90
|
+
else
|
|
91
|
+
build_color_widget(tui, palette.main)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private def build_color_widget(tui, color)
|
|
96
|
+
hex = color.hex
|
|
97
|
+
rgb = color.rgb
|
|
98
|
+
hsl = color.hsl_string
|
|
99
|
+
text_color = color.contrasting_text_color
|
|
100
|
+
bg_style = tui.style(bg: hex, fg: text_color)
|
|
101
|
+
|
|
102
|
+
tui.block(
|
|
103
|
+
title: "Export Formats",
|
|
104
|
+
borders: [:all],
|
|
105
|
+
style: bg_style,
|
|
106
|
+
children: [
|
|
107
|
+
tui.paragraph(
|
|
108
|
+
text: [
|
|
109
|
+
tui.text_line(spans: [
|
|
110
|
+
tui.text_span(content: "HEX: ", style: bg_style),
|
|
111
|
+
tui.text_span(content: hex, style: tui.style(bg: hex, fg: text_color, modifiers: [:underlined])),
|
|
112
|
+
]),
|
|
113
|
+
tui.text_line(spans: [
|
|
114
|
+
tui.text_span(content: "RGB: ", style: bg_style),
|
|
115
|
+
tui.text_span(content: rgb, style: tui.style(bg: hex, fg: text_color, modifiers: [:underlined])),
|
|
116
|
+
]),
|
|
117
|
+
tui.text_line(spans: [
|
|
118
|
+
tui.text_span(content: "HSL: ", style: bg_style),
|
|
119
|
+
tui.text_span(content: hsl, style: tui.style(bg: hex, fg: text_color, modifiers: [:underlined])),
|
|
120
|
+
]),
|
|
121
|
+
]
|
|
122
|
+
),
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
end
|