Cargo.lock 🔗
@@ -9503,9 +9503,12 @@ dependencies = [
"language",
"picker",
"project",
+ "schemars",
"serde",
"serde_json",
+ "settings",
"task",
+ "terminal",
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",
Piotr Osiewicz created
Release Notes:
- Added task status indicator to the status bar.
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(-)
@@ -9503,9 +9503,12 @@ dependencies = [
"language",
"picker",
"project",
+ "schemars",
"serde",
"serde_json",
+ "settings",
"task",
+ "terminal",
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",
@@ -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
@@ -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
@@ -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);
- })
- }))
}
}
@@ -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),
+ })
+ }
+}
@@ -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>,
+ ) {
+ }
+}
@@ -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
})