Initial scaffold: project structure, .gitignore, go.mod
This commit is contained in:
33
internal/tray/menu.go
Normal file
33
internal/tray/menu.go
Normal 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
76
internal/tray/panel.go
Normal 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
107
internal/tray/tray.go
Normal 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())
|
||||
}
|
||||
Reference in New Issue
Block a user