A controlled reader renders accepted application frames in a new virtual-only cthulhu-braille mode at the controller's width and applies its pan and route gestures; reader UI, flashes and secure frames are never exported, and hidden frames are unroutable. A controller reader reports its display width, shows peer cells as an untranslated raw frame and forwards local pan and routing keys. The physical helper now reports its width and edge pans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
3.9 KiB
Go
135 lines
3.9 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
|
|
readyGiveUp time.Duration // Wedged-helper recycle bound; zero uses the default.
|
|
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{}
|
|
// Remote Braille. cells is the connected display width (0 unknown).
|
|
// remoteExport receives accepted application frames; remoteFrame shows a
|
|
// peer's cells in place of them; remoteGesture receives display keys
|
|
// pressed while remoteFrame is shown. All are guarded by mu.
|
|
cells int
|
|
remoteExport func(braille.Frame)
|
|
remoteFrame *braille.Frame
|
|
remoteGesture func(gesture string, index int)
|
|
}
|
|
|
|
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
|
|
}
|