Small improvements of the task terminal spawn behavior (#9399)

Kirill Bulatov and Piotr Osiewicz created

* Add a `reveal: always|never` field in task definitions from tasks.json
, allowing to customize task terminal behavior on spawn
* Ensure reveal: always reveals the terminal even if the old task is
already running


Release Notes:

- Added a `reveal: always|never` (`always` is a default) field in task
definitions from tasks.json , allowing to customize task terminal
behavior on spawn

---------

Co-authored-by: Piotr Osiewicz <piotr@zed.dev>

Change summary

assets/settings/initial_tasks.json         |  7 ++
crates/task/src/lib.rs                     |  3 +
crates/task/src/oneshot_source.rs          |  5 +
crates/task/src/static_source.rs           | 18 ++++++
crates/terminal/src/terminal.rs            |  3 
crates/terminal_view/src/terminal_panel.rs | 62 +++++++++++++++++------
6 files changed, 79 insertions(+), 19 deletions(-)

Detailed changes

assets/settings/initial_tasks.json 🔗

@@ -5,6 +5,7 @@
   {
     "label": "Example task",
     "command": "for i in {1..5}; do echo \"Hello $i/5\"; sleep 1; done",
+    //"args": [],
     // Env overrides for the command, will be appended to the terminal's environment from the settings.
     "env": { "foo": "bar" },
     // Current working directory to spawn the command into, defaults to current project root.
@@ -12,6 +13,10 @@
     // Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`.
     "use_new_terminal": false,
     // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
-    "allow_concurrent_runs": false
+    "allow_concurrent_runs": false,
+    // What to do with the terminal pane and tab, after the command was started:
+    // * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
+    // * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
+    "reveal": "always"
   }
 ]

crates/task/src/lib.rs 🔗

@@ -6,6 +6,7 @@ pub mod static_source;
 
 use collections::HashMap;
 use gpui::ModelContext;
+use static_source::RevealStrategy;
 use std::any::Any;
 use std::path::{Path, PathBuf};
 use std::sync::Arc;
@@ -34,6 +35,8 @@ pub struct SpawnInTerminal {
     pub use_new_terminal: bool,
     /// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish.
     pub allow_concurrent_runs: bool,
+    /// What to do with the terminal pane and tab, after the command was started.
+    pub reveal: RevealStrategy,
 }
 
 /// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function)

crates/task/src/oneshot_source.rs 🔗

@@ -2,7 +2,9 @@
 
 use std::sync::Arc;
 
-use crate::{SpawnInTerminal, Task, TaskContext, TaskId, TaskSource};
+use crate::{
+    static_source::RevealStrategy, SpawnInTerminal, Task, TaskContext, TaskId, TaskSource,
+};
 use gpui::{AppContext, Context, Model};
 
 /// A storage and source of tasks generated out of user command prompt inputs.
@@ -48,6 +50,7 @@ impl Task for OneshotTask {
             env,
             use_new_terminal: Default::default(),
             allow_concurrent_runs: Default::default(),
+            reveal: RevealStrategy::default(),
         })
     }
 }

crates/task/src/static_source.rs 🔗

@@ -38,6 +38,7 @@ impl Task for StaticTask {
             label: self.definition.label.clone(),
             command: self.definition.command.clone(),
             args: self.definition.args.clone(),
+            reveal: self.definition.reveal,
             env: definition_env,
         })
     }
@@ -64,6 +65,7 @@ pub struct StaticSource {
 
 /// Static task definition from the tasks config file.
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
 pub(crate) struct Definition {
     /// Human readable name of the task to display in the UI.
     pub label: String,
@@ -84,6 +86,22 @@ pub(crate) struct Definition {
     /// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish.
     #[serde(default)]
     pub allow_concurrent_runs: bool,
+    /// What to do with the terminal pane and tab, after the command was started:
+    /// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
+    /// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
+    #[serde(default)]
+    pub reveal: RevealStrategy,
+}
+
+/// What to do with the terminal pane and tab, after the command was started.
+#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum RevealStrategy {
+    /// Always show the terminal pane, add and focus the corresponding task's tab in it.
+    #[default]
+    Always,
+    /// Do not change terminal pane focus, but still add/reuse the task's tab there.
+    Never,
 }
 
 /// A group of Tasks defined in a JSON file.

crates/terminal/src/terminal.rs 🔗

@@ -39,7 +39,7 @@ use pty_info::PtyProcessInfo;
 use serde::{Deserialize, Serialize};
 use settings::Settings;
 use smol::channel::{Receiver, Sender};
-use task::TaskId;
+use task::{static_source::RevealStrategy, TaskId};
 use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings};
 use theme::{ActiveTheme, Theme};
 use util::truncate_and_trailoff;
