Start cthulhu in laptop layout with -l.
This commit is contained in:
1 parent
7723bd7e09
commit
d8b59aa05b
10 files changed
+259
-5
No files matched your search
+10
-1
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user