Add command bypass and internal UI sessions to Wayland keyboard backend
This commit is contained in:
1 parent
bebde8fd5e
commit
fc25f7e7e8
9 files changed
+426
-25
No files matched your search
@@ -43,6 +43,7 @@ type Keyboard struct {
|
||||
done chan struct{}
|
||||
wake chan struct{}
|
||||
updates chan keyboardUpdate
|
||||
uiUpdates chan uiUpdate
|
||||
commands chan input.CommandEvent
|
||||
locks chan input.LockState
|
||||
failures chan error
|
||||
@@ -52,12 +53,17 @@ type Keyboard struct {
|
||||
secureGeneration atomic.Uint64
|
||||
enabled atomic.Bool
|
||||
browse atomic.Bool
|
||||
bypass atomic.Bool
|
||||
nativeButton atomic.Bool
|
||||
applied atomic.Bool
|
||||
browseApplied atomic.Bool
|
||||
bypassApplied atomic.Bool
|
||||
active atomic.Bool
|
||||
learn atomic.Bool
|
||||
learnEcho atomic.Bool
|
||||
uiSession atomic.Uint64
|
||||
uiSequence atomic.Uint64
|
||||
uiClose atomic.Uint64
|
||||
overflow atomic.Uint64
|
||||
intent input.IntentTracker
|
||||
}
|
||||
@@ -69,7 +75,7 @@ func New(modifiers []string, layout input.KeyboardLayout, logger *slog.Logger) *
|
||||
keyboard := &Keyboard{
|
||||
logger: logger,
|
||||
configuration: keyboardConfiguration{layout: layout, modifiers: append([]string(nil), modifiers...)},
|
||||
wake: make(chan struct{}, 1), updates: make(chan keyboardUpdate),
|
||||
wake: make(chan struct{}, 1), updates: make(chan keyboardUpdate), uiUpdates: make(chan uiUpdate),
|
||||
commands: make(chan input.CommandEvent, 32), locks: make(chan input.LockState, 16),
|
||||
failures: make(chan error, 1), events: make(chan input.Event, 64),
|
||||
}
|
||||
@@ -155,6 +161,16 @@ func (keyboard *Keyboard) SetBrowseNavigation(enabled bool) {
|
||||
keyboard.browse.Store(enabled)
|
||||
keyboard.notify()
|
||||
}
|
||||
|
||||
// SetCommandBypass drops every key grab except the bypass shortcut after
|
||||
// held keys release. Watched keys still report monitoring and echo; the
|
||||
// shared interface has no key synthesis, so reader-modifier chords other
|
||||
// than the retained shortcut are swallowed rather than replayed.
|
||||
func (keyboard *Keyboard) SetCommandBypass(enabled bool) {
|
||||
keyboard.bypass.Store(enabled)
|
||||
keyboard.notify()
|
||||
}
|
||||
func (keyboard *Keyboard) CommandBypassEnabled() bool { return keyboard.bypassApplied.Load() }
|
||||
func (keyboard *Keyboard) SetNativeButtonFocus(focused bool) {
|
||||
keyboard.nativeButton.Store(focused)
|
||||
keyboard.notify()
|
||||
|
||||
@@ -56,6 +56,7 @@ type keyboardOwner struct {
|
||||
keyboard *Keyboard
|
||||
connection *dbus.Conn
|
||||
owner string
|
||||
ctx context.Context
|
||||
configuration keyboardConfiguration
|
||||
policy keyboardPolicy
|
||||
bindings input.CommandBindings
|
||||
@@ -63,6 +64,7 @@ type keyboardOwner struct {
|
||||
held map[uint16]uint32
|
||||
captured map[uint16]bool
|
||||
learning bool
|
||||
uiSession uint64
|
||||
started time.Time
|
||||
sequence uint32
|
||||
generation uint64
|
||||
@@ -80,7 +82,7 @@ func (owner *keyboardOwner) requested_policy() keyboardPolicy {
|
||||
owner.learning = false
|
||||
keyboard.learn.Store(false)
|
||||
}
|
||||
return keyboardPolicy{enabled: keyboard.enabled.Load(), browse: keyboard.browse.Load(), button: keyboard.nativeButton.Load(), secure: keyboard.secure.Load(), learn: owner.learning}
|
||||
return keyboardPolicy{enabled: keyboard.enabled.Load(), browse: keyboard.browse.Load(), button: keyboard.nativeButton.Load(), secure: keyboard.secure.Load(), learn: owner.learning, bypass: keyboard.bypass.Load(), ui: owner.uiSession != 0}
|
||||
}
|
||||
|
||||
func (owner *keyboardOwner) retire_secure_generation() {
|
||||
@@ -96,6 +98,14 @@ func (owner *keyboardOwner) retire_secure_generation() {
|
||||
clear(owner.captured)
|
||||
owner.learning = false
|
||||
owner.keyboard.learn.Store(false)
|
||||
if owner.uiSession != 0 {
|
||||
session := owner.uiSession
|
||||
owner.dismiss_internal_ui()
|
||||
select {
|
||||
case owner.keyboard.commands <- input.CommandEvent{Command: input.InternalUIDismiss, UISession: session}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
owner.keyboard.active.Store(false)
|
||||
}
|
||||
|
||||
@@ -104,9 +114,10 @@ func (owner *keyboardOwner) configure(ctx context.Context, configuration keyboar
|
||||
if err := owner.call(ctx, "SetKeyGrabs", symbols, grabs); err != nil {
|
||||
return err
|
||||
}
|
||||
if policy.learn != owner.policy.learn {
|
||||
grabKeyboard := policy.learn || policy.ui
|
||||
if grabKeyboard != (owner.policy.learn || owner.policy.ui) {
|
||||
method := "UngrabKeyboard"
|
||||
if policy.learn {
|
||||
if grabKeyboard {
|
||||
method = "GrabKeyboard"
|
||||
}
|
||||
if err := owner.call(ctx, method); err != nil {
|
||||
@@ -116,7 +127,8 @@ func (owner *keyboardOwner) configure(ctx context.Context, configuration keyboar
|
||||
owner.configuration, owner.policy, owner.bindings = configuration, policy, bindings
|
||||
owner.router = input.NewCommandRouter(symbols, bindings)
|
||||
owner.keyboard.applied.Store(policy.enabled && !policy.secure)
|
||||
owner.keyboard.browseApplied.Store(policy.browse && policy.enabled && !policy.secure)
|
||||
owner.keyboard.browseApplied.Store(policy.browse && !policy.bypass && policy.enabled && !policy.secure)
|
||||
owner.keyboard.bypassApplied.Store(policy.bypass && policy.enabled && !policy.secure)
|
||||
owner.keyboard.learnEcho.Store(policy.learn)
|
||||
return nil
|
||||
}
|
||||
@@ -125,6 +137,8 @@ func (keyboard *Keyboard) run(ctx context.Context, configuration keyboardConfigu
|
||||
defer close(keyboard.done)
|
||||
defer keyboard.applied.Store(false)
|
||||
defer keyboard.browseApplied.Store(false)
|
||||
defer keyboard.bypassApplied.Store(false)
|
||||
defer keyboard.uiSession.Store(0)
|
||||
defer keyboard.active.Store(false)
|
||||
defer keyboard.learn.Store(false)
|
||||
defer keyboard.learnEcho.Store(false)
|
||||
@@ -180,12 +194,17 @@ func (keyboard *Keyboard) run(ctx context.Context, configuration keyboardConfigu
|
||||
}
|
||||
|
||||
func (owner *keyboardOwner) loop(ctx context.Context, queue *signalQueue) error {
|
||||
owner.ctx = ctx
|
||||
keyboard := owner.keyboard
|
||||
var pending *keyboardUpdate
|
||||
var pendingUI *uiUpdate
|
||||
defer func() {
|
||||
if pending != nil {
|
||||
pending.result <- errors.New("Wayland keyboard stopped")
|
||||
}
|
||||
if pendingUI != nil {
|
||||
pendingUI.result <- uiReply{err: errors.New("Wayland keyboard stopped")}
|
||||
}
|
||||
}()
|
||||
timer := time.NewTicker(20 * time.Millisecond)
|
||||
defer timer.Stop()
|
||||
@@ -219,8 +238,38 @@ func (owner *keyboardOwner) loop(ctx context.Context, queue *signalQueue) error
|
||||
return err
|
||||
}
|
||||
}
|
||||
// A close request revokes the session even if its acknowledgement
|
||||
// times out waiting for held keys, mirroring the Xorg owner. The
|
||||
// pending update (if any) then just confirms the ungrab.
|
||||
if owner.uiSession != 0 && keyboard.uiClose.Load() >= owner.uiSession {
|
||||
owner.dismiss_internal_ui()
|
||||
if err := owner.configure(ctx, owner.configuration, owner.requested_policy()); err != nil {
|
||||
return err
|
||||
}
|
||||
policy = owner.requested_policy()
|
||||
}
|
||||
if !stopping && len(owner.held) == 0 {
|
||||
if pending != nil {
|
||||
if pendingUI != nil {
|
||||
update := pendingUI
|
||||
pendingUI = nil
|
||||
reply := uiReply{err: update.ctx.Err()}
|
||||
if reply.err == nil {
|
||||
if update.closeSession != 0 {
|
||||
if update.closeSession == owner.uiSession {
|
||||
owner.dismiss_internal_ui()
|
||||
}
|
||||
if owner.policy.ui {
|
||||
reply.err = owner.configure(update.ctx, owner.configuration, owner.requested_policy())
|
||||
}
|
||||
} else {
|
||||
reply.session, reply.err = owner.begin_internal_ui(update.ctx)
|
||||
}
|
||||
}
|
||||
update.result <- reply
|
||||
if reply.err != nil && update.closeSession != 0 {
|
||||
return reply.err
|
||||
}
|
||||
} else if pending != nil {
|
||||
update := pending
|
||||
pending = nil
|
||||
err := update.ctx.Err()
|
||||
@@ -240,7 +289,7 @@ func (owner *keyboardOwner) loop(ctx context.Context, queue *signalQueue) error
|
||||
keyboard.mutex.Unlock()
|
||||
}
|
||||
update.result <- err
|
||||
} else if policy != owner.policy {
|
||||
} else if policy != owner.policy && (policy.bypass == owner.policy.bypass || len(owner.held) == 0) {
|
||||
if err := owner.configure(ctx, owner.configuration, policy); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -250,18 +299,31 @@ func (owner *keyboardOwner) loop(ctx context.Context, queue *signalQueue) error
|
||||
if pending != nil || stopping {
|
||||
updates = nil
|
||||
}
|
||||
uiUpdates := keyboard.uiUpdates
|
||||
if pendingUI != nil || stopping {
|
||||
uiUpdates = nil
|
||||
}
|
||||
var canceled <-chan struct{}
|
||||
if pending != nil {
|
||||
canceled = pending.ctx.Done()
|
||||
}
|
||||
var canceledUI <-chan struct{}
|
||||
if pendingUI != nil {
|
||||
canceledUI = pendingUI.ctx.Done()
|
||||
}
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-keyboard.wake:
|
||||
case <-canceled:
|
||||
pending.result <- pending.ctx.Err()
|
||||
pending = nil
|
||||
case <-canceledUI:
|
||||
pendingUI.result <- uiReply{err: pendingUI.ctx.Err()}
|
||||
pendingUI = nil
|
||||
case update := <-updates:
|
||||
pending = &update
|
||||
case update := <-uiUpdates:
|
||||
pendingUI = &update
|
||||
case <-queue.overflow:
|
||||
return errors.New("Wayland keyboard signal queue overflow")
|
||||
case <-owner.connection.Context().Done():
|
||||
|
||||
@@ -411,3 +411,51 @@ func TestNativeKeyboardStartsSecureWithoutMonitoringOrGrabs(t *testing.T) {
|
||||
keyboard.SetSecureInputActive(false)
|
||||
wait_snapshot(t, monitor, func(state MonitorSnapshot) bool { return state.watching && len(state.modifiers) == 1 })
|
||||
}
|
||||
|
||||
func TestNativeBypassAndInternalUIGrabLifecycle(t *testing.T) {
|
||||
monitor := start_monitor(t)
|
||||
keyboard := start_keyboard(t, monitor)
|
||||
keyboard.SetBrowseNavigation(true)
|
||||
wait_snapshot(t, monitor, func(state MonitorSnapshot) bool { return len(state.grabs) != 0 })
|
||||
keyboard.SetCommandBypass(true)
|
||||
wait_snapshot(t, monitor, func(state MonitorSnapshot) bool { return len(state.grabs) == 0 })
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for !keyboard.CommandBypassEnabled() {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("bypass was not applied")
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
// The retained toggle chord still dispatches through the modifier watch.
|
||||
monitor.key(t, 0xff63, 118, 0, false)
|
||||
monitor.key(t, 'z', 52, 0, false)
|
||||
await_command(t, keyboard, input.ToggleCommandBypass)
|
||||
monitor.key(t, 'z', 52, 0, true)
|
||||
monitor.key(t, 0xff63, 118, 0, true)
|
||||
keyboard.SetCommandBypass(false)
|
||||
wait_snapshot(t, monitor, func(state MonitorSnapshot) bool { return len(state.grabs) != 0 })
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
session, err := keyboard.BeginInternalUI(ctx)
|
||||
if err != nil || session == 0 {
|
||||
t.Fatalf("begin internal UI: session %d, %v", session, err)
|
||||
}
|
||||
if !keyboard.InternalUIActive() || keyboard.InternalUISession() != session {
|
||||
t.Fatal("internal UI session not published")
|
||||
}
|
||||
wait_snapshot(t, monitor, func(state MonitorSnapshot) bool { return state.grabbed })
|
||||
monitor.key(t, 'a', 38, 0, false)
|
||||
event := await_command(t, keyboard, input.InternalUIKey)
|
||||
if event.UISession != session || event.Text != "a" {
|
||||
t.Fatalf("internal UI key = %+v", event)
|
||||
}
|
||||
monitor.key(t, 'a', 38, 0, true)
|
||||
if err := keyboard.EndInternalUI(ctx, session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wait_snapshot(t, monitor, func(state MonitorSnapshot) bool { return !state.grabbed })
|
||||
if keyboard.InternalUIActive() || keyboard.InternalUISession() != 0 {
|
||||
t.Fatal("internal UI session survived end")
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
type keyGrab struct{ Symbol, Modifiers uint32 }
|
||||
type keyboardPolicy struct{ enabled, browse, button, secure, learn bool }
|
||||
type keyboardPolicy struct{ enabled, browse, button, secure, learn, bypass, ui bool }
|
||||
|
||||
func modifier_symbols(names []string) ([]uint32, error) {
|
||||
var symbols []uint32
|
||||
@@ -53,6 +53,10 @@ func key_name(symbol uint32) string {
|
||||
|
||||
func lower_symbol(symbol uint32) uint32 { return uint32(C.xkb_keysym_to_lower(C.xkb_keysym_t(symbol))) }
|
||||
|
||||
func symbol_utf32(symbol uint32) uint32 {
|
||||
return uint32(C.xkb_keysym_to_utf32(C.xkb_keysym_t(symbol)))
|
||||
}
|
||||
|
||||
// KeyboardMonitor sends keysyms rather than a keymap, and the symbol for a
|
||||
// shifted key is the shifted keysym: Shift+3 arrives as numbersign on a US
|
||||
// layout. Letter case is defined by XKB alone, but a digit or punctuation
|
||||
@@ -165,6 +169,9 @@ func make_grabs(configuration keyboardConfiguration, policy keyboardPolicy) ([]u
|
||||
}
|
||||
}
|
||||
}
|
||||
if policy.bypass {
|
||||
bindings = input.BypassCommandBindings(bindings)
|
||||
}
|
||||
if !policy.enabled || policy.secure {
|
||||
return []uint32{}, []keyGrab{}, bindings
|
||||
}
|
||||
@@ -250,10 +257,13 @@ func (owner *keyboardOwner) handle_key(key keyEvent) error {
|
||||
}
|
||||
if key.released {
|
||||
delete(owner.captured, key.code)
|
||||
} else if consumed || owner.learning {
|
||||
} else if consumed || owner.learning || owner.uiSession != 0 {
|
||||
owner.captured[key.code] = true
|
||||
}
|
||||
keyboard.active.Store(len(owner.captured) != 0 || owner.learning)
|
||||
keyboard.active.Store(len(owner.captured) != 0 || owner.learning || owner.uiSession != 0)
|
||||
if owner.uiSession != 0 {
|
||||
return owner.handle_ui_key(key, symbol, chord, repeated, timestamp)
|
||||
}
|
||||
if !key.released && !repeated && owner.learning {
|
||||
command = input.LearnKey
|
||||
switch {
|
||||
@@ -276,7 +286,16 @@ func (owner *keyboardOwner) handle_key(key keyEvent) error {
|
||||
keyboard.learnEcho.Store(true)
|
||||
}
|
||||
if command != "" && !repeated {
|
||||
event := input.CommandEvent{Command: command, Timestamp: timestamp, Chord: chord, KeyName: key_name(symbol), LearnModeStarted: command == input.ToggleLearnMode && owner.learning}
|
||||
uiSession := uint64(0)
|
||||
if command == input.ShowActions && !key.released {
|
||||
// Capture exclusively before reporting the command, as on Xorg.
|
||||
// An unavailable modal grab is a failed command, not a failed
|
||||
// reader: session zero makes dispatch announce unavailability.
|
||||
if session, err := owner.begin_internal_ui(owner.ctx); err == nil {
|
||||
uiSession = session
|
||||
}
|
||||
}
|
||||
event := input.CommandEvent{Command: command, UISession: uiSession, Timestamp: timestamp, Chord: chord, KeyName: key_name(symbol), LearnModeStarted: command == input.ToggleLearnMode && owner.learning}
|
||||
select {
|
||||
case keyboard.commands <- event:
|
||||
default:
|
||||
|
||||
@@ -317,3 +317,70 @@ func TestUnknownModifierBitsDoNotDispatchTruncatedChord(t *testing.T) {
|
||||
t.Fatal("wide modifier mask became Cthulhu+Q")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandBypassDropsGrabsButKeepsToggle(t *testing.T) {
|
||||
keyboard := New([]string{"Insert"}, input.DesktopKeyboardLayout, nil)
|
||||
policy := keyboardPolicy{enabled: true, browse: true, bypass: true}
|
||||
symbols, grabs, bindings := make_grabs(keyboard.configuration, policy)
|
||||
if len(grabs) != 0 {
|
||||
t.Fatalf("bypass retained key grabs: %v", grabs)
|
||||
}
|
||||
owner := &keyboardOwner{keyboard: keyboard, configuration: keyboard.configuration, policy: policy,
|
||||
bindings: bindings, router: input.NewCommandRouter(symbols, bindings),
|
||||
held: make(map[uint16]uint32), captured: make(map[uint16]bool), started: time.Now()}
|
||||
// Browse keys are no longer grabbed, so the compositor would deliver
|
||||
// them to the application; the watched event still reports monitoring.
|
||||
send_key(t, owner, 'h', 43, 0, false)
|
||||
send_key(t, owner, 'h', 43, 0, true)
|
||||
if len(keyboard.commands) != 0 {
|
||||
t.Fatal("browse key dispatched a command during bypass")
|
||||
}
|
||||
if len(keyboard.events) == 0 {
|
||||
t.Fatal("bypass lost watched typing for echo")
|
||||
}
|
||||
// Reader chords other than the retained toggle dispatch nothing.
|
||||
send_key(t, owner, 0xff63, 118, 0, false)
|
||||
send_key(t, owner, 't', 20, 0, false)
|
||||
send_key(t, owner, 't', 20, 0, true)
|
||||
send_key(t, owner, 0xff63, 118, 0, true)
|
||||
if len(keyboard.commands) != 0 {
|
||||
t.Fatal("reader chord dispatched during bypass")
|
||||
}
|
||||
send_key(t, owner, 0xff63, 118, 0, false)
|
||||
send_key(t, owner, 'z', 52, 0, false)
|
||||
next_command(t, keyboard, input.ToggleCommandBypass)
|
||||
}
|
||||
|
||||
func TestInternalUIKeyRoutingAndEscapeDismiss(t *testing.T) {
|
||||
owner := test_owner(t, input.DesktopKeyboardLayout, "Insert")
|
||||
owner.uiSession = 7
|
||||
owner.keyboard.uiSession.Store(7)
|
||||
send_key(t, owner, 'a', 38, 0, false)
|
||||
event := next_command(t, owner.keyboard, input.InternalUIKey)
|
||||
if event.UISession != 7 || event.Text != "a" {
|
||||
t.Fatalf("UI text event = %+v", event)
|
||||
}
|
||||
send_key(t, owner, 'a', 38, 0, true)
|
||||
send_key(t, owner, 0xff09, 23, 0, false)
|
||||
event = next_command(t, owner.keyboard, input.InternalUIKey)
|
||||
if event.Chord.Symbol != 0xff09 || event.Text != "" {
|
||||
t.Fatalf("UI tab event = %+v", event)
|
||||
}
|
||||
send_key(t, owner, 0xff09, 23, 0, true)
|
||||
// Reader chords never become field contents.
|
||||
send_key(t, owner, 0xff63, 118, 0, false)
|
||||
send_key(t, owner, 'x', 53, 0, false)
|
||||
send_key(t, owner, 'x', 53, 0, true)
|
||||
send_key(t, owner, 0xff63, 118, 0, true)
|
||||
if len(owner.keyboard.commands) != 0 {
|
||||
t.Fatal("reader chord reached internal UI")
|
||||
}
|
||||
send_key(t, owner, 0xff1b, 9, 0, false)
|
||||
event = next_command(t, owner.keyboard, input.InternalUIKey)
|
||||
if event.UISession != 7 || event.Chord.Symbol != 0xff1b {
|
||||
t.Fatalf("UI escape event = %+v", event)
|
||||
}
|
||||
if owner.uiSession != 0 || owner.keyboard.InternalUIActive() {
|
||||
t.Fatal("Escape did not dismiss the UI session")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package wayland
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"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 (keyboard *Keyboard) InternalUISession() uint64 { return keyboard.uiSession.Load() }
|
||||
func (keyboard *Keyboard) InternalUIActive() bool { return keyboard.uiSession.Load() != 0 }
|
||||
|
||||
// InternalUIEchoActive follows the session itself; the owner retains the
|
||||
// compositor grab until the session's last held key is released and the
|
||||
// ungrab is applied, so echo suppression never outlives key capture by
|
||||
// more than one policy cycle.
|
||||
func (keyboard *Keyboard) InternalUIEchoActive() bool { return keyboard.uiSession.Load() != 0 }
|
||||
|
||||
// BeginInternalUI acknowledges exclusive capture through the shared
|
||||
// interface's GrabKeyboard. EndInternalUI waits for physical releases,
|
||||
// and cannot close a newer UI. There is deliberately no BeginLocalUI:
|
||||
// the protocol cannot distinguish device-backed from synthesized keys,
|
||||
// so local approval input stays unavailable on this backend.
|
||||
func (keyboard *Keyboard) BeginInternalUI(ctx context.Context) (uint64, error) {
|
||||
return keyboard.update_internal_ui(ctx, 0)
|
||||
}
|
||||
|
||||
func (keyboard *Keyboard) 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 > keyboard.uiSequence.Load() {
|
||||
return errors.New("unknown internal UI session")
|
||||
}
|
||||
for previous := keyboard.uiClose.Load(); previous < session; previous = keyboard.uiClose.Load() {
|
||||
if keyboard.uiClose.CompareAndSwap(previous, session) {
|
||||
break
|
||||
}
|
||||
}
|
||||
_, err := keyboard.update_internal_ui(ctx, session)
|
||||
return err
|
||||
}
|
||||
|
||||
func (keyboard *Keyboard) update_internal_ui(ctx context.Context, closeSession uint64) (uint64, error) {
|
||||
keyboard.mutex.Lock()
|
||||
done := keyboard.done
|
||||
keyboard.mutex.Unlock()
|
||||
if done == nil {
|
||||
return 0, errors.New("Wayland keyboard is not running")
|
||||
}
|
||||
update := uiUpdate{ctx: ctx, closeSession: closeSession, result: make(chan uiReply, 1)}
|
||||
select {
|
||||
case keyboard.uiUpdates <- update:
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
case <-done:
|
||||
return 0, errors.New("Wayland keyboard stopped")
|
||||
}
|
||||
select {
|
||||
case result := <-update.result:
|
||||
return result.session, result.err
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
case <-done:
|
||||
return 0, errors.New("Wayland keyboard stopped")
|
||||
}
|
||||
}
|
||||
|
||||
// begin_internal_ui runs on the owner goroutine. The compositor grab is
|
||||
// applied before the session is acknowledged, mirroring the Xorg owner.
|
||||
func (owner *keyboardOwner) begin_internal_ui(ctx context.Context) (uint64, error) {
|
||||
if !owner.policy.enabled || owner.policy.secure || owner.learning || owner.uiSession != 0 {
|
||||
return 0, errors.New("internal UI input unavailable")
|
||||
}
|
||||
policy := owner.policy
|
||||
policy.ui = true
|
||||
if err := owner.configure(ctx, owner.configuration, policy); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
owner.uiSession = owner.keyboard.uiSequence.Add(1)
|
||||
owner.keyboard.uiSession.Store(owner.uiSession)
|
||||
return owner.uiSession, nil
|
||||
}
|
||||
|
||||
// dismiss_internal_ui only clears session identity. The owner loop's
|
||||
// next policy sync releases the compositor grab, mirroring how secure
|
||||
// generations retire held state outside handle_key.
|
||||
func (owner *keyboardOwner) dismiss_internal_ui() {
|
||||
owner.uiSession = 0
|
||||
owner.keyboard.uiSession.Store(0)
|
||||
}
|
||||
|
||||
// handle_ui_key consumes one key for the active session. Reader modifiers
|
||||
// and their chords never become field contents; Escape reports the key
|
||||
// and dismisses the session, matching the Xorg owner.
|
||||
func (owner *keyboardOwner) handle_ui_key(key keyEvent, symbol uint32, chord input.CommandChord, repeated bool, timestamp uint32) error {
|
||||
if key.released || repeated {
|
||||
return nil
|
||||
}
|
||||
if owner.router.IsModifier(key.symbol) {
|
||||
return nil
|
||||
}
|
||||
if chord.CthulhuModifier && symbol != 0xff1b {
|
||||
return nil
|
||||
}
|
||||
event := input.CommandEvent{
|
||||
Command: input.InternalUIKey,
|
||||
UISession: owner.uiSession,
|
||||
Timestamp: timestamp,
|
||||
Chord: chord,
|
||||
Text: key_text(key, symbol),
|
||||
}
|
||||
if symbol == 0xff1b {
|
||||
owner.dismiss_internal_ui()
|
||||
}
|
||||
select {
|
||||
case owner.keyboard.commands <- event:
|
||||
return nil
|
||||
default:
|
||||
return errors.New("Wayland command queue overflow")
|
||||
}
|
||||
}
|
||||
|
||||
// key_text prefers the character reported by the shared interface and
|
||||
// falls back to the keysym's Unicode mapping. Control characters never
|
||||
// become field text.
|
||||
func key_text(key keyEvent, symbol uint32) string {
|
||||
for _, candidate := range []uint32{key.character, symbol_utf32(symbol), symbol_utf32(key.symbol)} {
|
||||
if candidate >= 0x20 && candidate != 0x7f && candidate <= 0x10ffff {
|
||||
return string(rune(candidate))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in new issue
Block a user