// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package xorg /* #include "commands_native.h" */ import "C" import ( "context" "errors" "fmt" "git.stormux.org/storm/cthulhu/internal/input" ) // SetOCRReview acknowledges a transactional context change only after every // held chord has released. It belongs on a session worker, not the command // consumer. Failure retains the old context. Secure/terminal policy still wins. // The session must dismiss on real focus changes and ordinary XRecord input. func (listener *CommandListener) SetOCRReview(ctx context.Context, enabled bool) error { return listener.set_ocr_context(ctx, enabled, false, 0) } // SetOCRPending owns Escape only; recognition never claims document navigation. func (listener *CommandListener) SetOCRPending(ctx context.Context, enabled bool) error { return listener.set_ocr_context(ctx, enabled, enabled, 0) } // SetOCRContext admits a live session only in its expected foreground client. func (listener *CommandListener) SetOCRContext(ctx context.Context, foreground uint64, pending bool) error { if foreground == 0 { return errors.New("OCR foreground unavailable") } return listener.set_ocr_context(ctx, true, pending, foreground) } func (listener *CommandListener) set_ocr_context(ctx context.Context, enabled, pending bool, foreground uint64) error { if err := ctx.Err(); err != nil { return err } listener.mutex.Lock() done := listener.done listener.mutex.Unlock() if done == nil { return errors.New("command listener is not running") } update := bindingUpdate{ctx: ctx, ocrReview: &enabled, ocrPending: &pending, ocrForeground: foreground, result: make(chan error, 1)} select { case listener.bindingUpdates <- update: case <-ctx.Done(): return ctx.Err() case <-done: return errors.New("command listener has stopped") } select { case err := <-update.result: return err case <-ctx.Done(): return ctx.Err() case <-done: return errors.New("command listener has stopped") } } func (listener *CommandListener) OCRReviewEnabled() bool { return !listener.bypass.Load() && !listener.ocrPending.Load() && !listener.secure.Load() && listener.commandsEnabled.Load() && listener.commandsApplied.Load() && listener.ocrApplied.Load() } func (owner *commandOwner) context_bindings() input.CommandBindings { if owner.device.bypass != 0 { return input.BypassCommandBindings(owner.globalBindings) } if owner.ocrReview { if owner.ocrPending { return input.OCRPendingBindings(owner.globalBindings) } return input.OCRCommandBindings(owner.globalBindings) } return resolvedBindings(owner.layout, owner.device.browse != 0, owner.overrides) } // Configure grabs and routing together. Native replacement restores old // bindings on error; restore its old mode too before reporting a failed entry. func configure_context_bindings(device *C.CommandDevice, layout input.KeyboardLayout, modifiers []string, overrides []input.BindingOverride, browse, ocrReview bool, pending ...bool) error { previous, enabled := device.browse, device.commands device.browse = 0 if device.bypass == 0 && (browse || ocrReview) { device.browse = 1 } err := configure_native_bindings(device, layout, modifiers, overrides, ocrReview, pending...) if err != nil { device.browse = previous if result := C.set_command_keys(device, enabled); result != 0 { return fmt.Errorf("restore X11 input context: native error %d", int(result)) } return err } device.browseWindow = C.read_foreground(device.display).window return nil } func (owner *commandOwner) leave_ocr_review() bool { if !owner.ocrReview { return true } if err := configure_context_bindings(owner.device, owner.layout, owner.modifiers, owner.overrides, owner.listener.browse.Load(), false); err != nil { owner.listener.failures <- err return false } owner.ocrReview, owner.ocrPending = false, false owner.listener.ocrPending.Store(false) owner.listener.ocrApplied.Store(false) owner.listener.browseApplied.Store(owner.device.browse != 0) owner.router.ReplaceBindings(owner.context_bindings()) return true }