package table import ( "encoding/csv" "fmt" "os" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/gum/internal/stdin" "github.com/charmbracelet/gum/internal/timeout" "github.com/charmbracelet/gum/style" "github.com/charmbracelet/lipgloss" ltable "github.com/charmbracelet/lipgloss/table" "golang.org/x/text/encoding" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" ) // Run provides a shell script interface for rendering tabular data (CSV). func (o Options) Run() error { var input *os.File if o.File != "" { var err error input, err = os.Open(o.File) if err != nil { return fmt.Errorf("could not render file: %w", err) } } else { if stdin.IsEmpty() { return fmt.Errorf("no data provided") } input = os.Stdin } defer input.Close() //nolint: errcheck transformer := unicode.BOMOverride(encoding.Nop.NewDecoder()) reader := csv.NewReader(transform.NewReader(input, transformer)) reader.LazyQuotes = o.LazyQuotes reader.FieldsPerRecord = o.FieldsPerRecord separatorRunes := []rune(o.Separator) if len(separatorRunes) != 1 { return fmt.Errorf("separator must be single character") } reader.Comma = separatorRunes[0] writer := csv.NewWriter(os.Stdout) writer.Comma = separatorRunes[0] var columnNames []string var err error // If no columns are provided we'll use the first row of the CSV as the // column names. if len(o.Columns) <= 0 { columnNames, err = reader.Read() if err != nil { return fmt.Errorf("unable to parse columns") } } else { columnNames = o.Columns } data, err := reader.ReadAll() if err != nil { return fmt.Errorf("invalid data provided") } columns := make([]table.Column, 0, len(columnNames)) for i, title := range columnNames { width := lipgloss.Width(title) if len(o.Widths) > i { width = o.Widths[i] } columns = append(columns, table.Column{ Title: title, Width: width, }) } defaultStyles := table.DefaultStyles() styles := table.Styles{ Cell: defaultStyles.Cell.Inherit(o.CellStyle.ToLipgloss()), Header: defaultStyles.Header.Inherit(o.HeaderStyle.ToLipgloss()), Selected: o.SelectedStyle.ToLipgloss(), } rows := make([]table.Row, 0, len(data)) for row := range data { if len(data[row]) > len(columns) { return fmt.Errorf("invalid number of columns") } // fixes the data in case we have more columns than rows: for len(data[row]) < len(columns) { data[row] = append(data[row], "") } for i, col := range data[row] { if len(o.Widths) == 0 { width := lipgloss.Width(col) if width > columns[i].Width { columns[i].Width = width } } } rows = append(rows, table.Row(data[row])) } if o.Print { table := ltable.New(). Headers(columnNames...). Rows(data...). BorderStyle(o.BorderStyle.ToLipgloss()). Border(style.Border[o.Border]). StyleFunc(func(row, _ int) lipgloss.Style { if row == 0 { return styles.Header } return styles.Cell }) fmt.Println(table.Render()) return nil } opts := []table.Option{ table.WithColumns(columns), table.WithFocused(true), table.WithRows(rows), table.WithStyles(styles), } if o.Height > 0 { opts = append(opts, table.WithHeight(o.Height)) } table := table.New(opts...) ctx, cancel := timeout.Context(o.Timeout) defer cancel() m := model{ table: table, showHelp: o.ShowHelp, help: help.New(), keymap: defaultKeymap(), } tm, err := tea.NewProgram( m, tea.WithOutput(os.Stderr), tea.WithContext(ctx), ).Run() if err != nil { return fmt.Errorf("failed to start tea program: %w", err) } if tm == nil { return fmt.Errorf("failed to get selection") } m = tm.(model) if o.ReturnColumn > 0 && o.ReturnColumn <= len(m.selected) { if err = writer.Write([]string{m.selected[o.ReturnColumn-1]}); err != nil { return fmt.Errorf("failed to write col %d of selected row: %w", o.ReturnColumn, err) } } else { if err = writer.Write([]string(m.selected)); err != nil { return fmt.Errorf("failed to write selected row: %w", err) } } writer.Flush() return nil }