mprocs/
lib.rs

1mod app;
2mod client;
3mod clipboard;
4mod config;
5mod ctl;
6mod encode_term;
7mod error;
8mod event;
9mod key;
10mod keymap;
11mod package_json;
12mod proc;
13mod protocol;
14mod settings;
15mod state;
16mod theme;
17mod ui_add_proc;
18mod ui_confirm_quit;
19mod ui_keymap;
20mod ui_procs;
21mod ui_remove_proc;
22mod ui_term;
23mod ui_zoom_tip;
24mod yaml_val;
25
26use std::{io::Read, path::Path};
27use std::error::Error;
28
29use anyhow::{bail, Result};
30use app::server_main;
31use clap::{arg, command, ArgMatches};
32use client::client_main;
33use config::{CmdConfig, Config, ConfigContext, ProcConfig, ServerConfig};
34use ctl::run_ctl;
35use flexi_logger::FileSpec;
36use keymap::Keymap;
37use package_json::load_npm_procs;
38use proc::StopSignal;
39use protocol::{CltToSrv, SrvToClt};
40use serde_yaml::Value;
41use settings::Settings;
42use yaml_val::Val;
43
44pub async fn run_mprocs(yaml_path: &str) -> anyhow::Result<()> {
45    let config_value = Some((
46            read_value(&yaml_path)?,
47            ConfigContext { path: yaml_path.into() },
48        ));
49
50    let mut settings = Settings::default();
51
52    if let Some((value, _)) = &config_value {
53        settings
54            .merge_value(Val::new(value)?)
55            .map_err(|e| anyhow::Error::msg(format!("[{}] {}", "local config", e)))?;
56    }
57
58
59
60    let mut keymap = Keymap::new();
61    settings.add_to_keymap(&mut keymap).unwrap();
62
63
64    let mut config = if let Some((v, ctx)) = config_value {
65        Config::from_value(&v, &ctx, &settings)?
66    } else {
67        Config::make_default(&settings)
68    };
69
70
71    run_client_and_server(config, keymap).await
72}
73pub async fn run_app() -> anyhow::Result<()> {
74    let matches = command!()
75        .arg(arg!(-c --config [PATH] "Config path [default: mprocs.yaml]"))
76        .arg(arg!(-s --server [PATH] "Remote control server address. Example: 127.0.0.1:4050."))
77        .arg(arg!(--ctl [YAML] "Send yaml/json encoded command to running mprocs"))
78        .arg(arg!(--names [NAMES] "Names for processes provided by cli arguments. Separated by comma."))
79        .arg(arg!(--npm "Run scripts from package.json. Scripts are not started by default."))
80        .arg(arg!([COMMANDS]... "Commands to run (if omitted, commands from config will be run)"))
81        .get_matches();
82
83    let config_value = load_config_value(&matches)
84        .map_err(|e| anyhow::Error::msg(format!("[{}] {}", "config", e)))?;
85
86    let mut settings = Settings::default();
87
88    // merge ~/.config/mprocs/mprocs.yaml
89    settings.merge_from_xdg().map_err(|e| {
90        anyhow::Error::msg(format!("[{}] {}", "global settings", e))
91    })?;
92    // merge ./mprocs.yaml
93    if let Some((value, _)) = &config_value {
94        settings
95            .merge_value(Val::new(value)?)
96            .map_err(|e| anyhow::Error::msg(format!("[{}] {}", "local config", e)))?;
97    }
98
99    let mut keymap = Keymap::new();
100    settings.add_to_keymap(&mut keymap)?;
101
102    let config = {
103        let mut config = if let Some((v, ctx)) = config_value {
104            Config::from_value(&v, &ctx, &settings)?
105        } else {
106            Config::make_default(&settings)
107        };
108
109        if let Some(server_addr) = matches.value_of("server") {
110            config.server = Some(ServerConfig::from_str(server_addr)?);
111        }
112
113        if matches.occurrences_of("ctl") > 0 {
114            return run_ctl(matches.value_of("ctl").unwrap(), &config).await;
115        }
116
117        if let Some(cmds) = matches.values_of("COMMANDS") {
118            let names = matches
119                .value_of("names")
120                .map_or_else(|| Vec::new(), |arg| arg.split(",").collect::<Vec<_>>());
121            let procs = cmds
122                .into_iter()
123                .enumerate()
124                .map(|(i, cmd)| ProcConfig {
125                    name: names
126                        .get(i)
127                        .map_or_else(|| cmd.to_string(), |s| s.to_string()),
128                    cmd: CmdConfig::Shell {
129                        shell: cmd.to_owned(),
130                    },
131                    env: None,
132                    cwd: None,
133                    autostart: true,
134                    stop: StopSignal::default(),
135                })
136                .collect::<Vec<_>>();
137
138            config.procs = procs;
139        } else if matches.is_present("npm") {
140            let procs = load_npm_procs()?;
141            config.procs = procs;
142        }
143
144        config
145    };
146
147    run_client_and_server(config, keymap).await
148}
149
150async fn run_client_and_server(config: Config, keymap: Keymap) -> Result<()> {
151    let (clt_tx, srv_rx) = tokio::sync::mpsc::channel::<CltToSrv>(64);
152    let (srv_tx, clt_rx) = tokio::sync::mpsc::unbounded_channel::<SrvToClt>();
153
154    let client = tokio::spawn(async { client_main(clt_tx, clt_rx).await });
155    let server =
156        tokio::spawn(async { server_main(config, keymap, srv_tx, srv_rx).await });
157
158    let r1 = server
159        .await
160        .unwrap_or_else(|err| Err(anyhow::Error::from(err)));
161    let r2 = client
162        .await
163        .unwrap_or_else(|err| Err(anyhow::Error::from(err)));
164
165    r1.and(r2)
166        .map_err(|err| anyhow::Error::msg(err.to_string()))
167}
168
169fn load_config_value(
170    matches: &ArgMatches,
171) -> Result<Option<(Value, ConfigContext)>> {
172    if let Some(path) = matches.value_of("config") {
173        return Ok(Some((
174            read_value(path)?,
175            ConfigContext { path: path.into() },
176        )));
177    }
178
179
180    {
181        let path = "mprocs.yaml";
182        if Path::new(path).is_file() {
183            return Ok(Some((
184                read_value(path)?,
185                ConfigContext { path: path.into() },
186            )));
187        }
188    }
189
190    {
191        let path = "mprocs.json";
192        if Path::new(path).is_file() {
193            return Ok(Some((
194                read_value(path)?,
195                ConfigContext { path: path.into() },
196            )));
197        }
198    }
199
200    Ok(None)
201}
202
203fn read_value(path: &str) -> Result<Value> {
204    // Open the file in read-only mode with buffer.
205    let file = match std::fs::File::open(&path) {
206        Ok(file) => file,
207        Err(err) => match err.kind() {
208            std::io::ErrorKind::NotFound => {
209                bail!("Config file '{}' not found.", path);
210            }
211            _kind => return Err(err.into()),
212        },
213    };
214    let mut reader = std::io::BufReader::new(file);
215    let ext = std::path::Path::new(path)
216        .extension()
217        .map_or_else(|| "".to_string(), |ext| ext.to_string_lossy().to_string());
218    let value: Value = match ext.as_str() {
219        "yaml" | "yml" => serde_yaml::from_reader(reader)?,
220        _ => bail!("Supported config extensions: yaml, yml."),
221    };
222    Ok(value)
223}