ratatui_ruby 1.0.0 → 1.1.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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +1 -1
  3. data/.builds/ruby-3.3.yml +1 -1
  4. data/.builds/ruby-3.4.yml +1 -1
  5. data/.builds/ruby-4.0.0.yml +1 -1
  6. data/AGENTS.md +3 -2
  7. data/CHANGELOG.md +33 -7
  8. data/Steepfile +1 -0
  9. data/doc/concepts/application_testing.md +5 -5
  10. data/doc/concepts/event_handling.md +1 -1
  11. data/doc/contributors/design/ruby_frontend.md +40 -12
  12. data/doc/contributors/design/rust_backend.md +13 -1
  13. data/doc/contributors/releasing.md +215 -0
  14. data/doc/contributors/todo/align/api_completeness_audit-finished.md +6 -0
  15. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +1 -7
  16. data/doc/contributors/todo/align/term.md +351 -0
  17. data/doc/contributors/upstream_requests/paragraph_span_rects.md +259 -0
  18. data/doc/getting_started/quickstart.md +1 -1
  19. data/doc/getting_started/why.md +3 -3
  20. data/doc/images/app_external_editor.gif +0 -0
  21. data/doc/index.md +1 -6
  22. data/examples/app_external_editor/README.md +62 -0
  23. data/examples/app_external_editor/app.rb +344 -0
  24. data/examples/widget_list/app.rb +2 -4
  25. data/examples/widget_table/app.rb +8 -2
  26. data/ext/ratatui_ruby/Cargo.lock +1 -1
  27. data/ext/ratatui_ruby/Cargo.toml +1 -1
  28. data/ext/ratatui_ruby/src/events.rs +171 -203
  29. data/ext/ratatui_ruby/src/lib.rs +36 -0
  30. data/ext/ratatui_ruby/src/lib_header.rs +11 -0
  31. data/ext/ratatui_ruby/src/terminal/capabilities.rs +46 -0
  32. data/ext/ratatui_ruby/src/terminal/init.rs +92 -0
  33. data/ext/ratatui_ruby/src/terminal/mod.rs +12 -3
  34. data/ext/ratatui_ruby/src/terminal/queries.rs +15 -0
  35. data/ext/ratatui_ruby/src/terminal/query.rs +64 -2
  36. data/lib/ratatui_ruby/backend/window_size.rb +50 -0
  37. data/lib/ratatui_ruby/backend.rb +59 -0
  38. data/lib/ratatui_ruby/event/key/navigation.rb +10 -1
  39. data/lib/ratatui_ruby/event/key.rb +84 -0
  40. data/lib/ratatui_ruby/event/mouse.rb +95 -3
  41. data/lib/ratatui_ruby/event/resize.rb +45 -3
  42. data/lib/ratatui_ruby/layout/alignment.rb +91 -0
  43. data/lib/ratatui_ruby/layout/layout.rb +1 -2
  44. data/lib/ratatui_ruby/layout/size.rb +10 -3
  45. data/lib/ratatui_ruby/layout.rb +4 -0
  46. data/lib/ratatui_ruby/terminal/capabilities.rb +316 -0
  47. data/lib/ratatui_ruby/terminal/viewport.rb +1 -1
  48. data/lib/ratatui_ruby/terminal.rb +66 -0
  49. data/lib/ratatui_ruby/test_helper/global_state.rb +111 -0
  50. data/lib/ratatui_ruby/test_helper.rb +3 -0
  51. data/lib/ratatui_ruby/version.rb +1 -1
  52. data/lib/ratatui_ruby/widgets/table.rb +2 -2
  53. data/lib/ratatui_ruby.rb +25 -4
  54. data/sig/examples/app_external_editor/app.rbs +12 -0
  55. data/sig/generated/event_key_predicates.rbs +1348 -0
  56. data/sig/ratatui_ruby/backend/window_size.rbs +17 -0
  57. data/sig/ratatui_ruby/backend.rbs +12 -0
  58. data/sig/ratatui_ruby/event.rbs +7 -0
  59. data/sig/ratatui_ruby/layout/alignment.rbs +26 -0
  60. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -0
  61. data/sig/ratatui_ruby/terminal/capabilities.rbs +38 -0
  62. data/sig/ratatui_ruby/terminal/viewport.rbs +15 -1
  63. data/tasks/bump/bump_workflow.rb +49 -0
  64. data/tasks/bump/changelog.rb +57 -0
  65. data/tasks/bump/patch_release.rb +19 -0
  66. data/tasks/bump/release_branch.rb +17 -0
  67. data/tasks/bump/release_from_trunk.rb +49 -0
  68. data/tasks/bump/repository.rb +54 -0
  69. data/tasks/bump/ruby_gem.rb +6 -26
  70. data/tasks/bump/sem_ver.rb +4 -0
  71. data/tasks/bump/unreleased_section.rb +17 -0
  72. data/tasks/bump.rake +21 -11
  73. data/tasks/doc/documentation.rb +59 -0
  74. data/tasks/doc/link/file_url.rb +30 -0
  75. data/tasks/doc/link/relative_path.rb +61 -0
  76. data/tasks/doc/link/web_url.rb +55 -0
  77. data/tasks/doc/link.rb +52 -0
  78. data/tasks/doc/link_audit.rb +116 -0
  79. data/tasks/doc/problem.rb +40 -0
  80. data/tasks/doc/source_file.rb +93 -0
  81. data/tasks/doc.rake +18 -0
  82. data/tasks/rbs_predicates/predicate_catalog.rb +52 -0
  83. data/tasks/rbs_predicates/predicate_tests.rb +124 -0
  84. data/tasks/rbs_predicates/rbs_signature.rb +63 -0
  85. data/tasks/rbs_predicates.rake +31 -0
  86. data/tasks/test.rake +3 -0
  87. data/tasks/website/version.rb +23 -28
  88. metadata +38 -1
