crates/q_cli/src/cli/integrations.rs (512 lines of code) (raw):

use std::process::ExitCode; use anstream::println; use clap::Subcommand; use crossterm::style::Stylize; use eyre::Result; use fig_integrations::Integration as _; use fig_integrations::shell::ShellExt; use fig_integrations::ssh::SshIntegration; use fig_os_shim::Env; use fig_util::Shell; use serde_json::json; use tracing::debug; use super::OutputFormat; #[derive(Debug, PartialEq, Eq, Subcommand)] pub enum IntegrationsSubcommands { Install { /// Integration to install #[command(subcommand)] integration: Integration, /// Suppress status messages #[arg(long, short)] silent: bool, }, Uninstall { /// Integration to uninstall #[command(subcommand)] integration: Integration, /// Suppress status messages #[arg(long, short)] silent: bool, }, Reinstall { /// Integration to reinstall #[command(subcommand)] integration: Integration, /// Suppress status messages #[arg(long, short)] silent: bool, }, Status { /// Integration to check status of #[command(subcommand)] integration: Integration, #[arg(long, short, value_enum, default_value_t)] format: OutputFormat, }, } #[derive(Debug, Subcommand, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum Integration { Dotfiles { /// Only install the integrations for a single shell #[arg(value_enum)] shell: Option<Shell>, }, Ssh, InputMethod, #[command(name = "vscode")] VSCode, #[command(alias = "jetbrains-plugin")] IntellijPlugin, AutostartEntry, GnomeShellExtension, #[doc(hidden)] All, } impl IntegrationsSubcommands { pub async fn execute(self) -> Result<ExitCode> { match self { IntegrationsSubcommands::Install { integration, silent } => { if let Integration::All = integration { install(Integration::Dotfiles { shell: None }, silent).await?; install(Integration::Ssh, silent).await?; #[cfg(target_os = "macos")] install(Integration::InputMethod, silent).await?; } else { install(integration, silent).await?; } Ok(ExitCode::SUCCESS) }, IntegrationsSubcommands::Uninstall { integration, silent } => { if let Integration::All = integration { uninstall(Integration::Dotfiles { shell: None }, silent).await?; uninstall(Integration::Ssh, silent).await?; #[cfg(target_os = "macos")] uninstall(Integration::InputMethod, silent).await?; #[cfg(target_os = "linux")] uninstall(Integration::AutostartEntry, silent).await?; #[cfg(target_os = "linux")] uninstall(Integration::GnomeShellExtension, silent).await?; } else { uninstall(integration, silent).await?; } Ok(ExitCode::SUCCESS) }, IntegrationsSubcommands::Status { integration, format } => status(integration, format).await, IntegrationsSubcommands::Reinstall { integration, silent } => { if let Integration::All = integration { uninstall(Integration::Dotfiles { shell: None }, silent).await?; uninstall(Integration::Ssh, silent).await?; #[cfg(target_os = "macos")] uninstall(Integration::InputMethod, silent).await?; install(Integration::Dotfiles { shell: None }, silent).await?; install(Integration::Ssh, silent).await?; #[cfg(target_os = "macos")] install(Integration::InputMethod, silent).await?; } else { uninstall(integration, silent).await?; install(integration, silent).await?; } Ok(ExitCode::SUCCESS) }, } } } #[allow(unused_mut)] async fn install(integration: Integration, silent: bool) -> Result<()> { let mut installed = false; let mut errored = false; let mut status: Option<&str> = None; let result = match integration { Integration::All => Ok(()), Integration::Dotfiles { shell } => { let shells = match shell { Some(shell) => vec![shell], None => vec![Shell::Bash, Shell::Zsh, Shell::Fish], }; let mut errs: Vec<String> = vec![]; for shell in &shells { match shell.get_shell_integrations(&Env::new()) { Ok(integrations) => { for integration in integrations { match integration.is_installed().await { Ok(_) => { debug!("Skipping {}", integration.describe()); }, Err(_) => { installed = true; if let Err(e) = integration.install().await { errs.push(format!( "{}: {}", integration.describe().bold(), e.verbose_message() )); } }, } } }, Err(e) => { errs.push(format!("{shell}: {e}")); }, } } if errs.is_empty() { Ok(()) } else { Err(eyre::eyre!("\n\n{}", errs.join("\n\n"))) } }, Integration::Ssh => { let ssh_integration = SshIntegration::new()?; if ssh_integration.is_installed().await.is_err() { installed = true; ssh_integration.install().await.map_err(eyre::Report::from) } else { Ok(()) } }, Integration::InputMethod => { cfg_if::cfg_if! { if #[cfg(target_os = "macos")] { fig_settings::state::set_value("input-method.enabled", true).ok(); fig_integrations::input_method::InputMethod::default().install().await?; installed = true; status = Some("You must restart your terminal to finish installing the input method."); Ok(()) } else { errored = true; Err(eyre::eyre!("Input method integration is only supported on macOS")) } } }, Integration::VSCode => { cfg_if::cfg_if! { if #[cfg(target_os = "macos")] { let variants = fig_integrations::vscode::variants_installed(); installed = !variants.is_empty(); for variant in variants { fig_integrations::vscode::VSCodeIntegration { variant }.install().await?; } Ok(()) } else { errored = true; Err(eyre::eyre!("VSCode integration is only supported on macOS")) } } }, Integration::IntellijPlugin => { cfg_if::cfg_if! { if #[cfg(target_os = "macos")] { let variants = fig_integrations::intellij::variants_installed().await?; installed = !variants.is_empty(); for variant in variants { variant.install().await?; } Ok(()) } else { errored = true; Err(eyre::eyre!("IntelliJ integration is only supported on macOS")) } } }, Integration::AutostartEntry => { errored = true; Err(eyre::eyre!( "Installing the autostart entry from the CLI is not supported" )) }, Integration::GnomeShellExtension => { errored = true; Err(eyre::eyre!( "Installing the GNOME Shell extension from the CLI is not supported" )) }, }; if installed && result.is_ok() && !silent { println!("Installed!"); if let Some(status) = status { println!("{status}"); } } if !errored && !installed && !silent { println!("Already installed"); } result } async fn uninstall(integration: Integration, silent: bool) -> Result<()> { let mut uninstalled = false; let result = match integration { Integration::All => Ok(()), Integration::Dotfiles { shell } => { let shells = match shell { Some(shell) => vec![shell], None => vec![Shell::Bash, Shell::Zsh, Shell::Fish], }; let mut errs: Vec<String> = vec![]; for shell in &shells { match shell.get_shell_integrations(&Env::new()) { Ok(integrations) => { for integration in integrations { match integration.is_installed().await { Ok(_) => { uninstalled = true; if let Err(e) = integration.uninstall().await { errs.push(format!( "{}: {}", integration.describe().bold(), e.verbose_message() )); } }, Err(_) => { debug!("Skipping {}", integration.describe()); }, } } }, Err(e) => { errs.push(format!("{shell}: {e}")); }, } } if errs.is_empty() { Ok(()) } else { Err(eyre::eyre!("\n\n{}", errs.join("\n\n"))) } }, Integration::Ssh => { let ssh_integration = SshIntegration::new()?; if ssh_integration.is_installed().await.is_ok() { uninstalled = true; ssh_integration.uninstall().await.map_err(eyre::Report::from) } else { Ok(()) } }, Integration::InputMethod => { cfg_if::cfg_if! { if #[cfg(target_os = "macos")] { fig_integrations::input_method::InputMethod::default().uninstall().await?; uninstalled = true; Ok(()) } else { Err(eyre::eyre!("Input method integration is only supported on macOS")) } } }, Integration::VSCode => { cfg_if::cfg_if! { if #[cfg(target_os = "macos")] { for variant in fig_integrations::vscode::variants_installed() { let integration = fig_integrations::vscode::VSCodeIntegration { variant }; if integration.is_installed().await.is_ok() { integration.uninstall().await?; uninstalled = true; } } println!("Warning: VSCode integrations are automatically reinstalled on launch"); Ok(()) } else { Err(eyre::eyre!("VSCode integration is only supported on macOS")) } } }, Integration::IntellijPlugin => { cfg_if::cfg_if! { if #[cfg(any(target_os = "macos", target_os = "linux"))] { for variant in fig_integrations::intellij::variants_installed().await? { if variant.is_installed().await.is_ok() { variant.uninstall().await?; uninstalled = true; } } println!("Warning: IntelliJ integrations are automatically reinstalled on launch"); Ok(()) } else { Err(eyre::eyre!("IntelliJ integration is only supported on macOS and Linux")) } } }, Integration::AutostartEntry => { cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { use fig_integrations::desktop_entry::AutostartIntegration; use fig_os_shim::Context; AutostartIntegration::uninstall(&Context::new()).await?; uninstalled = true; Ok(()) } else { Err(eyre::eyre!("The autostart integration is only supported on Linux")) } } }, Integration::GnomeShellExtension => { cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { use std::sync::Arc; use dbus::gnome_shell::ShellExtensions; use fig_integrations::gnome_extension::GnomeExtensionIntegration; use fig_os_shim::Context; let ctx = Context::new(); let shell_extensions = ShellExtensions::new(Arc::downgrade(&ctx)); uninstalled = GnomeExtensionIntegration::new(&ctx, &shell_extensions, None::<&str>, None).uninstall_manually().await?; Ok(()) } else { Err(eyre::eyre!("The GNOME Shell extension is only supported on Linux")) } } }, }; if uninstalled && result.is_ok() && !silent { println!("Uninstalled!"); } if !uninstalled && !silent { println!("Not installed"); } result } async fn status(integration: Integration, format: OutputFormat) -> Result<ExitCode> { match integration { Integration::All => Err(eyre::eyre!( "Checking the status for all integrations is currently not supported" )), Integration::Ssh => { let ssh_integration = SshIntegration::new()?; let installed = ssh_integration.is_installed().await.is_ok(); format.print( || if installed { "Installed" } else { "Not installed" }, || { json!({ "installed": installed, }) }, ); Ok(ExitCode::SUCCESS) }, Integration::Dotfiles { .. } => { let mut all_integrations = vec![]; let mut errors = vec![]; for shell in &[Shell::Bash, Shell::Zsh, Shell::Fish] { match shell.get_shell_integrations(&Env::new()) { Ok(integrations) => { for integration in integrations { all_integrations.push(( integration.is_installed().await.is_ok(), integration.describe(), integration.get_shell(), integration.file_name().to_owned(), )); } }, Err(e) => { errors.push((shell.to_string(), e.verbose_message())); }, } } format.print( || { let mut s = String::new(); for (installed, describe, _, _) in &all_integrations { s.push_str(&if *installed { "✔ ".green().to_string() } else { "✘ ".red().to_string() }); s.push_str(describe); s.push('\n'); } for (shell, error) in &errors { s.push_str(&format!("{shell}: {error}\n")); } s }, || { let integrations = all_integrations .iter() .map(|(installed, describe, shell, file_name)| { json!({ "installed": installed, "description": describe, "shell": shell, "file_name": file_name, }) }) .collect::<Vec<_>>(); let errors = errors .iter() .map(|(shell, error)| { json!({ "shell": shell, "error": error, }) }) .collect::<Vec<_>>(); json!({ "integrations": integrations, "errors": errors, }) }, ); Ok(ExitCode::SUCCESS) }, Integration::InputMethod => { cfg_if::cfg_if! { if #[cfg(target_os = "macos")] { let input_method = fig_integrations::input_method::InputMethod::default(); let installed = input_method.is_installed().await.is_ok(); format.print( || if installed { "Installed" } else { "Not installed" }, || json!({ "installed": installed, }) ); Ok(ExitCode::SUCCESS) } else { Err(eyre::eyre!("Input method integration is only supported on macOS")) } } }, Integration::VSCode => { cfg_if::cfg_if! { if #[cfg(target_os = "macos")] { let variants = fig_integrations::vscode::variants_installed(); for variant in variants { let integration = fig_integrations::vscode::VSCodeIntegration { variant }; match integration.is_installed().await { Ok(_) => { println!("{}: Installed", integration.variant.application_name); } Err(_) => { println!("{}: Not installed", integration.variant.application_name); } } } Ok(ExitCode::SUCCESS) } else { Err(eyre::eyre!("VSCode integration is only supported on macOS")) } } }, Integration::IntellijPlugin => { cfg_if::cfg_if! { if #[cfg(any(target_os = "macos", target_os = "linux"))] { let variants = fig_integrations::intellij::variants_installed().await?; for variant in variants { match variant.is_installed().await { Ok(_) => { println!("{}: Installed", variant.variant.application_name()); } Err(_) => { println!("{}: Not installed", variant.variant.application_name()); } } } Ok(ExitCode::SUCCESS) } else { Err(eyre::eyre!("IntelliJ integration is only supported on macOS and Linux")) } } }, Integration::AutostartEntry => Err(eyre::eyre!( "Checking the status of the autostart entry from the CLI is not supported" )), Integration::GnomeShellExtension => Err(eyre::eyre!( "Checking the status of the GNOME Shell extension from the CLI is not supported" )), } }