Files

176 lines
5.6 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package xorg
/*
#cgo pkg-config: xkbcommon
#include "commands_native.h"
*/
import "C"
import (
"context"
"errors"
"fmt"
"git.stormux.org/storm/cthulhu/internal/input"
)
type uiReply struct {
session uint64
err error
}
type uiUpdate struct {
ctx context.Context
closeSession uint64
result chan uiReply
}
func (listener *CommandListener) InternalUISession() uint64 { return listener.uiSession.Load() }
func (listener *CommandListener) InternalUIActive() bool { return listener.uiSession.Load() != 0 }
func (listener *CommandListener) InternalUIEchoActive() bool { return listener.uiEcho.Load() }
// BeginInternalUI acknowledges exclusive capture without changing X input
// focus. EndInternalUI waits for physical releases, and cannot close a newer UI.
func (listener *CommandListener) BeginInternalUI(ctx context.Context) (uint64, error) {
return listener.update_internal_ui(ctx, 0)
}
func (listener *CommandListener) EndInternalUI(ctx context.Context, session uint64) error {
if session == 0 {
return errors.New("missing internal UI session")
}
// Revoke promptly even when the acknowledgement must wait for a held key.
if session > listener.uiSequence.Load() {
return errors.New("unknown internal UI session")
}
for previous := listener.uiClose.Load(); previous < session; previous = listener.uiClose.Load() {
if listener.uiClose.CompareAndSwap(previous, session) {
break
}
}
_, err := listener.update_internal_ui(ctx, session)
return err
}
func (listener *CommandListener) update_internal_ui(ctx context.Context, closeSession uint64) (uint64, error) {
listener.mutex.Lock()
done := listener.done
listener.mutex.Unlock()
if done == nil {
return 0, errors.New("command listener is not running")
}
update := uiUpdate{ctx: ctx, closeSession: closeSession, result: make(chan uiReply, 1)}
select {
case listener.uiUpdates <- update:
case <-ctx.Done():
return 0, ctx.Err()
case <-done:
return 0, errors.New("command listener stopped")
}
select {
case result := <-update.result:
return result.session, result.err
case <-done:
return 0, errors.New("command listener stopped")
}
}
func (owner *commandOwner) begin_internal_ui() (uint64, error) {
if !owner.commandGrabsAllowed() || owner.learnMode || owner.ocrReview || owner.uiSession != 0 {
return 0, errors.New("internal UI input unavailable")
}
var revert C.int
C.XGetInputFocus(owner.device.display, &owner.uiFocus, &revert)
if owner.uiFocus == 0 || owner.uiFocus == 1 {
return 0, errors.New("no focused application")
}
owner.uiWindow = C.read_foreground(owner.device.display).window
if status := C.acquire_internal_keyboard(owner.device); status != 0 {
return 0, fmt.Errorf("internal keyboard capture failed: %d", int(status))
}
owner.uiSession = owner.listener.uiSequence.Add(1)
owner.listener.uiSession.Store(owner.uiSession)
owner.listener.uiEcho.Store(true)
owner.listener.grabActive.Store(true)
return owner.uiSession, nil
}
func (owner *commandOwner) dismiss_internal_ui() {
owner.uiSession = 0
owner.listener.uiSession.Store(0)
C.reset_internal_compose(owner.device)
if len(owner.held) == 0 {
C.release_command_keyboard(owner.device)
owner.listener.uiEcho.Store(false)
owner.listener.grabActive.Store(false)
}
}
func (owner *commandOwner) report_ui_failure(err error) bool {
owner.listener.failures <- err
return false
}
func (owner *commandOwner) sync_internal_ui() bool {
if owner.uiSession != 0 {
var focus C.Window
var revert C.int
C.XGetInputFocus(owner.device.display, &focus, &revert)
requestedClose := owner.listener.uiClose.Load() == owner.uiSession
if requestedClose || !owner.commandGrabsAllowed() || focus != owner.uiFocus || C.read_foreground(owner.device.display).window != owner.uiWindow {
session := owner.uiSession
owner.dismiss_internal_ui()
// Retain ownership of the current chord until its releases arrive.
owner.bound = len(owner.held) != 0
if !requestedClose && !owner.listener.enqueueSecureCommand(input.CommandEvent{Command: input.InternalUIDismiss, UISession: session}) {
return owner.report_ui_failure(errors.New("internal UI queue overflow"))
}
}
}
if len(owner.held) == 0 {
select {
case update := <-owner.listener.uiUpdates:
result := uiReply{err: update.ctx.Err()}
if result.err == nil {
if update.closeSession != 0 {
if update.closeSession == owner.uiSession {
owner.dismiss_internal_ui()
}
} else {
result.session, result.err = owner.begin_internal_ui()
}
}
update.result <- result
default:
}
}
return true
}
func (owner *commandOwner) handle_internal_key(key C.CommandKey) bool {
owner.bound = true
owner.router.Handle(uint32(key.symbol), uint16(key.code), uint16(key.modifiers), key.pressed != 0)
if key.pressed == 0 {
return true
}
symbol := uint32(key.symbol)
if key.textSymbol != 0 {
symbol = uint32(key.textSymbol)
}
chord := owner.router.Chord(symbol, uint16(key.modifiers))
// Reader modifiers and their initiating chord never become field contents.
if owner.router.IsModifier(uint32(key.symbol)) {
return true
}
if chord.CthulhuModifier && uint32(key.symbol) != 0xff1b {
return true
}
event := input.CommandEvent{Command: input.InternalUIKey, UISession: owner.uiSession, Timestamp: uint32(key.timestamp), Chord: chord, Text: C.GoString(&key.text[0])}
if uint32(key.symbol) == 0xff1b {
owner.dismiss_internal_ui()
}
if !owner.listener.enqueueSecureCommand(event) {
return owner.report_ui_failure(errors.New("internal UI queue overflow"))
}
return true
}