// 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 input /* #cgo pkg-config: libevdev #include #include */ import "C" import ( "fmt" "slices" "strconv" "strings" "unsafe" "git.stormux.org/storm/fenrir/internal/command" ) // ScriptBinding uses Python Fenrir's normalized KEY_* names, including the // synthetic KEY_SCRIPT, KEY_FENRIR and generic modifier names. type ScriptBinding struct { ID command.ID Keys []string } func Key_code(name string) (uint16, bool) { name = strings.ToUpper(name) aliases := map[string]string{"KEY_CTRL": "KEY_LEFTCTRL", "KEY_SHIFT": "KEY_LEFTSHIFT", "KEY_ALT": "KEY_LEFTALT", "KEY_META": "KEY_LEFTMETA"} if alias := aliases[name]; alias != "" { name = alias } text := C.CString(name) defer C.free(unsafe.Pointer(text)) code := int(C.libevdev_event_code_from_name(C.EV_KEY, text)) return uint16(code), code >= 0 && code <= int(C.KEY_MAX) } func Valid_script_key(name string) bool { if name == "KEY_SCRIPT" || name == "KEY_FENRIR" { return true } _, ok := Key_code(name) return ok } // Key_name retains the physical left/right/keypad identity for tutorial input. func Key_name(code uint16) string { name := C.libevdev_event_code_get_name(C.EV_KEY, C.uint(code)) if name == nil { return strconv.Itoa(int(code)) } return C.GoString(name) } func Script_chord(keys []string) string { keys = slices.Clone(keys) slices.Sort(keys) return strings.Join(keys, ",") } func (r *Router) Configure_scripts(keys []string, bindings []ScriptBinding) error { scriptKeys, err := Modifier_codes(keys) if err != nil { return err } for code := range scriptKeys { if r.reader_modifier(code, false) { return fmt.Errorf("%s cannot be both a Fenrir key and a script key", Key_name(code)) } } scriptBindings := make(map[string]command.ID) for _, binding := range bindings { if binding.ID == "" { return fmt.Errorf("script binding has no command") } for _, name := range binding.Keys { if !Valid_script_key(name) { return fmt.Errorf("invalid script shortcut %q", name) } } chord := Script_chord(binding.Keys) if _, exists := scriptBindings[chord]; exists { return fmt.Errorf("duplicate script shortcut %s", chord) } scriptBindings[chord] = binding.ID } r.scriptKeys, r.scriptBindings = scriptKeys, scriptBindings return nil } func (r *Router) Script_keys() []uint16 { var keys []uint16 for key := range r.scriptKeys { keys = append(keys, key) } slices.Sort(keys) return keys } func (r *Router) script_name(key uint16, numLock bool) string { // Python normalizes left/right modifiers before resolving reader modifiers. physical := key switch key { case KeyRightCtrl: key = KeyCtrl case KeyRightShift: key = KeyShift case KeyRightAlt: key = KeyAlt case KeyRightMeta: key = KeyMeta } if r.script_modifier(physical, numLock) { return "KEY_SCRIPT" } if r.reader_modifier(physical, numLock) { return "KEY_FENRIR" } switch key { case KeyCtrl: return "KEY_CTRL" case KeyShift: return "KEY_SHIFT" case KeyAlt: return "KEY_ALT" } name := C.libevdev_event_code_get_name(C.EV_KEY, C.uint(key)) if name == nil { return "" } return C.GoString(name) } func (r *Router) script_binding(key uint16, numLock bool) (command.ID, bool) { active := r.script_modifier(key, numLock) if !active { for scriptKey := range r.scriptKeys { if r.script_modifier(scriptKey, numLock) && r.down(scriptKey) { active = true break } } } if !active { return "", false } names := map[string]bool{} for _, held := range r.keys { for code := range held { names[r.script_name(code, numLock)] = true } } names[r.script_name(key, numLock)] = true if !names["KEY_SCRIPT"] { return "", false } var keys []string for name := range names { keys = append(keys, name) } if id := r.scriptBindings[Script_chord(keys)]; id != "" { return id, true } // Modifier prefixes are silent. Nonmatching script chords are consumed. for _, name := range keys { switch name { case "KEY_SCRIPT", "KEY_FENRIR", "KEY_CTRL", "KEY_SHIFT", "KEY_ALT", "KEY_META": default: return command.Unbound, true } } return "", true }