Summary
When items are inserted or removed above or below a menu item with a large image= parameter, the incremental diff/patch introduced in b712f7a misaligns item indices. This causes adjacent item text to be rendered as the attributedTitle of the image's NSMenuItem, appearing to the right of the image. The ghost text is interactive (selectable/hoverable) and forces the menu to be wider than intended.
Steps to Reproduce
- Save the attached repro script as a SwiftBar plugin (e.g.,
image-patch-bug.3s.sh)
- Open the plugin's menu and keep it open
- Wait for a few 3-second refresh cycles
The script outputs a large noisy image (~300KB base64) that changes each refresh, with lines that appear/disappear above and below the image on alternating refreshes.
Expected
All text items should render below (or above) the image, never beside it. The menu width should be determined by the widest text line or the image width.
Actual
After a few incremental updates, text from neighboring menu items appears to the right of the image on the same row. The text is interactive (hoverable/selectable), confirming it is the attributedTitle of the image's NSMenuItem. This widens the menu significantly.
Screenshots
Cause
The positional diff in diffMenuNodes() (MenuDiff.swift) compares items by index. When items are inserted or removed, all items after the change point shift indices. The diff treats these as "updates" and patches old NSMenuItems with the wrong new content — e.g., patching an image item with a text item's parameters or vice versa. Since patchMenuItem() sets both attributedTitle and image, the image item can end up with a non-empty title (rendered to the right of the image) or a text item can inherit a stale image.
Introduced in b712f7a ("Reuse menu items instead of recreating them").
Repro Script
#!/bin/bash
# Bug repro: incremental update renders adjacent item text to the RIGHT of an image
# when items above/below the image are inserted or removed between refreshes.
#
# Steps: open this menu and keep it open. Within a few refreshes, text from
# neighboring items appears beside the image instead of below it.
COUNT_FILE="/tmp/swiftbar_patch_bug_count"
COUNT=$(cat "$COUNT_FILE" 2>/dev/null || echo 0)
COUNT=$((COUNT + 1))
echo "$COUNT" > "$COUNT_FILE"
# Large noisy image (~300KB base64) that changes every refresh
IMG=$(/usr/bin/python3 -c "
import base64, struct, zlib, random
random.seed($COUNT)
w, h = 750, 450
def chunk(ctype, data):
c = ctype + data
return struct.pack('>I', len(data)) + c + struct.pack('>I', zlib.crc32(c) & 0xffffffff)
raw = b''
for y in range(h):
raw += b'\x00'
for x in range(w):
raw += bytes([random.randint(0,255), random.randint(0,255), random.randint(0,255)])
sig = b'\x89PNG\r\n\x1a\n'
ihdr = chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0))
idat = chunk(b'IDAT', zlib.compress(raw, 1))
iend = chunk(b'IEND', b'')
print(base64.b64encode(sig + ihdr + idat + iend).decode())
")
echo "Image patch bug (refresh #\${COUNT})"
echo "---"
echo "Header line (\${COUNT})"
echo "Static line above image"
# Lines that appear/disappear above the image
if (( COUNT % 2 == 0 )); then
echo "Optional line A (even refreshes only)"
fi
if (( COUNT % 3 == 0 )); then
echo "Optional line B (every 3rd refresh)"
fi
echo "| image=\${IMG}"
# Lines that appear/disappear below the image
if (( COUNT % 2 == 0 )); then
echo "Short text below"
else
echo "Much longer text below the image that varies in width between refreshes to stress layout"
fi
echo "Submenu section"
echo "--Child item \${COUNT}"
if (( COUNT % 3 != 0 )); then
echo "--Extra child (removed every 3rd refresh)"
fi
echo "---"
echo "Footer (refresh #\${COUNT})"
Summary
When items are inserted or removed above or below a menu item with a large
image=parameter, the incremental diff/patch introduced in b712f7a misaligns item indices. This causes adjacent item text to be rendered as theattributedTitleof the image'sNSMenuItem, appearing to the right of the image. The ghost text is interactive (selectable/hoverable) and forces the menu to be wider than intended.Steps to Reproduce
image-patch-bug.3s.sh)The script outputs a large noisy image (~300KB base64) that changes each refresh, with lines that appear/disappear above and below the image on alternating refreshes.
Expected
All text items should render below (or above) the image, never beside it. The menu width should be determined by the widest text line or the image width.
Actual
After a few incremental updates, text from neighboring menu items appears to the right of the image on the same row. The text is interactive (hoverable/selectable), confirming it is the
attributedTitleof the image'sNSMenuItem. This widens the menu significantly.Screenshots
Cause
The positional diff in
diffMenuNodes()(MenuDiff.swift) compares items by index. When items are inserted or removed, all items after the change point shift indices. The diff treats these as "updates" and patches oldNSMenuItems with the wrong new content — e.g., patching an image item with a text item's parameters or vice versa. SincepatchMenuItem()sets bothattributedTitleandimage, the image item can end up with a non-empty title (rendered to the right of the image) or a text item can inherit a stale image.Introduced in b712f7a ("Reuse menu items instead of recreating them").
Repro Script