Files

212 lines
5.2 KiB
Go

// 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 <https://www.gnu.org/licenses/gpl-3.0.html>.
//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
}