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

+204
View File
@@ -0,0 +1,204 @@
// 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 (
"fmt"
"unsafe"
"git.stormux.org/storm/fenrir/internal/diagnostic"
"git.stormux.org/storm/fenrir/internal/input"
)
// Keyboard returns the input side of this session. Commands use exactly the
// same router, modal capture and app dispatcher as evdev.
func (s *Session) Keyboard() *Keyboard { return &Keyboard{session: s} }
type Keyboard struct{ session *Session }
func (k *Keyboard) Events() <-chan error { return k.session.inputEvents }
func (k *Keyboard) Scan() error { return nil }
func (k *Keyboard) Pass_next() { k.session.router.Pass_next() }
func (k *Keyboard) Capture(enabled bool) error {
s := k.session
if enabled && !s.grabbed {
status, err := s.capture_keyboard()
if err != nil {
return err
}
if status != 0 {
return fmt.Errorf("X11 terminal capture unavailable (status %d)", status)
}
}
s.router.Capture(enabled)
if enabled {
// Bytes already replayed by xterm must not reach the child during a form.
s.pending = nil
s.pasteStart, s.pasteEnd = 0, 0
}
// The closing key is already in the router but may not yet be in s.held:
// Capture(false) can run from handle inside Poll. Keep that grab until its
// release rather than ungrabbing and regrabbing with an older timestamp.
if !enabled && !s.router.Held(0) && len(s.held) == 0 && s.grabbed {
C.terminal_x_release(s.x)
s.grabbed = false
}
return nil
}
func (k *Keyboard) Close() error {
C.terminal_x_release(k.session.x)
return nil
}
// capture_keyboard treats desktop/focus contention as a refused chord, not a
// terminal-session failure. Keep modal intent while discarding stale key state.
func (s *Session) capture_keyboard() (int, error) {
status := C.terminal_x_consume(s.x)
if err := native_error(status); err != nil {
return 0, fmt.Errorf("acquire X11 terminal keyboard: %w", err)
}
if status == 0 {
s.grabbed = true
return 0, nil
}
diagnostic.Record("X11 keyboard capture refused", "status", int(status))
C.terminal_x_release(s.x)
captured := s.router.Captured()
s.router.Reset()
s.router.Capture(captured)
clear(s.held)
clear(s.replayed)
s.grabbed = false
s.captureBlocked = true
return int(status), nil
}
func (k *Keyboard) Poll(active, numLock bool, handle func(input.Decision)) error {
s := k.session
for code, key := range s.replayed {
if C.terminal_x_down(s.x, C.int(code)) == 0 {
s.router.Key(0, key, 0, numLock, true)
delete(s.replayed, code)
}
}
for {
var event C.TerminalEvent
code := C.terminal_x_next(s.x, &event)
if code == 0 {
break
}
if err := native_error(code); err != nil {
return fmt.Errorf("X11 terminal input: %w", err)
}
if event._type != 1 {
continue
}
if s.captureBlocked {
// Do not reinterpret the tail of a refused Fenrir chord as a bare
// command after its modifier state was cleared. Drain until key-up.
C.terminal_x_release(s.x)
continue
}
if !active {
C.terminal_x_release(s.x)
continue
}
// ReplayKeyboard redelivers a forwarded press to normal event
// observers, including us. It must not interrupt speech twice.
if event.value == 1 && s.lastReplay.code == int(event.keycode) && s.lastReplay.time == uint64(event.time) {
continue
}
key := uint16(C.terminal_key_code(event.symbol, event.keycode))
if held, exists := s.held[int(event.keycode)]; exists {
key = held
}
// Synchronize standard modifiers that xterm received before our grab.
for _, modifier := range []struct {
key uint16
mask uint32
}{
{input.KeyShift, 1}, {input.KeyCtrl, 4}, {input.KeyAlt, 8},
} {
value := int32(0)
if uint32(event.state)&modifier.mask != 0 {
value = 1
}
if modifier.key != key {
s.router.Observe_modifier(1, modifier.key, value, numLock)
}
}
value := int32(event.value)
if value == 1 {
if _, exists := s.held[int(event.keycode)]; exists {
value = 2
}
}
decision := s.router.Key(0, key, value, numLock, false)
// Acquire before dispatch: a failed capture must not execute a command
// or open a form from a partial, stale chord.
if event.value == 1 && !decision.Forward && !s.grabbed {
status, err := s.capture_keyboard()
if err != nil {
return err
}
if status != 0 {
continue
}
}
if decision.UI != nil {
decision.UI.Text = C.GoStringN(&event.text[0], event.length)
// Control+A arrives as a control character from XLookupString.
if decision.UI.Ctrl && event.symbol >= 'a' && event.symbol <= 'z' {
decision.UI.Text = string(rune(event.symbol))
}
}
handle(decision)
if event.value == 1 {
if decision.Forward && !s.grabbed {
C.terminal_x_replay(s.x, event.time)
s.lastReplay.code, s.lastReplay.time = int(event.keycode), uint64(event.time)
s.replayed[int(event.keycode)] = key
} else {
s.held[int(event.keycode)] = key
if decision.Forward {
var bytes [256]byte
n := int(C.terminal_key(s.vt.native, &event, (*C.char)(unsafe.Pointer(&bytes[0])), C.int(len(bytes))))
s.pending = append(s.pending, bytes[:n]...)
}
}
} else {
delete(s.replayed, int(event.keycode))
delete(s.held, int(event.keycode))
if len(s.held) == 0 && s.grabbed && !s.router.Captured() {
C.terminal_x_release(s.x)
s.grabbed = false
}
}
}
if s.captureBlocked && C.terminal_x_keys_down(s.x) == 0 {
s.captureBlocked = false
}
return s.relay_input()
}