gui: Linux tray quotas reach the bar's tooltip (atie on Discord: on Omarchy, hovering magpie's tray icon showed no quota with 设置 › 用量 › 菜单栏显示额度 on, after opening the panel and restarting) — Wails v3.0.0-beta.24's Linux SetTooltip is a "TBD" no-op: the StatusNotifierItem's ToolTip stayed the {title: "magpie"} it was made with and no NewToolTip was ever sent, so every bar showed "magpie" (Omarchy's bar shows tooltipTitle before Title, waybar ToolTip before Title too, and waybar reads ToolTip again only on NewToolTip). magpie now sets the ToolTip itself after Wails' SetTooltip: through the prop.Properties Wails exported (reached by reflection, so the (sa(iiay)ss) type it was made with is kept and PropertiesChanged goes out), then sends org.kde.StatusNotifierItem.NewToolTip from the same session-bus connection. The quotas go in the title, the part both Omarchy's bar and waybar show; waybar reads it as Pango markup, so &, < and > become look-alikes (sniTip) rather than losing the whole tooltip. Until the tray is up (or should a Wails update move the field) the loop tries again next time. TestSNITipIsSafeMarkup covers the markup. In the omarchy VM (Omarchy 4, Quickshell bar), built there with -tags "production gtk3", in a sandbox HOME with the VM's DeepSeek and Kimi balance keys: the released magpie's item kept ToolTip "magpie" (Title "¥79.15 | $8.83"); this build's ToolTip is (sa(iiay)ss) "DeepSeek · ¥79.15\n\nKimi · $8.83", dbus-monitor shows PropertiesChanged then NewToolTip from the item's connection, and hovering its icon in Omarchy's bar shows both balances. Waybar itself wasn't run (the VM's Omarchy 4 has none); what it does is from its source (sni/item.cpp). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut, timing tests that pass alone.

This commit is contained in:
yetone committed 2026-10-03 20:57:39 +08:00
1 parent 58edc00bba
commit bd45fa452a
5 files changed
+104

No files matched your search

+72
View File
@@ -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()))
}
+8
View File
@@ -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 }
+12
View File
@@ -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 <team>")
if want := "Claude Max 5h: 42% left\n\nR&D ‹team›"; got != want {
t.Fatalf("sniTip = %q, want %q", got, want)
}
}
+8
View File
@@ -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.
+4
View File
@@ -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.