refactor(tracker/gioui): unify default & user config yaml handling

This commit is contained in:
5684185+vsariola@users.noreply.github.com 2025-05-23 23:35:51 +03:00
parent 5b260d19f5
commit 32f1e1baea
4 changed files with 58 additions and 73 deletions

View File

@ -24,33 +24,17 @@ type (
var keyBindingMap = map[key.Event]string{} var keyBindingMap = map[key.Event]string{}
var keyActionMap = map[KeyAction]string{} // holds an informative string of the first key bound to an action var keyActionMap = map[KeyAction]string{} // holds an informative string of the first key bound to an action
func loadCustomKeyBindings() []KeyBinding {
var keyBindings []KeyBinding
_, err := ReadCustomConfigYml("keybindings.yml", &keyBindings)
if err != nil {
return nil
}
if len(keyBindings) == 0 {
return nil
}
return keyBindings
}
//go:embed keybindings.yml //go:embed keybindings.yml
var defaultKeyBindingsYaml []byte var defaultKeyBindings []byte
func loadDefaultKeyBindings() []KeyBinding {
var keyBindings []KeyBinding
err := yaml.UnmarshalStrict(defaultKeyBindingsYaml, &keyBindings)
if err != nil {
panic(fmt.Errorf("failed to unmarshal keybindings: %w", err))
}
return keyBindings
}
func init() { func init() {
keyBindings := loadDefaultKeyBindings() var keyBindings, userKeybindings []KeyBinding
keyBindings = append(keyBindings, loadCustomKeyBindings()...) if err := yaml.UnmarshalStrict(defaultKeyBindings, &keyBindings); err != nil {
panic(fmt.Errorf("failed to unmarshal default keybindings: %w", err))
}
if err := ReadCustomConfig("keybindings.yml", &userKeybindings); err == nil {
keyBindings = append(keyBindings, userKeybindings...)
}
for _, kb := range keyBindings { for _, kb := range keyBindings {
var mods key.Modifiers var mods key.Modifiers

View File

@ -13,8 +13,7 @@ import (
type ( type (
Preferences struct { Preferences struct {
Window WindowPreferences Window WindowPreferences
YmlError error
} }
WindowPreferences struct { WindowPreferences struct {
@ -25,39 +24,39 @@ type (
) )
//go:embed preferences.yml //go:embed preferences.yml
var defaultPreferencesYaml []byte var defaultPreferences []byte
func loadDefaultPreferences() Preferences { // ReadCustomConfig modifies the target argument, i.e. needs a pointer. Just
var preferences Preferences // fails silently if the file cannot be found/read, but will warn about
err := yaml.UnmarshalStrict(defaultPreferencesYaml, &preferences) // malformed files.
if err != nil { func ReadCustomConfig(filename string, target any) error {
panic(fmt.Errorf("failed to unmarshal preferences: %w", err))
}
return preferences
}
// ReadCustomConfigYml modifies the target argument, i.e. needs a pointer
func ReadCustomConfigYml(filename string, target interface{}) (exists bool, err error) {
configDir, err := os.UserConfigDir() configDir, err := os.UserConfigDir()
if err != nil { if err != nil {
return false, err return nil
} }
path := filepath.Join(configDir, "sointu", filename) path := filepath.Join(configDir, "sointu", filename)
bytes, err2 := os.ReadFile(path) bytes, err := os.ReadFile(path)
if err2 != nil { if err != nil {
return false, err2 return nil
} }
err = yaml.Unmarshal(bytes, target) if err := yaml.Unmarshal(bytes, target); err != nil {
return true, err return fmt.Errorf("ReadCustomConfig %v: %w", filename, err)
}
return nil
} }
func MakePreferences() Preferences { // ReadConfig first unmarshals the defaultConfig which should be the embedded
preferences := loadDefaultPreferences() // default config, and then tries to read the custom config with
exists, err := ReadCustomConfigYml("preferences.yml", &preferences) // ReadCustomConfig. It panics right away if the embedded defaultConfig could
if exists { // not be parsed as yaml as this should never happen except during development.
preferences.YmlError = err // The returned error should be treated as a warning: this function will always
// return at least the default config, and the warning will just tell if there
// was a problem parsing the custom config.
func ReadConfig(defaultConfig []byte, path string, target any) (warn error) {
if err := yaml.UnmarshalStrict(defaultConfig, target); err != nil {
panic(fmt.Errorf("ReadConfig %v failed to unmarshal the embedded default config: %w", path, err))
} }
return preferences return ReadCustomConfig(path, target)
} }
func (p Preferences) WindowSize() (unit.Dp, unit.Dp) { func (p Preferences) WindowSize() (unit.Dp, unit.Dp) {

View File

@ -2,14 +2,12 @@ package gioui
import ( import (
_ "embed" _ "embed"
"fmt"
"image/color" "image/color"
"gioui.org/text" "gioui.org/text"
"gioui.org/widget" "gioui.org/widget"
"gioui.org/widget/material" "gioui.org/widget/material"
"golang.org/x/exp/shiny/materialdesign/icons" "golang.org/x/exp/shiny/materialdesign/icons"
"gopkg.in/yaml.v2"
) )
type Theme struct { type Theme struct {
@ -120,19 +118,16 @@ type CursorStyle struct {
//go:embed theme.yml //go:embed theme.yml
var defaultTheme []byte var defaultTheme []byte
func NewTheme() *Theme { // NewTheme returns a new theme and potentially a warning if the theme file was not found or could not be read
var theme Theme func NewTheme() (*Theme, error) {
err := yaml.UnmarshalStrict(defaultTheme, &theme) var ret Theme
if err != nil { warn := ReadConfig(defaultTheme, "theme.yml", &ret)
panic(fmt.Errorf("failed to default theme: %w", err)) ret.Material.Shaper = &text.Shaper{}
} ret.Material.Icon.CheckBoxChecked = must(widget.NewIcon(icons.ToggleCheckBox))
ReadCustomConfigYml("theme.yml", &theme) ret.Material.Icon.CheckBoxUnchecked = must(widget.NewIcon(icons.ToggleCheckBoxOutlineBlank))
theme.Material.Shaper = &text.Shaper{} ret.Material.Icon.RadioChecked = must(widget.NewIcon(icons.ToggleRadioButtonChecked))
theme.Material.Icon.CheckBoxChecked = must(widget.NewIcon(icons.ToggleCheckBox)) ret.Material.Icon.RadioUnchecked = must(widget.NewIcon(icons.ToggleRadioButtonUnchecked))
theme.Material.Icon.CheckBoxUnchecked = must(widget.NewIcon(icons.ToggleCheckBoxOutlineBlank)) return &ret, warn
theme.Material.Icon.RadioChecked = must(widget.NewIcon(icons.ToggleRadioButtonChecked))
theme.Material.Icon.RadioUnchecked = must(widget.NewIcon(icons.ToggleRadioButtonUnchecked))
return &theme
} }
func must[T any](ic T, err error) T { func must[T any](ic T, err error) T {

View File

@ -71,7 +71,6 @@ var ZoomFactors = []float32{.25, 1. / 3, .5, 2. / 3, .75, .8, 1, 1.1, 1.25, 1.5,
func NewTracker(model *tracker.Model) *Tracker { func NewTracker(model *tracker.Model) *Tracker {
t := &Tracker{ t := &Tracker{
Theme: NewTheme(),
OctaveNumberInput: NewNumberInput(model.Octave().Int()), OctaveNumberInput: NewNumberInput(model.Octave().Int()),
InstrumentVoices: NewNumberInput(model.InstrumentVoices().Int()), InstrumentVoices: NewNumberInput(model.InstrumentVoices().Int()),
@ -93,15 +92,23 @@ func NewTracker(model *tracker.Model) *Tracker {
Model: model, Model: model,
filePathString: model.FilePath().String(), filePathString: model.FilePath().String(),
preferences: MakePreferences(), }
t.PopupAlert = NewPopupAlert(model.Alerts())
var warn error
if t.Theme, warn = NewTheme(); warn != nil {
model.Alerts().AddAlert(tracker.Alert{
Priority: tracker.Warning,
Message: warn.Error(),
Duration: 10 * time.Second,
})
} }
t.Theme.Material.Shaper = text.NewShaper(text.WithCollection(gofont.Collection())) t.Theme.Material.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
t.PopupAlert = NewPopupAlert(model.Alerts()) if warn := ReadConfig(defaultPreferences, "preferences.yml", &t.preferences); warn != nil {
if t.preferences.YmlError != nil { model.Alerts().AddAlert(tracker.Alert{
model.Alerts().Add( Priority: tracker.Warning,
fmt.Sprintf("Preferences YML Error: %s", t.preferences.YmlError), Message: warn.Error(),
tracker.Warning, Duration: 10 * time.Second,
) })
} }
t.TrackEditor.scrollTable.Focus() t.TrackEditor.scrollTable.Focus()
return t return t