Skip to content

Fix MenuItem.AddChild not making children visible to the desktop environment#2

Open
alex-poor wants to merge 3 commits intoDeedleFake:masterfrom
alex-poor:fix/menuitem-addchild-emit
Open

Fix MenuItem.AddChild not making children visible to the desktop environment#2
alex-poor wants to merge 3 commits intoDeedleFake:masterfrom
alex-poor:fix/menuitem-addchild-emit

Conversation

@alex-poor
Copy link
Copy Markdown

Summary

Two related bugs in MenuItem.AddChild cause newly added submenu children to never become visible to the desktop environment.

Bug 1: MenuItem.AddChild calls emitPropertiesUpdated on the parent

In menuitem.go:78, the dirty property names returned by `child.applyProps` are passed to `item.emitPropertiesUpdated`, where `item` is the parent, not the child:

```go
dirty, errs := child.applyProps(props)
errs = append(errs, item.menu.updateLayout(item))
errs = append(errs, item.emitPropertiesUpdated(dirty)) // wrong receiver
```

Inside `emitPropertiesUpdated`, this looks up child property names (`"label"`, `"enabled"`, etc.) in the parent's `props` map, where they don't exist:

```go
for change := range props {
updated = append(updated, prop{
Name: change,
Value: item.props[change], // nil for child prop names
})
}
```

The resulting D-Bus emit fails with `"runtime error: invalid memory address or nil pointer dereference"`. `Menu.AddChild` already does this correctly by calling `child.emitPropertiesUpdated(dirty)`.

Bug 2: `setChildren` mutates `children-display` without emitting it

`MenuItem.setChildren` sets `item.props["children-display"] = "submenu"` when the item gains its first child, but never marks this property as dirty or emits a `PropertiesUpdated` signal for it. As a result, the desktop environment is never told that the item has been converted into a submenu, so children added via `MenuItem.AddChild` remain invisible — even after Bug 1 is fixed.

`setChildren` now reports the names of any properties it changed, and each caller (`MenuItem.AddChild`, `Remove`, `appendChild`, `MoveBefore`) emits `PropertiesUpdated` for the affected node when needed.

Test plan

  • `go build ./...` and `go vet ./...` pass
  • Manually verified with the `examples/simple` example: "Edit" submenu now renders "Add" and "Remove" children correctly
  • Manually verified end-to-end in trayscale: a multi-item submenu added in `Start()` is now visible in the desktop environment's tray menu (KDE Plasma 6 on Wayland)

🤖 Generated with Claude Code

alex-poor and others added 3 commits April 7, 2026 11:46
…child

In MenuItem.AddChild, the dirty property names returned by
child.applyProps were being passed to item.emitPropertiesUpdated,
where item is the parent. This caused emitPropertiesUpdated to look
up the child's property names ("label", "enabled", etc.) in the
parent's props map, where they don't exist, and emit nil values over
D-Bus, triggering a nil pointer dereference during marshaling.

The result: any error from AddChild on a MenuItem propagates a
"runtime error: invalid memory address or nil pointer dereference",
and the desktop environment never receives the new child item's
property notifications.

Menu.AddChild already does this correctly by calling
child.emitPropertiesUpdated(dirty) on the child.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…bmenu

When a MenuItem gains its first child or loses its last, setChildren
mutates the item's children-display property in memory but never
emits a PropertiesUpdated signal for it. As a result, the desktop
environment is never told that a regular menu item has been converted
into a submenu (or vice-versa), so the children remain invisible.

This was previously somewhat masked by the bug fixed in the previous
commit, since item.emitPropertiesUpdated was being called on the
parent and accidentally re-emitted the parent's id, which some
implementations may have used as a hint to refetch. With that bug
fixed, the missing children-display emit becomes the only thing
preventing newly added children from showing.

setChildren now reports the names of any properties it changed, and
each caller (MenuItem.AddChild, Remove, appendChild, MoveBefore)
emits PropertiesUpdated for the affected node when needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
….GetLayout

MenuItem.SetProps acquired item.m before menu.m, while dbusmenu's
GetLayout (called by the desktop environment over D-Bus) acquires
menu.m before iterating and RLock'ing each item.m. With concurrent
SetProps from one goroutine and GetLayout from another, the two
holders deadlock waiting for each other's lock.

In practice this never showed up for the existing few status-bar
SetProps calls, because they happen during a brief synchronous burst
in Tray.update under a higher-level mutex. But any caller that runs
SetProps from a separate goroutine while the DE is actively fetching
the layout (which it does any time the menu is opened or after a
LayoutUpdated signal) hits the deadlock reliably.

Reorder SetProps to take menu.m first, then item.m, matching the
lock order GetLayout uses. The parent lookup that previously called
getParent (which would itself try to RLock menu.m) is inlined since
we already hold menu.m at that point.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@alex-poor
Copy link
Copy Markdown
Author

Pushed a third commit that fixes a related lock-order inversion in `MenuItem.SetProps`.

`SetProps` previously locked `item.m` before `menu.m` (line 379 then line 382). Meanwhile `dbusmenu.GetLayout` (the D-Bus method the desktop environment calls to fetch the menu) takes `menu.m` first and then `RLock`s each `item.m` as it walks the tree. With concurrent `SetProps` and `GetLayout`, the two paths deadlock waiting for each other.

In practice this hadn't shown up because `Tray.update` in trayscale runs all of its `SetProps` calls in a tight burst from a single goroutine under a higher-level mutex, and the DE rarely tries to `GetLayout` during that burst. The moment a caller invokes `SetProps` from a separate goroutine while the DE is fetching the layout (which it does on every menu open and after every `LayoutUpdated` signal), the deadlock is reliable.

The fix swaps the lock order in `SetProps` to match `GetLayout` and inlines the parent lookup that used to call `getParent` (which itself `RLock`s `menu.m` and would deadlock under the new order).

alex-poor added a commit to alex-poor/trayscale that referenced this pull request Apr 7, 2026
Allow switching Tailscale profiles directly from the system tray
right-click menu, without needing to open the main window. The active
profile is shown with a bullet indicator that updates whether the
switch is initiated from the tray submenu or from the main window's
profile dropdown.

Depends on DeedleFake/tray#2, which fixes three bugs in the tray
library that were preventing submenu children from rendering and
causing a deadlock when SetProps is called concurrently with the
desktop environment's GetLayout requests. The temporary replace
directive in go.mod should be removed once that PR is merged and
deedles.dev/tray is updated to a new pseudo-version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant