zed: Hide Install CLI menu button on Windows (#37843)

Jakub Konka and Peter Tripp created

The "Install CLI" menu button and `install_cli::Install` action are
effectively no-op on Windows since the CLI is directly available in
Windows Terminal (CMD prompt, PowerShell, etc.) after the user runs Zed
installer package.

Release Notes:

- N/A

Co-authored-by: Peter Tripp <peter@zed.dev>

Change summary

crates/install_cli/src/install_cli.rs         | 117 +-------------------
crates/install_cli/src/install_cli_binary.rs  | 101 ++++++++++++++++++
crates/install_cli/src/register_zed_scheme.rs |  15 ++
crates/zed/src/zed.rs                         |   9 +
crates/zed/src/zed/app_menus.rs               |   3 
5 files changed, 130 insertions(+), 115 deletions(-)

Detailed changes

crates/install_cli/src/install_cli.rs 🔗

@@ -1,112 +1,7 @@
-use anyhow::{Context as _, Result};
-use client::ZED_URL_SCHEME;
-use gpui::{AppContext as _, AsyncApp, Context, PromptLevel, Window, actions};
-use release_channel::ReleaseChannel;
-use std::ops::Deref;
-use std::path::{Path, PathBuf};
-use util::ResultExt;
-use workspace::notifications::{DetachAndPromptErr, NotificationId};
-use workspace::{Toast, Workspace};
+#[cfg(not(target_os = "windows"))]
+mod install_cli_binary;
+mod register_zed_scheme;
 
-actions!(
-    cli,
-    [
-        /// Installs the Zed CLI tool to the system PATH.
-        Install,
-        /// Registers the zed:// URL scheme handler.
-        RegisterZedScheme
-    ]
-);
-
-async fn install_script(cx: &AsyncApp) -> Result<PathBuf> {
-    let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??;
-    let link_path = Path::new("/usr/local/bin/zed");
-    let bin_dir_path = link_path.parent().unwrap();
-
-    // Don't re-create symlink if it points to the same CLI binary.
-    if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
-        return Ok(link_path.into());
-    }
-
-    // If the symlink is not there or is outdated, first try replacing it
-    // without escalating.
-    smol::fs::remove_file(link_path).await.log_err();
-    // todo("windows")
-    #[cfg(not(windows))]
-    {
-        if smol::fs::unix::symlink(&cli_path, link_path)
-            .await
-            .log_err()
-            .is_some()
-        {
-            return Ok(link_path.into());
-        }
-    }
-
-    // The symlink could not be created, so use osascript with admin privileges
-    // to create it.
-    let status = smol::process::Command::new("/usr/bin/osascript")
-        .args([
-            "-e",
-            &format!(
-                "do shell script \" \
-                    mkdir -p \'{}\' && \
-                    ln -sf \'{}\' \'{}\' \
-                \" with administrator privileges",
-                bin_dir_path.to_string_lossy(),
-                cli_path.to_string_lossy(),
-                link_path.to_string_lossy(),
-            ),
-        ])
-        .stdout(smol::process::Stdio::inherit())
-        .stderr(smol::process::Stdio::inherit())
-        .output()
-        .await?
-        .status;
-    anyhow::ensure!(status.success(), "error running osascript");
-    Ok(link_path.into())
-}
-
-pub async fn register_zed_scheme(cx: &AsyncApp) -> anyhow::Result<()> {
-    cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME))?
-        .await
-}
-
-pub fn install_cli(window: &mut Window, cx: &mut Context<Workspace>) {
-    const LINUX_PROMPT_DETAIL: &str = "If you installed Zed from our official release add ~/.local/bin to your PATH.\n\nIf you installed Zed from a different source like your package manager, then you may need to create an alias/symlink manually.\n\nDepending on your package manager, the CLI might be named zeditor, zedit, zed-editor or something else.";
-
-    cx.spawn_in(window, async move |workspace, cx| {
-        if cfg!(any(target_os = "linux", target_os = "freebsd")) {
-            let prompt = cx.prompt(
-                PromptLevel::Warning,
-                "CLI should already be installed",
-                Some(LINUX_PROMPT_DETAIL),
-                &["Ok"],
-            );
-            cx.background_spawn(prompt).detach();
-            return Ok(());
-        }
-        let path = install_script(cx.deref())
-            .await
-            .context("error creating CLI symlink")?;
-
-        workspace.update_in(cx, |workspace, _, cx| {
-            struct InstalledZedCli;
-
-            workspace.show_toast(
-                Toast::new(
-                    NotificationId::unique::<InstalledZedCli>(),
-                    format!(
-                        "Installed `zed` to {}. You can launch {} from your terminal.",
-                        path.to_string_lossy(),
-                        ReleaseChannel::global(cx).display_name()
-                    ),
-                ),
-                cx,
-            )
-        })?;
-        register_zed_scheme(cx).await.log_err();
-        Ok(())
-    })
-    .detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None);
-}
+#[cfg(not(target_os = "windows"))]
+pub use install_cli_binary::{InstallCliBinary, install_cli_binary};
+pub use register_zed_scheme::{RegisterZedScheme, register_zed_scheme};

crates/install_cli/src/install_cli_binary.rs 🔗

@@ -0,0 +1,101 @@
+use super::register_zed_scheme;

+use anyhow::{Context as _, Result};

+use gpui::{AppContext as _, AsyncApp, Context, PromptLevel, Window, actions};

+use release_channel::ReleaseChannel;

+use std::ops::Deref;

+use std::path::{Path, PathBuf};

+use util::ResultExt;

