129 lines
3.9 KiB
Go
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)
|
|
}
|