Files
magpie/internal/gui/traytip_linux.go
T
yetone bd45fa452a 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.
2026-10-03 20:57:39 +08:00

73 lines
2.3 KiB
Go

//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()))
}