package braille import ( "bufio" "context" "encoding/json" "errors" "fmt" "io" "runtime" "time" ) // RunClient owns BrlAPI inside the disposable helper process. The parent can // terminate this process if a native call blocks or BRLTTY disconnects. func RunClient(ctx context.Context, backend Backend, input io.ReadCloser, ready io.Writer) (result error) { // Keep the session on one OS thread. Migration between Open and ReadKey // produced an unexpected EOF with the live BRLTTY connection. runtime.LockOSThread() defer runtime.UnlockOSThread() geometry, err := backend.Open() defer func() { result = errors.Join(result, backend.Close()) }() if err != nil { return err } cells, err := geometry.Cells() if err != nil { return err } if _, err := io.WriteString(ready, "ready\n"); err != nil { return err } frames := make(chan Frame, 1) readErrors := make(chan error, 1) readDone := make(chan struct{}) // The session owns its input reader: close unblocks Scan, then join it. defer func() { _ = input.Close(); <-readDone }() go func() { defer close(readDone) scanner := bufio.NewScanner(input) scanner.Buffer(make([]byte, 4096), 65536) for scanner.Scan() { var frame Frame if err := json.Unmarshal(scanner.Bytes(), &frame); err != nil { readErrors <- fmt.Errorf("decode braille frame: %w", err) return } offerLatestFrame(frames, frame) } readErrors <- scanner.Err() }() v := viewport{width: cells} encoder := json.NewEncoder(ready) ticker := time.NewTicker(20 * time.Millisecond) defer ticker.Stop() inputClosed := false for { if inputClosed && len(frames) == 0 { return nil } select { case <-ctx.Done(): return nil case err := <-readErrors: if err != nil { return err } inputClosed = true readErrors = nil case frame := <-frames: if err := v.setFrame(frame); err != nil { return err } if err := writeViewport(backend, &v); err != nil { return err } case <-ticker.C: key, err := backend.ReadKey() if err != nil { return err } if v.pan(key) { if err := writeViewport(backend, &v); err != nil { return err } } if event, ok := v.route(key); ok { if err := encoder.Encode(event); err != nil { return err } } } } } type dotWriter interface { WriteWithDots(string, int, []byte) error } func writeViewport(backend Backend, viewport *viewport) error { if writer, ok := backend.(dotWriter); ok { return writer.WriteWithDots(viewport.frame(), viewport.displayCursor(), viewport.displayDots()) } return backend.Write(viewport.frame(), viewport.displayCursor()) } // The consumer may remove the old frame after the first nonblocking send // fails. Draining must therefore also be nonblocking. func offerLatestFrame(frames chan Frame, frame Frame) { select { case frames <- frame: return default: } select { case <-frames: default: } frames <- frame }