jedib0t / go-pretty

Table-writer and more in golang!
MIT License
3.06k stars 120 forks source link

[Feature Request] - Table full width based on terminal size / Header / Row content auto positioning #328

Closed ondrovic closed 1 month ago

ondrovic commented 3 months ago

Is your feature request related to a problem? Please describe. Tables with and column spacing seem to be dependent on row content

Header / Row content alignment, I found in using the text.Hyperlink(text, link) it table formatting

Describe the solution you'd like A config option allow the table to calculate no only the full table width, but also split the header / column locations based on the terminal size

Header / Row content alignment, I found in using the text.Hyperlink(text, link) seems to break the content alignment

Describe alternatives you've considered Tried adjusting the .SetColumnConfigs( WidthMin, WidthMax)

Additional context

Examples:

using regular non hyperlink - works as expected, wish I could force it to be the full size of the terminal with everything aligned perfectly

image image

using hyperlink - throws alignment off just a bit

image image

I was able to find fix the alignment issue by doing the following but it's very hacky and I haven't tested it enough, would be better if this was automagically calculated

image image

// DirectoryResults struct for the results
type DirectoryResult struct {
    Directory string
    Count     int
}

// EntryResults struct for more in depth entry info
type EntryResults struct {
    Directory string
    FileName  string
    FileSize  string
}

// The `FormatPath` function converts file paths to either Windows or Unix style based on the operating.
// system specified.
func FormatPath(path, goos string) string {
    switch goos {
    case "windows":
        // Convert to Windows style paths (with backslashes)
        return filepath.FromSlash(path)
    case "linux", "darwin":
        // Convert to Unix style paths (with forward slashes)
        return filepath.ToSlash(path)
    default:
        // Default to Unix style paths
        return path
    }
}

// DetailedResults example
detailedResults := []EntryResults {
    {Directory: "S:\\testdata\\zips", FileName: "tamara.zip", FileSize: "32.37 MB"},
    {Directory: "S:\\testdata\\zips", FileName: "pop.7z", FileSize: "121.22 MB"},
    {Directory: "S:\\testdata\\One\\Videos\\One", FileName: "videofile.mkv", FileSize: "0 B"},
}

// Non Detailed example
nonDetailedResuls := []DirectoryResult {
  {Directory: "s:\\testdata\\One\\Videos\\One", Count: 1},
  {Directory: "s:\\testdata\\zips" , Count: 2},
}

// shared vars
totalCount := 3
totalFileSize := 161061273

