package main import ( "context" "log" "os" "path/filepath" "github.com/wailsapp/wails/v2/pkg/runtime" "github.com/waste-go/internal/crypto" "github.com/waste-go/internal/ipc" "github.com/waste-go/internal/netmgr" "github.com/waste-go/internal/proto" ) const wsPort = 17338 // App is the Wails application backend. // It embeds the daemon directly — no subprocess needed. // The React frontend connects to the daemon's WebSocket IPC at ws://127.0.0.1:17338, // exactly as it does in browser-daemon mode. type App struct { ctx context.Context mgr *netmgr.Manager } func newApp() *App { return &App{} } // startup is called when the Wails window is ready. It initialises the daemon, // starts the WebSocket IPC listener, sets up the system tray, and begins // forwarding message/file events to the webview as OS notifications. func (a *App) startup(ctx context.Context) { a.ctx = ctx dir := dataDir() id, err := crypto.LoadOrCreate(dir, "") if err != nil { log.Printf("app: identity: %v", err) runtime.MessageDialog(ctx, runtime.MessageDialogOptions{ Type: runtime.ErrorDialog, Title: "waste — startup error", Message: "Failed to load identity: " + err.Error(), }) return } log.Printf("app: peer id: %s alias: %s", id.PeerID().Short(), id.Alias) a.mgr = netmgr.New(netmgr.Config{ MasterIdentity: id, StoreDir: dir, }) // Forward daemon events to the webview and generate OS notifications. go a.watchEvents() // Start the WebSocket IPC server; the webview connects here (daemon mode). go func() { if err := ipc.RunWS(a.mgr, wsPort); err != nil { log.Printf("app: IPC: %v", err) } }() log.Printf("app: WebSocket IPC listening on 127.0.0.1:%d", wsPort) // System tray (Linux/Windows; no-op on macOS — see tray_darwin.go). a.startTray() } // shutdown is called when the Wails app exits. func (a *App) shutdown(ctx context.Context) { if a.mgr != nil { a.mgr.LeaveAll() } } // watchEvents subscribes to all daemon events and emits OS notifications for // incoming messages and completed file transfers. The "notify" event is received // by the frontend via Wails EventsOn and displayed using the browser Notification API. func (a *App) watchEvents() { if a.mgr == nil { return } events := a.mgr.Subscribe() defer a.mgr.Unsubscribe(events) for evt := range events { switch evt.Type { case proto.EvtMessageReceived: if evt.Message == nil { continue } // Don't notify for messages sent by the local peer. if a.mgr.MasterIdentity() != nil && evt.Message.From == a.mgr.MasterIdentity().PeerID() { continue } runtime.EventsEmit(a.ctx, "notify", map[string]string{ "title": "waste — new message", "body": evt.Message.Text, }) case proto.EvtFileComplete: runtime.EventsEmit(a.ctx, "notify", map[string]string{ "title": "waste — file received", "body": filepath.Base(evt.Path), }) } } } // dataDir returns the OS-appropriate config directory for identity and stores. // macOS: ~/Library/Application Support/waste // Linux: ~/.config/waste // Windows: %APPDATA%\waste func dataDir() string { base, err := os.UserConfigDir() if err != nil { if home, err := os.UserHomeDir(); err == nil { return filepath.Join(home, ".waste") } return ".waste" } dir := filepath.Join(base, "waste") if err := os.MkdirAll(dir, 0o700); err != nil { log.Printf("app: mkdir %s: %v", dir, err) } return dir }