No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Kyle Davis 30edd5fe66
Some checks are pending
Rust CI / verify (push) Waiting to run
chore(repo): migrate to Ratatui
Added: regression tests, Ratatui experiments, and migration findings.

Changed: renderer, event loop, samples, generator, documentation, and CI for Ratatui 0.30.2.

Removed: tui-rs dependency and obsolete polling thread.
2026-10-23 15:28:00 +00:00
.github/workflows chore(repo): migrate to Ratatui 2026-10-23 15:28:00 +00:00
.sandbox/ratatui-experiments chore(repo): migrate to Ratatui 2026-10-23 15:28:00 +00:00
.vscode First commit. Created library to render markup in console. 2022-10-04 23:01:56 -05:00
samples chore(repo): migrate to Ratatui 2026-10-23 15:28:00 +00:00
src chore(repo): migrate to Ratatui 2026-10-23 15:28:00 +00:00
tests chore(repo): migrate to Ratatui 2026-10-23 15:28:00 +00:00
.gitignore chore(repo): migrate to Ratatui 2026-10-23 15:28:00 +00:00
Cargo.toml chore(repo): migrate to Ratatui 2026-10-23 15:28:00 +00:00
DISCOVERY.md chore(repo): migrate to Ratatui 2026-10-23 15:28:00 +00:00
LICENSE Initial commit 2022-10-04 22:56:17 -05:00
README.md chore(repo): migrate to Ratatui 2026-10-23 15:28:00 +00:00

tui-markup-renderer

Build terminal interfaces from XML-like markup with Ratatui. This fork migrates the original tui-rs renderer to Ratatui 0.30.2 while keeping layouts, styled text, buttons, tabs, dialogs, actions, state, and custom renderers.

Minimum supported Rust version: 1.88.

Install and generate a project

Add this repository as a path dependency while working on the fork:

[dependencies]
tui-markup-renderer = { path = "/path/to/tui-markup-renderer" }
ratatui = "=0.30.2"
crossterm = "0.29"

Run the bundled generator from this checkout:

cargo run --bin tui-markup-gen -- -n demo -o /tmp -t simple
cargo run --bin tui-markup-gen -- -n dashboard -o /tmp -t shell

Generated manifests use a path dependency on the checkout that built the generator, so generated projects compile before this fork is published. Change that path when moving the project to another machine. Both generated variants run cargo check before reporting success.

Markup and application

Save as assets/main.xml:

<layout id="root" direction="vertical">
  <styles>
    p {fg:green}
    button:focus {fg:white;bg:blue}
  </styles>
  <block border="all" title="Example" constraint="5">
    <p align="center">Press Tab and Enter</p>
  </block>
  <button id="quit" index="1" action="quit" constraint="3">Quit</button>
</layout>

Use a Ratatui backend and pass it to ui_loop:

use std::{collections::HashMap, io};
use ratatui::backend::CrosstermBackend;
use tui_markup_renderer::{event_response::EventResponse, markup_parser::MarkupParser};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let backend = CrosstermBackend::new(io::stdout());
    let mut ui = MarkupParser::try_new("assets/main.xml".into(), None, Some(HashMap::new()))?;
    ui.add_action("quit", |_, _| EventResponse::QUIT);
    ui.ui_loop(backend, |key, _state| {
        if key.code == crossterm::event::KeyCode::Char('q') {
            EventResponse::QUIT
        } else {
            EventResponse::NOOP
        }
    })
}

The loop handles Tab, Shift+Tab, and Enter for indexed controls, calls the application event callback for key presses, redraws on input or resize, and restores raw mode and the alternate screen when it exits or returns an error. EventResponse::STATE replaces the state map; CLEANFOCUS also clears focus. Actions receive the state map and the selected markup element.

Supported markup

  • layout splits children vertically or horizontally (direction). block and container provide borders, titles, and nested composition.
  • p renders wrapped, aligned text. button renders a focusable control. Give interactive elements an index, id, and action.
  • tabs, tabs-header, tab-item, tabs-body, and tab-content for="item-id" display one content panel at a time. The selected item is stored under <tabs-id>:index.
  • dialog show="state-key" buttons="Yes|No" is visible when the state value is true. Dialog button actions default to on_<dialog-id>_btn_<button-label>; action overrides that for all buttons.
  • constraint accepts a cell count (5), percentage (30%), minimum (10min), maximum (5max), or ratio (1:2). Like the original renderer, spare space in a split goes to the last child; Ratatui's Flex::Legacy is selected explicitly for this markup behavior.
  • border accepts all, none, or top|bottom|left|right. styles, focus_styles, and active_styles accept fg, bg, weight, and font-decoration. Global rules inside <styles> can target an element name, name:focus, name:active, or #id.

A custom element renderer can be registered through RendererStorage::add_renderer. Its callback receives (&mut Frame<'_>, Rect, &MarkupElement) so it can render within the assigned region. Pass the storage to MarkupParser::try_new.

More complete markup and applications are in samples/tui-markup-sample and samples/tldr.

Migration from tui-rs

Replace tui with Ratatui 0.30.2 and Crossterm 0.29. MarkupParser and RendererStorage no longer have a backend type parameter because Ratatui's Frame is backend independent. Custom renderer callbacks now receive the element's area and node. render_ui(&mut Frame<'_>) remains available for embedding in an existing Ratatui application. Prefer try_new to get a parse or file error; the original new constructor remains available. The old detached event polling thread was replaced by a synchronous loop with terminal cleanup. Buffer and layout rendering use Ratatui's current APIs.

Run verification with:

cargo fmt --all -- --check
cargo check --all-targets
cargo test --all-targets
cargo clippy --all-targets -- -D warnings
cargo doc --no-deps
cargo check --manifest-path samples/tui-markup-sample/Cargo.toml
cargo check --manifest-path samples/tldr/Cargo.toml

See DISCOVERY.md for migration findings and experiments. The project name is retained from the original library; tui-rs is historical context only.