docs: update README with detailed desktop features and setup requirements

This commit is contained in:
2026-07-17 14:41:10 +03:00
parent 55fbd357b0
commit 54fb100d3c
35 changed files with 3469 additions and 162 deletions
+164
View File
@@ -0,0 +1,164 @@
// 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 = &copy
}
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:
}
}
}
}