Files
cthulhu/internal/platform/xorg/commands_owner_loop.go

283 lines
10 KiB
Go

package xorg
/*
#include "commands_native.h"
#include <X11/XKBlib.h>
*/
import "C"
import (
"context"
"errors"
"fmt"
"time"
"git.stormux.org/storm/cthulhu/internal/input"
)
// commandOwner is confined to the X11 command goroutine and its locked OS thread.
// Its chord buffer and router are reset on completed chords and secure input;
// the whole owner is discarded when the listener stops.
type commandOwner struct {
listener *CommandListener
workerContext context.Context
device *C.CommandDevice
layout input.KeyboardLayout
modifiers []string
overrides []input.BindingOverride
core, tentacles []input.BindingOverride
symbols []uint32
globalBindings input.CommandBindings
router *input.CommandRouter
held map[int]bool
buffered []C.CommandKey
bound, passThrough, learnMode bool
ocrReview, dismissOCR bool
ocrPending bool
uiSession uint64
uiWindow, uiFocus C.Window
lastLocks, lastFocusMatch C.int
lastFocusCheck, drainDeadline time.Time
steamFocus steamFocusRepair
}
func (listener *CommandListener) runCommandLoop(workerContext context.Context, device *C.CommandDevice, layout input.KeyboardLayout, modifiers []string, core, tentacles []input.BindingOverride, symbols []uint32) {
owner := &commandOwner{listener: listener, workerContext: workerContext, device: device, layout: layout, modifiers: modifiers, core: core, tentacles: tentacles, overrides: input.MergeTentacleBindings(core, tentacles), symbols: symbols, held: make(map[int]bool)}
owner.run()
}
func (owner *commandOwner) run() {
owner.publishFocus()
owner.globalBindings = resolvedBindings(owner.layout, false, owner.overrides)
owner.router = input.NewCommandRouter(owner.symbols, owner.globalBindings)
owner.lastLocks = C.command_lock_state(owner.device)
for {
if owner.workerContext.Err() != nil {
if !owner.bound || len(owner.held) == 0 {
break
}
// Quit dispatches on press. Keep consuming its physical releases
// during shutdown, bounded in case a device disappears.
if owner.drainDeadline.IsZero() {
owner.drainDeadline = time.Now().Add(time.Second)
}
if time.Now().After(owner.drainDeadline) {
break
}
}
if !owner.sync_internal_ui() {
return
}
if !owner.applyBindingUpdate() {
return
}
if time.Since(owner.lastFocusCheck) >= xtermFocusCheckInterval {
owner.check_steam_focus(owner.publishFocus())
}
owner.resetForSecureInput()
if !owner.syncGrabs() {
return
}
var key C.CommandKey
result := C.next_command_key(owner.device, &key)
if result < 0 {
owner.listener.failures <- fmt.Errorf("X11 command listener failed: %d", int(result))
return
}
if result == 0 {
continue
}
if result == 2 {
owner.publishLocks(key)
continue
}
if !owner.handleKey(key) {
return
}
}
if len(owner.buffered) != 0 && !owner.bound {
result := C.replay_command_keys(owner.device, &owner.buffered[0], C.int(len(owner.buffered)))
if result != 0 {
owner.listener.failures <- fmt.Errorf("replay interrupted X11 chord: native error %d", int(result))
}
}
}
func (owner *commandOwner) publishFocus() steamFocusSample {
match, steam := read_command_focus(owner.device.display, !owner.listener.secure.Load() && owner.workerContext.Err() == nil)
owner.lastFocusCheck = time.Now()
if match == 0 || C.int(match) == owner.lastFocusMatch {
return steam
}
owner.lastFocusMatch = C.int(match)
state := FocusState{Known: true, XTerm: match == 1}
owner.listener.SetCommandGrabs(!state.XTerm)
owner.listener.publishFocusState(state)
return steam
}
// Native xterm focus vetoes delayed accessibility requests to restore grabs.
// lastFocusMatch belongs to this owner and survives unknown focus samples;
// only a known non-xterm window ends the handoff.
func (owner *commandOwner) commandGrabsAllowed() bool {
return owner.lastFocusMatch != 1 && owner.listener.commandsEnabled.Load() && !owner.listener.secure.Load()
}
func (owner *commandOwner) applyBindingUpdate() bool {
if len(owner.held) == 0 {
select {
case update := <-owner.listener.bindingUpdates:
nextLayout, nextModifiers := owner.layout, owner.modifiers
if update.replaceKeyboard {
nextLayout, nextModifiers = update.layout, update.modifiers
}
nextCore, nextTentacles := owner.core, owner.tentacles
if update.replaceTentacles {
nextTentacles = update.tentacles
} else if update.ocrReview == nil {
nextCore = update.overrides
}
nextOverrides, nextOCR := input.MergeTentacleBindings(nextCore, nextTentacles), owner.ocrReview
nextPending := owner.ocrPending
if update.ocrPending != nil {
nextPending = *update.ocrPending
}
if update.ocrReview != nil {
nextOverrides, nextOCR = owner.overrides, *update.ocrReview
}
err := update.ctx.Err()
if err == nil && nextOCR && (!owner.commandGrabsAllowed() || owner.learnMode) {
err = errors.New("OCR review input unavailable")
}
if err == nil && nextOCR {
foreground := C.read_foreground(owner.device.display)
if foreground.known == 0 || foreground.window == 0 || update.ocrForeground != 0 && uint64(foreground.window) != update.ocrForeground {
err = errors.New("OCR foreground unavailable")
}
}
if err == nil {
err = configure_context_bindings(owner.device, nextLayout, nextModifiers, nextOverrides, owner.listener.browse.Load(), nextOCR, nextPending)
}
if err == nil {
owner.layout, owner.modifiers, owner.overrides = nextLayout, nextModifiers, nextOverrides
owner.core, owner.tentacles = nextCore, nextTentacles
owner.listener.publishTentacleBindings(nextOverrides)
owner.ocrReview, owner.ocrPending = nextOCR, nextPending
owner.listener.ocrPending.Store(nextPending)
owner.listener.ocrApplied.Store(nextOCR)
owner.listener.browseApplied.Store(owner.device.browse != 0 && !nextOCR)
owner.symbols, _ = command_modifier_symbols(owner.modifiers)
owner.globalBindings = resolvedBindings(owner.layout, false, owner.overrides)
owner.router = input.NewCommandRouter(owner.symbols, owner.context_bindings())
}
update.result <- err
if owner.device.commands == 0 && owner.listener.commandsApplied.Load() {
owner.listener.failures <- errors.New("X11 could not restore command grabs")
return false
}
default:
}
}
return true
}
func (owner *commandOwner) resetForSecureInput() {
if owner.listener.secure.Load() && (owner.ocrReview || owner.device.commands != 0 || owner.device.active != 0 || len(owner.held) != 0) {
C.release_command_keyboard(owner.device)
C.set_command_keys(owner.device, 0)
owner.listener.commandsApplied.Store(false)
owner.listener.grabActive.Store(false)
owner.learnMode = false
owner.listener.learnMode.Store(false)
owner.listener.learnEcho.Store(false)
owner.dismiss_internal_ui()
owner.listener.uiEcho.Store(false)
clear(owner.held)
owner.buffered = nil
owner.bound, owner.passThrough = false, false
owner.ocrReview, owner.ocrPending = false, false
owner.listener.ocrPending.Store(false)
owner.listener.ocrApplied.Store(false)
if err := configure_context_bindings(owner.device, owner.layout, owner.modifiers, owner.overrides, false, false); err != nil {
owner.listener.failures <- err
}
owner.router = input.NewCommandRouter(owner.symbols, resolvedBindings(owner.layout, false, owner.overrides))
}
}
func (owner *commandOwner) syncGrabs() bool {
if owner.ocrReview && len(owner.held) == 0 {
foreground := C.read_foreground(owner.device.display)
if foreground.known == 0 || foreground.window == 0 || foreground.window != owner.device.browseWindow {
if !owner.leave_ocr_review() {
return false
}
if !owner.listener.enqueueSecureCommand(input.CommandEvent{Command: input.OCRDismiss}) {
return false
}
}
}
enabled := owner.commandGrabsAllowed()
if owner.ocrReview && !enabled && len(owner.held) == 0 {
if !owner.leave_ocr_review() {
return false
}
}
if len(owner.held) == 0 && !enabled && owner.learnMode {
// Learn mode keeps an explicit keyboard grab between chords. Removing
// passive command grabs alone cannot give that keyboard back to Fenrir.
C.release_command_keyboard(owner.device)
owner.learnMode = false
owner.router.Reset()
owner.listener.grabActive.Store(false)
owner.listener.learnMode.Store(false)
owner.listener.learnEcho.Store(false)
}
if len(owner.held) == 0 && (owner.device.commands != 0) != enabled {
value := C.int(0)
if enabled {
value = 1
}
if err := C.set_command_keys(owner.device, value); err != 0 {
owner.listener.failures <- fmt.Errorf("change X11 command grabs: native error %d", int(err))
return false
}
owner.listener.commandsApplied.Store(enabled)
}
if !owner.ocrReview && len(owner.held) == 0 && (owner.device.commands != 0) &&
(owner.device.browse != 0) != owner.listener.browse.Load() {
enabled := owner.listener.browse.Load()
value := C.int(0)
if enabled {
value = 1
}
if err := C.set_browse_keys(owner.device, value); err != 0 {
owner.listener.failures <- fmt.Errorf("change X11 browse grabs: native error %d", int(err))
return false
}
bindings := resolvedBindings(owner.layout, false, owner.overrides)
if enabled {
bindings = resolvedBindings(owner.layout, true, owner.overrides)
}
owner.router.ReplaceBindings(bindings)
owner.listener.browseApplied.Store(enabled)
}
owner.device.nativeButtonFocus = 0
if owner.listener.nativeButtonFocus.Load() && !owner.ocrReview {
owner.device.nativeButtonFocus = 1
}
owner.listener.nativeButtonApplied.Store(owner.device.nativeButtonFocus != 0)
return true
}
func (owner *commandOwner) publishLocks(key C.CommandKey) {
for index, name := range []string{"Caps Lock", "Num Lock", "Scroll Lock"} {
mask := C.int(1 << index)
if (key.locks^owner.lastLocks)&mask != 0 {
owner.listener.publish_lock_state(input.LockState{Name: name, Locked: key.locks&mask != 0, Timestamp: uint32(key.timestamp)})
}
}
owner.lastLocks = key.locks
}