@@ -289,6 +289,7 @@ pub struct SpawnTask {
     pub command: String,
     pub args: Vec<String>,
     pub env: HashMap<String, String>,
+    pub reveal: RevealStrategy,
 }
 
 // https://github.com/alacritty/alacritty/blob/cb3a79dbf6472740daca8440d5166c1d4af5029e/extra/man/alacritty.5.scd?plain=1#L207-L213

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -14,7 +14,7 @@ use project::{Fs, ProjectEntryId};
 use search::{buffer_search::DivRegistrar, BufferSearchBar};
 use serde::{Deserialize, Serialize};
 use settings::Settings;
-use task::{SpawnInTerminal, TaskId};
+use task::{static_source::RevealStrategy, SpawnInTerminal, TaskId};
 use terminal::{
     terminal_settings::{Shell, TerminalDockPosition, TerminalSettings},
     SpawnTask,
@@ -303,6 +303,7 @@ impl TerminalPanel {
             command: spawn_in_terminal.command.clone(),
             args: spawn_in_terminal.args.clone(),
             env: spawn_in_terminal.env.clone(),
+            reveal: spawn_in_terminal.reveal,
         };
         // Set up shell args unconditionally, as tasks are always spawned inside of a shell.
         let Some((shell, mut user_args)) = (match TerminalSettings::get_global(cx).shell.clone() {
@@ -322,6 +323,7 @@ impl TerminalPanel {
         spawn_task.command = shell;
         user_args.extend(["-i".to_owned(), "-c".to_owned(), command]);
         spawn_task.args = user_args;
+        let reveal = spawn_task.reveal;
 
         let working_directory = spawn_in_terminal.cwd.clone();
         let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
@@ -379,6 +381,20 @@ impl TerminalPanel {
                         .ok();
                 }),
             );
+
+            match reveal {
+                RevealStrategy::Always => {
+                    self.activate_terminal_view(existing_item_index, cx);
+                    let task_workspace = self.workspace.clone();
+                    cx.spawn(|_, mut cx| async move {
+                        task_workspace
+                            .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
+                            .ok()
+                    })
+                    .detach();
+                }
+                RevealStrategy::Never => {}
+            }
         }
     }
 
@@ -388,14 +404,20 @@ impl TerminalPanel {
         working_directory: Option<PathBuf>,
         cx: &mut ViewContext<Self>,
     ) {
+        let reveal = spawn_task.reveal;
         self.add_terminal(working_directory, Some(spawn_task), cx);
-        let task_workspace = self.workspace.clone();
-        cx.spawn(|_, mut cx| async move {
-            task_workspace
-                .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
-                .ok()
-        })
-        .detach();
+        match reveal {
+            RevealStrategy::Always => {
+                let task_workspace = self.workspace.clone();
+                cx.spawn(|_, mut cx| async move {
+                    task_workspace
+                        .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
+                        .ok()
+                })
+                .detach();
+            }
+            RevealStrategy::Never => {}
+        }
     }
 
     ///Create a new Terminal in the current working directory or the user's home directory
@@ -542,6 +564,7 @@ impl TerminalPanel {
             .workspace
             .update(cx, |workspace, _| workspace.project().clone())
             .ok()?;
+        let reveal = spawn_task.reveal;
         let window = cx.window_handle();
         let new_terminal = project.update(cx, |project, cx| {
             project
@@ -551,14 +574,21 @@ impl TerminalPanel {
         terminal_to_replace.update(cx, |terminal_to_replace, cx| {
             terminal_to_replace.set_terminal(new_terminal, cx);
         });
-        self.activate_terminal_view(terminal_item_index, cx);
-        let task_workspace = self.workspace.clone();
-        cx.spawn(|_, mut cx| async move {
-            task_workspace
-                .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
-                .ok()
-        })
-        .detach();
+
+        match reveal {
+            RevealStrategy::Always => {
+                self.activate_terminal_view(terminal_item_index, cx);
+                let task_workspace = self.workspace.clone();
+                cx.spawn(|_, mut cx| async move {
+                    task_workspace
+                        .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
+                        .ok()
+                })
+                .detach();
+            }
+            RevealStrategy::Never => {}
+        }
+
         Some(())
     }
 }