From bd45fa452a9dcf8d3ceede4e18d9b1cb76cc507f Mon Sep 17 00:00:00 2001 From: yetone Date: Sat, 3 Oct 2026 20:55:11 +0800 Subject: [PATCH] =?UTF-8?q?gui:=20Linux=20tray=20quotas=20reach=20the=20ba?= =?UTF-8?q?r's=20tooltip=20(atie=20on=20Discord:=20on=20Omarchy,=20hoverin?= =?UTF-8?q?g=20magpie's=20tray=20icon=20showed=20no=20quota=20with=20?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=20=E2=80=BA=20=E7=94=A8=E9=87=8F=20=E2=80=BA?= =?UTF-8?q?=20=E8=8F=9C=E5=8D=95=E6=A0=8F=E6=98=BE=E7=A4=BA=E9=A2=9D?= =?UTF-8?q?=E5=BA=A6=20on,=20after=20opening=20the=20panel=20and=20restart?= =?UTF-8?q?ing)=20=E2=80=94=20Wails=20v3.0.0-beta.24's=20Linux=20SetToolti?= =?UTF-8?q?p=20is=20a=20"TBD"=20no-op:=20the=20StatusNotifierItem's=20Tool?= =?UTF-8?q?Tip=20stayed=20the=20{title:=20"magpie"}=20it=20was=20made=20wi?= =?UTF-8?q?th=20and=20no=20NewToolTip=20was=20ever=20sent,=20so=20every=20?= =?UTF-8?q?bar=20showed=20"magpie"=20(Omarchy's=20bar=20shows=20tooltipTit?= =?UTF-8?q?le=20before=20Title,=20waybar=20ToolTip=20before=20Title=20too,?= =?UTF-8?q?=20and=20waybar=20reads=20ToolTip=20again=20only=20on=20NewTool?= =?UTF-8?q?Tip).=20magpie=20now=20sets=20the=20ToolTip=20itself=20after=20?= =?UTF-8?q?Wails'=20SetTooltip:=20through=20the=20prop.Properties=20Wails?= =?UTF-8?q?=20exported=20(reached=20by=20reflection,=20so=20the=20(sa(iiay?= =?UTF-8?q?)ss)=20type=20it=20was=20made=20with=20is=20kept=20and=20Proper?= =?UTF-8?q?tiesChanged=20goes=20out),=20then=20sends=20org.kde.StatusNotif?= =?UTF-8?q?ierItem.NewToolTip=20from=20the=20same=20session-bus=20connecti?= =?UTF-8?q?on.=20The=20quotas=20go=20in=20the=20title,=20the=20part=20both?= =?UTF-8?q?=20Omarchy's=20bar=20and=20waybar=20show;=20waybar=20reads=20it?= =?UTF-8?q?=20as=20Pango=20markup,=20so=20&,=20<=20and=20>=20become=20look?= =?UTF-8?q?-alikes=20(sniTip)=20rather=20than=20losing=20the=20whole=20too?= =?UTF-8?q?ltip.=20Until=20the=20tray=20is=20up=20(or=20should=20a=20Wails?= =?UTF-8?q?=20update=20move=20the=20field)=20the=20loop=20tries=20again=20?= =?UTF-8?q?next=20time.=20TestSNITipIsSafeMarkup=20covers=20the=20markup.?= =?UTF-8?q?=20In=20the=20omarchy=20VM=20(Omarchy=204,=20Quickshell=20bar),?= =?UTF-8?q?=20built=20there=20with=20-tags=20"production=20gtk3",=20in=20a?= =?UTF-8?q?=20sandbox=20HOME=20with=20the=20VM's=20DeepSeek=20and=20Kimi?= =?UTF-8?q?=20balance=20keys:=20the=20released=20magpie's=20item=20kept=20?= =?UTF-8?q?ToolTip=20"magpie"=20(Title=20"=C2=A579.15=20|=20$8.83");=20thi?= =?UTF-8?q?s=20build's=20ToolTip=20is=20(sa(iiay)ss)=20"DeepSeek=20=C2=B7?= =?UTF-8?q?=20=C2=A579.15\n\nKimi=20=C2=B7=20$8.83",=20dbus-monitor=20show?= =?UTF-8?q?s=20PropertiesChanged=20then=20NewToolTip=20from=20the=20item's?= =?UTF-8?q?=20connection,=20and=20hovering=20its=20icon=20in=20Omarchy's?= =?UTF-8?q?=20bar=20shows=20both=20balances.=20Waybar=20itself=20wasn't=20?= =?UTF-8?q?run=20(the=20VM's=20Omarchy=204=20has=20none);=20what=20it=20do?= =?UTF-8?q?es=20is=20from=20its=20source=20(sni/item.cpp).=20go=20vet,=20b?= =?UTF-8?q?uilds=20(darwin,=20GOOS=3Dlinux,=20GOOS=3Dwindows)=20and=20go?= =?UTF-8?q?=20test=20-tags=20nogui=20-count=3D1=20./...=20under=20a=20temp?= =?UTF-8?q?=20HOME=20pass=20but=20TestProvidersAnswerWhileListsComeIn=20an?= =?UTF-8?q?d=20TestClaudeSignedOut,=20timing=20tests=20that=20pass=20alone?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gui/traytip_linux.go | 72 ++++++++++++++++++++++++++++++++++ internal/gui/traytip_other.go | 8 ++++ internal/gui/traytip_test.go | 12 ++++++ internal/gui/trayusage.go | 8 ++++ internal/gui/trayusage_tray.go | 4 ++ 5 files changed, 104 insertions(+) create mode 100644 internal/gui/traytip_linux.go create mode 100644 internal/gui/traytip_other.go create mode 100644 internal/gui/traytip_test.go diff --git a/internal/gui/traytip_linux.go b/internal/gui/traytip_linux.go new file mode 100644 index 00000000..70c42a55 --- /dev/null +++ b/internal/gui/traytip_linux.go @@ -0,0 +1,72 @@ +//go:build linux && !nogui + +package gui + +import ( + "log" + "reflect" + "unsafe" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/prop" + "github.com/wailsapp/wails/v3/pkg/application" +) + +// setTrayTip puts the tooltip on the tray's StatusNotifierItem. Wails +// (v3.0.0-beta.24) leaves SetTooltip on Linux a no-op ("TBD"): the item's +// ToolTip stays the "magpie" it was made with and no NewToolTip is sent, so +// a bar never had the quotas to show (atie on Discord: Omarchy's bar showed +// none on hover with 菜单栏显示额度 on). This sets the property Wails +// exported, through its own prop.Properties (which keeps the (sa(iiay)ss) +// type), and sends NewToolTip from the same connection: waybar reads ToolTip +// again only on that signal, Quickshell on it or PropertiesChanged. +func setTrayTip(tray *application.SystemTray, tip string) bool { + props := trayProps(tray) + if props == nil { + return false + } + v := struct { + V0 string // icon name + V1 []application.PX // icon pixmaps + V2 string // title: what Omarchy's bar shows, and waybar + V3 string // description: waybar only, under the title + }{V2: sniTip(tip), V1: []application.PX{}} + if err := props.Set("org.kde.StatusNotifierItem", "ToolTip", dbus.MakeVariant(v)); err != nil { + log.Println("tray: tooltip:", err) + return false + } + conn, err := dbus.SessionBus() // the connection Wails' tray is on + if err != nil { + return false + } + if err := conn.Emit("/StatusNotifierItem", "org.kde.StatusNotifierItem.NewToolTip"); err != nil { + log.Println("tray: tooltip signal:", err) + } + return true +} + +// trayProps is the item's exported properties inside Wails' Linux tray +// (SystemTray.impl.props), nil once a Wails version keeps them elsewhere. +func trayProps(tray *application.SystemTray) (props *prop.Properties) { + defer func() { + if recover() != nil { + props = nil + } + }() + if tray == nil { + return nil + } + impl := reflect.ValueOf(tray).Elem().FieldByName("impl") + if !impl.IsValid() || impl.Kind() != reflect.Interface || impl.IsNil() { + return nil + } + p := impl.Elem() + if p.Kind() != reflect.Pointer || p.IsNil() || p.Elem().Kind() != reflect.Struct { + return nil + } + f := p.Elem().FieldByName("props") + if !f.IsValid() || f.Type() != reflect.TypeOf((*prop.Properties)(nil)) { + return nil + } + return *(**prop.Properties)(unsafe.Pointer(f.UnsafeAddr())) +} diff --git a/internal/gui/traytip_other.go b/internal/gui/traytip_other.go new file mode 100644 index 00000000..5b2d2be9 --- /dev/null +++ b/internal/gui/traytip_other.go @@ -0,0 +1,8 @@ +//go:build !linux && !nogui + +package gui + +import "github.com/wailsapp/wails/v3/pkg/application" + +// setTrayTip: Wails' SetTooltip does it here. +func setTrayTip(*application.SystemTray, string) bool { return true } diff --git a/internal/gui/traytip_test.go b/internal/gui/traytip_test.go new file mode 100644 index 00000000..13e6806b --- /dev/null +++ b/internal/gui/traytip_test.go @@ -0,0 +1,12 @@ +package gui + +import "testing" + +// A Linux tray's tooltip title is Pango markup in waybar: a bare & or < +// would lose all of it. +func TestSNITipIsSafeMarkup(t *testing.T) { + got := sniTip("Claude Max 5h: 42% left\n\nR&D ") + if want := "Claude Max 5h: 42% left\n\nR&D ‹team›"; got != want { + t.Fatalf("sniTip = %q, want %q", got, want) + } +} diff --git a/internal/gui/trayusage.go b/internal/gui/trayusage.go index 20782768..ae27c601 100644 --- a/internal/gui/trayusage.go +++ b/internal/gui/trayusage.go @@ -246,6 +246,14 @@ func trayUsageView(cards []provider.SubscriptionQuota, now time.Time, left bool) return cells, strings.Join(labels, " | "), strings.Join(tips, "\n\n") } +// sniTip is the tooltip as a Linux tray item's ToolTip title carries it. +// Omarchy's bar shows the title as plain text and waybar as Pango markup, +// where a bare "&" or "<" loses the whole tooltip; their look-alikes read +// the same in both. +func sniTip(tip string) string { + return strings.NewReplacer("&", "&", "<", "‹", ">", "›").Replace(tip) +} + // trayRow is a cell's row cut to fit the menu bar: a balance is a few // figures ("¥12345.67"), but one read from a plan's own page can be any // text, and the whole of it is in the tooltip. diff --git a/internal/gui/trayusage_tray.go b/internal/gui/trayusage_tray.go index 2fd13279..64a3eea0 100644 --- a/internal/gui/trayusage_tray.go +++ b/internal/gui/trayusage_tray.go @@ -73,6 +73,10 @@ func (h *host) watchTrayUsage() { h.tray.SetLabel(label) } h.tray.SetTooltip(tip) + // Wails' is a no-op on Linux; until the tray is up, again next time + if !setTrayTip(h.tray, tip) { + shown = "" + } } // Reapply even unchanged cells: a system menu-bar rebuild can // replace the composed image with Wails' bird icon.