Skip to content

Commit ea3261a

Browse files
authored
feat: adder-tray scaffolding (#608)
Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
1 parent 3a154fb commit ea3261a

File tree

14 files changed

+854
-5
lines changed

14 files changed

+854
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Binaries for programs and plugins
22
/adder
3+
/adder-tray
34

45
# Test binary, built with `go test -c`
56
*.test

Makefile

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ ROOT_DIR=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
55
GO_FILES=$(shell find $(ROOT_DIR) -name '*.go')
66

77
# Gather list of expected binaries
8-
BINARIES=$(shell cd $(ROOT_DIR)/cmd && ls -1 | grep -v ^common)
8+
BINARIES=$(shell cd $(ROOT_DIR)/cmd && ls -1 | grep -v ^common | grep -v ^adder-tray)
99

1010
# Extract Go module name from go.mod
1111
GOMODULE=$(shell grep ^module $(ROOT_DIR)/go.mod | awk '{ print $$2 }')
1212

1313
# Set version strings based on git tag and current ref
1414
GO_LDFLAGS=-ldflags "-s -w -X '$(GOMODULE)/internal/version.Version=$(shell git describe --tags --exact-match 2>/dev/null)' -X '$(GOMODULE)/internal/version.CommitHash=$(shell git rev-parse --short HEAD)'"
1515

16-
.PHONY: build mod-tidy clean test
16+
.PHONY: build build-tray mod-tidy clean test
1717

1818
# Alias for building program binary
1919
build: $(BINARIES)
@@ -39,6 +39,15 @@ swagger:
3939
test: mod-tidy
4040
go test -v -race ./...
4141

42+
# Build adder-tray binary
43+
# CGO is required on macOS for system tray support; Linux and
44+
# Windows use pure Go implementations.
45+
build-tray: mod-tidy $(GO_FILES)
46+
go build \
47+
$(GO_LDFLAGS) \
48+
-o adder-tray$(if $(filter windows,$(GOOS)),.exe,) \
49+
./cmd/adder-tray
50+
4251
# Build our program binaries
4352
# Depends on GO_FILES to determine when rebuild is needed
4453
$(BINARIES): mod-tidy $(GO_FILES)

cmd/adder-tray/main.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2026 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"log/slog"
19+
"os"
20+
"os/signal"
21+
"syscall"
22+
23+
"github.com/blinklabs-io/adder/tray"
24+
)
25+
26+
func main() {
27+
application, err := tray.NewApp()
28+
if err != nil {
29+
slog.Error("failed to create application", "error", err)
30+
os.Exit(1)
31+
}
32+
33+
// Handle OS signals for graceful shutdown
34+
sigChan := make(chan os.Signal, 1)
35+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
36+
37+
go func() {
38+
<-sigChan
39+
slog.Info("received shutdown signal")
40+
go func() {
41+
<-sigChan
42+
slog.Warn("received second signal, forcing exit")
43+
os.Exit(1)
44+
}()
45+
application.Shutdown()
46+
}()
47+
48+
application.Run()
49+
}

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.24.0
55
toolchain go1.24.4
66

