diff --git a/tracker/gioui/keybindings.go b/tracker/gioui/keybindings.go index e4eddac..f156480 100644 --- a/tracker/gioui/keybindings.go +++ b/tracker/gioui/keybindings.go @@ -24,33 +24,17 @@ type ( var keyBindingMap = map[key.Event]string{} 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 -var defaultKeyBindingsYaml []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 -} +var defaultKeyBindings []byte func init() { - keyBindings := loadDefaultKeyBindings() - keyBindings = append(keyBindings, loadCustomKeyBindings()...) + var keyBindings, userKeybindings []KeyBinding + 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 { var mods key.Modifiers diff --git a/tracker/gioui/preferences.go b/tracker/gioui/preferences.go index 2a04384..19a5b07 100644 --- a/tracker/gioui/preferences.go +++ b/tracker/gioui/preferences.go @@ -13,8 +13,7 @@ import ( type ( Preferences struct { - Window WindowPreferences - YmlError error + Window WindowPreferences } WindowPreferences struct { @@ -25,39 +24,39 @@ type ( ) //go:embed preferences.yml -var defaultPreferencesYaml []byte +var defaultPreferences []byte -func loadDefaultPreferences() Preferences { - var preferences Preferences - err := yaml.UnmarshalStrict(defaultPreferencesYaml, &preferences) - if err != nil { - 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) { +// ReadCustomConfig modifies the target argument, i.e. needs a pointer. Just +// fails silently if the file cannot be found/read, but will warn about +// malformed files. +func ReadCustomConfig(filename string, target any) error { configDir, err := os.UserConfigDir() if err != nil { - return false, err + return nil } path := filepath.Join(configDir, "sointu", filename) - bytes, err2 := os.ReadFile(path) - if err2 != nil { - return false, err2 + bytes, err := os.ReadFile(path) + if err != nil { + return nil } - err = yaml.Unmarshal(bytes, target) - return true, err + if err := yaml.Unmarshal(bytes, target); err != nil { + return fmt.Errorf("ReadCustomConfig %v: %w", filename, err) + } + return nil } -func MakePreferences() Preferences { - preferences := loadDefaultPreferences() - exists, err := ReadCustomConfigYml("preferences.yml", &preferences) - if exists { - preferences.YmlError = err +// ReadConfig first unmarshals the defaultConfig which should be the embedded +// default config, and then tries to read the custom config with +// ReadCustomConfig. It panics right away if the embedded defaultConfig could +// not be parsed as yaml as this should never happen except during development. +// 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) { diff --git a/tracker/gioui/theme.go b/tracker/gioui/theme.go index c263934..f4ae234 100644 --- a/tracker/gioui/theme.go +++ b/tracker/gioui/theme.go @@ -2,14 +2,12 @@ package gioui import ( _ "embed" - "fmt" "image/color" "gioui.org/text" "gioui.org/widget" "gioui.org/widget/material" "golang.org/x/exp/shiny/materialdesign/icons" - "gopkg.in/yaml.v2" ) type Theme struct { @@ -120,19 +118,16 @@ type CursorStyle struct { //go:embed theme.yml var defaultTheme []byte -func NewTheme() *Theme { - var theme Theme - err := yaml.UnmarshalStrict(defaultTheme, &theme) - if err != nil { - panic(fmt.Errorf("failed to default theme: %w", err)) - } - ReadCustomConfigYml("theme.yml", &theme) - theme.Material.Shaper = &text.Shaper{} - theme.Material.Icon.CheckBoxChecked = must(widget.NewIcon(icons.ToggleCheckBox)) - theme.Material.Icon.CheckBoxUnchecked = must(widget.NewIcon(icons.ToggleCheckBoxOutlineBlank)) - theme.Material.Icon.RadioChecked = must(widget.NewIcon(icons.ToggleRadioButtonChecked)) - theme.Material.Icon.RadioUnchecked = must(widget.NewIcon(icons.ToggleRadioButtonUnchecked)) - return &theme +// NewTheme returns a new theme and potentially a warning if the theme file was not found or could not be read +func NewTheme() (*Theme, error) { + var ret Theme + warn := ReadConfig(defaultTheme, "theme.yml", &ret) + ret.Material.Shaper = &text.Shaper{} + ret.Material.Icon.CheckBoxChecked = must(widget.NewIcon(icons.ToggleCheckBox)) + ret.Material.Icon.CheckBoxUnchecked = must(widget.NewIcon(icons.ToggleCheckBoxOutlineBlank)) + ret.Material.Icon.RadioChecked = must(widget.NewIcon(icons.ToggleRadioButtonChecked)) + ret.Material.Icon.RadioUnchecked = must(widget.NewIcon(icons.ToggleRadioButtonUnchecked)) + return &ret, warn } func must[T any](ic T, err error) T { diff --git a/tracker/gioui/tracker.go b/tracker/gioui/tracker.go index fb46f2c..b14e2ad 100644 --- a/tracker/gioui/tracker.go +++ b/tracker/gioui/tracker.go @@ -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 { t := &Tracker{ - Theme: NewTheme(), OctaveNumberInput: NewNumberInput(model.Octave().Int()), InstrumentVoices: NewNumberInput(model.InstrumentVoices().Int()), @@ -93,15 +92,23 @@ func NewTracker(model *tracker.Model) *Tracker { Model: model, 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.PopupAlert = NewPopupAlert(model.Alerts()) - if t.preferences.YmlError != nil { - model.Alerts().Add( - fmt.Sprintf("Preferences YML Error: %s", t.preferences.YmlError), - tracker.Warning, - ) + if warn := ReadConfig(defaultPreferences, "preferences.yml", &t.preferences); warn != nil { + model.Alerts().AddAlert(tracker.Alert{ + Priority: tracker.Warning, + Message: warn.Error(), + Duration: 10 * time.Second, + }) } t.TrackEditor.scrollTable.Focus() return t