194 lines
4.8 KiB
Go
194 lines
4.8 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>.
|
|
|
|
//go:build linux && cgo
|
|
|
|
package input
|
|
|
|
/*
|
|
#cgo pkg-config: libevdev
|
|
#include <libevdev/libevdev.h>
|
|
#include <stdlib.h>
|
|
*/
|
|
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
|
|
}
|