Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 74 additions & 5 deletions .github/workflows/build-gems.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ on:

jobs:
build:
name: Build (${{ matrix.os }})
name: Build (${{ matrix.os }}, Ruby ${{ matrix.ruby }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
ruby: ["3.2", "3.3", "3.4", "4.0"]
os: [ubuntu-latest, macos-latest, windows-latest]
env:
CI: "true"
Expand All @@ -25,13 +26,13 @@ jobs:

- name: Override Ruby version in mise.toml
shell: bash
run: sed -i.bak 's/ruby = .*/ruby = "4.0"/' mise.toml
run: sed -i.bak 's/ruby = .*/ruby = "${{ matrix.ruby }}"/' mise.toml

- name: Install Ruby via RubyInstaller (Windows)
if: runner.os == 'Windows'
uses: ruby/setup-ruby@v1
with:
ruby-version: "4.0"
ruby-version: ${{ matrix.ruby }}

- name: Configure Windows build environment
if: runner.os == 'Windows'
Expand Down Expand Up @@ -60,11 +61,79 @@ jobs:
- name: Default task
run: mise x -- bundle exec rake

- name: Build native gem
run: mise x -- bundle exec rake native gem
- name: Upload compiled binary
uses: actions/upload-artifact@v4
with:
name: native-${{ matrix.os }}-ruby${{ matrix.ruby }}
path: lib/ratatui_ruby/ratatui_ruby.*
if-no-files-found: error

package:
name: Package (${{ matrix.os }})
needs: build
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4

- name: Install Ruby via RubyInstaller (Windows)
if: runner.os == 'Windows'
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"

- name: Override Ruby version in mise.toml (Unix)
if: runner.os != 'Windows'
shell: bash
run: sed -i.bak 's/ruby = .*/ruby = "3.4"/' mise.toml

- name: Install toolchain via mise (Unix)
if: runner.os != 'Windows'
uses: jdx/mise-action@v2

- name: Configure Windows mise (skip Ruby)
if: runner.os == 'Windows'
shell: pwsh
run: |
Add-Content -Path .mise.local.toml -Value "[settings]`ndisable_tools = [`"ruby`"]"

- name: Install toolchain via mise (Windows)
if: runner.os == 'Windows'
uses: jdx/mise-action@v2

- name: Download Ruby 3.2 binary
uses: actions/download-artifact@v4
with:
name: native-${{ matrix.os }}-ruby3.2
path: lib/ratatui_ruby/3.2

- name: Download Ruby 3.3 binary
uses: actions/download-artifact@v4
with:
name: native-${{ matrix.os }}-ruby3.3
path: lib/ratatui_ruby/3.3

- name: Download Ruby 3.4 binary
uses: actions/download-artifact@v4
with:
name: native-${{ matrix.os }}-ruby3.4
path: lib/ratatui_ruby/3.4

- name: Download Ruby 4.0 binary
uses: actions/download-artifact@v4
with:
name: native-${{ matrix.os }}-ruby4.0
path: lib/ratatui_ruby/4.0

- name: Build platform gem
run: ruby tasks/release/platform_gem.rb

- name: Upload gem artifact
uses: actions/upload-artifact@v4
with:
name: gem-${{ matrix.os }}
path: pkg/*.gem
if-no-files-found: error
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Architecture:
### STRICT REQUIREMENTS

- **Check Before Implementing:** FIRST check tests for existing coverage. If it works, say so and point to the test.
- Every file MUST begin with an SPDX-compliant header. Use `AGPL-3.0-or-later` for code; `CC-BY-SA-4.0` for documentation. `reuse annotate` can help you generate the header. **For Ruby files**, wrap SPDX comments in `#--` / `#++` to hide them from RDoc output.
- Every file MUST begin with an SPDX-compliant header. License by directory: `LGPL-3.0-or-later` for library source (`lib/`, `ext/`, `test/`, `sig/`); `MIT-0` for widget and verify examples (`examples/widget_*`, `examples/verify_*`); `AGPL-3.0-or-later` for app examples, tasks, and tooling (`examples/app_*`, `tasks/`, `bin/`); `CC-BY-SA-4.0` for documentation. `reuse annotate` can help you generate the header. **For Ruby files**, wrap SPDX comments in `#--` / `#++` to hide them from RDoc output.
- Every line of Ruby MUST be covered by tests that would stand up to mutation testing.
- Tests must be meaningful and verify specific behavior or rendering output; simply verifying that code "doesn't crash" is insufficient and unacceptable.
- **Prefer snapshot tests** (`assert_snapshots`, plural) over manual `buffer_content` assertions for UI widgets. Snapshots are self-documenting and easier to maintain.
Expand Down
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,42 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Added

### Changed

### Fixed

### Removed

## [1.4.2] - 2026-02-22

### Added

### Changed

### Fixed

- **Rust source SPDX headers**: All 42 `.rs` files in `ext/` were incorrectly tagged `AGPL-3.0-or-later` instead of `LGPL-3.0-or-later`. The v0.9.0 relicensing to LGPL was applied to `lib/` and `sig/` but missed the Rust extension source in `ext/`.
- **`Text.width` emoji width**: `Text.width` now delegates to Ratatui's `Text::width()` instead of summing per-character widths. The per-character approach returned 1 for emoji with variation selectors (e.g. `➡️`), while the grapheme-aware `Text::width()` correctly returns 2.
- **Buffer serialization of wide characters**: `buffer_content` now skips continuation cells after wide characters (emoji, CJK). Previously, the continuation cell's space was included in the serialized string, inflating its display width by 1 per wide character.

### Removed

## [1.4.1] - 2026-02-15

### Added

### Changed

### Fixed

- **Precompiled Native Gems**: Now support Ruby 3.2, 3.3, 3.4, and 4.0 (previously only 4.0)

### Removed

## [1.4.0] - 2026-02-15

### Added

- **Class-Wide Snapshot Normalization**: `TestHelper::Snapshot` now supports a `normalize_snapshots` template method hook. Override it in your test class to mask dynamic content (timestamps, PIDs, temp paths) across all snapshot assertions without repeating normalization blocks. The hook composes with per-call blocks: the hook runs first, then the block.
- **Precompiled Native Gems**: Precompiled native gems are now published for Windows (x64-mingw-ucrt), macOS, and Linux, eliminating the need to compile the Rust extension from source.

Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
ratatui_ruby (1.3.0)
ratatui_ruby (1.4.2)
rb_sys (~> 0.9)
rexml (~> 3.4)

Expand Down Expand Up @@ -354,7 +354,7 @@ CHECKSUMS
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
rake-compiler (1.3.1) sha256=6b351612b6e2d73ddd5563ee799bb58685176e05363db6758504bd11573d670a
rake-compiler-dock (1.10.0) sha256=dd62ee19df2a185a3315697e560cfa8cc9129901332152851e023fab0e94bf11
ratatui_ruby (1.3.0)
ratatui_ruby (1.4.2)
rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe
rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e
rb_sys (0.9.123) sha256=c22ae84d1bca3eec0f13a45ae4ca9ba6eace93d5be270a40a9c0a9a5b92a34e5
Expand Down
1 change: 1 addition & 0 deletions examples/widget_table/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Data grids are complex. Users expect to navigate them with keys, select rows, an
- **Arrows (←/→)**: Navigate Columns (`selected_column`)
- **x**: Toggle Row Selection (`selected_row` = nil)
- **s**: Cycle Table Style (`style`)
- **y**: Cycle Symbol (`highlight_symbol`)
- **p**: Cycle Spacing (`highlight_spacing`)
- **c**: Toggle Column Highlight (`column_highlight_style`)
- **z**: Toggle Cell Highlight (`cell_highlight_style`)
Expand Down
17 changes: 15 additions & 2 deletions examples/widget_table/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@
].freeze

class WidgetTable
attr_reader :selected_index, :selected_col, :current_style_index, :column_spacing, :highlight_spacing, :column_highlight_style, :cell_highlight_style
attr_reader :selected_index, :selected_col, :current_style_index, :column_spacing, :column_highlight_style, :cell_highlight_style

HIGHLIGHT_SYMBOLS = [
"> ",
">",
"➡️",
"→",
].freeze

HIGHLIGHT_SPACINGS = [
{ name: "When Selected", spacing: :when_selected },
Expand Down Expand Up @@ -50,6 +57,7 @@ def initialize
@selected_col = 1
@current_style_index = 0
@column_spacing = 1
@highlight_symbol_index = 0
@highlight_spacing_index = 0
@show_column_highlight = true
@show_cell_highlight = true
Expand Down Expand Up @@ -126,6 +134,7 @@ def run

current_style_entry = @styles[@current_style_index]
current_spacing_entry = HIGHLIGHT_SPACINGS[@highlight_spacing_index]
current_symbol_entry = HIGHLIGHT_SYMBOLS[@highlight_symbol_index]
offset_mode_entry = OFFSET_MODES[@offset_mode_index]
flex_mode_entry = FLEX_MODES[@flex_mode_index]

Expand All @@ -145,7 +154,7 @@ def run
selected_column: @selected_col,
offset: effective_offset,
row_highlight_style:,
highlight_symbol: "> ",
highlight_symbol: current_symbol_entry,
highlight_spacing: current_spacing_entry[:spacing],
column_highlight_style: @show_column_highlight ? @column_highlight_style : nil,
cell_highlight_style: @show_cell_highlight ? @cell_highlight_style : nil,
Expand Down Expand Up @@ -183,6 +192,8 @@ def run
@tui.text_span(content: ": Style (#{current_style_entry[:name]}) "),
@tui.text_span(content: "p", style: @hotkey_style),
@tui.text_span(content: ": Spacing (#{current_spacing_entry[:name]}) "),
@tui.text_span(content: "y", style: @hotkey_style),
@tui.text_span(content: ": Symbol (#{current_symbol_entry}) "),
@tui.text_span(content: "t", style: @hotkey_style),
@tui.text_span(content: ": Tamp Row"),
]),
Expand Down Expand Up @@ -250,6 +261,8 @@ def run
@column_spacing += 1
in type: :key, code: "-"
@column_spacing = [@column_spacing - 1, 0].max
in type: :key, code: "y"
@highlight_symbol_index = (@highlight_symbol_index + 1) % HIGHLIGHT_SYMBOLS.length
in type: :key, code: "p"
@highlight_spacing_index = (@highlight_spacing_index + 1) % HIGHLIGHT_SPACINGS.length
in type: :key, code: "x"
Expand Down
19 changes: 6 additions & 13 deletions ext/ratatui_ruby/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions ext/ratatui_ruby/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

