// 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 console import ( "errors" "fmt" "io" "os" "path/filepath" "strconv" "strings" "sync/atomic" "syscall" "time" "git.stormux.org/storm/fenrir/internal/diagnostic" "git.stormux.org/storm/fenrir/internal/screen" ) type Source struct { root string state func(string) (bool, bool, bool, error) usage func(string) (uint16, error) changes *os.File watcher *readinessWatcher ignoredVTs atomic.Uint64 // Paste checks policy from its background worker. } // Configure_ignored_vts receives the validated console policy from the event loop. func (s *Source) Configure_ignored_vts(numbers []int) { var mask uint64 for _, number := range numbers { mask |= uint64(1) << uint(number) } s.ignoredVTs.Store(mask) } // OpenSource watches the foreground-console alias. Kernel POLLPRI notifications // follow the active VT, so text feedback needs no periodic screen sampler. func OpenSource() (*Source, error) { changes, err := os.Open("/dev/vcsa") if err != nil { return nil, fmt.Errorf("open foreground console notifications: %w", err) } watcher, err := newReadinessWatcher(syscall.EPOLLPRI) if err != nil { changes.Close() return nil, err } source := &Source{root: "/", state: query_state, usage: query_usage, changes: changes, watcher: watcher} if err := watcher.add(int(changes.Fd())); err != nil { source.Close() return nil, fmt.Errorf("watch foreground console: %w", err) } return source, nil } func (s *Source) Events() <-chan error { return s.watcher.events } func (s *Source) Close() error { return errors.Join(s.watcher.close(), s.changes.Close()) } func (s *Source) active() (int, bool, bool, bool, error) { data, err := os.ReadFile(filepath.Join(s.root, "sys/devices/virtual/tty/tty0/active")) if err != nil { return 0, false, false, false, fmt.Errorf("read active VT: %w", err) } name := strings.TrimSpace(string(data)) if !strings.HasPrefix(name, "tty") { return 0, false, false, false, fmt.Errorf("invalid active VT %q", name) } number, err := strconv.Atoi(strings.TrimPrefix(name, "tty")) if err != nil || number < 1 || number > 63 { return 0, false, false, false, fmt.Errorf("invalid active VT %q", name) } if s.ignoredVTs.Load()&(uint64(1)< 1 && number < 16 { mask, err := s.usage(filepath.Join(s.root, "dev/tty1")) if err != nil { return number, false, false, false, fmt.Errorf("query console use: %w", err) } if mask&(uint16(1)<