aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCaroline Larimore <caroline@larimo.re>2025-01-30 16:39:09 -0800
committerCaroline Larimore <caroline@larimo.re>2025-01-30 16:39:09 -0800
commit784ee51b9613cac5764c9a33c343655e514e20f0 (patch)
treed6291c6c2a6be604913952f529cc335e827473c9
Initial Commit
-rw-r--r--.gitignore1
-rw-r--r--corvid.go85
-rw-r--r--flake.lock27
-rw-r--r--flake.nix20
-rw-r--r--go.mod5
-rw-r--r--go.sum4
-rw-r--r--notification.go86
-rw-r--r--server.go133
8 files changed, 361 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6910f8a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+eww/*
diff --git a/corvid.go b/corvid.go
new file mode 100644
index 0000000..267f4e5
--- /dev/null
+++ b/corvid.go
@@ -0,0 +1,85 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "slices"
+ "sync"
+
+ "github.com/godbus/dbus/v5"
+)
+
+const DEFAULT_EXPIRATION = 5000
+const SORT_DIRECTION = 1 // 1 = newest first, -1 = oldest first
+const DBUS_OBJECT = "/org/freedesktop/Notifications"
+const DBUS_NAME = "org.freedesktop.Notifications"
+
+var conn *dbus.Conn
+
+type notificationStack = struct {
+ mutex *sync.Mutex
+ notifications map[uint32]notification
+ nextId uint32
+}
+
+var notifications = notificationStack{
+ mutex: &sync.Mutex{},
+ notifications: make(map[uint32]notification),
+ nextId: 1,
+}
+
+func output() {
+ arr := make([]notification, len(notifications.notifications))
+
+ i := 0
+ for _, notification := range notifications.notifications {
+ arr[i] = notification
+ i++
+ }
+
+ slices.SortFunc(arr, func(a, b notification) int {
+ if a.Timestamp > b.Timestamp {
+ return SORT_DIRECTION
+ } else if a.Timestamp < b.Timestamp {
+ return -SORT_DIRECTION
+ } else {
+ if a.Id > b.Id {
+ return SORT_DIRECTION
+ } else if a.Id < b.Id {
+ return -SORT_DIRECTION
+ }
+ }
+
+ return 0
+ })
+
+ j, err := json.Marshal(arr)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ fmt.Println(string(j))
+}
+
+func main() {
+ var err error
+ conn, err = dbus.SessionBus()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ conn.Export(server{}, DBUS_OBJECT, DBUS_NAME)
+
+ reply, err := conn.RequestName(DBUS_NAME, dbus.NameFlagReplaceExisting|dbus.NameFlagDoNotQueue)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ if reply != dbus.RequestNameReplyPrimaryOwner {
+ log.Fatalf("'%s' already taken", DBUS_NAME)
+ }
+
+ log.Print("connected to dbus")
+ select {}
+}
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..fbb56cc
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1737746512,
+ "narHash": "sha256-nU6AezEX4EuahTO1YopzueAXfjFfmCHylYEFCagduHU=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "825479c345a7f806485b7f00dbe3abb50641b083",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..68df52c
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,20 @@
+{
+ inputs = rec {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ };
+ outputs = inputs @ {
+ self,
+ nixpkgs,
+ ...
+ }:
+ let
+ pkgs = nixpkgs.legacyPackages.x86_64-linux;
+ in
+ {
+ devShells.x86_64-linux.default = pkgs.mkShell {
+ packages = with pkgs; [
+ go
+ ];
+ };
+ };
+} \ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..28f1b34
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module github.com/CartConnoisseur/corvid
+
+go 1.20.0
+
+require github.com/godbus/dbus/v5 v5.1.0
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..24d6d3b
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,4 @@
+github.com/dblezek/tga v0.0.0-20150626111426-80720cbc1017 h1:awJfkE1xXsPK+yOi1JfFBYCrSBkZXWbOgEFL6dmYeUA=
+github.com/dblezek/tga v0.0.0-20150626111426-80720cbc1017/go.mod h1:47yJHzYP/+2SCHY45B0eyR1QaecoOhkTTpS7UasE0DY=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
diff --git a/notification.go b/notification.go
new file mode 100644
index 0000000..2d5625d
--- /dev/null
+++ b/notification.go
@@ -0,0 +1,86 @@
+package main
+
+import (
+ "encoding/json"
+ "log"
+ "os"
+ "time"
+
+ "github.com/godbus/dbus/v5"
+)
+
+type hint struct {
+ dbus.Variant
+}
+
+func (h hint) MarshalJSON() ([]byte, error) {
+ //TODO: find a better way lol
+ switch h.Signature().String()[0] {
+ case 'y':
+ return json.Marshal(h.Value().(uint8))
+ case 'b':
+ return json.Marshal(h.Value().(bool))
+ case 'n':
+ return json.Marshal(h.Value().(int16))
+ case 'q':
+ return json.Marshal(h.Value().(uint16))
+ case 'i':
+ return json.Marshal(h.Value().(int32))
+ case 'u':
+ return json.Marshal(h.Value().(uint32))
+ case 'x':
+ return json.Marshal(h.Value().(int64))
+ case 't':
+ return json.Marshal(h.Value().(uint64))
+ case 'd':
+ return json.Marshal(h.Value().(float64))
+ case 's':
+ return json.Marshal(h.Value().(string))
+ default:
+ panic("Impossible type")
+ }
+}
+
+type closeReason uint32
+
+const (
+ CloseReasonExpire closeReason = 1
+ CloseReasonDismissed = iota
+ CloseReasonClosed = iota
+ CloseReasonOther = iota
+)
+
+type notification struct {
+ Id uint32 `json:"id"`
+ AppName string `json:"app_name"`
+ AppIcon string `json:"app_icon"`
+ Summary string `json:"summary"`
+ Body string `json:"body"`
+ Actions map[string]string `json:"actions"`
+ Hints map[string]hint `json:"hints"`
+ Timestamp int64 `json:"timestamp"`
+ Expiration int32 `json:"expiration"`
+ Image string `json:"image"`
+ timer *time.Timer
+}
+
+func (n notification) close(reason closeReason) {
+ notifications.mutex.Lock()
+ defer notifications.mutex.Unlock()
+
+ if n.timer != nil {
+ n.timer.Stop()
+ }
+
+ if n.Image != "" {
+ os.Remove(n.Image)
+ }
+
+ delete(notifications.notifications, n.Id)
+ output()
+
+ err := conn.Emit(DBUS_OBJECT, DBUS_NAME+".NotificationClosed", n.Id, reason)
+ if err != nil {
+ log.Print(err)
+ }
+}
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..9b94f70
--- /dev/null
+++ b/server.go
@@ -0,0 +1,133 @@
+package main
+
+import (
+ "image"
+ "image/png"
+ "log"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/godbus/dbus/v5"
+)
+
+type server struct{}
+
+func (s server) GetCapabilities() (capabilities []string, e *dbus.Error) {
+ // log.Print("GetCapabilities called")
+ return []string{
+ "body",
+ "actions",
+ }, nil
+}
+
+func (s server) GetServerInformation() (name, vendor, version, specVersion string, e *dbus.Error) {
+ // log.Print("GetServerInformation called")
+ return "corvid", "CartConnoisseur", "0.1.0", "1.2", nil
+}
+
+func (s server) CloseNotification(id uint32) (e *dbus.Error) {
+ // log.Printf("CloseNotification called: %d", id)
+ notification, ok := notifications.notifications[id]
+ if ok {
+ notification.close(CloseReasonClosed)
+ }
+
+ return nil
+}
+
+func (s server) Notify(appName string, replacesId uint32, appIcon string, summary string, body string, actions []string, hints map[string]dbus.Variant, expireTimeout int32) (id uint32, e *dbus.Error) {
+ // log.Print("Notify called")
+ notifications.mutex.Lock()
+ defer notifications.mutex.Unlock()
+
+ if replacesId == 0 {
+ id = notifications.nextId
+ notifications.nextId++
+ } else {
+ id = replacesId
+ }
+
+ actionMap := make(map[string]string)
+ for i := 0; i < len(actions)-1; i += 2 {
+ actionMap[actions[i]] = actions[i+1]
+ }
+
+ hintMap := make(map[string]hint)
+ img := ""
+
+ for key, value := range hints {
+ if !value.Signature().Empty() {
+ if strings.Contains("ybnqiuxtds", string(value.Signature().String()[0])) {
+ hintMap[key] = hint{Variant: value}
+ } else if key == "image-data" {
+ raw := value.Value().([]interface{})
+
+ var i image.Image
+ if raw[3].(bool) {
+ i = &image.NRGBA{
+ Pix: raw[6].([]uint8),
+ Stride: int(raw[2].(int32)),
+ Rect: image.Rect(0, 0, int(raw[0].(int32)), int(raw[1].(int32))),
+ }
+ } else {
+ rgb := raw[6].([]uint8)
+ rgba := make([]uint8, len(rgb)/3*4)
+
+ for i := 0; i < len(rgb)-1; i += 3 {
+ rgba[i/3*4] = rgb[i]
+ rgba[i/3*4+1] = rgb[i+1]
+ rgba[i/3*4+2] = rgb[i+2]
+ rgba[i/3*4+3] = 0xff
+ }
+
+ i = &image.NRGBA{
+ Pix: rgba,
+ Stride: int(raw[2].(int32)),
+ Rect: image.Rect(0, 0, int(raw[0].(int32)), int(raw[1].(int32))),
+ }
+ }
+ _ = i
+
+ f, err := os.CreateTemp(os.TempDir(), "corvid-*.png")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer f.Close()
+
+ png.Encode(f, i)
+
+ img = f.Name()
+ }
+ }
+ }
+
+ if expireTimeout == -1 {
+ expireTimeout = DEFAULT_EXPIRATION
+ }
+
+ notification := notification{
+ Id: id,
+ AppName: appName,
+ AppIcon: appIcon,
+ Summary: summary,
+ Body: body,
+ Actions: actionMap,
+ Hints: hintMap,
+ Timestamp: time.Now().Unix(),
+ Expiration: expireTimeout,
+ Image: img,
+ timer: nil,
+ }
+
+ if expireTimeout != 0 {
+ notification.timer = time.AfterFunc(time.Duration(expireTimeout)*time.Millisecond, func() {
+ notification.close(CloseReasonExpire)
+ })
+ }
+
+ notifications.notifications[id] = notification
+ output()
+
+ return id, nil
+}