// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later // Package wayland implements the shared accessibility KeyboardMonitor interface. // It has no compositor names, configuration formats, or desktop-specific paths. package wayland import ( "context" "errors" "fmt" "log/slog" "sync" "sync/atomic" "time" "git.stormux.org/storm/cthulhu/internal/input" ) type keyboardConfiguration struct { layout input.KeyboardLayout modifiers []string bindings []input.BindingOverride } type keyboardUpdate struct { ctx context.Context configuration keyboardConfiguration result chan error } // Keyboard owns both monitoring and commands. Only its run goroutine touches // the connection, pressed keys, and command router. Requested policy is atomic; // binding changes are acknowledged after held keys release. Stop joins that // owner and closes its private connection, releasing every compositor grab. type Keyboard struct { logger *slog.Logger mutex sync.Mutex configuration keyboardConfiguration started bool cancel context.CancelFunc done chan struct{} wake chan struct{} updates chan keyboardUpdate commands chan input.CommandEvent locks chan input.LockState failures chan error events chan input.Event secureMutex sync.Mutex secure atomic.Bool secureGeneration atomic.Uint64 enabled atomic.Bool browse atomic.Bool nativeButton atomic.Bool applied atomic.Bool browseApplied atomic.Bool active atomic.Bool learn atomic.Bool learnEcho atomic.Bool overflow atomic.Uint64 intent input.IntentTracker } func New(modifiers []string, layout input.KeyboardLayout, logger *slog.Logger) *Keyboard { if logger == nil { logger = slog.New(slog.DiscardHandler) } keyboard := &Keyboard{ logger: logger, configuration: keyboardConfiguration{layout: layout, modifiers: append([]string(nil), modifiers...)}, wake: make(chan struct{}, 1), updates: make(chan keyboardUpdate), commands: make(chan input.CommandEvent, 32), locks: make(chan input.LockState, 16), failures: make(chan error, 1), events: make(chan input.Event, 64), } keyboard.enabled.Store(true) return keyboard } // Monitor shares input with the command owner; its lifecycle is owned by Keyboard. type Monitor struct{ keyboard *Keyboard } func (keyboard *Keyboard) Monitor() *Monitor { return &Monitor{keyboard: keyboard} } func (monitor *Monitor) Start(ctx context.Context) error { return ctx.Err() } func (monitor *Monitor) Stop(context.Context) error { return nil } func (monitor *Monitor) Events() <-chan input.Event { return monitor.keyboard.events } func (monitor *Monitor) Overflow() uint64 { return monitor.keyboard.overflow.Load() } func (monitor *Monitor) ModifierKeycodes() input.ModifierKeycodes { return input.ModifierKeycodes{} } func (monitor *Monitor) RecentInput() input.Activity { return monitor.keyboard.intent.RecentInput() } func (monitor *Monitor) RecentIntent() input.Intent { return monitor.keyboard.intent.RecentIntent() } func (monitor *Monitor) SetSecureInputActive(active bool) { monitor.keyboard.SetSecureInputActive(active) } func (keyboard *Keyboard) Start(ctx context.Context) error { keyboard.mutex.Lock() defer keyboard.mutex.Unlock() if keyboard.started { return errors.New("Wayland keyboard already started") } if err := ctx.Err(); err != nil { return err } if err := validate_configuration(keyboard.configuration); err != nil { return err } keyboard.started = true workerContext, cancel := context.WithCancel(ctx) keyboard.cancel, keyboard.done = cancel, make(chan struct{}) setup := make(chan error, 1) go keyboard.run(workerContext, keyboard.configuration, setup) return <-setup } func (keyboard *Keyboard) Stop(ctx context.Context) error { keyboard.mutex.Lock() cancel, done := keyboard.cancel, keyboard.done keyboard.mutex.Unlock() if cancel == nil { return nil } cancel() select { case <-done: return nil case <-ctx.Done(): return ctx.Err() } } func (keyboard *Keyboard) Commands() <-chan input.CommandEvent { return keyboard.commands } func (keyboard *Keyboard) LockStates() <-chan input.LockState { return keyboard.locks } func (keyboard *Keyboard) Failures() <-chan error { return keyboard.failures } func (keyboard *Keyboard) CommandGrabsEnabled() bool { return keyboard.applied.Load() && keyboard.enabled.Load() && !keyboard.secure.Load() } func (keyboard *Keyboard) BrowseNavigationEnabled() bool { return keyboard.CommandGrabsEnabled() && keyboard.browse.Load() && keyboard.browseApplied.Load() } func (keyboard *Keyboard) CommandGrabActive() bool { return keyboard.active.Load() } func (keyboard *Keyboard) LearnModeActive() bool { return keyboard.learn.Load() } func (keyboard *Keyboard) LearnModeEchoActive() bool { return keyboard.learnEcho.Load() } func (keyboard *Keyboard) notify() { select { case keyboard.wake <- struct{}{}: default: } } func (keyboard *Keyboard) SetCommandGrabs(enabled bool) { keyboard.enabled.Store(enabled) keyboard.notify() } func (keyboard *Keyboard) SetBrowseNavigation(enabled bool) { keyboard.browse.Store(enabled) keyboard.notify() } func (keyboard *Keyboard) SetNativeButtonFocus(focused bool) { keyboard.nativeButton.Store(focused) keyboard.notify() } func (keyboard *Keyboard) SetSecureInputActive(active bool) { keyboard.secureMutex.Lock() defer keyboard.secureMutex.Unlock() if keyboard.secure.Swap(active) != active { keyboard.secureGeneration.Add(1) } keyboard.intent.Reset() if active { keyboard.browse.Store(false) } for { select { case <-keyboard.events: case <-keyboard.commands: case <-keyboard.locks: default: keyboard.notify() return } } } func validate_configuration(configuration keyboardConfiguration) error { if configuration.layout != input.DesktopKeyboardLayout && configuration.layout != input.LaptopKeyboardLayout { return fmt.Errorf("invalid keyboard layout %d", configuration.layout) } if _, err := modifier_symbols(configuration.modifiers); err != nil { return err } return input.ValidateBindingOverrides(configuration.bindings) } func (keyboard *Keyboard) SetKeyBindings(ctx context.Context, bindings []input.BindingOverride) error { keyboard.mutex.Lock() configuration := keyboard.configuration keyboard.mutex.Unlock() return keyboard.SetKeyboardConfiguration(ctx, configuration.layout, configuration.modifiers, bindings) } func (keyboard *Keyboard) SetKeyboardConfiguration(ctx context.Context, layout input.KeyboardLayout, modifiers []string, bindings []input.BindingOverride) error { configuration := keyboardConfiguration{layout: layout, modifiers: append([]string(nil), modifiers...), bindings: input.CloneBindingOverrides(bindings)} if err := ctx.Err(); err != nil { return err } if err := validate_configuration(configuration); err != nil { return err } keyboard.mutex.Lock() if !keyboard.started { keyboard.configuration = configuration keyboard.mutex.Unlock() return nil } done := keyboard.done keyboard.mutex.Unlock() update := keyboardUpdate{ctx: ctx, configuration: configuration, result: make(chan error, 1)} select { case keyboard.updates <- update: case <-done: return errors.New("Wayland keyboard is unavailable") case <-ctx.Done(): return ctx.Err() } // The owner resolves cancellation before returning, so a failed preference // update cannot silently commit after its caller has rolled back settings. select { case err := <-update.result: return err case <-done: return errors.New("Wayland keyboard stopped") } } func (keyboard *Keyboard) fail(err error) { select { case keyboard.failures <- err: default: } } const callTimeout = 750 * time.Millisecond