ratatui_ruby 0.1.0 → 0.2.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +52 -0
  3. data/.builds/ruby-3.3.yml +52 -0
  4. data/.builds/ruby-3.4.yml +52 -0
  5. data/.builds/ruby-4.0.0-preview3.yml +53 -0
  6. data/AGENTS.md +2 -2
  7. data/README.md +33 -13
  8. data/REUSE.toml +5 -0
  9. data/Rakefile +3 -100
  10. data/docs/images/examples-calendar_demo.rb.png +0 -0
  11. data/docs/images/examples-chart_demo.rb.png +0 -0
  12. data/docs/images/examples-list_styles.rb.png +0 -0
  13. data/docs/images/examples-quickstart_lifecycle.rb.png +0 -0
  14. data/docs/images/examples-stock_ticker.rb.png +0 -0
  15. data/docs/quickstart.md +57 -11
  16. data/examples/analytics.rb +2 -1
  17. data/examples/calendar_demo.rb +55 -0
  18. data/examples/chart_demo.rb +84 -0
  19. data/examples/list_styles.rb +66 -0
  20. data/examples/login_form.rb +2 -1
  21. data/examples/quickstart_dsl.rb +30 -0
  22. data/examples/quickstart_lifecycle.rb +40 -0
  23. data/examples/readme_usage.rb +21 -0
  24. data/examples/stock_ticker.rb +13 -5
  25. data/examples/system_monitor.rb +2 -1
  26. data/examples/test_calendar_demo.rb +66 -0
  27. data/examples/test_list_styles.rb +61 -0
  28. data/ext/ratatui_ruby/.cargo/config.toml +5 -0
  29. data/ext/ratatui_ruby/Cargo.lock +94 -1
  30. data/ext/ratatui_ruby/Cargo.toml +3 -2
  31. data/ext/ratatui_ruby/extconf.rb +1 -1
  32. data/ext/ratatui_ruby/src/events.rs +4 -1
  33. data/ext/ratatui_ruby/src/rendering.rs +4 -1
  34. data/ext/ratatui_ruby/src/terminal.rs +4 -6
  35. data/ext/ratatui_ruby/src/widgets/calendar.rs +81 -0
  36. data/ext/ratatui_ruby/src/widgets/chart.rs +253 -0
  37. data/ext/ratatui_ruby/src/widgets/list.rs +41 -4
  38. data/ext/ratatui_ruby/src/widgets/mod.rs +2 -1
  39. data/lib/ratatui_ruby/dsl.rb +62 -0
  40. data/lib/ratatui_ruby/schema/calendar.rb +26 -0
  41. data/lib/ratatui_ruby/schema/chart.rb +81 -0
  42. data/lib/ratatui_ruby/schema/list.rb +8 -2
  43. data/lib/ratatui_ruby/version.rb +1 -1
  44. data/lib/ratatui_ruby.rb +21 -1
  45. data/mise.toml +8 -0
  46. data/sig/ratatui_ruby/schema/calendar.rbs +13 -0
  47. data/sig/ratatui_ruby/schema/{line_chart.rbs → chart.rbs} +20 -1
  48. data/sig/ratatui_ruby/schema/list.rbs +4 -1
  49. data/tasks/bump/cargo_lockfile.rb +19 -0
  50. data/tasks/bump/manifest.rb +23 -0
  51. data/tasks/bump/ruby_gem.rb +39 -0
  52. data/tasks/bump/sem_ver.rb +28 -0
  53. data/tasks/bump.rake +45 -0
  54. data/tasks/doc.rake +24 -0
  55. data/tasks/extension.rake +12 -0
  56. data/tasks/lint.rake +49 -0
  57. data/tasks/rdoc_config.rb +15 -0
  58. data/tasks/resources/build.yml.erb +65 -0
  59. data/tasks/resources/index.html.erb +38 -0
  60. data/tasks/resources/rubies.yml +7 -0
  61. data/tasks/sourcehut.rake +29 -0
  62. data/tasks/test.rake +31 -0
  63. data/tasks/website/index_page.rb +28 -0
  64. data/tasks/website/version.rb +116 -0
  65. data/tasks/website/versioned_documentation.rb +48 -0
  66. data/tasks/website/website.rb +50 -0
  67. data/tasks/website.rake +26 -0
  68. metadata +51 -10
  69. data/.build.yml +0 -34
  70. data/.ruby-version +0 -1
  71. data/ext/ratatui_ruby/src/widgets/linechart.rs +0 -154
  72. data/lib/ratatui_ruby/schema/line_chart.rb +0 -41
