Start cthulhu in laptop layout with -l.

This commit is contained in:
Storm Dragon committed 2026-10-26 21:14:22 -04:00
1 parent 7723bd7e09
commit d8b59aa05b
10 files changed
+259 -5

No files matched your search

+10 -1
View File
@@ -110,6 +110,7 @@ type Options struct {
TentacleCgroupParent string
EnableRemote bool
OpenPreferences bool
LaptopLayout bool
newCommands func() commandListener
onPreferences func()
newSound func() soundService
@@ -169,6 +170,12 @@ func Run(ctx context.Context, options Options) error {
if options.Settings != nil {
settings = *options.Settings
}
savedSettings := settings
if options.LaptopLayout {
settings = laptopSettings(settings)
logger.Info("using temporary laptop keyboard layout", "keyboard_layout", settings.KeyboardLayout,
"cthulhu_modifier_keys", settings.CthulhuModifierKeys)
}
if options.newAdapter == nil {
options.newAdapter = func() Adapter {
adapter := atspi.New()
@@ -282,7 +289,9 @@ func Run(ctx context.Context, options Options) error {
focusGate.tentacleOutput = tentacles.output
services = append(services, lifecycle.Service{Name: "tentacles", Start: tentacles.Start, Stop: tentacles.Stop})
}
preferences := newPreferencesWorker(logger, settings, settingsUpdater)
preferences := newPreferencesWorker(logger, savedSettings, settingsUpdater)
preferences.laptopLayout = options.LaptopLayout
preferences.settings = settings
preferences.tentacles = tentacles
if tentacles != nil {
tentacles.readerSettings = preferences
+139
View File
@@ -0,0 +1,139 @@
// 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")
}
}
+1
View File
@@ -47,6 +47,7 @@ type preferencesWorker struct {
globalSettings config.Settings
appSettings map[string]config.Settings
quickSpeech quickSpeechSettings
laptopLayout bool
activeApp string
editApp string
editing config.Settings
+32
View File
@@ -9,8 +9,38 @@ import (
"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
@@ -42,6 +72,7 @@ func (worker *preferencesWorker) switchApplication(ctx context.Context, applicat
}
settings, err := worker.applicationSettings(application)
if err == nil {
settings = worker.sessionKeyboard(settings)
err = worker.applyRuntime(ctx, settings)
}
if err != nil {
@@ -83,5 +114,6 @@ func (worker *preferencesWorker) beginEdit(ctx context.Context, application stri
}
worker.editing = settings
}
worker.editing = worker.sessionKeyboard(worker.editing)
return true
}
+4
View File
@@ -68,6 +68,10 @@ func (worker *preferencesWorker) applyNext(ctx context.Context, next config.Sett
if err := worker.applyRuntime(ctx, next); err != nil {
return worker.response(err.Error(), false)
}
if worker.editingActive {
worker.laptopLayout = false
}
next = worker.retainProfileKeyboard(next, scopeApp)
// An explicit preferences Apply replaces the session's quick speech choices.
if worker.editingActive {
worker.quickSpeech = quickSpeechSettings{}
+4 -3
View File
@@ -22,13 +22,14 @@ func (worker *preferencesWorker) cycle_progress_output() {
default:
next.ProgressBar.OutputMode, message = "both", "Beep and speak progress bar updates"
}
profileNext := worker.retainProfileKeyboard(next, worker.activeApp)
if worker.activeApp != "" {
worker.appSettings[worker.activeApp] = next
worker.appSettings[worker.activeApp] = profileNext
} else {
for application, settings := range worker.appSettings {
worker.appSettings[application] = config.RebaseApplicationSettings(worker.globalSettings, next, settings)
worker.appSettings[application] = config.RebaseApplicationSettings(worker.globalSettings, profileNext, settings)
}
worker.globalSettings = next
worker.globalSettings = profileNext
}
worker.commit_settings(next)
worker.announceSpeechSetting(message)