Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Easier viewing of styles in widgets in a print debugging workflow #1045

Open
kdheepak opened this issue Apr 17, 2024 · 8 comments · May be fixed by #1065
Open

Easier viewing of styles in widgets in a print debugging workflow #1045

kdheepak opened this issue Apr 17, 2024 · 8 comments · May be fixed by #1065
Labels
enhancement New feature or request

Comments

@kdheepak
Copy link
Collaborator

Problem

Sometimes I want to print a widget that I'm developing to the terminal to see whether it is being displayed the way I expect it to. Currently, I have to set up a TUI with key handlers etc, run the TUI, exit the TUI.

I would like to instead just print the widget to the terminal.

TestBackend has a debug print which almost does what I want but it doesn't display any colors.

Solution

This works for me but I'm wondering if there's a better way to do this?

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn render_default() -> color_eyre::Result<()> {
        let mut buf = Vec::new();
        let backend = CrosstermBackend::new(&mut buf);
        let mut terminal = Terminal::with_options(
            backend,
            TerminalOptions {
                viewport: Viewport::Inline(1),
            },
        )?;
        terminal
            .draw(|f| f.render_widget("hello world", f.size()))?;
        drop(terminal);
        let view = String::from_utf8(buf)?;
        println!("{view}");
        Ok(())
    }
}

Ideally TestBackend would have a way to print with styles.

@kdheepak kdheepak added the enhancement New feature or request label Apr 17, 2024
@kdheepak kdheepak changed the title Easier debugging Easier viewing of styles in widgets in a print style debugging workflow Apr 17, 2024
@kdheepak kdheepak changed the title Easier viewing of styles in widgets in a print style debugging workflow Easier viewing of styles in widgets in a print debugging workflow Apr 17, 2024
@kdheepak
Copy link
Collaborator Author

We should probably update this function to handle styles as well:

/// Returns a string representation of the given buffer for debugging purpose.
///
/// This function is used to visualize the buffer content in a human-readable format.
/// It iterates through the buffer content and appends each cell's symbol to the view string.
/// If a cell is hidden by a multi-width symbol, it is added to the overwritten vector and
/// displayed at the end of the line.
fn buffer_view(buffer: &Buffer) -> String {
let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3);
for cells in buffer.content.chunks(buffer.area.width as usize) {
let mut overwritten = vec![];
let mut skip: usize = 0;
view.push('"');
for (x, c) in cells.iter().enumerate() {
if skip == 0 {
view.push_str(c.symbol());
} else {
overwritten.push((x, c.symbol()));
}
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
}
view.push('"');
if !overwritten.is_empty() {
write!(&mut view, " Hidden by multi-width symbols: {overwritten:?}").unwrap();
}
view.push('\n');
}
view
}

And make the TestBackend configurable such that it either strips ansi styles or not when Display is invoked.

@joshka
Copy link
Member

joshka commented Apr 20, 2024

Creating a new TestBuffer type for this would probably work ok.

Imagine:

let w = MyWidget::new(...);
let mut buf = TestBuffer::new(10, 3);
assert_eq!(
  buf.render_string(w)),
  "..."
);

Add props for ansi / plain.
Add a method for Vec<String> to make it easier to compare.

Maybe even TestBuffer::new(w, 10, 3).to_string() would work.

Even just a method would be fine perhaps but it would be nice to be able to configure this a little.

Internally it would allocate the right size buffer, render and pull out the string value.

@imsnif
Copy link
Contributor

imsnif commented Apr 24, 2024

Hey, I have a hacked together function that receives a ratatui Buffer and returns stringified ANSI (with styles and all). I unfortunately do not have the capacity to do a proper PR for this, but will it help if I paste it here?

@kdheepak
Copy link
Collaborator Author

Yes absolutely! I'm happy to make it a PR in the future too (crediting you of course)!

@imsnif
Copy link
Contributor

imsnif commented Apr 24, 2024

There you go (no need to credit me, most of this is copied from various places in the ratatui codebase, the rest is just some ANSI serialization).

