1package ui23import (4 "strconv"5 "strings"6 "time"78 tea "charm.land/bubbletea/v2"9 "charm.land/lipgloss/v2"10 "github.com/charmbracelet/x/ansi"1112 "src.rest/cat/012/internal/room"13 "src.rest/cat/012/internal/sheet"14 "src.rest/cat/012/internal/ui/theme"15)1617// Drawing who else is in a shared workbook, and what they changed: each18// other participant's pointer is their cell in their color,19// double-underlined, with their initial in the row header; a cell they20// changed in the last markFresh has a ▘ in its corner in their color;21// the status line lists who's here, and the context line says who is on22// the active cell or last changed it. Colors and cues are theme.Peer.2324// markFresh is how long a cell another changed stays marked.25const markFresh = 30 * time.Second2627// shareFrame is what a frame draws of the others, worked out once as it28// starts: their pointers on the sheet shown, and the fresh marks.29type shareFrame struct {30 peers []room.Peer31 here []room.Peer // on the sheet shown32 fresh []room.Mark // others' changes on the sheet shown, newest last33}3435// shareTickMsg redraws once marks have gone stale.36type shareTickMsg struct{}3738// frameShare works out the others for the frame about to be drawn.39func (m *Model) frameShare() {40 f := &m.share.frame41 f.peers, f.here, f.fresh = nil, f.here[:0], f.fresh[:0]42 m.frameAgents()43 seat := m.share.seat44 if seat == nil {45 return46 }47 f.peers = seat.Peers()48 for _, p := range f.peers {49 if p.Presence.Sheet == m.sheet {50 f.here = append(f.here, p)51 }52 }53 now := time.Now()54 for _, mk := range seat.Marks(0) {55 if mk.Author != seat.ID() && mk.Sheet == m.sheet && now.Sub(mk.At) < markFresh && mk.Kind != sheet.OpUndo {56 f.fresh = append(f.fresh, mk)57 }58 }59}6061// peerAt is the other whose pointer is on a, if one is.62func (m *Model) peerAt(a sheet.Addr) (room.Peer, bool) {63 for _, p := range m.share.frame.here {64 if p.Presence.Cursor == a {65 return p, true66 }67 }68 return room.Peer{}, false69}7071// peerRole is the role of a cell under another's pointer.72func (m *Model) peerRole(a sheet.Addr) (*lipgloss.Style, bool) {73 p, ok := m.peerAt(a)74 if !ok {75 return nil, false76 }77 st := m.th.PeerCell(p.Color)78 return &st, true79}8081// freshMark is the newest of others' fresh changes covering a.82func (m *Model) freshMark(a sheet.Addr) (room.Mark, bool) {83 f := m.share.frame.fresh84 for i := len(f) - 1; i >= 0; i-- {85 if f[i].Rect.Contains(a) {86 return f[i], true87 }88 }89 return room.Mark{}, false90}9192// changedMark draws the mark of a cell another changed lately over the93// first column of its text.94func (m *Model) changedMark(text string, a sheet.Addr, base lipgloss.Style, colored bool) string {95 mk, ok := m.freshMark(a)96 if !ok || m.sheet.ColWidth(a.Col) < 2 {97 return text98 }99 mark := m.th.PeerMark[mk.Color%theme.Peers]100 if colored {101 mark = base102 }103 return mark.Render("▘") + ansi.Cut(text, 1, m.sheet.ColWidth(a.Col))104}105106// rowTags are the initials of those whose pointer is on row, as chips107// in their colors, and how many columns they take.108func (m *Model) rowTags(row int) (string, int) {109 var b strings.Builder110 n := 0111 for _, p := range m.share.frame.here {112 if p.Presence.Cursor.Row != row || n >= 2 {113 continue114 }115 b.WriteString(m.th.Peer[p.Color%theme.Peers].Render(peerInitial(p.Name, p.Agent)))116 n++117 }118 return b.String(), n119}120121// initial is a name's first letter.122func initial(name string) string {123 for _, r := range name {124 return string(r)125 }126 return "?"127}128129// peersStatus is the status line's part about the others: their names130// in their colors, and in a one-writer room who writes.131func (m *Model) peersStatus() string {132 peers := m.share.frame.peers133 if len(peers) == 0 {134 return m.agentsStatus()135 }136 var b strings.Builder137 b.WriteString(" ")138 for i, p := range peers {139 if i == 3 {140 b.WriteString(m.th.Muted.Render(" +" + strconv.Itoa(len(peers)-3)))141 break142 }143 if i > 0 {144 b.WriteString(" ")145 }146 b.WriteString(m.th.Peer[p.Color%theme.Peers].Render(" " + peerLabel(p.Name, p.Agent) + " "))147 }148 if seat := m.share.seat; seat.Mode() == room.View {149 if seat.Writing() {150 b.WriteString(m.th.Muted.Render(" you write"))151 } else {152 b.WriteString(m.th.Muted.Render(" " + seat.Writer() + " writes"))153 }154 }155 return b.String() + m.agentsStatus()156}157158// shareLine is the context line's word on the active cell: who else is159// on it, or who changed it lately.160func (m *Model) shareLine() string {161 if m.share.seat == nil {162 return ""163 }164 if line := m.agentLine(); line != "" {165 return line166 }167 if p, ok := m.peerAt(m.cur); ok {168 what := " is here"169 if p.Presence.Editing {170 what = " is typing here"171 }172 return m.th.Peer[p.Color%theme.Peers].Render(" "+p.Name+" ") + m.th.Muted.Render(what)173 }174 if mk, ok := m.freshMark(m.cur); ok {175 ago := time.Since(mk.At).Round(time.Second)176 return m.th.Muted.Render("Changed by ") + m.th.Peer[mk.Color%theme.Peers].Render(" "+mk.Name+" ") +177 m.th.Muted.Render(" "+ago.String()+" ago: "+mk.Label)178 }179 return ""180}181182// shareTick redraws once the freshest mark has gone stale, while there183// are marks.184func (m *Model) shareTick() tea.Cmd {185 if len(m.share.frame.fresh) == 0 || m.share.ticking || m.share.still {186 return nil187 }188 m.share.ticking = true189 return tea.Tick(markFresh, func(time.Time) tea.Msg { return shareTickMsg{} })190}