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,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
@@ -0,0 +1,12 @@
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
+ def self.window_size: () -> WindowSize?
11
+ end
12
+ end
@@ -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
- module Terminal
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
@@ -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
@@ -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:, changelog:)
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 bump(segment)
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 puts_commit_message(message)
45
- puts "=" * 80
46
- puts message
47
- puts "=" * 80
26
+ private def refresh_bundler_lockfile
27
+ system("bundle install", exception: true)
48
28
  end
49
29
  end
@@ -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/changelog"
14
+ require_relative "bump/release_from_trunk"
15
+ require_relative "bump/patch_release"
15
16
 
16
17
  namespace :bump do
17
- ratatuiRuby = RubyGem.new(
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
- SemVer::SEGMENTS.each do |segment|
39
- desc "Bump #{segment} version"
40
- task segment do
41
- ratatuiRuby.bump(segment)
42
- Rake::Task["sourcehut"].invoke
43
- end
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
- ratatuiRuby.set(args[:version])
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