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.
- 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 +3 -2
- data/CHANGELOG.md +33 -7
- data/Steepfile +1 -0
- data/doc/concepts/application_testing.md +5 -5
- data/doc/concepts/event_handling.md +1 -1
- data/doc/contributors/design/ruby_frontend.md +40 -12
- data/doc/contributors/design/rust_backend.md +13 -1
- data/doc/contributors/releasing.md +215 -0
- data/doc/contributors/todo/align/api_completeness_audit-finished.md +6 -0
- data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +1 -7
- data/doc/contributors/todo/align/term.md +351 -0
- data/doc/contributors/upstream_requests/paragraph_span_rects.md +259 -0
- data/doc/getting_started/quickstart.md +1 -1
- data/doc/getting_started/why.md +3 -3
- data/doc/images/app_external_editor.gif +0 -0
- data/doc/index.md +1 -6
- data/examples/app_external_editor/README.md +62 -0
- data/examples/app_external_editor/app.rb +344 -0
- data/examples/widget_list/app.rb +2 -4
- data/examples/widget_table/app.rb +8 -2
- data/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/ext/ratatui_ruby/src/events.rs +171 -203
- data/ext/ratatui_ruby/src/lib.rs +36 -0
- data/ext/ratatui_ruby/src/lib_header.rs +11 -0
- data/ext/ratatui_ruby/src/terminal/capabilities.rs +46 -0
- data/ext/ratatui_ruby/src/terminal/init.rs +92 -0
- data/ext/ratatui_ruby/src/terminal/mod.rs +12 -3
- data/ext/ratatui_ruby/src/terminal/queries.rs +15 -0
- data/ext/ratatui_ruby/src/terminal/query.rs +64 -2
- data/lib/ratatui_ruby/backend/window_size.rb +50 -0
- data/lib/ratatui_ruby/backend.rb +59 -0
- data/lib/ratatui_ruby/event/key/navigation.rb +10 -1
- data/lib/ratatui_ruby/event/key.rb +84 -0
- data/lib/ratatui_ruby/event/mouse.rb +95 -3
- data/lib/ratatui_ruby/event/resize.rb +45 -3
- data/lib/ratatui_ruby/layout/alignment.rb +91 -0
- data/lib/ratatui_ruby/layout/layout.rb +1 -2
- data/lib/ratatui_ruby/layout/size.rb +10 -3
- data/lib/ratatui_ruby/layout.rb +4 -0
- data/lib/ratatui_ruby/terminal/capabilities.rb +316 -0
- data/lib/ratatui_ruby/terminal/viewport.rb +1 -1
- data/lib/ratatui_ruby/terminal.rb +66 -0
- data/lib/ratatui_ruby/test_helper/global_state.rb +111 -0
- data/lib/ratatui_ruby/test_helper.rb +3 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby/widgets/table.rb +2 -2
- data/lib/ratatui_ruby.rb +25 -4
- data/sig/examples/app_external_editor/app.rbs +12 -0
- data/sig/generated/event_key_predicates.rbs +1348 -0
- data/sig/ratatui_ruby/backend/window_size.rbs +17 -0
- data/sig/ratatui_ruby/backend.rbs +12 -0
- data/sig/ratatui_ruby/event.rbs +7 -0
- data/sig/ratatui_ruby/layout/alignment.rbs +26 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -0
- data/sig/ratatui_ruby/terminal/capabilities.rbs +38 -0
- data/sig/ratatui_ruby/terminal/viewport.rbs +15 -1
- data/tasks/bump/bump_workflow.rb +49 -0
- data/tasks/bump/changelog.rb +57 -0
- data/tasks/bump/patch_release.rb +19 -0
- data/tasks/bump/release_branch.rb +17 -0
- data/tasks/bump/release_from_trunk.rb +49 -0
- data/tasks/bump/repository.rb +54 -0
- data/tasks/bump/ruby_gem.rb +6 -26
- data/tasks/bump/sem_ver.rb +4 -0
- data/tasks/bump/unreleased_section.rb +17 -0
- data/tasks/bump.rake +21 -11
- data/tasks/doc/documentation.rb +59 -0
- data/tasks/doc/link/file_url.rb +30 -0
- data/tasks/doc/link/relative_path.rb +61 -0
- data/tasks/doc/link/web_url.rb +55 -0
- data/tasks/doc/link.rb +52 -0
- data/tasks/doc/link_audit.rb +116 -0
- data/tasks/doc/problem.rb +40 -0
- data/tasks/doc/source_file.rb +93 -0
- data/tasks/doc.rake +18 -0
- data/tasks/rbs_predicates/predicate_catalog.rb +52 -0
- data/tasks/rbs_predicates/predicate_tests.rb +124 -0
- data/tasks/rbs_predicates/rbs_signature.rb +63 -0
- data/tasks/rbs_predicates.rake +31 -0
- data/tasks/test.rake +3 -0
- data/tasks/website/version.rb +23 -28
- 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
|
+
[](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
|
data/examples/widget_list/app.rb
CHANGED
|
@@ -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
|
data/ext/ratatui_ruby/Cargo.lock
CHANGED