@@ -0,0 +1,13 @@
1
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ module RatatuiRuby
5
+ class Calendar < Data
6
+ attr_reader year: Integer
7
+ attr_reader month: Integer
8
+ attr_reader day_style: Style?
9
+ attr_reader header_style: Style?
10
+ attr_reader block: Block?
11
+ def self.new: (year: Integer, month: Integer, ?day_style: Style?, ?header_style: Style?, ?block: Block?) -> Calendar
12
+ end
13
+ end
@@ -2,11 +2,30 @@
2
2
  # SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  module RatatuiRuby
5
+ class Axis < Data
6
+ attr_reader title: String
7
+ attr_reader bounds: [Float, Float]
8
+ attr_reader labels: Array[String]
9
+ attr_reader style: Style?
10
+ def self.new: (?title: String, ?bounds: [Float, Float], ?labels: Array[String], ?style: Style?) -> Axis
11
+ end
12
+
5
13
  class Dataset < Data
6
14
  attr_reader name: String | Symbol
7
15
  attr_reader data: Array[[Float, Float]]
8
16
  attr_reader color: String | Symbol
9
- def self.new: (name: String | Symbol, data: Array[[Float, Float]], ?color: String | Symbol) -> Dataset
17
+ attr_reader marker: Symbol
18
+ attr_reader graph_type: Symbol
19
+ def self.new: (name: String | Symbol, data: Array[[Float, Float]], ?color: String | Symbol, ?marker: Symbol, ?graph_type: Symbol) -> Dataset
20
+ end
21
+
22
+ class Chart < Data
23
+ attr_reader datasets: Array[Dataset]
24
+ attr_reader x_axis: Axis
25
+ attr_reader y_axis: Axis
26
+ attr_reader block: Block?
27
+ attr_reader style: Style?
28
+ def self.new: (datasets: Array[Dataset], x_axis: Axis, y_axis: Axis, ?block: Block?, ?style: Style?) -> Chart
10
29
  end
11
30
 
12
31
  class LineChart < Data
@@ -5,7 +5,10 @@ module RatatuiRuby
5
5
  class List < Data
6
6
  attr_reader items: Array[String]
7
7
  attr_reader selected_index: Integer?
8
+ attr_reader style: Style?
9
+ attr_reader highlight_style: Style?
10
+ attr_reader highlight_symbol: String?
8
11
  attr_reader block: Block?
9
- def self.new: (?items: Array[String], ?selected_index: Integer?, ?block: Block?) -> List
12
+ def self.new: (?items: Array[String], ?selected_index: Integer?, ?style: Style?, ?highlight_style: Style?, ?highlight_symbol: String?, ?block: Block?) -> List
10
13
  end
11
14
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ # Lockfiles need to be refreshed by a tool after Manifests are changed.
7
+ class CargoLockfile < Data.define(:path, :dir, :name)
8
+ def exists?
9
+ File.exist?(path)
10
+ end
11
+
12
+ def refresh
13
+ return unless exists?
14
+
15
+ Dir.chdir(dir) do
16
+ system("cargo update -p #{name} --offline")
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ # Manifests hold a copy of the version number and should be changed manually.
7
+ # Use Regexp lookarounds in `pattern` to match the version number.
8
+ class Manifest < Data.define(:path, :pattern, :primary)
9
+ def read
10
+ File.read(path)
11
+ end
12
+
13
+ def initialize(path:, pattern:, primary: false)
14
+ super
15
+ end
16
+
17
+ def write(version)
18
+ return unless File.exist?(path)
19
+
20
+ new_content = read.gsub(pattern, version.to_s)
21
+ File.write(path, new_content)
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ class RubyGem
7
+ def initialize(manifests:, lockfile:)
8
+ raise ArgumentError, "Must have exactly one primary manifest" unless manifests.count(&:primary) == 1
9
+ @manifests = manifests
10
+ @lockfile = lockfile
11
+ end
12
+
13
+ def version
14
+ source = @manifests.find(&:primary)
15
+ content = source.read
16
+ match = content.match(source.pattern)
17
+ raise "Version missing in manifest #{source.path}" unless match
18
+
19
+ segments = Gem::Version.new(match[0]).segments
20
+ SemVer.new(segments.fill(0, 3).first(3))
21
+ end
22
+
23
+ def bump(segment)
24
+ target = version.next(segment)
25
+
26
+ puts "Bumping #{segment}: #{version} -> #{target}"
27
+ @manifests.each { |manifest| manifest.write(target) }
28
+ @lockfile.refresh
29
+ end
30
+
31
+ def set(version_string)
32
+ segments = Gem::Version.new(version_string).segments.fill(0, 3).first(3)
33
+ target = SemVer.new(segments)
34
+
35
+ puts "Setting version: #{version} -> #{target}"
36
+ @manifests.each { |manifest| manifest.write(target) }
37
+ @lockfile.refresh
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ # See https://semver.org/spec/v2.0.0.html
7
+ class SemVer
8
+ SEGMENTS = [:major, :minor, :patch].freeze
9
+
10
+ def initialize(segments)
11
+ @segments = segments
12
+ end
13
+
14
+ def next(segment)
15
+ index = SEGMENTS.index(segment)
16
+ raise ArgumentError, "Invalid segment: #{segment}" unless index
17
+
18
+ new_segments = @segments.dup
19
+ new_segments[index] += 1
20
+ new_segments.fill(0, (index + 1)..2)
21
+
22
+ SemVer.new(new_segments)
23
+ end
24
+
25
+ def to_s
26
+ @segments.join(".")
27
+ end
28
+ end
data/tasks/bump.rake ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require "rubygems"
7
+
8
+ require_relative "bump/sem_ver"
9
+ require_relative "bump/manifest"
10
+ require_relative "bump/cargo_lockfile"
11
+ require_relative "bump/ruby_gem"
12
+
13
+ namespace :bump do
14
+ ratatuiRuby = RubyGem.new(
15
+ manifests: [
16
+ Manifest.new(
17
+ path: "lib/ratatui_ruby/version.rb",
18
+ pattern: /(?<=VERSION = ")[^"]+(?=")/,
19
+ primary: true
20
+ ),
21
+ Manifest.new(
22
+ path: "ext/ratatui_ruby/Cargo.toml",
23
+ pattern: /(?<=^version = ")[^"]+(?=")/,
24
+ primary: false
25
+ ),
26
+ ],
27
+ lockfile: CargoLockfile.new(
28
+ path: "ext/ratatui_ruby/Cargo.lock",
29
+ dir: "ext/ratatui_ruby",
30
+ name: "ratatui_ruby"
31
+ )
32
+ )
33
+
34
+ SemVer::SEGMENTS.each do |segment|
35
+ desc "Bump #{segment} version"
36
+ task segment do
37
+ ratatuiRuby.bump(segment)
38
+ end
39
+ end
40
+
41
+ desc "Set exact version (e.g. rake bump:exact[0.1.0])"
42
+ task :exact, [:version] do |_, args|
43
+ ratatuiRuby.set(args[:version])
44
+ end
45
+ end
data/tasks/doc.rake ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require "rdoc/task"
7
+
8
+ require_relative "rdoc_config"
9
+
10
+ RDoc::Task.new do |rdoc|
11
+ rdoc.rdoc_dir = "doc"
12
+ rdoc.main = RDocConfig::MAIN
13
+ rdoc.rdoc_files.include(RDocConfig::RDOC_FILES)
14
+ end
15
+
16
+ task :copy_doc_images do
17
+ if Dir.exist?("docs/images")
18
+ FileUtils.mkdir_p "doc/docs/images"
19
+ FileUtils.cp_r Dir["docs/images/*.png"], "doc/docs/images"
20
+ end
21
+ end
22
+
23
+ Rake::Task[:rdoc].enhance [:copy_doc_images]
24
+ Rake::Task[:rerdoc].enhance [:copy_doc_images]
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require "rake/extensiontask"
7
+
8
+ spec = Gem::Specification.load("ratatui_ruby.gemspec")
9
+ Rake::ExtensionTask.new("ratatui_ruby", spec) do |ext|
10
+ ext.lib_dir = "lib/ratatui_ruby"
11
+ ext.ext_dir = "ext/ratatui_ruby"
12
+ end
data/tasks/lint.rake ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require "rubocop/rake_task"
7
+ require "rubycritic/rake_task"
8
+ require "inch/rake"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ RubyCritic::RakeTask.new do |task|
13
+ task.options = "--no-browser"
14
+ task.paths = FileList.new.include("exe/**/*.rb", "lib/**/*.rb", "sig/**/*.rbs")
15
+ end
16
+
17
+ Inch::Rake::Suggest.new("doc:suggest", "exe/**/*.rb", "lib/**/*.rb", "sig/**/*.rbs") do |suggest|
18
+ suggest.args << ""
19
+ end
20
+
21
+ namespace :cargo do
22
+ desc "Run cargo fmt"
23
+ task :fmt do
24
+ sh "cd ext/ratatui_ruby && cargo fmt --all -- --check"
25
+ end
26
+
27
+ desc "Run cargo clippy"
28
+ task :clippy do
29
+ sh "cd ext/ratatui_ruby && cargo clippy -- -D warnings"
30
+ end
31
+ end
32
+
33
+ namespace :reuse do
34
+ desc "Run the REUSE Tool to confirm REUSE compliance"
35
+ task :lint do
36
+ sh "reuse lint"
37
+ end
38
+ end
39
+ task(:reuse) { Rake::Task["reuse:lint"].invoke }
40
+
41
+ namespace :lint do
42
+ task docs: %w[rubycritic rdoc:coverage reuse:lint]
43
+ task code: %w[rubocop rubycritic cargo:fmt cargo:clippy cargo:test]
44
+ task licenses: %w[reuse:lint]
45
+ task all: %w[docs code licenses]
46
+ end
47
+
48
+ desc "Run all lint tasks"
49
+ task(:lint) { Rake::Task["lint:all"].invoke }
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ module RDocConfig
7
+ RDOC_FILES = %w[
8
+ **/*.md
9
+ **/*.rdoc
10
+ lib/**/*.rb
11
+ exe/**/*
12
+ ].freeze
13
+
14
+ MAIN = "README.md"
15
+ end
@@ -0,0 +1,65 @@
1
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ image: alpine/edge
5
+ packages:
6
+ - bash
7
+ - build-base
8
+ - curl
9
+ - openssl-dev
10
+ - yaml-dev
11
+ - zlib-dev
12
+ - readline-dev
13
+ - gdbm-dev
14
+ - ncurses-dev
15
+ - libffi-dev
16
+ - clang-dev
17
+ - git
18
+ artifacts:
19
+ - ratatui_ruby/pkg/<%= gem_filename %>
20
+ sources:
21
+ - https://git.sr.ht/~kerrick/ratatui_ruby
22
+ tasks:
23
+ - setup: |
24
+ curl https://mise.jdx.dev/install.sh | sh
25
+ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.buildenv
26
+ echo 'eval "$($HOME/.local/bin/mise activate bash)"' >> ~/.buildenv
27
+ . ~/.buildenv
28
+ export RUSTFLAGS="-C target-feature=-crt-static"
29
+ export CI="true"
30
+ cd ratatui_ruby
31
+ sed -i 's/ruby = .*/ruby = "<%= ruby_version %>"/' mise.toml
32
+ mise install
33
+ mise x -- pip install reuse
34
+ mise x -- gem install bundler
35
+ mise reshim
36
+ mise x -- bundle config set --local frozen 'true'
37
+ mise x -- bundle install
38
+ <% if ruby_version.start_with?("4.0") -%>
39
+ # We allow this to fail so we can see logs without stopping the CI pipeline
40
+ mise x -- bundle exec rake compile || echo "⚠️ Compilation failed (Allowed Failure)"
41
+ <% else -%>
42
+ mise x -- bundle exec rake compile
43
+ <% end -%>
44
+ - test: |
45
+ . ~/.buildenv
46
+ cd ratatui_ruby
47
+ echo "Testing Ruby <%= ruby_version %>"
48
+ <% if ruby_version.start_with?("4.0") -%>
49
+ mise x -- bundle exec rake test || true
50
+ <% else -%>
51
+ mise x -- bundle exec rake test
52
+ <% end -%>
53
+ - lint: |
54
+ . ~/.buildenv
55
+ cd ratatui_ruby
56
+ echo "Linting Ruby <%= ruby_version %>"
57
+ <% if ruby_version.start_with?("4.0") -%>
58
+ mise x -- bundle exec rake lint || true
59
+ <% else -%>
60
+ mise x -- bundle exec rake lint
61
+ <% end -%>
62
+ - package: |
63
+ . ~/.buildenv
64
+ cd ratatui_ruby
65
+ mise x -- bundle exec rake build
@@ -0,0 +1,38 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= project_name %> documentation</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <style>
7
+ :root { color-scheme: light dark; }
8
+ body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.5; color: light-dark(#111, #eee); background: light-dark(#fff, #111); }
9
+ h1 { border-bottom: 2px solid light-dark(#eee, #333); padding-bottom: 0.5rem; }
10
+ ul { list-style: none; padding: 0; }
11
+ li { margin: 0.5rem 0; border: 1px solid light-dark(#ddd, #444); border-radius: 4px; }
12
+ a { display: block; padding: 1rem; text-decoration: none; color: light-dark(#0055aa, #44aaff); font-weight: bold; }
13
+ a:hover { background: light-dark(#f5f5f5, #222); }
14
+ .meta { font-weight: normal; color: light-dark(#666, #aaa); font-size: 0.9em; float: right; }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <h1><%= project_name %> documentation</h1>
19
+ <ul>
20
+ <% versions.each do |version| %>
21
+ <li>
22
+ <a href='<%= version.slug %>/index.html'>
23
+ <%= version.name %>
24
+ <span class='meta'>
25
+ <% if version.latest? %>
26
+ Latest
27
+ <% elsif version.edge? %>
28
+ Edge
29
+ <% else %>
30
+ Historical
31
+ <% end %>
32
+ </span>
33
+ </a>
34
+ </li>
35
+ <% end %>
36
+ </ul>
37
+ </body>
38
+ </html>
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ - "3.2"
5
+ - "3.3"
6
+ - "3.4"
7
+ - "4.0.0-preview3"
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ desc "Generate SourceHut build manifests from template"
7
+ task :sourcehut do
8
+ require "erb"
9
+ require "yaml"
10
+
11
+ spec = Gem::Specification.load("ratatui_ruby.gemspec")
12
+ gem_filename = "#{spec.name}-#{spec.version}.gem"
13
+
14
+ rubies = YAML.load_file("tasks/resources/rubies.yml")
15
+ template = File.read("tasks/resources/build.yml.erb")
16
+ erb = ERB.new(template, trim_mode: "-")
17
+
18
+ FileUtils.mkdir_p ".builds"
19
+
20
+ # Remove old generated files to ensure a clean state
21
+ Dir.glob(".builds/*.yml").each { |f| File.delete(f) }
22
+
23
+ rubies.each do |ruby_version|
24
+ filename = ".builds/ruby-#{ruby_version}.yml"
25
+ puts "Generating #{filename}..."
26
+ content = erb.result_with_hash(ruby_version:, gem_filename:)
27
+ File.write(filename, content)
28
+ end
29
+ end
data/tasks/test.rake ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require "minitest/test_task"
7
+
8
+ namespace :cargo do
9
+ desc "Run cargo tests"
10
+ task :test do
11
+ sh "cd ext/ratatui_ruby && cargo test"
12
+ end
13
+ end
14
+
15
+ # Clear the default test task created by Minitest::TestTask if it exists
16
+ Rake::Task["test"].clear if Rake::Task.task_defined?("test")
17
+
18
+ desc "Run all tests (Ruby and Rust)"
19
+ task test: %w[test:ruby test:rust]
20
+
21
+ namespace :test do
22
+ desc "Run Rust tests"
23
+ task :rust do
24
+ Rake::Task["cargo:test"].invoke
25
+ end
26
+
27
+ # Create a specific Minitest task for Ruby tests
28
+ Minitest::TestTask.create(:ruby) do |t|
29
+ t.test_globs = ["test/**/test_*.rb", "examples/**/test_*.rb"]
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require "erb"
7
+
8
+ class IndexPage
9
+ def initialize(versions)
10
+ @versions = versions
11
+
12
+ latest_version = @versions.find { |v| v.is_a?(Tagged) }
13
+ latest_version.is_latest = true if latest_version
14
+ end
15
+
16
+ def publish_to(path, project_name:)
17
+ puts "Generating index page..."
18
+
19
+ template_path = File.expand_path("../resources/index.html.erb", __dir__)
20
+ template = File.read(template_path)
21
+
22
+ versions = @versions
23
+ # project_name is used in the ERB
24
+ html_content = ERB.new(template).result(binding)
25
+
26
+ File.write(path, html_content)
27
+ end
28
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require "rubygems"
7
+ require "fileutils"
8
+
9
+ class Version
10
+ def self.all
11
+ tags = `git tag`.split.grep(/^v\d/)
12
+ sorted_versions = tags.map { |t| Tagged.new(t) }
13
+ .sort_by { |v| v.semver }
14
+ .reverse
15
+
16
+ [Edge.new] + sorted_versions
17
+ end
18
+
19
+ def slug
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def name
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def type
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def checkout(globs:, &block)
32
+ raise NotImplementedError
33
+ end
34
+
35
+ def latest?
36
+ false
37
+ end
38
+
39
+ def edge?
40
+ false
41
+ end
42
+ end
43
+
44
+ class Edge < Version
45
+ def slug
46
+ "main"
47
+ end
48
+
49
+ def name
50
+ "main"
51
+ end
52
+
53
+ def type
54
+ :edge
55
+ end
56
+
57
+ def edge?
58
+ true
59
+ end
60
+
61
+ def checkout(globs:, &block)
62
+ Dir.mktmpdir do |path|
63
+ # Use git ls-files for accurate source list
64
+ files = `git ls-files`.split("\n").select do |f|
65
+ globs.any? { |glob| File.fnmatch(glob, f, File::FNM_PATHNAME) }
66
+ end
67
+
68
+ files.each do |file|
69
+ dest = File.join(path, file)
70
+ FileUtils.mkdir_p(File.dirname(dest))
71
+ FileUtils.cp(file, dest)
72
+ end
73
+
74
+ yield path
75
+ end
76
+ end
77
+ end
78
+
79
+ class Tagged < Version
80
+ attr_reader :tag
81
+
82
+ def initialize(tag)
83
+ @tag = tag
84
+ end
85
+
86
+ def slug
87
+ @tag
88
+ end
89
+
90
+ def name
91
+ @tag
92
+ end
93
+
94
+ def type
95
+ :version
96
+ end
97
+
98
+ def semver
99
+ Gem::Version.new(@tag.sub(/^v/, ""))
100
+ end
101
+
102
+ attr_accessor :is_latest
103
+
104
+ def latest?
105
+ @is_latest
106
+ end
107
+
108
+ def checkout(globs:, &block)
109
+ Dir.mktmpdir do |path|
110
+ system("git archive #{@tag} | tar -x -C #{path}")
111
+ # We could enforce globs here too, but git archive is usually sufficient.
112
+ FileUtils.rm_rf("#{path}/ext")
113
+ yield path
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require_relative "../rdoc_config"
7
+
8
+ class VersionedDocumentation
9
+ def initialize(version)
10
+ @version = version
11
+ end
12
+
13
+ def publish_to(path, project_name:, globs:, assets: [])
14
+ puts "Building documentation for #{@version.name}..."
15
+
16
+ absolute_path = File.absolute_path(path)
17
+ gemfile_path = File.absolute_path("Gemfile")
18
+
19
+ @version.checkout(globs: globs) do |source_path|
20
+ Dir.chdir(source_path) do
21
+ title = "#{project_name} #{@version.name}"
22
+ title = "#{project_name} (main)" if @version.edge?
23
+
24
+ # We need to expand globs relative to the source path
25
+ files = globs.flat_map { |glob| Dir[glob] }.uniq
26
+
27
+ system(
28
+ { "BUNDLE_GEMFILE" => gemfile_path },
29
+ "bundle exec rdoc -o #{absolute_path} --main #{RDocConfig::MAIN} --title '#{title}' #{files.join(' ')}"
30
+ )
31
+
32
+ copy_assets_to(absolute_path, assets)
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def copy_assets_to(path, assets)
40
+ assets.each do |asset_dir|
41
+ if Dir.exist?(asset_dir)
42
+ destination = File.join(path, asset_dir)
43
+ FileUtils.mkdir_p(destination)
44
+ FileUtils.cp_r Dir["#{asset_dir}/*"], destination
45
+ end
46
+ end
47
+ end
48
+ end