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 settings.merge_from_xdg().map_err(|e| {
90 anyhow::Error::msg(format!("[{}] {}", "global settings", e))
91 })?;
92 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 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}