Skip to content

Instantly share code, notes, and snippets.

@yuters
Created July 28, 2026 02:37
Show Gist options
  • Select an option

  • Save yuters/cdc10ee6fdab4c567674dc6c4a1d2d97 to your computer and use it in GitHub Desktop.

Select an option

Save yuters/cdc10ee6fdab4c567674dc6c4a1d2d97 to your computer and use it in GitHub Desktop.
Fix XWayland issues on Hyprland with the block/buzz AppImage (font rendering and animations)
#!/usr/bin/env bash
#
# Launch Buzz on this machine (Arch, Hyprland, NVIDIA proprietary).
#
# Runs the real .AppImage, not an unpacked copy: the AppImage runtime is what sets
# APPIMAGE=, and Tauri's updater refuses to self-update without it ("switch to AppImage
# for automatic updates"). The updater rewrites that file in place, so its name must stay
# version-less — never rename it to Buzz_<version>.AppImage.
#
# Everything below is a workaround for a specific, reproduced failure. None of it is
# decorative; each line has a symptom attached.
#
set -euo pipefail
# Resolve everything relative to this script, so the whole directory can be moved or
# renamed without editing paths. realpath resolves the ~/.local/bin symlink that the
# desktop entry actually invokes.
readonly buzz_root="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
readonly buzz_appimage="${buzz_root}/Buzz.AppImage"
if [[ ! -x "${buzz_appimage}" ]]; then
printf 'Buzz AppImage is missing or not executable: %s\n' "${buzz_appimage}" >&2
exit 1
fi
# Ask the dynamic linker where a library actually lives, rather than assuming /usr/lib
# (wrong on Debian-style multiarch, and on any distro that moves libdir).
resolve_lib() {
local soname="$1" path
path=$(ldconfig -p 2>/dev/null | awk -v s="$soname" '$1 == s { print $NF; exit }')
if [[ -z "${path}" || ! -e "${path}" ]]; then
printf 'required system library not found: %s\n' "${soname}" >&2
exit 1
fi
printf '%s' "${path}"
}
# X11/XWayland, so Hyprland draws server-side decorations. On the Wayland backend GTK3
# draws its own titlebar and GTK_CSD=0 does not suppress it. AppRun's
# linuxdeploy-plugin-gtk.sh hook sets the same value, so the two agree.
export GDK_BACKEND="x11"
# Hyprland runs no XSETTINGS manager, so on the X11 backend GDK ignores
# ~/.config/gtk-3.0/settings.ini for gtk-xft-* keys and falls back to its built-in
# hintstyle=hintmedium / rgba=rgb. hintmedium snaps glyph advances to whole pixels, which
# is what produces ragged inter-letter spacing ("Mentio ned", "unle ss") in WebKit.
#
# XENVIRONMENT scopes the corrected values to this process only, so no other X client is
# affected — unlike a global `xrdb -merge`. GDK reads the file once, when the X connection
# opens, so writing it here (before exec) is early enough. It lives in XDG_RUNTIME_DIR so
# it is regenerated every launch and cleaned up at logout; nothing to lose or maintain.
#
# Beware: X silently ignores an XENVIRONMENT pointing at a missing file, and the only
# symptom is the ragged text above — which reads as a font bug and sends you chasing
# fonts and CSS kerning rather than the backend. Hence the write is checked.
readonly xft_resources="${XDG_RUNTIME_DIR:-/tmp}/buzz-xft-resources.ad"
if ! cat > "${xft_resources}" <<'XFT'
! Generated by buzz-desktop-launcher — edits here are overwritten on next launch.
Xft.antialias: 1
Xft.hinting: 1
Xft.hintstyle: hintslight
Xft.rgba: none
XFT
then
printf 'could not write Xft resources: %s\n' "${xft_resources}" >&2
exit 1
fi
export XENVIRONMENT="${xft_resources}"
# Keep WebKit's DMABUF renderer but move buffers through shared memory. GBM allocation
# fails on this card ("Failed to create GBM buffer of size 3840x2160: Invalid argument").
# Measured on a blurred-gradient workload: FORCE_SHM ~20 paints/s (at the sampling
# ceiling), DISABLE_DMABUF_RENDERER ~6, plain GBM never paints at all.
#
# Do NOT swap in WEBKIT_DMABUF_RENDERER_DISABLE_GBM=1. It benchmarks well and renders a
# completely blank window.
export WEBKIT_DMABUF_RENDERER_FORCE_SHM="1"
# The bundled GStreamer plugins alone lack appsink/appsrc/autoaudiosink, which breaks
# audio. GST_PLUGIN_PATH_1_0 is the one that actually carries the system dir through:
# AppRun overwrites GST_PLUGIN_SYSTEM_PATH_1_0 with its own AppDir path and drops our
# value. Set both anyway — the SYSTEM_PATH one is inert here but costs nothing, and
# stops this looking like an oversight next time someone reads it.
if system_gstreamer_plugins=$(pkg-config --variable=pluginsdir gstreamer-1.0 2>/dev/null) \
&& [[ -d "${system_gstreamer_plugins}" ]]; then
export GST_PLUGIN_SYSTEM_PATH_1_0="${system_gstreamer_plugins}"
export GST_PLUGIN_PATH_1_0="${system_gstreamer_plugins}"
fi
# LD_PRELOAD carries two fixes. It is the only lever that still works, because
# AppRun.wrapped unconditionally PREPENDS the AppDir lib dirs to LD_LIBRARY_PATH — so
# setting LD_LIBRARY_PATH here would always lose.
#
# libsystemd Required to start at all. The bundle ships an older libsystemd than
# the system libmount.so.1 (not bundled, loaded from the system libdir)
# needs: libsystemd.so.0: version `LIBSYSTEMD_251' not found.
#
# libwebkit2gtk Fixes the stuttering splash animation. The bundled WebKit janks;
# libjavascriptcore the system one is smooth. Both must be preloaded together — they
# are a matched pair.
#
# Appends any caller-supplied LD_PRELOAD instead of clobbering it, so a one-off preload
# can be injected for an experiment without editing this file.
preload=""
for soname in libsystemd.so.0 libwebkit2gtk-4.1.so.0 libjavascriptcoregtk-4.1.so.0; do
preload="${preload}${preload:+:}$(resolve_lib "${soname}")"
done
export LD_PRELOAD="${preload}${LD_PRELOAD:+:$LD_PRELOAD}"
exec "${buzz_appimage}" "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment