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