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,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module RatatuiRuby
|
|
9
|
+
module Backend
|
|
10
|
+
class WindowSize < Data
|
|
11
|
+
attr_reader columns_rows: Layout::Size
|
|
12
|
+
attr_reader pixels: Layout::Size
|
|
13
|
+
|
|
14
|
+
def self.new: (columns_rows: Layout::Size, pixels: Layout::Size) -> instance
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/sig/ratatui_ruby/event.rbs
CHANGED
|
@@ -111,6 +111,11 @@ module RatatuiRuby
|
|
|
111
111
|
def drag?: () -> bool
|
|
112
112
|
def scroll_up?: () -> bool
|
|
113
113
|
def scroll_down?: () -> bool
|
|
114
|
+
def left?: () -> bool
|
|
115
|
+
def right?: () -> bool
|
|
116
|
+
def middle?: () -> bool
|
|
117
|
+
def to_sym: () -> Symbol
|
|
118
|
+
def ==: (top other) -> bool
|
|
114
119
|
def deconstruct_keys: (Array[Symbol]?) -> { type: :mouse, kind: String, x: Integer, y: Integer, button: String, modifiers: Array[String] }
|
|
115
120
|
end
|
|
116
121
|
|
|
@@ -119,6 +124,8 @@ module RatatuiRuby
|
|
|
119
124
|
attr_reader height: Integer
|
|
120
125
|
|
|
121
126
|
def initialize: (width: Integer, height: Integer) -> void
|
|
127
|
+
def to_sym: () -> Symbol
|
|
128
|
+
def ==: (top other) -> bool
|
|
122
129
|
def deconstruct_keys: (Array[Symbol]?) -> { type: :resize, width: Integer, height: Integer }
|
|
123
130
|
end
|
|
124
131
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module RatatuiRuby
|
|
9
|
+
module Layout
|
|
10
|
+
module HorizontalAlignment
|
|
11
|
+
LEFT: :left
|
|
12
|
+
CENTER: :center
|
|
13
|
+
RIGHT: :right
|
|
14
|
+
ALL: Array[:left | :center | :right]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module VerticalAlignment
|
|
18
|
+
TOP: :top
|
|
19
|
+
CENTER: :center
|
|
20
|
+
BOTTOM: :bottom
|
|
21
|
+
ALL: Array[:top | :center | :bottom]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Alignment: singleton(HorizontalAlignment)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -102,6 +102,8 @@ module RatatuiRuby
|
|
|
102
102
|
def self._get_terminal_size: () -> Hash[String, Integer]
|
|
103
103
|
def self._get_viewport_type: () -> String
|
|
104
104
|
def self.insert_before: (Integer height, ?widget? widget) ?{ () -> widget } -> void
|
|
105
|
+
def self._frame_count: () -> Integer
|
|
106
|
+
def self.frame_count: () -> Integer
|
|
105
107
|
|
|
106
108
|
# Color conversion (internal, Rust FFI)
|
|
107
109
|
def self._color_from_u32: (Integer) -> String
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module RatatuiRuby
|
|
9
|
+
class Terminal
|
|
10
|
+
# Environment-based terminal capability detection.
|
|
11
|
+
# Provides class methods for detecting terminal capabilities.
|
|
12
|
+
module Capabilities
|
|
13
|
+
def tty?: () -> bool
|
|
14
|
+
def dumb?: () -> bool
|
|
15
|
+
def no_color?: () -> bool
|
|
16
|
+
def force_color?: () -> bool
|
|
17
|
+
def interactive?: () -> bool
|
|
18
|
+
def available_color_count: () -> Integer
|
|
19
|
+
def color_support: () -> (:none | :basic | :ansi256 | :truecolor)
|
|
20
|
+
def supports_keyboard_enhancement?: () -> bool
|
|
21
|
+
def force_color_output: (bool enable) -> void
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# FFI methods (defined in Rust, exposed on Terminal singleton)
|
|
26
|
+
def _available_color_count: () -> Integer
|
|
27
|
+
def _supports_keyboard_enhancement: () -> bool
|
|
28
|
+
def _force_color_output: (bool enable) -> void
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
extend Capabilities
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# FFI singleton method for Backend.window_size
|
|
36
|
+
def self._terminal_window_size: () -> [Integer, Integer, Integer, Integer]?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -2,7 +2,21 @@
|
|
|
2
2
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
module RatatuiRuby
|
|
5
|
-
|
|
5
|
+
class Terminal
|
|
6
|
+
@viewport: Terminal::Viewport
|
|
7
|
+
@terminal_id: Integer
|
|
8
|
+
|
|
9
|
+
attr_reader terminal_id: Integer
|
|
10
|
+
|
|
11
|
+
def initialize: (?viewport: (Terminal::Viewport | :fullscreen | :inline)?, ?height: Integer?) -> void
|
|
12
|
+
def size: () -> Layout::Rect
|
|
13
|
+
|
|
14
|
+
# Private native methods (defined in Rust)
|
|
15
|
+
def self._init_test_terminal_instance: (Integer width, Integer height, String viewport_type, Integer? viewport_height) -> Integer
|
|
16
|
+
def self._get_terminal_size_instance: (Integer terminal_id) -> Layout::Rect
|
|
17
|
+
|
|
18
|
+
private def resolve_viewport: ((Terminal::Viewport | :fullscreen | :inline)? viewport, Integer? height) -> Terminal::Viewport
|
|
19
|
+
|
|
6
20
|
class Viewport < Data
|
|
7
21
|
attr_reader type: Symbol
|
|
8
22
|
attr_reader height: Integer?
|
|
@@ -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 "repository"
|
|
9
|
+
require_relative "changelog"
|
|
10
|
+
|
|
11
|
+
# Base class for version bump workflows.
|
|
12
|
+
# Subclasses implement the template methods: prepare, release_on_branch, finalize.
|
|
13
|
+
class BumpWorkflow
|
|
14
|
+
def initialize(gem:, repository: Repository.new)
|
|
15
|
+
@gem = gem
|
|
16
|
+
@repository = repository
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(segment)
|
|
20
|
+
@repository.assert_can_bump!(segment)
|
|
21
|
+
@target = @gem.version.next(segment)
|
|
22
|
+
|
|
23
|
+
prepare(segment)
|
|
24
|
+
release_on_branch
|
|
25
|
+
finalize
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_reader :target
|
|
29
|
+
|
|
30
|
+
private def release_on_branch
|
|
31
|
+
changelog = Changelog.new
|
|
32
|
+
@commit_message = changelog.commit_message(target)
|
|
33
|
+
changelog.release(target)
|
|
34
|
+
@gem.update_version(target)
|
|
35
|
+
generate_ci_manifests
|
|
36
|
+
@repository.commit_all(@commit_message)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private def generate_ci_manifests
|
|
40
|
+
Rake::Task["sourcehut:build:manifest"].reenable
|
|
41
|
+
Rake::Task["sourcehut:build"].reenable
|
|
42
|
+
Rake::Task["sourcehut"].reenable
|
|
43
|
+
Rake::Task["sourcehut"].invoke
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Template methods for subclasses
|
|
47
|
+
private def prepare(segment) = nil
|
|
48
|
+
private def finalize = nil
|
|
49
|
+
end
|
data/tasks/bump/changelog.rb
CHANGED
|
@@ -37,6 +37,54 @@ class Changelog
|
|
|
37
37
|
nil
|
|
38
38
|
end
|
|
39
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
|
+
|
|
40
88
|
def commit_message(version)
|
|
41
89
|
content = File.read(@path)
|
|
42
90
|
unreleased = UnreleasedSection.parse(content)
|
|
@@ -44,4 +92,13 @@ class Changelog
|
|
|
44
92
|
|
|
45
93
|
"chore: release v#{version}\n\n#{unreleased.commit_body}"
|
|
46
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
|
|
47
104
|
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
|
data/tasks/bump/ruby_gem.rb
CHANGED
|
@@ -5,45 +5,25 @@
|
|
|
5
5
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
6
|
#++
|
|
7
7
|
|
|
8
|
+
# RubyGem knows how to update its version: manifests, lockfiles.
|
|
8
9
|
class RubyGem
|
|
9
|
-
def initialize(manifests:, lockfile
|
|
10
|
+
def initialize(manifests:, lockfile:)
|
|
10
11
|
raise ArgumentError, "Must have exactly one primary manifest" unless manifests.count(&:primary) == 1
|
|
11
12
|
@manifests = manifests
|
|
12
13
|
@lockfile = lockfile
|
|
13
|
-
@changelog = changelog
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def version
|
|
17
17
|
@manifests.find(&:primary).version
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def
|
|
21
|
-
target = version.next(segment)
|
|
22
|
-
commit_message = @changelog.commit_message(target)
|
|
23
|
-
|
|
24
|
-
puts "Bumping #{segment}: #{version} -> #{target}"
|
|
25
|
-
@changelog.release(target)
|
|
26
|
-
@manifests.each { |manifest| manifest.write(target) }
|
|
27
|
-
@lockfile.refresh
|
|
28
|
-
|
|
29
|
-
puts_commit_message(commit_message)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def set(version_string)
|
|
33
|
-
target = SemVer.parse(version_string)
|
|
34
|
-
commit_message = @changelog.commit_message(target)
|
|
35
|
-
|
|
36
|
-
puts "Setting version: #{version} -> #{target}"
|
|
37
|
-
@changelog.release(target)
|
|
20
|
+
def update_version(target)
|
|
38
21
|
@manifests.each { |manifest| manifest.write(target) }
|
|
39
22
|
@lockfile.refresh
|
|
40
|
-
|
|
41
|
-
puts_commit_message(commit_message)
|
|
23
|
+
refresh_bundler_lockfile
|
|
42
24
|
end
|
|
43
25
|
|
|
44
|
-
private def
|
|
45
|
-
|
|
46
|
-
puts message
|
|
47
|
-
puts "=" * 80
|
|
26
|
+
private def refresh_bundler_lockfile
|
|
27
|
+
system("bundle install", exception: true)
|
|
48
28
|
end
|
|
49
29
|
end
|
data/tasks/bump/sem_ver.rb
CHANGED
|
@@ -22,6 +22,10 @@ class SemVer
|
|
|
22
22
|
@prerelease = prerelease
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def major = @segments[0]
|
|
26
|
+
def minor = @segments[1]
|
|
27
|
+
def patch = @segments[2]
|
|
28
|
+
|
|
25
29
|
def next(segment)
|
|
26
30
|
index = SEGMENTS.index(segment)
|
|
27
31
|
raise ArgumentError, "Invalid segment: #{segment}" unless index
|
|
@@ -53,4 +53,21 @@ class UnreleasedSection
|
|
|
53
53
|
.map { |line| formatter.wrap(line, 72) }
|
|
54
54
|
.join("\n\n")
|
|
55
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
|
|
56
73
|
end
|
data/tasks/bump.rake
CHANGED
|
@@ -11,10 +11,11 @@ require_relative "bump/sem_ver"
|
|
|
11
11
|
require_relative "bump/manifest"
|
|
12
12
|
require_relative "bump/cargo_lockfile"
|
|
13
13
|
require_relative "bump/ruby_gem"
|
|
14
|
-
require_relative "bump/
|
|
14
|
+
require_relative "bump/release_from_trunk"
|
|
15
|
+
require_relative "bump/patch_release"
|
|
15
16
|
|
|
16
17
|
namespace :bump do
|
|
17
|
-
|
|
18
|
+
gem = RubyGem.new(
|
|
18
19
|
manifests: [
|
|
19
20
|
Manifest.new(
|
|
20
21
|
path: "lib/ratatui_ruby/version.rb",
|
|
@@ -31,21 +32,30 @@ namespace :bump do
|
|
|
31
32
|
path: "ext/ratatui_ruby/Cargo.lock",
|
|
32
33
|
dir: "ext/ratatui_ruby",
|
|
33
34
|
name: "ratatui_ruby"
|
|
34
|
-
)
|
|
35
|
-
changelog: Changelog.new
|
|
35
|
+
)
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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)
|
|
44
51
|
end
|
|
45
52
|
|
|
46
53
|
desc "Set exact version (e.g. rake bump:exact[0.1.0])"
|
|
47
54
|
task :exact, [:version] do |_, args|
|
|
48
|
-
|
|
55
|
+
target = SemVer.parse(args[:version])
|
|
56
|
+
changelog = Changelog.new
|
|
57
|
+
changelog.release(target)
|
|
58
|
+
gem.update_version(target)
|
|
49
59
|
Rake::Task["sourcehut"].invoke
|
|
50
60
|
end
|
|
51
61
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
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 "pathname"
|
|
9
|
+
require_relative "source_file"
|
|
10
|
+
|
|
11
|
+
# All documentation files in the project.
|
|
12
|
+
#
|
|
13
|
+
# Projects contain many files. Only some are documentation: Ruby source with
|
|
14
|
+
# RDoc, Markdown guides, RDoc text files. Finding them manually is tedious.
|
|
15
|
+
# Missing files means missing broken links.
|
|
16
|
+
#
|
|
17
|
+
# Documentation enumerates all relevant files. It excludes vendor and temp
|
|
18
|
+
# directories. It wraps each path in a SourceFile for link extraction.
|
|
19
|
+
#
|
|
20
|
+
# === Example
|
|
21
|
+
#
|
|
22
|
+
# docs = Documentation.new("/project/root")
|
|
23
|
+
# docs.each do |file|
|
|
24
|
+
# file.links.each { |link| puts link.raw }
|
|
25
|
+
# end
|
|
26
|
+
# docs.count # => 392
|
|
27
|
+
#
|
|
28
|
+
class Documentation
|
|
29
|
+
include Enumerable
|
|
30
|
+
|
|
31
|
+
# File extensions to scan for links.
|
|
32
|
+
EXTENSIONS = %w[rb md rdoc].freeze
|
|
33
|
+
|
|
34
|
+
# Directories to skip.
|
|
35
|
+
EXCLUDES = %w[/vendor/ /tmp/].freeze
|
|
36
|
+
|
|
37
|
+
# Creates a new Documentation collection.
|
|
38
|
+
#
|
|
39
|
+
# [root] Project root directory to scan.
|
|
40
|
+
def initialize(root)
|
|
41
|
+
@root = Pathname.new(root)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Yields each SourceFile in the project.
|
|
45
|
+
def each(&)
|
|
46
|
+
files.each(&)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private def files # :nodoc:
|
|
50
|
+
@files ||= EXTENSIONS.flat_map { |ext| glob(ext) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private def glob(extension) # :nodoc:
|
|
54
|
+
Dir.glob(@root.join("**/*.#{extension}")).filter_map do |path|
|
|
55
|
+
next if EXCLUDES.any? { |exclude| path.include?(exclude) }
|
|
56
|
+
SourceFile.new(path, @root)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
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 "../problem"
|
|
9
|
+
|
|
10
|
+
# A <tt>file://</tt> URL in documentation.
|
|
11
|
+
#
|
|
12
|
+
# IDEs generate these links for local file access. They work on your machine.
|
|
13
|
+
# They break for everyone else. Published docs cannot contain local paths.
|
|
14
|
+
#
|
|
15
|
+
# FileUrl always returns a Problem. There is no valid use case for
|
|
16
|
+
# <tt>file://</tt> URLs in published documentation.
|
|
17
|
+
#
|
|
18
|
+
# === Example
|
|
19
|
+
#
|
|
20
|
+
# link = FileUrl.new("file:///path/to/file.rb", 10, source_file)
|
|
21
|
+
# link.problem(root) # => Problem (always)
|
|
22
|
+
#
|
|
23
|
+
class FileUrl < Link
|
|
24
|
+
# Returns a Problem. <tt>file://</tt> URLs never work in published docs.
|
|
25
|
+
#
|
|
26
|
+
# [_root] Unused. Present for interface compatibility.
|
|
27
|
+
def problem(_root)
|
|
28
|
+
Problem.new(self, "file:// URLs won't work when published")
|
|
29
|
+
end
|
|
30
|
+
end
|