283 lines
10 KiB
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
|
|
}
|