Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,120 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user