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 }