[package]
name = "ratatui_ruby"
version = "1.3.0"
version = "1.4.2"
edition = "2021"

[lib]
Expand All @@ -13,7 +13,6 @@ crate-type = ["cdylib", "staticlib"]
magnus = "0.8.2"
rb-sys = "*"
ratatui = { version = "0.30", features = ["widget-calendar", "layout-cache", "unstable-rendered-line-info", "palette"] }
unicode-width = "0.1"

bumpalo = "3.16"
lazy_static = "1.4"
Expand Down
2 changes: 1 addition & 1 deletion ext/ratatui_ruby/src/color.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: LGPL-3.0-or-later

//! Color conversion functions exposed to Ruby.
//!
Expand Down
2 changes: 1 addition & 1 deletion ext/ratatui_ruby/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: LGPL-3.0-or-later

use magnus::{prelude::*, Error, Value};

Expand Down
2 changes: 1 addition & 1 deletion ext/ratatui_ruby/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: LGPL-3.0-or-later

use magnus::{Error, IntoValue, TryConvert, Value};
use rb_sys::rb_thread_call_without_gvl;
Expand Down
2 changes: 1 addition & 1 deletion ext/ratatui_ruby/src/frame.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: LGPL-3.0-or-later

//! Frame wrapper for exposing Ratatui's Frame to Ruby.
//!
Expand Down
2 changes: 1 addition & 1 deletion ext/ratatui_ruby/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: LGPL-3.0-or-later

