Enhance portal signal handling: implement robust ID extraction and matching for Activated/Deactivated signals

This commit is contained in:
John Doe
2026-07-14 16:06:49 -07:00
parent ed6d00c730
commit cf2f73cd98
+77 -7
View File
@@ -381,6 +381,75 @@ func (hk *Hotkey) safeSendKeyup() {
hk.sendPortalEvent("keyup", hk.keyupIn) hk.sendPortalEvent("keyup", hk.keyupIn)
} }
// portalExtractIDs walks an Activated/Deactivated signal body argument and
// returns the shortcut IDs it references. The XDG GlobalShortcuts spec
// defines Activated/Deactivated with a single argument of type a(su):
// an array of (string id, uint state) structs. We are defensive about
// variant wrapping and about older drafts that may send a single struct
// (su) or even just a bare string id.
func portalExtractIDs(bodyArg interface{}) []string {
val := unwrapVariant(bodyArg)
switch v := val.(type) {
case []interface{}:
var ids []string
for _, item := range v {
ids = append(ids, portalExtractIDs(item)...)
}
return ids
case [][]interface{}:
// Each element is a (id, state) struct serialized as []interface{}.
var ids []string
for _, tuple := range v {
if arr, ok := tuple.([]interface{}); ok && len(arr) > 0 {
if id, ok := unwrapVariant(arr[0]).(string); ok {
ids = append(ids, id)
}
}
}
return ids
case []map[string]interface{}:
var ids []string
for _, m := range v {
if s, ok := m["id"].(string); ok {
ids = append(ids, s)
}
}
return ids
case map[string]interface{}:
if s, ok := v["id"].(string); ok {
return []string{s}
}
return nil
case []dbus.Struct:
// golang.org/x/dbus may decode (su) as dbus.Struct.
var ids []string
for _, s := range v {
if id, ok := unwrapVariant(s).(string); ok {
ids = append(ids, id)
}
}
return ids
case string:
return []string{v}
default:
return nil
}
}
// portalSignalMatchesID reports whether any of the signal's body arguments
// reference the given shortcut ID. It scans every body argument because the
// exact argument index varies across portal implementations.
func portalSignalMatchesID(body []interface{}, wantID string) bool {
for _, arg := range body {
for _, id := range portalExtractIDs(arg) {
if id == wantID {
return true
}
}
}
return false
}
func (hk *Hotkey) portalSignalLoop() { func (hk *Hotkey) portalSignalLoop() {
defer close(hk.portalDone) defer close(hk.portalDone)
@@ -394,7 +463,7 @@ func (hk *Hotkey) portalSignalLoop() {
ch := make(chan *dbus.Signal, 32) ch := make(chan *dbus.Signal, 32)
conn.Signal(ch) conn.Signal(ch)
defer conn.RemoveSignal(ch) defer conn.RemoveSignal(ch)
rule := fmt.Sprintf( rule := fmt.Sprintf(
"type='signal',sender='%s',interface='%s'", "type='signal',sender='%s',interface='%s'",
portalBusName, ifaceGlobalShortcuts, portalBusName, ifaceGlobalShortcuts,
@@ -415,15 +484,16 @@ func (hk *Hotkey) portalSignalLoop() {
if !ok || sig == nil { if !ok || sig == nil {
return return
} }
if len(sig.Body) < 2 { if len(sig.Body) < 1 {
continue continue
} }
// Bypass strict session path checking to avoid mismatch bugs. // The XDG GlobalShortcuts Activated/Deactivated signals carry a
// The shortcut ID is unique to our application. // single argument of type a(su): an array of (id, state) structs.
rawID := unwrapVariant(sig.Body[1]) // We match on our unique shortcut ID rather than on a specific
id, ok := rawID.(string) // body index, which is what the old code got wrong (it expected
if !ok || id != wisfreeGlobalShortcutID { // sig.Body[1] to be a string and silently dropped every signal).
if !portalSignalMatchesID(sig.Body, wisfreeGlobalShortcutID) {
continue continue
} }