ratatui_ruby 1.2.2 → 1.3.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 +54 -0
- data/.builds/ruby-3.3.yml +54 -0
- data/.builds/ruby-3.4.yml +54 -0
- data/.builds/ruby-4.0.0.yml +54 -0
- data/.pre-commit-config.yaml +16 -0
- data/.rubocop.yml +10 -0
- data/AGENTS.md +147 -0
- data/CHANGELOG.md +771 -0
- data/README.md +187 -0
- data/README.rdoc +302 -0
- data/Rakefile +11 -0
- data/Steepfile +50 -0
- data/doc/concepts/application_architecture.md +321 -0
- data/doc/concepts/application_testing.md +193 -0
- data/doc/concepts/async.md +190 -0
- data/doc/concepts/custom_widgets.md +247 -0
- data/doc/concepts/debugging.md +401 -0
- data/doc/concepts/event_handling.md +162 -0
- data/doc/concepts/interactive_design.md +146 -0
- data/doc/contributors/auditing/parity.md +239 -0
- data/doc/contributors/design/ruby_frontend.md +448 -0
- data/doc/contributors/design/rust_backend.md +434 -0
- data/doc/contributors/design.md +11 -0
- data/doc/contributors/developing_examples.md +400 -0
- data/doc/contributors/documentation_style.md +121 -0
- data/doc/contributors/index.md +21 -0
- data/doc/contributors/releasing.md +215 -0
- data/doc/contributors/todo/align/api_completeness_audit-finished.md +381 -0
- data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +200 -0
- data/doc/contributors/todo/align/term.md +351 -0
- data/doc/contributors/todo/align/terminal.md +647 -0
- data/doc/contributors/todo/future_work.md +169 -0
- data/doc/contributors/upstream_requests/paragraph_span_rects.md +259 -0
- data/doc/contributors/upstream_requests/tab_rects.md +173 -0
- data/doc/contributors/upstream_requests/title_rects.md +132 -0
- data/doc/custom.css +22 -0
- data/doc/getting_started/quickstart.md +291 -0
- data/doc/getting_started/why.md +93 -0
- 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 +34 -0
- data/doc/troubleshooting/async.md +4 -0
- data/doc/troubleshooting/terminal_limitations.md +131 -0
- data/doc/troubleshooting/tui_output.md +197 -0
- data/examples/app_all_events/README.md +114 -0
- data/examples/app_all_events/app.rb +98 -0
- data/examples/app_all_events/model/app_model.rb +159 -0
- data/examples/app_all_events/model/event_color_cycle.rb +43 -0
- data/examples/app_all_events/model/event_entry.rb +94 -0
- data/examples/app_all_events/model/msg.rb +39 -0
- data/examples/app_all_events/model/timestamp.rb +56 -0
- data/examples/app_all_events/update.rb +75 -0
- data/examples/app_all_events/view/app_view.rb +80 -0
- data/examples/app_all_events/view/controls_view.rb +54 -0
- data/examples/app_all_events/view/counts_view.rb +61 -0
- data/examples/app_all_events/view/live_view.rb +72 -0
- data/examples/app_all_events/view/log_view.rb +57 -0
- data/examples/app_all_events/view.rb +9 -0
- data/examples/app_cli_rich_moments/README.md +81 -0
- data/examples/app_cli_rich_moments/app.rb +189 -0
- data/examples/app_color_picker/README.md +156 -0
- data/examples/app_color_picker/app.rb +76 -0
- data/examples/app_color_picker/clipboard.rb +86 -0
- data/examples/app_color_picker/color.rb +193 -0
- data/examples/app_color_picker/controls.rb +92 -0
- data/examples/app_color_picker/copy_dialog.rb +168 -0
- data/examples/app_color_picker/export_pane.rb +128 -0
- data/examples/app_color_picker/harmony.rb +58 -0
- data/examples/app_color_picker/input.rb +176 -0
- data/examples/app_color_picker/main_container.rb +180 -0
- data/examples/app_color_picker/palette.rb +111 -0
- data/examples/app_debugging_showcase/README.md +119 -0
- data/examples/app_debugging_showcase/app.rb +318 -0
- data/examples/app_external_editor/README.md +62 -0
- data/examples/app_external_editor/app.rb +344 -0
- data/examples/app_login_form/README.md +58 -0
- data/examples/app_login_form/app.rb +109 -0
- data/examples/app_stateful_interaction/README.md +35 -0
- data/examples/app_stateful_interaction/app.rb +328 -0
- data/examples/timeout_demo.rb +45 -0
- data/examples/verify_quickstart_dsl/README.md +55 -0
- data/examples/verify_quickstart_dsl/app.rb +49 -0
- data/examples/verify_quickstart_layout/README.md +77 -0
- data/examples/verify_quickstart_layout/app.rb +73 -0
- data/examples/verify_quickstart_lifecycle/README.md +68 -0
- data/examples/verify_quickstart_lifecycle/app.rb +62 -0
- data/examples/verify_readme_usage/README.md +49 -0
- data/examples/verify_readme_usage/app.rb +42 -0
- data/examples/verify_website_managed/README.md +48 -0
- data/examples/verify_website_managed/app.rb +36 -0
- data/examples/verify_website_menu/README.md +60 -0
- data/examples/verify_website_menu/app.rb +84 -0
- data/examples/verify_website_spinner/README.md +44 -0
- data/examples/verify_website_spinner/app.rb +34 -0
- data/examples/widget_barchart/README.md +58 -0
- data/examples/widget_barchart/app.rb +240 -0
- data/examples/widget_block/README.md +44 -0
- data/examples/widget_block/app.rb +258 -0
- data/examples/widget_box/README.md +54 -0
- data/examples/widget_box/app.rb +255 -0
- data/examples/widget_calendar/README.md +48 -0
- data/examples/widget_calendar/app.rb +115 -0
- data/examples/widget_canvas/README.md +31 -0
- data/examples/widget_canvas/app.rb +130 -0
- data/examples/widget_cell/README.md +45 -0
- data/examples/widget_cell/app.rb +112 -0
- data/examples/widget_center/README.md +33 -0
- data/examples/widget_center/app.rb +118 -0
- data/examples/widget_chart/README.md +50 -0
- data/examples/widget_chart/app.rb +220 -0
- data/examples/widget_gauge/README.md +50 -0
- data/examples/widget_gauge/app.rb +229 -0
- data/examples/widget_layout_split/README.md +53 -0
- data/examples/widget_layout_split/app.rb +260 -0
- data/examples/widget_line_gauge/README.md +50 -0
- data/examples/widget_line_gauge/app.rb +219 -0
- data/examples/widget_list/README.md +58 -0
- data/examples/widget_list/app.rb +382 -0
- data/examples/widget_map/README.md +48 -0
- data/examples/widget_map/app.rb +95 -0
- data/examples/widget_overlay/README.md +45 -0
- data/examples/widget_overlay/app.rb +250 -0
- data/examples/widget_popup/README.md +45 -0
- data/examples/widget_popup/app.rb +106 -0
- data/examples/widget_ratatui_logo/README.md +43 -0
- data/examples/widget_ratatui_logo/app.rb +104 -0
- data/examples/widget_ratatui_mascot/README.md +43 -0
- data/examples/widget_ratatui_mascot/app.rb +95 -0
- data/examples/widget_rect/README.md +53 -0
- data/examples/widget_rect/app.rb +222 -0
- data/examples/widget_render/README.md +46 -0
- data/examples/widget_render/app.rb +186 -0
- data/examples/widget_render/app.rbs +41 -0
- data/examples/widget_rich_text/README.md +44 -0
- data/examples/widget_rich_text/app.rb +193 -0
- data/examples/widget_scroll_text/README.md +46 -0
- data/examples/widget_scroll_text/app.rb +109 -0
- data/examples/widget_scrollbar/README.md +46 -0
- data/examples/widget_scrollbar/app.rb +155 -0
- data/examples/widget_sparkline/README.md +51 -0
- data/examples/widget_sparkline/app.rb +277 -0
- data/examples/widget_style_colors/README.md +43 -0
- data/examples/widget_style_colors/app.rb +83 -0
- data/examples/widget_table/README.md +57 -0
- data/examples/widget_table/app.rb +285 -0
- data/examples/widget_tabs/README.md +50 -0
- data/examples/widget_tabs/app.rb +183 -0
- data/examples/widget_text_width/README.md +44 -0
- data/examples/widget_text_width/app.rb +117 -0
- data/ext/ratatui_ruby/Cargo.lock +1 -2
- data/ext/ratatui_ruby/Cargo.toml +1 -2
- data/ext/ratatui_ruby/src/events.rs +18 -157
- data/lib/ratatui_ruby/event/focus_gained.rb +50 -0
- data/lib/ratatui_ruby/event/focus_lost.rb +51 -0
- data/lib/ratatui_ruby/event/key/dwim.rb +301 -0
- data/lib/ratatui_ruby/event/key/modifier.rb +2 -0
- data/lib/ratatui_ruby/event/key.rb +9 -0
- data/lib/ratatui_ruby/event/mouse.rb +33 -0
- data/lib/ratatui_ruby/event/paste.rb +25 -0
- data/lib/ratatui_ruby/event/resize.rb +65 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- data/migrate_to_buffer.rb +145 -0
- data/mise.toml +8 -0
- data/sig/ratatui_ruby/event.rbs +97 -0
- data/tasks/autodoc/examples.rb +87 -0
- data/tasks/autodoc/member.rb +58 -0
- data/tasks/autodoc/name.rb +21 -0
- data/tasks/autodoc.rake +21 -0
- data/tasks/bump/bump_workflow.rb +49 -0
- data/tasks/bump/cargo_lockfile.rb +21 -0
- data/tasks/bump/changelog.rb +104 -0
- data/tasks/bump/header.rb +32 -0
- data/tasks/bump/history.rb +32 -0
- data/tasks/bump/links.rb +69 -0
- data/tasks/bump/manifest.rb +33 -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 +29 -0
- data/tasks/bump/sem_ver.rb +44 -0
- data/tasks/bump/unreleased_section.rb +73 -0
- data/tasks/bump.rake +61 -0
- 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 +905 -0
- data/tasks/example_viewer.html.erb +172 -0
- data/tasks/extension.rake +14 -0
- data/tasks/license/headers_md.rb +223 -0
- data/tasks/license/headers_rb.rb +210 -0
- data/tasks/license/license_utils.rb +130 -0
- data/tasks/license/snippets_md.rb +315 -0
- data/tasks/license/snippets_rdoc.rb +150 -0
- data/tasks/license.rake +91 -0
- data/tasks/lint.rake +170 -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/rdoc_config.rb +29 -0
- data/tasks/resources/build.yml.erb +60 -0
- data/tasks/resources/index.html.erb +141 -0
- data/tasks/resources/rubies.yml +7 -0
- data/tasks/sourcehut.rake +122 -0
- data/tasks/steep.rake +11 -0
- data/tasks/terminal_preview/app_screenshot.rb +45 -0
- data/tasks/terminal_preview/crash_report.rb +54 -0
- data/tasks/terminal_preview/example_app.rb +27 -0
- data/tasks/terminal_preview/launcher_script.rb +48 -0
- data/tasks/terminal_preview/preview_collection.rb +60 -0
- data/tasks/terminal_preview/preview_timing.rb +24 -0
- data/tasks/terminal_preview/safety_confirmation.rb +58 -0
- data/tasks/terminal_preview/saved_screenshot.rb +56 -0
- data/tasks/terminal_preview/system_appearance.rb +13 -0
- data/tasks/terminal_preview/terminal_window.rb +138 -0
- data/tasks/terminal_preview/window_id.rb +16 -0
- data/tasks/terminal_preview.rake +30 -0
- data/tasks/test.rake +36 -0
- data/tasks/website/index_page.rb +30 -0
- data/tasks/website/version.rb +122 -0
- data/tasks/website/version_menu.rb +68 -0
- data/tasks/website/versioned_documentation.rb +83 -0
- data/tasks/website/website.rb +53 -0
- data/tasks/website.rake +28 -0
- metadata +256 -1
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
# Lockfiles need to be refreshed by a tool after Manifests are changed.
|
|
9
|
+
class CargoLockfile < Data.define(:path, :dir, :name)
|
|
10
|
+
def exists?
|
|
11
|
+
File.exist?(path)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def refresh
|
|
15
|
+
return unless exists?
|
|
16
|
+
|
|
17
|
+
Dir.chdir(dir) do
|
|
18
|
+
system("cargo update -p #{name} --offline")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
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 "links"
|
|
9
|
+
require_relative "unreleased_section"
|
|
10
|
+
require_relative "history"
|
|
11
|
+
require_relative "header"
|
|
12
|
+
|
|
13
|
+
# Changelog manages the project's CHANGELOG.md file.
|
|
14
|
+
class Changelog
|
|
15
|
+
# Creates a new Changelog for the file at the given path.
|
|
16
|
+
def initialize(path: "CHANGELOG.md")
|
|
17
|
+
@path = path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Releases a new version in the changelog.
|
|
21
|
+
# This moves the unreleased changes to a new version heading and resets the unreleased section.
|
|
22
|
+
def release(new_version)
|
|
23
|
+
content = File.read(@path)
|
|
24
|
+
|
|
25
|
+
header = Header.parse(content)
|
|
26
|
+
unreleased = UnreleasedSection.parse(content)
|
|
27
|
+
links = Links.from_markdown(content)
|
|
28
|
+
|
|
29
|
+
raise "Could not parse CHANGELOG.md" unless header && unreleased && links
|
|
30
|
+
|
|
31
|
+
history = History.parse(content, header.length, unreleased.to_s.length, links.to_s)
|
|
32
|
+
|
|
33
|
+
links.release(new_version)
|
|
34
|
+
history.add(unreleased.as_version(new_version))
|
|
35
|
+
|
|
36
|
+
File.write(@path, "#{header}#{UnreleasedSection.fresh}\n\n#{history}\n#{links}")
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Removes entries from [Unreleased] that were released in the given version.
|
|
41
|
+
# Used when creating a release branch from trunk.
|
|
42
|
+
def prune_released_entries(released_entries)
|
|
43
|
+
content = File.read(@path)
|
|
44
|
+
|
|
45
|
+
header = Header.parse(content)
|
|
46
|
+
unreleased = UnreleasedSection.parse(content)
|
|
47
|
+
links = Links.from_markdown(content)
|
|
48
|
+
|
|
49
|
+
raise "Could not parse CHANGELOG.md" unless header && unreleased && links
|
|
50
|
+
|
|
51
|
+
history = History.parse(content, header.length, unreleased.to_s.length, links.to_s)
|
|
52
|
+
|
|
53
|
+
pruned = unreleased.without_entries(released_entries)
|
|
54
|
+
|
|
55
|
+
File.write(@path, "#{header}#{pruned}\n\n#{history}\n#{links}")
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Imports a release section from another branch's changelog.
|
|
60
|
+
# Adds the version section to history and dedupes from [Unreleased].
|
|
61
|
+
# Uses "first wins" — if entry already deduped, doesn't re-add it.
|
|
62
|
+
def import_release(version, release_changelog_content)
|
|
63
|
+
release_section = extract_version_section(release_changelog_content, version)
|
|
64
|
+
return unless release_section
|
|
65
|
+
|
|
66
|
+
content = File.read(@path)
|
|
67
|
+
|
|
68
|
+
header = Header.parse(content)
|
|
69
|
+
unreleased = UnreleasedSection.parse(content)
|
|
70
|
+
links = Links.from_markdown(content)
|
|
71
|
+
|
|
72
|
+
raise "Could not parse CHANGELOG.md" unless header && unreleased && links
|
|
73
|
+
|
|
74
|
+
history = History.parse(content, header.length, unreleased.to_s.length, links.to_s)
|
|
75
|
+
|
|
76
|
+
# Add the release section to history (inserted in version order)
|
|
77
|
+
history.add(release_section)
|
|
78
|
+
links.release(version)
|
|
79
|
+
|
|
80
|
+
# Dedupe from [Unreleased] (first-wins: if already gone, no-op)
|
|
81
|
+
release_entries = release_section.lines.select { |l| l.strip.start_with?("- ") }.map(&:strip)
|
|
82
|
+
pruned = unreleased.without_entries(release_entries)
|
|
83
|
+
|
|
84
|
+
File.write(@path, "#{header}#{pruned}\n\n#{history}\n#{links}")
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def commit_message(version)
|
|
89
|
+
content = File.read(@path)
|
|
90
|
+
unreleased = UnreleasedSection.parse(content)
|
|
91
|
+
return nil unless unreleased
|
|
92
|
+
|
|
93
|
+
"chore: release v#{version}\n\n#{unreleased.commit_body}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private def extract_version_section(changelog_content, version)
|
|
97
|
+
# Match the version heading and capture until the next version heading or links section
|
|
98
|
+
pattern = /^## \[#{Regexp.escape(version.to_s)}\][^\n]*\n(.*?)(?=^## \[|\n\[Unreleased\]:)/m
|
|
99
|
+
match = changelog_content.match(pattern)
|
|
100
|
+
return nil unless match
|
|
101
|
+
|
|
102
|
+
"## [#{version}]#{match[0].split("\n", 2).first.split(']', 2).last}\n#{match[1]}"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
# Header manages the header section of the changelog.
|
|
9
|
+
class Header
|
|
10
|
+
PATTERN = /^(.*?)(?=## \[Unreleased\])/m
|
|
11
|
+
|
|
12
|
+
# Extracts the header section from the given content.
|
|
13
|
+
def self.parse(content)
|
|
14
|
+
match = content.match(PATTERN)
|
|
15
|
+
new(match[1]) if match
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Creates a new Header from the given content.
|
|
19
|
+
def initialize(content)
|
|
20
|
+
@content = content.dup
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns the length of the header content.
|
|
24
|
+
def length
|
|
25
|
+
@content.length
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the current state of the header as a string.
|
|
29
|
+
def to_s
|
|
30
|
+
@content
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
# History manages the versioned history of the changelog.
|
|
9
|
+
class History
|
|
10
|
+
# Extracts the history section from the given content, between unreleased and links.
|
|
11
|
+
def self.parse(content, header_length, unreleased_length, links_text)
|
|
12
|
+
start = header_length + unreleased_length
|
|
13
|
+
text = "#{content[start...(content.index(links_text))].strip}\n"
|
|
14
|
+
new(text)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Creates a new History from the given content.
|
|
18
|
+
def initialize(content)
|
|
19
|
+
@content = content.dup
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Adds a new versioned section to the history.
|
|
23
|
+
def add(section)
|
|
24
|
+
@content = "#{"#{section}\n\n#{@content}".strip}\n"
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the current state of the history as a string.
|
|
29
|
+
def to_s
|
|
30
|
+
@content
|
|
31
|
+
end
|
|
32
|
+
end
|
data/tasks/bump/links.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
# Manages the version comparison links at the botton of the changelog.
|
|
9
|
+
#
|
|
10
|
+
# Release automation needs to update links. Manually calculating git diff URLs
|
|
11
|
+
# for every release is tedious and error-prone. SourceHut does not have
|
|
12
|
+
# standard comparison views, complicating matters further.
|
|
13
|
+
#
|
|
14
|
+
# This class manages the collection of links. It parses them from the markdown.
|
|
15
|
+
# It generates the correct tree links for SourceHut. It properly shifts the
|
|
16
|
+
# "Unreleased" pointer.
|
|
17
|
+
#
|
|
18
|
+
# Use it to update the changelog during a release.
|
|
19
|
+
class Links
|
|
20
|
+
PATTERN = /^(\[Unreleased\]: .*)$/m
|
|
21
|
+
UNRELEASED_PATTERN = %r{^\[Unreleased\]: (.*?/refs/)HEAD$}
|
|
22
|
+
|
|
23
|
+
# Creates a Links object from the full markdown content.
|
|
24
|
+
#
|
|
25
|
+
# [content] String. The full text of the changelog.
|
|
26
|
+
def self.from_markdown(content)
|
|
27
|
+
match = content.match(PATTERN)
|
|
28
|
+
return unless match
|
|
29
|
+
|
|
30
|
+
new(match[1].strip)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns the raw text of the links.
|
|
34
|
+
attr_reader :text
|
|
35
|
+
|
|
36
|
+
# Creates a new Links object.
|
|
37
|
+
#
|
|
38
|
+
# [text] String. The raw text of the links section.
|
|
39
|
+
def initialize(text)
|
|
40
|
+
@text = text.dup
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Releases a new version.
|
|
44
|
+
#
|
|
45
|
+
# Updates the "Unreleased" link to point to the new head. Adds a new link for
|
|
46
|
+
# the just-released version pointing to its specific tag.
|
|
47
|
+
#
|
|
48
|
+
# [version] String. The new version number (e.g., <tt>"0.5.0"</tt>).
|
|
49
|
+
def release(version)
|
|
50
|
+
return unless base_url
|
|
51
|
+
|
|
52
|
+
new_unreleased = "[Unreleased]: #{base_url}HEAD" # .../HEAD
|
|
53
|
+
new_version_link = "[#{version}]: #{base_url}v#{version}" # .../v1.0.0
|
|
54
|
+
|
|
55
|
+
@text.sub!(UNRELEASED_PATTERN, "#{new_unreleased}\n#{new_version_link}")
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns the string representation of the links.
|
|
60
|
+
def to_s
|
|
61
|
+
@text
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The base URL for the repository's references.
|
|
65
|
+
private def base_url
|
|
66
|
+
match = @text.match(UNRELEASED_PATTERN)
|
|
67
|
+
match[1] if match
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
# Manifests hold a copy of the version number and should be changed manually.
|
|
9
|
+
# Use Regexp lookarounds in `pattern` to match the version number.
|
|
10
|
+
class Manifest < Data.define(:path, :pattern, :primary)
|
|
11
|
+
def read
|
|
12
|
+
File.read(path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(path:, pattern:, primary: false)
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def version
|
|
20
|
+
content = read
|
|
21
|
+
match = content.match(pattern)
|
|
22
|
+
raise "Version missing in manifest #{path}" unless match
|
|
23
|
+
|
|
24
|
+
SemVer.parse(match[0])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def write(version)
|
|
28
|
+
return unless File.exist?(path)
|
|
29
|
+
|
|
30
|
+
new_content = read.gsub(pattern, version.to_s)
|
|
31
|
+
File.write(path, new_content)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
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 "bump_workflow"
|
|
9
|
+
|
|
10
|
+
# PatchRelease performs a patch release on the current release branch.
|
|
11
|
+
class PatchRelease < BumpWorkflow
|
|
12
|
+
private def prepare(segment)
|
|
13
|
+
puts "Bumping #{segment}: #{@gem.version} -> #{target}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private def finalize
|
|
17
|
+
puts "\nCommitted. Push and run: bundle exec rake release"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
# ReleaseBranch represents a release/X.Y branch for a version series.
|
|
9
|
+
class ReleaseBranch < Data.define(:major, :minor)
|
|
10
|
+
def self.for_version(semver)
|
|
11
|
+
new(semver.major, semver.minor)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def name
|
|
15
|
+
"release/#{major}.#{minor}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
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 "bump_workflow"
|
|
9
|
+
require_relative "release_branch"
|
|
10
|
+
|
|
11
|
+
# ReleaseFromTrunk creates a release branch, releases there, syncs trunk, returns to release branch.
|
|
12
|
+
class ReleaseFromTrunk < BumpWorkflow
|
|
13
|
+
private def prepare(segment)
|
|
14
|
+
@branch = ReleaseBranch.for_version(target)
|
|
15
|
+
|
|
16
|
+
puts "Creating release branch: #{@branch.name}"
|
|
17
|
+
puts "Bumping #{segment}: #{@gem.version} -> #{target}"
|
|
18
|
+
|
|
19
|
+
@repository.create_branch(@branch.name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private def release_on_branch
|
|
23
|
+
super
|
|
24
|
+
puts "\nCommitted on #{@branch.name}."
|
|
25
|
+
|
|
26
|
+
# Capture for trunk import
|
|
27
|
+
@released_changelog_content = File.read("CHANGELOG.md")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private def finalize
|
|
31
|
+
sync_trunk
|
|
32
|
+
return_to_release_branch
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private def sync_trunk
|
|
36
|
+
@repository.checkout("trunk")
|
|
37
|
+
trunk_changelog = Changelog.new
|
|
38
|
+
trunk_changelog.import_release(target, @released_changelog_content)
|
|
39
|
+
@gem.update_version(target)
|
|
40
|
+
generate_ci_manifests
|
|
41
|
+
@repository.commit_all("chore: import v#{target} to trunk")
|
|
42
|
+
puts "Committed on trunk."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private def return_to_release_branch
|
|
46
|
+
@repository.checkout(@branch.name)
|
|
47
|
+
puts "\nBack on #{@branch.name}. Review, push both branches, then: bundle exec rake release"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
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 "shellwords"
|
|
9
|
+
require "tmpdir"
|
|
10
|
+
|
|
11
|
+
class Repository
|
|
12
|
+
TRUNK = "trunk"
|
|
13
|
+
|
|
14
|
+
def on_trunk? = current_branch == TRUNK
|
|
15
|
+
|
|
16
|
+
def current_branch
|
|
17
|
+
`git branch --show-current`.strip
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create_branch(name)
|
|
21
|
+
system("git checkout -b #{name}", exception: true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def checkout(name)
|
|
25
|
+
system("git checkout #{name}", exception: true)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def assert_can_bump!(segment)
|
|
29
|
+
assert_pristine!
|
|
30
|
+
|
|
31
|
+
if on_trunk? && segment == :patch
|
|
32
|
+
raise ArgumentError, "Cannot bump:patch from trunk. Use a release branch."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if !on_trunk? && [:minor, :major].include?(segment)
|
|
36
|
+
raise ArgumentError, "Cannot bump:#{segment} from #{current_branch}. Switch to trunk first."
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def assert_pristine!
|
|
41
|
+
return if `git status --porcelain`.strip.empty?
|
|
42
|
+
|
|
43
|
+
raise ArgumentError, "Working tree is not clean. Commit or stash changes first."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def commit_all(message)
|
|
47
|
+
msg_file = File.join(Dir.tmpdir, "ratatui_ruby_commit_msg_#{$$}.txt")
|
|
48
|
+
system("git add -A", exception: true)
|
|
49
|
+
File.write(msg_file, message)
|
|
50
|
+
system("git commit -F #{msg_file.shellescape}", exception: true)
|
|
51
|
+
ensure
|
|
52
|
+
File.delete(msg_file) if msg_file && File.exist?(msg_file)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
# RubyGem knows how to update its version: manifests, lockfiles.
|
|
9
|
+
class RubyGem
|
|
10
|
+
def initialize(manifests:, lockfile:)
|
|
11
|
+
raise ArgumentError, "Must have exactly one primary manifest" unless manifests.count(&:primary) == 1
|
|
12
|
+
@manifests = manifests
|
|
13
|
+
@lockfile = lockfile
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def version
|
|
17
|
+
@manifests.find(&:primary).version
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def update_version(target)
|
|
21
|
+
@manifests.each { |manifest| manifest.write(target) }
|
|
22
|
+
@lockfile.refresh
|
|
23
|
+
refresh_bundler_lockfile
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private def refresh_bundler_lockfile
|
|
27
|
+
system("bundle install", exception: true)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
# See https://semver.org/spec/v2.0.0.html
|
|
9
|
+
class SemVer
|
|
10
|
+
SEGMENTS = [:major, :minor, :patch].freeze
|
|
11
|
+
|
|
12
|
+
def self.parse(string)
|
|
13
|
+
require "rubygems"
|
|
14
|
+
# Extract prerelease suffix (e.g., "-beta.1", "-alpha.2", "-rc.1")
|
|
15
|
+
base, prerelease = string.split("-", 2)
|
|
16
|
+
segments = Gem::Version.new(base).segments.fill(0, 3).first(3)
|
|
17
|
+
new(segments, prerelease:)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(segments, prerelease: nil)
|
|
21
|
+
@segments = segments
|
|
22
|
+
@prerelease = prerelease
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def major = @segments[0]
|
|
26
|
+
def minor = @segments[1]
|
|
27
|
+
def patch = @segments[2]
|
|
28
|
+
|
|
29
|
+
def next(segment)
|
|
30
|
+
index = SEGMENTS.index(segment)
|
|
31
|
+
raise ArgumentError, "Invalid segment: #{segment}" unless index
|
|
32
|
+
|
|
33
|
+
new_segments = @segments.dup
|
|
34
|
+
new_segments[index] += 1
|
|
35
|
+
new_segments.fill(0, (index + 1)..2)
|
|
36
|
+
|
|
37
|
+
SemVer.new(new_segments)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_s
|
|
41
|
+
base = @segments.join(".")
|
|
42
|
+
@prerelease ? "#{base}-#{@prerelease}" : base
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
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 "date"
|
|
9
|
+
require "rdoc"
|
|
10
|
+
|
|
11
|
+
# UnreleasedSection manages the [Unreleased] section of the changelog.
|
|
12
|
+
class UnreleasedSection
|
|
13
|
+
PATTERN = /^(## \[Unreleased\].*?)(?=## \[\d)/m
|
|
14
|
+
|
|
15
|
+
# Extracts the unreleased section from the given content.
|
|
16
|
+
def self.parse(content)
|
|
17
|
+
match = content.match(PATTERN)
|
|
18
|
+
new(match[1].strip) if match
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Creates a new UnreleasedSection from the given unreleased content.
|
|
22
|
+
def initialize(content)
|
|
23
|
+
@content = content.dup
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the unreleased content as a versioned section.
|
|
27
|
+
def as_version(new_version)
|
|
28
|
+
date = Date.today.iso8601
|
|
29
|
+
@content.sub(/^## \[Unreleased\]/, "## [#{new_version}] - #{date}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns a fresh unreleased section.
|
|
33
|
+
def self.fresh
|
|
34
|
+
new("## [Unreleased]\n\n### Added\n\n### Changed\n\n### Fixed\n\n### Removed")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns the current state of the section as a string.
|
|
38
|
+
def to_s
|
|
39
|
+
@content
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def commit_body
|
|
43
|
+
formatter = Class.new { include RDoc::Text }.new
|
|
44
|
+
@content
|
|
45
|
+
.sub(/^## \[Unreleased\].*$/, "")
|
|
46
|
+
.gsub(/^### (Added|Changed|Fixed|Removed)\n*$/, "")
|
|
47
|
+
.gsub(/^- \*\*([^*]+)\*\*:/, '\1:')
|
|
48
|
+
.gsub(/`([^`]+)`/, '\1')
|
|
49
|
+
.strip
|
|
50
|
+
.lines
|
|
51
|
+
.map { |line| line.gsub(/^- /, "").strip }
|
|
52
|
+
.reject(&:empty?)
|
|
53
|
+
.map { |line| formatter.wrap(line, 72) }
|
|
54
|
+
.join("\n\n")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns all changelog entry lines (lines starting with "- ")
|
|
58
|
+
def entries
|
|
59
|
+
@content.lines.select { |line| line.strip.start_with?("- ") }.map(&:strip)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns a new UnreleasedSection with entries removed that appear in the given list.
|
|
63
|
+
def without_entries(entries_to_remove)
|
|
64
|
+
return self if entries_to_remove.empty?
|
|
65
|
+
|
|
66
|
+
new_lines = @content.lines.reject do |line|
|
|
67
|
+
stripped = line.strip
|
|
68
|
+
entries_to_remove.include?(stripped)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
self.class.new(new_lines.join)
|
|
72
|
+
end
|
|
73
|
+
end
|
data/tasks/bump.rake
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "rubygems"
|
|
9
|
+
|
|
10
|
+
require_relative "bump/sem_ver"
|
|
11
|
+
require_relative "bump/manifest"
|
|
12
|
+
require_relative "bump/cargo_lockfile"
|
|
13
|
+
require_relative "bump/ruby_gem"
|
|
14
|
+
require_relative "bump/release_from_trunk"
|
|
15
|
+
require_relative "bump/patch_release"
|
|
16
|
+
|
|
17
|
+
namespace :bump do
|
|
18
|
+
gem = RubyGem.new(
|
|
19
|
+
manifests: [
|
|
20
|
+
Manifest.new(
|
|
21
|
+
path: "lib/ratatui_ruby/version.rb",
|
|
22
|
+
pattern: /(?<=VERSION = ")[^"]+(?=")/,
|
|
23
|
+
primary: true
|
|
24
|
+
),
|
|
25
|
+
Manifest.new(
|
|
26
|
+
path: "ext/ratatui_ruby/Cargo.toml",
|
|
27
|
+
pattern: /(?<=^version = ")[^"]+(?=")/,
|
|
28
|
+
primary: false
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
lockfile: CargoLockfile.new(
|
|
32
|
+
path: "ext/ratatui_ruby/Cargo.lock",
|
|
33
|
+
dir: "ext/ratatui_ruby",
|
|
34
|
+
name: "ratatui_ruby"
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
desc "Bump major version"
|
|
39
|
+
task :major do
|
|
40
|
+
ReleaseFromTrunk.new(gem:).call(:major)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
desc "Bump minor version"
|
|
44
|
+
task :minor do
|
|
45
|
+
ReleaseFromTrunk.new(gem:).call(:minor)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
desc "Bump patch version"
|
|
49
|
+
task :patch do
|
|
50
|
+
PatchRelease.new(gem:).call(:patch)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc "Set exact version (e.g. rake bump:exact[0.1.0])"
|
|
54
|
+
task :exact, [:version] do |_, args|
|
|
55
|
+
target = SemVer.parse(args[:version])
|
|
56
|
+
changelog = Changelog.new
|
|
57
|
+
changelog.release(target)
|
|
58
|
+
gem.update_version(target)
|
|
59
|
+
Rake::Task["sourcehut"].invoke
|
|
60
|
+
end
|
|
61
|
+
end
|