// 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 input import ( "errors" "fmt" "io" "os" "path/filepath" "strings" ) // Layout_paths keeps named TOML layouts in Fenrir's keyboard directories. // Explicit paths select exactly one file. The bundle is an on-disk runtime asset. func Layout_paths(layout, home string, uid int, bundledDirectory string) ([]string, error) { if strings.TrimSpace(layout) == "" || strings.ContainsAny(layout, "\x00\r\n") { return nil, fmt.Errorf("keyboard layout cannot be empty or contain control characters") } if filepath.Ext(layout) != "" && filepath.Ext(layout) != ".toml" { return nil, fmt.Errorf("keyboard layout must use TOML") } if strings.ContainsRune(layout, filepath.Separator) || filepath.IsAbs(layout) { if filepath.Ext(layout) != ".toml" { return nil, fmt.Errorf("keyboard layout path must end in .toml") } return []string{layout}, nil } name := strings.TrimSuffix(layout, ".toml") if name == "" || name == "." || name == ".." { return nil, fmt.Errorf("invalid keyboard layout name") } name += ".toml" paths := []string{} if uid != 0 { paths = append(paths, filepath.Join(home, ".local/share/stormux/fenrirscreenreader/keyboard", name)) } paths = append(paths, filepath.Join("/etc/fenrirscreenreader/keyboard", name)) if bundledDirectory != "" { paths = append(paths, filepath.Join(bundledDirectory, name)) } return paths, nil } // Load_bindings selects one whole file. A malformed or unreadable candidate // stops loading; only absent named candidates advance to the next directory. func Load_bindings(paths []string) (Layout, string, error) { for _, path := range paths { file, err := os.Open(path) if errors.Is(err, os.ErrNotExist) { continue } if err != nil { return Layout{}, path, err } data, readErr := io.ReadAll(io.LimitReader(file, (1<<20)+1)) closeErr := file.Close() if err := errors.Join(readErr, closeErr); err != nil { return Layout{}, path, err } if len(data) > 1<<20 { return Layout{}, path, fmt.Errorf("keyboard layout exceeds 1 MiB") } bindings, err := Decode_bindings(data) return bindings, path, err } return Layout{}, "", fmt.Errorf("keyboard layout not found (searched %s)", strings.Join(paths, ", ")) }