Files

121 lines
2.9 KiB
Go

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
}