alignment_audit

Legacy Migrations

Outstanding

alignment_audit

alignment_audit_granular_level

v0.7.0 Alignment Audit (Parameter-Level)

This document audits alignment between RatatuiRuby v0.7.0 and the upstream Ratatui/Crossterm Rust libraries at the parameter and enum value level. Only gaps are listed.


MISSING β€” Layout Module
Layout::Constraint β€” Batch Constructors βœ… DONE

All 6 batch constructors implemented in v0.8.0: - from_lengths, from_percentages, from_mins, from_maxes, from_fills, from_ratios


Layout::Layout β€” Missing Parameters
Missing Parameter Ratatui Type Notes
margin Margin { horizontal, vertical } Edge margins
spacing u16 Gap between segments

MISSING β€” Style Module
Style::Style β€” Missing Parameters/Methods
MISSING β€” Text Module
Text::Span β€” Missing Methods
Missing Method Signature Notes
width span.width β†’ Integer Display width in terminal cells
raw Span.raw(content) β†’ Span Constructor without style (alias for new(content:))
patch_style span.patch_style(style) β†’ Span Merge style on top of existing
reset_style span.reset_style β†’ Span Clear style

Text::Line β€” Missing Methods
Missing Method Signature Notes
left_aligned line.left_aligned β†’ Line Fluent setter for :left alignment
centered line.centered β†’ Line Fluent setter for :center alignment
right_aligned line.right_aligned β†’ Line Fluent setter for :right alignment
push_span line.push_span(span) β†’ Line Append span
patch_style line.patch_style(style) β†’ Line Merge style on all spans
reset_style line.reset_style β†’ Line Clear style on all spans


Widgets::Table β€” Missing Row Methods (via Widgets::Row)
Missing Ratatui API Notes
enable_strikethrough row.enable_strikethrough() Enable strikethrough on row

Widget Computation Methods β€” Missing

These are public &self methods on upstream widgets that compute/query values without rendering.

| Widget | Missing Method | Ratatui API | Notes | |——–|β€”β€”β€”β€”β€”-|β€”β€”β€”β€”-|β€”β€”-|

| List | len | list.len() β†’ usize | Number of items in the list | | List | is_empty | list.is_empty() β†’ bool | True if no items (empty? in Ruby) | | Canvas | get_point | canvas.get_point(x, y) β†’ Option<(usize, usize)> | Map coordinates to grid cell |

[!NOTE] Paragraph#line_count(width) and Paragraph#line_width ARE exposed βœ“Tabs#width IS exposed βœ“


State Navigation Methods β€” Missing

TableState has navigation helpers that are not exposed.

State Class Missing Method Ratatui API Notes
TableState selected_cell state.selected_cell() Get (row, col) tuple
TableState with_selected_cell state.with_selected_cell((r,c)) Builder pattern
TableState select_next_column state.select_next_column() Navigate columns
TableState select_previous_column state.select_previous_column() Navigate columns
TableState select_first_column state.select_first_column() Jump to first column
TableState select_last_column state.select_last_column() Jump to last column

Layout Module β€” Additional Missing
Module Missing Ratatui API Notes
Rect as_position rect.as_position() β†’ Position Convert to Position
Rect as_size rect.as_size() β†’ Size Convert to Size
Rect outer rect.outer(margin) β†’ Rect Expand by margin (inverse of inner)
Rect resize rect.resize(size) β†’ Rect Change dimensions, preserve position
Rect centered_horizontally rect.centered_horizontally(constraint) β†’ Rect Center horizontally via Layout
Rect centered_vertically rect.centered_vertically(constraint) β†’ Rect Center vertically via Layout
Rect centered rect.centered(h, v) β†’ Rect Center both axes
Constraint apply constraint.apply(length) β†’ u16 Compute constrained size
Layout split_with_spacers layout.split_with_spacers(area) Returns segments AND spacers

