aboutsummaryrefslogtreecommitdiffstats
path: root/cmd/notifyctl
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-15 13:53:34 +0200
committerDanilo M. <danix@danix.xyz>2026-09-15 13:53:34 +0200
commitbcf15590f9e43f2d8978cf7cccfbdf3bb66e376f (patch)
treebf6d77460dc7025582fc9a486d9271b176cb6e1b /cmd/notifyctl
parent3bdd5a50c3b8fa0a885dc58a3d0ec64880f95619 (diff)
downloadnotifyd-bcf15590f9e43f2d8978cf7cccfbdf3bb66e376f.tar.gz
notifyd-bcf15590f9e43f2d8978cf7cccfbdf3bb66e376f.zip
feat: add the control interface and notifyctl
The private interface carries what the spec cannot: close-all, invoke action and clear history. notifyctl reads the published files for list and history, because the files are the interface, and uses D-Bus only for the mutations.
Diffstat (limited to 'cmd/notifyctl')
-rw-r--r--cmd/notifyctl/main.go127
1 files changed, 127 insertions, 0 deletions
diff --git a/cmd/notifyctl/main.go b/cmd/notifyctl/main.go
new file mode 100644
index 0000000..ca1db86
--- /dev/null
+++ b/cmd/notifyctl/main.go
@@ -0,0 +1,127 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// 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 <verb>
+ list the live queue as JSON
+ history [n] the history ring as JSON, default 20
+ close <id> dismiss one notification
+ close-all dismiss every notification
+ action <id> <key> 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)
+ }
+}