From a9afcb54dbca9e6d189deca9060e867da9845dc9 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Sat, 9 Dec 2023 19:38:46 +0100 Subject: [PATCH] load and manage pages in HamDeck --- pkg/hamdeck/config.go | 115 +++++++++++++++++++++++++++------- pkg/hamdeck/hamdeck.go | 37 ++++++++++- pkg/hamdeck/pages_test.go | 128 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 253 insertions(+), 27 deletions(-) create mode 100644 pkg/hamdeck/pages_test.go diff --git a/pkg/hamdeck/config.go b/pkg/hamdeck/config.go index aa7392a..cf0fbed 100644 --- a/pkg/hamdeck/config.go +++ b/pkg/hamdeck/config.go @@ -13,6 +13,8 @@ import ( const ( ConfigDefaultFilename = "hamdeck.json" ConfigMainKey = "hamdeck" + ConfigStartPageID = "start_page" + ConfigPages = "pages" ConfigButtons = "buttons" ConfigType = "type" ConfigIndex = "index" @@ -25,43 +27,106 @@ func (d *HamDeck) ReadConfig(r io.Reader) error { return fmt.Errorf("cannot read the configuration: %w", err) } - var rawData interface{} + var rawData any err = json.Unmarshal(buffer.Bytes(), &rawData) if err != nil { return fmt.Errorf("cannot unmarshal the configuration: %w", err) } - configuration, ok := rawData.(map[string]interface{}) + configuration, ok := rawData.(map[string]any) if !ok { return fmt.Errorf("configuration is of wrong type: %T", rawData) } + effectiveConfiguration := findEffectiveConfiguration(configuration) + d.buttonsPerFactory = make([]int, len(d.factories)) + d.pages = make(map[string]Page) + d.startPageID, ok = effectiveConfiguration[ConfigStartPageID].(string) + if !ok { + d.startPageID = legacyPageID + } + + pages, ok := effectiveConfiguration[ConfigPages].(map[string]any) + if ok { + err = d.loadPages(pages) + } + if err != nil { + return err + } + + buttons, ok := effectiveConfiguration[ConfigButtons].([]any) + if ok { + err = d.loadLegacyPage(buttons) + } + if err != nil { + return err + } + + return d.AttachPage(d.startPageID) +} + +func findEffectiveConfiguration(configuration map[string]any) map[string]any { rawSubconfiguration, ok := configuration[ConfigMainKey] if !ok { - return d.attachConfiguredButtons(configuration) + return configuration } - subconfiguration, ok := rawSubconfiguration.(map[string]interface{}) + subconfiguration, ok := rawSubconfiguration.(map[string]any) if !ok { - return d.attachConfiguredButtons(configuration) + return configuration } - return d.attachConfiguredButtons(subconfiguration) + return subconfiguration } -func (d *HamDeck) attachConfiguredButtons(configuration map[string]interface{}) error { - rawButtons, ok := configuration[ConfigButtons] - if !ok { - return fmt.Errorf("configuration contains no 'buttons' key") +func (d *HamDeck) loadPages(configuration map[string]any) error { + for id, rawPage := range configuration { + pageConfiguration, ok := rawPage.(map[string]any) + if !ok { + return fmt.Errorf("%s is not a valid page", id) + } + + page, err := d.loadPage(id, pageConfiguration) + if err != nil { + return err + } + + d.pages[id] = page } + return nil +} - buttons, ok := rawButtons.([]interface{}) +func (d *HamDeck) loadPage(id string, configuration map[string]any) (Page, error) { + buttonsConfiguration, ok := configuration[ConfigButtons].([]any) if !ok { - return fmt.Errorf("'buttons' is not a list of button objects") + return Page{}, fmt.Errorf("page %s has no buttons defined", id) } - d.buttonsPerFactory = make([]int, len(d.factories)) - for i, rawButtonConfig := range buttons { - buttonConfig, ok := rawButtonConfig.(map[string]interface{}) + buttons, err := d.loadButtons(buttonsConfiguration) + if err != nil { + return Page{}, err + } + + return Page{ + buttons: buttons, + }, nil +} + +func (d *HamDeck) loadLegacyPage(configuration []any) error { + buttons, err := d.loadButtons(configuration) + if err != nil { + return err + } + + d.pages[legacyPageID] = Page{ + buttons: buttons, + } + return nil +} + +func (d *HamDeck) loadButtons(configuration []any) ([]Button, error) { + result := make([]Button, len(d.buttons)) + for i, rawButtonConfig := range configuration { + buttonConfig, ok := rawButtonConfig.(map[string]any) if !ok { log.Printf("buttons[%d] is not a button object", i) continue @@ -72,6 +137,9 @@ func (d *HamDeck) attachConfiguredButtons(configuration map[string]interface{}) log.Printf("buttons[%d] has no valid index", i) continue } + if buttonIndex <= 0 || buttonIndex >= len(d.buttons) { + log.Printf("%d is not a valid button index in [0, %d])", buttonIndex, len(d.buttons)) + } var button Button for j, factory := range d.factories { @@ -86,10 +154,9 @@ func (d *HamDeck) attachConfiguredButtons(configuration map[string]interface{}) continue } - d.Attach(buttonIndex, button) + result[buttonIndex] = button } - - return nil + return result, nil } func (d *HamDeck) CloseUnusedFactories() { @@ -100,7 +167,7 @@ func (d *HamDeck) CloseUnusedFactories() { } } -func ToInt(raw interface{}) (int, bool) { +func ToInt(raw any) (int, bool) { if raw == nil { return 0, false } @@ -120,7 +187,7 @@ func ToInt(raw interface{}) (int, bool) { } } -func ToFloat(raw interface{}) (float64, bool) { +func ToFloat(raw any) (float64, bool) { if raw == nil { return 0, false } @@ -140,7 +207,7 @@ func ToFloat(raw interface{}) (float64, bool) { } } -func ToBool(raw interface{}) (bool, bool) { +func ToBool(raw any) (bool, bool) { if raw == nil { return false, false } @@ -158,7 +225,7 @@ func ToBool(raw interface{}) (bool, bool) { } } -func ToString(raw interface{}) (string, bool) { +func ToString(raw any) (string, bool) { if raw == nil { return "", false } @@ -174,11 +241,11 @@ func ToString(raw interface{}) (string, bool) { } } -func ToStringArray(raw interface{}) ([]string, bool) { +func ToStringArray(raw any) ([]string, bool) { if raw == nil { return []string{}, false } - rawValues, ok := raw.([]interface{}) + rawValues, ok := raw.([]any) if !ok { return []string{}, false } diff --git a/pkg/hamdeck/hamdeck.go b/pkg/hamdeck/hamdeck.go index ae66e49..7478def 100644 --- a/pkg/hamdeck/hamdeck.go +++ b/pkg/hamdeck/hamdeck.go @@ -84,6 +84,8 @@ type ButtonFactory interface { CreateButton(config map[string]interface{}) Button } +const legacyPageID = "" + type HamDeck struct { device Device drawLock *sync.Mutex @@ -93,6 +95,13 @@ type HamDeck struct { flashOn bool factories []ButtonFactory buttonsPerFactory []int + + startPageID string + pages map[string]Page +} + +type Page struct { + buttons []Button } func New(device Device) *HamDeck { @@ -102,6 +111,7 @@ func New(device Device) *HamDeck { drawLock: new(sync.Mutex), gc: NewGraphicContext(device.Pixels()), buttons: make([]Button, buttonCount), + pages: make(map[string]Page), } result.noButton = &noButton{image: result.gc.DrawNoButton()} for i := range result.buttons { @@ -136,11 +146,32 @@ func (d *HamDeck) Redraw(index int, redrawImages bool) { d.device.SetImage(index, d.buttons[index].Image(d.gc, redrawImages)) } +func (d *HamDeck) AttachPage(id string) error { + page, ok := d.pages[id] + if !ok { + return fmt.Errorf("no page defined with name %s", id) + } + + for i, button := range page.buttons { + d.Attach(i, button) + } + + return nil +} + func (d *HamDeck) Attach(index int, button Button) { - d.buttons[index] = button + if d.buttons[index] != d.noButton { + d.buttons[index].Detached() + } + + if button != nil { + d.buttons[index] = button + ctx := &buttonContext{index: index, deck: d} + button.Attached(ctx) + } else { + d.buttons[index] = d.noButton + } - ctx := &buttonContext{index: index, deck: d} - button.Attached(ctx) d.Redraw(index, true) } diff --git a/pkg/hamdeck/pages_test.go b/pkg/hamdeck/pages_test.go new file mode 100644 index 0000000..09061e2 --- /dev/null +++ b/pkg/hamdeck/pages_test.go @@ -0,0 +1,128 @@ +package hamdeck + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadConfig_SinglePage(t *testing.T) { + runWithConfigString(t, `{ + "start_page": "main", + "pages": { + "main": { + "buttons": [ + { "type": "test.Button", "index": 0, "some_config": "some_value" } + ] + } + } +}`, func(t *testing.T, deck *HamDeck, device *testDevice, _ chan struct{}) { + assert.Equal(t, "main", deck.startPageID) + assert.Equal(t, 1, len(deck.pages)) + + page := deck.pages["main"] + require.Equal(t, len(deck.buttons), len(page.buttons)) + button, ok := page.buttons[0].(*testButton) + require.True(t, ok) + assert.Equal(t, "some_value", button.config["some_config"]) + + assert.Same(t, button, deck.buttons[0]) + assert.True(t, button.attached) + }) +} + +func TestReadConfig_SinglePagePlusLegacy(t *testing.T) { + runWithConfigString(t, `{ + "pages": { + "main": { + "buttons": [ + { "type": "test.Button", "index": 0, "some_config": "some_value" } + ] + } + }, + "buttons": [ + { "type": "test.Button", "index": 1, "legacy_config": "legacy_value" } + ] +}`, func(t *testing.T, deck *HamDeck, device *testDevice, _ chan struct{}) { + assert.Equal(t, legacyPageID, deck.startPageID) + assert.Equal(t, 2, len(deck.pages)) + + mainPage := deck.pages["main"] + require.Equal(t, len(deck.buttons), len(mainPage.buttons)) + button, ok := mainPage.buttons[0].(*testButton) + require.True(t, ok) + assert.Equal(t, "some_value", button.config["some_config"]) + + legacyPage := deck.pages[legacyPageID] + require.Equal(t, len(deck.buttons), len(legacyPage.buttons)) + button, ok = legacyPage.buttons[1].(*testButton) + require.True(t, ok) + assert.Equal(t, "legacy_value", button.config["legacy_config"]) + + assert.Same(t, button, deck.buttons[1]) + assert.True(t, button.attached) + }) +} + +func TestReadConfig_OnlyLegacy(t *testing.T) { + runWithConfigString(t, `{ + "buttons": [ + { "type": "test.Button", "index": 1, "legacy_config": "legacy_value" } + ] +}`, func(t *testing.T, deck *HamDeck, device *testDevice, _ chan struct{}) { + assert.Equal(t, legacyPageID, deck.startPageID) + assert.Equal(t, 1, len(deck.pages)) + + legacyPage := deck.pages[legacyPageID] + require.Equal(t, len(deck.buttons), len(legacyPage.buttons)) + button, ok := legacyPage.buttons[1].(*testButton) + require.True(t, ok) + assert.Equal(t, "legacy_value", button.config["legacy_config"]) + + assert.Same(t, button, deck.buttons[1]) + assert.True(t, button.attached) + }) +} + +func TestAttachPage(t *testing.T) { + runWithConfigString(t, `{ + "pages": { + "main": { + "buttons": [ + { "type": "test.Button", "index": 0, "some_config": "some_value" } + ] + } + }, + "buttons": [ + { "type": "test.Button", "index": 1, "legacy_config": "legacy_value" } + ] +}`, func(t *testing.T, deck *HamDeck, device *testDevice, _ chan struct{}) { + assert.Equal(t, legacyPageID, deck.startPageID) + assert.Equal(t, 2, len(deck.pages)) + + mainPage := deck.pages["main"] + require.Equal(t, len(deck.buttons), len(mainPage.buttons)) + mainButton, ok := mainPage.buttons[0].(*testButton) + require.True(t, ok) + assert.Equal(t, "some_value", mainButton.config["some_config"]) + + legacyPage := deck.pages[legacyPageID] + require.Equal(t, len(deck.buttons), len(legacyPage.buttons)) + legacyButton, ok := legacyPage.buttons[1].(*testButton) + require.True(t, ok) + assert.Equal(t, "legacy_value", legacyButton.config["legacy_config"]) + + assert.Same(t, legacyButton, deck.buttons[1]) + assert.True(t, legacyButton.attached) + + err := deck.AttachPage("main") + assert.NoError(t, err) + + assert.True(t, legacyButton.detached) + assert.True(t, mainButton.attached) + + assert.Same(t, mainButton, deck.buttons[0]) + assert.Same(t, deck.noButton, deck.buttons[1]) + }) +}