Skip to content

Commit bd5270d

Browse files
committed
feat(tray): connection manager
Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
1 parent 1e90d71 commit bd5270d

File tree

6 files changed

+503
-238
lines changed

6 files changed

+503
-238
lines changed

tray/app.go

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,17 @@ package tray
1616

1717
import (
1818
"log/slog"
19+
"os/exec"
20+
"runtime"
1921

2022
"fyne.io/systray"
2123
)
2224

2325
// App holds references to all major components of the tray
2426
// application.
2527
type App struct {
26-
config TrayConfig
27-
process *ProcessManager
28+
config TrayConfig
29+
conn *ConnectionManager
2830
}
2931

3032
// NewApp creates and initialises the tray application.
@@ -40,9 +42,9 @@ func NewApp() (*App, error) {
4042

4143
a := &App{
4244
config: cfg,
43-
process: NewProcessManager(
44-
WithBinary(cfg.AdderBinary),
45-
WithConfigFile(cfg.AdderConfig),
45+
conn: NewConnectionManager(
46+
WithConnectionAddress(cfg.APIAddress),
47+
WithConnectionPort(cfg.APIPort),
4648
),
4749
}
4850

@@ -55,43 +57,65 @@ func (a *App) Run() {
5557
}
5658

5759
// onReady is called when the system tray is initialised. It
58-
// configures the tray icon, menu, and starts adder if configured.
60+
// configures the tray icon, menu, and connects to adder if
61+
// configured.
5962
func (a *App) onReady() {
6063
systray.SetTitle("Adder")
6164
systray.SetTooltip("Adder - Cardano Event Streamer")
6265

63-
mStart := systray.AddMenuItem("Start", "Start adder")
64-
mStop := systray.AddMenuItem("Stop", "Stop adder")
65-
mRestart := systray.AddMenuItem(
66-
"Restart", "Restart adder",
66+
mStatus := systray.AddMenuItem(
67+
"Status: "+a.conn.status.Status().String(), "",
6768
)
69+
mStatus.Disable()
6870
systray.AddSeparator()
71+
72+
mConnect := systray.AddMenuItem("Connect", "Connect to adder")
73+
mDisconnect := systray.AddMenuItem(
74+
"Disconnect", "Disconnect from adder",
75+
)
76+
mReconnect := systray.AddMenuItem(
77+
"Reconnect", "Reconnect to adder",
78+
)
79+
systray.AddSeparator()
80+
81+
mShowConfig := systray.AddMenuItem(
82+
"Show Config Folder", "Open the config directory",
83+
)
84+
mShowLogs := systray.AddMenuItem(
85+
"Show Logs", "Open the log directory",
86+
)
87+
systray.AddSeparator()
88+
6989
mQuit := systray.AddMenuItem("Quit", "Quit adder-tray")
7090

91+
// Update status menu item when connection status changes
92+
a.conn.status.OnChange(func(s Status) {
93+
mStatus.SetTitle("Status: " + s.String())
94+
})
95+
7196
go func() {
7297
for {
7398
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 {
99+
case <-mConnect.ClickedCh:
100+
if err := a.conn.Connect(); err != nil {
83101
slog.Error(
84-
"failed to stop adder",
102+
"failed to connect",
85103
"error", err,
86104
)
87105
}
88-
case <-mRestart.ClickedCh:
89-
if err := a.process.Restart(); err != nil {
106+
case <-mDisconnect.ClickedCh:
107+
a.conn.Disconnect()
108+
case <-mReconnect.ClickedCh:
109+
if err := a.conn.Reconnect(); err != nil {
90110
slog.Error(
91-
"failed to restart adder",
111+
"failed to reconnect",
92112
"error", err,
93113
)
94114
}
115+
case <-mShowConfig.ClickedCh:
116+
openFolder(ConfigDir())
117+
case <-mShowLogs.ClickedCh:
118+
openFolder(LogDir())
95119
case <-mQuit.ClickedCh:
96120
systray.Quit()
97121
return
@@ -102,31 +126,42 @@ func (a *App) onReady() {
102126
slog.Info("starting adder-tray")
103127

104128
if a.config.AutoStart {
105-
if err := a.process.Start(); err != nil {
129+
if err := a.conn.Connect(); err != nil {
106130
slog.Error(
107-
"failed to auto-start adder",
131+
"failed to auto-connect to adder",
108132
"error", err,
109133
)
110134
}
111135
}
112136
}
113137

114-
// onExit is called when the system tray is shutting down.
138+
// onExit is called when the system tray is shutting down. It
139+
// disconnects the WS client but does NOT stop the adder service.
115140
func (a *App) onExit() {
116141
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-
}
142+
a.conn.Disconnect()
126143
}
127144

128-
// Shutdown requests a graceful shutdown of the tray application
129-
// and its managed adder process.
145+
// Shutdown requests a graceful shutdown of the tray application.
130146
func (a *App) Shutdown() {
131147
systray.Quit()
132148
}
149+
150+
// openFolder opens the given directory in the platform file manager.
151+
func openFolder(dir string) {
152+
var cmd string
153+
switch runtime.GOOS {
154+
case "darwin":
155+
cmd = "open"
156+
case "windows":
157+
cmd = "explorer"
158+
default:
159+
cmd = "xdg-open"
160+
}
161+
p := exec.Command(cmd, dir) //nolint:gosec // directory path from internal config
162+
if err := p.Start(); err != nil {
163+
slog.Error("failed to open folder", "dir", dir, "error", err)
164+
return
165+
}
166+
_ = p.Process.Release()
167+
}

tray/config.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,22 @@ const configFileName = "adder-tray.yaml"
2626

2727
// TrayConfig holds the configuration for the adder-tray application.
2828
type TrayConfig struct {
29-
// AdderBinary is the path to the adder binary.
30-
AdderBinary string `yaml:"adder_binary"`
29+
// APIAddress is the address of the adder API server.
30+
APIAddress string `yaml:"api_address"`
31+
// APIPort is the port of the adder API server.
32+
APIPort uint `yaml:"api_port"`
3133
// AdderConfig is the path to the adder configuration file.
3234
AdderConfig string `yaml:"adder_config"`
33-
// AutoStart controls whether adder starts automatically
34-
// with the tray application.
35+
// AutoStart controls whether the tray connects to adder
36+
// automatically on launch.
3537
AutoStart bool `yaml:"auto_start"`
3638
}
3739

3840
// DefaultConfig returns a TrayConfig with sensible defaults.
3941
func DefaultConfig() TrayConfig {
4042
return TrayConfig{
41-
AdderBinary: "adder",
43+
APIAddress: "127.0.0.1",
44+
APIPort: 8080,
4245
AdderConfig: "",
4346
AutoStart: false,
4447
}
@@ -73,9 +76,24 @@ func LoadConfig() (TrayConfig, error) {
7376
return cfg, fmt.Errorf("parsing config: %w", err)
7477
}
7578

79+
if err := validateConfig(cfg); err != nil {
80+
return cfg, err
81+
}
82+
7683
return cfg, nil
7784
}
7885

86+
// validateConfig checks that required TrayConfig fields are set.
87+
func validateConfig(cfg TrayConfig) error {
88+
if cfg.APIAddress == "" {
89+
return fmt.Errorf("invalid config: api_address must not be empty")
90+
}
91+
if cfg.APIPort == 0 {
92+
return fmt.Errorf("invalid config: api_port must be greater than 0")
93+
}
94+
return nil
95+
}
96+
7997
// SaveConfig writes the tray configuration to disk, creating the
8098
// config directory if necessary.
8199
func SaveConfig(cfg TrayConfig) error {

tray/config_test.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ func TestConfigPath(t *testing.T) {
4242

4343
func TestDefaultConfig(t *testing.T) {
4444
cfg := DefaultConfig()
45-
assert.Equal(t, "adder", cfg.AdderBinary)
45+
assert.Equal(t, "127.0.0.1", cfg.APIAddress)
46+
assert.Equal(t, uint(8080), cfg.APIPort)
4647
assert.Equal(t, "", cfg.AdderConfig)
4748
assert.False(t, cfg.AutoStart)
4849
}
@@ -70,7 +71,8 @@ func TestSaveAndLoadConfig(t *testing.T) {
7071
t.Setenv("XDG_CONFIG_HOME", tmpDir)
7172

7273
original := TrayConfig{
73-
AdderBinary: "/usr/local/bin/adder",
74+
APIAddress: "192.168.1.100",
75+
APIPort: 9090,
7476
AdderConfig: "/etc/adder/config.yaml",
7577
AutoStart: true,
7678
}
@@ -87,6 +89,53 @@ func TestSaveAndLoadConfig(t *testing.T) {
8789
assert.Equal(t, original, loaded)
8890
}
8991

92+
func TestLoadConfigInvalidAddress(t *testing.T) {
93+
if runtime.GOOS != "linux" {
94+
t.Skip("XDG_CONFIG_HOME only applies on Linux")
95+
}
96+
tmpDir := t.TempDir()
97+
t.Setenv("XDG_CONFIG_HOME", tmpDir)
98+
99+
// Write a config with empty api_address
100+
err := SaveConfig(TrayConfig{
101+
APIAddress: "placeholder",
102+
APIPort: 8080,
103+
})
104+
require.NoError(t, err)
105+
106+
// Overwrite with invalid content
107+
err = os.WriteFile(
108+
ConfigPath(),
109+
[]byte("api_address: \"\"\napi_port: 8080\n"),
110+
0o600,
111+
)
112+
require.NoError(t, err)
113+
114+
_, err = LoadConfig()
115+
assert.ErrorContains(t, err, "api_address")
116+
}
117+
118+
func TestLoadConfigInvalidPort(t *testing.T) {
119+
if runtime.GOOS != "linux" {
120+
t.Skip("XDG_CONFIG_HOME only applies on Linux")
121+
}
122+
tmpDir := t.TempDir()
123+
t.Setenv("XDG_CONFIG_HOME", tmpDir)
124+
125+
err := os.MkdirAll(filepath.Dir(ConfigPath()), 0o700)
126+
require.NoError(t, err)
127+
128+
err = os.WriteFile(
129+
ConfigPath(),
130+
[]byte("api_address: \"127.0.0.1\"\napi_port: 0\n"),
131+
0o600,
132+
)
133+
require.NoError(t, err)
134+
135+
_, err = LoadConfig()
136+
assert.ErrorContains(t, err, "api_port")
137+
}
138+
90139
func TestConfigExists(t *testing.T) {
91140
if runtime.GOOS != "linux" {
92141
t.Skip("XDG_CONFIG_HOME only applies on Linux")

0 commit comments

Comments
 (0)