diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ec6cc1..5f5142a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,10 +18,10 @@ jobs: - name: Install Linux packages run: sudo apt update && sudo apt install -y --no-install-recommends debhelper libhidapi-libusb0 - - name: Set up Go 1.16 + - name: Set up Go 1.21 uses: actions/setup-go@v2 with: - go-version: 1.16 + go-version: 1.21 id: go - name: Check out code into the Go module directory diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d83d68..2ddedec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,10 +19,10 @@ jobs: - name: Install Linux packages run: sudo apt update && sudo apt install -y --no-install-recommends debhelper libhidapi-libusb0 - - name: Set up Go 1.16 + - name: Set up Go 1.21 uses: actions/setup-go@v2 with: - go-version: 1.16 + go-version: 1.21 id: go - name: Check out code into the Go module directory diff --git a/Makefile b/Makefile index 74ea8bf..72f0064 100644 --- a/Makefile +++ b/Makefile @@ -2,35 +2,35 @@ VERSION?=$(shell git describe --tags) GITCOMMIT=$(shell git rev-parse --verify --short HEAD) BUILDTIME=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") -all: test build .PHONY: all +all: test build -hamdeck: - go build -v -o hamdeck -ldflags "-X github.com/ftl/hamdeck/cmd.version=$(VERSION) -X github.com/ftl/hamdeck/cmd.gitCommit=$(GITCOMMIT) -X github.com/ftl/hamdeck/cmd.buildTime=$(BUILDTIME)" . - -build: hamdeck .PHONY: build +build: hamdeck +.PHONY: test test: go test ./... -v -.PHONY: test +.PHONY: clean clean: -@rm ./hamdeck -.PHONY: clean +.PHONY: install install: hamdeck cp ./hamdeck /usr/bin/hamdeck mkdir -p /usr/share/hamdeck cp ./example_conf.json /usr/share/hamdeck/example_conf.json cp ./.debpkg/lib/systemd/system/hamdeck.service /lib/systemd/system/hamdeck.service cp ./.debpkg/lib/udev/rules.d/99-hamdeck.rules /lib/udev/rules.d/99-hamdeck.rules -.PHONY: install +.PHONY: uninstall uninstall: -rm /usr/bin/hamdeck -rm /usr/share/hamdeck/example_conf.json -rmdir /usr/share/hamdeck -rm /lib/systemd/system/hamdeck.service -rm /lib/udev/rules.d/99-hamdeck.rules -.PHONY: uninstall + +hamdeck: + go build -v -o hamdeck -ldflags "-X github.com/ftl/hamdeck/cmd.version=$(VERSION) -X github.com/ftl/hamdeck/cmd.gitCommit=$(GITCOMMIT) -X github.com/ftl/hamdeck/cmd.buildTime=$(BUILDTIME)" . diff --git a/cmd/root.go b/cmd/root.go index d700ace..2968a21 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -94,6 +94,7 @@ func run(cmd *cobra.Command, args []string) { device.SetBrightness(rootFlags.brightness) deck := hamdeck.New(device) + deck.RegisterFactory(hamdeck.NewButtonFactory(deck)) deck.RegisterFactory(pulse.NewButtonFactory()) if rootFlags.hamlibAddress != "" { deck.RegisterFactory(hamlib.NewButtonFactory(rootFlags.hamlibAddress)) diff --git a/example_conf.json b/example_conf.json index dc5c3c7..4ccf6d0 100644 --- a/example_conf.json +++ b/example_conf.json @@ -1,206 +1,275 @@ { - "buttons": [ - { - "type": "hamlib.SwitchToBand", - "index": 0, - "band": "80m" + "start_page": "main", + "pages": { + "main": { + "buttons": [ + { + "type": "hamdeck.Page", + "index": 0, + "page": "hamlib", + "label": "Hamlib" + }, + { + "type": "hamdeck.Page", + "index": 1, + "page": "tci", + "label": "TCI" + }, + { + "type": "hamdeck.Page", + "index": 2, + "page": "pulse", + "label": "Pulse" + }, + { + "type": "hamdeck.Page", + "index": 3, + "page": "mqtt", + "label": "MQTT" + } + ] }, - { - "type": "hamlib.SwitchToBand", - "index": 1, - "band": "40m" + "hamlib": { + "buttons": [ + { + "type": "hamlib.SwitchToBand", + "index": 0, + "band": "80m" + }, + { + "type": "hamlib.SwitchToBand", + "index": 1, + "band": "40m" + }, + { + "type": "hamlib.SwitchToBand", + "index": 2, + "band": "30m" + }, + { + "type": "hamlib.SwitchToBand", + "index": 3, + "band": "20m" + }, + { + "type": "hamlib.SwitchToBand", + "index": 4, + "band": "17m" + }, + { + "type": "hamlib.SwitchToBand", + "index": 5, + "band": "15m" + }, + { + "type": "hamlib.SwitchToBand", + "index": 6, + "band": "12m" + }, + { + "type": "hamlib.SwitchToBand", + "index": 7, + "band": "10m" + }, + { + "type": "hamlib.SetMode", + "index": 8, + "mode": "CW" + }, + { + "type": "hamlib.SetMode", + "index": 9, + "mode": "PKTUSB", + "label": "Data" + }, + { + "type": "hamlib.ToggleMode", + "index": 10, + "mode1": "LSB", + "mode2": "USB" + }, + { + "type": "hamlib.Set", + "index": 11, + "label": "Band▾", + "command": "vfo_op", + "args": ["BAND_DOWN"] + }, + { + "type": "hamlib.Set", + "index": 12, + "label": "Band▴", + "command": "vfo_op", + "args": ["BAND_UP"] + }, + { + "type": "hamlib.SetPowerLevel", + "index": 13, + "label": "10W", + "value": 0.1 + }, + { + "type": "hamlib.SetPowerLevel", + "index": 14, + "label": "50W", + "value": 0.5 + }, + { + "type": "hamlib.SetPowerLevel", + "index": 15, + "label": "100W", + "value": 1.0 + }, + { + "type": "hamlib.SetVFO", + "index": 16, + "label": "VFO A", + "vfo": "VFOA" + }, + { + "type": "hamlib.SetVFO", + "index": 17, + "label": "VFO B", + "vfo": "VFOB" + }, + { + "type": "hamlib.Set", + "index": 18, + "label": "A=B", + "command": "vfo_op", + "args": ["CPY"] + }, + { + "type": "hamlib.MOX", + "index": 20 + }, + { + "type": "hamdeck.Page", + "index": 31, + "page": "main", + "label": "Back" + } + ] }, - { - "type": "hamlib.SwitchToBand", - "index": 2, - "band": "30m" + "tci": { + "buttons": [ + { + "type": "tci.SetFilter", + "index": 0, + "label": "CW 100", + "icon": "filter_small", + "bottom_frequency": -50, + "top_frequency": 50, + "mode": "CW" + }, + { + "type": "tci.SetFilter", + "index": 1, + "label": "CW 300", + "icon": "filter_medium", + "bottom_frequency": -150, + "top_frequency": 150, + "mode": "CW" + }, + { + "type": "tci.SetFilter", + "index": 2, + "label": "DigU 500", + "icon": "filter_small_usb", + "bottom_frequency": 1250, + "top_frequency": 1750, + "mode": "PKTUSB" + }, + { + "type": "tci.SetFilter", + "index": 3, + "label": "DigU 3k", + "icon": "filter_usb", + "bottom_frequency": 0, + "top_frequency": 3000, + "mode": "PKTUSB" + }, + { + "type": "hamdeck.Page", + "index": 31, + "page": "main", + "label": "Back" + } + ] }, - { - "type": "hamlib.SwitchToBand", - "index": 3, - "band": "20m" + "pulse": { + "buttons": [ + { + "type": "pulse.ToggleMute", + "index": 0, + "sink": "my.sink", + "label": "Sink" + }, + { + "type": "pulse.ToggleMute", + "index": 1, + "source": "my.source", + "label": "Source" + }, + { + "type": "pulse.ToggleMute", + "index": 2, + "sinkInput": "Sink Input Media Name", + "label": "Source" + }, + { + "type": "pulse.ToggleMute", + "index": 3, + "sourceOutput": "Source Output Media Name", + "label": "Source" + }, + { + "type": "hamdeck.Page", + "index": 31, + "page": "main", + "label": "Back" + } + ] }, - { - "type": "hamlib.SwitchToBand", - "index": 4, - "band": "17m" - }, - { - "type": "hamlib.SwitchToBand", - "index": 5, - "band": "15m" - }, - { - "type": "hamlib.SwitchToBand", - "index": 6, - "band": "12m" - }, - { - "type": "hamlib.SwitchToBand", - "index": 7, - "band": "10m" - }, - { - "type": "hamlib.SetMode", - "index": 8, - "mode": "CW" - }, - { - "type": "hamlib.SetMode", - "index": 9, - "mode": "PKTUSB", - "label": "Data" - }, - { - "type": "hamlib.ToggleMode", - "index": 10, - "mode1": "LSB", - "mode2": "USB" - }, - { - "type": "hamlib.Set", - "index": 11, - "label": "Band▾", - "command": "vfo_op", - "args": ["BAND_DOWN"] - }, - { - "type": "hamlib.Set", - "index": 12, - "label": "Band▴", - "command": "vfo_op", - "args": ["BAND_UP"] - }, - { - "type": "hamlib.SetPowerLevel", - "index": 13, - "label": "10W", - "value": 0.1 - }, - { - "type": "hamlib.SetPowerLevel", - "index": 14, - "label": "50W", - "value": 0.5 - }, - { - "type": "hamlib.SetPowerLevel", - "index": 15, - "label": "100W", - "value": 1.0 - }, - { - "type": "hamlib.SetVFO", - "index": 16, - "label": "VFO A", - "vfo": "VFOA" - }, - { - "type": "hamlib.SetVFO", - "index": 17, - "label": "VFO B", - "vfo": "VFOB" - }, - { - "type": "hamlib.Set", - "index": 18, - "label": "A=B", - "command": "vfo_op", - "args": ["CPY"] - }, - { - "type": "tci.SetFilter", - "index": 19, - "label": "CW 100", - "icon": "filter_small", - "bottom_frequency": -50, - "top_frequency": 50, - "mode": "CW" - }, - { - "type": "tci.SetFilter", - "index": 20, - "label": "CW 300", - "icon": "filter_medium", - "bottom_frequency": -150, - "top_frequency": 150, - "mode": "CW" - }, - { - "type": "tci.SetFilter", - "index": 21, - "label": "DigU 500", - "icon": "filter_small_usb", - "bottom_frequency": 1250, - "top_frequency": 1750, - "mode": "PKTUSB" - }, - { - "type": "tci.SetFilter", - "index": 22, - "label": "DigU 3k", - "icon": "filter_usb", - "bottom_frequency": 0, - "top_frequency": 3000, - "mode": "PKTUSB" - }, - { - "type": "pulse.ToggleMute", - "index": 24, - "sink": "my.sink", - "label": "Sink" - }, - { - "type": "pulse.ToggleMute", - "index": 25, - "source": "my.source", - "label": "Source" - }, - { - "type": "pulse.ToggleMute", - "index": 26, - "sinkInput": "Sink Input Media Name", - "label": "Source" - }, - { - "type": "pulse.ToggleMute", - "index": 27, - "sourceOutput": "Source Output Media Name", - "label": "Source" - }, - { - "type": "mqtt.Switch", - "index": 28, - "label": "Lights", - "inputTopic": "tasmota/smartplug1/stat/POWER", - "outputTopic": "tasmota/smartplug1/cmnd/POWER", - "onPayload": "ON", - "offPayload": "OFF", - "mode": "toggle" - }, - { - "type": "mqtt.Switch", - "index": 29, - "label": "On", - "inputTopic": "tasmota/smartplug1/stat/POWER", - "outputTopic": "tasmota/smartplug1/cmnd/POWER", - "onPayload": "ON", - "offPayload": "OFF", - "mode": "ON" - }, - { - "type": "mqtt.Switch", - "index": 20, - "label": "Off", - "inputTopic": "tasmota/smartplug1/stat/POWER", - "outputTopic": "tasmota/smartplug1/cmnd/POWER", - "onPayload": "ON", - "offPayload": "OFF", - "mode": "OFF" - }, - { - "type": "hamlib.MOX", - "index": 31 + "mqtt": { + "buttons": [ + { + "type": "mqtt.Switch", + "index": 0, + "label": "Lights", + "inputTopic": "tasmota/smartplug1/stat/POWER", + "outputTopic": "tasmota/smartplug1/cmnd/POWER", + "onPayload": "ON", + "offPayload": "OFF", + "mode": "toggle" + }, + { + "type": "mqtt.Switch", + "index": 1, + "label": "On", + "inputTopic": "tasmota/smartplug1/stat/POWER", + "outputTopic": "tasmota/smartplug1/cmnd/POWER", + "onPayload": "ON", + "offPayload": "OFF", + "mode": "ON" + }, + { + "type": "mqtt.Switch", + "index": 2, + "label": "Off", + "inputTopic": "tasmota/smartplug1/stat/POWER", + "outputTopic": "tasmota/smartplug1/cmnd/POWER", + "onPayload": "ON", + "offPayload": "OFF", + "mode": "OFF" + }, + { + "type": "hamdeck.Page", + "index": 31, + "page": "main", + "label": "Back" + } + ] } - ] + } } diff --git a/go.mod b/go.mod index 13dd64e..2a3afb2 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ftl/hamdeck -go 1.19 +go 1.21 // replace github.com/ftl/tci => ../tci @@ -10,24 +10,28 @@ go 1.19 // replace github.com/jfreymuth/pulse => ../pulse require ( - github.com/eclipse/paho.mqtt.golang v1.4.2 + github.com/eclipse/paho.mqtt.golang v1.4.3 github.com/fogleman/gg v1.3.0 - github.com/ftl/hamradio v0.2.6 - github.com/ftl/rigproxy v0.2.0 - github.com/ftl/tci v0.3.1 + github.com/ftl/hamradio v0.2.7 + github.com/ftl/rigproxy v0.2.4 + github.com/ftl/tci v0.3.2 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/jfreymuth/pulse v0.1.0 - github.com/muesli/streamdeck v0.3.0 - github.com/spf13/cobra v1.6.1 - golang.org/x/image v0.5.0 + github.com/muesli/streamdeck v0.4.0 + github.com/spf13/cobra v1.8.0 + github.com/stretchr/testify v1.8.4 + golang.org/x/image v0.14.0 ) require ( - github.com/gorilla/websocket v1.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gorilla/websocket v1.5.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/karalabe/hid v1.0.1-0.20190806082151-9c14560f9ee8 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.5.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2245000..c32c5d8 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,22 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/eclipse/paho.mqtt.golang v1.4.2 h1:66wOzfUHSSI1zamx7jR6yMEI5EuHnT1G6rNA5PM12m4= -github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= +github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/ftl/hamradio v0.2.6 h1:AEgTLhoqYCZDg7pCZMeRFZYJPSoXTp4TEK65lBEcP2o= -github.com/ftl/hamradio v0.2.6/go.mod h1:FOZkf8liaM/H8F8Vyp36EN9iVzikKpaOJ5AqrW77cEo= -github.com/ftl/rigproxy v0.2.0 h1:CHdoFJM2wWx9IkM6jQEBMAbL7WeQiriR6AC0RNncuK0= -github.com/ftl/rigproxy v0.2.0/go.mod h1:3X0MLMrmbrglHCw0EUWVtYah15LY+Xgfh8NbynYzpVg= -github.com/ftl/tci v0.3.1 h1:Njtb38PzkVYbW9zlr4Babl6+frQBsCLWNEZDvxxUTzQ= -github.com/ftl/tci v0.3.1/go.mod h1:3B8x8FI/kBbUwbWnz725tTiiiNfJpIL9cQ67TnjW3aU= +github.com/ftl/hamradio v0.2.7 h1:AXg5JI6oPaSvO/YfM7kwAweVP49EF/zaEBiY+cxYDMI= +github.com/ftl/hamradio v0.2.7/go.mod h1:BvA+ni3sOKmrIJpLt6f2sYK9vc3VfihZm4x0h8kzOPw= +github.com/ftl/rigproxy v0.2.4 h1:8g9zfYX049Wd0fa1J6WujQ4wMPiAquXO286SVKNf57k= +github.com/ftl/rigproxy v0.2.4/go.mod h1:PrBUiqLwu/6zL44+uOz4lgmOfnis4FIvJDhxDNXoi60= +github.com/ftl/tci v0.3.2 h1:1Qdgprldiv7/DQvuK96OHMVqb+SDunqbxTHDcWsE5Tk= +github.com/ftl/tci v0.3.2/go.mod h1:3B8x8FI/kBbUwbWnz725tTiiiNfJpIL9cQ67TnjW3aU= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jfreymuth/pulse v0.1.0 h1:KN38/9hoF9PJvP5DpEVhMRKNuwnJUonc8c9ARorRXUA= @@ -25,51 +24,59 @@ github.com/jfreymuth/pulse v0.1.0/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZ github.com/karalabe/hid v1.0.1-0.20190806082151-9c14560f9ee8 h1:AP5krei6PpUCFOp20TSmxUS4YLoLvASBcArJqM/V+DY= github.com/karalabe/hid v1.0.1-0.20190806082151-9c14560f9ee8/go.mod h1:Vr51f8rUOLYrfrWDFlV12GGQgM5AT8sVh+2fY4MPeu8= github.com/muesli/coral v1.0.0/go.mod h1:bf91M/dkp7iHQw73HOoR9PekdTJMTD6ihJgWoDitde8= -github.com/muesli/streamdeck v0.3.0 h1:BrUXEPtzDy9hO/l2ZLMKsik0VrNDwt8hHRomKq0NlFQ= -github.com/muesli/streamdeck v0.3.0/go.mod h1:iBrvujOQ0WXWBJfi25gFOj/AVgq7FKfjPLi5AISKZx8= +github.com/muesli/streamdeck v0.4.0 h1:kBV1RCLFz3dYfVlBvCPG0cWxTFK7IOdcc1Jw2T4Qz4E= +github.com/muesli/streamdeck v0.4.0/go.mod h1:6Fjt/9so3B22BtraQLRTPHu33c7yVgUIcDPiZqzSHfE= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= -golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= +golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= +golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= +golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/hamdeck/buttons.go b/pkg/hamdeck/buttons.go new file mode 100644 index 0000000..d5c03aa --- /dev/null +++ b/pkg/hamdeck/buttons.go @@ -0,0 +1,41 @@ +package hamdeck + +import "image" + +/* + PageButton +*/ + +type PageButton struct { + BaseButton + image image.Image + + pageSwitcher PageSwitcher + id string + label string +} + +func NewPageButton(pageSwitcher PageSwitcher, id string, label string) *PageButton { + return &PageButton{ + pageSwitcher: pageSwitcher, + id: id, + label: label, + } +} + +func (b *PageButton) Image(gc GraphicContext, redrawImages bool) image.Image { + if b.image == nil || redrawImages { + gc.SetForeground(White) + gc.SetBackground(Black) + b.image = gc.DrawSingleLineTextButton(b.label) + } + return b.image +} + +func (b *PageButton) Pressed() { + b.pageSwitcher.AttachPage(b.id) +} + +func (b *PageButton) Released() { + // nop +} diff --git a/pkg/hamdeck/config.go b/pkg/hamdeck/config.go index aa7392a..ad4330c 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/factory.go b/pkg/hamdeck/factory.go new file mode 100644 index 0000000..fe62323 --- /dev/null +++ b/pkg/hamdeck/factory.go @@ -0,0 +1,51 @@ +package hamdeck + +import "log" + +const ( + ConfigPage = "page" + ConfigLabel = "label" +) + +const ( + PageButtonType = "hamdeck.Page" +) + +type Factory struct { + pageSwitcher PageSwitcher +} + +type PageSwitcher interface { + AttachPage(string) error +} + +func NewButtonFactory(pageSwitcher PageSwitcher) *Factory { + return &Factory{ + pageSwitcher: pageSwitcher, + } +} + +func (f *Factory) Close() { + // nop +} + +func (f *Factory) CreateButton(config map[string]any) Button { + switch config[ConfigType] { + case PageButtonType: + return f.createPageButton(config) + default: + return nil + } +} + +func (f *Factory) createPageButton(config map[string]any) Button { + id, haveID := ToString(config[ConfigPage]) + label, haveLabel := ToString(config[ConfigLabel]) + if !haveID { + log.Print("A hamdeck.Page button must have a page field.") + } + if !haveLabel { + log.Print("A hamdeck.Page button must have a label field.") + } + return NewPageButton(f.pageSwitcher, id, label) +} 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/hamdeck_test.go b/pkg/hamdeck/hamdeck_test.go new file mode 100644 index 0000000..900df5a --- /dev/null +++ b/pkg/hamdeck/hamdeck_test.go @@ -0,0 +1,197 @@ +package hamdeck + +import ( + "bytes" + "image" + "io" + "os" + "path/filepath" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHamDeckRoundtrip(t *testing.T) { + runWithConfigFile(t, "testRoundtrip", func(t *testing.T, deck *HamDeck, device *testDevice, _ chan struct{}) { + require.Equal(t, 32, len(deck.buttons)) + button := deck.buttons[12].(*testButton) + + assert.Equal(t, "some_value", button.config["some_config"]) + assert.True(t, button.attached) + assert.False(t, button.detached) + assert.False(t, button.pressed) + assert.False(t, button.released) + + device.Press(12) + device.WaitForLastKey() + assert.True(t, button.pressed) + assert.False(t, button.released) + + device.Release(12) + device.WaitForLastKey() + assert.True(t, button.released) + }) +} + +/* Test Harness */ + +func runWithConfigFile(t *testing.T, filename string, f func(*testing.T, *HamDeck, *testDevice, chan struct{})) { + device := newDefaultTestDevice() + deck := New(device) + deck.RegisterFactory(new(testButtonFactory)) + deck.RegisterFactory(NewButtonFactory(deck)) + + reader, err := openTestConfigFile(filename) + require.NoError(t, err) + defer reader.Close() + err = deck.ReadConfig(reader) + require.NoError(t, err) + + stopper := make(chan struct{}) + wg := new(sync.WaitGroup) + wg.Add(1) + go func() { + deck.Run(stopper) + wg.Done() + }() + + f(t, deck, device, stopper) + + select { + case <-stopper: + default: + close(stopper) + } + wg.Wait() +} + +func runWithConfigString(t *testing.T, config string, f func(*testing.T, *HamDeck, *testDevice, chan struct{})) { + device := newDefaultTestDevice() + deck := New(device) + deck.RegisterFactory(new(testButtonFactory)) + deck.RegisterFactory(NewButtonFactory(deck)) + + reader, err := openTestConfigString(config) + require.NoError(t, err) + defer reader.Close() + err = deck.ReadConfig(reader) + require.NoError(t, err) + + stopper := make(chan struct{}) + wg := new(sync.WaitGroup) + wg.Add(1) + go func() { + deck.Run(stopper) + wg.Done() + }() + + f(t, deck, device, stopper) + + select { + case <-stopper: + default: + close(stopper) + } + wg.Wait() +} + +func openTestConfigFile(filename string) (io.ReadCloser, error) { + return os.Open(filepath.Join("testdata", filename+".json")) +} + +func openTestConfigString(config string) (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBufferString(config)), nil +} + +type testDevice struct { + id string + serial string + firmwareVersion string + pixels int + rows int + columns int + keys chan Key +} + +func newDefaultTestDevice() *testDevice { + return newTestDevice(128, 4, 8) +} + +func newTestDevice(pixels int, rows int, columns int) *testDevice { + return &testDevice{ + pixels: pixels, + rows: rows, + columns: columns, + keys: make(chan Key), + } +} + +func (d *testDevice) Close() error { return nil } +func (d *testDevice) ID() string { return d.id } +func (d *testDevice) Serial() string { return d.serial } +func (d *testDevice) FirmwareVersion() string { return d.firmwareVersion } +func (d *testDevice) Pixels() int { return d.pixels } +func (d *testDevice) Rows() int { return d.rows } +func (d *testDevice) Columns() int { return d.columns } +func (d *testDevice) Clear() error { return nil } +func (d *testDevice) Reset() error { return nil } +func (d *testDevice) SetBrightness(int) error { return nil } +func (d *testDevice) SetImage(int, image.Image) error { return nil } +func (d *testDevice) ReadKeys() (chan Key, error) { return d.keys, nil } +func (d *testDevice) Press(index int) { + key := Key{ + Index: index, + Pressed: true, + } + d.keys <- key +} +func (d *testDevice) Release(index int) { + key := Key{ + Index: index, + Pressed: false, + } + d.keys <- key +} +func (d *testDevice) WaitForLastKey() { + d.keys <- Key{} +} + +const ( + testButtonType = "test.Button" +) + +type testButtonFactory struct{} + +func (f *testButtonFactory) Close() {} + +func (f *testButtonFactory) CreateButton(config map[string]any) Button { + switch config[ConfigType] { + case testButtonType: + return f.createTestButton(config) + default: + return nil + } +} + +func (f *testButtonFactory) createTestButton(config map[string]any) *testButton { + return &testButton{ + config: config, + } +} + +type testButton struct { + config map[string]any + + pressed bool + released bool + attached bool + detached bool +} + +func (b *testButton) Image(GraphicContext, bool) image.Image { return nil } +func (b *testButton) Pressed() { b.pressed = true } +func (b *testButton) Released() { b.released = true } +func (b *testButton) Attached(ButtonContext) { b.attached = true } +func (b *testButton) Detached() { b.detached = true } diff --git a/pkg/hamdeck/pages_test.go b/pkg/hamdeck/pages_test.go new file mode 100644 index 0000000..318be35 --- /dev/null +++ b/pkg/hamdeck/pages_test.go @@ -0,0 +1,168 @@ +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]) + }) +} + +func TestPageButton(t *testing.T) { + runWithConfigString(t, `{ + "pages": { + "main": { + "buttons": [ + { "type": "hamdeck.Page", "index": 0, "page": "", "label": "Back" } + ] + } + }, + "buttons": [ + { "type": "hamdeck.Page", "index": 0, "page": "main", "label": "Main" } + ] +}`, 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].(*PageButton) + require.True(t, ok) + assert.Equal(t, "Back", mainButton.label) + + legacyPage := deck.pages[legacyPageID] + require.Equal(t, len(deck.buttons), len(legacyPage.buttons)) + legacyButton, ok := legacyPage.buttons[0].(*PageButton) + require.True(t, ok) + assert.Equal(t, "Main", legacyButton.label) + + assert.Same(t, legacyButton, deck.buttons[0]) + + device.Press(0) + device.WaitForLastKey() + assert.Same(t, mainButton, deck.buttons[0]) + + device.Press(0) + device.WaitForLastKey() + assert.Same(t, legacyButton, deck.buttons[0]) + }) +} diff --git a/pkg/hamdeck/testdata/testEightPages.json b/pkg/hamdeck/testdata/testEightPages.json new file mode 100644 index 0000000..17debb3 --- /dev/null +++ b/pkg/hamdeck/testdata/testEightPages.json @@ -0,0 +1,54 @@ +{ + "buttons": [ + {"type": "hamdeck.Page", "index": 0, "page": "1", "label": "1"}, + {"type": "hamdeck.Page", "index": 1, "page": "2", "label": "2"}, + {"type": "hamdeck.Page", "index": 2, "page": "3", "label": "3"}, + {"type": "hamdeck.Page", "index": 3, "page": "4", "label": "4"}, + {"type": "hamdeck.Page", "index": 4, "page": "5", "label": "5"}, + {"type": "hamdeck.Page", "index": 5, "page": "6", "label": "6"}, + {"type": "hamdeck.Page", "index": 6, "page": "7", "label": "7"}, + {"type": "hamdeck.Page", "index": 7, "page": "8", "label": "8"} + ], + "pages": { + "1": { + "buttons": [ + {"type": "hamdeck.Page", "index": 0, "page": "", "label": "Back"} + ] + }, + "2": { + "buttons": [ + {"type": "hamdeck.Page", "index": 1, "page": "", "label": "Back"} + ] + }, + "3": { + "buttons": [ + {"type": "hamdeck.Page", "index": 2, "page": "", "label": "Back"} + ] + }, + "4": { + "buttons": [ + {"type": "hamdeck.Page", "index": 3, "page": "", "label": "Back"} + ] + }, + "5": { + "buttons": [ + {"type": "hamdeck.Page", "index": 4, "page": "", "label": "Back"} + ] + }, + "6": { + "buttons": [ + {"type": "hamdeck.Page", "index": 5, "page": "", "label": "Back"} + ] + }, + "7": { + "buttons": [ + {"type": "hamdeck.Page", "index": 6, "page": "", "label": "Back"} + ] + }, + "8": { + "buttons": [ + {"type": "hamdeck.Page", "index": 7, "page": "", "label": "Back"} + ] + } + } +} \ No newline at end of file diff --git a/pkg/hamdeck/testdata/testRoundtrip.json b/pkg/hamdeck/testdata/testRoundtrip.json new file mode 100644 index 0000000..3733817 --- /dev/null +++ b/pkg/hamdeck/testdata/testRoundtrip.json @@ -0,0 +1,9 @@ +{ + "buttons": [ + { + "type": "test.Button", + "index": 12, + "some_config": "some_value" + } + ] +} \ No newline at end of file