Files

167 lines
5.9 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 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
}