198 lines
6.7 KiB
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)
|
|
}
|