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 / diff / text.go
text.go 5.8K
1package diff23import (4 "bufio"5 "encoding/json"6 "fmt"7 "io"8 "maps"9 "slices"10 "strings"1112 "src.rest/cat/012/internal/nuon"13 "src.rest/cat/012/internal/sheet"14)1516// SGR codes of the text output's colors, from the 16 ANSI colors so the17// terminal's palette applies; the words carry the meaning without them.18const (19 sgrWhere = "\x1b[1m" // bold: the place20 sgrField = "\x1b[36m" // cyan: what about it21 sgrOld = "\x1b[31m" // red: what it was22 sgrNew = "\x1b[32m" // green: what it is23 sgrReset = "\x1b[0m"24)2526// Formats are what WriteChanges writes.27var Formats = []string{"text", "json", "nuon"}2829// WriteChanges writes changes in format: text, a line each (colored with30// color), or a table of records with the columns kind, sheet, item,31// field, old and new as JSON or NUON.32func WriteChanges(w io.Writer, changes []Change, format string, color bool) error {33 switch format {34 case "", "text":35 return writeText(w, changes, color)36 case "json":37 return writeJSON(w, changes)38 case "nuon":39 return writeNUON(w, changes)40 }41 return fmt.Errorf("--format %s: 012 diff writes %s", format, strings.Join(Formats, ", "))42}4344// writeText writes a line a change: its place, what about it changed,45// and "old → new", "+ new" for something added or "- old" for46// something removed.47func writeText(w io.Writer, changes []Change, color bool) error {48 bw := bufio.NewWriter(w)49 paint := func(sgr, s string) string {50 if !color || s == "" {51 return s52 }53 return sgr + s + sgrReset54 }55 for _, c := range changes {56 line := paint(sgrWhere, c.Where())57 if c.Field != "" {58 line += " " + paint(sgrField, c.Field)59 }60 switch {61 case c.Kind == KindSheet:62 line += sheetText(c)63 case c.Old.Kind == nuon.Null:64 line += " " + paint(sgrNew, "+ "+quoted(c.newText))65 case c.New.Kind == nuon.Null:66 line += " " + paint(sgrOld, "- "+quoted(c.oldText))67 default:68 line += " " + paint(sgrOld, quoted(c.oldText)) + " → " + paint(sgrNew, quoted(c.newText))69 }70 bw.WriteString(line + "\n")71 }72 return bw.Flush()73}7475// sheetText is the end of a sheet's line: its name or place in the76// first workbook, for a sheet renamed or put elsewhere.77func sheetText(c Change) string {78 switch c.Field {79 case "renamed":80 return " from " + c.oldText81 case "moved":82 return " from " + c.oldText + " to " + c.newText83 }84 return ""85}8687var recordCols = []string{"kind", "sheet", "item", "field", "old", "new"}8889func (c Change) values() []nuon.Value {90 str := func(s string) nuon.Value { return nuon.StringValue(s) }91 return []nuon.Value{str(c.Kind), str(c.Sheet), str(c.Item), str(c.Field), c.Old, c.New}92}9394func writeNUON(w io.Writer, changes []Change) error {95 tw := nuon.NewTableWriter(w, recordCols)96 for _, c := range changes {97 if err := tw.Write(c.values()); err != nil {98 return err99 }100 }101 return tw.Close()102}103104func writeJSON(w io.Writer, changes []Change) error {105 bw := bufio.NewWriter(w)106 bw.WriteString("[")107 fields := make([]nuon.Field, len(recordCols))108 for i, c := range changes {109 for j, v := range c.values() {110 fields[j] = nuon.Field{Key: recordCols[j], Value: v}111 }112 if i > 0 {113 bw.WriteString(",")114 }115 bw.WriteString("\n")116 bw.Write(nuon.AppendJSONRecord(nil, fields))117 }118 if len(changes) > 0 {119 bw.WriteString("\n")120 }121 bw.WriteString("]\n")122 return bw.Flush()123}124125// Dump writes a workbook as lines of text, one for each cell and each126// field, each naming its sheet: what git diffs when 012 diff --textconv127// is a .012 file's textconv. A formula's line ends with its value,128// unless it changes on every recalculation (NOW, RAND and what reads129// them), which would show as a change each time.130func Dump(w io.Writer, b *Book) error {131 bw := bufio.NewWriter(w)132 for _, s := range b.raw.sheets {133 sd := b.side(s)134 q := sheet.QuoteSheet(s.name)135 fmt.Fprintf(bw, "sheet %s\n", s.name)136 dumpFields(bw, s)137 for _, at := range sortedAddrs(s.cells) {138 bw.WriteString(q + "!" + cellLine(sd, at, s.cells[at.String()]) + "\n")139 }140 }141 names, _ := object(b.raw.top["names"])142 for _, k := range slices.Sorted(maps.Keys(names)) {143 fmt.Fprintf(bw, "name %s %s\n", k, rawText(names[k]))144 }145 macros := named(b.raw.top["macros"])146 for _, k := range slices.Sorted(maps.Keys(macros)) {147 fmt.Fprintf(bw, "macro %s %s\n", k, macros[k])148 }149 for _, k := range slices.Sorted(maps.Keys(b.raw.top)) {150 if !quietKeys[k] && k != "names" && k != "macros" {151 fmt.Fprintf(bw, "workbook %s %s\n", k, b.raw.top[k])152 }153 }154 return bw.Flush()155}156157// dumpFields writes a sheet's fields as Dump does: a notebook's cells,158// regions and tables one line each, and each other field whole.159func dumpFields(bw *bufio.Writer, s *rawSheet) {160 for _, k := range slices.Sorted(maps.Keys(s.fields)) {161 switch k {162 case "notebookCells":163 for _, line := range dumpNotebook(s.name, s.fields[k]) {164 bw.WriteString(line + "\n")165 }166 case "regions", "tables":167 what := strings.TrimSuffix(k, "s")168 for _, name := range slices.Sorted(maps.Keys(named(s.fields[k]))) {169 fmt.Fprintf(bw, "%s %s %s %s\n", s.name, what, name, named(s.fields[k])[name])170 }171 default:172 fmt.Fprintf(bw, "%s %s %s\n", s.name, k, s.fields[k])173 }174 }175}176177// cellLine is a cell as Dump writes it after its sheet: B7 =SUM(A1:A6)178// = 21 [currency, decimals 2] note: …179func cellLine(sd side, at sheet.Addr, raw json.RawMessage) string {180 p := splitCell(raw)181 line := at.String() + " " + quoted(p.input)182 if sheet.IsFormulaEntry(p.input) && sd.s != nil && !sd.vol[at] {183 _, shown := cellValue(sd.s, at)184 line += " = " + quoted(shown)185 }186 if p.format != nil {187 line += " [" + formatText(p.format) + "]"188 }189 if p.note != "" {190 line += " note: " + quoted(p.note)191 }192 return line193}194195// sortedAddrs are the cells' addresses in row order.196func sortedAddrs(cells map[string]json.RawMessage) []sheet.Addr {197 var out []sheet.Addr198 for k := range cells {199 if at, ok := sheet.ParseAddr(k); ok {200 out = append(out, at)201 }202 }203 slices.SortFunc(out, byRow)204 return out205}