Files
cthulhu/internal/app/braille_routing.go
Chase CichorzandClaude Opus 5.5 551ef86d5f Export and display Remote Braille through the reader
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>
2026-11-06 16:06:38 -05:00

89 lines
2.9 KiB
Go

package app
import (
"context"
"time"
"unicode/utf8"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/braille"
)
type brailleRouter interface {
RouteBraille(context.Context, atspi.BrailleRoute) error
}
func (worker *brailleWorker) route(ctx context.Context, event braille.RoutingEvent) {
worker.mu.Lock()
if callback, gesture, index, ok := worker.remote_gesture_locked(event); ok {
worker.mu.Unlock()
callback(gesture, index) // Must not block.
return
}
if event.Pan != "" {
worker.mu.Unlock()
return
}
frame := worker.frame
length := utf8.RuneCountInString(frame.Text)
if worker.secure || worker.stopping || frame.ID == 0 || event.ID != frame.ID || event.Offset < 0 ||
(frame.Virtual == nil && event.Offset >= length) || (frame.Virtual != nil && event.Offset > length) {
worker.mu.Unlock()
return
}
if route := frame.Virtual; route != nil {
if worker.ocr == nil || event.Offset < route.Start || event.Offset > route.End {
worker.mu.Unlock()
return
}
routeCtx, cancel := context.WithTimeout(ctx, time.Second)
worker.routeCancel = cancel
worker.mu.Unlock()
defer cancel()
worker.ocr.RouteBraille(routeCtx, route.Generation, route.SourceStart+event.Offset-route.Start)
return
}
if worker.router == nil && frame.Document == nil {
worker.mu.Unlock()
return
}
var target *braille.TextRoute
for _, route := range frame.Routes {
if event.Offset >= route.Start && event.Offset < route.End {
target = &route
break
}
}
if target == nil {
worker.mu.Unlock()
return
}
routeCtx, cancel := context.WithTimeout(ctx, time.Second)
worker.routeCancel = cancel
worker.mu.Unlock()
defer cancel()
if document := frame.Document; document != nil {
if worker.document == nil {
return
}
worker.logger.Debug("routing document braille", "frame_id", frame.ID, "document_epoch", document.Epoch,
"source_bus", target.BusName, "source_path", target.Path, "source_start", target.SourceStart,
"native_offset", target.SourceStart+min(event.Offset-target.Start, target.TextLength))
err := worker.document.RouteDocumentBraille(routeCtx, document.Epoch,
atspi.NewObjectID(document.FocusBus, document.FocusPath), atspi.NewObjectID(document.DocumentBus, document.DocumentPath),
atspi.NewObjectID(target.BusName, target.Path), target.SourceText, target.SourceStart,
target.SourceStart+min(event.Offset-target.Start, target.TextLength))
if err != nil && routeCtx.Err() == nil {
worker.logger.Debug("document braille routing rejected", "error", err)
}
return
}
err := worker.router.RouteBraille(routeCtx, atspi.BrailleRoute{
Focus: atspi.NewObjectID(target.BusName, target.Path), Line: target.SourceText, Start: target.SourceStart,
Offset: target.SourceStart + min(event.Offset-target.Start, target.TextLength),
})
if err != nil && routeCtx.Err() == nil {
worker.logger.Debug("braille routing rejected", "error", err)
}
}