// Fenrir console screen reader. // Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // // You should have received a copy of the GNU General Public License along with // this program. If not, see . //go:build linux && cgo package terminal /* #include "native.h" */ import "C" import ( "errors" "fmt" "io" "syscall" "git.stormux.org/storm/fenrir/internal/screen" ) func open_pty(rows, cols int) (int, int, error) { var master, slave C.int err := native_error(C.terminal_pty(&master, &slave, C.int(rows), C.int(cols))) return int(master), int(slave), err } type waitState struct { inputPending, outputPending bool } // The app loop replaces stale requests before handing readiness back to watch. func (s *Session) arm_watch() { select { case <-s.arm: default: } s.arm <- waitState{inputPending: len(s.pending) > 0, outputPending: len(s.outputPending) > 0} } func (s *Session) watch(xfd int) { defer s.workers.Done() for { var state waitState select { case <-s.stop: return case state = <-s.arm: } writeFlag := C.int(0) if state.inputPending { writeFlag = 1 } outputFlag := C.int(0) if state.outputPending { outputFlag = 1 } code := C.terminal_wait(C.int(s.master), C.int(s.input.Fd()), C.int(s.output.Fd()), C.int(xfd), C.int(s.wake[0]), writeFlag, outputFlag) if code == 1 { return } if code < 0 || code&2 != 0 { select { case s.events <- native_error(code): default: } } if code&4 != 0 { select { case s.inputEvents <- nil: default: } } if code < 0 { return } } } func (s *Session) Events() <-chan error { return s.events } // Read drains bounded PTY output and snapshots the emulator. Terminal responses // come from the outer xterm through stdin, avoiding duplicate query replies. func (s *Session) Read() (screen.Snapshot, error) { if s.eof && len(s.outputPending) == 0 { return screen.Snapshot{}, ErrClosed } if _, _, _, err := s.State(); err != nil { return screen.Snapshot{}, err } if err := s.flush_output(); err != nil { return screen.Snapshot{}, err } var buffer [32768]byte // Stop reading the child while xterm is full. At most one chunk remains // queued, and stdout readiness resumes delivery without blocking input. for total := 0; total < 131072 && len(s.outputPending) == 0 && !s.eof; { n, err := syscall.Read(s.master, buffer[:]) if errors.Is(err, syscall.EAGAIN) { break } if errors.Is(err, syscall.EINTR) { continue } if errors.Is(err, syscall.EIO) || n == 0 && err == nil { s.eof = true if total == 0 { return screen.Snapshot{}, ErrClosed } break } if err != nil { return screen.Snapshot{}, err } total += n s.vt.feed(buffer[:n]) s.outputPending = append(s.outputPending, buffer[:n]...) if err := s.flush_output(); err != nil { return screen.Snapshot{}, err } } snapshot := s.vt.snapshot(s.active, s.lock) s.retire_paste() snapshot.CapsLock = s.capsLock s.arm_watch() return snapshot, nil } // flush_output preserves partial writes and retries after stdout becomes writable. // Raw nonblocking writes keep a slow terminal from holding the app event loop. func (s *Session) flush_output() error { for len(s.outputPending) > 0 { n, err := syscall.Write(int(s.output.Fd()), s.outputPending) if n > 0 { s.outputPending = s.outputPending[n:] } if errors.Is(err, syscall.EAGAIN) { return nil } if errors.Is(err, syscall.EINTR) { continue } if err != nil { return fmt.Errorf("write terminal output: %w", err) } if n == 0 { return io.ErrNoProgress } } s.outputPending = nil return nil } // relay_input forwards stdin and queued key bytes to the child PTY. // The caller handles X11 key events before relaying terminal input. func (s *Session) relay_input() error { var bytes [4096]byte for total := 0; total < 65536 && len(s.pending) < 1<<20; { n, err := syscall.Read(int(s.input.Fd()), bytes[:]) if errors.Is(err, syscall.EAGAIN) { break } if errors.Is(err, syscall.EINTR) { continue } if err != nil { return err } if n == 0 { return ErrClosed } total += n // X key events own interruption. Stdin also carries terminal query // responses, which must never cancel speech as if they were typing. if !s.router.Captured() { s.pending = append(s.pending, bytes[:n]...) } } for len(s.pending) > 0 { n, err := syscall.Write(s.master, s.pending) if n > 0 { s.pending = s.pending[n:] s.pasteStart, s.pasteEnd = max(0, s.pasteStart-n), max(0, s.pasteEnd-n) } if errors.Is(err, syscall.EAGAIN) { break } if errors.Is(err, syscall.EINTR) { continue } if err != nil { return err } } s.arm_watch() return nil }