Skip to main content

agentmux_launcher/
splash_config.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! "Disable the splash" preference, resolved by the launcher *before* it spawns
5//! the splash (and before the host/srv exist), per
6//! `SPEC_SPLASH_USERINFO_AND_DISABLE_2026_06_21.md` §6.
7//!
8//! Precedence: env override wins; otherwise `settings.json` `"splash:disabled"`.
9//! **Fail-safe:** any error reading/parsing settings resolves to *enabled* — a
10//! broken read must never silently suppress the splash.
11
12use std::path::{Path, PathBuf};
13
14/// True when the splash should be suppressed entirely (no window created).
15pub fn splash_disabled() -> bool {
16    if let Some(forced) = env_override() {
17        return forced; // env is authoritative (Some(true)=disable, Some(false)=force-on)
18    }
19    // A login start is quiet: no window, so no splash either
20    // (SPEC_START_WITH_OS_2026_09_25.md §3.3). On macOS this also routes the
21    // start through the headless AppKit pump the menu-bar item needs.
22    if crate::autostart::login_start() {
23        return true;
24    }
25    // Best-effort settings.json read. Wrapped so a resolver panic/IO error can
26    // never take down launcher startup — on any failure we keep the splash.
27    std::panic::catch_unwind(|| {
28        config_dir()
29            .map(|d| settings_splash_disabled(&d.join("settings.json")))
30            .unwrap_or(false)
31    })
32    .unwrap_or(false)
33}
34
35/// `Some(true)` = disable, `Some(false)` = force-enable, `None` = no opinion.
36fn env_override() -> Option<bool> {
37    if let Ok(v) = std::env::var("AGENTMUX_NO_SPLASH") {
38        if truthy(&v) {
39            return Some(true);
40        }
41    }
42    if let Ok(v) = std::env::var("AGENTMUX_SPLASH") {
43        return match v.trim().to_ascii_lowercase().as_str() {
44            "0" | "false" | "off" | "no" => Some(true),
45            "1" | "true" | "on" | "yes" => Some(false),
46            _ => None,
47        };
48    }
49    None
50}
51
52fn truthy(v: &str) -> bool {
53    matches!(
54        v.trim().to_ascii_lowercase().as_str(),
55        "1" | "true" | "on" | "yes"
56    )
57}
58
59/// Read `"splash:disabled"` (flat `namespace:key`) from a settings.json file.
60/// Missing file / parse error / missing key → `false` (enabled).
61pub fn settings_splash_disabled(settings_path: &Path) -> bool {
62    let Ok(text) = std::fs::read_to_string(settings_path) else {
63        return false;
64    };
65    let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) else {
66        return false;
67    };
68    json.get("splash:disabled")
69        .and_then(|v| v.as_bool())
70        .unwrap_or(false)
71}
72
73/// Best-effort config dir (holds settings.json) resolved the same way
74/// `launcher_main` does — but early and failure-tolerant.
75fn config_dir() -> Option<PathBuf> {
76    let exe = std::env::current_exe().ok()?;
77    let exe_dir = exe.parent()?;
78    let version = env!("CARGO_PKG_VERSION");
79    crate::data_dir::resolve_paths(exe_dir, version)
80        .ok()
81        .map(|p| p.config_dir)
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    fn write_tmp(tag: &str, content: &str) -> PathBuf {
89        let p = std::env::temp_dir().join(format!("agentmux-splashcfg-{tag}-{}.json", std::process::id()));
90        std::fs::write(&p, content).unwrap();
91        p
92    }
93
94    #[test]
95    fn settings_disabled_true() {
96        let p = write_tmp("on", r#"{"splash:disabled": true}"#);
97        assert!(settings_splash_disabled(&p));
98        let _ = std::fs::remove_file(&p);
99    }
100
101    #[test]
102    fn settings_disabled_false_when_key_false() {
103        let p = write_tmp("off", r#"{"splash:disabled": false}"#);
104        assert!(!settings_splash_disabled(&p));
105        let _ = std::fs::remove_file(&p);
106    }
107
108    #[test]
109    fn settings_default_enabled_when_key_absent() {
110        let p = write_tmp("absent", r#"{"term:fontsize": 14}"#);
111        assert!(!settings_splash_disabled(&p));
112        let _ = std::fs::remove_file(&p);
113    }
114
115    #[test]
116    fn settings_fail_safe_enabled_on_missing_or_bad() {
117        // Missing file and malformed JSON both resolve to enabled (false).
118        assert!(!settings_splash_disabled(Path::new("/no/such/agentmux-settings.json")));
119        let p = write_tmp("bad", "this is not json {");
120        assert!(!settings_splash_disabled(&p));
121        let _ = std::fs::remove_file(&p);
122    }
123}