1use std::{ffi::OsString, path::PathBuf, str::FromStr};
2
3use anyhow::{Result, bail};
4use indexmap::IndexMap;
5use serde::{Deserialize, Serialize};
6use serde_yaml::Value;
7
8use crate::mprocs::{
9 event::AppEvent,
10 proc::StopSignal,
11 proc_log_config::LogConfig,
12 settings::Settings,
13 yaml_val::{Val, value_to_string},
14};
15use crate::process::process_spec::ProcessSpec;
16
17pub struct ConfigContext {
18 pub path: PathBuf,
19}
20
21fn resolve_config_path(path: &str, ctx: &ConfigContext) -> Result<PathBuf> {
22 let mut buf = PathBuf::new();
23 if let Some(rest) = path.strip_prefix("<CONFIG_DIR>") {
24 if let Some(parent) = dunce::canonicalize(&ctx.path)?.parent() {
25 buf.push(parent);
26 }
27 buf.push(rest.trim_start_matches(['/', '\\']));
28 } else {
29 buf.push(path);
30 }
31
32 Ok(buf)
33}
34
35pub struct Config {
36 pub procs: Vec<ProcConfig>,
37 pub server: Option<ServerConfig>,
38 pub exec: Option<AppEvent>,
39 pub hide_keymap_window: bool,
40 pub mouse_scroll_speed: usize,
41 pub scrollback_len: usize,
42 pub proc_list_width: usize,
43 pub proc_list_title: String,
44 pub on_init: Option<AppEvent>,
45 pub on_all_finished: Option<AppEvent>,
46 pub proc_log: Option<LogConfig>,
47}
48
49impl Config {
50 pub fn from_value(
51 value: &Value,
52 ctx: &ConfigContext,
53 settings: &Settings,
54 ) -> Result<Config> {
55 let config = Val::new(value)?;
56 let config = config.as_object()?;
57
58 let procs = if let Some(procs) = config.get(&Value::from("procs")) {
59 let procs = procs
60 .as_object()?
61 .into_iter()
62 .map(|(name, proc)| {
63 ProcConfig::from_val(
64 value_to_string(&name)?,
65 settings.mouse_scroll_speed,
66 settings.scrollback_len,
67 proc,
68 ctx,
69 )
70 })
71 .collect::<Result<Vec<_>>>()?
72 .into_iter()
73 .flatten()
74 .collect::<Vec<_>>();
75 procs
76 } else {
77 Vec::new()
78 };
79
80 let server = if let Some(addr) = config.get(&Value::from("server")) {
81 Some(ServerConfig::from_str(addr.as_str()?)?)
82 } else {
83 None
84 };
85
86 let proc_list_title =
87 if let Some(title) = config.get(&Value::from("proc_list_title")) {
88 title.as_str()?.to_string()
89 } else {
90 settings.proc_list_title.clone()
91 };
92
93 let on_all_finished =
94 if let Some(val) = config.get(&Value::from("on_all_finished")) {
95 Some(serde_yaml::from_value(val.raw().clone())?)
96 } else {
97 settings.on_all_finished.clone()
98 };
99
100 let proc_log = {
101 match config.get(&Value::from("proc_log")) {
102 Some(val) => {
103 crate::mprocs::proc_log_config::parse_log_config(val, |path| {
104 resolve_config_path(path, ctx)
105 })?
106 }
107 None => settings.proc_log.clone(),
108 }
109 };
110
111 let config = Config {
112 procs,
113 server,
114 exec: None,
115 hide_keymap_window: settings.hide_keymap_window,
116 mouse_scroll_speed: settings.mouse_scroll_speed,
117 scrollback_len: settings.scrollback_len,
118 proc_list_width: settings.proc_list_width,
119 proc_list_title,
120 on_init: None,
121 on_all_finished,
122 proc_log,
123 };
124
125 Ok(config)
126 }
127
128 pub fn make_default(settings: &Settings) -> anyhow::Result<Self> {
129 Ok(Self {
130 procs: Vec::new(),
131 server: None,
132 exec: None,
133 hide_keymap_window: settings.hide_keymap_window,
134 mouse_scroll_speed: settings.mouse_scroll_speed,
135 scrollback_len: settings.scrollback_len,
136 proc_list_width: settings.proc_list_width,
137 proc_list_title: settings.proc_list_title.clone(),
138 on_init: None,
139 on_all_finished: settings.on_all_finished.clone(),
140 proc_log: settings.proc_log.clone(),
141 })
142 }
143}
144
145#[derive(Clone)]
146pub struct ProcConfig {
147 pub name: String,
148 pub cmd: CmdConfig,
149 pub cwd: Option<OsString>,
150 pub env: Option<IndexMap<String, Option<String>>>,
151 pub autostart: bool,
152 pub autorestart: bool,
153
154 pub stop: StopSignal,
155
156 pub deps: Vec<String>,
157
158 pub mouse_scroll_speed: usize,
159 pub scrollback_len: usize,
160 pub log: Option<LogConfig>,
161}
162
163impl ProcConfig {
164 fn from_val(
165 name: String,
166 mouse_scroll_speed: usize,
167 scrollback_len: usize,
168 val: Val,
169 ctx: &ConfigContext,
170 ) -> Result<Option<ProcConfig>> {
171 match val.raw() {
172 Value::Null => Ok(None),
173 Value::Bool(_) => todo!(),
174 Value::Number(_) => todo!(),
175 Value::String(shell) => Ok(Some(ProcConfig {
176 name,
177 cmd: CmdConfig::Shell {
178 shell: shell.to_owned(),
179 },
180 cwd: None,
181 env: None,
182 autostart: true,
183 autorestart: false,
184 stop: StopSignal::default(),
185 deps: Vec::new(),
186
187 mouse_scroll_speed,
188 scrollback_len,
189 log: None,
190 })),
191 Value::Sequence(_) => {
192 let cmd = val.as_array()?;
193 let cmd = cmd
194 .into_iter()
195 .map(|item| item.as_str().map(|s| s.to_owned()))
196 .collect::<Result<Vec<_>>>()?;
197
198 Ok(Some(ProcConfig {
199 name,
200 cmd: CmdConfig::Cmd { cmd },
201 cwd: None,
202 env: None,
203 autostart: true,
204 autorestart: false,
205 stop: StopSignal::default(),
206 deps: Vec::new(),
207 mouse_scroll_speed,
208 scrollback_len,
209 log: None,
210 }))
211 }
212 Value::Mapping(_) => {
213 let map = val.as_object()?;
214
215 let cmd = {
216 let shell = map.get(&Value::from("shell"));
217 let cmd = map.get(&Value::from("cmd"));
218
219 match (shell, cmd) {
220 (None, Some(cmd)) => CmdConfig::Cmd {
221 cmd: cmd
222 .as_array()?
223 .into_iter()
224 .map(|v| v.as_str().map(|s| s.to_owned()))
225 .collect::<Result<Vec<_>>>()?,
226 },
227 (Some(shell), None) => CmdConfig::Shell {
228 shell: shell.as_str()?.to_owned(),
229 },
230 (None, None) => todo!(),
231 (Some(_), Some(_)) => todo!(),
232 }
233 };
234
235 let cwd = match map.get(&Value::from("cwd")) {
236 Some(cwd) => {
237 let cwd = cwd.as_str()?;
238 let mut buf = OsString::new();
239 if let Some(rest) = cwd.strip_prefix("<CONFIG_DIR>") {
240 if let Some(parent) = dunce::canonicalize(&ctx.path)?.parent() {
241 buf.push(parent);
242 }
243 buf.push(rest);
244 } else {
245 buf.push(cwd);
246 }
247 Some(buf)
248 }
249 None => None,
250 };
251
252 let log = {
253 match map.get(&Value::from("log")) {
254 Some(val) => {
255 crate::mprocs::proc_log_config::parse_log_config(val, |path| {
256 resolve_config_path(path, ctx)
257 })?
258 }
259 None => None,
260 }
261 };
262
263 let env = match map.get(&Value::from("env")) {
264 Some(env) => {
265 let env = env.as_object()?;
266 let env = env
267 .into_iter()
268 .map(|(k, v)| {
269 let v = match v.raw() {
270 Value::Null => Ok(None),
271 Value::String(v) => Ok(Some(v.to_owned())),
272 _ => Err(v.error_at("Expected string or null")),
273 };
274 Ok((value_to_string(&k)?, v?))
275 })
276 .collect::<Result<IndexMap<_, _>>>()?;
277 Some(env)
278 }
279 None => None,
280 };
281 let env = match map.get(&Value::from("add_path")) {
282 Some(add_path) => {
283 let extra_paths = match add_path.raw() {
284 Value::String(path) => vec![path.as_str()],
285 Value::Sequence(paths) => paths
286 .iter()
287 .filter_map(|path| path.as_str())
288 .collect::<Vec<_>>(),
289 _ => {
290 bail!(add_path.error_at("Expected string or array"));
291 }
292 };
293 let extra_paths = extra_paths
294 .into_iter()
295 .map(|p| PathBuf::from_str(p).map_err(anyhow::Error::from))
296 .collect::<Result<Vec<_>>>()?;
297 let mut paths = std::env::var_os("PATH").map_or_else(
298 || Vec::new(),
299 |path_var| {
300 std::env::split_paths(&path_var)
301 .map(|p| p.into_os_string())
302 .collect::<Vec<_>>()
303 },
304 );
305 for p in extra_paths {
306 paths.push(p.into_os_string());
307 }
308 let path_var =
309 std::env::join_paths(paths)?.to_string_lossy().to_string();
310 let env = if let Some(mut env) = env {
311 env.insert("PATH".to_string(), Some(path_var));
312 env
313 } else {
314 let mut env = IndexMap::with_capacity(1);
315 env.insert("PATH".to_string(), Some(path_var));
316 env
317 };
318 Some(env)
319 }
320 None => env,
321 };
322
323 let autostart = map
324 .get(&Value::from("autostart"))
325 .map_or(Ok(true), |v| v.as_bool())?;
326
327 let autorestart = map
328 .get(&Value::from("autorestart"))
329 .map_or(Ok(false), |v| v.as_bool())?;
330
331 let stop_signal = if let Some(val) = map.get(&Value::from("stop")) {
332 StopSignal::from_val(val)?
333 } else {
334 StopSignal::default()
335 };
336
337 let deps = if let Some(deps) = map.get(&Value::from("deps")) {
338 deps
339 .as_array()?
340 .iter()
341 .map(|d| d.as_str().map(|s| s.to_string()))
342 .collect::<Result<Vec<_>>>()?
343 } else {
344 Vec::new()
345 };
346
347 Ok(Some(ProcConfig {
348 name,
349 cmd,
350 cwd,
351 env,
352 autostart,
353 autorestart,
354 stop: stop_signal,
355 deps,
356 mouse_scroll_speed,
357 scrollback_len,
358 log,
359 }))
360 }
361 Value::Tagged(_) => anyhow::bail!("Yaml tags are not supported"),
362 }
363 }
364}
365
366pub enum ServerConfig {
367 Tcp(String),
368}
369
370impl ServerConfig {
371 pub fn from_str(server_addr: &str) -> Result<Self> {
372 Ok(Self::Tcp(server_addr.to_string()))
373 }
374}
375
376#[derive(Clone, Deserialize, Serialize)]
377#[serde(untagged)]
378pub enum CmdConfig {
379 Cmd { cmd: Vec<String> },
380 Shell { shell: String },
381}
382
383impl From<&ProcConfig> for ProcessSpec {
384 fn from(cfg: &ProcConfig) -> Self {
385 let mut cmd = match &cfg.cmd {
386 CmdConfig::Cmd { cmd } => ProcessSpec::from_argv(cmd.clone()),
387 CmdConfig::Shell { shell } => cmd_from_shell(shell),
388 };
389
390 if let Some(env) = &cfg.env {
391 for (k, v) in env {
392 if let Some(v) = v {
393 cmd.env(k, v);
394 } else {
395 cmd.env_remove(k);
396 }
397 }
398 }
399
400 if let Some(cwd) = &cfg.cwd {
401 cmd.cwd(cwd.to_string_lossy());
402 } else if let Ok(cwd) = std::env::current_dir() {
403 cmd.cwd(cwd.to_string_lossy());
404 }
405
406 cmd
407 }
408}
409
410#[cfg(windows)]
411pub fn cmd_from_shell(shell: &str) -> ProcessSpec {
412 ProcessSpec::from_argv(vec![
413 "pwsh.exe".into(),
414 "-Command".into(),
415 shell.into(),
416 ])
417}
418
419#[cfg(not(windows))]
420pub fn cmd_from_shell(shell: &str) -> ProcessSpec {
421 ProcessSpec::from_argv(vec!["/bin/sh".into(), "-c".into(), shell.into()])
422}