feat(tray): desktop tray icon companion with Theta logo, color-coded status, and home LAN detection (#10)
- tray_icons.go / cmd/theta-agent-tray: Renders iconic Theta 42 logo in status colors:
- 🔴 Red: Not connected to directory
- 🟡 Yellow: Connected to directory, but not on home LAN
- 🟢 Green: Connected to directory on home LAN
- 🔵 Blue: Connected to directory with active WireGuard tunnel
- home_detect.go: Compares agent public IP with home site public IP
- tray_server.go / tray_ipc.go: Unix socket IPC daemon server (/run/theta/tray.sock or /tmp/theta-tray.sock)
- cmd/theta-agent-tray: Desktop GUI binary with system tray menu (Auto-connect toggle, Connect/Disconnect VPN)
- Auto-detects DISPLAY / WAYLAND_DISPLAY environment variables
This commit is contained in:
+68
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
// IPC protocol between the root daemon (theta-agent) and the desktop tray
|
||||
// (theta-agent-tray). Both processes communicate via a Unix domain socket at
|
||||
// /run/theta/tray.sock (created by the daemon; the tray connects to it).
|
||||
//
|
||||
// Protocol: newline-delimited JSON. The daemon streams TrayStatus messages to
|
||||
// any connected tray client. The tray sends TrayCommand messages to the daemon.
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// TraySocket is the path to the IPC socket the daemon listens on.
|
||||
// Falls back to /tmp/theta-tray.sock if /run/theta/ is not writable.
|
||||
var TraySocketPaths = []string{
|
||||
"/run/theta/tray.sock",
|
||||
"/tmp/theta-tray.sock",
|
||||
}
|
||||
const TraySocket = "/tmp/theta-tray.sock"
|
||||
|
||||
|
||||
// TrayColor represents the icon color state.
|
||||
type TrayColor string
|
||||
|
||||
const (
|
||||
ColorRed TrayColor = "red" // not connected to directory
|
||||
ColorYellow TrayColor = "yellow" // connected, but not home
|
||||
ColorGreen TrayColor = "green" // connected, on home LAN (public IP matches)
|
||||
ColorBlue TrayColor = "blue" // connected + WireGuard tunnel active to home site
|
||||
)
|
||||
|
||||
// TrayStatus is sent from the daemon to the tray on every state change.
|
||||
type TrayStatus struct {
|
||||
Color TrayColor `json:"color"`
|
||||
Connected bool `json:"connected"` // directory WebSocket is up
|
||||
IsHome bool `json:"is_home"` // public IP matches home site
|
||||
VPNActive bool `json:"vpn_active"` // WireGuard tunnel is up
|
||||
AutoVPN bool `json:"auto_vpn"` // auto-connect preference
|
||||
SiteName string `json:"site_name"` // configured site name
|
||||
AgentPublicIP string `json:"agent_public_ip"` // this agent's detected public IP
|
||||
HomePublicIP string `json:"home_public_ip"` // home site's public IP (from directory)
|
||||
StatusText string `json:"status_text"` // one-line human description
|
||||
}
|
||||
|
||||
// TrayCommand is sent from the tray to the daemon.
|
||||
type TrayCommand struct {
|
||||
Command string `json:"command"` // "set_auto_vpn", "vpn_connect", "vpn_disconnect"
|
||||
Value bool `json:"value"` // used by set_auto_vpn
|
||||
}
|
||||
|
||||
func encodeTrayStatus(s TrayStatus) ([]byte, error) {
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(b, '\n'), nil
|
||||
}
|
||||
|
||||
func decodeTrayCommand(data []byte) (TrayCommand, error) {
|
||||
var cmd TrayCommand
|
||||
err := json.Unmarshal(data, &cmd)
|
||||
return cmd, err
|
||||
}
|
||||
|
||||
func decodeTrayStatus(data []byte) (TrayStatus, error) {
|
||||
var s TrayStatus
|
||||
err := json.Unmarshal(data, &s)
|
||||
return s, err
|
||||
}
|
||||
Reference in New Issue
Block a user