PrintToVeraeTimeStamp/internal/netprint/tray_menu.go
Cursor Agent 927229ab4f
AirDrop stamped PDFs from the macOS menu bar.
The menu-bar app, not the root daemon, opens the AirDrop share sheet for each finished job when the setting is on. The unsigned arm64 package installs the daemon and LaunchAgent, and Darwin DNSServiceRegister advertises the _universal and _print subtypes.

Co-authored-by: George Lambert <marchon@gmail.com>
2026-10-05 01:27:23 +00:00

237 lines
6.7 KiB
Go

package netprint
import (
"fmt"
"strings"
)
const (
idStatus = 1
idStart = 3
idStop = 4
idRestart = 5
idLaunch = 7
idTray = 8
idPublic = 9
idReceipts = 10
idSettings = 11
idEmail = 12
idAirDrop = 13
idAdvertise = 14
idAirDropAgain = 15
idQuit = 100
idRecent = 20
)
// MenuEntry is one row in the tray or menu bar. The UI layers only display it.
type MenuEntry struct {
ID int
Label string
Enabled bool
Checked bool
Separator bool
URL string
Kind string
}
// Probe reads status from the loopback control API. A failure means the service is stopped.
func Probe() Status {
st, err := NewClient(8630).Status()
if err != nil {
return StoppedStatus()
}
return st
}
// BuildMenu is the control surface: status, service, login items, receipts, and settings.
func BuildMenu(st Status) []MenuEntry {
state := "Stopped"
if st.Running {
state = "Running"
}
where := st.Name
if where == "" {
where = "Verae Timestamp"
}
if len(st.Addresses) > 0 && st.Port > 0 {
where = fmt.Sprintf("%s · %s:%d", where, st.Addresses[0], st.Port)
}
var out []MenuEntry
out = append(out,
MenuEntry{ID: idStatus, Label: fmt.Sprintf("%s — %s · %d jobs today", state, where, st.JobsToday), Kind: "status"},
MenuEntry{Label: discoveryLine(st), Kind: "status"},
)
if st.NetworkPublic && !st.AllowPublic {
out = append(out, MenuEntry{Label: "This network is Public. Other devices cannot reach the printer until you allow Public networks.", Kind: "status"})
}
out = append(out,
MenuEntry{Separator: true},
MenuEntry{ID: idStart, Label: "Start", Enabled: !st.Running, Kind: "start"},
MenuEntry{ID: idStop, Label: "Stop", Enabled: st.Running, Kind: "stop"},
MenuEntry{ID: idRestart, Label: "Restart", Enabled: st.Running, Kind: "restart"},
MenuEntry{Separator: true},
MenuEntry{ID: idLaunch, Label: "Launch printer at login", Enabled: st.Running, Checked: st.LaunchAtLogin, Kind: "launch"},
MenuEntry{ID: idTray, Label: "Launch this menu at login", Enabled: st.Running, Checked: st.TrayAtLogin, Kind: "tray"},
)
if st.SupportsPublic {
out = append(out, MenuEntry{ID: idPublic, Label: "Allow Public networks", Enabled: st.Running, Checked: st.AllowPublic, Kind: "public"})
}
if st.SupportsAirDrop {
out = append(out, MenuEntry{ID: idAdvertise, Label: "Advertise on the network", Enabled: st.Running, Checked: st.Advertise, Kind: "advertise"})
}
out = append(out, MenuEntry{Separator: true},
MenuEntry{ID: idEmail, Label: "Email stamped PDFs", Enabled: st.Running, Checked: st.EmailEnabled, Kind: "email"},
)
if st.SupportsAirDrop {
out = append(out, MenuEntry{ID: idAirDrop, Label: "AirDrop stamped PDF after each job", Enabled: st.Running, Checked: st.AirDropEnabled, Kind: "airdrop"})
}
if st.EmailError != "" {
out = append(out, MenuEntry{Label: "Email — " + clipMenu(st.EmailError, 90), Kind: "status"})
} else if st.EmailEnabled {
to := st.EmailTo
if to == "" {
to = DefaultEmailTo
}
out = append(out, MenuEntry{Label: "Email — on, to " + to, Kind: "status"})
}
if st.ReceiptsURL != "" {
out = append(out, MenuEntry{Label: "Receipts " + st.ReceiptsURL, Kind: "status"})
}
out = append(out,
MenuEntry{ID: idReceipts, Label: "Open receipts", Enabled: st.Running && (st.ReceiptsURL != "" || st.ControlURL != "" || st.AdminURL != ""), URL: localReceipts(st), Kind: "open"},
)
for i, rec := range st.Recent {
if i >= 6 {
break
}
label := firstNonEmpty(rec.Title, fmt.Sprintf("Job %d", rec.ID))
if rec.ReceiptID == "" {
continue
}
out = append(out, MenuEntry{
ID: idRecent + i, Label: "Receipt · " + label, Enabled: true,
URL: receiptURL(st, rec), Kind: "open",
})
}
settings := strings.TrimRight(st.ControlURL, "/") + "/settings"
if st.ControlURL == "" {
settings = "http://127.0.0.1:8630/settings"
}
out = append(out,
MenuEntry{ID: idSettings, Label: "Settings…", Enabled: st.Running, URL: settings, Kind: "open"},
MenuEntry{Separator: true},
MenuEntry{ID: idQuit, Label: "Quit menu", Enabled: true, Kind: "quit"},
)
return out
}
func discoveryLine(st Status) string {
if !st.Running {
return "Discovery — service stopped"
}
switch st.MDNSMode {
case "dns-sd":
if st.MDNSAdvertising {
if strings.Contains(st.MDNSDetail, "_ipp._tcp,_universal") {
return "Discovery — advertised (Bonjour _universal and _print)"
}
return "Discovery — advertised (Windows DNS-SD)"
}
case "udp":
if st.MDNSAdvertising {
return "Discovery — advertised (UDP 5353)"
}
case "off":
return "Discovery — off"
case "starting":
return "Discovery — starting"
case "error":
return "Discovery — not advertising: " + clipMenu(st.MDNSError, 80)
}
if st.MDNSError != "" {
return "Discovery — " + clipMenu(st.MDNSError, 80)
}
if st.MDNSDetail != "" {
return "Discovery — " + clipMenu(st.MDNSDetail, 80)
}
return "Discovery — waiting"
}
func clipMenu(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n-1] + "…"
}
func localReceipts(st Status) string {
if u := strings.TrimRight(st.ControlURL, "/"); u != "" {
return u + "/receipts"
}
if st.ReceiptsURL != "" {
return st.ReceiptsURL
}
if st.AdminURL != "" {
return st.AdminURL
}
return "http://127.0.0.1:8630/receipts"
}
func receiptURL(st Status, rec Record) string {
base := strings.TrimRight(st.ControlURL, "/")
if base == "" {
base = "http://127.0.0.1:8630"
}
return fmt.Sprintf("%s/jobs/%d/receipt.pdf", base, rec.ID)
}
// Dispatch performs one menu action. Settings and receipts open in the browser.
// Start uses the OS service manager when the control API is down. Other changes
// go through that API so this process never edits config files.
func Dispatch(st Status, e MenuEntry) error {
c := NewClient(8630)
if st.ControlURL != "" {
c.BaseURL = strings.TrimRight(st.ControlURL, "/")
}
switch e.Kind {
case "start":
if st.Running {
return c.Service("start")
}
return NewServiceControl().Start()
case "stop":
return c.Service("stop")
case "restart":
return c.Service("restart")
case "launch":
return c.SetLaunchAtLogin(!st.LaunchAtLogin)
case "tray":
return c.SetTrayAtLogin(!st.TrayAtLogin)
case "public":
return c.SetAllowPublic(!st.AllowPublic)
case "email":
return c.SetEmailEnabled(!st.EmailEnabled)
case "advertise":
return c.SetAdvertise(!st.Advertise)
case "airdrop":
return c.SetAirDropEnabled(!st.AirDropEnabled)
case "airdrop-again":
return airDropAgain()
case "open":
return OpenURL(e.URL)
case "quit":
return errQuit
default:
return nil
}
}
type quitError struct{}
func (quitError) Error() string { return "quit" }
var errQuit quitError
func IsQuit(err error) bool {
return err == errQuit
}