244 lines
7.9 KiB
Go
244 lines
7.9 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
|
|
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
|