A do-node and corresponding return-node for creating loops and task-lists.
LLM eval & testing toolkit
Ensure values are ordered consistently in your CSS.
Discard empty rules and values with PostCSS.
Ensure CSS selectors are unique.
Create a Red Hat package for your Electron app.
Runs prettier as an stylelint rule
A fully persistent balanced binary search tree
Modify colors using the color-mod() function in CSS
Merge Tailwind CSS classes without style conflicts
Node-RED nodes for the FlexDash dashboard
Colors, formatting and other tools for the console
A tiny utility to colorize stdin/stdout
This module provides basic ANSI color code support, to allow you to format your console output with foreground and background colors as well as providing bold, italic and underline support.
A list of color names and its values
Colors of Google's Material Design made available to coders
The color red, in ansi.
FlowFuse Dashboard - A collection of Node-RED nodes that provide functionality to build your own UI applications (inc. forms, buttons, charts).
Convert HEX color to RGBA
The fastest Node.js library for formatting terminal text with ANSI colors~!
Correctly declare progressive enhancements for CSS Custom Properties.
TypeScript definitions for katex
Node-RED nodes to send and receive simple emails.
Detect Browser, Engine, OS, CPU, and Device type/model from User-Agent & Client Hints data. Supports browser & node.js environment
Enhanced version of std::collections::BinaryHeap that supports max, min, and custom-order heaps. Makes some previously internal function public
An implementation of an undo/redo system, inspired by Godot Engine's UndoRedo object.
Undo/redo system for state management
This is a heavily modified fork of http://github.com/defunkt/colored gem, with many sensible pull requests combined. Since the authors of the original gem no longer support it, this might, perhaps, be considered a good alternative. Simple gem that adds various color methods to String class, and can be used as follows: require 'colored2' puts 'this is red'.red puts 'this is red with a yellow background'.red.on.yellow puts 'this is red with and italic'.red.italic puts 'this is green bold'.green.bold << ' and regular'.green puts 'this is really bold blue on white but reversed'.bold.blue.on.white.reversed puts 'this is regular, but '.red! << 'this is red '.yellow! << ' and yellow.'.no_color! puts ('this is regular, but '.red! do 'this is red '.yellow! do ' and yellow.'.no_color! end end)
This is a pure-ruby port of Takuma Ozawa's RBTree. (it has an identical interface and uses the identical unit tests from his version 0.3.0, however it is *not* a Red-Black tree.) It's intended for doing lookups and not for doing lots of insertions or deletions. Please see RBTree docs for a sense of how this is supposed to be used. (This one runs the unit tests about 15% slower than Ozawa's C-version, and 80% less lines of code ;)
# Fancy Logger An easily customizable logger with style. ## Install ### Bundler: `gem 'fancy_logger'` ### RubyGems: `gem install fancy_logger` ## Usage Simply use as if you were using the normal Ruby `Logger` class: ```ruby require 'fancy_logger' logger = FancyLogger.new(STDOUT) logger.info "Hello" ``` ### Config The `config` instance method allows you to modify the configuration of the Logger within a DSL. Continuing with our last example: ```ruby logger.config do timestamp_format "%c" styles do info do foreground :yellow blink true end end end logger.debug 'Look here!' logger.info 'Doing things...' logger.warn 'Watch out!' logger.error 'Bad' logger.fatal 'VERY bad' logger.unknown 'Weird unknown stuff' ``` #### Output ![][output_example] ### Config ```ruby # The format of the timestamp in the log. Follows the strftime standards. timestamp_format "%F %r" # On the first logged message, FancyLogger will prepend a help message # containing a list of all the severities (debug, info, warn, etc) styled # according to your config as reference. # You can disable this by setting the below option to false. show_help_message true # Under styles, you have a configuration for each severity. # Each severity has a configuration with the following valid options: # Key: foreground # Value: # :default, :black, :red, :green, :yellow, :blue, :magenta, :cyan, :white # # Key: background # Value: # :default, :black, :red, :green, :yellow, :blue, :magenta, :cyan, :white # # Key: reset # Value: true or false # # Key: bright # Value: true or false # # Key: italic # Value: true or false # # Key: underline # Value: true or false # # Key: # blink # Value: true or false # # Key: inverse # Value: true or false # # Key: hide # Value: true or false styles do debug do foreground :black background :cyan end info do foreground :default background :default end warn do foreground :yellow background :default blink true end error do foreground :red background :default end fatal do foreground :black background :red bold true underline true end unknown do foreground :black background :white underline true end end ``` ## Contributing * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it * Fork the project * Start or switch to a testing/unstable/feature/bugfix branch * Commit and push until you are happy with your contribution * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. * Please try not to mess with the Rakefile, VERSION or gemspec. ## Copyright Copyright © 2012 Ryan Scott Lewis <ryan@rynet.us>. The MIT License (MIT) - See LICENSE for further details. [output_example]: http://oi44.tinypic.com/sfwlkp.jpg
== Terminal UIs, the Ruby Way RatatuiRuby[https://rubygems.org/gems/ratatui_ruby] is a RubyGem built on Ratatui[https://ratatui.rs], a leading TUI library written in Rust[https://rust-lang.org]. You get native performance with the joy of Ruby. gem install ratatui_ruby {rdoc-image:https://ratatui-ruby.dev/hero.gif}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_cli_rich_moments/README_md.html] === Rich Moments Add a spinner, a progress bar, or an inline menu to your CLI script. No full-screen takeover. Your terminal history stays intact. ==== Inline Viewports Standard TUIs erase themselves on exit. Your carefully formatted CLI output disappears. Users lose their scrollback. <b>Inline viewports</b> solve this. They occupy a fixed number of lines, render rich UI, then leave the output in place when done. Perfect for spinners, menus, progress indicators—any brief moment of richness. require "ratatui_ruby" RatatuiRuby.run(viewport: :inline, height: 1) do |tui| until connected? status = tui.paragraph(text: "\#{spin} Connecting...") tui.draw { |frame| frame.render_widget(status, frame.area) } end end === Build Something Real Full-screen applications with {keyboard and mouse input}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_all_events/README_md.html]. The managed loop sets up the terminal and restores it on exit, even after crashes. RatatuiRuby.run do |tui| loop do tui.draw do |frame| frame.render_widget( tui.paragraph(text: "Hello, RatatuiRuby!", alignment: :center), frame.area ) end case tui.poll_event in { type: :key, code: "q" } then break else nil end end end ==== Widgets included: [Layout] {Block}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_block/README_md.html], {Center}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_center/README_md.html], {Clear (Popup, Modal)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_popup/README_md.html], {Layout (Split, Grid)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_layout_split/README_md.html], {Overlay}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_overlay/README_md.html] [Data] {Bar Chart}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_barchart/README_md.html], {Chart}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_chart/README_md.html], {Gauge}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_gauge/README_md.html], {Line Gauge}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_line_gauge/README_md.html], {Sparkline}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_sparkline/README_md.html], {Table}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_table/README_md.html] [Text] {Cell}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_cell/README_md.html], {List}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_list/README_md.html], {Rich Text (Line, Span)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_rich_text/README_md.html], {Scrollbar (Scroll)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_scrollbar/README_md.html], {Tabs}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_tabs/README_md.html] [Graphics] {Calendar}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_calendar/README_md.html], {Canvas}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_canvas/README_md.html], {Map (World Map)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_map/README_md.html] Need something else? {Build custom widgets}[https://www.ratatui-ruby.dev/docs/v0.10/doc/concepts/custom_widgets_md.html] in Ruby! --- === Testing Built In TUI testing is tedious. You need a headless terminal, event injection, snapshot comparisons, and style assertions. RatatuiRuby bundles all of it. require "ratatui_ruby/test_helper" class TestColorPicker < Minitest::Test include RatatuiRuby::TestHelper def test_swatch_widget with_test_terminal(10, 3) do RatatuiRuby.draw do |frame| frame.render_widget(Swatch.new(:red), frame.area) end assert_cell_style 2, 1, char: "█", bg: :red end end end ==== What's inside: - <b>Headless terminal</b> — No real TTY needed - <b>Snapshots</b> — Plain text and rich (ANSI colors) - <b>Event injection</b> — Keys, mouse, paste, resize - <b>Style assertions</b> — Color, bold, underline at any cell - <b>Test doubles</b> — Mock frames and stub rects - <b>UPDATE_SNAPSHOTS=1</b> — Regenerate baselines in one command --- ==== Inline Menu Example require "ratatui_ruby" # This example renders an inline menu. Arrow keys select, enter confirms. # The menu appears in-place, preserving scrollback. When the user chooses, # the TUI closes and the script continues with the selected value. class RadioMenu CHOICES = ["Production", "Staging", "Development"] # ASCII strings are universally supported. PREFIXES = { active: "●", inactive: "○" } # Some terminals may not support Unicode. CONTROLS = "↑/↓: Select | Enter: Choose | Ctrl+C: Cancel" # Let users know what keys you handle. TITLES = ["Select Environment", # The default title position is top left. { content: CONTROLS, # Multiple titles can save space. position: :bottom, # Titles go on the top or bottom, alignment: :right }] # aligned left, right, or center def call # This method blocks until a choice is made. RatatuiRuby.run(viewport: :inline, height: 5) do |tui| # RatauiRuby.run manages the terminal. @tui = tui # The TUI instance is safe to store. show_menu until chosen? # You can use any loop keyword you like. end # `run` won't return until your block does, RadioMenu::CHOICES[@choice] # so you can use it synchronously. end # Classes like RadioMenu are convenient for private # CLI authors to offer "rich moments." def show_menu = @tui.draw do |frame| # RatatuiRuby gives you low-level access. widget = @tui.paragraph( # But the TUI facade makes it easy to use. text: menu_items, # Text can be spans, lines, or paragraphs. block: @tui.block(borders: :all, titles: TITLES) # Blocks give you boxes and titles, and hold ) # one or more widgets. We only use one here, frame.render_widget(widget, frame.area) # but "area" lets you compose sub-views. end def chosen? # You are responsible for handling input. interaction = @tui.poll_event # Every frame, you receive an event object: return choose if interaction.enter? # Key, Mouse, Resize, Paste, FocusGained, # FocusLost, or None objects. They come with move_by(-1) if interaction.up? # predicates, support pattern matching, and move_by(1) if interaction.down? # can be inspected for properties directly. quit! if interaction.ctrl_c? # Your application must handle every input, false # even interrupts and other exit patterns. end def choose # Here, the loop is about to exit, and the prepare_next_line # block will return. The inline viewport @choice # will be torn down and the terminal will end # be restored, but you are responsible for # positioning the cursor. def prepare_next_line # To ensure the next output is on a new area = @tui.viewport_area # line, query the viewport area and move RatatuiRuby.cursor_position = [0, area.y + area.height] # the cursor to the start of the last line. puts # Then print a newline. end def quit! # All of your familiar Ruby control flow prepare_next_line # keywords work as expected, so we can exit 0 # use them to leave the TUI. end def move_by(line_count) # You are in full control of your UX, so @choice = (@choice + line_count) % CHOICES.size # you can implement any logic you need: end # Would you "wrap around" here, or not? # def menu_items = CHOICES.map.with_index do |choice, i| # Notably, RatatuiRuby has no concept of "\#{prefix_for(i)} \#{choice}" # "menus" or "radio buttons". You are in end # full control, but it also means you must def prefix_for(choice_index) # implement the logic yourself. For larger return PREFIXES[:active] if choice_index == @choice # applications, consider using Rooibos, PREFIXES[:inactive] # an MVU framework built with RatatuiRuby. end # Or, use the upcoming ratatui-ruby-kit, # our object-oriented component library. def initialize = @choice = 0 # However, those are both optional, and end # designed for full-screen Terminal UIs. # RatatuiRuby will always give you the most choice = RadioMenu.new.call # control, and is enough for "rich CLI puts "You chose \#{choice}!" # moments" like this one. --- === Full App Solutions RatatuiRuby renders. For complex applications, add a framework that manages state and composition. ==== Rooibos[https://www.rooibos.run] (Framework) Model-View-Update architecture. Inspired by Elm, Bubble Tea, and React + Redux. Your UI is a pure function of state. - Functional programming with MVU - Commands work off the main thread - Messages, not callbacks, drive updates ==== {Kit}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-3-the-object-path--kit] (Coming Soon) Component-based architecture. Encapsulate state, input handling, and rendering in reusable pieces. - OOP with stateful components - Separate UI state from domain logic - Built-in focus management & click handling Both use the same widget library and rendering engine. Pick the paradigm that fits your brain. --- === Why RatatuiRuby? Ruby deserves world-class terminal user interfaces. TUI developers deserve a world-class language. RatatuiRuby wraps Rust's Ratatui via native extension. The Rust library handles rendering. Your Ruby code handles design. >>> "Text UIs are seeing a renaissance with many new TUI libraries popping up. The Ratatui bindings have proven to be full featured and stable." — {Mike Perham}[https://www.mikeperham.com/], creator of Sidekiq[https://sidekiq.org/] and Faktory[https://contribsys.com/faktory/] ==== Why Rust? Why Ruby? Rust excels at low-level rendering. Ruby excels at expressing domain logic and UI. RatatuiRuby puts each language where it performs best. ==== Versus CharmRuby CharmRuby[https://charm-ruby.dev/] wraps Charm's Go libraries. Both projects give Ruby developers TUI options. [Integration] CharmRuby: Two runtimes, one process. RatatuiRuby: Native extension in Rust. [Runtime] CharmRuby: Go + Ruby (competing). RatatuiRuby: Ruby (Rust has no runtime). [Memory] CharmRuby: Two uncoordinated GCs. RatatuiRuby: One Garbage Collector. [Style] CharmRuby: The Elm Architecture (TEA). RatatuiRuby: TEA, OOP, or Imperative. --- === Links [Get Started] {Quickstart}[https://www.ratatui-ruby.dev/docs/v0.10/doc/getting_started/quickstart_md.html], {Examples}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_cli_rich_moments/README_md.html], {API Reference}[https://www.ratatui-ruby.dev/docs/v0.10/], {Guides}[https://www.ratatui-ruby.dev/docs/v0.10/doc/index_md.html] [Ecosystem] Rooibos[https://www.rooibos.run], {Kit}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-3-the-object-path--kit] (Planned), {Framework}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-5-the-framework] (Planned), {UI Widgets}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-6-licensing] (Planned) [Community] {Forum}[https://forum.setdef.com/c/ratatui-ruby/6], {Announcements}[https://forum.setdef.com/tags/c/ratatui-ruby/6/announcement], {Discussion}[https://forum.setdef.com/tags/c/ratatui-ruby/6/discussion], {Bug Tracker}[https://forum.setdef.com/tags/c/ratatui-ruby/6/bug] [Contribute] {Contributing Guide}[https://man.sr.ht/~kerrick/ratatui_ruby/contributing.md], {Code of Conduct}[https://man.sr.ht/~kerrick/ratatui_ruby/code_of_conduct.md], {Project History}[https://man.sr.ht/~kerrick/ratatui_ruby/history/index.md], {Pull Requests}[https://forum.setdef.com/tags/c/ratatui-ruby/6/patch] --- [Website] https://www.ratatui-ruby.dev [Source] https://github.com/setdef/RatatuiRuby [RubyGems] https://rubygems.org/gems/ratatui_ruby [Upstream] https://ratatui.rs [Build Status] https://builds.sr.ht/~kerrick/ratatui_ruby © 2026 Kerrick Long · Library: LGPL-3.0-or-later · Website: CC-BY-NC-ND-4.0 · Snippets: MIT-0
Crowdfund is a Ruby program developed based on Pragmatic Studio's Ruby Programming hands-on video course, and distributed as a Ruby gem. This program has been developed using all the strengths of Ruby including the following. Ruby Programming Environment * Installing Ruby on your favorite operating system (free exercise) * Running Ruby using the interactive Ruby shell (irb) and writing Ruby program files * Using Ruby's documentation system to get help * Installing external Ruby libraries using RubyGems * Troubleshooting common problems Ruby Language Constructs * Expressions and variables * Numbers, string, and symbols (free video & exercise) * Loops and conditional expressions * Arrays and hashes (free video & exercise on hashes) * Classes, modules, and structs Object-Oriented Programming * Using built-in Ruby classes * Defining your own classes with state and behavior (free video & exercise) * Creating unique objects * Telling objects what to do by calling methods * Modeling class-level inheritance relationships * Sharing code with mixins Object-Oriented Design Principles * Encapsulation * Separation of concerns * Polymorphism * Don't Repeat Yourself * Tell, Don't Ask Blocks and Iterators * Calling built-in methods that take blocks * Writing your own methods that yield to blocks * Implementing custom iterators * Effectively using blocks in your programs Organizing Ruby Code * Creating a Ruby project structure * Separating source files for easier reuse and testing * Namespacing to avoid naming clashes * Input/Output * Reading data from files * Writing data to files * Creating an interactive console prompt * Handling command-line input Unit Testing * Writing and running unit tests with RSpec * Test-driven development and the red-green-refactor cycle * Stubbing methods to control tests * Refactoring code, safely! Distribution * Conforming to RubyGems conventions * Writing a GemSpec * Building a RubyGem * Publishing a RubyGem to a public server Ruby Programming Idioms
Studio Game is a Ruby program developed based on Pragmatic Studio' Ruby Programming hands-on video course, and distributed as a Ruby gem. This program has been developed using all the strengths of Ruby including the following. Ruby Programming Environment * Installing Ruby on your favorite operating system (free exercise) * Running Ruby using the interactive Ruby shell (irb) and writing Ruby program files * Using Ruby's documentation system to get help * Installing external Ruby libraries using RubyGems * Troubleshooting common problems Ruby Language Constructs * Expressions and variables * Numbers, string, and symbols (free video & exercise) * Loops and conditional expressions * Arrays and hashes (free video & exercise on hashes) * Classes, modules, and structs Object-Oriented Programming * Using built-in Ruby classes * Defining your own classes with state and behavior (free video & exercise) * Creating unique objects * Telling objects what to do by calling methods * Modeling class-level inheritance relationships * Sharing code with mixins Object-Oriented Design Principles * Encapsulation * Separation of concerns * Polymorphism * Don't Repeat Yourself * Tell, Don't Ask Blocks and Iterators * Calling built-in methods that take blocks * Writing your own methods that yield to blocks * Implementing custom iterators * Effectively using blocks in your programs Organizing Ruby Code * Creating a Ruby project structure * Separating source files for easier reuse and testing * Namespacing to avoid naming clashes * Input/Output * Reading data from files * Writing data to files * Creating an interactive console prompt * Handling command-line input Unit Testing * Writing and running unit tests with RSpec * Test-driven development and the red-green-refactor cycle * Stubbing methods to control tests * Refactoring code, safely! Distribution * Conforming to RubyGems conventions * Writing a GemSpec * Building a RubyGem * Publishing a RubyGem to a public server Ruby Programming Idioms
== Confidently Build Terminal Apps Rooibos[https://rooibos.run] helps you build interactive terminal applications. Keep your code understandable and testable as it scales. Rooibos handles keyboard, mouse, and async work so you can focus on behavior and user experience. gem install rooibos <i>Currently in beta. APIs may change before 1.0.</i> === Get Started in Seconds rooibos new my_app cd my_app rooibos run That's it. You have a working app with keyboard navigation, mouse support, and clickable buttons. Open <tt>lib/my_app.rb</tt> to make it your own. --- === The Pattern \Rooibos uses Model-View-Update, the architecture behind Elm[https://guide.elm-lang.org/architecture/], Redux[https://redux.js.org/], and {Bubble Tea}[https://github.com/charmbracelet/bubbletea]. State lives in one place. Updates flow in one direction. The runtime handles rendering and runs background work for you. --- === Hello, MVU The simplest \Rooibos app. Press any key to increment the counter. Press <tt>Ctrl</tt>+<tt>C</tt> to quit. require "rooibos" module Counter # Init: How do you create the initial model? Init = -> { 0 } # View: What does the user see? View = -> (model, tui) { tui.paragraph(text: <<~END) } Current count: #{model}. Press any key to increment. Press Ctrl+C to quit. END # Update: What happens when things change? Update = -> (message, model) { if message.ctrl_c? Rooibos::Command.exit elsif message.key? model + 1 end } end Rooibos.run(Counter) That's the whole pattern: Model holds state, Init creates it, View renders it, and Update changes it. The runtime handles everything else. --- === Your First Real Application A file browser in sixty lines. It opens files, navigates directories, handles errors, styles directories and hidden files differently, and supports vim-style keyboard shortcuts. If you can do this much with this little code, imagine how easy _your_ app will be to build. require "rooibos" module FileBrowser # Model: What state does your app need? Model = Data.define(:path, :entries, :selected, :error) Init = -> { path = Dir.pwd entries = Entries[path] Ractor.make_shareable( # Ensures thread safety Model.new(path:, entries:, selected: entries.first, error: nil)) } View = -> (model, tui) { tui.block( titles: [model.error || model.path, { content: KEYS, position: :bottom, alignment: :right}], borders: [:all], border_style: if model.error then tui.style(fg: :red) else nil end, children: [tui.list(items: model.entries.map(&ListItem[model, tui]), selected_index: model.entries.index(model.selected), highlight_symbol: "", highlight_style: tui.style(modifiers: [:reversed]))] ) } Update = -> (message, model) { return model.with(error: ERROR) if message.error? model = model.with(error: nil) if model.error && message.key? if message.ctrl_c? || message.q? then Rooibos::Command.exit elsif message.home? || message.g? then model.with(selected: model.entries.first) elsif message.end? || message.G? then model.with(selected: model.entries.last) elsif message.up_arrow? || message.k? then Select[:-, model] elsif message.down_arrow? || message.j? then Select[:+, model] elsif message.enter? then Open[model] elsif message.escape? then Navigate[File.dirname(model.path), model] end } private # Lines below this are implementation details KEYS = "↑/↓/Home/End: Select | Enter: Open | Esc: Navigate Up | q: Quit" ERROR = "Sorry, opening the selected file failed." ListItem = -> (model, tui) { -> (name) { modifiers = name.start_with?(".") ? [:dim] : [] fg = :blue if name.end_with?("/") tui.list_item(content: name, style: tui.style(fg:, modifiers:)) } } Select = -> (operator, model) { new_index = model.entries.index(model.selected).public_send(operator, 1) model.with(selected: model.entries[new_index.clamp(0, model.entries.length - 1)]) } Open = -> (model) { full = File.join(model.path, model.selected.delete_suffix("/")) model.selected.end_with?("/") ? Navigate[full, model] : Rooibos::Command.open(full) } Navigate = -> (path, model) { entries = Entries[path] model.with(path:, entries:, selected: entries.first, error: nil) } Entries = -> (path) { Dir.children(path).map { |name| File.directory?(File.join(path, name)) ? "#{name}/" : name }.sort_by { |name| [name.end_with?("/") ? 0 : 1, name.downcase] } } end Rooibos.run(FileBrowser) --- === Batteries Included ==== Commands Applications fetch data, run shell commands, and set timers. \Rooibos Commands run off the main thread and send results back as messages. <b>HTTP requests:</b> Update = -> (message, model) { case message in :fetch_users [model.with(loading: true), Rooibos::Command.http(:get, "/api/users", :got_users)] in { type: :http, envelope: :got_users, status: 200, body: } model.with(loading: false, users: JSON.parse(body)) in { type: :http, envelope: :got_users, status: } model.with(error: "HTTP #{status}") end } <b>Shell commands:</b> Update = -> (message, model) { case message in :list_files Rooibos::Command.system("ls -la", :listed_files) in { type: :system, envelope: :listed_files, stdout:, status: 0 } model.with(files: stdout.lines.map(&:chomp)) in { type: :system, envelope: :listed_files, stderr:, status: } model.with(error: stderr) end } <b>Timers:</b> Update = -> (message, model) { case message in { type: :timer, envelope: :tick, elapsed: } [model.with(frame: model.frame + 1), Rooibos::Command.wait(1.0 / 24, :tick)] end } <b>And more!</b> \Rooibos includes <tt>all</tt>, <tt>batch</tt>, <tt>bubble</tt>, <tt>cancel</tt>, <tt>custom</tt>, <tt>deliver</tt>, <tt>exit</tt>, <tt>http</tt>, <tt>map</tt>, <tt>open</tt>, <tt>system</tt>, <tt>tick</tt>, and <tt>wait</tt> commands. You can also define your own custom commands for complex orchestration. Every command produces a message, and Update handles it the same way. ==== Testing \Rooibos makes TUIs so easy to test, you'll save more time by writing tests than by not testing. <b>Unit test Update, View, and Init.</b> No terminal needed. Test helpers included. def test_moves_selection_down_with_j model = Ractor.make_shareable(FileBrowser::Model.new( path: "/", entries: %w[bin exe lib], selected: "bin", error: nil)) message = RatatuiRuby::Event::Key.new(code: "j") result = FileBrowser::Update.call(message, model) assert_equal "exe", result.selected end <b>Style assertions.</b> Draw to a headless terminal, verify colors and modifiers. def test_directories_are_blue with_test_terminal(60, 10) do model = Ractor.make_shareable(FileBrowser::Model.new( path: "/", entries: %w[file.txt subdir/], selected: "file.txt", error: nil)) widget = FileBrowser::View.call(model, RatatuiRuby::TUI.new) RatatuiRuby.draw { |frame| frame.render_widget(widget, frame.area) } assert_blue(1, 2) # "subdir/" at column 1, row 2 end end <b>System tests.</b> Inject events, run the full app, snapshot the result. def test_selection_moves_down with_test_terminal(120, 30) do Dir.mktmpdir do |dir| FileUtils.touch(File.join(dir, "a")) FileUtils.touch(File.join(dir, "b")) FileUtils.touch(File.join(dir, "c")) inject_key(:down) inject_key(:ctrl_c) # Tests use explicit params to inject deterministic initial state. Rooibos.run( model: Ractor.make_shareable(FileBrowser::Model.new( path: dir, entries: %w[a b c], selected: "a", error: nil)), view: FileBrowser::View, update: FileBrowser::Update ) assert_snapshots("selection_moved_down") do |lines| title = "┌/tmp/test#{'─' * 107}┐" lines.map do |l| l.gsub(/┌#{Regexp.escape(dir)}[^┐]*┐/, title) end end end end end Snapshots record both plain text and ANSI colors. Normalization blocks mask dynamic content (timestamps, temp paths) for cross-platform reproducibility. Run <tt>UPDATE_SNAPSHOTS=1 rake test</tt> to regenerate baselines. ==== Scale Up Large applications decompose into fragments. Each fragment has its own Model, View, Update, and Init. Parents compose children. The pattern scales. The Router DSL eliminates boilerplate: module Dashboard include Rooibos::Router route :stats, to: StatsPanel route :network, to: NetworkPanel receive_events :ctrl_c, -> { Rooibos::Command.exit } only when: -> (_message, model) { !model.modal_open } do receive_events :q, -> { Rooibos::Command.exit } forward_events :s, to: :stats, as: :fetch forward_events :p, to: :network, as: :ping end Update = from_router # ... Model, Init, View below end Declare routes and event handlers. The router generates Update for you. Use guards to ignore messages when needed. ==== CLI The <tt>rooibos</tt> command scaffolds projects and runs applications. rooibos new my_app # Generate project structure rooibos run # Run the app in current directory Generated apps include tests, type signatures, and a working welcome screen with keyboard and mouse support. --- === The Ecosystem \Rooibos builds on RatatuiRuby[https://www.ratatui-ruby.dev], a Rubygem built on Ratatui[https://ratatui.rs]. You get native performance with the joy of Ruby. \Rooibos is one way to manage state and composition. Kit is another. ==== Rooibos[https://www.rooibos.run] Model-View-Update architecture. Inspired by Elm, Bubble Tea, and React + Redux. Your UI is a pure function of state. - Functional programming with MVU - Commands work off the main thread - Messages, not callbacks, drive updates ==== {Kit}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-3-the-object-path--kit] (Coming Soon) Component-based architecture. Encapsulate state, input handling, and rendering in reusable pieces. - OOP with stateful components - Separate UI state from domain logic - Built-in focus management & click handling Both use the same widget library and rendering engine. Pick the paradigm that fits your brain. --- === Links [Get Started] {Getting Started}[https://www.rooibos.run/docs/trunk/doc/getting_started/index_md.html], {Tutorial}[https://www.rooibos.run/docs/trunk/doc/tutorial/index_md.html], {Examples}[https://www.rooibos.run/docs/trunk/examples/app_fractal_dashboard/README_md.html] [Coming From...] {React/Redux}[https://www.rooibos.run/docs/trunk/doc/getting_started/for_react_developers_md.html], {BubbleTea}[https://www.rooibos.run/docs/trunk/doc/getting_started/for_go_developers_md.html], {Textual}[https://www.rooibos.run/docs/trunk/doc/getting_started/for_python_developers_md.html] [Learn More] {Essentials}[https://www.rooibos.run/docs/trunk/doc/essentials/index_md.html], {Scaling Up}[https://www.rooibos.run/docs/trunk/doc/scaling_up/index_md.html], {Best Practices}[https://www.rooibos.run/docs/trunk/doc/best_practices/index_md.html], {Troubleshooting}[https://www.rooibos.run/docs/trunk/doc/troubleshooting/index_md.html] [Community] {Forum}[https://forum.setdef.com/c/rooibos], {Announcements}[https://forum.setdef.com/tags/c/rooibos/announcement], {Bug Tracker}[https://forum.setdef.com/tags/c/rooibos/bug], {Contribution Guide}[https://github.com/setdef/Rooibos/blob/trunk/CONTRIBUTING.md], {Code of Conduct}[https://github.com/setdef/Rooibos/blob/trunk/CODE_OF_CONDUCT.md] --- [Website] https://rooibos.run [Source] https://github.com/setdef/Rooibos [RubyGems] https://rubygems.org/gems/rooibos © 2026 Kerrick Long · Library: LGPL-3.0-or-later · Website: CC-BY-NC-ND-4.0 · Snippets: MIT-0
# Trope **[Documentation][docs] - [Gem][gems] - [Source][source]** Prototyping language that transcompiles into pure Ruby code. 1. Build your concept in Trope. 2. Write specs. 3. Transcompile into Ruby. 4. Destroy Trope files. 5. Red, green, refactor. ## Install > NOTE: Trope is not released yet, the gem is just a placeholder. ### Bundler: `gem 'trope'` ### RubyGems: `gem install trope` ## Example Create `library.trope`: ```ruby object Book attr name <String> -!wd 'Unnamed book' attr isbn <Integer> -w attr library <Library> -w do before write { @library.books.delete(self) unless @library.nil? } after write { @library.books.push(self) unless @library.books.include?(self) } end end object Library attr books <Array> -d Array.new meth add_book do |attributes_or_book <Hash, Book>| book = attributes_or_book.is_a?(Book) ? attributes_or_book : Book.new(attributes_or_book) book.library = self @books << book end end ``` Now generate the Ruby code: ```sh $ trope compile libary.trope ``` Those 15 lines will be transcompiled into the following pure Ruby code in `library.rb`: ```ruby class Book class Error < RuntimeError; end class InvalidAttributesError < Error def to_s 'attributes must be a Hash or respond to #to_h' end end class MissingAttributeError < Error def initialize(attr_name, attr_class) @name, @class = attr_name.to_s, attr_class.to_s end def to_s "attribute '#@name' does not exist for #@class" end end class MissingNameError < Error def to_s 'name cannot be nil' end end class InvalidNameError < Error def to_s 'name must be an instance of String or respond to :to_s' end end class InvalidIsbnError < Error def to_s 'isbn must be an instance of Integer or respond to :to_i' end end class MissingLibraryError < Error def to_s 'library cannot be nil' end end class InvalidLibraryError < Error def to_s 'library must be an instance of Library' end end attr_reader *(@@_attributes = [:name, :isbn, :library]) def initialize(attributes={}) raise InvalidAttributesError unless attributes.is_a?(Hash) || attributes.respond_to?(:to_h) attributes = attributes.to_h unless attributes.is_a?(Hash) raise MissingNameError if attributes.has_key?(:name) && attributes[:name].nil? attributes[:name] = 'Unnamed book' unless attributes.has_key?(:name) attributes.each do |name, value| raise MissingAttributeError.new(name, self.class) unless @@_attributes.include?(name.to_sym) setter_method = "#{name}=" setter_method = "_#{setter_method}" unless self.class.method_defined?(setter_method) send(setter_method, value) end end def name=(value) raise MissingNameError if value.nil? raise InvalidNameError unless value.is_a?(String) || value.respond_to?(:to_s) value = value.to_i unless value.is_a?(Integer) @name = value end def isbn=(value) raise InvalidIsbnError unless value.is_a?(Integer) || value.respond_to?(:to_i) value = value.to_i unless value.is_a?(Integer) @isbn = value end def library=(value) raise InvalidLibraryError unless value.is_a?(Library) || value.nil? @library.books.delete(self) unless @library.nil? @library = value @library.books.push(self) unless @library.books.include?(self) @library end end class Library class Error < RuntimeError; end class InvalidAttributesError < Error def to_s 'attributes must be an instance of Hash or respond to #to_h' end end class MissingAttributeError < Error def initialize(attr_name, attr_class) @name, @class = attr_name.to_s, attr_class.to_s end def to_s "attribute '#@name' does not exist for #@class" end end class InvalidBooksError < Error def to_s 'books must be an instance of Array or respond to #to_a' end end attr_reader *(@@_attributes = [:books]) def initialize(attributes={}) raise InvalidAttributesError unless attributes.is_a?(Hash) || attributes.respond_to?(:to_h) attributes = attributes.to_h unless attributes.is_a?(Hash) attributes[:books] = Array.new unless attributes.has_key?(:books) attributes.each do |name, value| raise MissingAttributeError.new(name, self.class) unless @@_attributes.include?(name.to_sym) setter_method = "#{name}=" setter_method = "_#{setter_method}" unless self.class.method_defined?(setter_method) send(setter_method, value) end end def add_book(attributes_or_book={}) raise InvalidAttributesError unless attributes_or_book.is_a?(Hash) || attributes_or_book.respond_to?(:to_h) || attributes_or_book.is_a?(Book) attributes_or_book = attributes_or_book.to_h unless attributes_or_book.is_a?(Hash) || attributes_or_book.is_a?(Book) book = attributes_or_book.is_a?(Book) ? attributes_or_book : Book.new(attributes_or_book) book.library = self @books << book end protected def _books=(value) raise InvalidBooksError unless value.is_a?(Array) || value.respond_to?(:to_a) value = value.to_a unless value.is_a?(Array) @books = value end end ``` Using the transcompiled Ruby code will produce the expected results: ```ruby p library = Library.new # => #<Library:0x007fc55c0ce418 @books=[]> p library.add_book name: 'Book 1', isbn: 1 # => [#<Book:0x007fc55c0cde78 @name=0, @isbn=1, @library=#<Library:0x007fc55c0ce418 @books=[...]>>] p library # => #<Library:0x007fc55c0ce418 @books=[#<Book:0x007fc55c0cde78 @name=0, @isbn=1, @library=#<Library:0x007fc55c0ce418 ...>>]> p library.books.first # => #<Book:0x007fc55c0cde78 @name=0, @isbn=1, @library=#<Library:0x007fc55c0ce418 @books=[#<Book:0x007fc55c0cde78 ...>]>> p library.books.first.isbn = nil # => nil p library.books.first.name = nil # => Book::MissingNameError: name cannot be nil p library.books.first.library = nil # => Book::MissingLibraryError: library cannot be nil p library.books.first.isbn = ['array'] # => Book::InvalidIsbnError: isbn must be an instance of Integer or respond to :to_i p library = Library.new(books: 123) # => Library::InvalidBooksError: books must be an instance of Array or respond to #to_a ``` ### Breakdown ```ruby object Book attr name <String> -!wd 'Unnamed book' end ``` This says that I have an object `Book` that has an attribute `name` (`attr name`) that must either be an instance/subclass of `String` or be able to convert to an instance of `String` using `#to_s` (`<String>`). It is a required attribute that can never be set to nil (`!`), has a writer method (`w`), and defaults to 'Unnamed book'. The minus sign (`-`) indicates a 'switch' or 'option', must like most *nix command line programs. The example could also have been written like so: ```ruby object Book attr name <String> -! -w -d 'Unnamed book' end ``` The above examples will transcompile into the following: ```ruby class Book class Error < RuntimeError; end class InvalidAttributesError < Error def to_s 'attributes must be a Hash or respond to #to_h' end end class MissingAttributeError < Error def initialize(attr_name, attr_class) @name, @class = attr_name.to_s, attr_class.to_s end def to_s "attribute '#@name' does not exist for #@class" end end class MissingNameError < Error def to_s 'name cannot be nil' end end class InvalidNameError < Error def to_s 'name must be an instance of String or respond to :to_s' end end attr_reader *(@@_attributes = [:name]) @@_required_attributes = [:name] def initialize(attributes={}) raise InvalidAttributesError unless attributes.is_a?(Hash) || attributes.respond_to?(:to_h) attributes = attributes.to_h unless attributes.is_a?(Hash) raise MissingNameError if attributes.has_key?(:name) && attributes[:name].nil? attributes[:name] = 'Unnamed book' unless attributes.has_key?(:name) attributes.each do |name, value| raise MissingAttributeError.new(name, self.class) unless @@_attributes.include?(name.to_sym) setter_method = "#{name}=" setter_method = "_#{setter_method}" unless self.class.method_defined?(setter_method) send(setter_method, value) end end def name=(value) raise MissingNameError if value.nil? raise InvalidNameError unless value.is_a?(String) || value.respond_to?(:to_s) value = value.to_i unless value.is_a?(Integer) @name = value end end ``` ## Contributing * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it * Fork the project * Start a feature/bugfix branch * Commit and push until you are happy with your contribution * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. * Please try not to mess with the Rakefile, VERSION, or Gemfile. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. ## Copyright Copyright © 2012 Ryan Scott Lewis <ryan@rynet.us>. The MIT License (MIT) - See LICENSE for further details. [docs]: http://rubydoc.info/gems/trope/frames [gems]: https://rubygems.org/gems/trope [source]: https://github.com/RyanScottLewis/trope
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.