cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / headless / get.go
get.go 6.0K
1package headless23import (4 "fmt"5 "io"6 "strings"7 "unicode/utf8"89 "src.rest/cat/012/internal/fileio"10 "src.rest/cat/012/internal/nuon"11 "src.rest/cat/012/internal/sheet"12)1314// Formats are what Get writes: text as the sheet shows it, or a table15// as 012 --pipe and exports write it.16var Formats = []string{"text", "csv", "tsv", "json", "nuon"}1718// GetOptions say how Get writes.19type GetOptions struct {20 Format string // one of Formats; "" is text21 // Input writes what was typed (a formula, or an entry as stored)22 // rather than the value.23 Input bool24 // NoHeader names a JSON or NUON table's columns by their letters and25 // makes every row a record, rather than taking the names from the26 // first row.27 NoHeader bool28}2930// Get writes the target's values. One cell is written alone: as it31// shows, or as a JSON or NUON value typed by its format (a number, a32// date, a file size). A range or sheet is written as a table: aligned33// columns as text, or as CSV, TSV, JSON or NUON, whose columns take34// their names from the first row (see GetOptions.NoHeader).35func Get(out io.Writer, t Target, o GetOptions) error {36 if o.Format == "" {37 o.Format = "text"38 }39 if t.Cell() {40 return getCell(out, t.Sheet, t.Range.From, o)41 }42 r := t.Range43 if t.Whole {44 r = sheet.Rect{}45 }46 snap := fileio.Snap(t.Sheet, r, t.Sheet.Name())47 snap.HiddenRows = nil // a script reads the whole range, whatever a filter shows48 if o.Input {49 inputs(snap)50 }51 if o.Format == "text" {52 return writeText(out, t.Sheet, snap, o.Input)53 }54 k, ok := fileio.KindNamed(o.Format)55 if !ok || !k.IsText() {56 return fmt.Errorf("--format %s: 012 get writes %s", o.Format, strings.Join(Formats, ", "))57 }58 if o.NoHeader && (k == fileio.JSON || k == fileio.NUON) {59 snap = lettered(snap)60 }61 if k == fileio.JSON {62 return writeJSONTable(out, snap)63 }64 _, err := fileio.Encode(out, k, snap)65 return err66}6768// writeJSONTable writes snap as a JSON list of records named by its69// first row, one to a line, each value a Value.70func writeJSONTable(out io.Writer, snap *fileio.Snapshot) error {71 r := snap.Range72 cols := fileio.SnapColumns(snap)73 b := []byte("[")74 for row := r.From.Row + 1; row <= r.To.Row; row++ {75 if row > r.From.Row+1 {76 b = append(b, ',')77 }78 b = append(b, "\n{"...)79 for i, name := range cols {80 if i > 0 {81 b = append(b, ',')82 }83 b = append(nuon.AppendJSON(b, nuon.StringValue(name)), ':')84 c, ok := snap.Cells[sheet.Addr{Col: r.From.Col + i, Row: row}]85 if !ok {86 b = append(b, "null"...)87 continue88 }89 b = append(b, valueOf(c.Value, c.Format)...)90 }91 b = append(b, '}')92 }93 if r.To.Row > r.From.Row {94 b = append(b, '\n')95 }96 _, err := out.Write(append(b, "]\n"...))97 return err98}99100// getCell writes one cell's value or input.101func getCell(out io.Writer, s *sheet.Sheet, a sheet.Addr, o GetOptions) error {102 c := fileio.SnapCell{Value: s.Value(a), Format: s.DisplayFormat(a)}103 text := s.LocalText(a)104 if o.Input {105 input := ""106 if cell := s.Cell(a); cell != nil {107 input = cell.Input108 }109 c, text = fileio.SnapCell{Value: sheet.Value{Kind: sheet.Text, Str: input}}, input110 if input == "" {111 c.Value = sheet.Value{}112 }113 }114 var line []byte115 switch o.Format {116 case "text":117 line = []byte(text)118 case "csv", "tsv":119 return getTable(out, s, a, o)120 case "json":121 line = valueOf(c.Value, c.Format)122 case "nuon":123 line = nuon.Append(nil, fileio.CellValue(c))124 default:125 return fmt.Errorf("--format %s: 012 get writes %s", o.Format, strings.Join(Formats, ", "))126 }127 _, err := out.Write(append(line, '\n'))128 return err129}130131// getTable writes one cell as a table of one row: a CSV or TSV line.132func getTable(out io.Writer, s *sheet.Sheet, a sheet.Addr, o GetOptions) error {133 snap := fileio.Snap(s, sheet.Rect{From: a, To: a}, s.Name())134 snap.Range.To = a135 if o.Input {136 inputs(snap)137 }138 k, _ := fileio.KindNamed(o.Format)139 _, err := fileio.Encode(out, k, snap)140 return err141}142143// inputs makes each cell of snap hold its input as text in place of its144// value; a cell an array spilled into has none.145func inputs(snap *fileio.Snapshot) {146 for a, c := range snap.Cells {147 v := sheet.Value{Kind: sheet.Text, Str: c.Input}148 if c.Input == "" {149 v = sheet.Value{}150 }151 snap.Cells[a] = fileio.SnapCell{Input: c.Input, Value: v}152 }153}154155// lettered is snap with a first row naming each column by its letter,156// so the range's own first row becomes a record.157func lettered(snap *fileio.Snapshot) *fileio.Snapshot {158 out := *snap159 r := snap.Range160 out.Range.To.Row++161 out.Cells = make(map[sheet.Addr]fileio.SnapCell, len(snap.Cells)+r.To.Col-r.From.Col+1)162 for a, c := range snap.Cells {163 out.Cells[sheet.Addr{Col: a.Col, Row: a.Row + 1}] = c164 }165 for col := r.From.Col; col <= r.To.Col; col++ {166 name := sheet.ColName(col)167 out.Cells[sheet.Addr{Col: col, Row: r.From.Row}] = fileio.SnapCell{Input: name, Value: sheet.Value{Kind: sheet.Text, Str: name}}168 }169 return &out170}171172// writeText writes snap's range as aligned columns of what each cell173// shows (or its input), numbers to the right, two spaces between174// columns and no spaces at the ends of lines.175func writeText(out io.Writer, s *sheet.Sheet, snap *fileio.Snapshot, input bool) error {176 r := snap.Range177 cols := r.To.Col - r.From.Col + 1178 text := func(a sheet.Addr) (string, bool) {179 c, ok := snap.Cells[a]180 if !ok {181 return "", false182 }183 if input {184 return c.Input, false185 }186 return s.LocalText(a), c.Value.Kind == sheet.Number187 }188 widths := make([]int, cols)189 for a := range snap.Cells {190 t, _ := text(a)191 widths[a.Col-r.From.Col] = max(widths[a.Col-r.From.Col], utf8.RuneCountInString(t))192 }193 var b strings.Builder194 for row := r.From.Row; row <= r.To.Row; row++ {195 var line strings.Builder196 for col := r.From.Col; col <= r.To.Col; col++ {197 t, right := text(sheet.Addr{Col: col, Row: row})198 if col > r.From.Col {199 line.WriteString(" ")200 }201 pad := strings.Repeat(" ", widths[col-r.From.Col]-utf8.RuneCountInString(t))202 if right {203 t = pad + t204 } else {205 t += pad206 }207 line.WriteString(t)208 }209 b.WriteString(strings.TrimRight(line.String(), " "))210 b.WriteByte('\n')211 }212 _, err := io.WriteString(out, b.String())213 return err214}