| @@ -1,10 +1,11 @@ |
| 1 | 1 | use super::register_zed_scheme; |
| 2 | | −use anyhow::{Context as _, Result}; |
| 2 | +use anyhow::Result; |
| 3 | 3 | use gpui::{AppContext as _, AsyncApp, Context, PromptLevel, Window, actions}; |
| 4 | 4 | use release_channel::ReleaseChannel; |
| 5 | 5 | use std::ops::Deref; |
| 6 | 6 | use std::path::{Path, PathBuf}; |
| 7 | 7 | use util::ResultExt; |
| 8 | +use workspace::notifications::simple_message_notification::MessageNotification; |
| 8 | 9 | use workspace::notifications::{DetachAndPromptErr, NotificationId}; |
| 9 | 10 | use workspace::{Toast, Workspace}; |
| 10 | 11 | |
| @@ -16,14 +17,20 @@ actions!( · 5 unmodified lines |
| 16 | 17 | ] |
| 17 | 18 | ); |
| 18 | 19 | |
| 19 | | −async fn install_script(cx: &AsyncApp) -> Result<PathBuf> { |
| 20 | +const CANT_INSTALL_DOCS_URL: &str = "https://zed.dev/docs/macos#cant-install-cli"; |
| 21 | + |
| 22 | +/// Attempts to install the CLI symlink. Returns the installed path on success, |
| 23 | +/// or `None` if the user dismissed the macOS administrator authentication |
| 24 | +/// prompt. Returns an error if the install could not be completed, most |
| 25 | +/// commonly because the user is not an admin. |
| 26 | +async fn install_script(cx: &AsyncApp) -> Result<Option<PathBuf>> { |
| 20 | 27 | let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))?; |
| 21 | 28 | let link_path = Path::new("/usr/local/bin/zed"); |
| 22 | 29 | let bin_dir_path = link_path.parent().unwrap(); |
| 23 | 30 | |
| 24 | 31 | // Don't re-create symlink if it points to the same CLI binary. |
| 25 | 32 | if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) { |
| 26 | | − return Ok(link_path.into()); |
| 33 | + return Ok(Some(link_path.into())); |
| 27 | 34 | } |
| 28 | 35 | |
| 29 | 36 | // If the symlink is not there or is outdated, first try replacing it |
| @@ -34,12 +41,12 @@ async fn install_script(cx: &AsyncApp) -> Result<PathBuf> { · 4 unmodified lines |
| 34 | 41 | .log_err() |
| 35 | 42 | .is_some() |
| 36 | 43 | { |
| 37 | | − return Ok(link_path.into()); |
| 44 | + return Ok(Some(link_path.into())); |
| 38 | 45 | } |
| 39 | 46 | |
| 40 | | − // The symlink could not be created, so use osascript with admin privileges |
| 41 | | − // to create it. |
| 42 | | − let status = smol::process::Command::new("/usr/bin/osascript") |
| 47 | + // The symlink could not be created without escalating, so use osascript |
| 48 | + // with admin privileges to create it. |
| 49 | + let output = smol::process::Command::new("/usr/bin/osascript") |
| 43 | 50 | .args([ |
| 44 | 51 | "-e", |
| 45 | 52 | &format!( |
| @@ -52,13 +59,24 @@ async fn install_script(cx: &AsyncApp) -> Result<PathBuf> { · 6 unmodified lines |
| 52 | 59 | link_path.to_string_lossy(), |
| 53 | 60 | ), |
| 54 | 61 | ]) |
| 55 | | − .stdout(smol::process::Stdio::inherit()) |
| 56 | | − .stderr(smol::process::Stdio::inherit()) |
| 57 | 62 | .output() |
| 58 | | − .await? |
| 59 | | − .status; |
| 60 | | − anyhow::ensure!(status.success(), "error running osascript"); |
| 61 | | − Ok(link_path.into()) |
| 63 | + .await?; |
| 64 | + |
| 65 | + if output.status.success() { |
| 66 | + return Ok(Some(link_path.into())); |
| 67 | + } |
| 68 | + |
| 69 | + // osascript reports "User canceled." (error -128) when the administrator |
| 70 | + // prompt is dismissed. Treat that as a cancellation rather than a failure |
| 71 | + // so we don't show an error the user already chose to avoid. |
| 72 | + let stderr = String::from_utf8_lossy(&output.stderr); |
| 73 | + if stderr.contains("User canceled") || stderr.contains("-128") { |
| 74 | + return Ok(None); |
| 75 | + } |
| 76 | + |
| 77 | + // The privileged write failed, most commonly because the user is not an |
| 78 | + // admin. |
| 79 | + anyhow::bail!("error running osascript: {}", stderr.trim()); |
| 62 | 80 | } |
| 63 | 81 | |
| 64 | 82 | pub fn install_cli_binary(window: &mut Window, cx: &mut Context<Workspace>) { |
| @@ -75,9 +93,34 @@ pub fn install_cli_binary(window: &mut Window, cx: &mut Context<Workspace>) { · 10 unmodified lines |
| 75 | 93 | cx.background_spawn(prompt).detach(); |
| 76 | 94 | return Ok(()); |
| 77 | 95 | } |
| 78 | | − let path = install_script(cx.deref()) |
| 79 | | − .await |
| 80 | | − .context("error creating CLI symlink")?; |
| 96 | + let path = match install_script(cx.deref()).await { |
| 97 | + Ok(Some(path)) => path, |
| 98 | + // The user dismissed the administrator prompt; nothing to do. |
| 99 | + Ok(None) => return Ok(()), |
| 100 | + Err(error) => { |
| 101 | + log::error!("failed to install zed CLI: {error:#}"); |
| 102 | + workspace.update(cx, |workspace, cx| { |
| 103 | + struct CliInstallFailed; |
| 104 | + |
| 105 | + workspace.show_notification( |
| 106 | + NotificationId::unique::<CliInstallFailed>(), |
| 107 | + cx, |
| 108 | + |cx| { |
| 109 | + cx.new(|cx| { |
| 110 | + MessageNotification::new( |
| 111 | + "You can add `zed` to your PATH manually.", |
| 112 | + cx, |
| 113 | + ) |
| 114 | + .with_title("Couldn't install the Zed CLI") |
| 115 | + .more_info_message("Show me how") |
| 116 | + .more_info_url(CANT_INSTALL_DOCS_URL) |
| 117 | + }) |
| 118 | + }, |
| 119 | + ); |
| 120 | + })?; |
| 121 | + return Ok(()); |
| 122 | + } |
| 123 | + }; |
| 81 | 124 | |
| 82 | 125 | workspace.update_in(cx, |workspace, _, cx| { |
| 83 | 126 | struct InstalledZedCli; |
| @@ -97,5 +140,5 @@ pub fn install_cli_binary(window: &mut Window, cx: &mut Context<Workspace>) { · 13 unmodified lines |
| 97 | 140 | register_zed_scheme(cx).await.log_err(); |
| 98 | 141 | Ok(()) |
| 99 | 142 | }) |
| 100 | | − .detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None); |
| 143 | + .detach_and_prompt_err("Cannot install the Zed CLI", window, cx, |_, _, _| None); |
| 101 | 144 | } |