Linux notes

September 5, 2026 · View on GitHub

What behaves differently on Linux, what is not available at all, and what to check first when a Linux user reports something odd. The original findings were verified on Ubuntu with app version 0.4.7 (Electron 43, Chromium 150), on X11 (Xvfb with xfwm4) and Wayland (headless Weston). See the 0.4.22 Linux review for the latest packaging fixes, functional test results, and remaining coverage gaps.

The 0.4.23 follow-up covers MCP transport fixes and the new packaged-app checks in Linux CI. The source-dialog review adds keyboard selection and persistence coverage.

Capture in windows you cannot see

This is the big one, and it is fixed rather than merely documented — see hidden-window-keepalive.js.

A source window that is not on screen may get no compositor frames at all, and requestAnimationFrame is driven by frames. Chat pages append messages from frame-scheduled work, so capture stops even though timers keep running and backgroundThrottling: false is set. Measured with a source window created hidden, which is what you get whenever a source's window is switched off and what every window gets under --ssapp-headless-control:

statecompositor frames
created hidden, X11 desktop0–1 per second
created hidden, headless control mode0
on screen, Wayland, real chat page~1 per second
hidden after having been shown, X1160 per second

A frame pump injected into capture pages runs queued frame callbacks from a timer when real frames stop, and stays dormant when they do not. Verified over 45- and 35-minute soaks with real YouTube and Twitch chat, including with compositor frames forced to zero. SSAPP_DISABLE_FRAME_PUMP=1 turns it off.

Hiding a source window on Linux uses a real hide(). Off-screen parking, which is what Windows and macOS do, cannot work here: window managers clamp far-off-screen coordinates back towards the desktop (a request for -30000 came back at -676, leaving a visible sliver) and Wayland forbids programmatic positioning outright.

Not available on Linux

Portable mode. Windows-only by design: resolveEarlyDataPaths() returns null on every other platform. The AppImage is the Linux equivalent and runs from any directory. For portable data, set SSAPP_USER_DATA_DIR to a folder beside the AppImage — that relocates settings, sessions, logins, logs and cache. Note that Chromium's own --user-data-dir is not enough: the app calls app.setPath('userData', ...) during startup, which overrides it for sessions and recovered settings.

System TTS voices. Electron on Linux reports no speech-synthesis voices at all: speechSynthesis.getVoices() stays empty even with speech-dispatcher installed and working (spd-say -L lists voices) and with --enable-speech-dispatcher passed. Use the bundled TTS engine, which works. The system-voice test skips on non-Windows for this reason.

setOpacity(). A no-op on Linux — Electron documents it as Windows and macOS only, and it was confirmed to silently return 1 after setting 0.5, on both X11 and Wayland. Anything that tries to hide a window by making it transparent will appear to succeed and do nothing.

Needs something from the desktop environment

Desktop notifications need a notification daemon. Without one, Notification.show() used to block the whole main process for two minutes while D-Bus timed out, with Notification.isSupported() reporting true the entire time. The app now probes for org.freedesktop.Notifications once, gives up permanently after a failure, and never tries under --ssapp-headless-control. If notifications do not appear on a minimal desktop, that is why; look for [Notifications] No desktop notification service on this session.

The tray icon needs a tray host. GNOME has none by default and many minimal window managers never had one, so close-to-tray can hide the window with no icon to click. Relaunching the app now restores the running window, which is the way back when there is no tray. Headless control mode is exempt, since keeping windows hidden is the point of it.

Transparent windows need a compositing window manager. Without a compositor, transparency does not work. This is why the AppImage can give a transparent chat overlay on KWin but not on a bare window manager.

Global shortcuts register and fire on X11 — verified by delivering real key events. On Wayland, delivery is up to the compositor: globalShortcut.register() returns true either way, so a shortcut that never fires is not something the app can detect.

GPU and stability

The app forces --ignore-gpu-blocklist and --enable-gpu-rasterization, which is exactly what tends to crash on a blocklisted or flaky Linux driver. The crash-recovery ladder now actually applies on Linux: L1 drops WebGPU, L2 respects the GPU blocklist, L3 disables GPU rasterization. Boot was verified at every level from 0 to 4. The L4 rung, which relaxes the GPU sandbox, stays Windows-only on purpose — that is a security trade-off, not a stability knob.

--no-hwa disables hardware acceleration but does not remove the GPU process: it still runs at around 100 MB doing software compositing. What it buys on a GPU-less machine is avoiding repeated driver probing and the GL error spam that comes with it.

Footprint

Measured with everything hidden and chat flowing: about 600 MB and 12 processes for one live YouTube source, and roughly 120 MB for each additional platform. Extra sources on a platform you already run cost a few MB each and no extra processes, because same-origin windows share a renderer. Idle CPU with a hidden source window is under 1% of one core, and memory was flat across a 20-minute watch.

Updates

There is no auto-updater — no electron-updater, no autoUpdater usage. The app compares its version against GitHub releases and links to the release page; you download a new AppImage yourself.

Developing and testing on Linux

Keep the repositories side by side, for example ~/code/ssn_app and ~/code/social_stream. From ssn_app, run npm install, then npm run start-linux. The launcher resolves the neighboring source directory, supports spaces in the path, and stops with a clear error if that checkout is missing. It does not load the disposable build fallback as development source. Use File > Load Social Stream From Folder for a checkout in another location.

For an isolated development profile:

SSAPP_USER_DATA_DIR="$(mktemp -d /tmp/ssapp-dev.XXXXXX)" npm run start-linux