Color β€” Missing Constructors
Missing Ratatui API Notes
from_u32 Color::from_u32(0xRRGGBB) Construct from hex integer
from_hsl Color::from_hsl(hsl) Construct from HSL (requires palette feature)
from_hsluv Color::from_hsluv(hsluv) Construct from HSLuv (requires palette feature)

Buffer β€” Missing Query Methods
Missing Method Ratatui API Notes
content buffer.content() β†’ &[Cell] Get all cells as slice
get buffer.get(x, y) β†’ &Cell Get cell at position
index_of buffer.index_of(x, y) β†’ usize Position to linear index
pos_of buffer.pos_of(i) β†’ (u16, u16) Linear index to position

alignment_audit_high_level

v0.7.0 Alignment Audit

This document audits strict alignment between RatatuiRuby v0.7.0 and the upstream Ratatui/Crossterm Rust libraries. The audit covers modules, classes, static methods, and constructor arguments as specified in the Ruby Frontend Design.

[!NOTE] The TUI facade API is explicitly excluded from this audit. It provides ergonomic shortcuts that intentionally diverge from Ratatui naming.

β€”

| margin | Margin | ❌ Missing | Gap | | spacing | u16 | ❌ Missing | Gap |

Verdict: Core layout aligned. Margin and spacing are gaps.



Gaps Analysis: MISSING vs MISALIGNED

[!IMPORTANT] MISSING = Can be added as new features without breaking backwards compatibility.
MISALIGNED = Requires breaking changes before v1.0.0 to fix API shape.

MISSING Features (Additive, Backwards-Compatible) βœ…

These are gaps that can be filled in future minor releases without breaking existing code:

Component Missing Feature Notes
Constraint from_lengths(), from_percentages(), etc. New class methods
Layout margin, spacing New optional constructor args
Style sub_modifier, underline_color New optional constructor args
Span width() instance method New instance method
Span raw() constructor New class method (alias for new)
Line left_aligned(), centered(), right_aligned() New instance methods (fluent)

alignment_audit_symbol_sets

Audit of symbol set alignment between Ratatui’s symbols:: module and RatatuiRuby.

[!IMPORTANT] MISSING = Can be added as new features, backwards-compatible. MISALIGNED = Requires breaking changes before v1.0.0.

| scrollbar::Set | 4 predefined sets | ❌ Not exposed | MISSING | | shade constants | 5 constants | ❌ Not exposed | MISSING |

MISSING β€” Marker Enum
Marker::HalfBlock

| Marker::HalfBlock | ❌ Not exposed | MISSING |

Impact: Users cannot use the HalfBlock marker type, which provides double-resolution square pixels using β–ˆ, β–„, and β–€ characters.


MISSING β€” Line Set Customization

Ratatui provides symbols::line::Set with predefined sets: - NORMAL β€” Standard box-drawing characters - ROUNDED β€” Rounded corners - DOUBLE β€” Double-line characters - THICK β€” Thick line characters

Ruby Status: Not directly exposed. Users cannot customize line symbols for widgets that use them internally.


MISSING β€” Bar Set Customization

Ratatui provides symbols::bar::Set with predefined sets: - THREE_LEVELS β€” 3 distinct fill levels - NINE_LEVELS β€” 9 distinct fill levels (default)

Ruby Status: Not exposed. Used internally by widgets like Sparkline but not configurable.


MISSING β€” Block Set Customization

Ratatui provides symbols::block::Set with predefined sets: - THREE_LEVELS β€” 3 distinct fill levels - NINE_LEVELS β€” 9 distinct fill levels (default)

Ruby Status: Not exposed. Used internally by Gauge widget but not configurable.


MISSING β€” Scrollbar Set Customization

Ratatui provides symbols::scrollbar::Set with predefined sets: - DOUBLE_VERTICAL β€” Double-line vertical scrollbar - DOUBLE_HORIZONTAL β€” Double-line horizontal scrollbar - VERTICAL β€” Single-line vertical scrollbar - HORIZONTAL β€” Single-line horizontal scrollbar