@@ -0,0 +1,62 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # External Editor Example
7
+
8
+ [![External Editor](../../doc/images/app_external_editor.gif)](app.rb)
9
+
10
+ Demonstrates temporarily exiting the TUI to invoke an external editor, then seamlessly re-entering—like editing a commit message in lazygit or tig.
11
+
12
+ Most applications use `RatatuiRuby.run { }` which handles terminal setup and teardown automatically. But some workflows require **repeatedly leaving and re-entering raw mode** during a single session. This example shows the low-level lifecycle API that makes this possible.
13
+
14
+ ## Features Demonstrated
15
+
16
+ - **Full Lifecycle Control**: Using `init_terminal` and `restore_terminal` directly instead of the `run` block.
17
+ - **External Process Invocation**: Safely restoring the terminal before spawning an interactive subprocess.
18
+ - **Session Re-entry**: Returning to the TUI after the external process exits, with state preserved.
19
+ - **Split-Pane Layout**: Dynamically splitting the display when scratch content exists.
20
+ - **Focus-Aware Scrolling**: Keyboard scrolling affects the focused pane; mouse scroll affects the hovered pane.
21
+ - **Wrapped Line Clamping**: Scroll limits based on wrapped (not raw) line count using `paragraph.line_count`.
22
+ - **Mouse Hit Testing**: Using `area.contains?(x, y)` to detect which pane is hovered.
23
+
24
+ ## Hotkeys
25
+
26
+ | Key | Action |
27
+ |-----|--------|
28
+ | `↑`/`↓` or `j`/`k` | Scroll one line |
29
+ | `PgUp`/`PgDn` | Scroll one page |
30
+ | `Home`/`End` | Jump to top/bottom |
31
+ | Mouse wheel | Scroll hovered pane |
32
+ | `Tab` or `←`/`→` or `h`/`l` | Switch focus between panes |
33
+ | `e` | Edit this README.md in `$EDITOR` |
34
+ | `s` | Edit scratch file; saved content appears in split pane |
35
+ | `q` | Quit |
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_external_editor/app.rb
46
+ ```
47
+ <!-- SPDX-SnippetEnd -->
48
+
49
+ Press `e` to edit this README. Press `s` to open a scratch file—when you save content, it appears beside the README in a split view.
50
+
51
+ ## Learning Outcomes
52
+
53
+ Use this example if you need to...
54
+
55
+ - Implement commit message editing (like lazygit, tig, or git rebase -i).
56
+ - Spawn an external config editor from a TUI settings menu.
57
+ - Build a workflow that alternates between TUI and external tools.
58
+ - Create a split-pane layout with focus-aware scrolling.
59
+ - Calculate wrapped line counts for proper scroll clamping.
60
+ - Implement mouse hit testing for pane-specific interactions.
61
+
62
+ [Read the source code →](app.rb)
@@ -0,0 +1,344 @@
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
@@ -215,8 +215,7 @@ class WidgetList
215
215
  end
216
216
  end
217
217
 
218
- # :nodoc:
219
- private def render
218
+ private def render # :nodoc:
220
219
  items = @item_sets[@item_set_index][:items]
221
220
  direction_config = @direction_configs[@direction_index]
222
221
  spacing_config = @highlight_spacing_configs[@highlight_spacing_index]
@@ -342,8 +341,7 @@ class WidgetList
342
341
  end
343
342
  end
344
343
 
345
- # :nodoc:
346
- private def handle_input
344
+ private def handle_input # :nodoc:
347
345
  case @tui.poll_event
348
346
  in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
349
347
  :quit
@@ -53,6 +53,7 @@ class WidgetTable
53
53
  @highlight_spacing_index = 0
54
54
  @show_column_highlight = true
55
55
  @show_cell_highlight = true
56
+ @show_header = true
56
57
  @offset_mode_index = 0
57
58
  @flex_mode_index = 0
58
59
  @strikethrough_pids = Set.new # Track which rows have strikethrough
@@ -135,8 +136,9 @@ class WidgetTable
135
136
  offset_label = effective_offset.nil? ? "auto" : effective_offset.to_s
136
137
 
137
138
  # Main table
139
+ header_label = @show_header ? "On" : "Off"
138
140
  table = @tui.table(
139
- header: ["PID", "Name", "CPU"],
141
+ header: @show_header ? ["PID", "Name", "CPU"] : nil,
140
142
  rows:,
141
143
  widths:,
142
144
  selected_row: effective_selection,
@@ -198,7 +200,9 @@ class WidgetTable
198
200
  @tui.text_span(content: "z", style: @hotkey_style),
199
201
  @tui.text_span(content: ": Cell Highlight (#{@show_cell_highlight ? 'On' : 'Off'}) "),
200
202
  @tui.text_span(content: "o", style: @hotkey_style),
201
- @tui.text_span(content: ": Offset Mode (#{offset_mode_entry[:name]})"),
203
+ @tui.text_span(content: ": Offset Mode (#{offset_mode_entry[:name]}) "),
204
+ @tui.text_span(content: "d", style: @hotkey_style),
205
+ @tui.text_span(content: ": Header (#{header_label})"),
202
206
  ]),
203
207
  ]
204
208
  ),
@@ -268,6 +272,8 @@ class WidgetTable
268
272
  @offset_mode_index = (@offset_mode_index + 1) % OFFSET_MODES.length
269
273
  in type: :key, code: "f"
270
274
  @flex_mode_index = (@flex_mode_index + 1) % FLEX_MODES.length
275
+ in type: :key, code: "d"
276
+ @show_header = !@show_header
271
277
  else
272
278
  nil
273
279
  end
@@ -1059,7 +1059,7 @@ dependencies = [
1059
1059
 
1060
1060
  [[package]]
1061
1061
  name = "ratatui_ruby"
1062
- version = "1.0.0"
1062
+ version = "1.1.0"
1063
1063
  dependencies = [
1064
1064
  "bumpalo",
1065
1065
  "lazy_static",
@@ -3,7 +3,7 @@
3
3
 
4
4
  [package]
5
5
  name = "ratatui_ruby"
6
- version = "1.0.0"
6
+ version = "1.1.0"
7
7
  edition = "2021"
8
8
 
9
9
  [lib]