From 6e5ffb8a9505217360c3fecaae5d723a4d4da14e Mon Sep 17 00:00:00 2001 From: Sly_tom_cat Date: Tue, 31 Dec 2024 03:05:09 +0300 Subject: [PATCH] pass notify icon as pixbuf via D-bus --- icons/icons.go | 24 +++----------- icons/icons_test.go | 16 +++------ notify/notify.go | 75 +++++++++++++++++++++++++++++-------------- notify/notify_test.go | 27 +++++++++------- yd.go | 12 +++---- 5 files changed, 80 insertions(+), 74 deletions(-) diff --git a/icons/icons.go b/icons/icons.go index f761c5a..4cefada 100644 --- a/icons/icons.go +++ b/icons/icons.go @@ -3,7 +3,6 @@ package icons import ( "context" "fmt" - "os" "sync" "time" ) @@ -12,7 +11,7 @@ var interval = time.Millisecond * 333 // Icon is the icon helper type Icon struct { - NotifyIcon string // path to notification icon stored as file on disk + NotifyIcon []byte lock sync.Mutex // data protection lock currentStatus string currentIcon int @@ -28,29 +27,20 @@ type Icon struct { // NewIcon initializes the icon helper and returns it. // Use icon.CleanUp() for properly utilization of icon helper. func NewIcon(theme string, set func([]byte)) (*Icon, error) { - file, err := os.CreateTemp("", "yd_notify_icon*.png") - if err != nil { - return nil, fmt.Errorf("icon store error: %v", err) - } - defer file.Close() ctx, cancel := context.WithCancel(context.Background()) i := &Icon{ currentStatus: "", currentIcon: 0, - NotifyIcon: file.Name(), + NotifyIcon: yd128, setFunc: set, ticker: time.NewTicker(interval), stopper: cancel, } i.ticker.Stop() - if err = i.SetTheme(theme); err != nil { + if err := i.SetTheme(theme); err != nil { return nil, err } i.setFunc(i.pauseIcon) - _, err = file.Write(yd128) - if err != nil { - return nil, fmt.Errorf("icon store error: %v", err) - } go i.loop(ctx) return i, nil } @@ -121,12 +111,8 @@ func (i *Icon) loop(ctx context.Context) { } } -// CleanUp removes temporary file for notification icon and stops internal loop -func (i *Icon) CleanUp() error { +// Close stops internal loop +func (i *Icon) Close() { i.ticker.Stop() i.stopper() - if err := os.Remove(i.NotifyIcon); err != nil { - return fmt.Errorf("icon remove error: %v", err) - } - return nil } diff --git a/icons/icons_test.go b/icons/icons_test.go index 2e9da2c..9725fd6 100644 --- a/icons/icons_test.go +++ b/icons/icons_test.go @@ -33,7 +33,7 @@ func TestNewIcon(t *testing.T) { i, err := NewIcon("dark", mi.set) require.NoError(t, err) require.NotNil(t, i) - defer i.CleanUp() + defer i.Close() assert.Equal(t, darkPause, mi.get()) assert.Equal(t, darkError, i.errorIcon) assert.Equal(t, darkIdle, i.idleIcon) @@ -44,7 +44,7 @@ func TestNewIcon(t *testing.T) { func TestSetTheme(t *testing.T) { i, err := NewIcon("dark", mi.set) require.NoError(t, err) - defer i.CleanUp() + defer i.Close() assert.Equal(t, darkPause, mi.get()) i.SetTheme("light") assert.Equal(t, darkPause, mi.get()) // current icon should not be changed if Set() was not called after NewIcon() as status is still unknown @@ -62,7 +62,7 @@ func TestSet(t *testing.T) { i, err := NewIcon("dark", mi.set) require.NoError(t, err) require.NotNil(t, i) - defer i.CleanUp() + defer i.Close() i.Set("error") assert.Equal(t, darkError, mi.get()) i.Set("idle") @@ -84,7 +84,7 @@ func TestAnimation(t *testing.T) { i, err := NewIcon("dark", mi.set) require.NoError(t, err) require.NotNil(t, i) - defer i.CleanUp() + defer i.Close() i.Set("index") assert.Equal(t, darkBusy1, mi.get()) assert.Eventually(t, event(darkBusy2), waitFor, tick) @@ -99,11 +99,3 @@ func TestWrongTheme(t *testing.T) { require.Error(t, err) require.Nil(t, i) } - -func TestDoubleCleanUp(t *testing.T) { - i, err := NewIcon("dark", mi.set) - require.NoError(t, err) - require.NotNil(t, i) - require.NoError(t, i.CleanUp()) - require.Error(t, i.CleanUp()) -} diff --git a/notify/notify.go b/notify/notify.go index c5568f4..272ec74 100644 --- a/notify/notify.go +++ b/notify/notify.go @@ -1,6 +1,9 @@ package notify import ( + "bytes" + "image" + _ "image/png" "sync/atomic" "github.com/godbus/dbus/v5" @@ -8,13 +11,13 @@ import ( // Notify holds D-Bus connection and defaults for the notifications. type Notify struct { - app string - icon string - replace bool - time int - conn *dbus.Conn - connObj dbus.BusObject - lastID uint32 + app string + iconHints map[string]dbus.Variant + replace bool + time int + conn *dbus.Conn + connObj dbus.BusObject + lastID uint32 } const ( @@ -24,21 +27,21 @@ const ( // New creates new Notify component. // The application is the name of application. -// The defaultIcon is icon name/path to be used for notification when no icon specified during the Send call. +// The icon is png/ico image data to use in Send. // True value of replace means that a new notification will replace the previous one if it is still displayed. // The time sets the time in milliseconds after which the notification will disappear. Set it to -1 to use Desktop default settings. -func New(application, defaultIcon string, replace bool, time int) (*Notify, error) { +func New(application string, icon []byte, replace bool, time int) (*Notify, error) { conn, err := dbus.ConnectSessionBus() if err != nil { return nil, err } notify := &Notify{ - app: application, - icon: defaultIcon, - replace: replace, - time: time, - conn: conn, - connObj: conn.Object(dBusDest, dBusPath), + app: application, + iconHints: map[string]dbus.Variant{"image-data": dbus.MakeVariant(convertToPixels(icon))}, + replace: replace, + time: time, + conn: conn, + connObj: conn.Object(dBusDest, dBusPath), } if _, err = notify.Cap(); err != nil { return nil, err @@ -51,22 +54,46 @@ func (n *Notify) Close() { n.conn.Close() } -// Send sends the desktop notification. If icon is not provided ("") then defaultIcon passed to New is used. -func (n *Notify) Send(icon, title, message string) { - - if icon == "" { - icon = n.icon - } +// Send sends the desktop notification. +func (n *Notify) Send(title, message string) { var last uint32 if n.replace { last = atomic.LoadUint32(&n.lastID) } - call := n.connObj.Call(dBusDest+".Notify", dbus.Flags(0), - n.app, last, icon, title, message, []string{}, map[string]interface{}{}, n.time) + call := n.connObj.Call(dBusDest+".Notify", dbus.Flags(0), n.app, last, "", title, message, []string{}, n.iconHints, n.time) if call.Err == nil { atomic.StoreUint32(&n.lastID, call.Body[0].(uint32)) } - // ignore any error as it can be called even New return error. + // ignore rest possible errors +} + +type ImageData struct { + Width, Height, RowStride int32 + HasAlpha bool + BitsPerSample, Channels int32 + ImageData []byte +} + +func convertToPixels(data []byte) ImageData { + src, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + panic(err) + } + img := image.NewRGBA(src.Bounds()) + for x := range src.Bounds().Dx() { + for y := range src.Bounds().Dy() { + img.Set(x, y, src.At(x, y)) + } + } + return ImageData{ + Width: int32(img.Bounds().Dx()), + Height: int32(img.Bounds().Dy()), + RowStride: int32(img.Stride), + HasAlpha: true, + BitsPerSample: 8, + Channels: 4, + ImageData: img.Pix, + } } // Cap returns the notification server capabilities diff --git a/notify/notify_test.go b/notify/notify_test.go index df85bca..100baa8 100644 --- a/notify/notify_test.go +++ b/notify/notify_test.go @@ -13,8 +13,15 @@ func TestDBusNotify(t *testing.T) { if os.Getenv("CI") != "" { t.Skip("Skipping testing in CI environment") } - icon := "dialog-information" - n, err := New("appName", "", true, -1) + // read icon + p, err := os.Getwd() + require.NoError(t, err) + p, _ = path.Split(p) + p += "/icons/img/yd128.png" + icon, err := os.ReadFile(p) + require.NoError(t, err) + + n, err := New("appName", icon, true, -1) require.NoError(t, err) require.NotNil(t, n) defer n.Close() @@ -23,19 +30,15 @@ func TestDBusNotify(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, cap) - n.Send(icon, "title", "message") + n.Send("title", "message") time.Sleep(time.Second) - n.Send("dialog-error", "title1", "message1") + n.Send("title1", "message1") time.Sleep(time.Second) - n.Send("dialog-warning", "title2", "message2") + n.Send("title2", "message2") time.Sleep(time.Second) - n.Send(icon, "title3", "message3") + n.replace = false + n.Send("title3", "message3") time.Sleep(time.Second) - n.Send("", "title4", "message4") + n.Send("title4", "message4") time.Sleep(time.Second) - p, err := os.Getwd() - require.NoError(t, err) - p, _ = path.Split(p) - p += "/icons/img/yd128.png" - n.Send(p, "cool title", "cool message") } diff --git a/yd.go b/yd.go index fd3ce30..41808bf 100644 --- a/yd.go +++ b/yd.go @@ -122,7 +122,7 @@ func onReady() { notifyAvailable = true notifySend = func(title, body string) { log.Debug("sending_message", "title", title, "message", body) - notifyHandler.Send("", title, body) + notifyHandler.Send(title, body) } } @@ -197,13 +197,13 @@ func eventHandler(m *menu, cfg *tools.Config, YD *ydisk.YDisk, notifyHandler *no go YD.Start() } defer func() { - if notifyHandler != nil { - notifyHandler.Close() - } if cfg.StopDaemon { YD.Stop() } YD.Close() + if notifyHandler != nil { + notifyHandler.Close() + } systray.Quit() }() // register interrupt signals chan @@ -363,8 +363,6 @@ func handleNotifications(yds *ydisk.YDvals) { func onExit() { appConfig.Save() - if err := icon.CleanUp(); err != nil { - log.Error("icon_cleanup", "error", err) - } + icon.Close() log.Debug("exit", "status", "done") }