// Require SAFETY comments on all unsafe blocks
#![warn(clippy::undocumented_unsafe_blocks)]
Expand Down
2 changes: 1 addition & 1 deletion ext/ratatui_ruby/src/lib_header.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: LGPL-3.0-or-later

// Require SAFETY comments on all unsafe blocks
#![warn(clippy::undocumented_unsafe_blocks)]
Expand Down
3 changes: 2 additions & 1 deletion ext/ratatui_ruby/src/rendering.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: LGPL-3.0-or-later

use crate::style::{parse_color_value, parse_modifier_str, parse_style};
use crate::widgets;
Expand Down Expand Up @@ -56,6 +56,7 @@ pub fn render_node(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), E
"RatatuiRuby::Widgets::Table" => widgets::table::render(buffer, area, node)?,
"RatatuiRuby::Widgets::Block" => widgets::block::render(buffer, area, node)?,
"RatatuiRuby::Widgets::Tabs" => widgets::tabs::render(buffer, area, node)?,
"RatatuiRuby::Widgets::ScrollView" => widgets::scroll_view::render(buffer, area, node)?,
"RatatuiRuby::Widgets::Scrollbar" => widgets::scrollbar::render(buffer, area, node)?,
"RatatuiRuby::Widgets::BarChart" => widgets::barchart::render(buffer, area, node)?,
"RatatuiRuby::Widgets::Canvas" => widgets::canvas::render(buffer, area, node)?,
Expand Down
Loading