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

Two column layout #472

Open
pronebird opened this issue Dec 22, 2024 · 9 comments
Open

Two column layout #472

pronebird opened this issue Dec 22, 2024 · 9 comments
Labels
enhancement New feature or request good first issue Good for newcomers

Comments

@pronebird
Copy link

Hi,

is it possible to flip a traditional table with columns at the top and values below into two-column layout similar to psql expanded display but using modern style?

@zhiburt
Copy link
Owner

zhiburt commented Dec 22, 2024

is it possible to flip a traditional table with columns at the top and values below into two-column layout similar to psql expanded display but using modern style?

Hi @pronebird
I pretty sure we could do smth.

But could you draw an example of the table you want?
Cause I did not exactly how it must be looking.

@pronebird
Copy link
Author

is it possible to flip a traditional table with columns at the top and values below into two-column layout similar to psql expanded display but using modern style?

Hi @pronebird I pretty sure we could do smth.

But could you draw an example of the table you want? Cause I did not exactly how it must be looking.

Same as ExtendedTable except with modern style from Table or potentially any other style it supports.

Basically column a is field and column b is value. Then what used to be a column is printed for each entry as row.

I am gonna try putting together example when I am back at the keyboard.

@zhiburt
Copy link
Owner

zhiburt commented Dec 22, 2024

Something like this?

─┤ RECORD 0 ├──────
name        │ Manjaro
is_active   │ true
is_cool     │ true
─┤ RECORD 1 ├──────
name        │ Debian
is_active   │ true
is_cool     │ true

If it is the case,
It's actually interesting.
We would need to do some little work to make it happen.
But it's a solid proposition.

I wonder if you have any ideas what more styles we could add to ExtendedTable.

@zhiburt
Copy link
Owner

zhiburt commented Dec 22, 2024

Actually reading the name of the issue Two column layout and your description maybe you mean slightly different thing.

use tabled::{
    settings::{themes::Layout, Alignment, Style},
    Table, Tabled,
};

#[derive(Debug, Tabled)]
struct KV {
    key: String,
    value: String,
}

impl KV {
    fn new(key: &str, value: &str) -> Self {
        Self {
            key: key.to_string(),
            value: value.to_string(),
        }
    }
}

fn main() {
    let data = vec![
        KV::new("Hello", "1"),
        KV::new("World", "2"),
        KV::new("!", "3"),
    ];

    let mut table = Table::new(data);
    table
        .with(Layout::new(Alignment::left(), false))
        .with(Style::modern());

    println!("{table}")
}
┌───────┬───────┬───────┬───┐
│ key   │ Hello │ World │ ! │
├───────┼───────┼───────┼───┤
│ value │ 1     │ 2     │ 3 │
└───────┴───────┴───────┴───┘

The same could be achived by Rotate, or Builder::index(), or just building table using Builder.

@pronebird
Copy link
Author

pronebird commented Dec 22, 2024

Something like this?

─┤ RECORD 0 ├──────
name        │ Manjaro
is_active   │ true
is_cool     │ true
─┤ RECORD 1 ├──────
name        │ Debian
is_active   │ true
is_cool     │ true

If it is the case, It's actually interesting. We would need to do some little work to make it happen. But it's a solid proposition.

I wonder if you have any ideas what more styles we could add to ExtendedTable.

Meant exactly that. I have a table with way too many columns. There aren’t going to be many entries in it so a vertical layout seems like a better fit. I will elaborate more in the morning as I have a few examples where it could save me some time if it was built in.

@pronebird
Copy link
Author

pronebird commented Dec 23, 2024

So I have a table with a lot of columns, i.e

#[derive(Tabled)]
struct Company<'a> {
  #[tabled(rename = "Company")]
  name: &'a str,
  #[tabled(rename = "Street")]
  street: &'a str,
  #[tabled(rename = "City")]
  city: &'a str,
  #[tabled(rename = "ZIP code")]
  zip_code: &'a str,
  // etc..
}

A Vec of these is fed into ExtendedTable which works great but it lacks styling of a regular Table which breaks the consistency of my output to certain extent. What I think might be appealing is to be able to turn traditional table into series of keys and values where the first column is a field name and the second column is value, i.e:

┌──────────────┬───────────────────────────────────────────────┐
│ Field        │ Value                                         │
├──────────────┼───────────────────────────────────────────────┤
│ Company      │ INTEL CORP                                    │
├──────────────┼───────────────────────────────────────────────┤
│ Street       │ 2200 MISSION COLLEGE BLVD, RNB-4-151          │
├──────────────┼───────────────────────────────────────────────┤
│ City         │ SANTA CLARA                                   │
├──────────────┼───────────────────────────────────────────────┤
│ ZIP code     │ 95054                                         │
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
│ Company      │ Apple Inc.                                    │
├──────────────┼───────────────────────────────────────────────┤
│ Street       │ ONE APPLE PARK WAY                            │
├──────────────┼───────────────────────────────────────────────┤
│ City         │ CUPERTINO                                     │
├──────────────┼───────────────────────────────────────────────┤
│ ZIP code     │ 95014                                         │
└──────────────┴───────────────────────────────────────────────┘