Ruby Status: Not exposed. Scrollbar widget not currently implemented in RatatuiRuby.


MISSING β€” Shade Constants

Ratatui provides symbols::shade constants: - EMPTY β€” (space) - LIGHT β€” β–‘ - MEDIUM β€” β–’ - DARK β€” β–“


Recommendations
Priority Item Notes
Low Add :half_block marker Single symbol addition
Low Expose line::Set customization For LineGauge widget
Low Expose bar::Set customization For Sparkline widget
Low Expose block::Set customization For Gauge widget
Medium Implement Scrollbar widget Would include scrollbar::Set

All missing items are additive and do not require breaking changes.


alignment_audit_tui_final

TUI API Alignment Audit

This document audits the RatatuiRuby::TUI facade API for method and parameter naming, with a focus on Developer Experience (DX) before the v1.0.0 release.

Design Philosophy

The TUI API follows a β€œMullet Architecture”: structured namespaces in the library, flat ergonomic DSL for users.

Guiding Principles:

  1. Terseness β€” Fewer keystrokes for common operations

  2. DWIM β€” Do What I Mean; intuitive defaults

  3. TIMTOWTDI β€” Multiple valid ways to express the same thing

  4. Big Tent β€” Aliases for CSS/frontend developers, Ratatui natives, and Ruby purists

  5. Two Levels Max β€” tui.thing and tui.scope_thing, never tui.scope.thing, never tui.ascope_bscope_thing

Breaking Changes: Pre-1.0 with few external users. Rename aggressively for DX. Document in CHANGELOG.


Method Naming Recommendations
Pattern: Base Methods + Aliases + Dispatchers

Base methods align with Ratatui’s module API names. Aliases provide ergonomic shortcuts.

  1. Base method (Ratatui-aligned): tui.shape_circle(...) β€” matches Widgets::Shape::Circle

  2. Aliases (ergonomic): tui.circle(...), tui.circle_shape(...)

  3. Dispatcher: tui.shape(:circle, ...) β€” errors helpfully on missing type

This pattern applies to: shapes, constraints, text elements.


Canvas Shapes
Base Method Add Aliases Dispatcher
shape_circle circle, circle_shape shape(:circle, ...)
shape_line line_shape (not bare line β€” conflicts with Text::Line) shape(:line, ...)
shape_point point, point_shape shape(:point, ...)
shape_rectangle rectangle, rectangle_shape shape(:rectangle, ...)
shape_map map, map_shape shape(:map, ...)
shape_label label, label_shape shape(:label, ...)

Dispatcher signature:

def shape(type, **kwargs)
  case type
  when :circle then shape_circle(**kwargs)
  when :line then shape_line(**kwargs)
  # ...
  else
    raise ArgumentError, "Unknown shape type: #{type.inspect}. " \
      "Valid types: :circle, :line, :point, :rectangle, :map, :label"
  end
end

Layout Constraints

Ratatui’s constraints map well to CSS layout concepts. Offer aliases for both communities:

Base Method Add Aliases (CSS-Friendly) Dispatcher
constraint_length(n) fixed(n), length(n) constraint(:length, n)
constraint_percentage(n) percent(n), percentage(n) constraint(:percentage, n)
constraint_min(n) min(n), min_content(n) constraint(:min, n)
constraint_max(n) max(n), max_content(n) constraint(:max, n)
constraint_fill(n) fill(n), flex(n), fr(n) constraint(:fill, n)
constraint_ratio(a,b) ratio(a,b), aspect(a,b) constraint(:ratio, a, b)

CSS Flexbox/Grid parallels: - fill(1) β‰ˆ CSS flex: 1 or 1fr - fixed(100) β‰ˆ CSS width: 100px - min(50) β‰ˆ CSS min-width: 50px - percent(25) β‰ˆ CSS width: 25%

Dispatcher signature:

def constraint(type, *args)
  case type
  when :length, :fixed then constraint_length(*args)
  when :percentage, :percent then constraint_percentage(*args)
  when :min then constraint_min(*args)
  when :max then constraint_max(*args)
  when :fill, :flex, :fr then constraint_fill(*args)
  when :ratio, :aspect then constraint_ratio(*args)
  else
    raise ArgumentError, "Unknown constraint type: #{type.inspect}. " \
      "Valid types: :length, :percentage, :min, :max, :fill, :ratio"
  end
end

Layout Operations
Current Add Alias Rationale
layout_split split 52 usages in examples; clear in context

Text Factories
Current Status Notes
text_span βœ“ Has alias span
text_line βœ“ Has alias line
text_width Keep as-is Distinct from length (constraint)

Add dispatcher:

def text(type, **kwargs)
  case type
  when :span then text_span(**kwargs)
  when :line then text_line(**kwargs)
  else
    raise ArgumentError, "Unknown text type: #{type.inspect}. Valid types: :span, :line"
  end
end

Widget Factories
Current Add Alias Rationale
list_item item Clear in list context
table_row Keep alongside row DWIM β€” both valid mental models
table_cell Keep as-is cell means Buffer::Cell
bar_chart_bar Keep alongside bar DWIM β€” don’t deprecate
bar_chart_bar_group Keep alongside bar_group DWIM β€” don’t deprecate

Add dispatcher:

def widget(type, **kwargs)
  case type
  when :block then block(**kwargs)
  when :paragraph then paragraph(**kwargs)
  when :list then list(**kwargs)
  when :table then table(**kwargs)
  # ... all widgets
  else
    raise ArgumentError, "Unknown widget type: #{type.inspect}."
  end
end

State Factories
Current Status
list_state Keep as-is
table_state Keep as-is
scrollbar_state Keep as-is

Add dispatcher:

def state(type, **kwargs)
  case type
  when :list then list_state(**kwargs)
  when :table then table_state(**kwargs)
  when :scrollbar then scrollbar_state(**kwargs)
  else
    raise ArgumentError, "Unknown state type: #{type.inspect}."
  end
end

Parameter Names

All current parameter names are well-chosen. No changes recommended.

Widget Parameter Status
List selected_index, highlight_style, etc. βœ“
Table row_highlight_style, selected_row, etc. βœ“
Scrollbar content_length, position, etc. βœ“
All block, style, offset βœ“ Consistent

Summary of Changes
High Priority (Immediate DX Wins)
  1. Add item alias for list_item

  2. Add terse shape aliases: circle, point, rectangle, map, label

Medium Priority (Pattern Completion)
  1. Add dispatcher methods: shape(type, ...), constraint(type, ...), text(type, ...), widget(type, ...), state(type, ...)

  2. Add bidirectional shape aliases: circle_shape, point_shape, etc.

Not Changing

Implementation Checklist

Breaking Changes Analysis

If all recommendations in this audit are adopted, none constitute breaking changes under semver.

Recommendation Breaking? Rationale
Add item alias No Additive; list_item unchanged
Add terse shape aliases (circle, etc.) No Additive; shape_* methods unchanged
Add bidirectional aliases (*_shape) No Additive; does not remove existing forms
Add dispatcher methods No Additive; new methods only
Keep verbose forms (table_row, etc.) No No removal or rename

Conclusion: This audit recommends only additive changes. All existing code will continue to work unchanged.

[!NOTE] If we later decide to remove verbose forms like bar_chart_bar or bar_chart_bar_group, that would be a breaking change requiring a major version bump. This audit explicitly recommends keeping them (DWIM philosophy).


dwim_dx

dwim_dx

Problem Statement

Ruby’s philosophy of β€œDo What I Mean” (DWIM) and human-centric design should extend to ratatui_ruby’s API. Currently, app developers encounter friction points that force them to remember non-obvious conventions, use overly verbose code, or pattern-match when simple predicates would suffice.

