cat / 012
clone

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

main
012 / internal / macro / macro.go
macro.go 6.6K
1// Package macro runs macros: Starlark scripts that act on a spreadsheet2// through a small API, as Google Sheets macros are Apps Script. A3// recorded macro is a list of Actions written out as a script (Source),4// so recorded and hand-written macros run the same way.5//6// Scripts can't reach the file system, the network, the clock or random7// numbers: the only way out is the Host. Every run has a step limit and8// can be cancelled from another goroutine. The package knows nothing of9// the terminal or the engine; internal/ui implements Host.10package macro1112import (13 "fmt"14 "strconv"15 "strings"1617 "go.starlark.net/syntax"18)1920// Host is the spreadsheet a script acts on. References are A1 strings,21// optionally with a sheet ("Sheet2!B3:C9"); a range is a cell or a22// rectangle. Its methods are only called inside Env.Do.23type Host interface {24 // Values returns the computed values of ref, row by row: nil for a25 // blank, float64, string, bool, or an error value's code as a string26 // ("#DIV/0!").27 Values(ref string) ([][]any, error)28 // Formulas returns what was typed in each cell of ref, row by row,29 // when it's a formula, and "" otherwise.30 Formulas(ref string) ([][]string, error)31 // SetInput enters input, as if typed, in every cell of ref.32 SetInput(ref, input string) error33 // SetInputs enters a block of inputs, as if typed, from ref's first34 // cell.35 SetInputs(ref string, rows [][]string) error36 // SetFormula enters formula in ref's first cell and fills it over the37 // rest, adjusting relative references as a copy does.38 SetFormula(ref, formula string) error39 // Clear erases the contents of ref, or of the selection when ref is "".40 Clear(ref string) error41 // NumberFormat is the number format of ref's first cell: its kind42 // ("auto", "number", "currency"...) and decimals.43 NumberFormat(ref string) (kind string, decimals int, err error)44 // SetNumberFormat formats ref; decimals < 0 keeps the kind's default,45 // and pattern is for the "custom" kind.46 SetNumberFormat(ref, kind string, decimals int, pattern string) error4748 // Selection is the selected range and the active cell in it.49 Selection() (sel, active string)50 // Select selects ref, switching sheets if it names one, with active51 // as the active cell ("" for ref's first cell).52 Select(ref, active string) error53 // Move moves the active cell by cols and rows and drops the54 // selection.55 Move(cols, rows int) error56 // Extend selects from the active cell to cols and rows away from it;57 // whole is "", "columns" or "rows".58 Extend(cols, rows int, whole string) error59 // Jump moves as Ctrl+arrows, Home, Ctrl+Home or Ctrl+End do (see60 // JumpTargets), extending the selection when extend is set.61 Jump(to string, extend bool) error62 // Enter stores text in the active cell as if typed and accepted, or63 // in every selected cell, references adjusted, when fill is set. With64 // an origin, a formula is taken as typed there and moved to the65 // active cell, relative references following, as in a copy.66 Enter(text string, fill bool, origin string) error67 // PasteText pastes tab-separated text at the active cell, as pasting68 // from another program does.69 PasteText(text string) error7071 // Sheets lists the sheet names in tab order.72 Sheets() []string73 // ActiveSheet names the sheet shown.74 ActiveSheet() string75 // ActivateSheet shows the named sheet.76 ActivateSheet(name string) error77 // AddSheet adds a sheet after the one shown and shows it, returning78 // its name; name "" picks the next free one.79 AddSheet(name string) (string, error)80 // MoveSheet moves the sheet shown to position pos, counting from 1.81 MoveSheet(pos int) error8283 // Run runs a registered command by id. A command that asks a84 // question (a width, a name, a confirmation) gets answer, which is85 // required then; one that opens a dialog gets the dialog's choices86 // as JSON text, from a dict or list the script gave.87 Run(id string, answer *string) error88 // SetWidth sets the width of the columns cols, e.g. "B" or "B:D".89 SetWidth(cols string, width int) error90 // SetHeight sets the height of the rows rows, e.g. "3" or "3:5", in91 // lines; 0 fits them to their contents.92 SetHeight(rows string, height int) error93 // Fill fills from the selection as dragging the fill handle does: to94 // the range to, or by rows (down, or up when negative) or cols.95 Fill(to string, rows, cols int) error96}9798// JumpTargets are the places Jump goes: the edge of the data in a99// direction (Ctrl+arrows), the row's first column (Home), A1 (Ctrl+Home)100// and the last used cell (Ctrl+End).101var JumpTargets = []string{"up", "down", "left", "right", "home", "start", "end"}102103// Action is one step of a recording: a call of one of the script's104// functions, or a comment when Func is "". It is plain data, so a log105// can be kept, compared or saved before it becomes a script.106type Action struct {107 Func string `json:"func,omitempty"`108 Args []any `json:"args,omitempty"` // string, int, bool or JSON109 Named []Arg `json:"named,omitempty"`110 Comment string `json:"comment,omitempty"`111}112113// Arg is a named argument, e.g. fill=True.114type Arg struct {115 Name string `json:"name"`116 Value any `json:"value"`117}118119// Call is a shorthand for an Action calling fn with args.120func Call(fn string, args ...any) Action { return Action{Func: fn, Args: args} }121122// Note is a comment in a recording.123func Note(text string) Action { return Action{Comment: text} }124125// With adds a named argument.126func (a Action) With(name string, v any) Action {127 a.Named = append(a.Named, Arg{name, v})128 return a129}130131// String is the action as a line of Starlark.132func (a Action) String() string {133 if a.Func == "" {134 return "# " + strings.ReplaceAll(a.Comment, "\n", " ")135 }136 parts := make([]string, 0, len(a.Args)+len(a.Named))137 for _, v := range a.Args {138 parts = append(parts, literal(v))139 }140 for _, n := range a.Named {141 parts = append(parts, n.Name+"="+literal(n.Value))142 }143 return a.Func + "(" + strings.Join(parts, ", ") + ")"144}145146// literal writes a Go value as a Starlark literal.147func literal(v any) string {148 switch v := v.(type) {149 case string:150 return syntax.Quote(v, false)151 case int:152 return strconv.Itoa(v)153 case bool:154 if v {155 return "True"156 }157 return "False"158 case nil:159 return "None"160 case JSON:161 lit, err := starlarkLiteral(string(v))162 if err != nil {163 panic(fmt.Sprintf("macro: %v", err))164 }165 return lit166 }167 panic(fmt.Sprintf("macro: no literal for %T", v))168}169170// Source writes a recording as a script, one action per line, under a171// header comment.172func Source(header string, actions []Action) string {173 var b strings.Builder174 for _, l := range strings.Split(header, "\n") {175 b.WriteString(strings.TrimRight("# "+l, " ") + "\n")176 }177 for _, a := range actions {178 b.WriteString(a.String() + "\n")179 }180 return b.String()181}