mod app;
mod client;
mod clipboard;
mod config;
mod ctl;
mod encode_term;
mod error;
mod event;
mod key;
mod keymap;
mod package_json;
mod proc;
mod protocol;
mod settings;
mod state;
mod theme;
mod ui_add_proc;
mod ui_confirm_quit;
mod ui_keymap;
mod ui_procs;
mod ui_remove_proc;
mod ui_term;
mod ui_zoom_tip;
mod yaml_val;
use std::{io::Read, path::Path};
use std::error::Error;
use anyhow::{bail, Result};
use app::server_main;
use clap::{arg, command, ArgMatches};
use client::client_main;
use config::{CmdConfig, Config, ConfigContext, ProcConfig, ServerConfig};
use ctl::run_ctl;
use flexi_logger::FileSpec;
use keymap::Keymap;
use package_json::load_npm_procs;
use proc::StopSignal;
use protocol::{CltToSrv, SrvToClt};
use serde_yaml::Value;
use settings::Settings;
use yaml_val::Val;
pub async fn run_mprocs(yaml_path: &str) -> anyhow::Result<()> {
let config_value = Some((
read_value(&yaml_path)?,
ConfigContext { path: yaml_path.into() },
));
let mut settings = Settings::default();
if let Some((value, _)) = &config_value {
settings
.merge_value(Val::new(value)?)
.map_err(|e| anyhow::Error::msg(format!("[{}] {}", "local config", e)))?;
}
let mut keymap = Keymap::new();
settings.add_to_keymap(&mut keymap).unwrap();
let mut config = if let Some((v, ctx)) = config_value {
Config::from_value(&v, &ctx, &settings)?
} else {
Config::make_default(&settings)
};
run_client_and_server(config, keymap).await
}
pub async fn run_app() -> anyhow::Result<()> {
let matches = command!()
.arg(arg!(-c --config [PATH] "Config path [default: mprocs.yaml]"))
.arg(arg!(-s --server [PATH] "Remote control server address. Example: 127.0.0.1:4050."))
.arg(arg!(--ctl [YAML] "Send yaml/json encoded command to running mprocs"))
.arg(arg!(--names [NAMES] "Names for processes provided by cli arguments. Separated by comma."))
.arg(arg!(--npm "Run scripts from package.json. Scripts are not started by default."))
.arg(arg!([COMMANDS]... "Commands to run (if omitted, commands from config will be run)"))
.get_matches();
let config_value = load_config_value(&matches)
.map_err(|e| anyhow::Error::msg(format!("[{}] {}", "config", e)))?;
let mut settings = Settings::default();
settings.merge_from_xdg().map_err(|e| {
anyhow::Error::msg(format!("[{}] {}", "global settings", e))
})?;
if let Some((value, _)) = &config_value {
settings
.merge_value(Val::new(value)?)
.map_err(|e| anyhow::Error::msg(format!("[{}] {}", "local config", e)))?;
}
let mut keymap = Keymap::new();
settings.add_to_keymap(&mut keymap)?;
let config = {
let mut config = if let Some((v, ctx)) = config_value {
Config::from_value(&v, &ctx, &settings)?
} else {
Config::make_default(&settings)
};
if let Some(server_addr) = matches.value_of("server") {
config.server = Some(ServerConfig::from_str(server_addr)?);
}
if matches.occurrences_of("ctl") > 0 {
return run_ctl(matches.value_of("ctl").unwrap(), &config).await;
}
if let Some(cmds) = matches.values_of("COMMANDS") {
let names = matches
.value_of("names")
.map_or_else(|| Vec::new(), |arg| arg.split(",").collect::<Vec<_>>());
let procs = cmds
.into_iter()
.enumerate()
.map(|(i, cmd)| ProcConfig {
name: names
.get(i)
.map_or_else(|| cmd.to_string(), |s| s.to_string()),
cmd: CmdConfig::Shell {
shell: cmd.to_owned(),
},
env: None,
cwd: None,
autostart: true,
stop: StopSignal::default(),
})
.collect::<Vec<_>>();
config.procs = procs;
} else if matches.is_present("npm") {
let procs = load_npm_procs()?;
config.procs = procs;
}
config
};
run_client_and_server(config, keymap).await
}
async fn run_client_and_server(config: Config, keymap: Keymap) -> Result<()> {
let (clt_tx, srv_rx) = tokio::sync::mpsc::channel::<CltToSrv>(64);
let (srv_tx, clt_rx) = tokio::sync::mpsc::unbounded_channel::<SrvToClt>();
let client = tokio::spawn(async { client_main(clt_tx, clt_rx).await });
let server =
tokio::spawn(async { server_main(config, keymap, srv_tx, srv_rx).await });
let r1 = server
.await
.unwrap_or_else(|err| Err(anyhow::Error::from(err)));
let r2 = client
.await
.unwrap_or_else(|err| Err(anyhow::Error::from(err)));
r1.and(r2)
.map_err(|err| anyhow::Error::msg(err.to_string()))
}
fn load_config_value(
matches: &ArgMatches,
) -> Result<Option<(Value, ConfigContext)>> {
if let Some(path) = matches.value_of("config") {
return Ok(Some((
read_value(path)?,
ConfigContext { path: path.into() },
)));
}
{
let path = "mprocs.yaml";
if Path::new(path).is_file() {
return Ok(Some((
read_value(path)?,
ConfigContext { path: path.into() },
)));
}
}
{
let path = "mprocs.json";
if Path::new(path).is_file() {
return Ok(Some((
read_value(path)?,
ConfigContext { path: path.into() },
)));
}
}
Ok(None)
}
fn read_value(path: &str) -> Result<Value> {
let file = match std::fs::File::open(&path) {
Ok(file) => file,
Err(err) => match err.kind() {
std::io::ErrorKind::NotFound => {
bail!("Config file '{}' not found.", path);
}
_kind => return Err(err.into()),
},
};
let mut reader = std::io::BufReader::new(file);
let ext = std::path::Path::new(path)
.extension()
.map_or_else(|| "".to_string(), |ext| ext.to_string_lossy().to_string());
let value: Value = match ext.as_str() {
"yaml" | "yml" => serde_yaml::from_reader(reader)?,
_ => bail!("Supported config extensions: yaml, yml."),
};
Ok(value)
}