Files
cthulhu/internal/platform/xorg/tentacle_native_test.go

198 lines
6.7 KiB
Go

package xorg
import (
"context"
"git.stormux.org/storm/cthulhu/internal/input"
"os"
"os/exec"
"strings"
"testing"
"time"
)
func TestNativeTentacleBindingsUpdateAndRelease(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_X11") != "1" {
t.Skip("requires isolated X11 display")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Own a foreground client; browse-only grabs deliberately replay when
// no client owns focus or the foreground window has changed.
probe := exec.CommandContext(ctx, "xev", "-name", "Cthulhu Tentacle Bindings", "-event", "keyboard")
if err := probe.Start(); err != nil {
t.Fatal(err)
}
defer func() { _ = probe.Process.Kill(); _ = probe.Wait() }()
window := ""
for window == "" {
output, err := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "^Cthulhu Tentacle Bindings$").Output()
if err == nil {
window = strings.TrimSpace(string(output))
}
select {
case <-ctx.Done():
t.Fatal(ctx.Err())
case <-time.After(time.Millisecond):
}
}
if err := exec.CommandContext(ctx, "xdotool", "windowfocus", window).Run(); err != nil {
t.Fatal(err)
}
listener := NewCommands([]string{"Insert"}, input.DesktopKeyboardLayout)
custom := []input.BindingOverride{
{Layout: input.DesktopKeyboardLayout, Scope: "global", Command: input.Command("tentacle/test/global"), Keys: []string{"Cthulhu+Ctrl+Alt+y"}},
{Layout: input.DesktopKeyboardLayout, Scope: "global", Command: input.Command("tentacle/test/global"), Keys: []string{"F9"}},
{Layout: input.DesktopKeyboardLayout, Scope: "browse", Command: input.Command("tentacle/test/browse"), Keys: []string{"F6"}},
{Layout: input.DesktopKeyboardLayout, Scope: "global", Command: input.Command("tentacle/test/wormhole"), Keys: []string{"Cthulhu+Ctrl+Shift+W"}},
}
if err := listener.SetTentacleBindings(ctx, custom); err != nil {
t.Fatal(err)
}
if err := listener.Start(ctx); err != nil {
t.Fatal(err)
}
defer listener.Stop(context.Background())
key := func(args ...string) {
t.Helper()
if out, err := exec.CommandContext(ctx, "xdotool", args...).CombinedOutput(); err != nil {
t.Fatalf("keys: %v %s", err, out)
}
}
command := func(want input.Command) {
t.Helper()
select {
case got := <-listener.Commands():
if got.Command != want {
t.Fatalf("command %s want %s", got.Command, want)
}
case err := <-listener.Failures():
t.Fatal(err)
case <-ctx.Done():
t.Fatal(ctx.Err())
}
}
key("keydown", "Insert", "keydown", "Control_L", "keydown", "Alt_L", "keydown", "y")
command(input.Command("tentacle/test/global"))
// Queue a new configuration while Y is held: it must wait for releases.
applied := make(chan error, 1)
go func() { applied <- listener.SetTentacleBindings(ctx, nil) }()
select {
case err := <-applied:
t.Fatalf("update completed while keys were held: %v", err)
case <-time.After(40 * time.Millisecond):
}
key("keyup", "Insert", "keyup", "Control_L", "keyup", "Alt_L", "keyup", "y")
if err := <-applied; err != nil {
t.Fatal(err)
}
key("keydown", "Insert", "key", "space", "keyup", "Insert")
command(input.Preferences)
if err := listener.SetTentacleBindings(ctx, custom); err != nil {
t.Fatal(err)
}
key("key", "F9")
command(input.Command("tentacle/test/global"))
key("keydown", "Insert", "keydown", "Control_L", "keydown", "Shift_L", "key", "w", "keyup", "Shift_L", "keyup", "Control_L", "keyup", "Insert")
command(input.Command("tentacle/test/wormhole"))
// All modifiers released: an ordinary W must reach the focused application.
key("key", "w")
key("key", "F9")
command(input.Command("tentacle/test/global"))
if err := listener.SetKeyBindings(ctx, []input.BindingOverride{{Layout: input.DesktopKeyboardLayout, Scope: "global", Command: input.Preferences, Keys: []string{"F9"}}}); err != nil {
t.Fatal(err)
}
key("key", "F9")
command(input.Preferences)
if err := listener.SetKeyBindings(ctx, nil); err != nil {
t.Fatal(err)
}
key("key", "F9")
command(input.Command("tentacle/test/global"))
key("keydown", "Insert", "key", "h", "keyup", "Insert")
command(input.ToggleLearnMode)
key("key", "F9")
command(input.LearnKey)
key("key", "Escape")
command(input.ToggleLearnMode)
listener.SetBrowseNavigation(true)
for !listener.BrowseNavigationEnabled() {
select {
case <-ctx.Done():
t.Fatal(ctx.Err())
case <-time.After(time.Millisecond):
}
}
key("key", "F6")
command(input.Command("tentacle/test/browse"))
listener.SetBrowseNavigation(false)
for listener.browseApplied.Load() {
select {
case <-ctx.Done():
t.Fatal(ctx.Err())
case <-time.After(time.Millisecond):
}
}
key("key", "F6")
key("key", "F9")
command(input.Command("tentacle/test/global")) // F6 outside browse mode must not dispatch.
competitor := NewCommands([]string{"KP_Insert"}, input.LaptopKeyboardLayout)
if err := competitor.SetKeyBindings(ctx, []input.BindingOverride{{Layout: input.LaptopKeyboardLayout, Scope: "global", Command: input.PresentDate, Keys: []string{"F10"}}}); err != nil {
t.Fatal(err)
}
if err := competitor.Start(ctx); err != nil {
t.Fatal(err)
}
collision := input.CloneBindingOverrides(custom)
collision[0].Keys = []string{"F10"}
if err := listener.SetTentacleBindings(ctx, collision); err == nil {
t.Fatal("accepted conflicting native grab")
}
key("key", "F9")
command(input.Command("tentacle/test/global")) // Native rollback retained the old shortcut.
browseCollision := input.CloneBindingOverrides(custom)
browseCollision[2].Keys = []string{"F10"}
if err := listener.SetTentacleBindings(ctx, browseCollision); err == nil {
t.Fatal("accepted conflicting prospective browse grab")
}
key("key", "F9")
command(input.Command("tentacle/test/global"))
if err := competitor.Stop(ctx); err != nil {
t.Fatal(err)
}
unavailable := input.CloneBindingOverrides(custom)
unavailable[0].Keys = []string{"Cthulhu+F35"}
if err := listener.SetTentacleBindings(ctx, unavailable); err == nil {
t.Fatal("accepted unavailable key")
}
key("key", "F9")
command(input.Command("tentacle/test/global")) // Rejected update preserved the previous router.
listener.SetSecureInputActive(true)
for listener.CommandGrabsEnabled() {
select {
case <-ctx.Done():
t.Fatal(ctx.Err())
case <-time.After(time.Millisecond):
}
}
key("key", "F9")
listener.SetSecureInputActive(false)
for !listener.CommandGrabsEnabled() {
select {
case <-ctx.Done():
t.Fatal(ctx.Err())
case <-time.After(time.Millisecond):
}
}
key("key", "F9")
command(input.Command("tentacle/test/global"))
if err := listener.SetTentacleBindings(ctx, nil); err != nil {
t.Fatal(err)
}
if len(listener.TentacleBindings()) != 0 {
t.Fatal("removed bindings still published")
}
key("key", "F9")
key("keydown", "Insert", "key", "space", "keyup", "Insert")
command(input.Preferences)
}