pub mod install;
pub mod plugin;
use crate::config::ShellType;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum ShellTypeArg {
Bash,
Zsh,
Fish,
}
impl From<ShellTypeArg> for ShellType {
fn from(arg: ShellTypeArg) -> Self {
match arg {
ShellTypeArg::Bash => ShellType::Bash,
ShellTypeArg::Zsh => ShellType::Zsh,
ShellTypeArg::Fish => ShellType::Fish,
}
}
}
#[derive(Parser)]
#[command(name = "par-term")]
#[command(author, version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(long, value_name = "SHADER")]
pub shader: Option<String>,
#[arg(long, value_name = "SECONDS")]
pub exit_after: Option<f64>,
#[arg(long, value_name = "PATH", num_args = 0..=1, default_missing_value = "")]
pub screenshot: Option<PathBuf>,
#[arg(long, value_name = "COMMAND")]
pub command_to_send: Option<String>,
#[arg(long, value_name = "SCRIPT")]
pub ui_test: Option<PathBuf>,
#[arg(long, value_name = "PATH", requires = "ui_test")]
pub ui_test_report: Option<PathBuf>,
#[arg(long)]
pub log_session: bool,
#[arg(long, value_name = "SESSION")]
pub attach: Option<String>,
#[arg(long, value_enum, value_name = "LEVEL")]
pub log_level: Option<LogLevelArg>,
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum LogLevelArg {
Off,
Error,
Warn,
Info,
Debug,
Trace,
}
impl LogLevelArg {
pub fn to_level_filter(self) -> log::LevelFilter {
match self {
LogLevelArg::Off => log::LevelFilter::Off,
LogLevelArg::Error => log::LevelFilter::Error,
LogLevelArg::Warn => log::LevelFilter::Warn,
LogLevelArg::Info => log::LevelFilter::Info,
LogLevelArg::Debug => log::LevelFilter::Debug,
LogLevelArg::Trace => log::LevelFilter::Trace,
}
}
}
#[derive(Subcommand)]
pub enum Commands {
InstallShaders {
#[arg(short = 'y', long)]
yes: bool,
#[arg(short, long)]
force: bool,
},
InstallShellIntegration {
#[arg(long, value_enum)]
shell: Option<ShellTypeArg>,
},
UninstallShellIntegration,
UninstallShaders {
#[arg(short, long)]
force: bool,
},
InstallMuxExtensions {
#[arg(short = 'y', long)]
yes: bool,
},
UninstallMuxExtensions,
InstallMuxHooks {
#[arg(short = 'y', long)]
yes: bool,
},
UninstallMuxHooks,
ShaderLint {
path: PathBuf,
#[arg(long)]
readability: bool,
#[arg(long)]
apply: bool,
#[arg(long)]
no_prompt: bool,
},
InstallIntegrations {
#[arg(short = 'y', long)]
yes: bool,
},
SelfUpdate {
#[arg(short = 'y', long)]
yes: bool,
},
Plugin {
#[command(subcommand)]
command: plugin::PluginCommands,
},
McpServer,
#[command(external_subcommand)]
External(Vec<String>),
}
#[derive(Clone, Debug, Default)]
pub struct RuntimeOptions {
pub shader: Option<String>,
pub exit_after: Option<f64>,
pub screenshot: Option<PathBuf>,
pub command_to_send: Option<String>,
pub ui_test: Option<PathBuf>,
pub ui_test_report: Option<PathBuf>,
pub log_session: bool,
pub log_level: Option<log::LevelFilter>,
pub attach: Option<String>,
}
pub enum CliResult {
Continue(RuntimeOptions),
Exit(i32),
}
pub fn process_cli() -> CliResult {
use install::{
install_integrations_cli, install_mux_extensions_cli, install_mux_hooks_cli,
install_shaders_cli, install_shell_integration_cli, self_update_cli,
uninstall_mux_extensions_cli, uninstall_mux_hooks_cli, uninstall_shaders_cli,
uninstall_shell_integration_cli,
};
let cli = Cli::parse();
match cli.command {
Some(Commands::InstallShaders { yes, force }) => {
let result = install_shaders_cli(yes || force);
CliResult::Exit(if result.is_ok() { 0 } else { 1 })
}
Some(Commands::InstallShellIntegration { shell }) => {
let result = install_shell_integration_cli(shell.map(Into::into));
CliResult::Exit(if result.is_ok() { 0 } else { 1 })
}
Some(Commands::UninstallShellIntegration) => {
let result = uninstall_shell_integration_cli();
CliResult::Exit(if result.is_ok() { 0 } else { 1 })
}
Some(Commands::UninstallShaders { force }) => {
let result = uninstall_shaders_cli(force);
CliResult::Exit(if result.is_ok() { 0 } else { 1 })
}
Some(Commands::InstallMuxExtensions { yes }) => {
let result = install_mux_extensions_cli(yes);
CliResult::Exit(if result.is_ok() { 0 } else { 1 })
}
Some(Commands::UninstallMuxExtensions) => {
let result = uninstall_mux_extensions_cli();
CliResult::Exit(if result.is_ok() { 0 } else { 1 })
}
Some(Commands::InstallMuxHooks { yes }) => {
let result = install_mux_hooks_cli(yes);
CliResult::Exit(if result.is_ok() { 0 } else { 1 })
}
Some(Commands::UninstallMuxHooks) => {
let result = uninstall_mux_hooks_cli();
CliResult::Exit(if result.is_ok() { 0 } else { 1 })
}
Some(Commands::ShaderLint {
path,
readability,
apply,
no_prompt,
}) => {
let result = crate::shader_lint::shader_lint_cli(&path, readability, apply, !no_prompt);
CliResult::Exit(if result.is_ok() { 0 } else { 1 })
}
Some(Commands::InstallIntegrations { yes }) => {
let result = install_integrations_cli(yes);
CliResult::Exit(if result.is_ok() { 0 } else { 1 })
}
Some(Commands::SelfUpdate { yes }) => {
let result = self_update_cli(yes);
CliResult::Exit(if result.is_ok() { 0 } else { 1 })
}
Some(Commands::Plugin { command }) => {
use plugin::PluginCommands;
let result = match command {
PluginCommands::Add { url, yes } => plugin::plugin_add_cli(&url, yes),
PluginCommands::Update { id, yes } => plugin::plugin_update_cli(id.as_deref(), yes),
PluginCommands::Remove { id, yes } => plugin::plugin_remove_cli(&id, yes),
};
CliResult::Exit(if result.is_ok() { 0 } else { 1 })
}
Some(Commands::McpServer) => {
crate::mcp_server::set_app_version(crate::VERSION);
crate::mcp_server::run_mcp_server();
CliResult::Exit(0)
}
Some(Commands::External(parts)) => {
run_external_command(&parts)
}
None => {
let options = RuntimeOptions {
shader: cli.shader,
exit_after: cli.exit_after,
screenshot: cli.screenshot,
command_to_send: cli.command_to_send,
ui_test: cli.ui_test,
ui_test_report: cli.ui_test_report,
log_session: cli.log_session,
log_level: cli.log_level.map(|l| l.to_level_filter()),
attach: cli.attach,
};
CliResult::Continue(options)
}
}
}
fn run_external_command(parts: &[String]) -> CliResult {
use par_term_config::CustomActionConfig;
use par_term_config::agent_commands as cmd_fmt;
let id = &parts[0];
let mut extra: Vec<String> = parts[1..].to_vec();
let mut assume_yes = false;
if extra.first().is_some_and(|a| a == "--yes") {
assume_yes = true;
extra.remove(0);
}
let dir = cmd_fmt::commands_dir();
let path = dir.join(format!("{id}.yaml"));
if !path.exists() {
eprintln!(
"Unknown subcommand or command id {id:?}. No such file: {}. Run \
`par-term --help` for built-in subcommands.",
path.display()
);
return CliResult::Exit(1);
}
let file = match cmd_fmt::load_command_file(&path) {
Ok(f) => f,
Err(e) => {
eprintln!("Command {id:?} is invalid: {e:#}");
return CliResult::Exit(1);
}
};
match file.action.clone() {
CustomActionConfig::ShellCommand {
command,
mut args,
timeout_secs,
..
} => {
let mut ledger = cmd_fmt::load_confirmation_ledger(&dir);
let hash = file.body_hash();
if ledger.get(file.id()) != Some(&hash) {
if !assume_yes {
println!(
"Unconfirmed agent script command {}. Run `par-term {id} --yes` \
to execute it, or approve it once in the app's palette dialog.",
file.title()
);
println!("--- body ---");
println!("{command} {}", args.join(" "));
return CliResult::Exit(2);
}
ledger.insert(file.id().to_string(), hash);
if let Err(e) = cmd_fmt::save_confirmation_ledger(&ledger, &dir) {
eprintln!("Warning: could not persist confirmation ({e:#})");
}
}
args.extend(extra);
let env = [
("PAR_TERM_COMMAND_ID", Some(file.id().to_string())),
("PAR_TERM_COMMAND_SOURCE_AGENT", file.source_agent.clone()),
];
let mut cmd = std::process::Command::new(&command);
cmd.args(&args);
for (k, v) in env.iter().filter_map(|(k, v)| v.clone().map(|v| (k, v))) {
cmd.env(k, v);
}
let status = match cmd.spawn() {
Ok(mut child) => {
let deadline =
std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
loop {
match child.try_wait() {
Ok(Some(s)) => break Some(Ok(s)),
Ok(None) => {
if std::time::Instant::now() > deadline {
let _ = child.kill();
let _ = child.wait();
break None;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(e) => break Some(Err(e)),
}
}
}
Err(e) => Some(Err(e)),
};
match status {
Some(Ok(s)) => CliResult::Exit(s.code().unwrap_or(1)),
Some(Err(e)) => {
eprintln!("Failed to run {command:?}: {e}");
CliResult::Exit(1)
}
None => {
eprintln!("Command {id:?} timed out after {timeout_secs}s");
CliResult::Exit(1)
}
}
}
macro_action => {
let _ = ¯o_action;
eprintln!(
"Command {id:?} is a macro ({}); macros drive the par-term UI \
and run from the command palette (open it and search \"{}\"), \
not from the CLI.",
file.title(),
file.title()
);
CliResult::Exit(1)
}
}
}