zhiburt / tabled

An easy to use library for pretty print tables of Rust structs and enums.
MIT License
2.01k stars 84 forks source link

Having alternating BorderColor matching alternating Colorization::rows? #435

Open k86td opened 5 days ago

k86td commented 5 days ago

Hello there. I'm asking because I'm not sure if it's possible with this crate, and I'm still new to Rust. I'm trying to create alternating BorderColor to follow alternating Rows colour. The reason is, I want to create a colourized table with the header underlined. To avoid having a single line underlined going through the header, I'm setting a border of ' ' in the first row, the problem is when I try to colourize, borders are added on all rows which are not colourized using the current row Colorization. I've tried setting BorderColor, but I was setting it globally for all rows except the first. The result is half the rows would look okay, but the rows with the alternate colours do not display correctly.

Here's a sample of the code I'm using to display my table:

#[derive(Debug, Tabled)]
pub struct Expense {
    pub uuid: String,
    pub created: DateTime<Utc>,
    pub modified: DateTime<Utc>,
    pub data: Value,
}

// ...

let expenses = ctx.repo.get_all(25)?;

let underline_style = Color::new(AsciiStyling::Underline, AsciiStyling::Reset);
let display_table = Table::new(expenses)
    .with(Style::empty())
    .with(Colorization::rows([Color::BG_BLACK, Color::BG_WHITE]))
    .with(Colorization::exact([underline_style], Rows::first()))
    .modify(
        Rows::new(0..).not(Rows::first()),
        BorderColor::new().left(Color::BG_BLACK),
    )
    .modify(
        Rows::first().not(Columns::first()),
        Border::new().set_left(' '),
    )
    .to_string();

println!("{}", display_table);

Is it possible? I've checked the examples and tried different combination and this is the closest to the actual result I'm looking for. I haven't touched really the iterators yet, maybe it's just about extending Colorize to accept BorderColor? I will update this issue if I make progress on this. Thanks!

zhiburt commented 4 days ago

Hi @k86td

Sorry for late reply. I am pretty confident that it's possible to do with tabled. But to be honest I haven't understood the issue properly :( Could you example once again?


In mean time, look at my example. I think it's what you want? To be honest I quite dissatisfied how complex it is :disappointed:

It looks like it would great to add some type of iterator callback to modify cells.

use std::convert::TryFrom;

use owo_colors::OwoColorize;
use tabled::{
    settings::{object::Rows, style::BorderColor, themes::Colorization, Color, Style},
    Table,
};

fn main() {
    let expenses = vec![
        ("1", "11.11.11", "11.11.11", "11.11.11"),
        ("2", "11.22.11", "11.11.11", "11.11.11"),
        ("3", "11.33.11", "11.11.11", "11.11.11"),
        ("4", "11.44.11", "11.11.11", "11.11.11"),
    ];

    let underline_color = Color::try_from(".".underline().to_string()).unwrap();

    let mut table = Table::new(expenses);
    table
        .with(Style::blank())
        .with(Colorization::rows([Color::BG_BLACK, Color::BG_WHITE]))
        .with(Colorization::exact([underline_color], Rows::first()));

    for row in (1..table.count_rows()).step_by(2) {
        table.modify(Rows::single(row), BorderColor::new().left(Color::BG_WHITE));
    }

    for row in (2..table.count_rows()).step_by(2) {
        table.modify(Rows::single(row), BorderColor::new().left(Color::BG_BLACK));
    }

    println!("{}", table);
}

image


And @k86td if you want to you can contribute some of the things like italic, underline etc into Color. So it could be used as Color::Black | Color::Underline | Color::Bold.

It's a fairly simple trick. You can open a PR if you wish. See

https://github.com/zhiburt/tabled/blob/68f139fce3d0e7c09283035bba97d001ac62a0e3/tabled/src/settings/color/mod.rs#L183

k86td commented 3 days ago

Hey, thanks for your quick reply! It's exactly what I was looking for.

I agree with the extra complexity, I'm not familiar with exposing internal iterators to the outside, would it be like Table::new().rows_iter() ?

I would gladly try to help you with this. As for the colors, I was using an enum to support ANSI escape codes, not just colors. In my testing project I've made this little enum, maybe it would be something interesting to add inside Color ?

pub enum AsciiStyling<'a> {
    Reset,
    Bold,
    Underline,
    BackgroundColor8Bit(&'a str),
    // ...
}

impl From<AsciiStyling<'_>> for String {
    // ...
}

