Skip to content

Fix status width oscillation at fractional scales - #92

Open
guzzo wants to merge 1 commit into
HANCORE-linux:mainfrom
guzzo:fix/status-spacing-savings-cycle
Open

guzzo wants to merge 1 commit into
HANCORE-linux:mainfrom
guzzo:fix/status-spacing-savings-cycle

Conversation

@guzzo

@guzzo guzzo commented Oct 29, 2026 •

Copy link
Copy Markdown

Summary

Affected plugin: hancore.shibumi.status

In the Notch shell at 1.25x scale, the whole bar shakes horizontally by 1–3 px every frame. The bell shakes the most. Quickshell sits at ~100% CPU, and restarting the shell doesn't help. At 1.6x the same layout settles and looks fine.

Cause

spacingSavings (G3's interior ink contraction) feeds back into its own input:

  1. updateShift and bellShift come from inkEdge(), which uses statusPlacement.origin.x, the widget's absolute window position. The glyph ink they measure is also snapped to absolute device pixels (BarGlyph.symbolCanvas uses snapX).
  2. spacingSavings is subtracted from contentWidth, so it changes G3's implicitWidth. It also reaches the bar's spacingRecovery.
  3. Fit, Dock and Notch shells are content-sized and centered, so a width change moves shellX and every widget in it, G3 included.
  4. At 1.25x that move puts the ink edges on a different device-pixel phase, so the snapped savings flip back to the previous value. Qt.callLater only defers each flip by one frame, so the cycle never ends.

Observed with shell debugBarGeometry: G3's width alternated 89 ↔ 86 and the left budget alternated 530.36 ↔ 527.96, which shifted every group after it. With temporary logging, spacingSavings stepped through 2.4 → 5.6 → 3.2 → 4.8 → … before the fix.

Fix

syncSpacingSavings() now remembers the value it last left and when it left it. If the new value is a return to that value within 500 ms, the sync is skipped, so the reservation settles on its current value. A change to a different value still applies immediately, as does a return after 500 ms. This uses no Timer, so the "exactly one lifecycle timer" contract still holds. The worst case is a reservation one snapped device pixel off from the painted shifts until the next real layout change.

This breaks the cycle without changing the device-pixel snapping. A deeper fix would make the reservation independent of absolute position. I left that out because it touches the shared BarInk/BarGlyph placement model.

Also adds a source guard to tests/status-plugin-regression.sh.

Checks

  • Live, Omarchy 4.0.6-1, Quickshell 0.3.2 (2b4501a), Hyprland v0.56.2, Shibumi 0.1.1-beta.16.5.1, single 2560x1440 output at 1.25x, Top, Notch shell, G3 in Full mode with update + notification presented:
    • Before: consecutive bar captures alternated between two states (~1300 px differing). Geometry flipped on most debugBarGeometry samples, and Quickshell ran at ~100% CPU.
    • After (omarchy restart shell): savings settle after a few adjustments. 20/20 debugBarGeometry samples were identical, frames only differ where telemetry values update, and Quickshell is at ~1% CPU.
    • A 3840x2160 output at 1.6x still renders correctly.
  • OMARCHY_PATH=/usr/share/omarchy ./tests/status-plugin-regression.sh: passed, including the new guard. The guard fails against the original file.
  • OMARCHY_PATH=/usr/share/omarchy ./tests/contract-regression.sh: ended with Shibumi complete contract regression passed (Omarchy baseline installed-package-v4.0.6). Three steps were made non-fatal in a local, uncommitted run so the rest could execute:
    • test_package_release.py › test_lifecycle_contract_pins_exact_release_identities, and test_shibumi_suite.py › test_current_package_rejects_byte_and_mode_drift / test_exact_current_package_identity_is_admitted. These fail by design for any unreleased plugin change: the only difference is the hancore.shibumi.status payload digest against the pinned public-beta.16.5.1 identity. They pass on clean main. I didn't touch the release pins, since a new identity is a maintainer release step.
    • network-plugin-regression.sh › network-qr-encoder-regression.py. This is environmental: the test hard-codes /usr/bin/node, and Node on this machine is installed via mise. The rest of network-plugin-regression.sh passed with that step made non-fatal.
    • Every other step passed.

Remaining gate

Repeat on the isolated validation system at 1.25x with Fit, Dock and Notch. Check that G3 settles with tray and drawer items present and absent, and that the update→bell gap still meets the 14 px ink target to within one device pixel.

🤖 Generated with Claude Code

In compact shells (Fit/Dock/Notch) the bar is centered and sized by its
content, so the status widget moves whenever its own width changes.
spacingSavings is derived from ink edges snapped to absolute device
pixels, so at 1.25x that move flips the snapped value back, and
Qt.callLater only defers each flip by one frame. The whole bar then
resizes by 1-3 px every frame and Quickshell pins a CPU core.

Ignore a return to the value just left within 500 ms so the reservation
settles. Changes to new values still apply immediately.

Co-Authored-By: Claude Opus 5.5 <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