121 lines
2.9 KiB
Go
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
|
|
}
|