Initial scaffold: project structure, .gitignore, go.mod

This commit is contained in:
Axel Meyer
2026-03-03 21:07:31 +01:00
commit 2256df9dd7
15 changed files with 892 additions and 0 deletions

33
internal/tray/menu.go Normal file
View File

@@ -0,0 +1,33 @@
package tray
import (
"log"
"github.com/energye/systray"
)
// buildMenu creates the initial context menu (Phase 1: minimal).
func (a *App) buildMenu() {
mStatus := systray.AddMenuItem("Status: Connecting...", "")
mStatus.Disable()
systray.AddSeparator()
mOpenPanel := systray.AddMenuItem("Open Admin Panel", "Open Syncthing admin panel")
mOpenPanel.Click(func() {
a.openPanel()
})
systray.AddSeparator()
mQuit := systray.AddMenuItem("Quit", "Exit SyncWarden")
mQuit.Click(func() {
log.Println("Quit clicked")
systray.Quit()
})
// Store reference for updates
a.mu.Lock()
a.statusItem = mStatus
a.mu.Unlock()
}

76
internal/tray/panel.go Normal file
View File

@@ -0,0 +1,76 @@
package tray
import (
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"sync"
)
var (
panelMu sync.Mutex
panelProc *os.Process
)
// launchPanel starts the syncwarden-panel subprocess if not already running.
func launchPanel(baseURL string) {
panelMu.Lock()
defer panelMu.Unlock()
// Check if already running
if panelProc != nil {
// Check if process is still alive
if err := panelProc.Signal(os.Signal(nil)); err == nil {
log.Println("panel already running")
return
}
panelProc = nil
}
panelBin := panelBinaryPath()
if panelBin == "" {
log.Println("syncwarden-panel binary not found")
return
}
cmd := exec.Command(panelBin, "-addr", baseURL)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
log.Printf("failed to start panel: %v", err)
return
}
panelProc = cmd.Process
log.Printf("panel started (pid %d)", cmd.Process.Pid)
// Wait for process to exit in background
go func() {
_ = cmd.Wait()
panelMu.Lock()
panelProc = nil
panelMu.Unlock()
log.Println("panel exited")
}()
}
// panelBinaryPath finds the syncwarden-panel binary next to the main binary.
func panelBinaryPath() string {
exe, err := os.Executable()
if err != nil {
return ""
}
dir := filepath.Dir(exe)
name := "syncwarden-panel"
if runtime.GOOS == "windows" {
name = "syncwarden-panel.exe"
}
p := filepath.Join(dir, name)
if _, err := os.Stat(p); err == nil {
return p
}
return ""
}

107
internal/tray/tray.go Normal file
View File

@@ -0,0 +1,107 @@
package tray
import (
"log"
"sync"
"github.com/energye/systray"
"git.davoryn.de/calic/syncwarden/internal/config"
"git.davoryn.de/calic/syncwarden/internal/icons"
stClient "git.davoryn.de/calic/syncwarden/internal/syncthing"
)
// App manages the tray icon and Syncthing monitoring.
type App struct {
mu sync.Mutex
cfg config.Config
client *stClient.Client
state icons.State
statusItem *systray.MenuItem
}
// Run starts the tray application (blocking).
func Run() {
app := &App{}
systray.Run(app.onReady, app.onExit)
}
func (a *App) onReady() {
a.cfg = config.Load()
// Auto-discover API key if not configured
if a.cfg.SyncthingAPIKey == "" {
if key, err := stClient.DiscoverAPIKey(); err == nil && key != "" {
a.cfg.SyncthingAPIKey = key
_ = config.Save(a.cfg)
log.Printf("auto-discovered Syncthing API key")
}
}
a.client = stClient.NewClient(a.cfg.BaseURL(), a.cfg.SyncthingAPIKey)
// Set initial icon
a.setState(icons.StateDisconnected)
systray.SetTitle("SyncWarden")
systray.SetTooltip("SyncWarden: connecting...")
// Right-click shows menu
systray.SetOnRClick(func(menu systray.IMenu) {
menu.ShowMenu()
})
// Double-click opens admin panel
systray.SetOnDClick(func(menu systray.IMenu) {
a.openPanel()
})
// Build menu
a.buildMenu()
// Check connection
go a.initialCheck()
}
func (a *App) onExit() {
log.Println("SyncWarden exiting")
}
func (a *App) setState(s icons.State) {
a.mu.Lock()
a.state = s
a.mu.Unlock()
iconData, err := icons.Render(s)
if err != nil {
log.Printf("icon render error: %v", err)
return
}
systray.SetIcon(iconData)
}
func (a *App) initialCheck() {
_, err := a.client.Health()
if err != nil {
log.Printf("Syncthing not reachable: %v", err)
a.setState(icons.StateDisconnected)
systray.SetTooltip("SyncWarden: Syncthing not reachable")
a.mu.Lock()
if a.statusItem != nil {
a.statusItem.SetTitle("Status: Disconnected")
}
a.mu.Unlock()
return
}
log.Println("Syncthing is reachable")
a.setState(icons.StateIdle)
systray.SetTooltip("SyncWarden: Idle")
a.mu.Lock()
if a.statusItem != nil {
a.statusItem.SetTitle("Status: Idle")
}
a.mu.Unlock()
}
func (a *App) openPanel() {
launchPanel(a.cfg.BaseURL())
}