Files

129 lines
3.9 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package xorg
import (
"context"
"os"
"os/exec"
"strings"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/input"
)
func TestNativeLockStatesAndShiftCapsLock(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()
listener := NewCommands([]string{"Caps_Lock"}, input.LaptopKeyboardLayout)
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)
}
}
state := func(name string, locked bool) {
t.Helper()
select {
case event := <-listener.LockStates():
if event.Name != name || event.Locked != locked || event.Timestamp == 0 {
t.Fatalf("lock state = %+v, want %s %v with timestamp", event, name, locked)
}
case err := <-listener.Failures():
t.Fatal(err)
case <-ctx.Done():
t.Fatalf("missing lock state %s %v", name, locked)
}
}
command := func(want input.Command) {
t.Helper()
select {
case event := <-listener.Commands():
if event.Command != want || event.Timestamp == 0 {
t.Fatalf("command = %+v, want %s with timestamp", event, want)
}
case err := <-listener.Failures():
t.Fatal(err)
case <-ctx.Done():
t.Fatalf("missing command %s", want)
}
}
lockMask := func() string {
t.Helper()
out, err := exec.CommandContext(ctx, "xset", "q").Output()
if err != nil {
t.Fatal(err)
}
fields := strings.Fields(string(out))
for index, field := range fields {
if field == "LED" && index+2 < len(fields) {
return fields[index+2]
}
}
t.Fatal("missing lock state")
return ""
}
initial := lockMask()
// Caps Lock alone and Shift+CapsLock+V must not toggle capitalization.
key("key", "Caps_Lock")
key("keydown", "Shift_L", "keydown", "Caps_Lock", "key", "v", "keyup", "Shift_L", "keyup", "Caps_Lock")
command(input.PresentVersion)
if lockMask() != initial {
t.Fatal("version chord changed capitalization")
}
for _, shift := range []string{"Shift_L", "Shift_R"} {
key("keydown", shift, "key", "Caps_Lock", "keyup", shift)
state("Caps Lock", true)
if lockMask() == initial {
t.Fatal("Shift+CapsLock did not enable capitalization")
}
// The reader modifier and commands still work while capitalization is on.
key("keydown", "Caps_Lock", "key", "t", "keyup", "Caps_Lock")
command(input.PresentTime)
key("keydown", "Caps_Lock", "keydown", shift, "key", "v", "keyup", "Caps_Lock", "keyup", shift)
command(input.PresentVersion)
if lockMask() == initial {
t.Fatal("reader chord cleared capitalization")
}
// Reverse modifier order and release Shift first.
key("keydown", "Caps_Lock", "keydown", shift, "keyup", shift, "keyup", "Caps_Lock")
state("Caps Lock", false)
if lockMask() != initial {
t.Fatal("Shift+CapsLock did not disable capitalization")
}
}
key("key", "Num_Lock")
state("Num Lock", true)
key("key", "Num_Lock")
state("Num Lock", false)
// A default keymap may leave Scroll Lock without a lock action. Exercise
// the XKB indicator itself to verify its actual state notifications.
for _, enabled := range []bool{true, false} {
option := "led"
if !enabled {
option = "-led"
}
if out, err := exec.CommandContext(ctx, "xset", option, "named", "Scroll Lock").CombinedOutput(); err != nil {
t.Fatalf("Scroll Lock indicator: %v %s", err, out)
}
state("Scroll Lock", enabled)
}
if err := listener.SetKeyboardConfiguration(ctx, input.LaptopKeyboardLayout, nil, nil); err != nil {
t.Fatal(err)
}
key("key", "Caps_Lock")
state("Caps Lock", true)
key("key", "Caps_Lock")
state("Caps Lock", false)
}