The app needs a display even when every window is hidden. --ozone-platform=headless looks like it should avoid that but segfaults during startup on both Electron 38 and 43 — use Xvfb. On hosts whose drivers advertise GLX but cannot serve it, Xvfb itself crashes unless started with -extension GLX.

Xvfb :99 -screen 0 1920x1080x24 -extension GLX -nolisten tcp &
export DISPLAY=:99
npm run test:hidden-capture          # add --headless or --start-hidden

See AGENTS.md for the full list of hidden-capture test entry points, and docs/CLOUD_HOSTING.md for running on a server.

The compact navigation menu supports Enter/Space to open, Escape to close, and returns keyboard focus to its button when navigation hides a focused link. The menu name, expanded state, current page, and language selector are exposed to accessibility tools. Their labels follow the selected app language.

To check a built AppImage or extracted executable with temporary profiles:

SSAPP_TEST_APP="/path/to/socialstreamninja.AppImage" npm run test:headless-launcher:e2e
DISPLAY=:99 SSAPP_TEST_APP="/path/to/socialstreamninja.AppImage" npm run test:mcp-control:e2e
DISPLAY=:99 SSAPP_TEST_APP="/path/to/socialstreamninja.AppImage" npm run test:navigation-accessibility:e2e
DISPLAY=:99 SSAPP_TEST_APP="/path/to/socialstreamninja.AppImage" npm run test:tts
SSAPP_MCP_BINARY="/path/to/socialstreamninja.AppImage" npm run test:mcp-launch:e2e

The launcher test starts its own display; the control and navigation tests need a working display. These packaged tests select bundled assets so they do not silently depend on a developer's neighboring checkout. The adapter-only test intentionally runs without a display. The navigation test uses --no-sandbox in its isolated test process to accommodate Linux CI hosts; that switch is not added by the development launcher.

To run the full packaged-app gate used by both Linux build workflows:

npm run test:linux-package -- /absolute/path/to/app.AppImage

It extracts the AppImage into a temporary installation (no FUSE required), checks MCP discovery without a display, tests setup and headless recovery, then runs control, interrupted-response/large-screenshot, navigation/localization, and speech workflows on private Xvfb displays. The temporary installation is removed when the script exits. Install xvfb, xauth, and x11-utils first. CI sets SSAPP_TEST_NO_SANDBOX=1 only for isolated testing on runners that prohibit Chromium's sandbox; normal launches are unchanged.

Run the packaged speech test as well as the source test. Sharp's native library and its @img dependencies must be outside app.asar; otherwise the Linux loader cannot find libvips and the speech worker fails even though speech works from source. The explicit asarUnpack entries in package.json keep those libraries accessible.

Known gaps in this testing

  • Multi-monitor placement is unverified. Xvfb reports a single display to Electron even with two screens and Xinerama, and its RandR implementation will not accept virtual monitors, so per-display window-state restore was only exercised against one display. This needs a real dual-head machine.
  • Fully occluded windows on KWin and Mutter are unverified. Those compositors cull occluded surfaces; headless weston does not, so the exact condition from issue #875 could not be reproduced locally. The frame pump is page-agnostic and covers it in principle.
  • Tray icon appearance is unverified. No tray host was available; only the consequence of its absence was tested and mitigated.

Source settings with a keyboard

Open a source's settings button and choose User Agent Settings or Browser Session Settings. Tab and Shift+Tab stay inside the dialog. Use Enter or Space to select a browser session; Save applies that choice. Escape or Cancel closes the dialog and returns focus to the source's settings button. Removing a saved entry returns focus to the remaining selection control, so Escape continues to work. Escape used during input-method composition does not close the dialog.

Dialog titles and the custom-input labels follow the app language. Some other text in these dialogs is still English. Validate with an actual screen reader before claiming full screen-reader support; the automated Linux check verifies keyboard behavior and accessible markup.

Run npm run test:source-dialog-accessibility:e2e under Xvfb for this workflow alone, or npm run test:linux-package -- /absolute/path/to/app.AppImage for the complete package gate.

Moving saved browser settings

Settings exports now include the reusable custom browser-session and user-agent lists. Import restores these definitions alongside the source configuration. Browser-session definitions are names and metadata; they do not transfer login cookies. Sign into the accounts again on a new machine. Older backup files omitted these lists and cannot recreate definitions they never saved.

Malformed definition lists are rejected before import changes the profile. As with other imports, the previous profile settings are saved to settings-before-import.data before a valid import. The Linux package gate also tests export/restore and repeated capture-page crash recovery.

Arch / AUR validation

The published socialstreamninja 0.4.18-2 recipe was reproduced in a clean Arch container: it built and installed, but launch failed because required shared libraries were missing. The checked-in 0.4.18-3 recipe makes GTK3, NSS, ALSA, CUPS, and Mesa runtime dependencies. FUSE is unnecessary for this package because it installs extracted AppImage contents. .SRCINFO was regenerated with makepkg --printsrcinfo. On a minimal installation, the optional noto-fonts and noto-fonts-emoji packages provide normal text and emoji icons; both were checked in the running app. CJK fonts are also recommended for those languages, but were not tested here.

For the headless launcher on Arch, install xorg-server-xvfb and xorg-xdpyinfo, then set SSAPP_BINARY=/usr/bin/socialstreamninja. The integration tests additionally use xorg-xauth and xorg-xwininfo; these are test tools rather than app dependencies.

See practical Linux validation for real installation, upgrade/reinstall, browser, window, screenshot, and long-session results. The corrected recipe was published to AUR on September 5, 2026 as 0.4.18-3 (AUR commit 3388a6c). The public package page and Git repository were verified. This is a dependency correction for the existing 0.4.18 release, not publication of the local 0.4.24 build.