Files

221 lines
7.6 KiB
Go

package xorg
/*
#include "commands_native.h"
#include <X11/XKBlib.h>
*/
import "C"
import (
"context"
"errors"
"fmt"
"git.stormux.org/storm/cthulhu/internal/input"
"strings"
)
type bindingUpdate struct {
tentacles []input.BindingOverride
replaceTentacles bool
ocrForeground uint64
ocrReview *bool
ocrPending *bool
layout input.KeyboardLayout
modifiers []string
replaceKeyboard bool
ctx context.Context
overrides []input.BindingOverride
result chan error
}
// SetKeyBindings validates and acknowledges a native update after the current
// chord releases. Before Start it sets the configuration used at startup.
func (listener *CommandListener) SetKeyBindings(ctx context.Context, overrides []input.BindingOverride) error {
return listener.update_keyboard(ctx, bindingUpdate{overrides: overrides})
}
// SetKeyboardConfiguration applies layout, modifier keys, and shortcuts together
// after held keys release. A failed native grab retains the whole old setup.
func (listener *CommandListener) SetKeyboardConfiguration(ctx context.Context, layout input.KeyboardLayout, modifiers []string, overrides []input.BindingOverride) error {
if layout != input.DesktopKeyboardLayout && layout != input.LaptopKeyboardLayout {
return fmt.Errorf("invalid keyboard layout %d", layout)
}
if _, err := command_modifier_symbols(modifiers); err != nil {
return err
}
return listener.update_keyboard(ctx, bindingUpdate{layout: layout, modifiers: append([]string(nil), modifiers...), overrides: overrides, replaceKeyboard: true})
}
func (listener *CommandListener) update_keyboard(ctx context.Context, update bindingUpdate) error {
if err := ctx.Err(); err != nil {
return err
}
if err := input.ValidateBindingOverrides(update.overrides); err != nil {
return err
}
update.overrides = input.CloneBindingOverrides(update.overrides)
listener.mutex.Lock()
if listener.done == nil {
if update.replaceTentacles {
listener.tentacleBindings = update.tentacles
} else {
listener.bindingOverrides = update.overrides
}
if update.replaceKeyboard {
listener.layout, listener.modifiers = update.layout, update.modifiers
}
listener.mutex.Unlock()
return nil
}
done := listener.done
listener.mutex.Unlock()
update.ctx, update.result = ctx, make(chan error, 1)
select {
case listener.bindingUpdates <- update:
case <-done:
return errors.New("command listener has stopped")
case <-ctx.Done():
return ctx.Err()
}
select {
case err := <-update.result:
return err
case <-done:
return errors.New("command listener has stopped")
}
}
// SetTentacleBindings replaces all runtime suggestions atomically after held
// keys release. Native failure restores the previous complete configuration.
func (listener *CommandListener) SetTentacleBindings(ctx context.Context, bindings []input.BindingOverride) error {
if err := input.ValidateTentacleBindings(bindings); err != nil {
return err
}
return listener.update_keyboard(ctx, bindingUpdate{replaceTentacles: true, tentacles: input.CloneBindingOverrides(bindings)})
}
func (listener *CommandListener) TentacleBindings() []input.BindingOverride {
listener.registrationMutex.RLock()
defer listener.registrationMutex.RUnlock()
return input.CloneBindingOverrides(listener.appliedTentacles)
}
func (listener *CommandListener) publishTentacleBindings(merged []input.BindingOverride) {
listener.registrationMutex.Lock()
defer listener.registrationMutex.Unlock()
listener.appliedTentacles = nil
for _, binding := range merged {
if strings.HasPrefix(string(binding.Command), "tentacle/") {
listener.appliedTentacles = append(listener.appliedTentacles, input.CloneBindingOverrides([]input.BindingOverride{binding})[0])
}
}
}
func command_modifier_symbols(modifiers []string) ([]uint32, error) {
var symbols []uint32
for _, name := range modifiers {
switch name {
case "Insert":
symbols = append(symbols, 0xff63)
case "KP_Insert":
symbols = append(symbols, 0xff9e)
case "Caps_Lock":
symbols = append(symbols, 0xffe5)
default:
return nil, fmt.Errorf("command modifier %q is unsupported; use Insert, KP_Insert, or Caps_Lock", name)
}
}
return symbols, nil
}
func resolvedBindings(layout input.KeyboardLayout, browse bool, overrides []input.BindingOverride) input.CommandBindings {
bindings, _ := input.ResolveCommandBindings(layout, browse, overrides)
return bindings
}
func configureNativeBindings(device *C.CommandDevice, layout input.KeyboardLayout, modifiers []string, overrides []input.BindingOverride) error {
return configure_native_bindings(device, layout, modifiers, overrides, false)
}
func configure_native_bindings(device *C.CommandDevice, layout input.KeyboardLayout, modifiers []string, overrides []input.BindingOverride, ocrReview bool, pending ...bool) error {
symbols, err := command_modifier_symbols(modifiers)
if err != nil {
return err
}
var insertCode, keypadCode, capsCode C.int
for _, symbol := range symbols {
code := C.XKeysymToKeycode(device.display, C.KeySym(symbol))
if code == 0 {
return fmt.Errorf("Cthulhu modifier %x is unavailable on this X11 keymap", symbol)
}
switch symbol {
case 0xff63:
insertCode = C.int(code)
case 0xff9e:
keypadCode = C.int(code)
case 0xffe5:
capsCode = C.int(code)
}
}
global := resolvedBindings(layout, false, overrides)
browse := resolvedBindings(layout, true, overrides)
if ocrReview {
browse = input.OCRCommandBindings(global)
if len(pending) > 0 && pending[0] {
browse = input.OCRPendingBindings(global)
}
}
customizedBrowse := make(map[input.CommandChord]bool)
for _, override := range overrides {
if override.Layout == layout && override.Scope == "browse" {
for _, key := range override.Keys {
chord, _ := input.ParseCommandChord(key)
customizedBrowse[chord] = true
}
}
}
var native []C.NativeBinding
var extraModifierMask uint16
for chord, command := range browse {
if chord.CthulhuModifier {
extraModifierMask |= chord.Modifiers & (8 | 64)
// Such commands are delivered after a screen-reader modifier grab.
// Validate that the native keymap can actually produce the symbol.
code := C.XKeysymToKeycode(device.display, C.KeySym(chord.Symbol))
if code == 0 || uint32(C.XkbKeycodeToKeysym(device.display, code, 0, 0)) != chord.Symbol {
return fmt.Errorf("key %s is unavailable as a base key on this X11 keymap", input.FormatCommandChord(chord))
}
continue
}
value := C.NativeBinding{symbol: C.ulong(chord.Symbol), modifiers: C.uint(chord.Modifiers)}
if global[chord] == "" || ocrReview {
value.browseOnly = 1
}
if customizedBrowse[chord] && !ocrReview {
value.probe = 1
}
if command == input.DocumentActivate {
value.activate = 1
}
native = append(native, value)
}
// Own Control/Shift from their press in temporary review. Otherwise the
// application can see a modifier press whose release the arrow grab owns.
// Unbound chords are replayed whole after all their physical keys release.
if ocrReview && !(len(pending) > 0 && pending[0]) {
for _, symbol := range []uint32{0xffe1, 0xffe2, 0xffe3, 0xffe4} {
for _, mask := range []uint16{0, 1, 4, 5} {
native = append(native, C.NativeBinding{symbol: C.ulong(symbol), modifiers: C.uint(mask), browseOnly: 1})
}
}
}
var first *C.NativeBinding
if len(native) != 0 {
first = &native[0]
}
if err := C.configure_command_bindings(device, first, C.int(len(native)), C.uint(extraModifierMask), insertCode, keypadCode, capsCode); err != 0 {
return fmt.Errorf("change X11 key bindings: native error %d (key unavailable or grabbed by another application)", int(err))
}
return nil
}