Let me know what you think, otherwise I'll add the constants.

zhiburt commented 2 days ago

I agree with the extra complexity,

I am thinking about adding Iterator like step() function for modify arguments. Something like this. What do you think.

The hardest question is how to call it step or step_by :thinking:

// instead of this
    for row in (1..table.count_rows()).step_by(2) {
        table.modify(Rows::single(row), BorderColor::new().left(Color::BG_WHITE));
    }

// this
    table.modify(Rows::new(1..).step(2), BorderColor::new().left(Color::BG_WHITE));

I'm not familiar with exposing internal iterators to the outside, would it be like Table::new().rows_iter() ?

There's a few methods to do that. I am not sure why you need it but something like

    for row in table.get_records().iter_rows() {
        for cell in row {
            println!("{cell}")
        }
    }

If you need access to cells you could use Format instead, which uses a function/closure.

    table.modify(
        Rows::new(1..),
        Format::content(|text| text.on_red().to_string()),
    );

I was using an enum

These constants are supposed to look like enum :sweat_smile: That's how we approach it. I think that's better then original enum in this case.

zhiburt commented 2 days ago

Actually implemented already it on master.

    table.modify(Rows::new(1..).step_by(2), BorderColor::new().left(Color::BG_WHITE));
k86td commented 1 day ago

That looks very clean.

There's a few methods to do that. I am not sure why you need it but something like

I was asking how you thought of approaching this, not a request:)

These constants are supposed to look like enum 😅

Yeah, you want to use static everywhere to avoid dynamic allocation? It would be great to have better support for more colour, but since it's not that hard to create Color it might not be useful here, but I'm still asking.

let color_white = Color::BG_8BIT::255;
let color_white = Color::8Bit::BG::255;
// or
let color_white = Color::BG_RGB(255, 255, 255);

Keep me posted, thanks for your very quick reply:)

zhiburt commented 1 day ago

Yeah, you want to use static everywhere to avoid dynamic allocation?

Exactly. But more importantly is because of this it could be used in const context. While general enum has some downsides, in particular cause of custom color creation.

It would be great to have better support for more colour, but since it's not that hard to create Color it might not be useful here, but I'm still asking.

No I think it's pretty valuable, as it reduces boilerplate code.

Your idea about RGB is actually very good, I never thought about it.

The only think I would probably make it a function? Yes it could be const.

let color_white = Color::rgb_bg(255, 255, 255);
k86td commented 1 day ago

So a simple method in Color would probably do the trick, but having it const causes problems from what I've tested.

impl Color {

  // ...

    // tried also with `&'static str` type
    const fn rgb_bg(r: u8, g: u8, b: u8) -> Self {
        let color = StaticColor::new(&format!("\u{1b}[38;{};{};{}m", r, g, b), "\u{1b}[49m");
        // or
        // let color = StaticColor::new(formatcp!("\u{1b}[38;{};{};{}m", r, g, b), "\u{1b}[49m");

        let inner = ColorInner::Static(color);

        Self { inner }
    }

  // ...

}

I checked using generics parameters, I don't think it'll be possible either:

const fn rgb_bg<const R: u8, const G: u8, const B: u8>() -> Self {
  // ...
}

I found const_format, but I don't think it'll help since the arguments aren't const. Would you need to wrap it around something else?

Does the output of rgb_bg really needs to be constant?

zhiburt commented 20 hours ago

So a simple method in Color would probably do the trick, but having it const causes problems from what I've tested.

Ahhhh yesss I did missed it. You're right.

Though your original comment about enum would work in const in case of RGB.

See there's downsides/upsides to some approaches :smile:

Does the output of rgb_bg really needs to be constant?

It shouldn't. I think it'll be all right to be generic.

k86td commented 8 hours ago

So I've made the two functions that can create colors from rgb. I also added an example & some tests.