1package ui23import (4 "errors"5 "fmt"6 "strconv"7 "strings"89 "src.rest/cat/012/internal/macro"10 "src.rest/cat/012/internal/sheet"11)1213// scriptHost is what a running macro acts on: the model, through the14// same paths a user's keys take (entries, the selection, commands). Its15// methods run on the UI goroutine, inside the run's undo step; see16// macrorun.go.17type scriptHost struct {18 m *Model19}2021var _ macro.Host = scriptHost{}2223// maxReadCells bounds what get and get_formula return at once.24const maxReadCells = 1 << 182526// resolve finds the sheet and range a script means: "" for the27// selection, A1 or A1:B3, whole columns (B:D) or rows (3:5), or a named28// range, any of them after a sheet name.29func (h scriptHost) resolve(ref string) (*sheet.Sheet, sheet.Rect, error) {30 m := h.m31 if strings.TrimSpace(ref) == "" {32 return m.sheet, m.selection(), nil33 }34 name, rest := sheet.SplitSheet(ref)35 s := m.sheet36 if name != "" {37 if s = m.book().Lookup(name); s == nil {38 return nil, sheet.Rect{}, fmt.Errorf("there's no sheet named %s", name)39 }40 }41 if r, ok := parseWhole(rest); ok {42 return s, r, nil43 }44 if n, ok := m.sheet.LookupName(rest); ok && name == "" {45 if n.Gone() {46 return nil, sheet.Rect{}, fmt.Errorf("the range named %s was deleted", n.Name)47 }48 return n.Sheet, n.Range, nil49 }50 return nil, sheet.Rect{}, fmt.Errorf("not a cell or range: %s", ref)51}5253// parseWhole parses A1, A1:B3, whole columns B:D and whole rows 3:5.54func parseWhole(s string) (sheet.Rect, bool) {55 if r, ok := sheet.ParseRange(s); ok {56 return r, true57 }58 from, to, ok := strings.Cut(strings.TrimSpace(s), ":")59 if !ok {60 return sheet.Rect{}, false61 }62 if a, ok1 := sheet.ParseCol(strings.ToUpper(from)); ok1 {63 if b, ok2 := sheet.ParseCol(strings.ToUpper(to)); ok2 {64 return sheet.NewRect(sheet.Addr{Col: a}, sheet.Addr{Col: b, Row: sheet.MaxRows - 1}), true65 }66 }67 a, err1 := strconv.Atoi(from)68 b, err2 := strconv.Atoi(to)69 if err1 != nil || err2 != nil || a < 1 || b < 1 || a > sheet.MaxRows || b > sheet.MaxRows {70 return sheet.Rect{}, false71 }72 return sheet.NewRect(sheet.Addr{Row: a - 1}, sheet.Addr{Col: sheet.MaxCols - 1, Row: b - 1}), true73}7475// wholeRef writes r as a script would: B:D for whole columns, 3:5 for76// whole rows, otherwise A1 or A1:B3.77func wholeRef(r sheet.Rect) string {78 allRows := r.From.Row == 0 && r.To.Row == sheet.MaxRows-179 allCols := r.From.Col == 0 && r.To.Col == sheet.MaxCols-180 switch {81 case allRows && !allCols:82 return sheet.ColName(r.From.Col) + ":" + sheet.ColName(r.To.Col)83 case allCols && !allRows:84 return strconv.Itoa(r.From.Row+1) + ":" + strconv.Itoa(r.To.Row+1)85 }86 return r.String()87}8889// readRange is r to read: a large range (whole columns, say) is trimmed90// to its last row and column with contents. Small ones are read whole,91// blanks and all, as written.92func readRange(s *sheet.Sheet, r sheet.Rect) (sheet.Rect, error) {93 if (r.To.Col-r.From.Col+1)*(r.To.Row-r.From.Row+1) <= 1<<12 {94 return r, nil95 }96 data, ok := s.FilledBounds(r)97 r.To = r.From98 if ok {99 r.To = data.To100 }101 if (r.To.Col-r.From.Col+1)*(r.To.Row-r.From.Row+1) > maxReadCells {102 return r, fmt.Errorf("%s has more than %d cells to read at once", r, maxReadCells)103 }104 return r, nil105}106107// each calls fn for the cells of ref, row by row, to read them.108func (h scriptHost) each(ref string, fn func(s *sheet.Sheet, a sheet.Addr, row int)) error {109 s, r, err := h.resolve(ref)110 if err != nil {111 return err112 }113 if r, err = readRange(s, r); err != nil {114 return err115 }116 for row := r.From.Row; row <= r.To.Row; row++ {117 for col := r.From.Col; col <= r.To.Col; col++ {118 fn(s, sheet.Addr{Col: col, Row: row}, row-r.From.Row)119 }120 }121 return nil122}123124func (h scriptHost) Values(ref string) ([][]any, error) {125 var rows [][]any126 err := h.each(ref, func(s *sheet.Sheet, a sheet.Addr, row int) {127 if row == len(rows) {128 rows = append(rows, nil)129 }130 rows[row] = append(rows[row], scriptValue(s.Value(a)))131 })132 return rows, err133}134135// scriptValue is a value as scripts see it.136func scriptValue(v sheet.Value) any {137 switch v.Kind {138 case sheet.Number:139 return v.Num140 case sheet.Text, sheet.Error:141 return v.Str142 case sheet.Bool:143 return v.Num != 0144 }145 return nil146}147148func (h scriptHost) Formulas(ref string) ([][]string, error) {149 var rows [][]string150 err := h.each(ref, func(s *sheet.Sheet, a sheet.Addr, row int) {151 if row == len(rows) {152 rows = append(rows, nil)153 }154 f := ""155 if c := s.Cell(a); c != nil && c.IsFormula() {156 f = c.Input157 }158 rows[row] = append(rows[row], f)159 })160 return rows, err161}162163func (h scriptHost) SetInput(ref, input string) error {164 s, r, err := h.resolve(ref)165 if err != nil {166 return err167 }168 if n := (r.To.Col - r.From.Col + 1) * (r.To.Row - r.From.Row + 1); n > maxReadCells {169 return fmt.Errorf("%s has more than %d cells to set at once", ref, maxReadCells)170 }171 return s.Batch(sheet.Change{Label: "set " + r.String(), Focus: r}, func() error {172 for row := r.From.Row; row <= r.To.Row; row++ {173 for col := r.From.Col; col <= r.To.Col; col++ {174 if err := s.Set(sheet.Addr{Col: col, Row: row}, input); err != nil {175 return fmt.Errorf("%s: %w", sheet.Addr{Col: col, Row: row}, err)176 }177 }178 }179 return nil180 })181}182183func (h scriptHost) SetInputs(ref string, rows [][]string) error {184 s, r, err := h.resolve(ref)185 if err != nil {186 return err187 }188 return s.Batch(sheet.Change{Label: "set " + r.String(), Focus: r}, func() error {189 for i, row := range rows {190 for j, input := range row {191 a := sheet.Addr{Col: r.From.Col + j, Row: r.From.Row + i}192 if !a.Valid() {193 return fmt.Errorf("the values run off the sheet at row %d, column %d", i+1, j+1)194 }195 if err := s.Set(a, input); err != nil {196 return fmt.Errorf("%s: %w", a, err)197 }198 }199 }200 return nil201 })202}203204func (h scriptHost) SetFormula(ref, f string) error {205 s, r, err := h.resolve(ref)206 if err != nil {207 return err208 }209 return s.FillEntry(r, r.From, f)210}211212func (h scriptHost) Clear(ref string) error {213 s, r, err := h.resolve(ref)214 if err != nil {215 return err216 }217 s.EraseRange(r)218 return nil219}220221func (h scriptHost) NumberFormat(ref string) (string, int, error) {222 s, r, err := h.resolve(ref)223 if err != nil {224 return "", 0, err225 }226 f := s.DisplayFormat(r.From)227 return f.Kind.String(), f.Decimals, nil228}229230func (h scriptHost) SetNumberFormat(ref, kind string, decimals int, pattern string) error {231 s, r, err := h.resolve(ref)232 if err != nil {233 return err234 }235 k, ok := sheet.ParseFormatKind(kind)236 if !ok {237 return fmt.Errorf("no number format %q; try number, percent, currency, date, time or custom", kind)238 }239 f := sheet.Preset(k)240 if decimals >= 0 {241 f.Decimals = min(decimals, sheet.MaxDecimals)242 }243 if k == sheet.FmtCustom && pattern == "" {244 return errors.New("a custom format needs a pattern, e.g. pattern=\"0.0%\"")245 }246 f.Pattern = pattern247 s.SetFormat(r, f)248 return nil249}