Note that this does not consolidate styles (basically it'll serialize styling for each cell regardless of what came before it). IMO this is not an issue in this context, seeing as we basically want a debug print and the performance cost is not Humanly noticeable in modern terminals - but ofc up to you if you'd like to add some sort of style consolidations.

I copy/pasted this from my project without testing, so let me know if I forgot anything!

fn render_widget(widget: impl Widget, rows: usize, cols: usize) {
    let rect = Rect {
        x: 0,
        y: 0,
        width: cols as u16,
        height: rows.saturating_sub(1) as u16,
    };
    let mut buf = Buffer::empty(rect.clone());
    widget.render(rect, &mut buf);
    let mut x = 0;
    let mut y = 0;
    fn stringify_cell_mofidier(modifier: Modifier) -> String {
        let mut stringified = String::new();
        match modifier {
            Modifier::BOLD => {
                stringified.push_str("\u{1b}[1m");
            },
            Modifier::ITALIC => {
                stringified.push_str("\u{1b}[3m");
            },
            Modifier::UNDERLINED => {
                stringified.push_str("\u{1b}[4m");
            },
            _ => {}
        }
        stringified
    }
    for (i, cell) in buf.content().iter().enumerate() {
        let modifier = stringify_cell_mofidier(cell.modifier);
        print!("{}{}{}{}", modifier, Fg(cell.fg), Bg(cell.bg), cell.symbol());
        x += 1;
        if x >= rect.width {
            x = 0;
            y += 1;
            print!("\n");
        }
    }
}
struct Fg(Color);

struct Bg(Color);

impl fmt::Display for Bg {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.0 {
            Color::Reset => write!(f, "\u{1b}[49m"),
            Color::Black => write!(f, "\u{1b}[40m"),
            Color::Red => write!(f, "\u{1b}[41m"),
            Color::Green => write!(f, "\u{1b}[42m"),
            Color::Yellow => write!(f, "\u{1b}[43m"),
            Color::Blue => write!(f, "\u{1b}[44m"),
            Color::Magenta => write!(f, "\u{1b}[45m"),
            Color::Cyan => write!(f, "\u{1b}[46m"),
            Color::Gray => write!(f, "\u{1b}[47m"),
            Color::DarkGray => write!(f, "\u{1b}[100m"),
            Color::LightRed => write!(f, "\u{1b}[101m"),
            Color::LightGreen => write!(f, "\u{1b}[102m"),
            Color::LightYellow => write!(f, "\u{1b}[103m"),
            Color::LightBlue => write!(f, "\u{1b}[104m"),
            Color::LightMagenta => write!(f, "\u{1b}[105m"),
            Color::LightCyan => write!(f, "\u{1b}[106m"),
            Color::White => write!(f, "\u{1b}[107m"),
            Color::Indexed(i) => write!(f, "\u{1b}[{}m", i),
            Color::Rgb(r, g, b) => write!(f, "\u{1b}[48;2;{};{};{}m", r, g, b),
        }
    }
}
impl fmt::Display for Fg {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.0 {
            Color::Reset => write!(f, "\u{1b}[39m"),
            Color::Black => write!(f, "\u{1b}[30m"),
            Color::Red => write!(f, "\u{1b}[31m"),
            Color::Green => write!(f, "\u{1b}[32m"),
            Color::Yellow => write!(f, "\u{1b}[33m"),
            Color::Blue => write!(f, "\u{1b}[34m"),
            Color::Magenta => write!(f, "\u{1b}[35m"),
            Color::Cyan => write!(f, "\u{1b}[36m"),
            Color::Gray => write!(f, "\u{1b}[37m"),
            Color::DarkGray => write!(f, "\u{1b}[90m"),
            Color::LightRed => write!(f, "\u{1b}[91m"),
            Color::LightGreen => write!(f, "\u{1b}[92m"),
            Color::LightYellow => write!(f, "\u{1b}[93m"),
            Color::LightBlue => write!(f, "\u{1b}[94m"),
            Color::LightMagenta => write!(f, "\u{1b}[95m"),
            Color::LightCyan => write!(f, "\u{1b}[96m"),
            Color::White => write!(f, "\u{1b}[97m"),
            Color::Indexed(i) => write!(f, "\u{1b}[{}m", i),
            Color::Rgb(r, g, b) => write!(f, "\u{1b}[38;2;{};{};{}m", r, g, b),
        }
    }
}

joshka added a commit that referenced this issue Apr 26, 2024
Importing the `WidgetExt` trait allows users to easily get a string
representation of a widget with ANSI escape sequences for the terminal.
This is useful for debugging and testing widgets.

```rust
use ratatui::{prelude::*, widgets::widget_ext::WidgetExt};

fn main() {
    let greeting = Text::from(vec![
        Line::styled("Hello", Color::Blue),
        Line::styled("World ", Color::Green),
    ]);
    println!("{}", greeting.to_ansi_string(5, 2));
}
```

Fixes: #1045
@joshka joshka linked a pull request Apr 26, 2024 that will close this issue
@joshka
Copy link
Member

joshka commented Apr 26, 2024

@imsnif I expanded on your approach in #1065

The cool thing about this is that it enables the following rather elegant code...

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

fn main() {
    let greeting = Text::from(vec![
        Line::styled("Hello", Color::Blue),
        Line::styled("World ", Color::Green),
    ]);
    println!("{}", greeting.to_ansi_string(5, 2));
}

widget_ext

joshka added a commit that referenced this issue Apr 26, 2024
Importing the `WidgetExt` trait allows users to easily get a string
representation of a widget with ANSI escape sequences for the terminal.
This is useful for debugging and testing widgets.

```rust
use ratatui::{prelude::*, widgets::widget_ext::WidgetExt};

fn main() {
    let greeting = Text::from(vec![
        Line::styled("Hello", Color::Blue),
        Line::styled("World ", Color::Green),
    ]);
    println!("{}", greeting.to_ansi_string(5, 2));
}
```

Fixes: #1045
@imsnif
Copy link
Contributor

imsnif commented Apr 26, 2024

Amazing! I see another dependency+feature was added... I hope it will still compile to the wasm32-wasi target? That's my use-case.

I'll try to give it a spin to make sure later.

@joshka
Copy link
Member

joshka commented Apr 26, 2024

Amazing! I see another dependency+feature was added... I hope it will still compile to the wasm32-wasi target? That's my use-case.

Just a feature flag due to the various uncertainties over naming / approach, no extra deps added.
Nothing that would change the wasm approach I'd expect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants