Files

118 lines
3.9 KiB
Go

// 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.ocrPending.Load() && !listener.secure.Load() && listener.commandsEnabled.Load() && listener.commandsApplied.Load() && listener.ocrApplied.Load()
}
func (owner *commandOwner) context_bindings() input.CommandBindings {
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 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
}