package xorg /* #include "commands_native.h" #include */ 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 device.bypass != 0 { global = input.BypassCommandBindings(global) browse = global } else 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 device.bypass == 0 && 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 }