tasks: Add status indicator to the status bar (#10267)

Piotr Osiewicz created

Release Notes:

- Added task status indicator to the status bar.

Change summary

Cargo.lock                              |  3 
crates/tasks_ui/Cargo.toml              |  3 
crates/tasks_ui/src/lib.rs              |  6 +
crates/tasks_ui/src/modal.rs            | 12 +-
crates/tasks_ui/src/settings.rs         | 34 +++++++++
crates/tasks_ui/src/status_indicator.rs | 98 +++++++++++++++++++++++++++
crates/zed/src/zed.rs                   |  3 
7 files changed, 153 insertions(+), 6 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9503,9 +9503,12 @@ dependencies = [
  "language",
  "picker",
  "project",
+ "schemars",
  "serde",
  "serde_json",
+ "settings",
  "task",
+ "terminal",
  "tree-sitter-rust",
  "tree-sitter-typescript",
  "ui",

crates/tasks_ui/Cargo.toml 🔗

@@ -17,9 +17,12 @@ gpui.workspace = true
 picker.workspace = true
 project.workspace = true
 task.workspace = true
+schemars.workspace = true
 serde.workspace = true
+settings.workspace = true
 ui.workspace = true
 util.workspace = true
+terminal.workspace = true
 workspace.workspace = true
 language.workspace = true
 itertools.workspace = true

crates/tasks_ui/src/lib.rs 🔗

@@ -1,5 +1,6 @@
 use std::{path::PathBuf, sync::Arc};
 
+use ::settings::Settings;
 use editor::Editor;
 use gpui::{AppContext, ViewContext, WeakView, WindowContext};
 use language::{Language, Point};
@@ -10,8 +11,13 @@ use util::ResultExt;
 use workspace::Workspace;
 
 mod modal;
+mod settings;
+mod status_indicator;
+
+pub use status_indicator::TaskStatusIndicator;
 
 pub fn init(cx: &mut AppContext) {
+    settings::TaskSettings::register(cx);
     cx.observe_new_views(
         |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
             workspace

crates/tasks_ui/src/modal.rs 🔗

@@ -30,6 +30,11 @@ pub struct Spawn {
     pub task_name: Option<String>,
 }
 
+impl Spawn {
+    pub(crate) fn modal() -> Self {
+        Self { task_name: None }
+    }
+}
 /// Rerun last task
 #[derive(PartialEq, Clone, Deserialize, Default)]
 pub struct Rerun {
@@ -144,16 +149,11 @@ impl TasksModal {
 }
 
 impl Render for TasksModal {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::prelude::IntoElement {
+    fn render(&mut self, _: &mut ViewContext<Self>) -> impl gpui::prelude::IntoElement {
         v_flex()
             .key_context("TasksModal")
             .w(rems(34.))
             .child(self.picker.clone())
-            .on_mouse_down_out(cx.listener(|modal, _, cx| {
-                modal.picker.update(cx, |picker, cx| {
-                    picker.cancel(&Default::default(), cx);
-                })
-            }))
     }
 }
 

crates/tasks_ui/src/settings.rs 🔗

@@ -0,0 +1,34 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, PartialEq, Default)]
+pub(crate) struct TaskSettings {
+    pub(crate) show_status_indicator: bool,
+}
+
+/// Task-related settings.
+#[derive(Serialize, Deserialize, PartialEq, Default, Clone, JsonSchema)]
+pub(crate) struct TaskSettingsContent {
+    /// Whether to show task status indicator in the status bar. Default: true
+    show_status_indicator: Option<bool>,
+}
+
+impl settings::Settings for TaskSettings {
+    const KEY: Option<&'static str> = Some("task");
+
+    type FileContent = TaskSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut gpui::AppContext,
+    ) -> gpui::Result<Self>
+    where
+        Self: Sized,
+    {
+        let this = Self::json_merge(default_value, user_values)?;
+        Ok(Self {
+            show_status_indicator: this.show_status_indicator.unwrap_or(true),
+        })
+    }
+}

crates/tasks_ui/src/status_indicator.rs 🔗

@@ -0,0 +1,98 @@
+use gpui::{IntoElement, Render, View, WeakView};
+use settings::Settings;
+use ui::{
+    div, ButtonCommon, Clickable, Color, FluentBuilder, IconButton, IconName, Tooltip,
+    VisualContext, WindowContext,
+};
+use workspace::{item::ItemHandle, StatusItemView, Workspace};
+
+use crate::{modal::Spawn, settings::TaskSettings};
+
+enum TaskStatus {
+    Failed,
+    Running,
+    Succeeded,
+}
+
+/// A status bar icon that surfaces the status of running tasks.
+/// It has a different color depending on the state of running tasks:
+/// - red if any open task tab failed
+/// - else, yellow if any open task tab is still running
+/// - else, green if there tasks tabs open, and they have all succeeded
+/// - else, no indicator if there are no open task tabs
+pub struct TaskStatusIndicator {
+    workspace: WeakView<Workspace>,
+}
+
+impl TaskStatusIndicator {
+    pub fn new(workspace: WeakView<Workspace>, cx: &mut WindowContext) -> View<Self> {
+        cx.new_view(|_| Self { workspace })
+    }
+    fn current_status(&self, cx: &mut WindowContext) -> Option<TaskStatus> {
+        self.workspace
+            .update(cx, |this, cx| {
+                let mut status = None;
+                let project = this.project().read(cx);
+
+                for handle in project.local_terminal_handles() {
+                    let Some(handle) = handle.upgrade() else {
+                        continue;
+                    };
+                    let handle = handle.read(cx);
+                    let task_state = handle.task();
+                    if let Some(state) = task_state {
+                        match state.status {
+                            terminal::TaskStatus::Running => {
+                                let _ = status.insert(TaskStatus::Running);
+                            }
+                            terminal::TaskStatus::Completed { success } => {
+                                if !success {
+                                    let _ = status.insert(TaskStatus::Failed);
+                                    return status;
+                                }
+                                status.get_or_insert(TaskStatus::Succeeded);
+                            }
+                            _ => {}
+                        };
+                    }
+                }
+                status
+            })
+            .ok()
+            .flatten()
+    }
+}
+
+impl Render for TaskStatusIndicator {
+    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
+        if !TaskSettings::get_global(cx).show_status_indicator {
+            return div().into_any_element();
+        }
+        let current_status = self.current_status(cx);
+        let color = current_status.map(|status| match status {
+            TaskStatus::Failed => Color::Error,
+            TaskStatus::Running => Color::Warning,
+            TaskStatus::Succeeded => Color::Success,
+        });
+        IconButton::new("tasks-activity-indicator", IconName::Play)
+            .when_some(color, |this, color| this.icon_color(color))
+            .on_click(cx.listener(|this, _, cx| {
+                this.workspace
+                    .update(cx, |this, cx| {
+                        crate::spawn_task_or_modal(this, &Spawn::modal(), cx)
+                    })
+                    .ok();
+            }))
+            .tooltip(|cx| Tooltip::for_action("Spawn tasks", &Spawn { task_name: None }, cx))
+            .into_any_element()
+    }
+}
+
+impl StatusItemView for TaskStatusIndicator {
+    fn set_active_pane_item(
+        &mut self,
+        _: Option<&dyn ItemHandle>,
+        _: &mut ui::prelude::ViewContext<Self>,
+    ) {
+    }
+}

crates/zed/src/zed.rs 🔗

@@ -127,6 +127,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
             cx.new_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
         let activity_indicator =
             activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
+        let tasks_indicator = tasks_ui::TaskStatusIndicator::new(workspace.weak_handle(), cx);
         let active_buffer_language =
             cx.new_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
         let vim_mode_indicator = cx.new_view(|cx| vim::ModeIndicator::new(cx));
@@ -136,6 +137,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
             status_bar.add_left_item(diagnostic_summary, cx);
             status_bar.add_left_item(activity_indicator, cx);
             status_bar.add_right_item(copilot, cx);
+            status_bar.add_right_item(tasks_indicator, cx);
             status_bar.add_right_item(active_buffer_language, cx);
             status_bar.add_right_item(vim_mode_indicator, cx);
             status_bar.add_right_item(cursor_position, cx);
@@ -3077,6 +3079,7 @@ mod tests {
             project_panel::init((), cx);
             terminal_view::init(cx);
             assistant::init(app_state.client.clone(), cx);
+            tasks_ui::init(cx);
             initialize_workspace(app_state.clone(), cx);
             app_state
         })