diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | corvid.go | 85 | ||||
| -rw-r--r-- | flake.lock | 27 | ||||
| -rw-r--r-- | flake.nix | 20 | ||||
| -rw-r--r-- | go.mod | 5 | ||||
| -rw-r--r-- | go.sum | 4 | ||||
| -rw-r--r-- | notification.go | 86 | ||||
| -rw-r--r-- | server.go | 133 |
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 @@ -0,0 +1,5 @@ +module github.com/CartConnoisseur/corvid + +go 1.20.0 + +require github.com/godbus/dbus/v5 v5.1.0 @@ -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 +} |