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 / ui / rulemacro.go
rulemacro.go 4.7K
1package ui23import (4 "encoding/json"5 "fmt"6 "strconv"7 "strings"89 tea "charm.land/bubbletea/v2"1011 "src.rest/cat/012/internal/sheet"12)1314// Rules in macros: what the rules panel does is recorded as the commands15// that do it from a line of the file, and those commands are how scripts16// change rules. A rule is numbered from 1 in the panel's order.17//18// run("format.conditional_add", answer={"ranges": "B2:B9", ...}) a rule added19// run("format.conditional_set", answer={"rule": 2, "ranges": ...}) rule 2 replaced20// run("format.conditional_remove", answer=2) rule 2 removed21// run("format.conditional_move", answer={"rule": 2, "to": 1}) rule 2 moved first22//23// and the same for data validation, whose rules have no order.2425func init() {26 hasFormats := func(m *Model) bool { return len(m.sheet.CondFormats()) > 0 }27 hasValidations := func(m *Model) bool { return len(m.sheet.Validations()) > 0 }28 register(29 &command{id: "format.conditional_set", title: "Replace conditional format rule", enabled: hasFormats,30 desc: `Replace a rule with one written as a line of the file, numbered: {"rule":2,"ranges":"B2:B9",...}`,31 run: func(m *Model) tea.Cmd {32 m.askRule(func(m *Model, text string) error {33 i, err := ruleNumber(text, len(m.sheet.CondFormats()))34 if err != nil {35 return err36 }37 f, err := sheet.ParseCondFormat(text)38 if err != nil {39 return err40 }41 return m.host().SaveFormat(i, f)42 })43 return nil44 }},45 &command{id: "format.conditional_remove", title: "Remove conditional format rule", enabled: hasFormats,46 desc: "Remove a rule by its number in the list, counting from 1",47 run: func(m *Model) tea.Cmd {48 m.askRule(func(m *Model, text string) error {49 i, err := ruleNumber(text, len(m.sheet.CondFormats()))50 if err == nil {51 m.sheet.DeleteCondFormat(i)52 m.changed = true53 }54 return err55 })56 return nil57 }},58 &command{id: "format.conditional_move", title: "Move conditional format rule", enabled: hasFormats,59 desc: `Move a rule to another place in the list, where the first rule that matches wins: {"rule":2,"to":1}`,60 run: func(m *Model) tea.Cmd {61 m.askRule(func(m *Model, text string) error {62 n := len(m.sheet.CondFormats())63 i, err := ruleNumber(text, n)64 if err != nil {65 return err66 }67 var a struct {68 To int `json:"to"`69 }70 if json.Unmarshal([]byte(text), &a); a.To < 1 || a.To > n {71 return fmt.Errorf(`give "to", a place from 1 to %d`, n)72 }73 m.sheet.MoveCondFormat(i, a.To-1)74 m.changed = true75 return nil76 })77 return nil78 }},79 &command{id: "data.validation_set", title: "Replace data validation rule", enabled: hasValidations,80 desc: `Replace a rule with one written as a line of the file, numbered: {"rule":2,"ranges":"D2:D9",...}`,81 run: func(m *Model) tea.Cmd {82 m.askRule(func(m *Model, text string) error {83 i, err := ruleNumber(text, len(m.sheet.Validations()))84 if err != nil {85 return err86 }87 v, err := sheet.ParseValidation(text)88 if err != nil {89 return err90 }91 return m.host().SaveValidation(i, v)92 })93 return nil94 }},95 &command{id: "data.validation_remove", title: "Remove data validation rule", enabled: hasValidations,96 desc: "Remove a rule by its number in the list, counting from 1",97 run: func(m *Model) tea.Cmd {98 m.askRule(func(m *Model, text string) error {99 i, err := ruleNumber(text, len(m.sheet.Validations()))100 if err == nil {101 m.sheet.DeleteValidation(i)102 m.changed = true103 }104 return err105 })106 return nil107 }},108 )109}110111// askRule asks for a rule's number, or a rule, on the context line, and112// hands it to do.113func (m *Model) askRule(do func(m *Model, text string) error) {114 m.openText("Rule:", "", func(m *Model, text string) tea.Cmd {115 m.failOn(do(m, text))116 return nil117 })118}119120// ruleNumber reads which of n rules an answer names, counting from 1:121// the number itself, or "rule" in a line of the file.122func ruleNumber(text string, n int) (int, error) {123 i, err := strconv.Atoi(strings.TrimSpace(text))124 if err != nil {125 var a struct {126 Rule int `json:"rule"`127 }128 if json.Unmarshal([]byte(text), &a) != nil || a.Rule == 0 {129 return 0, fmt.Errorf(`give the rule's number, from 1 to %d: {"rule":1,...}`, n)130 }131 i = a.Rule132 }133 if i < 1 || i > n {134 return 0, fmt.Errorf("there's no rule %d: the sheet has %d", i, n)135 }136 return i - 1, nil137}138139// numbered is a rule's line with its number first, as the commands that140// replace or move a rule take it.141func numbered(i int, line string) string {142 rest := strings.TrimPrefix(line, "{")143 if rest != "}" {144 rest = "," + rest145 }146 return `{"rule":` + strconv.Itoa(i+1) + rest147}