// 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 . package clipboard import ( "fmt" "os" "os/user" "path/filepath" "sort" "strconv" "strings" ) type Target struct{ Display, Authority string } // Display_candidates never falls through an explicit setting or DISPLAY to a // different session. Discovery uses existing local sockets, not a fixed :0..:9. func Display_candidates(configured, current, authority, home, socketDirectory string) ([]Target, error) { if authority == "" { authority = filepath.Join(home, ".Xauthority") } if configured != "" { return []Target{{configured, authority}}, nil } if current != "" { return []Target{{current, authority}}, nil } entries, err := os.ReadDir(socketDirectory) if err != nil { return nil, err } var targets []Target for _, entry := range entries { name := entry.Name() if !strings.HasPrefix(name, "X") { continue } number, err := strconv.Atoi(name[1:]) if err != nil || number < 0 { continue } info, err := entry.Info() if err == nil && info.Mode()&os.ModeSocket != 0 { targets = append(targets, Target{fmt.Sprintf(":%d", number), authority}) } } sort.Slice(targets, func(i, j int) bool { return targets[i].Display < targets[j].Display }) if len(targets) == 0 { return nil, fmt.Errorf("no local X display") } // Multiple desktops require an explicit choice; do not silently mix users. if len(targets) > 1 { return nil, fmt.Errorf("multiple X displays; set clipboard.sync_display") } return targets, nil } func system_targets(configured string) ([]Target, error) { home, _ := os.UserHomeDir() if os.Geteuid() == 0 && os.Getenv("SUDO_UID") != "" { if caller, err := user.LookupId(os.Getenv("SUDO_UID")); err == nil { home = caller.HomeDir } } targets, err := Display_candidates(configured, os.Getenv("DISPLAY"), os.Getenv("XAUTHORITY"), home, "/tmp/.X11-unix") if err != nil { return nil, err } if os.Getenv("XAUTHORITY") == "" { // Console services may lack a desktop environment. Xorg's -auth names // the actual cookie file, including display-manager runtime locations. // Read command-line metadata only; never log or copy cookie contents. paths, _ := filepath.Glob("/proc/[0-9]*/cmdline") for _, path := range paths { data, err := os.ReadFile(path) if err != nil { continue } args := strings.Split(strings.TrimRight(string(data), "\x00"), "\x00") if len(args) == 0 { continue } switch filepath.Base(args[0]) { case "Xorg", "X", "Xwayland": default: continue } display, auth := "", "" for i, arg := range args { if strings.HasPrefix(arg, ":") { display = arg } if arg == "-auth" && i+1 < len(args) { auth = args[i+1] } } for i := range targets { if display != "" && strings.SplitN(targets[i].Display, ".", 2)[0] == display && auth != "" { targets[i].Authority = auth } } } } return targets, nil }