From 784ee51b9613cac5764c9a33c343655e514e20f0 Mon Sep 17 00:00:00 2001 From: Caroline Larimore Date: Thu, 30 Jan 2025 16:39:09 -0800 Subject: Initial Commit --- .gitignore | 1 + corvid.go | 85 ++++++++++++++++++++++++++++++++++++ flake.lock | 27 ++++++++++++ flake.nix | 20 +++++++++ go.mod | 5 +++ go.sum | 4 ++ notification.go | 86 ++++++++++++++++++++++++++++++++++++ server.go | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 361 insertions(+) create mode 100644 .gitignore create mode 100644 corvid.go create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 go.mod create mode 100644 go.sum create mode 100644 notification.go create mode 100644 server.go 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 +} -- cgit v1.2.3