|
| 1 | +# Electron Flickering on Hyprland — Technical Report |
| 2 | + |
| 3 | +**Date:** 2026-05-24 |
| 4 | +**Platform:** Arch Linux + Hyprland (Wayland compositor) |
| 5 | +**App:** OpenLinear Electron desktop wrapper |
| 6 | +**Reporter:** kaizen |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## Problem Summary |
| 11 | + |
| 12 | +The Electron desktop app (`pnpm start:electron`) opens with severe visual flickering on Hyprland. The window appears to rapidly flash/resize during initial load, making the app unusable. This does **not** occur with the Tauri build on the same system, nor with the web app in Chromium. |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +## Environment |
| 17 | + |
| 18 | +- **OS:** Arch Linux |
| 19 | +- **WM/Compositor:** Hyprland (Wayland) |
| 20 | +- **Electron:** v35.7.5 (system `/usr/bin/electron`) |
| 21 | +- **Node:** v24.12.0 |
| 22 | +- **OpenLinear branch:** `dev` |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## What Was Tried (Chronological) |
| 27 | + |
| 28 | +### 1. CSS Flexbox Scroll Fix (chat-message-list.tsx) |
| 29 | +Added `min-h-0` to the chat message list scroll container. This fixed a WebKitGTK-specific flexbox scroll issue but is **irrelevant** to Electron (Electron uses Chromium, not WebKitGTK). |
| 30 | + |
| 31 | +**Status:** Unrelated to flickering. |
| 32 | + |
| 33 | +--- |
| 34 | + |
| 35 | +### 2. Electron Wrapper Creation |
| 36 | +Created `apps/desktop-electron/` as a minimal Electron main process wrapping the existing Next.js static export. |
| 37 | + |
| 38 | +**Architecture:** |
| 39 | +- Main process: `src/main.ts` — creates BrowserWindow, spawns sidecar, handles IPC |
| 40 | +- Preload: `src/preload.ts` — exposes `window.electronAPI` via contextBridge |
| 41 | +- Sidecar: `src/sidecar.ts` — spawns `openlinear-sidecar` binary via Node child_process |
| 42 | +- Builder: `electron-builder.json` — targets AppImage + deb for Linux |
| 43 | + |
| 44 | +**Frontend patches:** Runtime detection of Electron vs Tauri in: |
| 45 | +- `lib/api/client.ts` — sidecar URL resolution |
| 46 | +- `lib/api/auth.ts` — desktop login flow |
| 47 | +- `hooks/use-auth.tsx` — auth callback listener |
| 48 | +- `components/layout/sidebar.tsx` — window controls |
| 49 | +- `components/desktop/*.tsx` — native API calls |
| 50 | + |
| 51 | +--- |
| 52 | + |
| 53 | +### 3. Initial Flickering Diagnosis — CSS Transitions |
| 54 | + |
| 55 | +**Hypothesis:** The CSS "fast" render profile (disables backdrop-filter, transitions, animations) was only active for Tauri + Linux, not Electron + Linux. |
| 56 | + |
| 57 | +**Fix:** Patched `app/layout.tsx` inline script to detect `window.electronAPI` and apply `data-openlinear-render-profile="fast"` for Electron on Linux. |
| 58 | + |
| 59 | +**Result:** Flickering persisted. The CSS transitions were already disabled, but the window itself was still flashing. |
| 60 | + |
| 61 | +--- |
| 62 | + |
| 63 | +### 4. Window Creation Timing |
| 64 | + |
| 65 | +**Hypothesis:** `ready-to-show` event fires before the compositor has stabilized, causing a flash of partially-rendered content. |
| 66 | + |
| 67 | +**Fixes tried:** |
| 68 | +- `paintWhenInitiallyHidden: false` — prevents painting until explicitly shown |
| 69 | +- `show: false` + `did-finish-load` (instead of `ready-to-show`) on Linux |
| 70 | +- 250ms `setTimeout` delay before `win.show()` |
| 71 | +- `backgroundColor: "#0a0a0a"` — prevents white flash |
| 72 | + |
| 73 | +**Result:** Flickering persisted. The window shows but flashes during the first few seconds. |
| 74 | + |
| 75 | +--- |
| 76 | + |
| 77 | +### 5. Hardware Acceleration & GPU |
| 78 | + |
| 79 | +**Hypothesis:** GPU compositor conflicts with Hyprland's Wayland compositor. |
| 80 | + |
| 81 | +**Fixes tried:** |
| 82 | +- `app.disableHardwareAcceleration()` — disabled GPU compositing entirely |
| 83 | +- `app.commandLine.appendSwitch("disable-gpu-vsync")` |
| 84 | +- `app.commandLine.appendSwitch("disable-software-rasterizer")` |
| 85 | +- `app.commandLine.appendSwitch("ozone-platform", "x11")` — force XWayland |
| 86 | +- `app.commandLine.appendSwitch("ozone-platform-hint", "x11")` |
| 87 | +- `process.env.ELECTRON_OZONE_PLATFORM_HINT = "x11"` |
| 88 | + |
| 89 | +**Result:** Window went **completely blank** with error: |
| 90 | +``` |
| 91 | +[ERROR:ui/base/x/x11_software_bitmap_presenter.cc:147] XGetWindowAttributes failed for window 1 |
| 92 | +``` |
| 93 | + |
| 94 | +This indicates `disableHardwareAcceleration()` + XWayland force causes Chromium's software bitmap presenter to fail on Hyprland. |
| 95 | + |
| 96 | +--- |
| 97 | + |
| 98 | +### 6. Window Properties |
| 99 | + |
| 100 | +**Fixes tried:** |
| 101 | +- `transparent: false` — disables alpha blending |
| 102 | +- `hasShadow: false` — disables drop shadow |
| 103 | +- `frame: true` — native window frame instead of custom |
| 104 | +- `titleBarStyle: "default"` — standard title bar |
| 105 | + |
| 106 | +**Result:** No improvement. Flickering persisted. |
| 107 | + |
| 108 | +--- |
| 109 | + |
| 110 | +### 7. Positioning (Latest Attempt) |
| 111 | + |
| 112 | +**Hypothesis:** `center: true` in BrowserWindow options causes Electron to create the window at (0,0) then reposition it to center. Hyprland animates this reposition with a slide effect, causing flicker. |
| 113 | + |
| 114 | +**Fix:** Removed `center: true`. Calculated center position explicitly using `screen.getPrimaryDisplay().workAreaSize` and passed `x`/`y` to BrowserWindow. |
| 115 | + |
| 116 | +**Result:** Flickering persisted. |
| 117 | + |
| 118 | +--- |
| 119 | + |
| 120 | +## Current State (As of 2026-05-24 11:30 IST) |
| 121 | + |
| 122 | +The Electron app: |
| 123 | +1. ✅ Compiles successfully (`tsc` passes) |
| 124 | +2. ✅ Next.js static export builds correctly |
| 125 | +3. ✅ Sidecar spawns and reports ready on ephemeral port |
| 126 | +4. ✅ Local HTTP server serves frontend files |
| 127 | +5. ✅ Window opens at correct position |
| 128 | +6. ❌ **Severe flickering during initial 2-3 seconds of window life** |
| 129 | +7. ✅ After flickering stops, app appears to function normally |
| 130 | + |
| 131 | +--- |
| 132 | + |
| 133 | +## Leading Hypotheses (Unverified) |
| 134 | + |
| 135 | +### Hypothesis A: Hyprland Window Animation Rules |
| 136 | +Hyprland applies entrance animations (slide/scale) to all new windows by default. Electron/Chromium's initial window surface may be resizing or repainting rapidly during load, causing Hyprland to restart the entrance animation repeatedly. |
| 137 | + |
| 138 | +**Potential fix:** Add a Hyprland window rule to disable animations for OpenLinear: |
| 139 | +```conf |
| 140 | +# ~/.config/hypr/hyprland.conf |
| 141 | +windowrulev2 = noanim, class:^(OpenLinear)$ |
| 142 | +windowrulev2 = noborder, class:^(OpenLinear)$ |
| 143 | +windowrulev2 = noshadow, class:^(OpenLinear)$ |
| 144 | +``` |
| 145 | + |
| 146 | +--- |
| 147 | + |
| 148 | +### Hypothesis B: Chromium Ozone/Wayland Backend Issues |
| 149 | +Electron 35 on Linux defaults to the Ozone platform abstraction. On Hyprland (a custom wlroots-based compositor), Chromium's Wayland backend may have unpatched bugs with surface commits during initial load. |
| 150 | + |
| 151 | +**Potential fix:** Force native X11 backend (not XWayland) by setting environment variable before launching: |
| 152 | +```bash |
| 153 | +export ELECTRON_OZONE_PLATFORM_HINT=x11 |
| 154 | +pnpm start:electron |
| 155 | +``` |
| 156 | +Note: Earlier attempt with `app.commandLine.appendSwitch` failed because it was combined with `disableHardwareAcceleration()`. These should be tested **independently**. |
| 157 | + |
| 158 | +--- |
| 159 | + |
| 160 | +### Hypothesis C: Next.js Hydration Mismatch |
| 161 | +The static export produces HTML that React then hydrates. During hydration, React may re-render the entire DOM tree, causing layout shifts that Electron/Chromium renders as visible flashes. |
| 162 | + |
| 163 | +**Potential fix:** Add `suppressHydrationWarning` and ensure server/client HTML match exactly. Or disable SSR entirely for the Electron build. |
| 164 | + |
| 165 | +--- |
| 166 | + |
| 167 | +### Hypothesis D: EventSource / SSE Connection Racing |
| 168 | +The frontend immediately tries to connect to `/api/events` via EventSource. If the sidecar isn't fully ready, rapid connection retries may cause UI updates (loading states) that manifest as flicker. |
| 169 | + |
| 170 | +**Potential fix:** Delay EventSource connection until `sidecar:ready` event is confirmed. |
| 171 | + |
| 172 | +--- |
| 173 | + |
| 174 | +## Recommended Next Steps for Investigating Agent |
| 175 | + |
| 176 | +1. **Test Hyprland window rules** — Add `noanim` rule and verify if flickering stops. This is the fastest test. |
| 177 | + |
| 178 | +2. **Test X11 backend without hardware accel disable** — Set `ELECTRON_OZONE_PLATFORM_HINT=x11` as an **environment variable** before launch (not via `appendSwitch`), and do NOT call `app.disableHardwareAcceleration()`. Test if this produces a stable window. |
| 179 | + |
| 180 | +3. **Profile with `WAYLAND_DEBUG=1`** — Run with Wayland protocol debugging to see if there are excessive `wl_surface_commit` calls during flickering. |
| 181 | + |
| 182 | +4. **Check `hyprctl clients` output** — Verify whether the Electron window is running under native Wayland or XWayland. |
| 183 | + |
| 184 | +5. **Test with a minimal HTML file** — Create a minimal `BrowserWindow` loading `about:blank` or a simple static HTML file. If this also flickers, it's an Electron/Hyprland issue, not an OpenLinear app issue. |
| 185 | + |
| 186 | +6. **Try `new BrowserWindow({ show: true })`** — Remove `show: false` entirely and let the window appear immediately. The `ready-to-show` pattern may be interacting poorly with Hyprland's animation system. |
| 187 | + |
| 188 | +7. **Consider Tauri as primary Linux target** — If Electron cannot be made stable on Hyprland, the Tauri build (with the CSS scroll fixes already applied) may be the more pragmatic Linux choice despite WebKitGTK's limitations. The web app at openlinear.tech already works perfectly in Chromium. |
| 189 | + |
| 190 | +--- |
| 191 | + |
| 192 | +## Files Involved |
| 193 | + |
| 194 | +- `apps/desktop-electron/src/main.ts` — Main process, window creation |
| 195 | +- `apps/desktop-electron/src/preload.ts` — IPC bridge |
| 196 | +- `apps/desktop-electron/src/sidecar.ts` — Sidecar spawning |
| 197 | +- `apps/desktop-ui/app/layout.tsx` — CSS render profile detection |
| 198 | +- `apps/desktop-ui/app/globals.css` — Fast render profile CSS (disables transitions/animations) |
| 199 | + |
| 200 | +--- |
| 201 | + |
| 202 | +## Build Commands |
| 203 | + |
| 204 | +```bash |
| 205 | +# Development (loads from localhost:3000, no static build needed) |
| 206 | +pnpm dev:electron |
| 207 | + |
| 208 | +# Production-like (builds static export + launches Electron) |
| 209 | +pnpm start:electron |
| 210 | + |
| 211 | +# Build distributable |
| 212 | +pnpm build:electron:linux # AppImage + deb |
| 213 | +``` |
| 214 | + |
| 215 | +--- |
| 216 | + |
| 217 | +## Notes |
| 218 | + |
| 219 | +- The Tauri desktop app (`pnpm --filter @openlinear/desktop tauri dev`) works without flickering on the same Hyprland setup, confirming this is Electron-specific. |
| 220 | +- The web app at `https://openlinear.tech` works perfectly in Chromium on the same system. |
| 221 | +- This strongly suggests the issue is in the **Electron ↔ Hyprland interaction**, not the OpenLinear frontend code itself. |
0 commit comments