We do things not because they’re easy, but because we thought they were going to be easy
Gm,
I wanted to share some insights into Beautty. The goal is ambitious yet, I believe, valuable: to create a modern tui framework for Ruby, leveraging the power and familiarity of Flexbox for layout.
But why? Why the terminal? Why ruby?
If you’re familiar with some of my previous work you might have noticed that… I fucking love the terminal (thanks you Hackers), everything is better through either a cli or a tui (porque no los dos?).
On a daily basis I use:
- lazy git if I need to figure out branch stuff
- lazy docker to manage containers
- lazy sql to view development db’s
- spotify tui well it’s self explanatory
- nodekit to manage my Algorand node
If I need to do slides I use slides If I have to create gifs I use vhs
Actually shout out to charm because most of what I’m trying to achieve is their stack but in ruby and hopefully with ergonomics more to my taste.
In short I see the terminal a treasure trove of distraction-free functionality, efficiency and techno-brutalist bliss.
So when I saw this tweet, my reaction was: I want that. After scouring the interwebs I discovered that there isn’t a ruby tui framework, and I want to write ruby. So I mistakingly thought: it’s just drawing pixels on a screen, it shouldn’t be that difficult.

so… yeah, here we are.
Why beautty?
Many existing TUI libraries, while powerful, often rely on older paradigms or abstractions (like the venerable curses
library). My hope for Beautty is to offer a more declarative and intuitive approach, akin to modern web development. Imagine defining complex, responsive terminal layouts using familiar concepts like row
, column
, flex_grow
, justify_content
, and align_items
.
A key technical decision was to bypass curses
entirely and work directly with raw terminal handling. It was proving quite buggy with ghostty (which is my current preferred terminal emulator). This means using direct system calls (io/console
, termios
on Unix-like systems) and ANSI escape codes for everything: positioning the cursor, setting colors, clearing the screen, managing terminal modes (raw/cooked), and capturing input. While this adds complexity, it offers finer control and potentially better performance, avoiding the need for an extra dependency and its specific screen buffer model.
Following the white rabbit
I’ve laid down several foundational pieces, each step getting me closer to seeing the clearer picture:
-
Beautty::Terminal
Module: This is our direct interface to the terminal. It uses raw ANSI escape codes and system calls (io/console
,Termios
) to handle everything: detecting terminal size (winsize
), switching between raw and cooked modes (essential for capturing keypresses immediately without waiting for Enter), saving and restoring the terminal’s state, positioning the cursor, clearing parts of the screen, setting text colors and styles, and reading input (currently character by character). This low-level control is crucial for managing the screen precisely as needed. -
Handling Terminal Size: A crucial aspect is adapting to the terminal window’s dimensions. When the application starts,
Beautty::Terminal
fetches the initial width and height (IO.console.winsize
). This size directly sets thelayout[:width]
andlayout[:height]
of the rootContainer
element. Furthermore, theBeautty::Application
sets up a signal handler (SIGWINCH
) that listens for terminal resize events. When you resize your terminal window, this handler triggers, fetches the new dimensions usingBeautty::Terminal
, and updates the rootContainer
’s layout accordingly. This updated container size then serves as the top-level constraint for theLayoutEngine
during the next layout calculation pass, ensuring the UI reflows to fit the available space. This is the foundational piece for somewhat of aresponsive
terminal layout idea I have. -
Core
Element
Classes: The foundation of the UI is theBeautty::Element
base class. Every visual component inherits from this, forming a tree structure withparent
andchildren
references. Key attributes include astyle
hash (holding developer-defined properties like colors, borders, and Flexbox rules) and alayout
hash (storing the calculatedx
,y
,width
, andheight
relative to the parent element, populated by theLayoutEngine
). I’ve implemented essential elements:Container
: The root of the UI tree, matching the terminal window dimensions.Row
: A layout element defaulting toflex_direction: :row
, arranging its children horizontally.Div
: A general-purpose container for grouping elements, capable of rendering its own border and header text.Text
: A simple element for displaying string content, with basic clipping. This is pretty basic at the moment, I have many elements/widget ideas… but this is the minimum necessary to actually test functionality of the Layout Engine.
-
Beautty::DSLBuilder
Module: To make UI definition intuitive, I’m creating a Domain-Specific Language (DSL). TheDSLBuilder
provides helper methods likediv
,row
, andtext
that allow defining the UI structure using nested Ruby blocks (e.g.,row { text "Label"; text "Value" }
). It manages the parent context during block execution, automatically linking elements into the tree structure as they are declared. At the moment it currently looks like this:
row style: {justify_content: :"space-between"} do div(style: { border: :single, header: "row 2"}) do text("hello world 1") end div(style: { border: :single, header: "row 2"}) do text("hello world 2") end end
The result should eventually resemble an “html with inline css”-like syntax.
-
Beautty::Application
Class: This class orchestrates the entire application lifecycle. It initializes theTerminal
, sets up the initial UI tree (often built using theDSLBuilder
), handles terminal state saving/restoration, and runs the main event loop. The loop listens for input (Terminal.read_input
), handles signals like terminal resize (SIGWINCH
), and triggers layout recalculations and screen rendering when necessary. -
Beautty::LayoutEngine
Module: This is the brain behind the Flexbox implementation. It traverses theElement
(I should/will probably rename this to eitherwidget
orcomponent
) tree, reads thestyle
properties of each element (likeflex_direction
,flex_grow
,border
), and calculates the final position (x
,y
) and size (width
,height
) for every element, storing the results in their respectivelayout
hashes. It currently handles basic:row
and:column
directions and distributes space according toflex_grow
, considering element borders. This component is central to achieving the desired dynamic layout behavior, but also where I face our current challenges. The end goal is to have all the concepts listed in the CSS Tricks Flexbox Layout guide supported.
I really shouldn’t have: The Layout Engine Challenge
This brings me to the current focus and, frankly, the biggest hurdle: the LayoutEngine
. Implementing Flexbox, even a subset, from scratch in a TUI context is proving non-trivial.
I’m currently wrestling with:
- Complex Interactions: Getting properties like
flex_grow
,justify_content
(main-axis alignment), andalign_items
(cross-axis alignment) to work together correctly, especially with nested elements and borders, requires careful state management and calculation logic. - Two-Pass Layout: I’ve adopted a two-pass approach (measure, then arrange) common in layout systems. Ensuring preferred sizes are calculated correctly in the first pass and then accurately distributed and positioned in the second pass, while respecting all the flex rules and constraints, is intricate.
- Tests: Whilst I, historically, hated writing tests, I’ve finally found a project that’s making me love them. The plan includes comprehensive testing, I haven’t fully built out the test suite for the
LayoutEngine
yet. This has made development more difficult, as regressions are harder to catch automatically. I’ll be taking quite a bit of time to actually develop an extremely robust testing suite. This should be a short-term pain, long-term gain thing.
How deep does the rabbit hole go?
Despite these challenges, the core vision remains strong. The current focus is on stabilizing the LayoutEngine
, particularly ensuring flex_grow
, basic alignment (justify_content
, align_items
), and border calculations are robust. Fixing the failing assertions in our tests is the immediate next step, followed by expanding the test suite to cover more edge cases. There’s still a truckload of items on the to-do list before I would consider it in alpha stage, but I think most of the difficult implementations are the ones i’m facing now.
Building Beautty is a roller coaster, from extremely high highs to frustrating lows (there’s no caniuse for terminals, it’s mostly boring ancient documentation). It’s bringing me to a level of depth in the computing stack I never would’ve imagined, this shit’s fun.
Happy hacking.