use std::io::{self, Write};
use crate::config::ShellType;
use crate::shader_installer;
use crate::shell_integration_installer;
pub fn install_shaders_cli(skip_prompt: bool) -> anyhow::Result<()> {
let shaders_dir = crate::config::Config::shaders_dir();
println!("=============================================");
println!(" par-term Shader Installer");
println!("=============================================");
println!();
println!("Target directory: {}", shaders_dir.display());
println!();
if shaders_dir.exists() && shader_installer::has_shader_files(&shaders_dir) && !skip_prompt {
println!("WARNING: This will overwrite existing shaders in:");
println!(" {}", shaders_dir.display());
println!();
print!("Do you want to continue? [y/N] ");
io::stdout().flush()?;
let mut response = String::new();
io::stdin().read_line(&mut response)?;
let response = response.trim().to_lowercase();
if response != "y" && response != "yes" {
println!("Installation cancelled.");
return Ok(());
}
println!();
}
println!("Fetching latest release information...");
const REPO: &str = "paulrobello/par-term";
let api_url = format!("https://api.github.com/repos/{}/releases/latest", REPO);
let (zip_url, checksum_url) = shader_installer::get_shaders_download_url(&api_url, REPO)
.map_err(|e| anyhow::anyhow!(e))?;
println!("Downloading shaders from: {}", zip_url);
println!();
let zip_data = shader_installer::download_and_verify(&zip_url, checksum_url.as_deref())
.map_err(|e| anyhow::anyhow!(e))?;
std::fs::create_dir_all(&shaders_dir)?;
println!("Extracting shaders to {}...", shaders_dir.display());
shader_installer::extract_shaders(&zip_data, &shaders_dir).map_err(|e| anyhow::anyhow!(e))?;
let shader_count = shader_installer::count_shader_files(&shaders_dir);
println!();
println!("=============================================");
println!(" Installation complete!");
println!("=============================================");
println!();
println!("Installed {} shaders to:", shader_count);
println!(" {}", shaders_dir.display());
println!();
println!("To use a shader, add to your config.yaml:");
println!(" custom_shader: \"shader_name.glsl\"");
println!(" custom_shader_enabled: true");
println!();
println!("For cursor shaders:");
println!(" cursor_shader: \"cursor_glow.glsl\"");
println!(" cursor_shader_enabled: true");
println!();
println!("See docs/SHADERS.md for the full shader gallery.");
Ok(())
}
pub fn install_shell_integration_cli(shell: Option<ShellType>) -> anyhow::Result<()> {
let detected = shell_integration_installer::detected_shell();
let target_shell = shell.unwrap_or(detected);
println!("=============================================");
println!(" par-term Shell Integration Installer");
println!("=============================================");
println!();
if target_shell == ShellType::Unknown {
eprintln!("Error: Could not detect shell type.");
eprintln!("Please specify your shell with --shell bash|zsh|fish");
return Err(anyhow::anyhow!("Unknown shell type"));
}
println!("Detected shell: {:?}", target_shell);
if shell_integration_installer::is_installed() {
println!("Shell integration is already installed.");
print!("Do you want to reinstall? [y/N] ");
io::stdout().flush()?;
let mut response = String::new();
io::stdin().read_line(&mut response)?;
let response = response.trim().to_lowercase();
if response != "y" && response != "yes" {
println!("Installation cancelled.");
return Ok(());
}
println!();
}
println!("Installing shell integration...");
match shell_integration_installer::install(Some(target_shell)) {
Ok(result) => {
println!();
println!("=============================================");
println!(" Installation complete!");
println!("=============================================");
println!();
println!("Script installed to:");
println!(" {}", result.script_path.display());
println!();
println!("Added source line to:");
println!(" {}", result.rc_file.display());
println!();
if result.needs_restart {
println!("Please restart your shell or run:");
println!(" source {}", result.rc_file.display());
}
Ok(())
}
Err(e) => {
eprintln!("Error: {}", e);
Err(anyhow::anyhow!(e))
}
}
}
pub fn uninstall_shell_integration_cli() -> anyhow::Result<()> {
println!("=============================================");
println!(" par-term Shell Integration Uninstaller");
println!("=============================================");
println!();
if !shell_integration_installer::is_installed() {
println!("Shell integration is not installed.");
return Ok(());
}
println!("Uninstalling shell integration...");
match shell_integration_installer::uninstall() {
Ok(result) => {
println!();
println!("=============================================");
println!(" Uninstallation complete!");
println!("=============================================");
println!();
if !result.cleaned.is_empty() {
println!("Cleaned RC files:");
for path in &result.cleaned {
println!(" {}", path.display());
}
println!();
}
if !result.scripts_removed.is_empty() {
println!("Removed integration scripts:");
for path in &result.scripts_removed {
println!(" {}", path.display());
}
println!();
}
if !result.needs_manual.is_empty() {
println!("WARNING: Some files need manual cleanup:");
for path in &result.needs_manual {
println!(" {}", path.display());
}
println!();
}
Ok(())
}
Err(e) => {
eprintln!("Error: {}", e);
Err(anyhow::anyhow!(e))
}
}
}
pub fn uninstall_shaders_cli(force: bool) -> anyhow::Result<()> {
let shaders_dir = crate::config::Config::shaders_dir();
println!("=============================================");
println!(" par-term Shader Uninstaller");
println!("=============================================");
println!();
println!("Shaders directory: {}", shaders_dir.display());
println!();
if !shaders_dir.exists() {
println!("No shaders installed.");
return Ok(());
}
let manifest_path = shaders_dir.join("manifest.json");
if !manifest_path.exists() {
println!("No manifest.json found. Cannot determine which files are bundled.");
println!("Only files installed with the installer can be safely uninstalled.");
return Err(anyhow::anyhow!("No manifest found"));
}
if !force {
println!("This will remove bundled shader files.");
println!("User-created and modified files will be preserved.");
println!();
print!("Do you want to continue? [y/N] ");
io::stdout().flush()?;
let mut response = String::new();
io::stdin().read_line(&mut response)?;
let response = response.trim().to_lowercase();
if response != "y" && response != "yes" {
println!("Uninstallation cancelled.");
return Ok(());
}
println!();
}
println!("Uninstalling shaders...");
match shader_installer::uninstall_shaders(force) {
Ok(result) => {
println!();
println!("=============================================");
println!(" Uninstallation complete!");
println!("=============================================");
println!();
println!("Removed {} bundled files.", result.removed);
if result.kept > 0 {
println!("Preserved {} user files.", result.kept);
}
if !result.needs_confirmation.is_empty() {
println!();
println!("Modified files that were preserved:");
for path in &result.needs_confirmation {
println!(" {}", path);
}
}
Ok(())
}
Err(e) => {
eprintln!("Error: {}", e);
Err(anyhow::anyhow!(e))
}
}
}
pub fn self_update_cli(skip_prompt: bool) -> anyhow::Result<()> {
use par_term_update::self_updater;
use par_term_update::update_checker;
println!("=============================================");
println!(" par-term Self-Updater");
println!("=============================================");
println!();
let current_version = env!("CARGO_PKG_VERSION");
println!("Current version: {}", current_version);
let installation = self_updater::detect_installation().map_err(|e| anyhow::anyhow!(e))?;
println!("Installation type: {}", installation.description());
println!();
match &installation {
self_updater::InstallationType::Homebrew => {
println!("par-term is installed via Homebrew.");
println!("Please update with:");
println!(" brew upgrade --cask par-term");
return Err(anyhow::anyhow!("Cannot self-update Homebrew installation"));
}
self_updater::InstallationType::CargoInstall => {
println!("par-term is installed via cargo.");
println!("Please update with:");
println!(" cargo install par-term");
return Err(anyhow::anyhow!("Cannot self-update cargo installation"));
}
_ => {}
}
println!("Checking for updates...");
let release_info = update_checker::fetch_latest_release().map_err(|e| anyhow::anyhow!(e))?;
let latest_version = release_info
.version
.strip_prefix('v')
.unwrap_or(&release_info.version);
let current = semver::Version::parse(current_version)?;
let latest = semver::Version::parse(latest_version)?;
if latest <= current {
println!();
println!(
"You are already running the latest version ({}).",
current_version
);
return Ok(());
}
println!();
println!(
"New version available: {} -> {}",
current_version, latest_version
);
if let Some(ref notes) = release_info.release_notes {
println!();
println!("Release notes:");
for line in notes.lines().take(10) {
println!(" {}", line);
}
if notes.lines().count() > 10 {
println!(" ...");
}
}
println!();
if !skip_prompt {
print!("Do you want to update? [y/N] ");
io::stdout().flush()?;
let mut response = String::new();
io::stdin().read_line(&mut response)?;
let response = response.trim().to_lowercase();
if response != "y" && response != "yes" {
println!("Update cancelled.");
return Ok(());
}
println!();
}
println!("Downloading and installing update...");
match self_updater::perform_update(latest_version, crate::VERSION) {
Ok(result) => {
println!();
println!("=============================================");
println!(" Update complete!");
println!("=============================================");
println!();
println!("Updated: {} -> {}", result.old_version, result.new_version);
println!("Location: {}", result.install_path.display());
if result.needs_restart {
println!();
println!("Please restart par-term to use the new version.");
}
Ok(())
}
Err(e) => {
eprintln!("Update failed: {}", e);
Err(anyhow::anyhow!(e))
}
}
}
pub fn install_integrations_cli(skip_prompt: bool) -> anyhow::Result<()> {
println!("=============================================");
println!(" par-term Integrations Installer");
println!("=============================================");
println!();
println!("This will install:");
println!(" 1. Shader collection from latest release");
println!(" 2. Shell integration for your current shell");
println!();
if !skip_prompt {
print!("Do you want to continue? [y/N] ");
io::stdout().flush()?;
let mut response = String::new();
io::stdin().read_line(&mut response)?;
let response = response.trim().to_lowercase();
if response != "y" && response != "yes" {
println!("Installation cancelled.");
return Ok(());
}
println!();
}
println!("Step 1: Installing shaders...");
println!("---------------------------------------------");
let shader_result = install_shaders_cli(true);
if shader_result.is_err() {
println!();
println!("WARNING: Shader installation failed.");
println!("Continuing with shell integration...");
}
println!();
println!("Step 2: Installing shell integration...");
println!("---------------------------------------------");
let shell_result = install_shell_integration_cli(None);
println!();
println!("=============================================");
println!(" Integrations Installation Summary");
println!("=============================================");
println!();
match (&shader_result, &shell_result) {
(Ok(()), Ok(())) => {
println!("All integrations installed successfully!");
}
(Err(_), Ok(())) => {
println!("Shell integration: INSTALLED");
println!("Shaders: FAILED (see above for errors)");
}
(Ok(()), Err(_)) => {
println!("Shaders: INSTALLED");
println!("Shell integration: FAILED (see above for errors)");
}
(Err(_), Err(_)) => {
println!("Both installations failed. See above for errors.");
}
}
if shader_result.is_ok() || shell_result.is_ok() {
Ok(())
} else {
Err(anyhow::anyhow!("Both installations failed"))
}
}
pub fn install_mux_extensions_cli(skip_prompt: bool) -> anyhow::Result<()> {
use crate::mux_extension_installer;
println!("=============================================");
println!(" par-term par-mux Extension Installer");
println!("=============================================");
println!();
println!("This installs the par-mux agent-state extension into each");
println!("installed agent's extension directory:");
println!(
" pi: {}",
dir_display(mux_extension_installer::pi_extension_dir())
);
println!(
" omp: {}",
dir_display(mux_extension_installer::omp_extension_dir())
);
println!();
println!("An agent that is not installed is reported and skipped.");
if !skip_prompt {
println!();
print!("Do you want to continue? [y/N] ");
io::stdout().flush()?;
let mut response = String::new();
io::stdin().read_line(&mut response)?;
let response = response.trim().to_lowercase();
if response != "y" && response != "yes" {
println!("Installation cancelled.");
return Ok(());
}
}
println!();
let pi_result = mux_extension_installer::install_pi_extension();
report_install("pi", &pi_result);
let omp_result = mux_extension_installer::install_omp_extension();
report_install("omp", &omp_result);
if pi_result.is_ok() || omp_result.is_ok() {
println!();
println!("At least one extension installed successfully.");
Ok(())
} else {
println!();
Err(anyhow::anyhow!("No extension installed (see errors above)"))
}
}
pub fn uninstall_mux_extensions_cli() -> anyhow::Result<()> {
use crate::mux_extension_installer;
println!("=============================================");
println!(" par-term par-mux Extension Uninstaller");
println!("=============================================");
println!();
let pi_result = mux_extension_installer::uninstall_pi_extension();
report_removal("pi", &pi_result);
let omp_result = mux_extension_installer::uninstall_omp_extension();
report_removal("omp", &omp_result);
println!();
println!("Done.");
Ok(())
}
pub fn install_mux_hooks_cli(skip_prompt: bool) -> anyhow::Result<()> {
use crate::mux_hook_installer;
println!("=============================================");
println!(" par-term par-mux Hook Installer");
println!("=============================================");
println!();
println!("This installs the par-mux session hooks. Unlike the pi/omp");
println!("extensions (a file drop into the agent's directory), these agents");
println!("have no extension directory: the hook is an ENTRY inside your");
println!("existing agent config, merged without touching your other settings,");
println!("comments, or formatting. Reinstalling is a no-op; uninstalling");
println!("removes only par-term's entries.");
println!();
println!("Agents:");
println!(
" claude: {}",
dir_display(mux_hook_installer::claude_settings_path())
);
println!(
" codex: {}",
dir_display(mux_hook_installer::codex_config_dir())
);
println!(
" grok: {}",
dir_display(mux_hook_installer::grok_config_dir().map(|dir| dir.join("hooks")))
);
println!();
if !skip_prompt {
println!();
print!("Do you want to continue? [y/N] ");
io::stdout().flush()?;
let mut response = String::new();
io::stdin().read_line(&mut response)?;
let response = response.trim().to_lowercase();
if response != "y" && response != "yes" {
println!("Installation cancelled.");
return Ok(());
}
}
println!();
let claude_result = mux_hook_installer::install_claude_hook();
match &claude_result {
Ok(result) if result.settings_changed => {
println!(
"claude: hook entry added to {} (script at {})",
result.settings_path.display(),
result.hook_path.display()
);
}
Ok(result) => {
println!(
"claude: already installed ({} unchanged, script refreshed)",
result.settings_path.display()
);
}
Err(err) => println!("claude: FAILED — {err}"),
}
let codex_result = mux_hook_installer::install_codex_hook();
match &codex_result {
Ok(result) => {
let summary = match (result.hooks_changed, result.config_changed) {
(true, true) => format!(
"hook entry added to {} and features.hooks enabled in {} (script at {})",
result.hooks_path.display(),
result.config_path.display(),
result.hook_path.display()
),
(true, false) => format!(
"hook entry added to {} (features.hooks already enabled; script at {})",
result.hooks_path.display(),
result.hook_path.display()
),
(false, true) => format!(
"features.hooks enabled in {} (hook entry already present; script at {})",
result.config_path.display(),
result.hook_path.display()
),
(false, false) => format!(
"already installed ({} unchanged, script refreshed)",
result.hooks_path.display()
),
};
println!("codex: {summary}");
}
Err(err) => println!("codex: FAILED — {err}"),
}
let grok_result = mux_hook_installer::install_grok_hook();
match &grok_result {
Ok(result) => println!(
"grok: hook config written to {} (script at {})",
result.config_path.display(),
result.hook_path.display()
),
Err(err) => println!("grok: FAILED — {err}"),
}
if claude_result.is_ok() || codex_result.is_ok() || grok_result.is_ok() {
println!();
println!("At least one hook installed successfully.");
Ok(())
} else {
println!();
Err(anyhow::anyhow!("No hook installed (see errors above)"))
}
}
pub fn uninstall_mux_hooks_cli() -> anyhow::Result<()> {
use crate::mux_hook_installer;
println!("=============================================");
println!(" par-term par-mux Hook Uninstaller");
println!("=============================================");
println!();
let claude_result = mux_hook_installer::uninstall_claude_hook();
match &claude_result {
Ok(result) => {
if result.settings_changed {
println!(
"claude: entries removed from {}",
result.settings_path.display()
);
}
if result.hook_removed {
println!("claude: removed {}", result.hook_path.display());
}
if !result.settings_changed && !result.hook_removed {
if result.hook_path.is_file() {
println!(
"claude: {} exists but is not par-term's (left untouched)",
result.hook_path.display()
);
} else {
println!("claude: not installed");
}
}
}
Err(err) => println!("claude: FAILED — {err}"),
}
let codex_result = mux_hook_installer::uninstall_codex_hook();
match &codex_result {
Ok(result) => {
if result.hooks_changed {
println!(
"codex: entries removed from {}",
result.hooks_path.display()
);
}
if result.hook_removed {
println!("codex: removed {}", result.hook_path.display());
}
if !result.hooks_changed && !result.hook_removed {
if result.hook_path.is_file() {
println!(
"codex: {} exists but is not par-term's (left untouched)",
result.hook_path.display()
);
} else {
println!("codex: not installed");
}
}
if result.hooks_changed || result.hook_removed {
println!(
"codex: note: [features] hooks = true left in {} — inert while no hooks remain; remove it manually if unwanted",
result.config_path.display()
);
}
}
Err(err) => println!("codex: FAILED — {err}"),
}
let grok_result = mux_hook_installer::uninstall_grok_hook();
match &grok_result {
Ok(result) => {
if result.config_removed {
println!("grok: removed {}", result.config_path.display());
}
if result.hook_removed {
println!("grok: removed {}", result.hook_path.display());
}
if !result.config_removed && !result.hook_removed {
if result.hook_path.is_file() {
println!(
"grok: {} exists but is not par-term's (left untouched)",
result.hook_path.display()
);
} else {
println!("grok: not installed");
}
}
}
Err(err) => println!("grok: FAILED — {err}"),
}
println!();
println!("Done.");
Ok(())
}
fn dir_display(dir: std::io::Result<std::path::PathBuf>) -> String {
match dir {
Ok(path) => path.display().to_string(),
Err(err) => format!("<unresolved: {err}>"),
}
}
fn report_install(agent: &str, result: &std::io::Result<std::path::PathBuf>) {
match result {
Ok(path) => println!("{agent}: installed at {}", path.display()),
Err(err) => println!("{agent}: FAILED — {err}"),
}
}
fn report_removal(
agent: &str,
result: &std::io::Result<crate::mux_extension_installer::ExtensionRemoval>,
) {
match result {
Ok(removal) if removal.removed => {
println!("{agent}: removed {}", removal.path.display());
}
Ok(removal) if removal.path.is_file() => println!(
"{agent}: {} exists but is not par-term's (left untouched)",
removal.path.display()
),
Ok(_) => println!("{agent}: not installed"),
Err(err) => println!("{agent}: FAILED — {err}"),
}
}