cat / 012
clone

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

List rule screens, and order rule menu items as Sheets does

Dami committed 87ad117
Data validation follows the named ranges in the Data menu and Checkbox
and Dropdown follow Chart in the Insert menu; tests reaching items by
letter or from the end of a menu step past them. The rules panel says
how rules combine: in order for conditional formats, one per cell for
validation.
commit
87ad1175b6575cdb184746ee099f880d2d7ba66e
parent
83650221725fa24a63adca746fec1ae890c173d2
author
Dami <dami.etoile@gmail.com>
8 files changed, +143 -15
Me2e/charts_test.go2+-
Me2e/formula_help_test.go2+-
Ae2e/screenrules_test.go120++++++++++++++++++++++++++++++
Me2e/screensetup_test.go2+-
Minternal/ui/menubar.go2+-
Minternal/ui/rules/condfmt.go4+-
Minternal/ui/rules/editor.go13++-
Minternal/ui/rules/validation.go13++--
M e2e/charts_test.go +1 -1
M e2e/formula_help_test.go +1 -1
A e2e/screenrules_test.go +120 -0
@@ -0,0 +1,120 @@
1+package e2e2+3+import (4+ "os"5+ "path/filepath"6+ "testing"7+)8+9+// tasksBook is a task list with rules on every column: owners from a10+// dropdown (one of them not on its list), hours on a color scale, done11+// tasks struck through in green, and checkboxes.12+const tasksBook = `{13+ "version": 2,14+ "widths": {"A": 14, "B": 10, "C": 8, "D": 7},15+ "cells": {16+ "A1": {"input":"Task","bold":true},17+ "B1": {"input":"Owner","bold":true},18+ "C1": {"input":"Hours","bold":true,"align":"right"},19+ "D1": {"input":"Done","bold":true,"align":"center"},20+ "A2": "Write spec", "B2": "Ann", "C2": "12", "D2": "TRUE",21+ "A3": "Build UI", "B3": "Bo", "C3": "40", "D3": "FALSE",22+ "A4": "Tests", "B4": "Cy", "C4": "26", "D4": "TRUE",23+ "A5": "Docs", "B5": "Dee", "C5": "6",24+ "A6": "Release", "B6": "Ann", "C6": "3", "D6": "FALSE",25+ "A7": "Total", "C7": "=SUM(C2:C6)"26+ },27+ "conditionalFormats": [28+ {"ranges":"A2:A6","condition":"formula","values":["=$D2"],"text":"green","strikethrough":true},29+ {"ranges":"C2:C6","scale":[{"type":"min","color":"green"},{"type":"percentile","value":"50","color":"yellow"},{"type":"max","color":"red"}]},30+ {"ranges":"C7","condition":"gt","values":["80"],"fill":"magenta","bold":true}31+ ],32+ "validations": [33+ {"ranges":"B2:B6","criteria":"list","items":["Ann","Bo","Cy"],"help":"Pick who does it"},34+ {"ranges":"D2:D6","criteria":"checkbox"},35+ {"ranges":"C2:C6","criteria":"number","condition":"between","values":["0","80"],"reject":true}36+ ]37+}38+`39+40+func tasksFile(t *testing.T, dir string) {41+ if err := os.WriteFile(filepath.Join(dir, "tasks.012"), []byte(tasksBook), 0o644); err != nil {42+ t.Fatal(err)43+ }44+}45+46+// openTasks opens the task list.47+func openTasks(s *session) {48+ s.keys("<ctrl+o>")49+ s.waitFor("Open file:")50+ s.keys("tasks", "<enter>")51+ s.waitFor("Write spec")52+}53+54+var ruleScreens = []screen{55+ {name: "rules-sheet", files: tasksFile, setup: func(s *session) {56+ openTasks(s)57+ s.keys("<down>", "<down>", "<down>", "<down>", "<right>")58+ s.waitFor("Invalid: Pick who does it")59+ }},60+ {name: "rules-sheet-dracula", opts: options{config: "theme = Dracula\n"}, files: tasksFile, setup: func(s *session) {61+ openTasks(s)62+ s.keys("<down>", "<down>", "<right>", "<right>", "<right>")63+ s.waitFor("check or uncheck")64+ }},65+ {name: "rules-sheet-latte", opts: options{light: true, config: "theme = Catppuccin Latte\n"}, files: tasksFile, setup: func(s *session) {66+ openTasks(s)67+ s.keys("<down>", "<down>", "<right>", "<right>", "<right>")68+ s.waitFor("check or uncheck")69+ }},70+ {name: "rules-panel", files: tasksFile, setup: func(s *session) {71+ openTasks(s)72+ s.keys("<ctrl+k>", "conditional formatting", "<enter>")73+ s.waitFor("Conditional format rules")74+ s.keys("<down>", "<down>")75+ s.waitFor("first rule that matches")76+ }},77+ {name: "rules-editor", files: tasksFile, setup: func(s *session) {78+ openTasks(s)79+ s.keys("<ctrl+k>", "conditional formatting", "<enter>")80+ s.waitFor("Conditional format rules")81+ s.keys("<down>", "<enter>")82+ s.waitFor("Edit conditional format")83+ }},84+ {name: "rules-editor-narrow", opts: options{cols: 60, rows: 16}, files: tasksFile, setup: func(s *session) {85+ openTasks(s)86+ s.keys("<ctrl+k>", "conditional formatting", "<enter>")87+ s.waitFor("Conditional format rules")88+ s.keys("<down>", "<down>", "<enter>")89+ s.waitFor("Minpoint")90+ }},91+ {name: "validation-editor", files: tasksFile, setup: func(s *session) {92+ openTasks(s)93+ s.keys("<ctrl+k>", "data validation", "<enter>")94+ s.waitFor("Data validation rules")95+ s.keys("<down>", "<enter>")96+ s.waitFor("Edit data validation")97+ }},98+ {name: "dropdown-open", files: tasksFile, setup: func(s *session) {99+ openTasks(s)100+ s.keys("<down>", "<down>", "<right>", "<alt+down>")101+ s.waitFor("Search the items")102+ }},103+ {name: "validation-reject", files: tasksFile, setup: func(s *session) {104+ openTasks(s)105+ s.keys("<down>", "<right>", "<right>", "120", "<enter>")106+ s.waitFor("Invalid entry in C2")107+ }},108+}109+110+func init() {111+ light := map[string]bool{"rules-sheet": true, "rules-panel": true, "rules-editor": true, "validation-editor": true, "dropdown-open": true}112+ for _, sc := range ruleScreens {113+ screens = append(screens, sc)114+ if light[sc.name] {115+ sc.name += "-light"116+ sc.opts.light = true117+ screens = append(screens, sc)118+ }119+ }120+}
M e2e/screensetup_test.go +1 -1
M internal/ui/menubar.go +1 -1
M internal/ui/rules/condfmt.go +3 -1
M internal/ui/rules/editor.go +9 -4
@@ -51,7 +51,10 @@ type kind interface {
5151 remove(s *sheet.Sheet, i int)5252 // move moves rule i to to, and reports whether rules have an order.5353 move(s *sheet.Sheet, i, to int) bool54- addHint() string54+ // ordered reports whether the rules' order matters, and listHint55+ // says how rules combine, for the status line.56+ ordered() bool57+ listHint() string5558 }5659 5760 // form is a rule being added or edited.
@@ -311,7 +314,7 @@ func (e *Editor) listLines(inner int) []string {
311314 base = th.MenuSelected312315 }313316 if i == 0 {314- lines = append(lines, spread(base, base.Render(" + Add rule"), th.Muted.Inherit(base).Render(e.k.addHint()+" "), inner))317+ lines = append(lines, theme.Cells(base, " + Add rule", inner))315318 continue316319 }317320 lines = append(lines, e.k.item(th, e.h, i-1, inner, i == e.list.Sel))
@@ -348,8 +351,10 @@ func (e *Editor) Status() (string, string) {
348351 case e.f == nil && e.list.Sel == 0:349352 desc, pairs = "Add a rule over the selection", []string{"Enter", "add", "Esc", "close"}350353 case e.f == nil:351- desc, pairs = "Rules apply in order: the first that matches a cell formats it",352- []string{"Enter", "edit", "Del", "remove", "Shift+↑/↓", "move", "Esc", "close"}354+ desc, pairs = e.k.listHint(), []string{"Enter", "edit", "Del", "remove", "Esc", "close"}355+ if e.k.ordered() {356+ pairs = []string{"Enter", "edit", "Del", "remove", "Shift+↑/↓", "move", "Esc", "close"}357+ }353358 default:354359 r := e.rows()[e.sel]355360 desc = r.hint
M internal/ui/rules/validation.go +7 -6