| package table |
| |
| import ( |
| "encoding/csv" |
| "fmt" |
| "os" |
| |
| "github.com/alecthomas/kong" |
| "github.com/charmbracelet/bubbles/table" |
| tea "github.com/charmbracelet/bubbletea" |
| "github.com/mattn/go-runewidth" |
| |
| "github.com/charmbracelet/gum/internal/stdin" |
| "github.com/charmbracelet/gum/style" |
| ) |
| |
| // Run provides a shell script interface for rendering tabular data (CSV). |
| func (o Options) Run() error { |
| var reader *csv.Reader |
| if o.File != "" { |
| file, err := os.Open(o.File) |
| if err != nil { |
| return fmt.Errorf("could not find file at path %s", o.File) |
| } |
| reader = csv.NewReader(file) |
| } else { |
| if stdin.IsEmpty() { |
| return fmt.Errorf("no data provided") |
| } |
| reader = csv.NewReader(os.Stdin) |
| } |
| |
| 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 || len(data) == 0 { |
| return fmt.Errorf("invalid data provided") |
| } |
| var columns = make([]table.Column, 0, len(columnNames)) |
| |
| for i, title := range columnNames { |
| width := runewidth.StringWidth(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(), |
| } |
| |
| var rows = make([]table.Row, 0, len(data)) |
| for _, row := range data { |
| if len(row) > len(columns) { |
| return fmt.Errorf("invalid number of columns") |
| } |
| rows = append(rows, table.Row(row)) |
| } |
| |
| table := table.New( |
| table.WithColumns(columns), |
| table.WithFocused(true), |
| table.WithHeight(o.Height), |
| table.WithRows(rows), |
| table.WithStyles(styles), |
| ) |
| |
| tm, err := tea.NewProgram(model{table: table}, tea.WithOutput(os.Stderr)).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 err = writer.Write([]string(m.selected)); err != nil { |
| return fmt.Errorf("failed to write selected row: %w", err) |
| } |
| |
| writer.Flush() |
| |
| return nil |
| } |
| |
| // BeforeReset hook. Used to unclutter style flags. |
| func (o Options) BeforeReset(ctx *kong.Context) error { |
| style.HideFlags(ctx) |
| return nil |
| } |