// Package history retains a bounded in-memory caption transcript for the GUI. package history import ( "errors" "strings" "sync" "time" "tea.chunkbyte.com/kato/captioneer/src/captions" ) type Entry struct { CreatedAt time.Time Text string StartedAt time.Duration EndedAt time.Duration } type Snapshot struct { Finals []Entry Provisional *Entry } type Sink struct { mu sync.Mutex capacity int enabled bool finals []Entry provisional *Entry subscribers map[chan Snapshot]struct{} closed bool } func New(capacity int, enabled bool) *Sink { if capacity < 1 { capacity = 1 } return &Sink{ capacity: capacity, enabled: enabled, subscribers: make(map[chan Snapshot]struct{}), } } func (s *Sink) Publish(event captions.Event) error { s.mu.Lock() defer s.mu.Unlock() if s.closed { return errors.New("caption history is closed") } if !s.enabled { return nil } now := time.Now() switch event.Kind { case captions.Provisional: s.provisional = &Entry{CreatedAt: now, Text: event.Text, StartedAt: event.StartedAt, EndedAt: event.EndedAt} case captions.Final: s.provisional = nil s.finals = append(s.finals, Entry{CreatedAt: now, Text: event.Text, StartedAt: event.StartedAt, EndedAt: event.EndedAt}) if len(s.finals) > s.capacity { s.finals = append([]Entry(nil), s.finals[len(s.finals)-s.capacity:]...) } case captions.Hide: s.provisional = nil } s.publishLocked() return nil } func (s *Sink) SetEnabled(enabled bool) { s.mu.Lock() s.enabled = enabled if !enabled { s.provisional = nil } s.publishLocked() s.mu.Unlock() } func (s *Sink) Clear() { s.mu.Lock() s.finals = nil s.provisional = nil s.publishLocked() s.mu.Unlock() } func (s *Sink) Snapshot() Snapshot { s.mu.Lock() defer s.mu.Unlock() return s.snapshotLocked() } func (s *Sink) Transcript() string { s.mu.Lock() defer s.mu.Unlock() lines := make([]string, len(s.finals)) for index, entry := range s.finals { lines[index] = entry.Text } return strings.Join(lines, "\n") } func (s *Sink) Subscribe(buffer int) (<-chan Snapshot, func()) { if buffer < 1 { buffer = 1 } updates := make(chan Snapshot, buffer) s.mu.Lock() s.subscribers[updates] = struct{}{} updates <- s.snapshotLocked() s.mu.Unlock() return updates, func() { s.mu.Lock() if _, ok := s.subscribers[updates]; ok { delete(s.subscribers, updates) close(updates) } s.mu.Unlock() } } func (s *Sink) Close() error { s.mu.Lock() defer s.mu.Unlock() if s.closed { return nil } s.closed = true for subscriber := range s.subscribers { close(subscriber) } s.subscribers = nil return nil } func (s *Sink) snapshotLocked() Snapshot { snapshot := Snapshot{Finals: append([]Entry(nil), s.finals...)} if s.provisional != nil { copy := *s.provisional snapshot.Provisional = © } return snapshot } func (s *Sink) publishLocked() { snapshot := s.snapshotLocked() for subscriber := range s.subscribers { select { case subscriber <- snapshot: default: select { case <-subscriber: default: } select { case subscriber <- snapshot: default: } } } }