// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "strings" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/input" ) func laptopSettings(settings config.Settings) config.Settings { settings.KeyboardLayout = int(input.LaptopKeyboardLayout) settings.CthulhuModifierKeys = []string{"Caps_Lock"} return settings } // Keep the startup keyboard choice out of profile settings and speech saves. // Explicit preferences Apply replaces it; otherwise it lasts until exit. func (worker *preferencesWorker) sessionKeyboard(settings config.Settings) config.Settings { if worker.laptopLayout { return laptopSettings(settings) } return settings } // Shortcut and remote changes start from runtime settings. Keep the profile's // keyboard choice beneath the startup override when caching those changes. func (worker *preferencesWorker) retainProfileKeyboard(settings config.Settings, application string) config.Settings { if worker.laptopLayout { base := worker.globalSettings if cached, ok := worker.appSettings[application]; ok { base = cached } settings.KeyboardLayout = base.KeyboardLayout settings.CthulhuModifierKeys = append([]string(nil), base.CthulhuModifierKeys...) } return settings } func (worker *preferencesWorker) applicationSettings(application string) (config.Settings, error) { if settings, ok := worker.appSettings[application]; ok { return worker.quickSpeech.apply(settings), nil } store, err := config.NewStore(worker.globalSettings.Path) if err != nil { return config.Settings{}, err } persistedGlobal, err := store.Load() if err != nil { return config.Settings{}, err } settings, mapped, err := store.LoadForApplication(application) if err != nil { return config.Settings{}, err } if !mapped { return worker.globalSettings, nil } settings = config.RebaseApplicationSettings(persistedGlobal, worker.globalSettings, settings) worker.appSettings[application] = settings return worker.quickSpeech.apply(settings), nil } func (worker *preferencesWorker) switchApplication(ctx context.Context, application string) { application = strings.TrimSpace(application) if application == "" || application == worker.activeApp { return } settings, err := worker.applicationSettings(application) if err == nil { settings = worker.sessionKeyboard(settings) err = worker.applyRuntime(ctx, settings) } if err != nil { worker.logger.Error("switch application settings", "application", application, "error", err) return } worker.activeApp = application if worker.live != nil { worker.live.application.Store(&application) worker.live.notify_clipboard() } } func (worker *preferencesWorker) beginEdit(ctx context.Context, application string) bool { // A focus event and its shortcut can arrive on separate goroutines. Use the // latest accepted focus before deciding which application to edit. select { case focused := <-worker.applicationChanges: worker.switchApplication(ctx, focused) default: } if application == "\x00" { application = worker.activeApp if application == "" { worker.announceSpeechSetting("No active application for application preferences.") return false } } worker.editApp = application worker.editingActive = true worker.editing = worker.globalSettings if application != "" { settings, err := worker.applicationSettings(application) if err != nil { worker.logger.Error("load application settings", "application", application, "error", err) worker.announceSpeechSetting("Could not load application preferences.") worker.editingActive = false return false } worker.editing = settings } worker.editing = worker.sessionKeyboard(worker.editing) return true }