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 / macro / builtins.go
builtins.go 14.1K
1package macro23import (4 "fmt"5 "math"6 "slices"7 "strconv"8 "strings"910 "go.starlark.net/starlark"1112 "src.rest/cat/012/internal/formula"13)1415// The functions scripts call. Each checks its arguments, then makes one16// call to the Host through Env.Do. docs/reference/macro-api.md documents them; keep17// the two in step.1819type builtinFn func(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error)2021func (r *Run) builtins() starlark.StringDict {22 fns := map[string]builtinFn{23 "get": r.get, "get_formula": r.getFormula, "set": r.set, "set_formula": r.setFormula,24 "clear": r.clear, "number_format": r.numberFormat, "get_number_format": r.getNumberFormat,25 "selection": r.selection, "active_cell": r.activeCell, "select": r.selectRange,26 "move": r.move, "extend": r.extend, "jump": r.jump, "enter": r.enter, "paste_text": r.pasteText,27 "sheets": r.sheets, "active_sheet": r.activeSheet, "activate_sheet": r.activateSheet,28 "add_sheet": r.addSheet, "move_sheet": r.moveSheet,29 "run": r.run, "set_width": r.setWidth, "set_height": r.setHeight, "fill": r.fill, "offset": offset,30 }31 out := make(starlark.StringDict, len(fns))32 for name, fn := range fns {33 out[name] = starlark.NewBuiltin(name, func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {34 return fn(args, kwargs)35 })36 }37 return out38}3940// host makes one call to the Host where it may be used.41func (r *Run) host(fn func(h Host) error) error {42 r.calls++43 var err error44 r.env.Do(func() { err = fn(r.env.Host) })45 return err46}4748// unpack reads arguments for the function whose name leads pairs'49// error messages; see starlark.UnpackArgs.50func unpack(fn string, args starlark.Tuple, kwargs []starlark.Tuple, pairs ...any) error {51 return starlark.UnpackArgs(fn, args, kwargs, pairs...)52}5354func (r *Run) get(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {55 var ref string56 if err := unpack("get", args, kwargs, "ref", &ref); err != nil {57 return nil, err58 }59 var rows [][]any60 if err := r.host(func(h Host) (err error) { rows, err = h.Values(ref); return }); err != nil {61 return nil, err62 }63 return shape(ref, rows, toValue), nil64}6566func (r *Run) getFormula(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {67 var ref string68 if err := unpack("get_formula", args, kwargs, "ref", &ref); err != nil {69 return nil, err70 }71 var rows [][]string72 if err := r.host(func(h Host) (err error) { rows, err = h.Formulas(ref); return }); err != nil {73 return nil, err74 }75 return shape(ref, rows, func(s string) starlark.Value { return starlark.String(s) }), nil76}7778// shape returns one value for a single cell, or a list of rows.79func shape[T any](ref string, rows [][]T, conv func(T) starlark.Value) starlark.Value {80 if len(rows) == 1 && len(rows[0]) == 1 && !strings.Contains(ref, ":") {81 return conv(rows[0][0])82 }83 out := make([]starlark.Value, len(rows))84 for i, row := range rows {85 vals := make([]starlark.Value, len(row))86 for j, v := range row {87 vals[j] = conv(v)88 }89 out[i] = starlark.NewList(vals)90 }91 return starlark.NewList(out)92}9394// toValue converts a cell's value: whole numbers become ints, as they95// read in a sheet.96func toValue(v any) starlark.Value {97 switch v := v.(type) {98 case float64:99 if v == math.Trunc(v) && math.Abs(v) < 1<<53 {100 return starlark.MakeInt64(int64(v))101 }102 return starlark.Float(v)103 case string:104 return starlark.String(v)105 case bool:106 return starlark.Bool(v)107 }108 return starlark.None109}110111// input is a value as it would be typed into a cell.112func input(fn string, v starlark.Value) (string, error) {113 switch v := v.(type) {114 case starlark.NoneType:115 return "", nil116 case starlark.String:117 return string(v), nil118 case starlark.Bool:119 if v {120 return "TRUE", nil121 }122 return "FALSE", nil123 case starlark.Int:124 return v.String(), nil125 case starlark.Float:126 f := float64(v)127 if math.IsInf(f, 0) || math.IsNaN(f) {128 return "", fmt.Errorf("%s: %v can't go in a cell", fn, v)129 }130 return strconv.FormatFloat(f, 'g', -1, 64), nil131 }132 return "", fmt.Errorf("%s: a cell takes a number, text, True, False or None, not %s", fn, v.Type())133}134135func (r *Run) set(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {136 var ref string137 var v starlark.Value138 if err := unpack("set", args, kwargs, "ref", &ref, "value", &v); err != nil {139 return nil, err140 }141 if list, ok := v.(*starlark.List); ok {142 rows, err := inputRows(list)143 if err != nil {144 return nil, err145 }146 return starlark.None, r.host(func(h Host) error { return h.SetInputs(ref, rows) })147 }148 in, err := input("set", v)149 if err != nil {150 return nil, err151 }152 return starlark.None, r.host(func(h Host) error { return h.SetInput(ref, in) })153}154155// inputRows reads a list of rows, each a list of values.156func inputRows(list *starlark.List) ([][]string, error) {157 rows := make([][]string, list.Len())158 for i := range list.Len() {159 row, ok := list.Index(i).(*starlark.List)160 if !ok {161 return nil, fmt.Errorf("set: a range takes a list of rows, e.g. [[1, 2], [3, 4]]")162 }163 for j := range row.Len() {164 in, err := input("set", row.Index(j))165 if err != nil {166 return nil, err167 }168 rows[i] = append(rows[i], in)169 }170 }171 return rows, nil172}173174func (r *Run) setFormula(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {175 var ref, f string176 if err := unpack("set_formula", args, kwargs, "ref", &ref, "formula", &f); err != nil {177 return nil, err178 }179 if !strings.HasPrefix(f, "=") {180 f = "=" + f181 }182 return starlark.None, r.host(func(h Host) error { return h.SetFormula(ref, f) })183}184185func (r *Run) clear(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {186 var ref string187 if err := unpack("clear", args, kwargs, "ref?", &ref); err != nil {188 return nil, err189 }190 return starlark.None, r.host(func(h Host) error { return h.Clear(ref) })191}192193func (r *Run) numberFormat(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {194 var ref, kind, pattern string195 var decimals starlark.Value = starlark.None196 if err := unpack("number_format", args, kwargs, "ref", &ref, "kind", &kind, "decimals?", &decimals, "pattern?", &pattern); err != nil {197 return nil, err198 }199 d := -1200 if decimals != starlark.None {201 var err error202 if d, err = starlark.AsInt32(decimals); err != nil || d < 0 {203 return nil, fmt.Errorf("number_format: decimals is a whole number from 0, not %s", decimals)204 }205 }206 return starlark.None, r.host(func(h Host) error { return h.SetNumberFormat(ref, kind, d, pattern) })207}208209func (r *Run) getNumberFormat(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {210 var ref, kind string211 if err := unpack("get_number_format", args, kwargs, "ref", &ref); err != nil {212 return nil, err213 }214 err := r.host(func(h Host) (err error) { kind, _, err = h.NumberFormat(ref); return })215 return starlark.String(kind), err216}217218func (r *Run) selection(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {219 if err := unpack("selection", args, kwargs); err != nil {220 return nil, err221 }222 var sel string223 r.host(func(h Host) error { sel, _ = h.Selection(); return nil })224 return starlark.String(sel), nil225}226227func (r *Run) activeCell(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {228 if err := unpack("active_cell", args, kwargs); err != nil {229 return nil, err230 }231 var active string232 r.host(func(h Host) error { _, active = h.Selection(); return nil })233 return starlark.String(active), nil234}235236func (r *Run) selectRange(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {237 var ref, active string238 if err := unpack("select", args, kwargs, "ref", &ref, "active?", &active); err != nil {239 return nil, err240 }241 return starlark.None, r.host(func(h Host) error { return h.Select(ref, active) })242}243244func (r *Run) move(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {245 var cols, rows int246 if err := unpack("move", args, kwargs, "cols?", &cols, "rows?", &rows); err != nil {247 return nil, err248 }249 return starlark.None, r.host(func(h Host) error { return h.Move(cols, rows) })250}251252func (r *Run) extend(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {253 var cols, rows int254 var whole string255 if err := unpack("extend", args, kwargs, "cols?", &cols, "rows?", &rows, "whole?", &whole); err != nil {256 return nil, err257 }258 if whole != "" && whole != "columns" && whole != "rows" {259 return nil, fmt.Errorf("extend: whole is \"columns\" or \"rows\", not %q", whole)260 }261 return starlark.None, r.host(func(h Host) error { return h.Extend(cols, rows, whole) })262}263264func (r *Run) jump(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {265 var to string266 var ext bool267 if err := unpack("jump", args, kwargs, "to", &to, "extend?", &ext); err != nil {268 return nil, err269 }270 if !slices.Contains(JumpTargets, to) {271 return nil, fmt.Errorf("jump: to is one of %s, not %q", strings.Join(JumpTargets, ", "), to)272 }273 return starlark.None, r.host(func(h Host) error { return h.Jump(to, ext) })274}275276func (r *Run) enter(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {277 var v starlark.Value278 var fill bool279 var origin string280 if err := unpack("enter", args, kwargs, "text", &v, "fill?", &fill, "origin?", &origin); err != nil {281 return nil, err282 }283 text, err := input("enter", v)284 if err != nil {285 return nil, err286 }287 return starlark.None, r.host(func(h Host) error { return h.Enter(text, fill, origin) })288}289290func (r *Run) pasteText(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {291 var text string292 if err := unpack("paste_text", args, kwargs, "text", &text); err != nil {293 return nil, err294 }295 return starlark.None, r.host(func(h Host) error { return h.PasteText(text) })296}297298func (r *Run) sheets(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {299 if err := unpack("sheets", args, kwargs); err != nil {300 return nil, err301 }302 var names []string303 r.host(func(h Host) error { names = h.Sheets(); return nil })304 vals := make([]starlark.Value, len(names))305 for i, n := range names {306 vals[i] = starlark.String(n)307 }308 return starlark.NewList(vals), nil309}310311func (r *Run) activeSheet(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {312 if err := unpack("active_sheet", args, kwargs); err != nil {313 return nil, err314 }315 var name string316 r.host(func(h Host) error { name = h.ActiveSheet(); return nil })317 return starlark.String(name), nil318}319320func (r *Run) activateSheet(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {321 var name string322 if err := unpack("activate_sheet", args, kwargs, "name", &name); err != nil {323 return nil, err324 }325 return starlark.None, r.host(func(h Host) error { return h.ActivateSheet(name) })326}327328func (r *Run) addSheet(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {329 var name string330 if err := unpack("add_sheet", args, kwargs, "name?", &name); err != nil {331 return nil, err332 }333 var got string334 err := r.host(func(h Host) (err error) { got, err = h.AddSheet(name); return })335 return starlark.String(got), err336}337338func (r *Run) moveSheet(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {339 var pos int340 if err := unpack("move_sheet", args, kwargs, "position", &pos); err != nil {341 return nil, err342 }343 return starlark.None, r.host(func(h Host) error { return h.MoveSheet(pos) })344}345346func (r *Run) run(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {347 var id string348 var answer starlark.Value = starlark.None349 if err := unpack("run", args, kwargs, "id", &id, "answer?", &answer); err != nil {350 return nil, err351 }352 var a *string353 switch answer.(type) {354 case starlark.NoneType:355 case *starlark.Dict, *starlark.List, starlark.Tuple:356 text, err := answerJSON(answer)357 if err != nil {358 return nil, err359 }360 a = &text361 default:362 text, err := input("run", answer)363 if err != nil {364 return nil, err365 }366 a = &text367 }368 return starlark.None, r.host(func(h Host) error { return h.Run(id, a) })369}370371func (r *Run) setWidth(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {372 var cols string373 var width int374 if err := unpack("set_width", args, kwargs, "cols", &cols, "width", &width); err != nil {375 return nil, err376 }377 return starlark.None, r.host(func(h Host) error { return h.SetWidth(cols, width) })378}379380func (r *Run) setHeight(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {381 var rows string382 var height int383 if err := unpack("set_height", args, kwargs, "rows", &rows, "height", &height); err != nil {384 return nil, err385 }386 return starlark.None, r.host(func(h Host) error { return h.SetHeight(rows, height) })387}388389func (r *Run) fill(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {390 var to string391 var rows, cols int392 if err := unpack("fill", args, kwargs, "to?", &to, "rows?", &rows, "cols?", &cols); err != nil {393 return nil, err394 }395 if (to != "") == (rows != 0 || cols != 0) || rows != 0 && cols != 0 {396 return nil, fmt.Errorf("fill: give either to, or one of rows and cols")397 }398 return starlark.None, r.host(func(h Host) error { return h.Fill(to, rows, cols) })399}400401// offset is ref moved by cols and rows, keeping its sheet and size; it402// doesn't need the spreadsheet.403func offset(args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {404 var ref string405 var cols, rows int406 if err := unpack("offset", args, kwargs, "ref", &ref, "cols?", &cols, "rows?", &rows); err != nil {407 return nil, err408 }409 name, rest := formula.SplitSheet(ref)410 rng, ok := formula.ParseRange(rest)411 if !ok {412 return nil, fmt.Errorf("offset: not a cell or range: %s", ref)413 }414 rng.From.Col, rng.To.Col = rng.From.Col+cols, rng.To.Col+cols415 rng.From.Row, rng.To.Row = rng.From.Row+rows, rng.To.Row+rows416 if !rng.From.Valid() || !rng.To.Valid() {417 return nil, fmt.Errorf("offset: %s moved by %d, %d is off the sheet", ref, cols, rows)418 }419 out := rng.String()420 if rng.From == rng.To && !strings.Contains(rest, ":") {421 out = rng.From.String()422 }423 if name != "" {424 out = formula.QuoteSheet(name) + "!" + out425 }426 return starlark.String(out), nil427}428429// Functions lists the names of the functions scripts can call, sorted.430func Functions() []string {431 names := make([]string, 0, len(predeclared))432 for n := range predeclared {433 names = append(names, n)434 }435 slices.Sort(names)436 return names437}