ratatui-org / ratatui

Rust library that's all about cooking up terminal user interfaces (TUIs) 👨‍🍳🐀
https://ratatui.rs
MIT License
8.86k stars 269 forks source link

RSX-based interface to reduce boilerplate code #1121

Closed d4h0 closed 1 month ago

d4h0 commented 1 month ago

Problem

Hi,

I'm wondering if it would be possible to add an RSX-based interface (i.e., an HTML-like macro) to reduce boilerplate code.

Solution

In case you are not aware of it, there is the rstml crate which could do most of the heavy lifting (rstml is a fork of syn-rsx whose maintainer unfortunately passed away).

Alternatives

None, just continuing to write a lot of boilerplate code.

Additional context

Here is a list of the features that rstml offers and how it would look like.

rstml is used by leptos so also should be good for ratatui.

PS: Whoever came up with the name ratatui is a genius..! :D

Edit:

Here is how RSX could improve the ergonomics of ratatui (taken from my comment below):

Let's take the following example from the README:

use ratatui::{prelude::*, widgets::*};

fn ui(frame: &mut Frame) {
    let main_layout = Layout::new(
        Direction::Vertical,
        [
            Constraint::Length(1),
            Constraint::Min(0),
            Constraint::Length(1),
        ],
    )
    .split(frame.size());
    frame.render_widget(
        Block::new().borders(Borders::TOP).title("Title Bar"),
        main_layout[0],
    );
    frame.render_widget(
        Block::new().borders(Borders::TOP).title("Status Bar"),
        main_layout[2],
    );

    let inner_layout = Layout::new(
        Direction::Horizontal,
        [Constraint::Percentage(50), Constraint::Percentage(50)],
    )
    .split(main_layout[1]);
    frame.render_widget(
        Block::bordered().title("Left"),
        inner_layout[0],
    );
    frame.render_widget(
        Block::bordered().title("Right"),
        inner_layout[1],
    );
}

With JSX it could look like this:

