Files
cthulhu/internal/app/braille_worker.go

126 lines
3.4 KiB
Go

package app
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"sync"
"time"
"git.stormux.org/storm/cthulhu/internal/braille"
"git.stormux.org/storm/cthulhu/internal/review"
)
// brailleWorker keeps all BrlAPI calls in a disposable child process. Only the
// newest frame is retained; focus and input callbacks never wait for the display.
// mu owns frames, flash generations, routing cancellation and session flags.
// The session joins its child and pipe workers before reconnecting; routing
// events retain their own bounded FIFO and validate the current frame identity.
type brailleWorker struct {
internalUIFrame *braille.Frame
router brailleRouter
document *review.Worker
ocr *ocrSession
sequence uint64
routeCancel context.CancelFunc
secure bool
stopping bool
connected bool
logger *slog.Logger
path string
mu sync.Mutex
frame braille.Frame
baseFrame braille.Frame
flashTimer *time.Timer
flashGeneration uint64
flashContext context.Context // A tentacle flash expires with its command/session.
protectedFlash bool // Focus updates retain baseFrame until expiry or user input.
contracted bool
wordWrap bool
table string
wake chan struct{}
cancel context.CancelFunc
done chan struct{}
}
func newBrailleWorker(logger *slog.Logger) (*brailleWorker, error) {
executable, err := os.Executable()
if err != nil {
return nil, err
}
return &brailleWorker{logger: logger, path: filepath.Join(filepath.Dir(executable), "cthulhu-braille"),
frame: braille.Frame{Cursor: -1}, baseFrame: braille.Frame{Cursor: -1}, wake: make(chan struct{}, 1)}, nil
}
func (worker *brailleWorker) Start() error {
worker.mu.Lock()
defer worker.mu.Unlock()
if worker.stopping {
return errors.New("braille helper shutdown is still pending")
}
if worker.cancel != nil {
return nil
}
info, err := os.Stat(worker.path)
if err != nil || info.IsDir() || info.Mode()&0111 == 0 {
return fmt.Errorf("braille helper %q is unavailable; build cthulhu-braille with -tags brlapi", worker.path)
}
ctx, cancel := context.WithCancel(context.Background())
worker.cancel = cancel
worker.done = make(chan struct{})
go worker.run(ctx, worker.done)
return nil
}
func (worker *brailleWorker) Stop(ctx context.Context) error {
worker.mu.Lock()
cancel, done := worker.cancel, worker.done
if cancel == nil {
worker.mu.Unlock()
return nil
}
worker.stopping = true
if worker.routeCancel != nil {
worker.routeCancel()
}
worker.mu.Unlock()
cancel()
select {
case <-done:
worker.mu.Lock()
if worker.done == done {
worker.cancel, worker.done = nil, nil
worker.stopping = false
worker.connected = false
worker.protectedFlash = false
worker.flashContext = nil
worker.frame = braille.Frame{Cursor: -1}
worker.baseFrame = braille.Frame{Cursor: -1}
if worker.flashTimer != nil {
worker.flashTimer.Stop()
worker.flashTimer = nil
}
}
worker.mu.Unlock()
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (worker *brailleWorker) Configure(ctx context.Context, enabled bool) error {
if enabled {
return worker.Start()
}
return worker.Stop(ctx)
}
func (worker *brailleWorker) Running() bool {
worker.mu.Lock()
defer worker.mu.Unlock()
return worker.cancel != nil && !worker.stopping && worker.connected
}