Move install_cli function to a seperate crate

Mikayla Maki created

Add install cli button to welcome experience
Add toast pop ups for CLI installation status

Change summary

Cargo.lock                            | 14 +++++++
Cargo.toml                            |  1 
crates/install_cli/Cargo.toml         | 18 +++++++++
crates/install_cli/src/install_cli.rs | 56 ++++++++++++++++++++++++++++
crates/welcome/Cargo.toml             |  1 
crates/welcome/src/welcome.rs         |  1 
crates/workspace/Cargo.toml           |  1 
crates/workspace/src/notifications.rs | 28 ++++++++++----
crates/workspace/src/workspace.rs     | 26 ++++++++++++-
crates/zed/Cargo.toml                 |  1 
crates/zed/src/menus.rs               |  2 
crates/zed/src/zed.rs                 | 57 ++--------------------------
12 files changed, 142 insertions(+), 64 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3018,6 +3018,17 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
 
+[[package]]
+name = "install_cli"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui",
+ "log",
+ "smol",
+ "util",
+]
+
 [[package]]
 name = "instant"
 version = "0.1.12"
@@ -8020,6 +8031,7 @@ dependencies = [
  "anyhow",
  "editor",
  "gpui",
+ "install_cli",
  "log",
  "project",
  "settings",
@@ -8304,6 +8316,7 @@ dependencies = [
  "futures 0.3.25",
  "gpui",
  "indoc",
+ "install_cli",
  "language",
  "lazy_static",
  "log",
@@ -8412,6 +8425,7 @@ dependencies = [
  "ignore",
  "image",
  "indexmap",
+ "install_cli",
  "isahc",
  "journal",
  "language",

Cargo.toml 🔗

@@ -26,6 +26,7 @@ members = [
     "crates/go_to_line",
     "crates/gpui",
     "crates/gpui_macros",
+    "crates/install_cli",
     "crates/journal",
     "crates/language",
     "crates/live_kit_client",

crates/install_cli/Cargo.toml 🔗

@@ -0,0 +1,18 @@
+[package]
+name = "install_cli"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/install_cli.rs"
+
+[features]
+test-support = []
+
+[dependencies]
+smol = "1.2.5"
+anyhow = "1.0.38"
+log = "0.4"
+gpui = { path = "../gpui" }
+util = { path = "../util" }

crates/install_cli/src/install_cli.rs 🔗

@@ -0,0 +1,56 @@
+use std::path::Path;
+
+use anyhow::{Result, anyhow};
+use gpui::{AsyncAppContext, actions};
+use util::ResultExt;
+
+actions!(cli, [ Install ]);
+
+
+pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
+    let cli_path = cx.platform().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(());
+    }
+
+    // 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(());
+    }
+
+    // The symlink could not be created, so use osascript with admin privileges
+    // to create it.
+    let status = smol::process::Command::new("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;
+    if status.success() {
+        Ok(())
+    } else {
+        Err(anyhow!("error running osascript"))
+    }
+}

crates/welcome/Cargo.toml 🔗

@@ -15,6 +15,7 @@ anyhow = "1.0.38"
 log = "0.4"
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
+install_cli = { path = "../install_cli" }
 project = { path = "../project" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }

crates/welcome/src/welcome.rs 🔗

@@ -66,6 +66,7 @@ impl View for WelcomePage {
                     .boxed(),
                     self.render_cta_button(2, "Choose a theme", theme_selector::Toggle, width, cx),
                     self.render_cta_button(3, "Choose a keymap", theme_selector::Toggle, width, cx),
+                    self.render_cta_button(4, "Install the CLI", install_cli::Install, width, cx),
                     self.render_settings_checkbox::<Metrics>(
                         "Do you want to send telemetry?",
                         &theme.welcome.checkbox,

crates/workspace/Cargo.toml 🔗

@@ -27,6 +27,7 @@ context_menu = { path = "../context_menu" }
 drag_and_drop = { path = "../drag_and_drop" }
 fs = { path = "../fs" }
 gpui = { path = "../gpui" }
+install_cli = { path = "../install_cli" }
 language = { path = "../language" }
 menu = { path = "../menu" }
 project = { path = "../project" }

crates/workspace/src/notifications.rs 🔗

@@ -122,6 +122,8 @@ impl Workspace {
 
 pub mod simple_message_notification {
 
+    use std::borrow::Cow;
+
     use gpui::{
         actions,
         elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
@@ -153,9 +155,9 @@ pub mod simple_message_notification {
     }
 
     pub struct MessageNotification {
-        message: String,
+        message: Cow<'static, str>,
         click_action: Option<Box<dyn Action>>,
-        click_message: Option<String>,
+        click_message: Option<Cow<'static, str>>,
     }
 
     pub enum MessageNotificationEvent {
@@ -167,23 +169,23 @@ pub mod simple_message_notification {
     }
 
     impl MessageNotification {
-        pub fn new_message<S: AsRef<str>>(message: S) -> MessageNotification {
+        pub fn new_message<S: Into<Cow<'static, str>>>(message: S) -> MessageNotification {
             Self {
-                message: message.as_ref().to_string(),
+                message: message.into(),
                 click_action: None,
                 click_message: None,
             }
         }
 
-        pub fn new<S1: AsRef<str>, A: Action, S2: AsRef<str>>(
+        pub fn new<S1: Into<Cow<'static, str>>, A: Action, S2: Into<Cow<'static, str>>>(
             message: S1,
             click_action: A,
             click_message: S2,
         ) -> Self {
             Self {
-                message: message.as_ref().to_string(),
+                message: message.into(),
                 click_action: Some(Box::new(click_action) as Box<dyn Action>),
-                click_message: Some(click_message.as_ref().to_string()),
+                click_message: Some(click_message.into()),
             }
         }
 
@@ -210,6 +212,8 @@ pub mod simple_message_notification {
             let click_message = self.click_message.as_ref().map(|message| message.clone());
             let message = self.message.clone();
 
+            let has_click_action = click_action.is_some();
+
             MouseEventHandler::<MessageNotificationTag>::new(0, cx, |state, cx| {
                 Flex::column()
                     .with_child(
@@ -243,6 +247,7 @@ pub mod simple_message_notification {
                                 .on_click(MouseButton::Left, move |_, cx| {
                                     cx.dispatch_action(CancelMessageNotification)
                                 })
+                                .with_cursor_style(CursorStyle::PointingHand)
                                 .aligned()
                                 .constrained()
                                 .with_height(
@@ -272,12 +277,19 @@ pub mod simple_message_notification {
                     .contained()
                     .boxed()
             })
-            .with_cursor_style(CursorStyle::PointingHand)
+            // Since we're not using a proper overlay, we have to capture these extra events
+            .on_down(MouseButton::Left, |_, _| {})
+            .on_up(MouseButton::Left, |_, _| {})
             .on_click(MouseButton::Left, move |_, cx| {
                 if let Some(click_action) = click_action.as_ref() {
                     cx.dispatch_any_action(click_action.boxed_clone())
                 }
             })
+            .with_cursor_style(if has_click_action {
+                CursorStyle::PointingHand
+            } else {
+                CursorStyle::Arrow
+            })
             .boxed()
         }
     }

crates/workspace/src/workspace.rs 🔗

@@ -17,7 +17,7 @@ mod toolbar;
 
 pub use smallvec;
 
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Result, Context};
 use call::ActiveCall;
 use client::{
     proto::{self, PeerId},
@@ -65,7 +65,7 @@ use crate::{
 };
 use lazy_static::lazy_static;
 use log::{error, warn};
-use notifications::NotificationHandle;
+use notifications::{NotificationHandle, NotifyResultExt};
 pub use pane::*;
 pub use pane_group::*;
 use persistence::{model::SerializedItem, DB};
@@ -267,6 +267,28 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
                 })
         },
     );
+    
+    cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
+        cx.spawn(|workspace, mut cx| async move {
+            let err = install_cli::install_cli(&cx).await.context("Failed to create CLI symlink");
+                        
+            cx.update(|cx| {
+                workspace.update(cx, |workspace, cx| {
+                    if matches!(err, Err(_)) {
+                        err.notify_err(workspace, cx);
+                    } else {
+                        workspace.show_notification(1, cx, |cx| {
+                            cx.add_view(|_| MessageNotification::new_message("Successfully installed the 'zed' binary"))
+                        });
+                    }
+                })
+            })
+        
+        }).detach();
+        
+        
+        
+    });
 
     let client = &app_state.client;
     client.add_view_request_handler(Workspace::handle_follow);

crates/zed/Cargo.toml 🔗

@@ -39,6 +39,7 @@ fsevent = { path = "../fsevent" }
 fuzzy = { path = "../fuzzy" }
 go_to_line = { path = "../go_to_line" }
 gpui = { path = "../gpui" }
+install_cli = { path = "../install_cli" }
 journal = { path = "../journal" }
 language = { path = "../language" }
 lsp = { path = "../lsp" }

crates/zed/src/menus.rs 🔗

@@ -19,7 +19,7 @@ pub fn menus() -> Vec<Menu<'static>> {
                         MenuItem::action("Select Theme", theme_selector::Toggle),
                     ],
                 }),
-                MenuItem::action("Install CLI", super::InstallCommandLineInterface),
+                MenuItem::action("Install CLI", install_cli::Install),
                 MenuItem::separator(),
                 MenuItem::action("Hide Zed", super::Hide),
                 MenuItem::action("Hide Others", super::HideOthers),

crates/zed/src/zed.rs 🔗

@@ -2,7 +2,7 @@ pub mod languages;
 pub mod menus;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
-use anyhow::{anyhow, Context, Result};
+use anyhow::Context;
 use assets::Assets;
 use breadcrumbs::Breadcrumbs;
 pub use client;
@@ -21,7 +21,7 @@ use gpui::{
     geometry::vector::vec2f,
     impl_actions,
     platform::{WindowBounds, WindowOptions},
-    AssetSource, AsyncAppContext, Platform, PromptLevel, TitlebarOptions, ViewContext, WindowKind,
+    AssetSource,  Platform, PromptLevel, TitlebarOptions, ViewContext, WindowKind,
 };
 use language::Rope;
 pub use lsp;
@@ -68,7 +68,6 @@ actions!(
         IncreaseBufferFontSize,
         DecreaseBufferFontSize,
         ResetBufferFontSize,
-        InstallCommandLineInterface,
         ResetDatabase,
         WelcomeExperience
     ]
@@ -144,8 +143,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
             cx.refresh_windows();
         });
     });
-    cx.add_global_action(move |_: &InstallCommandLineInterface, cx| {
-        cx.spawn(|cx| async move { install_cli(&cx).await.context("error creating CLI symlink") })
+    cx.add_global_action(move |_: &install_cli::Install, cx| {
+        cx.spawn(|cx| async move { install_cli::install_cli(&cx).await.context("error creating CLI symlink") })
             .detach_and_log_err(cx);
     });
     cx.add_action({
@@ -505,54 +504,6 @@ fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
     );
 }
 
-async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
-    let cli_path = cx.platform().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(());
-    }
-
-    // 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(());
-    }
-
-    // The symlink could not be created, so use osascript with admin privileges
-    // to create it.
-    let status = smol::process::Command::new("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;
-    if status.success() {
-        Ok(())
-    } else {
-        Err(anyhow!("error running osascript"))
-    }
-}
-
 fn open_config_file(
     path: &'static Path,
     app_state: Arc<AppState>,