// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "bytes" "errors" "log/slog" "os" "path/filepath" "reflect" "testing" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/input" "git.stormux.org/storm/cthulhu/internal/preferences" ) func TestLaptopLayoutSessionAndExplicitPreferences(t *testing.T) { global := config.DefaultSettings() global.Path = filepath.Join(t.TempDir(), "settings.toml") store, err := config.NewStore(global.Path) if err != nil { t.Fatal(err) } if err := store.Save(global); err != nil { t.Fatal(err) } app := global app.CthulhuModifierKeys = []string{"Insert"} app.TimeFormat = "24-hour" for _, name := range []string{"Cached Application", "New Application"} { if err := store.SaveForApplication(name, app); err != nil { t.Fatal(err) } } original, err := os.ReadFile(global.Path) if err != nil { t.Fatal(err) } worker := newPreferencesWorker(slog.New(slog.DiscardHandler), global, acceptingRemoteSpeechSettings{}) worker.laptopLayout = true worker.settings = worker.sessionKeyboard(global) worker.live = &liveSettings{} worker.live.Store(worker.settings) recorder := &keyboardRecorder{} worker.bindings = recorder checkLaptop := func(settings config.Settings) { t.Helper() if settings.KeyboardLayout != int(input.LaptopKeyboardLayout) || !reflect.DeepEqual(settings.CthulhuModifierKeys, []string{"Caps_Lock"}) { t.Fatalf("laptop keyboard lost: layout=%d keys=%v", settings.KeyboardLayout, settings.CthulhuModifierKeys) } } checkLaptop(worker.settings) worker.cycle_progress_output() if worker.globalSettings.KeyboardLayout != global.KeyboardLayout || !reflect.DeepEqual(worker.globalSettings.CthulhuModifierKeys, global.CthulhuModifierKeys) { t.Fatal("progress shortcut cached the startup keyboard in global settings") } for _, name := range []string{"Cached Application", "Other Application", "Cached Application", "New Application"} { worker.switchApplication(t.Context(), name) if worker.activeApp != name { t.Fatal("application switch failed") } checkLaptop(worker.settings) checkLaptop(worker.live.Load()) if name != "Other Application" && worker.settings.TimeFormat != "24-hour" { t.Fatal("startup keyboard override discarded application preferences") } } current, err := os.ReadFile(global.Path) if err != nil || !bytes.Equal(current, original) { t.Fatalf("switching applications saved the override: %v", err) } // A remote session change and the speech-save shortcut must not promote // the startup keyboard override into global or cached profile settings. next := worker.settings next.EnableKeyEcho = true if response := worker.applyNext(t.Context(), next, false, false); !response.Accepted { t.Fatal(response.Message) } worker.cycle_progress_output() cached := worker.appSettings[worker.activeApp] if cached.KeyboardLayout != app.KeyboardLayout || !reflect.DeepEqual(cached.CthulhuModifierKeys, app.CthulhuModifierKeys) { t.Fatal("session changes cached the startup keyboard in application settings") } worker.handleSpeechCommand(t.Context(), input.IncreaseSpeechSetting) worker.handleSpeechCommand(t.Context(), input.SaveSpeechSettings) loaded, err := store.Load() if err != nil || loaded.KeyboardLayout != global.KeyboardLayout || !reflect.DeepEqual(loaded.CthulhuModifierKeys, global.CthulhuModifierKeys) { t.Fatalf("speech save changed the saved keyboard: %v", err) } worker.switchApplication(t.Context(), "Other Application") worker.switchApplication(t.Context(), "New Application") checkLaptop(worker.settings) if !worker.settings.EnableKeyEcho { t.Fatal("remote application change was lost") } if !worker.beginEdit(t.Context(), "") { t.Fatal("could not open global preferences") } checkLaptop(worker.editing) values := preferences.FromSettings(worker.editing) values.KeyboardLayout = global.KeyboardLayout values.CthulhuModifierKeys = global.CthulhuModifierKeys recorder.err = errors.New("grab conflict") if response := worker.apply(t.Context(), preferences.Request{Values: values}); response.Accepted || !worker.laptopLayout { t.Fatal("failed preferences apply discarded the startup override") } checkLaptop(worker.settings) // Saving the displayed laptop choice explicitly makes it permanent. recorder.err = nil values = preferences.FromSettings(worker.editing) if response := worker.apply(t.Context(), preferences.Request{Values: values, Save: true}); !response.Accepted || worker.laptopLayout { t.Fatalf("explicit save did not replace the startup override: %+v", response) } loaded, err = store.Load() if err != nil { t.Fatal(err) } checkLaptop(loaded) worker.editingActive = false if !worker.beginEdit(t.Context(), "") { t.Fatal("could not reopen preferences") } values = preferences.FromSettings(worker.editing) values.KeyboardLayout = global.KeyboardLayout values.CthulhuModifierKeys = global.CthulhuModifierKeys if response := worker.apply(t.Context(), preferences.Request{Values: values}); !response.Accepted { t.Fatal(response.Message) } worker.editingActive = false worker.switchApplication(t.Context(), "Another Application") if worker.settings.KeyboardLayout != global.KeyboardLayout || !reflect.DeepEqual(worker.settings.CthulhuModifierKeys, global.CthulhuModifierKeys) { t.Fatal("preferences could not replace the laptop keyboard for the session") } }