zhiburt / tabled

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

Set column wrap dependent on terminal size #407

Open inf17101 opened 3 months ago

inf17101 commented 3 months ago

I have the following example table

 col1     col2           col3         col4              col5                                                               
 Item 1   Some text      More text    Additional text   This is a very long description that should be wrapped.            
 Item 2   Another text   Extra text   Further text      Another long description that needs to be wrapped appropriately...

I am searching for a possibility to wrap the column "col5" and use the rest of the available size of the terminal for that last column.

Depending on the user's terminal size I need something like this:

 col1     col2           col3         col4              col5                        
 Item 1   Some text      More text    Additional text   This is a very long descrip 
                                                        tion that should be wrapped.                           
 Item 2   Another text   Extra text   Further text      Another long description th 
                                                        at needs to be wrapped appr 
                                                        opriately...

Width::wrap supports a static number, but I need a setting inside tabled crate that it uses the rest of the available terminal width and wraps up the text if it cannot be displayed in a single line.

I can determine the terminal length by using another crate and calculate the rest of the available width for the last column I want to wrap by using the length of the string content itself or by parsing the ".to_string()" output of the table again to get all final column widths., but that seems unhandy and error prune. I miss a more pragmatic way of accessing the width of each of the columns (column name + content length) in the table object.

Example code:

[dependencies]
tabled = "0.12"
use tabled::{Table, Tabled, settings::{Style, Modify, Width, object::Columns}};

#[derive(Tabled)]
struct Item {
    col1: String,
    col2: String,
    col3: String,
    col4: String,
    col5: String,
}

fn main() {
    let items = vec![
        Item {
            col1: "Item 1".to_string(),
            col2: "Some text".to_string(),
            col3: "More text".to_string(),
            col4: "Additional text".to_string(),
            col5: "This is a very long description that should be wrapped.".to_string(),
        },
        Item {
            col1: "Item 2".to_string(),
            col2: "Another text".to_string(),
            col3: "Extra text".to_string(),
            col4: "Further text".to_string(),
            col5: "Another long description that needs to be wrapped appropriately...".to_string(),
        },
    ];

    let mut binding = Table::new(items);
    let table = binding
        .with(Style::blank())
        .with(
        Modify::new(Columns::single(4))
            .with(Width::wrap(100)), // how can I determine here the rest of available size for this column and wrap it to fit the terminal width
    );

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

Do you have a solution or workaround for that?

zhiburt commented 3 months ago

Hi @inf17101

I see 2 good options.

1. If you actually don't care about exact column; you just want to wrap the biggest one.

use tabled::{
    settings::{peaker::PriorityMax, Style, Width},
    Table, Tabled,
};

#[derive(Tabled)]
struct Item {
    col1: String,
    col2: String,
    col3: String,
    col4: String,
    col5: String,
}

fn main() {
    let items = vec![
        Item {
            col1: "Item 1".to_string(),
            col2: "Some text".to_string(),
            col3: "More text".to_string(),
            col4: "Additional text".to_string(),
            col5: "This is a very long description that should be wrapped.".to_string(),
        },
        Item {
            col1: "Item 2".to_string(),
            col2: "Another text".to_string(),
            col3: "Extra text".to_string(),
            col4: "Further text".to_string(),
            col5: "Another long description that needs to be wrapped appropriately...".to_string(),
        },
    ];

    let mut table = Table::new(items);
    table
        .with(Style::blank())
        .with(Width::wrap(100).priority::<PriorityMax>());

    println!("{}", table);
}
 col1     col2           col3         col4              col5                                        
 Item 1   Some text      More text    Additional text   This is a very long description that should 
                                                         be wrapped.                                
 Item 2   Another text   Extra text   Further text      Another long description that needs to be w 
                                                        rapped appropriately...

2. If you actually want to wrap only exact column. But be ware that if there may be not enough space after all.

use tabled::{
    settings::{peaker::{Peaker, PriorityMax}, Style, Width},
    Table, Tabled,
};

#[derive(Tabled)]
struct Item {
    col1: String,
    col2: String,
    col3: String,
    col4: String,
    col5: String,
}

struct PriorityColumn<const N: usize>;

impl<const N: usize> Peaker for PriorityColumn<N> {
    fn create() -> Self {
        Self
    }

    fn peak(&mut self, min_widths: &[usize], widths: &[usize]) -> Option<usize> {
        let col = N;
        if widths[col] <= min_widths[col] {
            None
        } else {
            Some(col)
        }
    }
}

fn main() {
    let items = vec![
        Item {
            col1: "Item 1".to_string(),
            col2: "Some text".to_string(),
            col3: "More text".to_string(),
            col4: "Additional text".to_string(),
            col5: "This is a very long description that should be wrapped.".to_string(),
        },
        Item {
            col1: "Item 2".to_string(),
            col2: "Another text".to_string(),
            col3: "Extra text".to_string(),
            col4: "Further text".to_string(),
            col5: "Another long description that needs to be wrapped appropriately...".to_string(),
        },
    ];

    let mut table = Table::new(items);
    table
        .with(Style::blank())
        .with(Width::wrap(70).priority::<PriorityColumn::<4>>());

    println!("{}", table);
}
 col1     col2           col3         col4              col5          
 Item 1   Some text      More text    Additional text   This is a ver 
                                                        y long descri 
                                                        ption that sh 
                                                        ould be wrapp 
                                                        ed.           
 Item 2   Another text   Extra text   Further text      Another long  
                                                        description t 
                                                        hat needs to  
                                                        be wrapped ap 
                                                        propriately.. 
                                                        .

Let me know if that helps.


Now I am thinking that having priority set as generic does not bring a lot for the table... Probably shall be changed as a general argument.

zhiburt commented 3 months ago

And yes maybe PriorityColumn must be included into library itself. I guess it may be usefull

zhiburt commented 3 months ago

Also thinking maybe we shall return a status of operations. /success/fail