Files
fenrir/internal/input/script_keys_linux.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
}