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 / cowork / board.go
board.go 6.2K
1package cowork23import (4 "errors"5 "fmt"6 "slices"7 "time"89 "src.rest/cat/012/internal/room"10 "src.rest/cat/012/internal/sheet"11)1213// BoardKey is the room value (room.Seat.Value) holding its Board.14const BoardKey = "agents"1516// BoardOf is the Board of seat's room, made the first time. Call it in17// a turn.18func BoardOf(seat *room.Seat) *Board {19 return seat.Value(BoardKey, func() any { return &Board{} }).(*Board)20}2122// Board is what a room shares of its agents: their suggestions, their23// questions to the people, and what the people let them do.24type Board struct {25 // Scope is what agents may change, as the person who invited them26 // chose; the whole workbook in 012 serve.27 Scope Scope28 // Direct lets agents' changes be made at once, as their own steps,29 // rather than suggested ("Let agents edit directly").30 Direct bool31 Grants Grants32 // Host is the seat of the person who invited the agents, who is33 // asked their questions; 0 in 012 serve, where any person is.34 Host int3536 list []*Suggestion // oldest first37 next int38 asks []*Ask39 nextAsk int40}4142// CellState is what became of one cell of a suggestion.43type CellState int4445const (46 CellPending CellState = iota47 CellAccepted48 CellRejected49)5051// Suggestion is one of the agents's change waiting for a person: the proposal,52// who made it and why, and what became of each of its cells.53type Suggestion struct {54 ID int55 Author int // the agent's seat56 Agent string // its name57 Color int58 Message string59 At time.Time60 *sheet.Proposal61 // States are its cells', in the order of Proposal.Cells; a change62 // made whole only has one, for all of it.63 States []CellState64 // Failed is why accepting it failed, when it did.65 Failed string66 reported bool67}6869// Pending reports whether any of it waits for the person.70func (s *Suggestion) Pending() bool { return slices.Contains(s.States, CellPending) }7172// Count is how many of its cells are in state st.73func (s *Suggestion) Count(st CellState) int {74 n := 075 for _, x := range s.States {76 if x == st {77 n++78 }79 }80 return n81}8283// Outcome is what became of it in words: pending, accepted, accepted in84// part or rejected.85func (s *Suggestion) Outcome() string {86 switch acc, rej := s.Count(CellAccepted), s.Count(CellRejected); {87 case s.Pending():88 return "pending"89 case rej == 0:90 return "accepted"91 case acc == 0:92 return "rejected"93 }94 return "accepted in part"95}9697// Add puts a suggestion on the board and numbers it.98func (b *Board) Add(s *Suggestion) int {99 b.next++100 s.ID = b.next101 if s.At.IsZero() {102 s.At = time.Now()103 }104 n := len(s.Cells)105 if s.Whole() || n == 0 {106 n = 1107 }108 s.States = make([]CellState, n)109 b.list = append(b.list, s)110 return s.ID111}112113// Pending are the suggestions still waiting, oldest first.114func (b *Board) Pending() []*Suggestion {115 var out []*Suggestion116 for _, s := range b.list {117 if s.Pending() {118 out = append(out, s)119 }120 }121 return out122}123124// Get is the suggestion numbered id.125func (b *Board) Get(id int) *Suggestion {126 for _, s := range b.list {127 if s.ID == id {128 return s129 }130 }131 return nil132}133134// Of are the suggestions agent's seat made, oldest first.135func (b *Board) Of(author int) []*Suggestion {136 var out []*Suggestion137 for _, s := range b.list {138 if s.Author == author {139 out = append(out, s)140 }141 }142 return out143}144145// At is the newest pending suggestion setting the cell a of the sheet146// named name, and the index of the cell in it.147func (b *Board) At(name string, a sheet.Addr) (*Suggestion, int, bool) {148 for i := len(b.list) - 1; i >= 0; i-- {149 s := b.list[i]150 for j, c := range s.Cells {151 if c.Sheet == name && c.At == a && s.state(j) == CellPending {152 return s, j, true153 }154 }155 }156 return nil, 0, false157}158159// state is cell i's state; all of a whole change's cells are its one.160func (s *Suggestion) state(i int) CellState {161 if s.Whole() {162 return s.States[0]163 }164 return s.States[i]165}166167// ErrGone is accepting or rejecting a suggestion no longer waiting.168var ErrGone = errors.New("that suggestion isn't waiting any more")169170// Accept makes the cells of suggestion id that are picked (nil: all it171// has pending) on w, as one step in the agent's name, and marks them172// accepted. A change made whole only is accepted whole.173func (b *Board) Accept(w *sheet.Workbook, id int, cells []int) error {174 s := b.Get(id)175 if s == nil || !s.Pending() {176 return ErrGone177 }178 if s.Whole() && cells != nil {179 return sheet.ErrWhole180 }181 pick := b.picked(s, cells)182 author := w.Author()183 w.SetAuthor(s.Author)184 err := s.Apply(w, func(i int) bool { return pick[i] })185 w.SetAuthor(author)186 if err != nil {187 s.Failed = err.Error()188 return fmt.Errorf("accepting %s's suggestion: %w", s.Agent, err)189 }190 b.mark(s, pick, CellAccepted)191 return nil192}193194// Reject marks the cells of suggestion id picked (nil: all it has195// pending) rejected, changing nothing.196func (b *Board) Reject(id int, cells []int) error {197 s := b.Get(id)198 if s == nil || !s.Pending() {199 return ErrGone200 }201 b.mark(s, b.picked(s, cells), CellRejected)202 return nil203}204205// picked are the pending cells of s among cells, all of them for nil.206func (b *Board) picked(s *Suggestion, cells []int) map[int]bool {207 pick := map[int]bool{}208 for i := range s.Cells {209 if s.state(i) == CellPending && (cells == nil || slices.Contains(cells, i)) {210 pick[i] = true211 }212 }213 return pick214}215216// mark sets the picked cells' state.217func (b *Board) mark(s *Suggestion, pick map[int]bool, st CellState) {218 if s.Whole() || len(s.Cells) == 0 {219 s.States[0] = st220 return221 }222 for i := range pick {223 s.States[i] = st224 }225}226227// Resolved are the agent's suggestions settled since it last asked,228// which it hasn't been told of.229func (b *Board) Resolved(author int) []*Suggestion {230 var out []*Suggestion231 for _, s := range b.list {232 if s.Author == author && !s.Pending() && !s.reported {233 s.reported = true234 out = append(out, s)235 }236 }237 return out238}239240// Notice is what the agent is told of a settled suggestion.241func (s *Suggestion) Notice() string {242 what := fmt.Sprintf("Suggestion %d (%s) was %s", s.ID, s.Label, s.Outcome())243 if n := len(s.Cells); n > 1 && s.Outcome() == "accepted in part" {244 what += fmt.Sprintf(": %d of its %d cells", s.Count(CellAccepted), n)245 }246 if s.Failed != "" {247 what += "; accepting it failed: " + s.Failed248 }249 return what250}