Files

261 lines
8.6 KiB
Go

// 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
uiUpdates chan uiUpdate
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
bypass atomic.Bool
nativeButton atomic.Bool
applied atomic.Bool
browseApplied atomic.Bool
bypassApplied atomic.Bool
active atomic.Bool
learn atomic.Bool
learnEcho atomic.Bool
uiEcho atomic.Bool
uiSession atomic.Uint64
uiSequence atomic.Uint64
uiClose atomic.Uint64
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), uiUpdates: make(chan uiUpdate),
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()
}
// SetCommandBypass drops every key grab except the bypass shortcut after
// held keys release. Watched keys still report monitoring and echo; the
// shared interface has no key synthesis, so reader-modifier chords other
// than the retained shortcut are swallowed rather than replayed.
func (keyboard *Keyboard) SetCommandBypass(enabled bool) {
keyboard.bypass.Store(enabled)
keyboard.notify()
}
func (keyboard *Keyboard) CommandBypassEnabled() bool { return keyboard.bypassApplied.Load() }
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