212 lines
5.2 KiB
Go
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
|
|
}
|