1use std::path::{Path, PathBuf};
51
52pub const BACKGROUND_KEY: &str = "app:runinbackground";
54pub const TRAY_KEY: &str = "app:showtray";
55
56const ENV_BACKGROUND: &str = "AGENTMUX_BACKGROUND_SERVICE";
57const ENV_TRAY: &str = "AGENTMUX_TRAY";
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct Settings {
62 pub run_in_background: bool,
63 pub show_tray: bool,
64}
65
66impl Default for Settings {
67 fn default() -> Self {
68 Settings {
69 run_in_background: false,
70 show_tray: true,
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct Resolved {
78 pub background: bool,
79 pub tray: bool,
80}
81
82pub fn resolve(env_background: bool, env_tray: bool, background_flag: bool, settings: Settings) -> Resolved {
84 let background = env_background || background_flag || settings.run_in_background;
85 let tray = background || env_tray || settings.show_tray;
86 Resolved { background, tray }
87}
88
89pub fn read_settings(settings_path: &Path) -> Settings {
92 let mut s = Settings::default();
93 let Ok(text) = std::fs::read_to_string(settings_path) else {
94 return s;
95 };
96 let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) else {
97 return s;
98 };
99 if let Some(v) = json.get(BACKGROUND_KEY).and_then(|v| v.as_bool()) {
100 s.run_in_background = v;
101 }
102 if let Some(v) = json.get(TRAY_KEY).and_then(|v| v.as_bool()) {
103 s.show_tray = v;
104 }
105 s
106}
107
108pub fn apply(args: &[String]) -> Resolved {
114 let env_background = std::env::var_os(ENV_BACKGROUND).is_some();
115 let env_tray = std::env::var_os(ENV_TRAY).is_some();
116 let flag = crate::autostart::background_requested(args);
117 let settings = std::panic::catch_unwind(|| {
118 config_dir()
119 .map(|d| read_settings(&d.join("settings.json")))
120 .unwrap_or_default()
121 })
122 .unwrap_or_default();
123
124 let r = resolve(env_background, env_tray, flag, settings);
125 if r.background {
126 std::env::set_var(ENV_BACKGROUND, "1");
127 }
128 if r.tray {
129 std::env::set_var(ENV_TRAY, "1");
130 }
131 r
132}
133
134fn config_dir() -> Option<PathBuf> {
137 let exe = std::env::current_exe().ok()?;
138 let exe_dir = exe.parent()?;
139 let version = env!("CARGO_PKG_VERSION");
140 crate::data_dir::resolve_paths(exe_dir, version)
141 .ok()
142 .map(|p| p.config_dir)
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 fn write_tmp(tag: &str, content: &str) -> PathBuf {
150 let p = std::env::temp_dir().join(format!(
151 "agentmux-bgcfg-{tag}-{}.json",
152 std::process::id()
153 ));
154 std::fs::write(&p, content).unwrap();
155 p
156 }
157
158 fn read(tag: &str, content: &str) -> Settings {
159 let p = write_tmp(tag, content);
160 let s = read_settings(&p);
161 let _ = std::fs::remove_file(&p);
162 s
163 }
164
165 const DEFAULTS: Settings = Settings {
166 run_in_background: false,
167 show_tray: true,
168 };
169
170 #[test]
173 fn defaults_are_tray_on_background_off() {
174 assert_eq!(read_settings(Path::new("/no/such/agentmux-settings.json")), DEFAULTS);
175 assert_eq!(read("absent", r#"{"term:fontsize": 14}"#), DEFAULTS);
176 assert_eq!(read("empty-obj", "{}"), DEFAULTS);
177 assert_eq!(
178 resolve(false, false, false, DEFAULTS),
179 Resolved { background: false, tray: true }
180 );
181 }
182
183 #[test]
184 fn each_key_is_read_independently() {
185 assert_eq!(
186 read("bg-on", r#"{"app:runinbackground": true}"#),
187 Settings { run_in_background: true, show_tray: true }
188 );
189 assert_eq!(
190 read("tray-off", r#"{"app:showtray": false}"#),
191 Settings { run_in_background: false, show_tray: false }
192 );
193 }
194
195 #[test]
196 fn a_bad_file_or_wrong_type_keeps_the_defaults() {
197 assert_eq!(read("bad", "not json {"), DEFAULTS);
198 assert_eq!(read("str", r#"{"app:runinbackground": "true", "app:showtray": "false"}"#), DEFAULTS);
200 }
201
202 #[test]
203 fn background_is_opt_in_from_any_single_source() {
204 assert!(resolve(true, false, false, DEFAULTS).background, "env override");
205 assert!(resolve(false, false, true, DEFAULTS).background, "--background");
206 let on = Settings { run_in_background: true, ..DEFAULTS };
207 assert!(resolve(false, false, false, on).background, "setting");
208 assert!(!resolve(false, true, false, DEFAULTS).background, "the tray alone never implies background");
209 }
210
211 #[test]
213 fn background_forces_the_tray_even_when_hidden_in_settings() {
214 let hidden = Settings { run_in_background: false, show_tray: false };
215 assert_eq!(resolve(false, false, false, hidden), Resolved { background: false, tray: false });
216 assert_eq!(resolve(false, false, true, hidden), Resolved { background: true, tray: true });
217 let both = Settings { run_in_background: true, show_tray: false };
218 assert_eq!(resolve(false, false, false, both), Resolved { background: true, tray: true });
219 }
220
221 #[test]
224 fn background_flag_reaches_launcher_tray_gate() {
225 let args = vec!["agentmux".to_string(), crate::autostart::BACKGROUND_FLAG.to_string()];
226 let prev_bg = std::env::var_os(ENV_BACKGROUND);
229 let prev_tray = std::env::var_os(ENV_TRAY);
230 std::env::remove_var(ENV_BACKGROUND);
231 std::env::remove_var(ENV_TRAY);
232
233 assert!(apply(&args).background);
234 assert!(crate::tray::should_enable(
235 crate::tray::tray_opt_in_from_env(),
236 crate::tray::background_service_from_env()
237 ));
238
239 match prev_bg {
240 Some(v) => std::env::set_var(ENV_BACKGROUND, v),
241 None => std::env::remove_var(ENV_BACKGROUND),
242 }
243 match prev_tray {
244 Some(v) => std::env::set_var(ENV_TRAY, v),
245 None => std::env::remove_var(ENV_TRAY),
246 }
247 }
248}