176 lines
5.6 KiB
Go
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
|
|
}
|