1pub mod install;
8pub mod plugin;
9
10use crate::config::ShellType;
11use clap::{Parser, Subcommand};
12use std::path::PathBuf;
13
14#[derive(Debug, Clone, Copy, clap::ValueEnum)]
16pub enum ShellTypeArg {
17 Bash,
18 Zsh,
19 Fish,
20}
21
22impl From<ShellTypeArg> for ShellType {
23 fn from(arg: ShellTypeArg) -> Self {
24 match arg {
25 ShellTypeArg::Bash => ShellType::Bash,
26 ShellTypeArg::Zsh => ShellType::Zsh,
27 ShellTypeArg::Fish => ShellType::Fish,
28 }
29 }
30}
31
32#[derive(Parser)]
34#[command(name = "par-term")]
35#[command(author, version, about, long_about = None)]
36pub struct Cli {
37 #[command(subcommand)]
38 pub command: Option<Commands>,
39
40 #[arg(long, value_name = "SHADER")]
42 pub shader: Option<String>,
43
44 #[arg(long, value_name = "SECONDS")]
46 pub exit_after: Option<f64>,
47
48 #[arg(long, value_name = "PATH", num_args = 0..=1, default_missing_value = "")]
50 pub screenshot: Option<PathBuf>,
51
52 #[arg(long, value_name = "COMMAND")]
54 pub command_to_send: Option<String>,
55
56 #[arg(long, value_name = "SCRIPT")]
59 pub ui_test: Option<PathBuf>,
60
61 #[arg(long, value_name = "PATH", requires = "ui_test")]
63 pub ui_test_report: Option<PathBuf>,
64
65 #[arg(long)]
67 pub log_session: bool,
68
69 #[arg(long, value_enum, value_name = "LEVEL")]
71 pub log_level: Option<LogLevelArg>,
72}
73
74#[derive(Debug, Clone, Copy, clap::ValueEnum)]
76pub enum LogLevelArg {
77 Off,
78 Error,
79 Warn,
80 Info,
81 Debug,
82 Trace,
83}
84
85impl LogLevelArg {
86 pub fn to_level_filter(self) -> log::LevelFilter {
88 match self {
89 LogLevelArg::Off => log::LevelFilter::Off,
90 LogLevelArg::Error => log::LevelFilter::Error,
91 LogLevelArg::Warn => log::LevelFilter::Warn,
92 LogLevelArg::Info => log::LevelFilter::Info,
93 LogLevelArg::Debug => log::LevelFilter::Debug,
94 LogLevelArg::Trace => log::LevelFilter::Trace,
95 }
96 }
97}
98
99#[derive(Subcommand)]
100pub enum Commands {
101 InstallShaders {
103 #[arg(short = 'y', long)]
105 yes: bool,
106
107 #[arg(short, long)]
109 force: bool,
110 },
111
112 InstallShellIntegration {
114 #[arg(long, value_enum)]
116 shell: Option<ShellTypeArg>,
117 },
118
119 UninstallShellIntegration,
121
122 UninstallShaders {
124 #[arg(short, long)]
126 force: bool,
127 },
128
129 InstallMuxExtensions {
131 #[arg(short = 'y', long)]
133 yes: bool,
134 },
135
136 UninstallMuxExtensions,
138
139 InstallMuxHooks {
141 #[arg(short = 'y', long)]
143 yes: bool,
144 },
145
146 UninstallMuxHooks,
148
149 ShaderLint {
151 path: PathBuf,
153
154 #[arg(long)]
156 readability: bool,
157
158 #[arg(long)]
160 apply: bool,
161
162 #[arg(long)]
164 no_prompt: bool,
165 },
166
167 InstallIntegrations {
169 #[arg(short = 'y', long)]
171 yes: bool,
172 },
173
174 SelfUpdate {
176 #[arg(short = 'y', long)]
178 yes: bool,
179 },
180
181 Plugin {
183 #[command(subcommand)]
184 command: plugin::PluginCommands,
185 },
186
187 McpServer,
189
190 #[command(external_subcommand)]
194 External(Vec<String>),
195}
196
197#[derive(Clone, Debug, Default)]
199pub struct RuntimeOptions {
200 pub shader: Option<String>,
202 pub exit_after: Option<f64>,
204 pub screenshot: Option<PathBuf>,
206 pub command_to_send: Option<String>,
208 pub ui_test: Option<PathBuf>,
210 pub ui_test_report: Option<PathBuf>,
212 pub log_session: bool,
214 pub log_level: Option<log::LevelFilter>,
216}
217
218pub enum CliResult {
220 Continue(RuntimeOptions),
222 Exit(i32),
224}
225
226pub fn process_cli() -> CliResult {
228 use install::{
229 install_integrations_cli, install_mux_extensions_cli, install_mux_hooks_cli,
230 install_shaders_cli, install_shell_integration_cli, self_update_cli,
231 uninstall_mux_extensions_cli, uninstall_mux_hooks_cli, uninstall_shaders_cli,
232 uninstall_shell_integration_cli,
233 };
234
235 let cli = Cli::parse();
236
237 match cli.command {
238 Some(Commands::InstallShaders { yes, force }) => {
239 let result = install_shaders_cli(yes || force);
240 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
241 }
242 Some(Commands::InstallShellIntegration { shell }) => {
243 let result = install_shell_integration_cli(shell.map(Into::into));
244 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
245 }
246 Some(Commands::UninstallShellIntegration) => {
247 let result = uninstall_shell_integration_cli();
248 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
249 }
250 Some(Commands::UninstallShaders { force }) => {
251 let result = uninstall_shaders_cli(force);
252 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
253 }
254 Some(Commands::InstallMuxExtensions { yes }) => {
255 let result = install_mux_extensions_cli(yes);
256 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
257 }
258 Some(Commands::UninstallMuxExtensions) => {
259 let result = uninstall_mux_extensions_cli();
260 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
261 }
262 Some(Commands::InstallMuxHooks { yes }) => {
263 let result = install_mux_hooks_cli(yes);
264 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
265 }
266 Some(Commands::UninstallMuxHooks) => {
267 let result = uninstall_mux_hooks_cli();
268 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
269 }
270 Some(Commands::ShaderLint {
271 path,
272 readability,
273 apply,
274 no_prompt,
275 }) => {
276 let result = crate::shader_lint::shader_lint_cli(&path, readability, apply, !no_prompt);
277 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
278 }
279 Some(Commands::InstallIntegrations { yes }) => {
280 let result = install_integrations_cli(yes);
281 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
282 }
283 Some(Commands::SelfUpdate { yes }) => {
284 let result = self_update_cli(yes);
285 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
286 }
287 Some(Commands::Plugin { command }) => {
288 use plugin::PluginCommands;
289 let result = match command {
290 PluginCommands::Add { url, yes } => plugin::plugin_add_cli(&url, yes),
291 PluginCommands::Update { id, yes } => plugin::plugin_update_cli(id.as_deref(), yes),
292 PluginCommands::Remove { id, yes } => plugin::plugin_remove_cli(&id, yes),
293 };
294 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
295 }
296 Some(Commands::McpServer) => {
297 crate::mcp_server::set_app_version(crate::VERSION);
298 crate::mcp_server::run_mcp_server();
299 CliResult::Exit(0)
300 }
301 Some(Commands::External(parts)) => {
302 run_external_command(&parts)
306 }
307 None => {
308 let options = RuntimeOptions {
310 shader: cli.shader,
311 exit_after: cli.exit_after,
312 screenshot: cli.screenshot,
313 command_to_send: cli.command_to_send,
314 ui_test: cli.ui_test,
315 ui_test_report: cli.ui_test_report,
316 log_session: cli.log_session,
317 log_level: cli.log_level.map(|l| l.to_level_filter()),
318 };
319 CliResult::Continue(options)
320 }
321 }
322}
323
324fn run_external_command(parts: &[String]) -> CliResult {
334 use par_term_config::CustomActionConfig;
335 use par_term_config::agent_commands as cmd_fmt;
336
337 let id = &parts[0];
338 let mut extra: Vec<String> = parts[1..].to_vec();
339 let mut assume_yes = false;
340 if extra.first().is_some_and(|a| a == "--yes") {
341 assume_yes = true;
342 extra.remove(0);
343 }
344
345 let dir = cmd_fmt::commands_dir();
346 let path = dir.join(format!("{id}.yaml"));
347 if !path.exists() {
348 eprintln!(
349 "Unknown subcommand or command id {id:?}. No such file: {}. Run \
350 `par-term --help` for built-in subcommands.",
351 path.display()
352 );
353 return CliResult::Exit(1);
354 }
355
356 let file = match cmd_fmt::load_command_file(&path) {
357 Ok(f) => f,
358 Err(e) => {
359 eprintln!("Command {id:?} is invalid: {e:#}");
360 return CliResult::Exit(1);
361 }
362 };
363
364 match file.action.clone() {
365 CustomActionConfig::ShellCommand {
366 command,
367 mut args,
368 timeout_secs,
369 ..
370 } => {
371 let mut ledger = cmd_fmt::load_confirmation_ledger(&dir);
372 let hash = file.body_hash();
373 if ledger.get(file.id()) != Some(&hash) {
374 if !assume_yes {
375 println!(
376 "Unconfirmed agent script command {}. Run `par-term {id} --yes` \
377 to execute it, or approve it once in the app's palette dialog.",
378 file.title()
379 );
380 println!("--- body ---");
381 println!("{command} {}", args.join(" "));
382 return CliResult::Exit(2);
383 }
384 ledger.insert(file.id().to_string(), hash);
388 if let Err(e) = cmd_fmt::save_confirmation_ledger(&ledger, &dir) {
389 eprintln!("Warning: could not persist confirmation ({e:#})");
390 }
391 }
392 args.extend(extra);
393 let env = [
394 ("PAR_TERM_COMMAND_ID", Some(file.id().to_string())),
395 ("PAR_TERM_COMMAND_SOURCE_AGENT", file.source_agent.clone()),
396 ];
397 let mut cmd = std::process::Command::new(&command);
398 cmd.args(&args);
399 for (k, v) in env.iter().filter_map(|(k, v)| v.clone().map(|v| (k, v))) {
400 cmd.env(k, v);
401 }
402 let status = match cmd.spawn() {
405 Ok(mut child) => {
406 let deadline =
407 std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
408 loop {
409 match child.try_wait() {
410 Ok(Some(s)) => break Some(Ok(s)),
411 Ok(None) => {
412 if std::time::Instant::now() > deadline {
413 let _ = child.kill();
414 let _ = child.wait();
415 break None;
416 }
417 std::thread::sleep(std::time::Duration::from_millis(50));
418 }
419 Err(e) => break Some(Err(e)),
420 }
421 }
422 }
423 Err(e) => Some(Err(e)),
424 };
425 match status {
426 Some(Ok(s)) => CliResult::Exit(s.code().unwrap_or(1)),
427 Some(Err(e)) => {
428 eprintln!("Failed to run {command:?}: {e}");
429 CliResult::Exit(1)
430 }
431 None => {
432 eprintln!("Command {id:?} timed out after {timeout_secs}s");
433 CliResult::Exit(1)
434 }
435 }
436 }
437 macro_action => {
438 let _ = ¯o_action;
439 eprintln!(
440 "Command {id:?} is a macro ({}); macros drive the par-term UI \
441 and run from the command palette (open it and search \"{}\"), \
442 not from the CLI.",
443 file.title(),
444 file.title()
445 );
446 CliResult::Exit(1)
447 }
448 }
449}