Skip to main content

lib/mprocs/
config.rs

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}