(Please note, that I haven't used ratatui yet, so the following might not make complete sense. However, something similar to it seems possible)

use ratatui::{prelude::*, widgets::*};

fn ui(frame: &mut Frame) {
    rsx! {
        <VLayout split=frame.size()>
            <Block title="Title Bar" border:top length=1><Block>
            <HLayout min=0 split:available>
                <Block title="Left" border></Block>
                <Block title="Right" border></Block>
            </HLayout>
            <Block title="Status Bar" length=1><Block>
        </VLayout>
    }
}

And again with some comments:

use ratatui::{prelude::*, widgets::*};

fn ui(frame: &mut Frame) {
    rsx! {
        <VLayout split=frame.size()>
            // Or `<VerticalLayout>` but I like short names like `<VLayout>` more

            <Block title="Title Bar" border:top length=1>
                // Note: `border:top` is a boolean property and a shorthand for
                // `border:top=true`. The `:` is just for namespacing (so could
                // also be `border_top`).
                // Note: the `main_layout[0]` is here defined by the order of the RSX elements
            <Block>
            <HLayout min=0 split:available>
                // Note: `split:available` is the replacement for `.split(main_layout[1])`.
                // The meaning is "split the available space". Not sure if that makes sense.
                // There most-likely also should be a `split=whatever` attribute, to specify
                // custom values.
                <Block title="Left" border></Block>
                <Block title="Right" border></Block>
                // Note: `border`, of course, also could be `bordered`. For consistency I'd
                // prefer `border`, however.
            </HLayout>
            <Block title="Status Bar" length=1><Block>
        </VLayout>
    }
}

Much nicer, isn't it..?

To be honest, I'm not a big fan of macros and HTML-like syntax - however, I'm even less a fan of writing a lot of unnecessary boilerplate code.

I seriously think, adding RSX would take ratatui to the next level.

I've been following tui.rs/ratatui already many years, but I have never used it - mostly because it seems incredibly unergonomic to use (compared to how ergonomic it could be). It just always seemed easier to build a simple web application (e.g., for personal tools), however, I really like the idea of TUI applications.

joshka commented 1 month ago

PS: Whoever came up with the name ratatui is a genius..! :D

That was @sayanarijit in https://github.com/ratatui-org/ratatui/discussions/72.

I'm wondering if it would be possible to add an RSX-based interface (i.e., an HTML-like macro) to reduce boilerplate code.

There's a few things we've experimented with in https://github.com/ratatui-org/ratatui-macros (for line, span, text, as well as constraints / layout). I suspect we'll probably bring the line/span/text stuff into ratatui core at some point in the next few releases.

Do you have any ideas about what an RSX-based approach might look from a syntax perspective?

d4h0 commented 1 month ago

PS: Whoever came up with the name ratatui is a genius..! :D

That was @sayanarijit in https://github.com/ratatui-org/ratatui/discussions/72.

Good job, @sayanarijit!

There's a few things we've experimented with in https://github.com/ratatui-org/ratatui-macros (for line, span, text, as well as constraints / layout).

Yes, I saw those already (which I should have mentioned), but unfortunately, I believe this isn't going far enough.

Do you have any ideas about what an RSX-based approach might look from a syntax perspective?

Sure.

Let's take the following example from the README:

use ratatui::{prelude::*, widgets::*};

fn ui(frame: &mut Frame) {
    let main_layout = Layout::new(
        Direction::Vertical,
        [
            Constraint::Length(1),
            Constraint::Min(0),
            Constraint::Length(1),
        ],
    )
    .split(frame.size());
    frame.render_widget(
        Block::new().borders(Borders::TOP).title("Title Bar"),
        main_layout[0],
    );
    frame.render_widget(
        Block::new().borders(Borders::TOP).title("Status Bar"),
        main_layout[2],
    );

    let inner_layout = Layout::new(
        Direction::Horizontal,
        [Constraint::Percentage(50), Constraint::Percentage(50)],
    )
    .split(main_layout[1]);
    frame.render_widget(
        Block::bordered().title("Left"),
        inner_layout[0],
    );
    frame.render_widget(
        Block::bordered().title("Right"),
        inner_layout[1],
    );
}

With JSX it could look like this:

(Please note, that I haven't used ratatui yet, so the following might not make complete sense. However, something similar to it seems possible)

use ratatui::{prelude::*, widgets::*};

fn ui(frame: &mut Frame) {
    rsx! {
        <VLayout split=frame.size()>
            <Block title="Title Bar" border:top length=1><Block>
            <HLayout min=0 split:available>
                <Block title="Left" border></Block>
                <Block title="Right" border></Block>
            </HLayout>
            <Block title="Status Bar" length=1><Block>
        </VLayout>
    }
}

And again with some comments:

use ratatui::{prelude::*, widgets::*};

fn ui(frame: &mut Frame) {
    rsx! {
        <VLayout split=frame.size()>
            // Or `<VerticalLayout>` but I like short names like `<VLayout>` more

            <Block title="Title Bar" border:top length=1>
                // Note: `border:top` is a boolean property and a shorthand for
                // `border:top=true`. The `:` is just for namespacing (so could
                // also be `border_top`).
                // Note: the `main_layout[0]` is here defined by the order of the RSX elements
            <Block>
            <HLayout min=0 split:available>
                // Note: `split:available` is the replacement for `.split(main_layout[1])`.
                // The meaning is "split the available space". Not sure if that makes sense.
                // There most-likely also should be a `split=whatever` attribute, to specify
                // custom values.
                <Block title="Left" border></Block>
                <Block title="Right" border></Block>
                // Note: `border`, of course, also could be `bordered`. For consistency I'd
                // prefer `border`, however.
            </HLayout>
            <Block title="Status Bar" length=1><Block>
        </VLayout>
    }
}

Much nicer, isn't it..?

To be honest, I'm not a big fan of macros and HTML-like syntax - however, I'm even less a fan of writing a lot of unnecessary boilerplate code.

I seriously think, adding RSX would take ratatui to the next level.

I've been following tui.rs/ratatui already many years, but I have never used it - mostly because it seems incredibly unergonomic to use (compared to how ergonomic it could be). It just always seemed easier to build a simple web application (e.g., for personal tools), however, I really like the idea of TUI applications.

(Another issue is, that basic building blocks are missing or require importing third-party libraries, like for example input widgets. I might open another issue for this, at some point)

PS: I've added most of the above to my initial post.

joshka commented 1 month ago

I'd imagine this is probably possible to write outside of Ratatui core. There's nothing preventing you from adding something like this on top of the existing system afaik, maintenance burden is one thing that would steer me personally away from it in the short term (i.e. I'd imagine this would be a lot of code, which would be difficult to keep up to date with any changes to the core internals - doubling the work / tests / etc.)

I'd definitely encourage you to experiment with this - you may end up with something pretty neat for building TUIs.

d4h0 commented 1 month ago

Alright, that is too bad. Feel free to close this issue then. I'm not closing it myself in case you are interested in hearing further feedback from other people.

joshka commented 1 month ago

I'm going to convert this to a discussion rather than an issue. You might also consider posting it on the new (launched this week) Ratatui forum to see if anyone is interested in working on this.