221 lines
7.6 KiB
Go
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
|
|
}
|