Created
January 25, 2026 10:37
-
-
Save felipealfonsog/1f0ba48bdbcbf585f1356bf040b069fb to your computer and use it in GitHub Desktop.
A minimal, manual healthcheck and stabilization tool for Chromium/Electron Wayland issues on Arch KDE.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # wayland-ozone-doctor - v 2 | |
| # Manual healthcheck + repair for Chromium/Electron Wayland resize/compositor issues (Arch/KDE). | |
| # Commands: | |
| # - check : short headless Wayland probe (colors + spinner) | |
| # - fix : apply max-stability configs (force X11) + write per-app flags files (best-effort) | |
| # - fix-wrap : create per-app wrappers in ~/.local/bin (strongest per-app enforcement) | |
| # - install : self-install to ~/.local/bin/wayland-ozone-doctor | |
| # - status : show detected apps + config presence | |
| # - readme : operating guide / FAQ (English) | |
| VERSION="1.1.0" | |
| # ---------------------------- | |
| # UI helpers (colors + spinner) | |
| # ---------------------------- | |
| if [[ -t 1 ]]; then | |
| RED=$'\033[31m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m'; BLUE=$'\033[34m' | |
| BOLD=$'\033[1m'; DIM=$'\033[2m'; RESET=$'\033[0m' | |
| else | |
| RED=""; GREEN=""; YELLOW=""; BLUE=""; BOLD=""; DIM=""; RESET="" | |
| fi | |
| spinner() { | |
| local pid="$1" | |
| local msg="$2" | |
| local spin='|/-\' | |
| local i=0 | |
| printf "%s%s%s " "${BLUE}${BOLD}" "$msg" "${RESET}" | |
| while kill -0 "$pid" 2>/dev/null; do | |
| printf "\b%s" "${spin:i++%${#spin}:1}" | |
| sleep 0.08 | |
| done | |
| printf "\b" | |
| } | |
| ok() { printf "%s✔%s %s\n" "${GREEN}${BOLD}" "${RESET}" "$1"; } | |
| warn() { printf "%s!%s %s\n" "${YELLOW}${BOLD}" "${RESET}" "$1"; } | |
| bad() { printf "%s✘%s %s\n" "${RED}${BOLD}" "${RESET}" "$1"; } | |
| info() { printf "%s%s%s\n" "${DIM}" "$1" "${RESET}"; } | |
| die() { bad "$1"; exit "${2:-1}"; } | |
| # ---------------------------- | |
| # Paths / Config | |
| # ---------------------------- | |
| SELF_PATH="$(readlink -f "${0}")" | |
| INSTALL_DIR="${HOME}/.local/bin" | |
| INSTALL_PATH="${INSTALL_DIR}/wayland-ozone-doctor" | |
| ENV_DIR="${HOME}/.config/environment.d" | |
| ENV_FILE="${ENV_DIR}/99-stable-x11.conf" | |
| TIMEOUT_S="${TIMEOUT_S:-6}" | |
| LOG_LINES="${LOG_LINES:-10}" | |
| # Error patterns that strongly correlate with "Wayland path still unhealthy" | |
| BAD_RE='GLib: g_main_context_pop_thread_default|SharedImageManager::ProduceSkia|wl_display|Wayland.*(error|failed)|ozone.*wayland.*(error|failed)|XDG_RUNTIME_DIR.*(invalid|not set)' | |
| # Candidates for engine-layer probe | |
| CANDIDATES=( "chromium" "google-chrome-stable" ) | |
| # Flags: stable posture for your exact issue | |
| FLAGS_X11_STABLE=$'--ozone-platform=x11\n--disable-gpu\n' | |
| # "Known" per-app flags filenames (best effort) | |
| declare -A APP_FLAGS_FILES=( | |
| ["google-chrome-stable"]="${HOME}/.config/chrome-flags.conf" | |
| ["chromium"]="${HOME}/.config/chromium-flags.conf" | |
| ["electron"]="${HOME}/.config/electron-flags.conf" | |
| ["code"]="${HOME}/.config/code-flags.conf" | |
| ["codium"]="${HOME}/.config/codium-flags.conf" | |
| ) | |
| # Common Electron apps (best-effort detection) | |
| # Add/remove as you wish; wrappers are created only if the binary exists. | |
| ELECTRON_APPS=( | |
| slack | |
| discord | |
| signal-desktop | |
| obsidian | |
| teams | |
| telegram-desktop | |
| spotify | |
| code | |
| codium | |
| ) | |
| # ---------------------------- | |
| # Helpers | |
| # ---------------------------- | |
| have_cmd() { command -v "$1" >/dev/null 2>&1; } | |
| write_file_atomic() { | |
| local path="$1" | |
| local content="$2" | |
| local tmp | |
| tmp="$(mktemp)" | |
| printf "%s" "$content" >"$tmp" | |
| mkdir -p "$(dirname "$path")" | |
| if [[ -f "$path" ]]; then | |
| cp -a "$path" "${path}.bak.$(date +%F_%H%M%S)" || true | |
| fi | |
| mv -f "$tmp" "$path" | |
| } | |
| detect_apps() { | |
| # Prints detected relevant apps (one per line) | |
| local found=() | |
| for k in "${!APP_FLAGS_FILES[@]}"; do | |
| if [[ "$k" == "electron" ]]; then | |
| continue | |
| fi | |
| if have_cmd "$k"; then | |
| found+=("$k") | |
| fi | |
| done | |
| # If any known Electron app exists, include logical target "electron" for electron-flags.conf. | |
| local electronish=0 | |
| for x in "${ELECTRON_APPS[@]}"; do | |
| if have_cmd "$x"; then | |
| electronish=1 | |
| break | |
| fi | |
| done | |
| if [[ "$electronish" -eq 1 ]]; then | |
| found+=("electron") | |
| fi | |
| printf "%s\n" "${found[@]:-}" | |
| } | |
| real_path_for_cmd() { | |
| local c="$1" | |
| # prefer /usr/bin if present; fallback to PATH resolution | |
| if [[ -x "/usr/bin/$c" ]]; then | |
| printf "%s" "/usr/bin/$c" | |
| else | |
| command -v "$c" 2>/dev/null || true | |
| fi | |
| } | |
| # ---------------------------- | |
| # README / FAQ (built in) | |
| # ---------------------------- | |
| show_readme() { | |
| cat <<'EOF' | |
| Wayland Ozone Doctor (Arch/KDE) — Operating Guide | |
| ================================================= | |
| What this is | |
| ------------ | |
| A minimal, manual healthcheck + mitigation tool for Chromium/Electron Wayland issues | |
| (e.g., auto-resizing windows, broken settings UI, compositor/rendering glitches). | |
| It provides: | |
| - check : short headless Wayland probe for Chromium engines (cheap smoke test) | |
| - fix : maximum-stability posture (force X11) via environment.d + per-app flags files | |
| - fix-wrap : strongest per-app enforcement via wrappers in ~/.local/bin | |
| - status : operational visibility (what is installed / configured) | |
| Why this exists | |
| --------------- | |
| Wayland + compositor + GPU + Chromium/Electron can misbehave in rolling environments. | |
| This tool prioritizes deterministic stability. You decide when/if to experiment with Wayland again. | |
| Recommended workflow | |
| -------------------- | |
| 1) Apply stable fix (once): | |
| wayland-ozone-doctor fix | |
| Then LOG OUT fully and log back in (required). | |
| 2) If some Electron apps still misbehave depending on how they are launched: | |
| wayland-ozone-doctor fix-wrap | |
| This creates wrappers in ~/.local/bin that force X11 for those apps. | |
| 3) When you want to reassess Wayland viability: | |
| wayland-ozone-doctor check | |
| If "clean", you may experiment later (one app at a time). | |
| What "fix" changes | |
| ------------------ | |
| - Creates/updates: ~/.config/environment.d/99-stable-x11.conf | |
| Forces X11 for Chromium/Electron/GTK/Qt: | |
| OZONE_PLATFORM=x11 | |
| ELECTRON_OZONE_PLATFORM_HINT=x11 | |
| GDK_BACKEND=x11 | |
| QT_QPA_PLATFORM=xcb | |
| - Writes per-app flags files when relevant: | |
| ~/.config/chrome-flags.conf | |
| ~/.config/chromium-flags.conf | |
| ~/.config/electron-flags.conf | |
| ~/.config/code-flags.conf | |
| ~/.config/codium-flags.conf | |
| What "fix-wrap" changes | |
| ----------------------- | |
| - Creates wrappers in ~/.local/bin for common Electron apps (only if installed). | |
| - Wrappers force the same X11 environment and exec the real binary in /usr/bin/... | |
| Operational notes | |
| ----------------- | |
| - environment.d changes require full logout/login. | |
| - Wrappers work when PATH prioritizes ~/.local/bin before /usr/bin. | |
| - If a .desktop explicitly calls /usr/bin/app, wrappers may not be used. In that case, | |
| clone and patch the desktop entry in ~/.local/share/applications (optional). | |
| Limitations | |
| ----------- | |
| - There is no universal, deterministic "Wayland is fixed" signal for every Electron app. | |
| - 'check' validates the engine layer (Chromium/Chrome). Electron usually follows, not guaranteed. | |
| EOF | |
| } | |
| # ---------------------------- | |
| # check: headless probe (Wayland) | |
| # ---------------------------- | |
| probe_cmd() { | |
| local cmd="$1" | |
| local log | |
| log="$(mktemp)" | |
| ( | |
| # Override global X11-forcing env vars only for this probe: | |
| # we WANT to test Wayland viability while keeping real apps stable on X11. | |
| env -u OZONE_PLATFORM -u ELECTRON_OZONE_PLATFORM_HINT -u GDK_BACKEND -u QT_QPA_PLATFORM \ | |
| OZONE_PLATFORM=wayland \ | |
| ELECTRON_OZONE_PLATFORM_HINT=wayland \ | |
| GDK_BACKEND=wayland \ | |
| QT_QPA_PLATFORM=wayland \ | |
| timeout "${TIMEOUT_S}" \ | |
| "$cmd" \ | |
| --headless=new \ | |
| --disable-gpu \ | |
| --no-first-run \ | |
| --no-default-browser-check \ | |
| --disable-background-networking \ | |
| --disable-sync \ | |
| --disable-breakpad \ | |
| --disable-features=Translate,MediaRouter \ | |
| --enable-logging=stderr --v=0 \ | |
| about:blank \ | |
| >"$log" 2>&1 || true | |
| ) & | |
| local pid=$! | |
| spinner "$pid" "Probing $cmd (Wayland headless, ${TIMEOUT_S}s)…" | |
| wait "$pid" 2>/dev/null || true | |
| printf "\n" | |
| if grep -Eqi "$BAD_RE" "$log"; then | |
| bad "$cmd: Wayland probe shows known conflict signals." | |
| info "Last ${LOG_LINES} log lines:" | |
| tail -n "${LOG_LINES}" "$log" | sed 's/^/ /' | |
| rm -f "$log" | |
| return 1 | |
| fi | |
| ok "$cmd: probe looks clean (no strong conflict signals)." | |
| rm -f "$log" | |
| return 0 | |
| } | |
| cmd_check() { | |
| printf "%sWayland / Ozone Healthcheck%s (v%s)\n" "${BOLD}" "${RESET}" "$VERSION" | |
| printf "%sSession:%s %s\n" "${BOLD}" "${RESET}" "${XDG_SESSION_TYPE:-unknown}" | |
| printf "%sDesktop:%s %s\n\n" "${BOLD}" "${RESET}" "${XDG_CURRENT_DESKTOP:-unknown}" | |
| if [[ "${XDG_SESSION_TYPE:-}" != "wayland" ]]; then | |
| warn "Not a Wayland session. This check is only meaningful on Wayland." | |
| info "Operational: nothing to probe. If Chromium/Electron misbehave, it's not Wayland-specific." | |
| return 0 | |
| fi | |
| local found_any=0 pass_any=0 fail_any=0 | |
| for c in "${CANDIDATES[@]}"; do | |
| if have_cmd "$c"; then | |
| found_any=1 | |
| if probe_cmd "$c"; then | |
| pass_any=$((pass_any+1)) | |
| else | |
| fail_any=$((fail_any+1)) | |
| fi | |
| printf "\n" | |
| fi | |
| done | |
| if [[ "$found_any" -eq 0 ]]; then | |
| die "No chromium-based engine found (chromium / google-chrome-stable not in PATH)." 1 | |
| fi | |
| printf "%sFinal operational verdict%s\n" "${BOLD}" "${RESET}" | |
| if [[ "$fail_any" -gt 0 ]]; then | |
| bad "Wayland still appears risky for Chromium/Electron on this machine today." | |
| info "Operational: keep your stable posture (X11-forced env + flags + wrappers if needed)." | |
| return 2 | |
| fi | |
| ok "Wayland looks healthy for Chromium engines (based on headless probe)." | |
| info "Operational: you may test Wayland later, one app at a time (controlled experiment)." | |
| warn "Limitation: this validates the engine layer; Electron usually follows but is not guaranteed." | |
| return 0 | |
| } | |
| # ---------------------------- | |
| # fix: apply stable configs | |
| # ---------------------------- | |
| apply_env_fix() { | |
| local content | |
| content=$'# Force X11 for Chromium / Chrome / Electron (maximum stability)\n\nOZONE_PLATFORM=x11\nELECTRON_OZONE_PLATFORM_HINT=x11\n\n# GTK apps (avoid Wayland resize bugs)\nGDK_BACKEND=x11\n\n# Qt / hybrid apps\nQT_QPA_PLATFORM=xcb\n' | |
| write_file_atomic "$ENV_FILE" "$content" | |
| ok "Applied environment fix: $ENV_FILE" | |
| } | |
| apply_flags_fix_for_app() { | |
| local app="$1" | |
| local flags_file="${APP_FLAGS_FILES[$app]:-}" | |
| [[ -z "$flags_file" ]] && return 0 | |
| write_file_atomic "$flags_file" "$FLAGS_X11_STABLE" | |
| ok "Wrote flags: $flags_file (for $app)" | |
| } | |
| cmd_fix() { | |
| printf "%sApply maximum-stability fix (no auto)%s\n" "${BOLD}" "${RESET}" | |
| printf "%sThis forces X11 for Chromium/Electron stacks.%s\n\n" "${DIM}" "${RESET}" | |
| apply_env_fix | |
| local detected | |
| detected="$(detect_apps || true)" | |
| if [[ -z "$detected" ]]; then | |
| warn "No known affected apps detected in PATH. (Still applied global env fix.)" | |
| else | |
| ok "Detected relevant targets:" | |
| while IFS= read -r a; do | |
| [[ -z "$a" ]] && continue | |
| info " - $a" | |
| done <<<"$detected" | |
| printf "\n" | |
| # Apply per-app flags best-effort | |
| while IFS= read -r a; do | |
| [[ -z "$a" ]] && continue | |
| apply_flags_fix_for_app "$a" | |
| done <<<"$detected" | |
| fi | |
| printf "\n" | |
| warn "Operational next step:" | |
| info " You MUST log out completely and log back in for environment.d changes to take effect." | |
| info " Restarting apps is not enough." | |
| return 0 | |
| } | |
| # ---------------------------- | |
| # fix-wrap: create per-app wrappers (strong enforcement) | |
| # ---------------------------- | |
| create_wrapper() { | |
| local app="$1" | |
| local real | |
| real="$(real_path_for_cmd "$app")" | |
| [[ -z "$real" ]] && return 0 | |
| local wrapper="${HOME}/.local/bin/${app}" | |
| # Avoid wrapping ourselves or already-local wrapper | |
| if [[ "$real" == "$wrapper" ]]; then | |
| return 0 | |
| fi | |
| local content | |
| content="#!/usr/bin/env bash | |
| set -euo pipefail | |
| # Wrapper generated by wayland-ozone-doctor (forces X11 for stability) | |
| export OZONE_PLATFORM=x11 | |
| export ELECTRON_OZONE_PLATFORM_HINT=x11 | |
| export GDK_BACKEND=x11 | |
| export QT_QPA_PLATFORM=xcb | |
| exec \"$real\" \"\$@\" | |
| " | |
| write_file_atomic "$wrapper" "$content" | |
| chmod +x "$wrapper" | |
| ok "Wrapper installed: $wrapper -> $real" | |
| } | |
| cmd_fix_wrap() { | |
| printf "%sCreate per-app wrappers (strong enforcement)%s\n" "${BOLD}" "${RESET}" | |
| printf "%sThis creates wrappers in ~/.local/bin for common Electron apps detected on your system.%s\n\n" "${DIM}" "${RESET}" | |
| if ! echo "${PATH:-}" | tr ':' '\n' | grep -qx "${HOME}/.local/bin"; then | |
| warn "~/.local/bin is not in PATH. Wrappers won't take effect until it is." | |
| info "Add to your shell rc: export PATH=\"\$HOME/.local/bin:\$PATH\"" | |
| fi | |
| local any=0 | |
| for a in "${ELECTRON_APPS[@]}"; do | |
| if have_cmd "$a"; then | |
| any=1 | |
| create_wrapper "$a" | |
| fi | |
| done | |
| if [[ "$any" -eq 0 ]]; then | |
| warn "No known Electron apps detected from the built-in list." | |
| info "Operational: tell me your exact app commands (e.g., 'slack', 'discord', 'obsidian'), and I’ll add them." | |
| fi | |
| printf "\n" | |
| warn "Operational note:" | |
| info " Wrappers take effect when launching apps from terminal or launchers that respect PATH ordering." | |
| info " If a .desktop entry calls /usr/bin/app explicitly, wrappers may be bypassed." | |
| info " If that happens, we can patch the desktop entry in ~/.local/share/applications (optional)." | |
| return 0 | |
| } | |
| # ---------------------------- | |
| # status | |
| # ---------------------------- | |
| cmd_status() { | |
| printf "%sStatus%s\n" "${BOLD}" "${RESET}" | |
| printf "%sInstalled binary:%s %s\n" "${BOLD}" "${RESET}" "$(have_cmd wayland-ozone-doctor && echo "yes ($(command -v wayland-ozone-doctor))" || echo "no")" | |
| printf "%sEnv fix file:%s %s\n" "${BOLD}" "${RESET}" "$([[ -f "$ENV_FILE" ]] && echo "present ($ENV_FILE)" || echo "missing")" | |
| printf "%sSession:%s %s\n" "${BOLD}" "${RESET}" "${XDG_SESSION_TYPE:-unknown}" | |
| printf "%sDesktop:%s %s\n\n" "${BOLD}" "${RESET}" "${XDG_CURRENT_DESKTOP:-unknown}" | |
| ok "Per-app flags (best-effort):" | |
| local detected | |
| detected="$(detect_apps || true)" | |
| if [[ -z "$detected" ]]; then | |
| info " (no relevant targets detected)" | |
| else | |
| while IFS= read -r a; do | |
| [[ -z "$a" ]] && continue | |
| local f="${APP_FLAGS_FILES[$a]:-}" | |
| if [[ -n "$f" ]]; then | |
| if [[ -f "$f" ]]; then | |
| info " - $a : flags present -> $f" | |
| else | |
| info " - $a : flags missing -> $f" | |
| fi | |
| else | |
| info " - $a" | |
| fi | |
| done <<<"$detected" | |
| fi | |
| printf "\n" | |
| ok "Wrappers in ~/.local/bin (detected from common Electron list):" | |
| local any=0 | |
| for a in "${ELECTRON_APPS[@]}"; do | |
| if [[ -x "${HOME}/.local/bin/${a}" ]]; then | |
| any=1 | |
| info " - ${a} -> $(head -n 1 "${HOME}/.local/bin/${a}" >/dev/null 2>&1; echo "~/.local/bin/${a}")" | |
| fi | |
| done | |
| if [[ "$any" -eq 0 ]]; then | |
| info " (none)" | |
| fi | |
| return 0 | |
| } | |
| # ---------------------------- | |
| # install: self-install | |
| # ---------------------------- | |
| cmd_install() { | |
| mkdir -p "$INSTALL_DIR" | |
| cp -f "$SELF_PATH" "$INSTALL_PATH" | |
| chmod +x "$INSTALL_PATH" | |
| ok "Installed: $INSTALL_PATH" | |
| if ! echo "${PATH:-}" | tr ':' '\n' | grep -qx "${HOME}/.local/bin"; then | |
| warn "~/.local/bin is not in PATH (current shell)." | |
| info "Operational: add this to your shell rc (e.g., ~/.bashrc):" | |
| info " export PATH=\"\$HOME/.local/bin:\$PATH\"" | |
| else | |
| ok "~/.local/bin is in PATH." | |
| fi | |
| printf "\n" | |
| ok "Try:" | |
| info " wayland-ozone-doctor status" | |
| info " wayland-ozone-doctor fix" | |
| info " wayland-ozone-doctor fix-wrap" | |
| info " wayland-ozone-doctor check" | |
| return 0 | |
| } | |
| # ---------------------------- | |
| # help | |
| # ---------------------------- | |
| show_help() { | |
| cat <<EOF | |
| wayland-ozone-doctor (v${VERSION}) | |
| Usage: | |
| $0 install # self-install to ~/.local/bin/wayland-ozone-doctor | |
| $0 fix # apply maximum-stability (force X11) configs + flags (best-effort) | |
| $0 fix-wrap # create per-app wrappers in ~/.local/bin (strongest per-app enforcement) | |
| $0 check # lightweight headless Wayland probe (engine-layer) | |
| $0 status # show detected apps + config presence | |
| $0 readme # operating guide / FAQ (English) | |
| EOF | |
| } | |
| # ---------------------------- | |
| # Main | |
| # ---------------------------- | |
| cmd="${1:-help}" | |
| case "$cmd" in | |
| install) cmd_install ;; | |
| fix) cmd_fix ;; | |
| fix-wrap) cmd_fix_wrap ;; | |
| check) cmd_check ;; | |
| status) cmd_status ;; | |
| readme) show_readme ;; | |
| help|-h|--help) show_help ;; | |
| *) show_help; die "Unknown command: $cmd" 2 ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment