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>
237 lines
6.7 KiB
Go
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
|
|
}
|