Make linux prompts a bit better (#13067)

Conrad Irwin created

Also prompt with a sensible error on install:cli

Release Notes:

- N/A

Change summary

Cargo.lock                          |   1 
crates/zed/Cargo.toml               |   1 
crates/zed/src/main.rs              |   3 
crates/zed/src/zed.rs               |  13 +++
crates/zed/src/zed/linux_prompts.rs | 120 +++++++++++++++++++++++++++++++
5 files changed, 138 insertions(+)

Detailed changes

Cargo.lock 🔗

@@ -13338,6 +13338,7 @@ dependencies = [
  "theme_selector",
  "tree-sitter-markdown",
  "tree-sitter-rust",
+ "ui",
  "urlencoding",
  "util",
  "uuid",

crates/zed/Cargo.toml 🔗

@@ -94,6 +94,7 @@ terminal_view.workspace = true
 theme.workspace = true
 theme_selector.workspace = true
 urlencoding = "2.1.2"
+ui.workspace = true
 util.workspace = true
 uuid.workspace = true
 vim.workspace = true

crates/zed/src/main.rs 🔗

@@ -168,6 +168,9 @@ fn init_ui(app_state: Arc<AppState>, cx: &mut AppContext) -> Result<()> {
     SystemAppearance::init(cx);
     load_embedded_fonts(cx);
 
+    #[cfg(target_os = "linux")]
+    crate::zed::linux_prompts::init(cx);
+
     theme::init(theme::LoadThemes::All(Box::new(Assets)), cx);
     app_state.languages.set_theme(cx.theme().clone());
     command_palette::init(cx);

crates/zed/src/zed.rs 🔗

@@ -1,5 +1,7 @@
 mod app_menus;
 pub mod inline_completion_registry;
+#[cfg(target_os = "linux")]
+pub(crate) mod linux_prompts;
 #[cfg(not(target_os = "linux"))]
 pub(crate) mod only_instance;
 mod open_listener;
@@ -262,9 +264,20 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
             .register_action(move |_, _: &ResetBufferFontSize, cx| theme::reset_font_size(cx))
             .register_action(|_, _: &install_cli::Install, cx| {
                 cx.spawn(|workspace, mut cx| async move {
+                    if cfg!(target_os = "linux") {
+                        let prompt = cx.prompt(
+                            PromptLevel::Warning,
+                            "Could not install the CLI",
+                            Some("If you installed Zed from our official release add ~/.local/bin to your PATH.\n\nIf you installed Zed from a different source you may need to create an alias/symlink manually."),
+                            &["Ok"],
+                        );
+                        cx.background_executor().spawn(prompt).detach();
+                        return Ok(());
+                    }
                     let path = install_cli::install_cli(cx.deref())
                         .await
                         .context("error creating CLI symlink")?;
+
                     workspace.update(&mut cx, |workspace, cx| {
                         struct InstalledZedCli;
 

crates/zed/src/zed/linux_prompts.rs 🔗

@@ -0,0 +1,120 @@
+use gpui::{
+    div, opaque_grey, AppContext, EventEmitter, FocusHandle, FocusableView, FontWeight,
+    InteractiveElement, IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse,
+    Render, RenderablePromptHandle, Styled, ViewContext, VisualContext, WindowContext,
+};
+use ui::{h_flex, v_flex, ButtonCommon, ButtonStyle, Clickable, ElevationIndex, LabelSize};
+use workspace::ui::StyledExt;
+
+pub fn init(cx: &mut AppContext) {
+    cx.set_prompt_builder(fallback_prompt_renderer)
+}
+/// Use this function in conjunction with [AppContext::set_prompt_renderer] to force
+/// GPUI to always use the fallback prompt renderer.
+pub fn fallback_prompt_renderer(
+    level: PromptLevel,
+    message: &str,
+    detail: Option<&str>,
+    actions: &[&str],
+    handle: PromptHandle,
+    cx: &mut WindowContext,
+) -> RenderablePromptHandle {
+    let renderer = cx.new_view({
+        |cx| FallbackPromptRenderer {
+            _level: level,
+            message: message.to_string(),
+            detail: detail.map(ToString::to_string),
+            actions: actions.iter().map(ToString::to_string).collect(),
+            focus: cx.focus_handle(),
+        }
+    });
+
+    handle.with_view(renderer, cx)
+}
+
+/// The default GPUI fallback for rendering prompts, when the platform doesn't support it.
+pub struct FallbackPromptRenderer {
+    _level: PromptLevel,
+    message: String,
+    detail: Option<String>,
+    actions: Vec<String>,
+    focus: FocusHandle,
+}
+
+impl Render for FallbackPromptRenderer {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let prompt =
+            v_flex()
+                .cursor_default()
+                .track_focus(&self.focus)
+                .elevation_3(cx)
+                .w_72()
+                .overflow_hidden()
+                .p_4()
+                .gap_4()
+                .font_family("Zed Sans")
+                .child(
+                    div()
+                        .w_full()
+                        .font_weight(FontWeight::BOLD)
+                        .child(self.message.clone())
+                        .text_color(ui::Color::Default.color(cx)),
+                )
+                .children(self.detail.clone().map(|detail| {
+                    div()
+                        .w_full()
+                        .text_xs()
+                        .text_color(ui::Color::Muted.color(cx))
+                        .child(detail)
+                }))
+                .child(h_flex().justify_end().gap_2().children(
+                    self.actions.iter().enumerate().map(|(ix, action)| {
+                        ui::Button::new(ix, action.clone())
+                            .label_size(LabelSize::Large)
+                            .style(ButtonStyle::Filled)
+                            .layer(ElevationIndex::ModalSurface)
+                            .on_click(cx.listener(move |_, _, cx| {
+                                cx.emit(PromptResponse(ix));
+                            }))
+                    }),
+                ));
+
+        div()
+            .size_full()
+            .occlude()
+            .child(
+                div()
+                    .size_full()
+                    .bg(opaque_grey(0.5, 0.6))
+                    .absolute()
+                    .top_0()
+                    .left_0(),
+            )
+            .child(
+                div()
+                    .size_full()
+                    .absolute()
+                    .top_0()
+                    .left_0()
+                    .flex()
+                    .flex_col()
+                    .justify_around()
+                    .child(
+                        div()
+                            .w_full()
+                            .flex()
+                            .flex_row()
+                            .justify_around()
+                            .child(prompt),
+                    ),
+            )
+    }
+}
+
+impl EventEmitter<PromptResponse> for FallbackPromptRenderer {}
+
+impl FocusableView for FallbackPromptRenderer {
+    fn focus_handle(&self, _: &crate::AppContext) -> FocusHandle {
+        self.focus.clone()
+    }
+}