// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package input import ( "testing" "time" "git.stormux.org/storm/fenrir/internal/command" ) func TestTriplePressSequences(t *testing.T) { for _, layout := range []string{"desktop", "laptop"} { for _, modifierFirst := range []bool{false, true} { t.Run(layout+"/"+map[bool]string{false: "key first", true: "modifier first"}[modifierFirst], func(t *testing.T) { bindings, _, err := Load_bindings([]string{"../../config/keyboard/" + layout + ".toml"}) if err != nil { t.Fatal(err) } primary := uint16(KeyKP5) target := command.ReadScreen if layout == "laptop" { primary = 37 target = command.CurrentWord } for index := range bindings.Bindings { if bindings.Bindings[index].Command == target { bindings.Bindings[index].Command = command.CurrentWord bindings.Bindings[index].DoubleCommand = command.CurrentChar bindings.Bindings[index].TripleCommand = command.CurrentLine } } router, err := New_router(bindings) if err != nil { t.Fatal(err) } if err := router.Set_multi_press_timeout(700 * time.Millisecond); err != nil { t.Fatal(err) } now := time.Unix(1000, 0) router.now = func() time.Time { return now } for _, want := range []command.ID{command.CurrentWord, command.CurrentChar, command.CurrentLine, command.CurrentWord} { router.Key(1, KeyInsert, 1, false, false) got := router.Key(1, primary, 1, false, false) if got.Command != want || got.Forward || !got.Keypress || !got.Interrupt { t.Fatal(got, want) } // Both evdev repeats and duplicate X11 presses stay in the held action. for _, value := range []int32{2, 1, 2} { got := router.Key(1, primary, value, false, false) expected := command.ID("") if value == 2 { expected = want } if got.Command != expected || got.Keypress || got.Forward { t.Fatal("held", got, want) } } releases := []uint16{primary, KeyInsert} if modifierFirst { releases[0], releases[1] = releases[1], releases[0] } for _, key := range releases { if got := router.Key(1, key, 0, false, false); got.Forward || got.Command != "" { t.Fatal("release", got) } } // Total triple duration exceeds the window; each adjacent gap fits. now = now.Add(600 * time.Millisecond) } }) } } } func TestTriplePressBoundariesAndInvalidation(t *testing.T) { for _, scenario := range []string{"inside", "at boundary", "outside", "backward clock", "other chord", "typing", "numlock", "reset", "capture", "drop", "repair", "unchanged repair", "other device", "pass next", "reconfigure"} { t.Run(scenario, func(t *testing.T) { router, err := New_router(Layout{Bindings: []Binding{{Name: "Review", Keys: []string{"KP5"}, Command: command.CurrentWord, DoubleCommand: command.CurrentChar, TripleCommand: command.CurrentLine}, {Name: "Other", Keys: []string{"FENRIR", "KP5"}, Command: command.Time, DoubleCommand: command.Date}}}) if err != nil { t.Fatal(err) } now := time.Unix(1000, 0) router.now = func() time.Time { return now } for _, want := range []command.ID{command.CurrentWord, command.CurrentChar} { if got := router.Key(1, KeyKP5, 1, false, false); got.Command != want { t.Fatal(got) } router.Key(1, KeyKP5, 0, false, false) now = now.Add(100 * time.Millisecond) } want, device := command.CurrentWord, 1 switch scenario { case "inside": want = command.CurrentLine case "at boundary": now = now.Add(100 * time.Millisecond) want = command.CurrentLine case "outside": now = now.Add(100*time.Millisecond + time.Nanosecond) case "backward clock": now = now.Add(-101 * time.Millisecond) case "other chord": router.Key(1, KeyInsert, 1, false, false) router.Key(1, KeyKP5, 1, false, false) router.Key(1, KeyKP5, 0, false, false) router.Key(1, KeyInsert, 0, false, false) case "typing": if got := router.Key(1, 30, 1, false, false); !got.Typing || !got.Forward { t.Fatal(got) } router.Key(1, 30, 0, false, false) case "numlock": if got := router.Key(1, KeyKP5, 1, true, false); !got.Forward { t.Fatal(got) } router.Key(1, KeyKP5, 0, true, false) case "reset": router.Reset() case "capture": router.Capture(true) if got := router.Key(1, KeyKP5, 1, false, false); got.UI == nil || got.Command != "" || got.Forward { t.Fatal(got) } router.Key(1, KeyKP5, 0, false, false) router.Capture(false) case "drop": router.Drop(1) case "repair": router.Key(1, KeyShift, 1, false, true) router.Key(1, KeyShift, 0, false, true) case "unchanged repair": router.Key(1, KeyShift, 0, false, true) want = command.CurrentLine case "other device": device = 2 case "pass next": router.Pass_next() if got := router.Key(1, KeyKP5, 1, false, false); !got.Forward || got.Command != "" { t.Fatal(got) } router.Key(1, KeyKP5, 0, false, false) case "reconfigure": if err := router.Set_multi_press_timeout(500 * time.Millisecond); err != nil { t.Fatal(err) } } if got := router.Key(device, KeyKP5, 1, false, false); got.Command != want || got.Forward { t.Fatal(got, want) } }) } } func TestTripleBindingValidation(t *testing.T) { for _, data := range []string{ "version=1\n[[bindings]]\nname='Review'\nkeys=['KP5']\ncommand='current_word'\ntriple_command='current_line'\n", "version=1\n[[bindings]]\nname='Review'\nkeys=['KP5']\ncommand='current_word'\ndouble_command='current_character'\ntriple_command='unknown'\n", } { if _, err := Decode_bindings([]byte(data)); err == nil { t.Fatal("invalid triple accepted", data) } } router := desktop_router(t) for _, timeout := range []time.Duration{0, -time.Second, time.Millisecond - time.Nanosecond, 5*time.Second + time.Nanosecond} { if err := router.Set_multi_press_timeout(timeout); err == nil { t.Fatal(timeout) } } } func TestRetainedFeatureKeyNames(t *testing.T) { for _, key := range []string{"F1", "KPMINUS", "KPSLASH", "RIGHTBRACE"} { for _, bare := range []bool{false, true} { keys := []string{"FENRIR", key} if bare { keys = keys[1:] } _, err := New_router(Layout{Bindings: []Binding{{Name: "Retained key", Keys: keys, Command: command.Help}}}) wantValid := !bare || key == "KPMINUS" || key == "KPSLASH" if (err == nil) != wantValid { t.Fatal(key, bare, err) } } } }