package xorg /* #include "commands_native.h" #include */ 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 remote *remoteCaptureOwner 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 } owner.sync_remote_capture() 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 result == 3 { // A changed XI2 hierarchy invalidates the local source set. A // removed device may never release its held keys; retire that chord. session := owner.uiSession owner.end_remote_capture("local") clear(owner.held) owner.buffered = nil owner.router.Reset() owner.bound, owner.passThrough = false, false owner.dismiss_internal_ui() if session != 0 && !owner.listener.enqueueSecureCommand(input.CommandEvent{Command: input.InternalUIDismiss, UISession: session}) { owner.report_ui_failure(errors.New("internal UI queue overflow")) return } 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 or verified i3 no-client focus 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 || owner.listener.bypass.Load()) { 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 || owner.remote != nil) { owner.end_remote_capture("local") 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 len(owner.held) == 0 && (owner.device.bypass != 0) != owner.listener.bypass.Load() { if owner.ocrReview { if !owner.leave_ocr_review() || !owner.listener.enqueueSecureCommand(input.CommandEvent{Command: input.OCRDismiss}) { return false } } previous := owner.device.bypass owner.device.bypass = 0 if owner.listener.bypass.Load() { owner.device.bypass = 1 } if err := configure_context_bindings(owner.device, owner.layout, owner.modifiers, owner.overrides, owner.listener.browse.Load(), false); err != nil { owner.device.bypass = previous owner.listener.failures <- err return false } owner.router.ReplaceBindings(owner.context_bindings()) owner.listener.bypassApplied.Store(owner.device.bypass != 0) owner.listener.browseApplied.Store(owner.device.browse != 0) } 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() && owner.device.bypass == 0) { enabled := owner.listener.browse.Load() && owner.device.bypass == 0 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 } owner.router.ReplaceBindings(owner.context_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) // Remote's lock keys belong to the peer, including its feedback. Local // announcements would cancel incoming speech and repeat the peer's echo. if owner.remote == nil && (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 }