// 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 ( "bytes" "fmt" "strings" "git.stormux.org/storm/fenrir/internal/command" "github.com/pelletier/go-toml/v2" ) type Layout struct { Version int `toml:"version"` FenrirKeys []string `toml:"fenrir_keys"` Bindings []Binding `toml:"bindings"` } type Binding struct { Name string `toml:"name"` Keys []string `toml:"keys"` Command command.ID `toml:"command"` DoubleCommand command.ID `toml:"double_command,omitempty"` TripleCommand command.ID `toml:"triple_command,omitempty"` } var keyCodes = map[string]uint16{ "F": KeyF, "Q": KeyQ, "H": KeyH, "F4": KeyF4, "BACKSPACE": KeyBackspace, "KP7": KeyKP7, "KP8": KeyKP8, "KP9": KeyKP9, "KP4": KeyKP4, "KP5": KeyKP5, "KP6": KeyKP6, "KP1": KeyKP1, "KP2": KeyKP2, "KP3": KeyKP3, "KPDOT": KeyKPDot, "KPENTER": KeyKPEnter, "KPASTERISK": 55, "KPPLUS": KeyKPPlus, "A": 30, "B": 48, "C": 46, "D": 32, "E": 18, "G": 34, "I": 23, "J": 36, "K": 37, "L": 38, "M": 50, "N": 49, "O": 24, "P": 25, "R": 19, "S": 31, "T": 20, "U": 22, "V": 47, "W": 17, "X": 45, "Y": 21, "Z": 44, "0": 11, "1": 2, "2": 3, "3": 4, "4": 5, "5": 6, "6": 7, "7": 8, "8": 9, "9": 10, "COMMA": 51, "DOT": 52, "SLASH": 53, "UP": KeyUp, "DOWN": KeyDown, "LEFT": KeyLeft, "RIGHT": KeyRight, "HOME": KeyHome, "END": KeyEnd, "DELETE": KeyDelete, "TAB": KeyTab, "ENTER": KeyEnter, "SPACE": KeySpace, "ESC": KeyEsc, "PAGEUP": 104, "PAGEDOWN": 109, "F5": 63, "F6": 64, "F7": 65, "F8": 66, "F1": 59, "KPMINUS": 74, "KPSLASH": 98, "RIGHTBRACE": 27, } type modifiers uint8 const ( fenrirModifier modifiers = 1 << iota ctrlModifier shiftModifier altModifier ) type chord struct { key uint16 modifiers modifiers } var modifierNames = map[string]modifiers{"FENRIR": fenrirModifier, "CTRL": ctrlModifier, "SHIFT": shiftModifier, "ALT": altModifier} // Decode_bindings rejects unsupported keys, commands and duplicate chords. func Decode_bindings(data []byte) (Layout, error) { var file Layout if err := toml.NewDecoder(bytes.NewReader(data)).DisallowUnknownFields().Decode(&file); err != nil { return Layout{}, err } if file.Version != 1 { return Layout{}, fmt.Errorf("unsupported keyboard version %d", file.Version) } _, err := New_router(file) return file, err } // New_router compiles a private layout. Reset and capture preserve these bindings. func New_router(layout Layout) (Router, error) { bindings := layout.Bindings router := Router{bindings: make(map[chord]command.ID), doubleBindings: make(map[chord]command.ID), tripleBindings: make(map[chord]command.ID), multiPressTimeout: DefaultMultiPressTimeout} keys := layout.FenrirKeys if len(keys) == 0 { keys = []string{"INSERT", "SUPER", "KP0"} } router.readerKeys = make(map[uint16]bool) for _, name := range keys { var codes []uint16 switch name { case "INSERT": codes = []uint16{KeyInsert} case "SUPER": codes = []uint16{KeyMeta, KeyRightMeta} case "CAPSLOCK": codes = []uint16{KeyCapsLock} case "KP0": codes = []uint16{KeyKP0} default: return Router{}, fmt.Errorf("unsupported Fenrir modifier %q", name) } for _, code := range codes { if router.readerKeys[code] { return Router{}, fmt.Errorf("duplicate Fenrir modifier %q", name) } router.readerKeys[code] = true } } if len(bindings) == 0 { return Router{}, fmt.Errorf("keyboard layout has no bindings") } for _, binding := range bindings { if binding.TripleCommand != "" && (!command.Known(binding.TripleCommand) || binding.DoubleCommand == "") { return Router{}, fmt.Errorf("invalid triple command in binding %q (requires a double command)", binding.Name) } if binding.DoubleCommand != "" && !command.Known(binding.DoubleCommand) { return Router{}, fmt.Errorf("invalid double command in binding %q", binding.Name) } if strings.TrimSpace(binding.Name) == "" || len(binding.Keys) == 0 || !command.Known(binding.Command) { return Router{}, fmt.Errorf("invalid keyboard binding %q", binding.Name) } key, valid := keyCodes[binding.Keys[len(binding.Keys)-1]] if !valid { return Router{}, fmt.Errorf("unsupported key in binding %q", binding.Name) } var mods modifiers for _, name := range binding.Keys[:len(binding.Keys)-1] { modifier, valid := modifierNames[name] if !valid || mods&modifier != 0 { return Router{}, fmt.Errorf("invalid modifiers in binding %q", binding.Name) } mods |= modifier } // Both adapters can intercept Fenrir chords and bare review keypad keys. // Other bare keys would require additional X11 passive grabs. bareKeypad := false switch key { case KeyKP1, KeyKP2, KeyKP3, KeyKP4, KeyKP5, KeyKP6, KeyKP7, KeyKP8, KeyKP9, KeyKPDot, KeyKPEnter, KeyKPPlus, 74, 98: bareKeypad = true } if mods&fenrirModifier == 0 && (mods != 0 || !bareKeypad) { return Router{}, fmt.Errorf("binding %q requires FENRIR outside bare review keypad keys", binding.Name) } combo := chord{key: key, modifiers: mods} if _, exists := router.bindings[combo]; exists { return Router{}, fmt.Errorf("duplicate keyboard chord in binding %q", binding.Name) } router.bindings[combo] = binding.Command router.doubleBindings[combo] = binding.DoubleCommand router.tripleBindings[combo] = binding.TripleCommand } return router, nil }