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 / rooms.go
rooms.go 4.7K
1package ui23import (4 "context"56 tea "charm.land/bubbletea/v2"78 "src.rest/cat/012/internal/sheet"9)1011// Shared is a served session's guarded model as Bubble Tea runs it when12// workbooks are shared: every turn the model takes (Init, Update, View)13// holds its room's lock (room.Seat.Do), with its steps made in its own14// name, so sessions act on the workbook one at a time, in the order the15// server takes them. Between turns it joins and leaves rooms, never16// holding two locks. It also holds back input while someone else's step17// is open (a macro they run spans several turns) and hands it on once18// the step ends, and in a one-writer room it takes back what a19// follower changed by a way no check stopped.20type Shared struct {21 g *Guarded22}2324// InRooms runs g's model in the rooms it was given (Model.ShareRooms).25func InRooms(g *Guarded) *Shared { return &Shared{g: g} }2627// Attach sends the program a roomMsg whenever the model's room changes,28// until ctx ends.29func (s *Shared) Attach(ctx context.Context, p *tea.Program) {30 link := s.g.m.share.link31 go func() {32 for {33 select {34 case <-ctx.Done():35 return36 case <-link.kick:37 p.Send(roomMsg{})38 }39 }40 }()41}4243// Init implements tea.Model.44func (s *Shared) Init() tea.Cmd {45 var cmd tea.Cmd46 s.turn(func() { cmd = s.g.Init() })47 return tea.Batch(cmd, s.between())48}4950// Update implements tea.Model.51func (s *Shared) Update(msg tea.Msg) (tea.Model, tea.Cmd) {52 var cmd tea.Cmd53 s.turn(func() { cmd = s.update(msg) })54 return s, tea.Batch(cmd, s.between())55}5657// View implements tea.Model.58func (s *Shared) View() tea.View {59 var v tea.View60 s.turn(func() { v = s.g.View() })61 return v62}6364// update is a turn's Update: input held while another's step is open,65// and handed on once it ends.66func (s *Shared) update(msg tea.Msg) tea.Cmd {67 m := s.g.m68 seat := m.share.seat69 if seat != nil && isInput(msg) {70 if open, who := seat.Book().InStep(); open && who != seat.ID() {71 m.share.held = append(m.share.held, msg)72 return nil73 }74 }75 _, cmd := s.g.Update(msg)76 if _, ok := msg.(roomMsg); !ok || len(m.share.held) == 0 {77 return cmd78 }79 if open, _ := seat.Book().InStep(); open {80 return cmd81 }82 held := m.share.held83 m.share.held = nil84 cmds := []tea.Cmd{cmd}85 for _, in := range held {86 _, c := s.g.Update(in)87 cmds = append(cmds, c)88 }89 return tea.Batch(cmds...)90}9192// isInput reports whether msg is the user's input, which waits while93// another's step is open.94func isInput(msg tea.Msg) bool {95 switch msg.(type) {96 case tea.KeyPressMsg, tea.PasteMsg, tea.MouseClickMsg, tea.MouseReleaseMsg, tea.MouseMotionMsg, tea.MouseWheelMsg:97 return true98 }99 return false100}101102// turn runs fn holding the model's room, if it has one; after it, the103// others learn where the user is.104func (s *Shared) turn(fn func()) {105 m := s.g.m106 seat := m.share.seat107 if seat == nil {108 fn()109 return110 }111 seat.Do(func(w *sheet.Workbook) {112 w.SetTrace(m.spans)113 before := w.LastOp()114 fn()115 if s.g.crash != nil {116 if open, who := w.InStep(); open && who == seat.ID() {117 w.EndStep() // the crashed session's macro ends here118 }119 return120 }121 if m.share.seat == seat && m.book() == w {122 s.g.m.takeBack(w, before)123 seat.Move(m.presence())124 }125 })126}127128// takeBack undoes the steps a follower made in a one-writer room, which129// only the writer edits.130func (m *Model) takeBack(w *sheet.Workbook, before uint64) {131 seat := m.share.seat132 if seat.Writing() {133 return134 }135 ops, _ := w.OpsSince(before)136 for _, o := range ops {137 if o.Author == seat.ID() && o.Kind == sheet.OpDo {138 w.Undo()139 m.mayEdit() // says why140 }141 }142}143144// between joins and leaves rooms after a turn: leaving the room of a145// workbook the model no longer shows, and joining the one it asked for.146func (s *Shared) between() tea.Cmd {147 m := s.g.m148 if s.g.crash != nil {149 return nil150 }151 if seat := m.share.seat; seat != nil && (m.share.want != nil || m.book() != seat.Book()) {152 m.share.seat = nil153 m.leftRoom(seat.Leave())154 }155 req := m.share.want156 if req == nil {157 return nil158 }159 m.share.want = nil160 seat, created := m.share.reg.Join(req.key, m.share.link, func() *sheet.Workbook {161 if req.book == nil {162 return sheet.New().Book()163 }164 return req.book165 })166 var cmd tea.Cmd167 seat.Do(func(*sheet.Workbook) {168 cmd = s.g.do(func() tea.Cmd { return m.enterRoom(seat, created, req) })169 seat.Move(m.presence())170 })171 return cmd172}173174// leftRoom lets go of what the model shared in a room it left: the last175// one out stops what the room ran; otherwise it goes on without this176// model.177func (m *Model) leftRoom(last bool) {178 m.leftLocal()179 if last {180 m.stopCells()181 return182 }183 m.nb.runs = &nbRuns{}184 for _, f := range m.follow.by {185 m.dropFollower(f)186 }187 m.src.run = &sourceHost{} // the room's goes on with the others188}189190// Model is the model run.191func (s *Shared) Model() *Model { return s.g.m }