atagen

monolith

public
Star0
Watch0

Commit 38314f607eb9

tarball

atagen <boss@atagen.co>2026-10-17 10:29 UTC

3 files changed+63-15
monolith: log to a file under XDG_STATE_HOME and report config warnings

A session started from a display manager had its stderr scattered or discarded, so rejected config options were invisible. The previous run's log is kept beside it, and nested runs log separately so testing does not clobber the host session's log.
Modified file: crates/monolith/src/commands.rs+11 additions−3 deletions
@@ -207,10 +207,11 @@ impl State {
207207 }
208208 }
209209
210- /// Swaps in a freshly parsed config. A broken file leaves the running
211- /// config untouched.
210+ /// Swaps in a freshly parsed config. A file that is not TOML leaves the
211+ /// running config untouched.
212212 pub fn reload(&mut self) -> Result<(), String> {
213- let config = Config::load(self.settings.path.as_deref()).map_err(|e| e.to_string())?;
213+ let loaded = Config::load(self.settings.path.as_deref()).map_err(|e| e.to_string())?;
214+ let config = warn_config(loaded);
214215 let keyboard_changed = config.keyboard != self.settings.config.keyboard;
215216 self.settings.binds = Binds::new(&config);
216217 self.wm.layout.gaps = crate::state::gaps(&config);
@@ -223,3 +224,10 @@ impl State {
223224 Ok(())
224225 }
225226 }
227+
228+pub fn warn_config(loaded: monolith_config::Loaded) -> Config {
229+ for w in &loaded.warnings {
230+ tracing::warn!("config: {w}");
231+ }
232+ loaded.config
233+}
Modified file: crates/monolith/src/main.rs+43 additions−8 deletions
@@ -116,11 +116,26 @@ fn main() -> ExitCode {
116116 }
117117 Some(Cmd::Subscribe) => return ipc::client(Request::Subscribe {}),
118118 };
119- let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
119+ let backend = match backend {
120+ Backend::Auto if std::env::var_os("WAYLAND_DISPLAY").is_some() => Backend::Nested,
121+ Backend::Auto => Backend::Drm,
122+ b => b,
123+ };
124+ let filter = || EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
120125 let fmt = tracing_subscriber::fmt::layer()
121126 .with_writer(std::io::stderr)
122127 .with_ansi(std::io::IsTerminal::is_terminal(&std::io::stderr()))
123- .with_filter(filter);
128+ .with_filter(filter());
129+ let log_path = log_file(backend);
130+ let file = log_path.as_ref().and_then(|p| {
131+ let file = std::fs::File::create(p).ok()?;
132+ Some(
133+ tracing_subscriber::fmt::layer()
134+ .with_writer(std::sync::Mutex::new(file))
135+ .with_ansi(false)
136+ .with_filter(filter()),
137+ )
138+ });
124139 #[cfg(feature = "spans")]
125140 let slow = slow_spans::SlowSpans::from_env();
126141 #[cfg(feature = "spans")]
@@ -129,14 +144,16 @@ fn main() -> ExitCode {
129144 let spans = Some(slow.with_filter(tracing_subscriber::filter::LevelFilter::TRACE));
130145 #[cfg(not(feature = "spans"))]
131146 let spans: Option<tracing_subscriber::layer::Identity> = None;
132- tracing_subscriber::registry().with(fmt).with(spans).init();
147+ tracing_subscriber::registry()
148+ .with(fmt)
149+ .with(file)
150+ .with(spans)
151+ .init();
152+ if let Some(p) = &log_path {
153+ tracing::info!(path = %p.display(), "logging to a file");
154+ }
133155 #[cfg(feature = "spans")]
134156 tracing::info!("logging spans slower than {threshold_us} us");
135- let backend = match backend {
136- Backend::Auto if std::env::var_os("WAYLAND_DISPLAY").is_some() => Backend::Nested,
137- Backend::Auto => Backend::Drm,
138- b => b,
139- };
140157 match run(config, backend, &program) {
141158 Ok(()) => ExitCode::SUCCESS,
142159 Err(e) => {
@@ -146,6 +163,24 @@ fn main() -> ExitCode {
146163 }
147164 }
148165
166+/// `$XDG_STATE_HOME/monolith/monolith.log`, with the previous run kept as
167+/// `monolith.old.log`. Nested runs log beside it, so testing one does not
168+/// overwrite the log of the session hosting it.
169+fn log_file(backend: Backend) -> Option<PathBuf> {
170+ let dir = std::env::var_os("XDG_STATE_HOME")
171+ .map(PathBuf::from)
172+ .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/state")))?
173+ .join("monolith");
174+ std::fs::create_dir_all(&dir).ok()?;
175+ let name = match backend {
176+ Backend::Nested => "nested",
177+ _ => "monolith",
178+ };
179+ let path = dir.join(format!("{name}.log"));
180+ let _ = std::fs::rename(&path, dir.join(format!("{name}.old.log")));
181+ Some(path)
182+}
183+
149184 /// Puts the compositor thread on SCHED_RR so a busy desktop cannot delay a
150185 /// frame. Reset-on-fork keeps threads and programs it starts off realtime.
151186 /// The NixOS capability wrapper raises CAP_SYS_NICE as an ambient capability,
Modified file: crates/monolith/src/state.rs+9 additions−4 deletions
@@ -102,10 +102,15 @@ impl State {
102102 ) -> Result<Self, Box<dyn std::error::Error>> {
103103 let dh = display.handle();
104104 let config_path = config_path.or_else(Config::path);
105- let config = Config::load(config_path.as_deref()).unwrap_or_else(|e| {
106- tracing::error!("config: {e}, using the built-in default");
107- Config::parse(monolith_config::DEFAULT).expect("the default config parses")
108- });
105+ let config = match Config::load(config_path.as_deref()) {
106+ Ok(loaded) => crate::commands::warn_config(loaded),
107+ Err(e) => {
108+ tracing::error!("config: {e}, using the built-in default");
109+ Config::parse(monolith_config::DEFAULT)
110+ .expect("the default config parses")
111+ .config
112+ }
113+ };
109114 let renderer = match gpu {
110115 Some(node) => VulkanRenderer::for_node(node)?,
111116 None => VulkanRenderer::new()?,