Default laptop key set to capslock. Keys that can be set to fenrir key or script key selectable via checkbox.

This commit is contained in:
Storm Dragon committed 2026-10-29 18:01:42 -04:00
1 parent 47f18fdd28
commit 15e04aa8f8
47 files changed
+1560 -351

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package preferences
import (
"fmt"
"path/filepath"
"slices"
"git.stormux.org/storm/fenrir/internal/config"
"git.stormux.org/storm/fenrir/internal/input"
"git.stormux.org/storm/fenrir/internal/interaction"
)
type modifierOption struct{ name, Label string }
var modifierOptions = []modifierOption{
{name: "KEY_KP0", Label: "Keypad Insert"},
{name: "KEY_INSERT", Label: "Insert"},
{name: "KEY_CAPSLOCK", Label: "Caps Lock"},
{name: "KEY_META", Label: "Super"},
}
func reader_names(settings config.Settings) []string {
if settings.Keyboard.FenrirKeys != nil {
return settings.Keyboard.FenrirKeys
}
// Running applications supply the resolved layout's keys at startup.
// Standalone forms use the built-in layout defaults.
var names []string
switch filepath.Base(settings.Keyboard.Layout) {
case "laptop", "laptop.toml":
names = []string{"KEY_KP0", "KEY_INSERT", "KEY_CAPSLOCK"}
default:
names = []string{"KEY_KP0", "KEY_INSERT", "KEY_META"}
}
available, err := input.Available_fenrir_keys(names, settings.General.ScriptKeys)
if err != nil {
return []string{}
}
return available
}
func modifier_checked(names []string, name string) bool {
keys, _ := input.Modifier_codes(names)
option, _ := input.Modifier_codes([]string{name})
for key := range option {
if keys[key] {
return true
}
}
return false
}
func modifier_controls(settings config.Settings) []interaction.Control {
var controls []interaction.Control
for _, role := range []struct {
id, Label string
names []string
}{
{id: "fenrir_key.", Label: "Fenrir keys", names: reader_names(settings)},
{id: "script_key.", Label: "Script keys", names: settings.General.ScriptKeys},
} {
options := slices.Clone(modifierOptions)
if role.id == "script_key." {
options = append(options, modifierOption{name: "KEY_COMPOSE", Label: "Compose"})
}
for _, option := range options {
controls = append(controls, interaction.Control{ID: role.id + option.name, Label: option.Label, Group: role.Label, Kind: interaction.CheckBox, Checked: modifier_checked(role.names, option.name)})
}
}
return controls
}
func selected_modifiers(values map[string]interaction.Control, prefix string, names []string, options []modifierOption) ([]string, bool, error) {
selected := slices.Clone(names)
changed := false
for _, option := range options {
control, ok := values["keyboard."+prefix+option.name]
if !ok {
return nil, false, fmt.Errorf("missing modifier checkbox %s", option.name)
}
if control.Checked == modifier_checked(names, option.name) {
continue
}
changed = true
if control.Checked {
selected = append(selected, option.name)
continue
}
selected = slices.DeleteFunc(selected, func(name string) bool { return modifier_checked([]string{name}, option.name) })
}
return selected, changed, nil
}
func modifier_settings(values map[string]interaction.Control, base *config.Settings) error {
reader, changed, err := selected_modifiers(values, "fenrir_key.", reader_names(*base), modifierOptions)
if err != nil {
return err
}
scripts, _, err := selected_modifiers(values, "script_key.", base.General.ScriptKeys, append(slices.Clone(modifierOptions), modifierOption{name: "KEY_COMPOSE", Label: "Compose"}))
if err != nil {
return err
}
if err := input.Validate_modifier_keys(reader, scripts); err != nil {
return err
}
if changed {
base.Keyboard.FenrirKeys = reader
}
base.General.ScriptKeys = scripts
return nil
}
+110
View File
@@ -0,0 +1,110 @@
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package preferences
import (
"path/filepath"
"reflect"
"testing"
"git.stormux.org/storm/fenrir/internal/config"
"git.stormux.org/storm/fenrir/internal/interaction"
)
func focus_modifier(t *testing.T, s *interaction.Session, id string) {
t.Helper()
for range 40 {
if control, ok := s.Focused_control(); ok && control.ID == id {
return
}
s.Handle(interaction.Key{Name: "tab"})
}
t.Fatal("could not focus", id)
}
func TestModifierCheckboxesRejectConflictAndSaveSelections(t *testing.T) {
for _, key := range []string{"KEY_KP0", "KEY_INSERT", "KEY_CAPSLOCK", "KEY_META"} {
base := config.Defaults()
base.Keyboard.Devices = []string{}
base.Keyboard.FenrirKeys = []string{key}
base.General.ScriptKeys = []string{"KEY_COMPOSE", "KEY_F9"}
session, err := New_session(base, []string{"keyboard"})
if err != nil {
t.Fatal(err)
}
for _, role := range []string{"fenrir_key.", "script_key."} {
control := session.Values()["keyboard."+role+key]
if control.Kind != interaction.CheckBox || control.Checked != (role == "fenrir_key.") {
t.Fatal(control)
}
}
focus_modifier(t, session, "script_key."+key)
session.Handle(interaction.Key{Name: "space"})
if _, err := Settings(session, base); err == nil {
t.Fatal("overlapping checkbox roles saved", key)
}
focus_modifier(t, session, "fenrir_key."+key)
session.Handle(interaction.Key{Name: "space"})
if _, err := Settings(session, base); err == nil {
t.Fatal("all reader keys unchecked accepted")
}
replacement := "KEY_INSERT"
if key == replacement {
replacement = "KEY_CAPSLOCK"
}
focus_modifier(t, session, "fenrir_key."+replacement)
session.Handle(interaction.Key{Name: "space"})
draft, err := Settings(session, base)
if err != nil || !modifier_checked(draft.General.ScriptKeys, key) || !modifier_checked(draft.Keyboard.FenrirKeys, replacement) || modifier_checked(draft.Keyboard.FenrirKeys, key) {
t.Fatal(draft.Keyboard, draft.General, err)
}
if !reflect.DeepEqual(base.Keyboard.FenrirKeys, []string{key}) || len(base.General.ScriptKeys) != 2 {
t.Fatal("draft changed original")
}
path := filepath.Join(t.TempDir(), "settings.toml")
doc, err := config.Open_document(path, base)
if err != nil {
t.Fatal(err)
}
if err := doc.Save(draft); err != nil {
t.Fatal(err)
}
saved, _, err := config.Load(path, nil)
if err != nil || !reflect.DeepEqual(saved.Keyboard.FenrirKeys, draft.Keyboard.FenrirKeys) || !reflect.DeepEqual(saved.General.ScriptKeys, draft.General.ScriptKeys) {
t.Fatal(saved.Keyboard, saved.General, err)
}
if !modifier_checked(saved.General.ScriptKeys, "KEY_F9") {
t.Fatal("custom script key lost")
}
}
}
func TestModifierCheckboxesPreserveSideSpecificSuperAndDefaults(t *testing.T) {
base := config.Defaults()
base.Keyboard.Devices = []string{}
base.Keyboard.FenrirKeys = []string{"KEY_RIGHTMETA"}
base.General.ScriptKeys = []string{"KEY_LEFTMETA"}
session, err := New_session(base, []string{"keyboard"})
if err != nil {
t.Fatal(err)
}
unchanged, err := Settings(session, base)
if err != nil || !reflect.DeepEqual(unchanged, base) {
t.Fatal(unchanged, err)
}
base = config.Defaults()
base.Keyboard.Devices = []string{}
base.General.ScriptKeys = []string{"KEY_META", "KEY_COMPOSE"}
session, err = New_session(base, []string{"keyboard"})
if err != nil {
t.Fatal(err)
}
if session.Values()["keyboard.fenrir_key.KEY_META"].Checked || !session.Values()["keyboard.script_key.KEY_META"].Checked {
t.Fatal("defaults claimed existing script key")
}
unchanged, err = Settings(session, base)
if err != nil || !reflect.DeepEqual(unchanged, base) {
t.Fatal(unchanged, err)
}
}
+8
View File
@@ -78,6 +78,9 @@ func new_session(settings config.Settings, order []string, title string, applica
return err
}
}
if section.ID == "keyboard" && !application {
tab.Controls = append(tab.Controls, modifier_controls(settings)...)
}
if section.ID == "presentation.speech" {
driver, _ := speech.Find_driver(settings.Presentation.Speech.Driver)
tab.Controls = append(tab.Controls, interaction.Control{ID: "speech.test", Label: "Test synthesizer", Group: "Speech", Kind: interaction.Button, Action: "test-synthesizer", Hidden: !driver.Hardware})
@@ -219,6 +222,11 @@ func settings_values(session *interaction.Session, base config.Settings, applica
return config.Settings{}, err
}
}
if !application {
if err := modifier_settings(values, &base); err != nil {
return config.Settings{}, err
}
}
// Inactive Speech Dispatcher choices stay in the form's draft for switching
// back, but cannot be sent to or saved as selections for a hardware driver.
for _, definition := range config.Definitions() {
+37 -13
View File
@@ -38,7 +38,7 @@ func TestSettingsFormCoversSchemaAndReturnsIsolatedDraft(t *testing.T) {
t.Fatal(got)
}
values := session.Values()
if len(values) != 55 {
if len(values) != 64 {
t.Fatal("missing settings or buttons", len(values))
}
unchanged, err := Settings(session, base)
@@ -147,24 +147,48 @@ func TestCapitalCheckboxesRoundTripIndependently(t *testing.T) {
}
}
func TestInvalidSettingsEntriesDoNotSubmit(t *testing.T) {
for _, field := range []string{"devices", "layout"} {
session, err := New_session(config.Defaults(), []string{"keyboard"})
func TestInvalidDeviceEntryDoesNotSubmit(t *testing.T) {
session, err := New_session(config.Defaults(), []string{"keyboard"})
if err != nil {
t.Fatal(err)
}
session.Handle(interaction.Key{Name: "tab"})
session.Handle(interaction.Key{Ctrl: true, Text: "a"})
session.Handle(interaction.Key{Text: "invalid value"})
if _, err := Settings(session, config.Defaults()); err == nil {
t.Fatal("invalid devices accepted")
}
}
func TestKeyboardLayoutComboBox(t *testing.T) {
for _, saved := range []string{"desktop", "laptop", "/tmp/custom.toml"} {
base := config.Defaults()
base.Keyboard.Layout = saved
session, err := New_session(base, []string{"keyboard"})
if err != nil {
t.Fatal(err)
}
control := session.Values()["keyboard.layout"]
if control.Kind != interaction.ComboBox || control.Choices[0].ID != "desktop" || control.Choices[1].ID != "laptop" || control.Choices[control.Selected].ID != saved {
t.Fatal(control)
}
session.Handle(interaction.Key{Name: "tab"})
session.Handle(interaction.Key{Name: "tab"})
if field == "layout" {
session.Handle(interaction.Key{Name: "tab"})
}
session.Handle(interaction.Key{Ctrl: true, Text: "a"})
value := "invalid value"
if field == "layout" {
value = "old.conf"
session.Handle(interaction.Key{Text: "old.conf"})
draft, err := Settings(session, base)
if err != nil || draft.Keyboard.Layout != saved {
t.Fatal(draft.Keyboard.Layout, err)
}
session.Handle(interaction.Key{Text: value})
if _, err := Settings(session, config.Defaults()); err == nil {
t.Fatal("invalid field accepted", field)
if saved == "desktop" {
session.Handle(interaction.Key{Name: "down"})
draft, err = Settings(session, base)
if err != nil || draft.Keyboard.Layout != "laptop" {
t.Fatal(draft.Keyboard.Layout, err)
}
if base.Keyboard.Layout != "desktop" {
t.Fatal("draft changed original settings")
}
}
}
}