// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. package main import ( "encoding/json" "fmt" "os" "path/filepath" "strconv" "danix.xyz/notifyd/internal/notify" "github.com/godbus/dbus/v5" ) func usage() { fmt.Fprintf(os.Stderr, `usage: %s list the live queue as JSON history [n] the history ring as JSON, default 20 close dismiss one notification close-all dismiss every notification action invoke an action on a live notification clear-history empty the history ring `, filepath.Base(os.Args[0])) os.Exit(2) } func main() { if len(os.Args) < 2 { usage() } verb := os.Args[1] switch verb { case "list": printFile("queue.json", 0) case "history": limit := 20 if len(os.Args) >= 3 { n, err := strconv.Atoi(os.Args[2]) if err != nil { usage() } limit = n } printFile("history.json", limit) case "close": need(3) id := parseID(os.Args[2]) callControl("Dismiss", id) case "close-all": callControl("CloseAll") case "action": need(4) id := parseID(os.Args[2]) callControl("InvokeAction", id, os.Args[3]) case "clear-history": callControl("ClearHistory") default: usage() } } func need(n int) { if len(os.Args) < n { usage() } } func parseID(s string) uint32 { id, err := strconv.ParseUint(s, 10, 32) if err != nil { fmt.Fprintf(os.Stderr, "%s: bad id %q\n", filepath.Base(os.Args[0]), s) os.Exit(1) } return uint32(id) } // printFile reads a published file, because the files are the interface. func printFile(name string, limit int) { data, err := os.ReadFile(filepath.Join(notify.RuntimeDir(), name)) if err != nil { fmt.Fprintf(os.Stderr, "notifyctl: %v\n", err) os.Exit(1) } if limit > 0 { var all []notify.Popup if err := json.Unmarshal(data, &all); err != nil { fmt.Fprintf(os.Stderr, "notifyctl: %v\n", err) os.Exit(1) } if len(all) > limit { all = all[:limit] } out, _ := json.MarshalIndent(all, "", " ") fmt.Println(string(out)) return } var pretty any json.Unmarshal(data, &pretty) out, _ := json.MarshalIndent(pretty, "", " ") fmt.Println(string(out)) } // callControl drives the daemon over the private interface. It is the only // thing that mutates state. func callControl(method string, args ...any) { conn, err := dbus.SessionBus() if err != nil { fmt.Fprintf(os.Stderr, "notifyctl: session bus: %v\n", err) os.Exit(1) } obj := conn.Object(notify.Name, dbus.ObjectPath("/xyz/danix/Notifyd")) if err := obj.Call("xyz.danix.Notifyd."+method, 0, args...).Err; err != nil { fmt.Fprintf(os.Stderr, "notifyctl: %v\n", err) os.Exit(1) } }