diff --git a/config.example.toml b/config.example.toml index 0a0eb33..3258ee1 100644 --- a/config.example.toml +++ b/config.example.toml @@ -8,6 +8,12 @@ portal_socket = "$XDG_RUNTIME_DIR/wo-portal.sock" enable_syscalls = true focus_notify = true +# Environment variables passed to all spawned child processes. +# DISPLAY defaults to ":0" so X11 apps (e.g. Steam) work even before +# XWayland is ready. XWayland overrides DISPLAY with the actual value. +[compositor.environment] +DISPLAY = ":0" + [[root]] name = "main" url = "http://localhost:5173" diff --git a/src/config.rs b/src/config.rs index 0d8e0da..67559cc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,5 @@ use serde::Deserialize; +use std::collections::HashMap; use std::path::PathBuf; use anyhow::{Context, Result}; @@ -89,6 +90,13 @@ pub struct CompositorConfig { /// Cursor size in pixels (default: 24) #[serde(default = "default_cursor_size")] pub cursor_size: u32, + + /// Extra environment variables to set before spawning child processes. + /// Defaults include DISPLAY=":0" so X11 applications (e.g. Steam) can + /// fall back to a sensible value when XWayland hasn't started yet. + /// XWayland will override DISPLAY with the actual value once ready. + #[serde(default = "default_environment")] + pub environment: HashMap, } impl Default for CompositorConfig { @@ -110,6 +118,7 @@ impl Default for CompositorConfig { applications: vec![], cursor_theme: default_cursor_theme(), cursor_size: default_cursor_size(), + environment: default_environment(), } } } @@ -278,6 +287,12 @@ fn default_true() -> bool { true } fn default_cursor_theme() -> String { "breeze_cursors".into() } fn default_cursor_size() -> u32 { 24 } +fn default_environment() -> HashMap { + let mut env = HashMap::new(); + env.insert("DISPLAY".into(), ":0".into()); + env +} + fn expand_path(s: &str) -> String { shellexpand::full(s) .map(|cow| cow.into_owned()) @@ -320,6 +335,21 @@ impl Config { pub fn config_dir() -> PathBuf { dirs_config() } + + /// Apply configured environment variables to the current process so all + /// subsequently-spawned children inherit them. Existing env vars are + /// **not** overwritten so that, e.g., a DISPLAY already set by the + /// parent session or XWayland takes priority. + pub fn apply_environment(&self) { + for (key, value) in &self.compositor.environment { + if std::env::var(key).is_err() { + tracing::info!("Setting environment {key}={value} (from config default)"); + std::env::set_var(key, value); + } else { + tracing::debug!("Environment {key} already set, skipping config default"); + } + } + } } fn config_path() -> PathBuf { diff --git a/src/main.rs b/src/main.rs index 34fe228..a1d65a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -403,6 +403,10 @@ fn main() -> Result<(), anyhow::Error> { let config = Config::load().context("loading config")?; + // Apply configured environment variables (e.g. DISPLAY=:0) so that all + // children inherit them. XWayland will override DISPLAY later if needed. + config.apply_environment(); + // Determine backend priority: nested (gles) is prioritized with DRM fallback let force_drm = matches!(config.compositor.nested, Some(false)); diff --git a/src/nested.rs b/src/nested.rs index 67a3a85..8e34c1b 100644 --- a/src/nested.rs +++ b/src/nested.rs @@ -85,6 +85,10 @@ pub fn run_nested(config: Config) -> Result<()> { info!("Starting nested compositor mode"); + // Apply configured environment variables (e.g. DISPLAY=:0) early so that + // all children inherit them. XWayland will override DISPLAY later. + config.apply_environment(); + if config.compositor.exit_on_escape { info!("Press ESC to exit"); } else {