Files

118 lines
3.6 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>.
package app
import (
"fmt"
"log/slog"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"git.stormux.org/storm/fenrir/internal/i18n"
"git.stormux.org/storm/fenrir/internal/presentation"
"git.stormux.org/storm/fenrir/internal/scripts"
)
// script_username supplies Python's current-user argument without changing
// process credentials, HOME, cwd or any inherited environment variable.
// Session lookup runs in a script worker, never in the input dispatcher.
func script_username(terminalMode bool, terminal int) string {
if terminalMode {
for _, name := range []string{os.Getenv("LOGNAME"), os.Getenv("USER")} {
if name != "" {
return name
}
}
if current, err := user.Current(); err == nil {
return current.Username
}
return ""
}
// Read logind/elogind's local session metadata, matching Python's active-VT
// lookup without adding a synchronous D-Bus round trip to keyboard handling.
for _, directory := range []string{"/run/systemd/sessions", "/run/elogind/sessions"} {
entries, _ := os.ReadDir(directory)
for _, entry := range entries {
data, err := os.ReadFile(filepath.Join(directory, entry.Name()))
if err != nil {
continue
}
values := map[string]string{}
for _, line := range strings.Split(string(data), "\n") {
if key, value, ok := strings.Cut(line, "="); ok {
values[key] = value
}
}
if values["VTNR"] != strconv.Itoa(terminal) && values["TTY"] != fmt.Sprintf("tty%d", terminal) {
continue
}
if current, err := user.LookupId(values["UID"]); err == nil {
return current.Username
}
}
}
return "" // Python also leaves this empty when no session was found.
}
func remote_authorized(uid, terminal int, terminalMode bool) bool {
if uid == 0 || uid == os.Geteuid() {
return true
}
if terminalMode || os.Geteuid() != 0 || terminal < 1 {
return false
}
info, err := os.Stat(fmt.Sprintf("/dev/tty%d", terminal))
if err != nil {
return false
}
stat, ok := info.Sys().(*syscall.Stat_t)
return ok && int(stat.Uid) == uid
}
func (a *application) script_result(result scripts.Result) {
if result.Generation != a.scriptGeneration || result.Terminal != a.lastTerminal || !a.lastActive {
return
}
if result.Err != nil {
slog.Warn("script invocation failed", "error", result.Err, "at", time.Now())
text := "Script failed"
switch result.Err.Error() {
case "Script file not found", "Script source is not a valid file", "Script file is not executable":
text = result.Err.Error()
}
a.present(i18n.T(text), presentation.Error)
return
}
if result.Truncated {
slog.Warn("script output truncated", "at", time.Now())
}
// Match Python's stderr-before-stdout result delivery. Speech still
// follows this reader's shared queue and mute policy.
if result.Stderr != "" {
a.present(result.Stderr, "")
}
if result.Stdout != "" {
a.present(result.Stdout, "")
}
}