77
require (
8+
fyne.io/systray v1.12.0
89
github.com/SundaeSwap-finance/kugo v1.3.0
910
github.com/SundaeSwap-finance/ogmigo/v6 v6.2.0
1011
github.com/blinklabs-io/gouroboros v0.152.2
@@ -25,6 +26,7 @@ require (
2526
go.uber.org/automaxprocs v1.6.0
2627
golang.org/x/oauth2 v0.34.0
2728
gopkg.in/yaml.v2 v2.4.0
29+
gopkg.in/yaml.v3 v3.0.1
2830
)
2931

3032
// XXX: uncomment when testing local changes to gouroboros
@@ -64,7 +66,7 @@ require (
6466
github.com/go-playground/validator/v10 v10.27.0 // indirect
6567
github.com/goccy/go-json v0.10.4 // indirect
6668
github.com/goccy/go-yaml v1.18.0 // indirect
67-
github.com/godbus/dbus/v5 v5.1.0 // indirect
69+
github.com/godbus/dbus/v5 v5.2.2 // indirect
6870
github.com/gorilla/websocket v1.5.3 // indirect
6971
github.com/holiman/uint256 v1.3.2 // indirect
7072
github.com/jackmordaunt/icns/v3 v3.0.1 // indirect
@@ -99,5 +101,4 @@ require (
99101
golang.org/x/text v0.33.0 // indirect
100102
golang.org/x/tools v0.40.0 // indirect
101103
google.golang.org/protobuf v1.36.11 // indirect
102-
gopkg.in/yaml.v3 v3.0.1 // indirect
103104
)

go.sum

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2Qx
22
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
33
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
44
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
5+
fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM=
6+
fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
57
git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm5euVuE=
68
git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo=
79
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
@@ -124,8 +126,9 @@ github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
124126
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
125127
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
126128
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
127-
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
128129
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
130+
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
131+
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
129132
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
130133
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
131134
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=

tray/app.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright 2026 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package tray
16+
17+
import (
18+
"log/slog"
19+
20+
"fyne.io/systray"
21+
)
22+
23+
// App holds references to all major components of the tray
24+
// application.
25+
type App struct {
26+
config TrayConfig
27+
process *ProcessManager
28+
}
29+
30+
// NewApp creates and initialises the tray application.
31+
func NewApp() (*App, error) {
32+
cfg, err := LoadConfig()
33+
if err != nil {
34+
slog.Warn(
35+
"failed to load config, using defaults",
36+
"error", err,
37+
)
38+
cfg = DefaultConfig()
39+
}
40+
41+
a := &App{
42+
config: cfg,
43+
process: NewProcessManager(
44+
WithBinary(cfg.AdderBinary),
45+
WithConfigFile(cfg.AdderConfig),
46+
),
47+
}
48+
49+
return a, nil
50+
}
51+
52+
// Run starts the system tray and blocks until Quit is called.
53+
func (a *App) Run() {
54+
systray.Run(a.onReady, a.onExit)
55+
}
56+
57+
// onReady is called when the system tray is initialised. It
58+
// configures the tray icon, menu, and starts adder if configured.
59+
func (a *App) onReady() {
60+
systray.SetTitle("Adder")
61+
systray.SetTooltip("Adder - Cardano Event Streamer")
62+
63+
mStart := systray.AddMenuItem("Start", "Start adder")
64+
mStop := systray.AddMenuItem("Stop", "Stop adder")
65+
mRestart := systray.AddMenuItem(
66+
"Restart", "Restart adder",
67+
)
68+
systray.AddSeparator()
69+
mQuit := systray.AddMenuItem("Quit", "Quit adder-tray")
70+
71+
go func() {
72+
for {
73+
select {
74+
case <-mStart.ClickedCh:
75+
if err := a.process.Start(); err != nil {
76+
slog.Error(
77+
"failed to start adder",
78+
"error", err,
79+
)
80+
}
81+
case <-mStop.ClickedCh:
82+
if err := a.process.Stop(); err != nil {
83+
slog.Error(
84+
"failed to stop adder",
85+
"error", err,
86+
)
87+
}
88+
case <-mRestart.ClickedCh:
89+
if err := a.process.Restart(); err != nil {
90+
slog.Error(
91+
"failed to restart adder",
92+
"error", err,
93+
)
94+
}
95+
case <-mQuit.ClickedCh:
96+
systray.Quit()
97+
return
98+
}
99+
}
100+
}()
101+
102+
slog.Info("starting adder-tray")
103+
104+
if a.config.AutoStart {
105+
if err := a.process.Start(); err != nil {
106+
slog.Error(
107+
"failed to auto-start adder",
108+
"error", err,
109+
)
110+
}
111+
}
112+
}
113+
114+
// onExit is called when the system tray is shutting down.
115+
func (a *App) onExit() {
116+
slog.Info("shutting down adder-tray")
117+
118+
if a.process.IsRunning() {
119+
if err := a.process.Stop(); err != nil {
120+
slog.Error(
121+
"error stopping adder during shutdown",
122+
"error", err,
123+
)
124+
}
125+
}
126+
}
127+
128+
// Shutdown requests a graceful shutdown of the tray application
129+
// and its managed adder process.
130+
func (a *App) Shutdown() {
131+
systray.Quit()
132+
}

tray/config.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright 2026 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package tray
16+
17+
import (
18+
"fmt"
19+
"os"
20+
"path/filepath"
21+
22+
"gopkg.in/yaml.v3"
23+
)
24+
25+
const configFileName = "adder-tray.yaml"
26+
27+
// TrayConfig holds the configuration for the adder-tray application.
28+
type TrayConfig struct {
29+
// AdderBinary is the path to the adder binary.
30+
AdderBinary string `yaml:"adder_binary"`
31+
// AdderConfig is the path to the adder configuration file.
32+
AdderConfig string `yaml:"adder_config"`
33+
// AutoStart controls whether adder starts automatically
34+
// with the tray application.
35+
AutoStart bool `yaml:"auto_start"`
36+
}
37+
38+
// DefaultConfig returns a TrayConfig with sensible defaults.
39+
func DefaultConfig() TrayConfig {
40+
return TrayConfig{
41+
AdderBinary: "adder",
42+
AdderConfig: "",
43+
AutoStart: false,
44+
}
45+
}
46+
47+
// ConfigPath returns the full path to the tray configuration file.
48+
func ConfigPath() string {
49+
return filepath.Join(ConfigDir(), configFileName)
50+
}
51+
52+
// ConfigExists reports whether the configuration file exists on disk.
53+
func ConfigExists() bool {
54+
_, err := os.Stat(ConfigPath())
55+
return err == nil
56+
}
57+
58+
// LoadConfig reads the tray configuration from disk. If the file does
59+
// not exist, it returns the default configuration.
60+
func LoadConfig() (TrayConfig, error) {
61+
cfg := DefaultConfig()
62+
path := ConfigPath()
63+
64+
data, err := os.ReadFile(path)
65+
if err != nil {
66+
if os.IsNotExist(err) {
67+
return cfg, nil
68+
}
69+
return cfg, fmt.Errorf("reading config: %w", err)
70+
}
71+
72+
if err := yaml.Unmarshal(data, &cfg); err != nil {
73+
return cfg, fmt.Errorf("parsing config: %w", err)
74+
}
75+
76+
return cfg, nil
77+
}
78+
79+
// SaveConfig writes the tray configuration to disk, creating the
80+
// config directory if necessary.
81+
func SaveConfig(cfg TrayConfig) error {
82+
dir := ConfigDir()
83+
if err := os.MkdirAll(dir, 0o700); err != nil {
84+
return fmt.Errorf("creating config directory: %w", err)
85+
}
86+
87+
data, err := yaml.Marshal(&cfg)
88+
if err != nil {
89+
return fmt.Errorf("marshalling config: %w", err)
90+
}
91+
92+
if err := os.WriteFile(ConfigPath(), data, 0o600); err != nil {
93+
return fmt.Errorf("writing config: %w", err)
94+
}
95+
96+
return nil
97+
}

0 commit comments

Comments
 (0)