#![allow(unused)]
use anyhow::Context;
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
use prox::{Prox, ProxEvent, ProxSignal, error};
use std::{path::PathBuf, sync::mpsc, thread::spawn, time::Duration};
#[derive(Parser)]
#[command(version, about = "A process manager for development environments", long_about = None)]
struct ProxCli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(short, long, global = true)]
config: Option<PathBuf>,
}
#[derive(Subcommand, Default)]
enum Commands {
#[default]
Start,
Init {
#[arg(short, long, value_enum, default_value = "toml")]
format: ConfigFormat,
path: Option<PathBuf>,
},
Check {
path: Option<PathBuf>,
},
Show {
path: Option<PathBuf>,
},
}
#[derive(Clone, ValueEnum)]
enum ConfigFormat {
Toml,
Yaml,
Json,
}
fn main() -> anyhow::Result<()> {
let args = ProxCli::parse();
match args.command.unwrap_or_default() {
Commands::Start => {
let config_path = find_config_file(args.config)?;
let mut prox = load_config(&config_path)?;
let (signal_tx, signal_rx) = mpsc::channel::<ProxSignal>();
prox.signal_rx = Some(signal_rx);
prox.config.handle_control_c = false;
let event_rx = prox.setup_event_rx()?;
let status_refs = prox.status_refs.clone();
spawn(move || {
for event in event_rx {
println!("EVENT: {event:?}");
match event {
ProxEvent::Idle => {
println!("Statuses: {status_refs:?}");
println!("Key controls: 's' = Start, 'r' = Restart, 'q' = Shutdown");
}
_ => {}
}
}
});
let tx_clone = signal_tx.clone();
ctrlc::set_handler(move || {
tx_clone.send(ProxSignal::Shutdown).ok();
})
.context("set Control-C handler")?;
spawn(move || {
use std::io::{self, Read};
use termios::{ECHO, ICANON, TCSANOW, Termios, tcgetattr, tcsetattr};
let stdin_fd = 0;
let mut orig_termios =
Termios::from_fd(stdin_fd).expect("Failed to get terminal attributes");
let mut new_termios = orig_termios.clone();
new_termios.c_lflag &= !(ICANON | ECHO);
new_termios.c_cc[termios::VMIN] = 1;
new_termios.c_cc[termios::VTIME] = 0;
if tcsetattr(stdin_fd, TCSANOW, &new_termios).is_err() {
eprintln!("Failed to set terminal attributes");
return;
}
let _cleanup = scopeguard::guard((), move |_| {
let _ = tcsetattr(stdin_fd, TCSANOW, &orig_termios);
});
let mut stdin = io::stdin();
let mut buffer = [0u8; 1];
loop {
match stdin.read(&mut buffer) {
Ok(1) => {
let ch = buffer[0] as char;
let signal = ProxSignal::try_from(ch).ok();
if let Some(sig) = signal {
if signal_tx.send(sig).is_err() {
error!("Failed to send signal, exiting key listener");
break;
}
}
}
Ok(_) => continue,
Err(_) => break,
}
}
});
println!("Starting prox from: {}", config_path.display());
prox.start()?;
}
Commands::Init { format, path } => {
init_config(format, path)?;
}
Commands::Check { path } => {
let config_path = find_config_file(path)?;
check_config(&config_path)?;
}
Commands::Show { path } => {
let config_path = find_config_file(path)?;
show_config(&config_path)?;
}
}
Ok(())
}
fn load_config(path: &PathBuf) -> anyhow::Result<Prox> {
match path.extension().and_then(|ext| ext.to_str()) {
Some("toml") => Prox::load_toml(path),
Some("yaml") | Some("yml") => Prox::load_yaml(path),
Some("json") => Prox::load_json(path),
_ => anyhow::bail!("Unsupported config file format. Use .toml, .yaml/.yml, or .json"),
}
}
fn init_config(format: ConfigFormat, path: Option<PathBuf>) -> anyhow::Result<()> {
let file_path = path.unwrap_or_else(|| {
PathBuf::from(match format {
ConfigFormat::Toml => "prox.toml",
ConfigFormat::Yaml => "prox.yaml",
ConfigFormat::Json => "prox.json",
})
});
if file_path.exists() {
anyhow::bail!("Configuration file already exists: {}", file_path.display());
}
let template_content = match format {
ConfigFormat::Toml => include_str!("../templates/prox.toml"),
ConfigFormat::Yaml => include_str!("../templates/prox.yaml"),
ConfigFormat::Json => include_str!("../templates/prox.json"),
};
std::fs::write(&file_path, template_content)
.context(format!("write template file to: {}", file_path.display()))?;
println!("Created configuration file: {}", file_path.display());
Ok(())
}
fn check_config(path: &PathBuf) -> anyhow::Result<()> {
println!("Checking configuration file: {}", path.display());
let prox = load_config(path)?;
println!("✓ Configuration file is valid");
println!("✓ Found {} process(es)", prox.procs().len());
for proc in prox.procs() {
println!(
" - {}: {} {}",
proc.name,
proc.command.to_string_lossy(),
proc.args
.iter()
.map(|a| a.to_string_lossy())
.collect::<Vec<_>>()
.join(" ")
);
}
Ok(())
}
fn show_config(path: &PathBuf) -> anyhow::Result<()> {
println!("Configuration from: {}", path.display());
println!();
let prox = load_config(path)?;
println!("{prox:#?}");
Ok(())
}
fn find_config_file(explicit_path: Option<PathBuf>) -> anyhow::Result<PathBuf> {
if let Some(path) = explicit_path {
if path.exists() {
return Ok(path);
} else {
anyhow::bail!("Config file not found: {path:?}");
}
}
let mut current_dir = std::env::current_dir().context("get current directory")?;
loop {
let mut found_configs = Vec::new();
let config_names = ["prox.toml", "prox.yaml", "prox.yml", "prox.json"];
for name in &config_names {
let path = current_dir.join(name);
if path.exists() {
found_configs.push(path);
}
}
match found_configs.len() {
0 => {
if let Some(parent) = current_dir.parent() {
current_dir = parent.to_path_buf();
continue;
} else {
eprintln!(
"\nNo prox.(toml|json|yaml) config file found. Please create one with `init`, or use `--config`.\n"
);
ProxCli::command().print_help().ok();
std::process::exit(1);
}
}
1 => {
return Ok(found_configs.into_iter().next().unwrap());
}
_ => {
let config_list = found_configs
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy())
.collect::<Vec<_>>()
.join(", ");
anyhow::bail!(
"Ambiguous config files found in {}: {config_list}. Please use --config to specify which one to use.",
current_dir.display()
);
}
}
}
}