// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package xorg /* #cgo pkg-config: xkbcommon #cgo pkg-config: xi #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 localOnly bool } 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, false) } // BeginLocalUI admits only device-backed XI2 events and excludes XTEST. This // is a core API, never a Tentacle-controlled form flag. It fails on unsupported // servers; ordinary forms keep their existing input path. func (listener *CommandListener) BeginLocalUI(ctx context.Context) (uint64, error) { return listener.update_internal_ui(ctx, 0, true) } 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, false) return err } func (listener *CommandListener) update_internal_ui(ctx context.Context, closeSession uint64, localOnly bool) (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), localOnly: localOnly} 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_mode(localOnly bool) (uint64, error) { if !owner.commandGrabsAllowed() || owner.learnMode || owner.ocrReview || owner.uiSession != 0 || owner.remote != nil { 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 var status C.int if localOnly { status = C.acquire_local_keyboard(owner.device) } else { status = C.acquire_internal_keyboard(owner.device) } if 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) begin_internal_ui() (uint64, error) { return owner.begin_internal_ui_mode(false) } 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_mode(update.localOnly) } } 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 }