// renderResults to table takes and interface and renders the table accordingly.
```go
func renderResultsToTable(results interface{}, totalCount int, totalFileSize int64, ff types.FileFinder) {
    t := table.Table{}

    // Determine header and footer based on the type of results
    var header table.Row
    var footer table.Row
    switch results.(type) {
    case []types.DirectoryResult:
        header = table.Row{"Directory", "\t\tCount"}
        footer = table.Row{"Total", pterm.Sprintf("\t\t  %v", totalCount)}
    case []types.EntryResult:
        header = table.Row{"Directory", "\t\tFileName", "\t\tFileSize"}
        footer = table.Row{"Total", pterm.Sprintf("\t\t  %v", totalCount), pterm.Sprintf("\t\t%v", commonFormatters.FormatSize(totalFileSize))}
    default:
        return // Exit if results type is not supported
    }

    t.AppendHeader(header)

    // Append rows based on the display mode
    switch results := results.(type) {
    case []types.DirectoryResult:
        for _, result := range results {
            t.AppendRow(table.Row{
                text.Hyperlink(commonFormatters.FormatPath(result.Directory, runtime.GOOS), result.Directory),
                pterm.Sprintf("\t   %v", result.Count),
            })
        }
    case []types.EntryResult:
        if ff.DisplayDetailedResults {
            for _, result := range results {
                t.AppendRow(table.Row{
                    text.Hyperlink(commonFormatters.FormatPath(result.Directory, runtime.GOOS), result.Directory),
                    "\t " + result.FileName,
                    "\t " + result.FileSize,
                })
            }
        }
    }

    t.AppendFooter(footer)

    t.SetStyle(table.StyleColoredDark)
    t.SetOutputMirror(os.Stdout)
    t.Render()
}

another example using colors with hyperlink illustration how alignment is broken - illustration how my temp fix is garbage

func formatResultHyperLink(link, txt string) string {
    text.EnableColors()

    link = commonFormatters.FormatPath(link, runtime.GOOS)
    txt = text.FgGreen.Sprint(txt)

    return text.Hyperlink(link, txt)
}

func renderResultsToTable(results interface{}, totalCount int, totalFileSize int64, ff types.FileFinder) {
    t := table.Table{}

    // Determine header and footer based on the type of results
    var header table.Row
    var footer table.Row
    switch results.(type) {
    case []types.DirectoryResult:
        header = table.Row{"Directory", "Count"}
        footer = table.Row{"Total", pterm.Sprintf("%v", totalCount)}
    case []types.EntryResult:
        header = table.Row{"Directory", "FileName", "FileSize"}
        footer = table.Row{"Total", pterm.Sprintf("%v", totalCount), pterm.Sprintf("%v", commonFormatters.FormatSize(totalFileSize))}
    default:
        return // Exit if results type is not supported
    }

    t.AppendHeader(header)

    // Append rows based on the display mode
    switch results := results.(type) {
    case []types.DirectoryResult:
        for _, result := range results {
            t.AppendRow(table.Row{
                formatResultHyperLink(result.Directory, result.Directory),
                pterm.Sprintf("%v", result.Count),
            })
        }
    case []types.EntryResult:
        if ff.DisplayDetailedResults {
            for _, result := range results {
                newLink := pterm.Sprintf("%s/%s", result.Directory, result.FileName)
                t.AppendRow(table.Row{
                    formatResultHyperLink(result.Directory, result.Directory),
                    formatResultHyperLink(newLink, result.FileName),
                    result.FileSize,
                })
            }
        }
    }

    t.AppendFooter(footer)

    t.SetStyle(table.StyleColoredDark)
    t.SetOutputMirror(os.Stdout)
    t.Render()
}

image image

jedib0t commented 3 months ago

Auto-sizing tables has been something I wanted to implement, thanks for the reminder.

jedib0t commented 1 month ago

Hey @ondrovic try the code in branch table-auto-width and see if that works for you?

Sample usage: https://github.com/jedib0t/go-pretty/blob/table-auto-width/table/render_test.go#L1356-L1533

ondrovic commented 1 month ago

Hey @ondrovic try the code in branch table-auto-width and see if that works for you?

Sample usage: https://github.com/jedib0t/go-pretty/blob/table-auto-width/table/render_test.go#L1356-L1533

I will give it a try later today and let you know

ondrovic commented 1 month ago

Hey @ondrovic try the code in branch table-auto-width and see if that works for you?

Sample usage: https://github.com/jedib0t/go-pretty/blob/table-auto-width/table/render_test.go#L1356-L1533

As a quick test I tried it like this

func renderResultsToTable(results interface{}, totalCount int, totalFileSize int64, ff types.FileFinder) {
    t := table.Table{}

    // Determine header and footer based on the type of results
    var header table.Row
    var footer table.Row
    switch results.(type) {
    case []types.DirectoryResult:
        header = table.Row{"Directory", "Count"}
        footer = table.Row{"Total", pterm.Sprintf("%v", totalCount)}
    case []types.EntryResult:
        header = table.Row{"Directory", "FileName", "FileSize"}
        footer = table.Row{"Total", pterm.Sprintf("%v", totalCount), pterm.Sprintf("%v", commonFormatters.FormatSize(totalFileSize))}
    default:
        return // Exit if results type is not supported
    }

    t.AppendHeader(header)

    // Append rows based on the display mode
    switch results := results.(type) {
    case []types.DirectoryResult:
        for _, result := range results {
            t.AppendRow(table.Row{
                formatResultHyperLink(result.Directory, result.Directory),
                pterm.Sprintf("%v", result.Count),
            })
        }
    case []types.EntryResult:
        if ff.DisplayDetailedResults {
            for _, result := range results {
                newLink := pterm.Sprintf("%s/%s", result.Directory, result.FileName)
                t.AppendRow(table.Row{
                    formatResultHyperLink(result.Directory, result.Directory),
                    formatResultHyperLink(newLink, result.FileName),
                    result.FileSize,
                })
            }
        }
    }

    t.AppendFooter(footer)

    t.SetStyle(table.StyleColoredDark)
    t.SetStyle(table.Style{Size: table.SizeOptions{
        WidthMax: 50,
        WidthMin: 0,
    }})
    t.SetOutputMirror(os.Stdout)
    t.Render()
}

It didn't quite work as I expected

without size styles

image

With new size styles image

It seems like it broke the headers / footers, it could have been that I wasn't using it like your example using the table writer, I will try that when I get back from errands this morning

But also I was hoping more for an automatic / dynamic min / max width where it would auto calculate the min / and the maximum based on the content so you don't have to hard code in the values. While I understand it may increase rendering times based on the the size of the data set.

jedib0t commented 1 month ago

Your second SetStyle just overrode the first call's effect. Try it like this:

    t.SetStyle(table.StyleColoredDark)
    t.Style().Size = table.SizeOptions{
        WidthMax: 50,
        WidthMin: 0,
    }})

To auto-size for your terminal, you'd do something like this:

package main

import (
    "fmt"
    "os"

    "github.com/jedib0t/go-pretty/v6/table"
    "golang.org/x/term"
)

func main() {
    w, h, err := term.GetSize(int(os.Stdin.Fd()))
    if err != nil {
        panic(err.Error())
    }
    fmt.Println(w, h)

    tw := table.NewWriter()
    tw.SetTitle("Title")
    tw.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"})
    tw.AppendRows([]table.Row{
        {1, "Arya", "Stark", 3000},
        {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
        {300, "Tyrion", "Lannister", 5000},
    })
    tw.AppendFooter(table.Row{"", "", "Total", 10000})
    tw.SetStyle(table.StyleLight)
    tw.Style().Size = table.SizeOptions{
        WidthMin: w,
    }
    fmt.Println(tw.Render())
}

Output: image

Title sizing logic is broken now and I will fix it before merging this code.

jedib0t commented 1 month ago

I've pushed a couple of commit just now. So try to use the latest to get the fix for the title rendering being broken.

image

ondrovic commented 1 month ago

I've pushed a couple of commit just now. So try to use the latest to get the fix for the title rendering being broken.

image

so based on this still using non table.NewWriter() method (going to do that next)

func renderResultsToTable(results interface{}, totalCount int, totalFileSize int64, ff types.FileFinder) {
    t := table.Table{}
    w, h, err := getTerminalSize()
    if err != nil {
        panic(err.Error())
    }
    fmt.Printf("Terminal size: Width = %d, Height = %d\n", w, h)
    // Determine header and footer based on the type of results
    var header table.Row
    var footer table.Row
    switch results.(type) {
    case []types.DirectoryResult:
        header = table.Row{"Directory", "Count"}
        footer = table.Row{"Total", pterm.Sprintf("%v", totalCount)}
    case []types.EntryResult:
        header = table.Row{"Directory", "FileName", "FileSize"}
        footer = table.Row{"Total", pterm.Sprintf("%v", totalCount), pterm.Sprintf("%v", commonFormatters.FormatSize(totalFileSize))}
    default:
        return // Exit if results type is not supported
    }

    t.AppendHeader(header)

    // Append rows based on the display mode
    switch results := results.(type) {
    case []types.DirectoryResult:
        for _, result := range results {
            t.AppendRow(table.Row{
                formatResultHyperLink(result.Directory, result.Directory),
                pterm.Sprintf("%v", result.Count),
            })
        }
    case []types.EntryResult:
        if ff.DisplayDetailedResults {
            for _, result := range results {
                newLink := pterm.Sprintf("%s/%s", result.Directory, result.FileName)
                t.AppendRow(table.Row{
                    formatResultHyperLink(result.Directory, result.Directory),
                    formatResultHyperLink(newLink, result.FileName),
                    result.FileSize,
                })
            }
        }
    }

    t.AppendFooter(footer)

    t.SetStyle(table.StyleColoredDark)
    t.SetStyle(table.Style{Size: table.SizeOptions{
        WidthMin: w,
    }})
    t.SetOutputMirror(os.Stdout)
    t.Render()
}

it does seem to have fixed the formatting image

The only thing is with really long filenames Main monitor 5120x1440 image Second monitor 1920x860 image

jedib0t commented 1 month ago

Two things:

  1. You are still calling SetStyle() twice with the second call overriding everything except for Size; please use Style().Size for the second call.
  2. This logic does auto-expand, and not auto-contract; maybe I'll find a way to do it next.
ondrovic commented 1 month ago

Two things:

  1. You are still calling SetStyle() twice with the second call overriding everything except for Size; please use Style().Size for the second call.
  2. This logic does auto-expand, and not auto-contract; maybe I'll find a way to do it next.
  1. That worked

    // just posting for anyone else who stumbles across this ;-)
    t.Style().Size = table.SizeOptions{
        WidthMin: w,
    }

    image

  2. Let me know if you end up doing it and need me to test it out