This proposal identifies DX issues across the widget API and suggests improvements that maintain backward compatibility while providing ergonomic alternatives.

DX Issues Identified

1. Confusing Event Method Names

Current problem: event.char doesn’t exist, but event.code returns things like "enter", "ctrl", not just characters.

What users expect: - event.char should return the printable character (matching the name) - event.ctrl_c?, event.enter?, etc. should work for all key combinations - event.key?, event.mouse? predicates exist but only for broad categories

Solution implemented: Added char method and dynamic predicates via method_missing. See lib/ratatui_ruby/event/key.rb.

2. Dual Parameter APIs Without Predicates

Current problem: Widgets accept both forms but no convenience methods to query the state:

### Both work, but which one does the widget store?
gauge1 = Gauge.new(ratio: 0.75)
gauge2 = Gauge.new(percent: 75)
gauge1.ratio  # Works
gauge1.percent  # Does NOT exist

Similarly with List and Table:

list.selected_index = 2  # Works
list.selected?  # Does NOT exist
list.is_selected?  # Does NOT exist

Affected widgets: - Gauge (ratio vs percent) - LineGauge (ratio vs percent) - List (selected_index with no query methods) - Table (selected_row and selected_column with no query methods)

Suggested solutions:

For Gauge and LineGauge:

### Add convenience predicates
gauge.percent  # => 75 (coerced from ratio internally)
gauge.percent = 50  # => Updates ratio to 0.5

### Or provide explicit accessors
gauge.as_percent  # => 75
gauge.as_ratio   # => 0.75

For List and Table:

list.selected?  # => true if selected_index is not nil
list.selection  # => 2 (alias for selected_index)
list.selected_item  # => "Item 3"

table.selected_row?  # => true if selected_row is not nil
table.selected_cell?  # => true if both row and column selected
4. Inconsistent Style APIs

Current problem: Different widgets accept styles differently:

### Table accepts both
table = Table.new(style: Style.new(fg: :blue))
table = Table.new(style: { fg: :blue })  # Hash shorthand

### But Paragraph doesn't
paragraph = Paragraph.new(text: "hi", style: Style.new(fg: :blue))
paragraph = Paragraph.new(text: "hi", style: { fg: :blue })  # Works but undocumented

### And Gauge has separate properties
gauge = Gauge.new(style: Style.new(fg: :blue), gauge_style: Style.new(fg: :green))

Suggested solution: Standardize style handling across all widgets:

  1. All widgets should accept Style objects and Hash shorthand

  2. Document this clearly in each widget

  3. Add a convenience constructor:

class Style
  def self.with(fg: nil, bg: nil, modifiers: [])
    Style.new(fg: fg, bg: bg, modifiers: modifiers)
  end
end

### Cleaner than always spelling out keyword args
paragraph = Paragraph.new(text: "hi", style: Style.with(fg: :blue))
6. Magic Numeric Coercions

Current problem: Widgets accept Numeric but silently coerce:

### These all work, but behavior is undocumented
list = List.new(selected_index: "2")    # Coerced to 2
list = List.new(selected_index: 2.7)    # Coerced to 2
list = List.new(selected_index: 2.0)    # Coerced to 2

gauge = Gauge.new(percent: 150)  # Should clamp?
gauge = Gauge.new(ratio: 1.5)    # Should clamp?

Suggested solution:

  1. Document coercion rules explicitly in RDoc

  2. Add validation and raise on invalid inputs:

def initialize(percent: nil, ...)
  if percent
    raise ArgumentError, "percent must be 0..100, got #{percent}" unless percent.between?(0, 100)
    ratio = Float(percent) / 100.0
  end
end
  1. Provide clear error messages:

    gauge = Gauge.new(percent: 150)
    ### => ArgumentError: percent must be between 0 and 100 (got 150)
    

Implementation Strategy

Phase 3: Style Consistency
Phase 4: Numeric Coercion Validation
Phase 5: Convenience Accessors

