// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package braille import ( "bufio" "context" "encoding/json" "errors" "io" ) // VirtualComputerTable is the explicit table used to turn ordinary // (uncontracted) frames into cells for a remote display. The physical display // leaves that translation to BRLTTY; a virtual display has no BRLTTY, so the // policy is stated here rather than guessed from local configuration. const VirtualComputerTable = "en-us-comp8.ctb" // VirtualCursorDots marks the cursor cell with dots 7 and 8. It is a static // overlay, not BRLTTY's configured cursor shape or blink timing. const VirtualCursorDots = 0xc0 // VirtualRequest is one line from the parent. Exactly one field is set. type VirtualRequest struct { Width *int `json:"width,omitempty"` Frame *Frame `json:"frame,omitempty"` Pan string `json:"pan,omitempty"` Route *int `json:"route,omitempty"` } // VirtualResponse is one line to the parent: new cells, or a routing event // against the frame ID the parent supplied. type VirtualResponse struct { Cells []int `json:"cells,omitempty"` Route *RoutingEvent `json:"route,omitempty"` } // RunVirtual renders frames into a virtual viewport without opening BrlAPI. // It shares the physical helper's viewport, liblouis translation, wrapping, // panning and routing maps, so remote cells match local behaviour apart from // the explicit uncontracted table and static cursor. It exits on EOF. func RunVirtual(ctx context.Context, input io.Reader, output io.Writer) error { scanner := bufio.NewScanner(input) scanner.Buffer(make([]byte, 4096), 65536) encoder := json.NewEncoder(output) v := viewport{} var current *Frame emit := func() error { if v.width == 0 || current == nil { return nil } cells, err := v.rawCells(VirtualCursorDots) if err != nil { return err } values := make([]int, len(cells)) for index, cell := range cells { values[index] = int(cell) } return encoder.Encode(VirtualResponse{Cells: values}) } render := func() error { if v.width == 0 || current == nil { return nil } frame := *current if !frame.Contract { frame.Contract, frame.Table = true, VirtualComputerTable } if err := v.setFrame(frame); err != nil { return err } return emit() } for scanner.Scan() { if ctx.Err() != nil { return nil } var request VirtualRequest if err := json.Unmarshal(scanner.Bytes(), &request); err != nil { return errors.New("invalid virtual braille request") } var err error switch { case request.Width != nil: if *request.Width < 0 || *request.Width > 4096 { return errors.New("invalid virtual braille width") } v.width = *request.Width err = render() case request.Frame != nil: frame := *request.Frame current = &frame err = render() case request.Pan == "left" || request.Pan == "right": key := PanLeft if request.Pan == "right" { key = PanRight } if current != nil && v.width > 0 && v.pan(key) { err = emit() } case request.Route != nil: if current != nil && v.width > 0 { if event, ok := v.route(RouteBase + Key(*request.Route)); ok { err = encoder.Encode(VirtualResponse{Route: &event}) } } default: return errors.New("invalid virtual braille request") } if err != nil { return err } } return scanner.Err() }