The field/value header can be omitted and the inter-record separator can either be:

  • A thick line or
  • One of the fields can be turned into a span with a centered heading which I personally find really cool (see below)
┌──────────────────────────────────────────────────────────────┐
│                         INTEL CORP                           │
├──────────────┬───────────────────────────────────────────────┤
│ Street       │ 2200 MISSION COLLEGE BLVD, RNB-4-151          │
├──────────────┼───────────────────────────────────────────────┤
│ City         │ SANTA CLARA                                   │
├──────────────┼───────────────────────────────────────────────┤
│ ZIP code     │ 95054                                         │
├──────────────┴───────────────────────────────────────────────┤
│                         Apple Inc.                           │
├──────────────┬───────────────────────────────────────────────┤
│ Street       │ ONE APPLE PARK WAY                            │
├──────────────┼───────────────────────────────────────────────┤
│ City         │ CUPERTINO                                     │
├──────────────┼───────────────────────────────────────────────┤
│ ZIP code     │ 95014                                         │
└──────────────┴───────────────────────────────────────────────┘

I realize that I could probably build a PoolTable manually but that seems like quite a bit of work isn't it?

@zhiburt
Copy link
Owner

zhiburt commented Dec 23, 2024

So I do like your proposition a lot.
And I'd like to add such a layout to the library, though I am not sure whether it must be a separate Table or a setting which would make it happen.
The thing with a setting is it'll be necessary to rebuild table which is a bit inefficient.

So there's no out of the box solution yet.
In a meantime I am thinking you can use the next snippet.

If you have some more ideas in this regard please let me know.

PS: Yes PoolTable also could be used.

use tabled::{
    builder::Builder,
    grid::config::HorizontalLine,
    settings::{style::BorderSpanCorrection, Dup, Span, Style, Theme},
    Table, Tabled,
};

#[derive(Tabled)]
struct Company<'a> {
    #[tabled(rename = "Company", format("{}", self.name.to_uppercase()))]
    name: &'a str,
    #[tabled(rename = "Street", format("{}", self.street.to_uppercase()))]
    street: &'a str,
    #[tabled(rename = "City", format("{}", self.city.to_uppercase()))]
    city: &'a str,
    #[tabled(rename = "ZIP code", format("{}", self.zip_code.to_uppercase()))]
    zip_code: &'a str,
}

fn kv_table_build<T>(list: &[T]) -> Table
where
    T: Tabled,
{
    let mut b = Builder::with_capacity(list.len() * <T as Tabled>::LENGTH, 2);

    b.push_record(["Key", "Value"]);

    for company in list {
        let values = <T as Tabled>::headers()
            .into_iter()
            .zip(Tabled::fields(&company));

        for (k, v) in values {
            b.push_record([k, v]);
        }
    }

    b.build()
}

fn kv_table_span<T>(t: &mut Table)
where
    T: Tabled,
{
    let count_columns = <T as Tabled>::LENGTH;
    let length = (t.count_rows() - 1) / count_columns;

    for i in 0..length {
        let row = 1 + (i * count_columns);

        t.with(Dup::new((row, 0), (row, 1)));
        t.modify((row, 0), Span::column(2));
    }

    t.with(Style::modern());
    t.with(BorderSpanCorrection);
}

fn kv_table_line_separation<T>(t: &mut Table)
where
    T: Tabled,
{
    let count_columns = <T as Tabled>::LENGTH;
    let length = (t.count_rows() - 1) / count_columns;

    let mut theme = Theme::from(Style::modern());
    for i in 1..length {
        let pos = 1 + (i * count_columns);
        theme.insert_horizontal_line(pos, HorizontalLine::full('━', '╋', '┣', '┫'));
    }

    t.with(theme);
}


fn main() {
    let companies = vec![
        Company { name: "INTEL CORP", city: "SANTA CLARA", street: "2200 MISSION COLLEGE BLVD, RNB-4-151", zip_code: "95054" },
        Company { name: "Apple Inc.", city: "CUPERTINO", street: "ONE APPLE PARK WAY", zip_code: "95014" },
    ];

    let mut table = kv_table_build(&companies);

    /* Do for horizontal line change */
    // kv_table_line_separation::<Company>(&mut table);

    /* Do for panel change */
    // kv_table_span::<Company>(&mut table);

    println!("{table}")
}

Uncomment one of the commented lines and you see what your expect.

@zhiburt zhiburt added enhancement New feature or request good first issue Good for newcomers labels Dec 23, 2024
@pronebird
Copy link
Author

pronebird commented Dec 23, 2024

And I'd like to add such a layout to the library, though I am not sure whether it must be a separate Table or a setting which would make it happen.

Maybe we could add customisation to the ExtendedTable because it has the right layout. One of the fields could be used to replace RECORD {} with the value.

Right now I use template() to pick the company name:

let store_rc = Rc::new(store);
let entries = store_rc.all_entries().iter().map(Company::from);
let table = ExtendedTable::new(entries).template(move |index| {
    let entry = store_rc
      .all_entries()
      .iter()
      .skip(index)
      .take(1)
      .next()
      .expect("failed to get entry");
    entry.name.to_owned()
});

Also it would be great to extend template and pass &Item inside to avoid that Rc<Store> roundtrip for anyone wanting to dynamically generate the heading so that the code would become this:

let entries = store.all_entries().iter().map(Company::from);
let table = ExtendedTable::new(entries).template_with_item(move |index, item /* &Company */| {
    item.name.to_owned()
});

I imagine that a derive macro can be added to define the heading of each entry but that's more advanced. I'd be happy to have something simple first.

Appreciate the example code above, I'll give it a spin! 💪 Note that uppercase values come from 3rd party system so I am not intentionally uppercasing anything but good to know that the value can be transformed with derive macro.

@zhiburt zhiburt mentioned this issue Jan 6, 2025
fdncred pushed a commit to nushell/nushell that referenced this issue Jan 22, 2025
Sorry was a little bit busy

close #14842

I've added a test but I'd check if it solved it.

cc: @fdncred 

__________________________

**Unrelated**

Recently got a pretty good format idea
(zhiburt/tabled#472)
Just wanna highlight that we could probably experiment with it, if it
being a bit elaborated.

It's sort of KV table which nushell already has,
But it's more for a default table where each row/record being rendered
as a KV table.

It's not something super nice I guess but maybe it could get some
appliance.
So yes pointing it out just in case.

Like these.

```
┌──────────────┬───────────────────────────────────────────────┐
│ Field        │ Value                                         │
├──────────────┼───────────────────────────────────────────────┤
│ Company      │ INTEL CORP                                    │
├──────────────┼───────────────────────────────────────────────┤
│ Street       │ 2200 MISSION COLLEGE BLVD, RNB-4-151          │
├──────────────┼───────────────────────────────────────────────┤
│ City         │ SANTA CLARA                                   │
├──────────────┼───────────────────────────────────────────────┤
│ ZIP code     │ 95054                                         │
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
│ Company      │ Apple Inc.                                    │
├──────────────┼───────────────────────────────────────────────┤
│ Street       │ ONE APPLE PARK WAY                            │
├──────────────┼───────────────────────────────────────────────┤
│ City         │ CUPERTINO                                     │
├──────────────┼───────────────────────────────────────────────┤
│ ZIP code     │ 95014                                         │
└──────────────┴───────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────┐
│                         INTEL CORP                           │
├──────────────┬───────────────────────────────────────────────┤
│ Street       │ 2200 MISSION COLLEGE BLVD, RNB-4-151          │
├──────────────┼───────────────────────────────────────────────┤
│ City         │ SANTA CLARA                                   │
├──────────────┼───────────────────────────────────────────────┤
│ ZIP code     │ 95054                                         │
├──────────────┴───────────────────────────────────────────────┤
│                         Apple Inc.                           │
├──────────────┬───────────────────────────────────────────────┤
│ Street       │ ONE APPLE PARK WAY                            │
├──────────────┼───────────────────────────────────────────────┤
│ City         │ CUPERTINO                                     │
├──────────────┼───────────────────────────────────────────────┤
│ ZIP code     │ 95014                                         │
└──────────────┴───────────────────────────────────────────────┘

```

PS: Now thinking about it,
it's sort of like doing a iteration over rows and building a current KV
table,
Which is interesting cause we could do it row by row, in which case
doing CTRLC would not ruin build but got some data rendered.
All though it's a different kind of approach. Just saying.
@zhiburt
Copy link
Owner

zhiburt commented Jan 24, 2025

Hey @pronebird

Got a little simplifications for my example: Table::kv, negative Span::column, and LayoutIterator.
Just wanna let you know that these will be available on the next release hopefully today.

use tabled::{
    iter::LayoutIterator,
    settings::{style::BorderSpanCorrection, Span, Style},
    Table, Tabled,
};

#[derive(Tabled)]
struct Company<'a> {
    name: &'a str,
    street: &'a str,
    city: &'a str,
    zip_code: &'a str,
}

fn main() {
    let companies = vec![
        Company { name: "INTEL CORP", city: "SANTA CLARA", street: "2200 MISSION COLLEGE BLVD, RNB-4-151", zip_code: "95054" },
        Company { name: "Apple Inc.", city: "CUPERTINO", street: "ONE APPLE PARK WAY", zip_code: "95014" },
    ];

    let mut table = Table::kv(&companies);

    for row in LayoutIterator::kv_batches::<Company>(&table) {
        table.modify((row, 1), Span::column(-1));
    }

    table.with(Style::modern());
    table.with(BorderSpanCorrection);

    println!("{table}")
}

According to your comment about ExtendedTable dynamic header seems like something must be done.
But overall I kind of didn't get the whole picture.
Maybe you could elaborate if you still have some thoughts.

Take care.
Have a great weekend.

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

No branches or pull requests

3 participants
@pronebird @zhiburt and others