+use workspace::notifications::{DetachAndPromptErr, NotificationId};

+use workspace::{Toast, Workspace};

+

+actions!(

+    cli,

+    [

+        /// Installs the Zed CLI tool to the system PATH.

+        InstallCliBinary,

+    ]

+);

+

+async fn install_script(cx: &AsyncApp) -> Result<PathBuf> {

+    let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??;

+    let link_path = Path::new("/usr/local/bin/zed");

+    let bin_dir_path = link_path.parent().unwrap();

+

+    // Don't re-create symlink if it points to the same CLI binary.

+    if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {

+        return Ok(link_path.into());

+    }

+

+    // If the symlink is not there or is outdated, first try replacing it

+    // without escalating.

+    smol::fs::remove_file(link_path).await.log_err();

+    if smol::fs::unix::symlink(&cli_path, link_path)

+        .await

+        .log_err()

+        .is_some()

+    {

+        return Ok(link_path.into());

+    }

+

+    // The symlink could not be created, so use osascript with admin privileges

+    // to create it.

+    let status = smol::process::Command::new("/usr/bin/osascript")

+        .args([

+            "-e",

+            &format!(

+                "do shell script \" \

+                    mkdir -p \'{}\' && \

+                    ln -sf \'{}\' \'{}\' \

+                \" with administrator privileges",

+                bin_dir_path.to_string_lossy(),

+                cli_path.to_string_lossy(),

+                link_path.to_string_lossy(),

+            ),

+        ])

+        .stdout(smol::process::Stdio::inherit())

+        .stderr(smol::process::Stdio::inherit())

+        .output()

+        .await?

+        .status;

+    anyhow::ensure!(status.success(), "error running osascript");

+    Ok(link_path.into())

+}

+

+pub fn install_cli_binary(window: &mut Window, cx: &mut Context<Workspace>) {

+    const LINUX_PROMPT_DETAIL: &str = "If you installed Zed from our official release add ~/.local/bin to your PATH.\n\nIf you installed Zed from a different source like your package manager, then you may need to create an alias/symlink manually.\n\nDepending on your package manager, the CLI might be named zeditor, zedit, zed-editor or something else.";

+

+    cx.spawn_in(window, async move |workspace, cx| {

+        if cfg!(any(target_os = "linux", target_os = "freebsd")) {

+            let prompt = cx.prompt(

+                PromptLevel::Warning,

+                "CLI should already be installed",

+                Some(LINUX_PROMPT_DETAIL),

+                &["Ok"],

+            );

+            cx.background_spawn(prompt).detach();

+            return Ok(());

+        }

+        let path = install_script(cx.deref())

+            .await

+            .context("error creating CLI symlink")?;

+

+        workspace.update_in(cx, |workspace, _, cx| {

+            struct InstalledZedCli;

+

+            workspace.show_toast(

+                Toast::new(

+                    NotificationId::unique::<InstalledZedCli>(),

+                    format!(

+                        "Installed `zed` to {}. You can launch {} from your terminal.",

+                        path.to_string_lossy(),

+                        ReleaseChannel::global(cx).display_name()

+                    ),

+                ),

+                cx,

+            )

+        })?;

+        register_zed_scheme(cx).await.log_err();

+        Ok(())

+    })

+    .detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None);

+}

crates/install_cli/src/register_zed_scheme.rs 🔗

@@ -0,0 +1,15 @@
+use client::ZED_URL_SCHEME;

+use gpui::{AsyncApp, actions};

+

+actions!(

+    cli,

+    [

+        /// Registers the zed:// URL scheme handler.

+        RegisterZedScheme

+    ]

+);

+

+pub async fn register_zed_scheme(cx: &AsyncApp) -> anyhow::Result<()> {

+    cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME))?

+        .await

+}

crates/zed/src/zed.rs 🔗

@@ -806,7 +806,6 @@ fn register_actions(
                 }
             }
         })
-        .register_action(install_cli)
         .register_action(|_, _: &install_cli::RegisterZedScheme, window, cx| {
             cx.spawn_in(window, async move |workspace, cx| {
                 install_cli::register_zed_scheme(cx).await?;
@@ -915,6 +914,9 @@ fn register_actions(
             capture_audio(workspace, window, cx);
         });
 
+    #[cfg(not(target_os = "windows"))]
+    workspace.register_action(install_cli);
+
     if workspace.project().read(cx).is_via_remote_server() {
         workspace.register_action({
             move |workspace, _: &OpenServerSettings, window, cx| {
@@ -1030,13 +1032,14 @@ fn about(
     .detach();
 }
 
+#[cfg(not(target_os = "windows"))]
 fn install_cli(
     _: &mut Workspace,
-    _: &install_cli::Install,
+    _: &install_cli::InstallCliBinary,
     window: &mut Window,
     cx: &mut Context<Workspace>,
 ) {
-    install_cli::install_cli(window, cx);
+    install_cli::install_cli_binary(window, cx)
 }
 
 static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false);

crates/zed/src/zed/app_menus.rs 🔗

@@ -39,7 +39,8 @@ pub fn app_menus() -> Vec<Menu> {
                 MenuItem::os_submenu("Services", gpui::SystemMenuType::Services),
                 MenuItem::separator(),
                 MenuItem::action("Extensions", zed_actions::Extensions::default()),
-                MenuItem::action("Install CLI", install_cli::Install),
+                #[cfg(not(target_os = "windows"))]
+                MenuItem::action("Install CLI", install_cli::InstallCliBinary),
                 MenuItem::separator(),
                 #[cfg(target_os = "macos")]
                 MenuItem::action("Hide Zed", super::Hide),