After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.

This commit is contained in:
Storm Dragon committed 2026-10-27 20:30:55 -04:00
1 parent c018f5fd29
commit 0e50de4c76
1047 files changed
+58364 -63747

No files matched your search

+211
View File
@@ -0,0 +1,211 @@
// 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
}