Example: Before and After

Before (Confusing)
class GameApp
  def initialize
    @menu = List.new(
      items: ["Start Game", "Load Game", "Options", "Quit"],
      selected_index: 0,
      highlight_spacing: :when_selected,  # What's valid here?
      direction: :top_to_bottom
    )
  end

  def handle_input(event)
    case event
    when :ctrl_c
      exit
    when :up
      if @menu.selected_index && @menu.selected_index > 0
        @menu = @menu.with(selected_index: @menu.selected_index - 1)
      end
    end
  end

  def render(tui)
    tui.draw(@menu)
  end
end
After (DWIM)
class GameApp
  def initialize
    @menu = List.new(
      items: ["Start Game", "Load Game", "Options", "Quit"],
      selected_index: 0,
      highlight_spacing: List::HIGHLIGHT_WHEN_SELECTED,  # IDE autocomplete!
      direction: List::DIRECTION_TOP_TO_BOTTOM
    )
  end

  def handle_input(event)
    return if event.ctrl_c?  # Dynamic predicate!
    
    if event.up?
      move_menu_up if @menu.selected?  # State predicate!
    end
  end

  def move_menu_up
    index = @menu.selected_index
    return if index == 0
    @menu = @menu.with(selected_index: index - 1)
  end

  def render(tui)
    tui.draw(@menu)
  end
end

Migration Path

All changes are backward compatible (additive): - Existing code using symbols continues to work - New constants coexist with symbols - New predicates don’t change existing behavior - New methods are additions, not replacements

Apps can migrate at their own pace:

### Old style still works
list = List.new(highlight_spacing: :when_selected)

### New style also works
list = List.new(highlight_spacing: List::HIGHLIGHT_WHEN_SELECTED)

### Mix and match
if list.selected?  # New predicate
  puts list.selected_index  # Old accessor
end

Metrics for Success

  1. Discoverability: New developers can find valid options via IDE autocomplete

  2. Clarity: Code self-documents valid states and modes

  3. Type safety: Constants and predicates provide type checking

  4. Error feedback: Invalid inputs raise with helpful messages

  5. Backward compatibility: Zero breaking changes, all existing code works

Related Issues


examples_audit

examples_audit

P1: Moderate (Quality)
  1. {Add RDoc Cross-Links}[#1-add-rdoc-cross-links-examples–aliases] (Documentation discoverability)

  2. Link library classes/methods to examples

  3. Link DWIM/TIMTOWTDI aliases

  4. Create consistent pattern across public APIs


p2_moderate

Priority 2: Moderate (Quality Gates)

These are v1.0.0 quality improvements that refine the example suite after P0 is complete. Not blocking, but recommended for maintainability and API consistency.


1. Add RDoc Cross-Links (Examples & Aliases)

Status: Important for API discoverability β€” Documentation should link library and examples

RDoc should cross-link between: - Library classes/methods ↔ Examples that use them (See also: examples/widget_foo) - Primary methods ↔ DWIM/TIMTOWTDI aliases (See also: tui.foo_bar as alias for tui.foo(:bar))

Current Practice

Done for: - RatatuiRuby::Frame#set_cursor_position ↔ RatatuiRuby::Cursor (cross-linking) - Limited elsewhere

Gaps
Action
  1. Add # See also: examples/widget_foo/app.rb to class/method RDoc

  2. Link DWIM methods to TIMTOWTDI variants: # Also available as: tui.constraint_length (DWIM) vs tui.constraint(:length) (TIMTOWTDI)

  3. Create consistent pattern across all public APIs in lib/ratatui_ruby/

Example Pattern
#### Renders text with styling.
#
#### See also: examples/widget_paragraph/app.rb (basic paragraph rendering)
class Paragraph < Data.define(...)
  # ...
end

#### DWIM version of constraint creation
#### Also available as: constraint(type, value) for explicit control
def constraint_length(length)
  constraint(:length, length)
end