Extend task templates with `shell` and `hide` fields to use custom shells and custom close behavior (#15031)

Kirill Bulatov created

Change summary

assets/settings/initial_tasks.json         |  23 ++++
crates/project/src/project.rs              |  51 ++++++++++-
crates/project/src/terminals.rs            |   6 
crates/proto/proto/zed.proto               |  27 +++++
crates/recent_projects/src/dev_servers.rs  |   5 
crates/remote/src/ssh_session.rs           |  10 +-
crates/task/src/lib.rs                     |  25 +++++
crates/task/src/task_template.rs           |  27 +++++
crates/terminal/src/terminal.rs            |  30 ++++--
crates/terminal/src/terminal_settings.rs   | 107 -----------------------
crates/terminal_view/src/terminal_panel.rs | 106 +++++++++++++++++++++-
crates/workspace/src/tasks.rs              |   2 
crates/workspace/src/workspace.rs          |   2 
crates/zed/src/zed.rs                      |   6 
docs/src/tasks.md                          |  23 ++++
15 files changed, 302 insertions(+), 148 deletions(-)

Detailed changes

assets/settings/initial_tasks.json 🔗

@@ -17,6 +17,27 @@
     // 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"
+    "reveal": "always",
+    // What to do with the terminal pane and tab, after the command had finished:
+    // * `never` — Do nothing when the command finishes (default)
+    // * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it
+    // * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`
+    "hide": "never",
+    // Which shell to use when running a task inside the terminal.
+    // May take 3 values:
+    // 1. (default) Use the system's default terminal configuration in /etc/passwd
+    //      "shell": "system"
+    // 2. A program:
+    //      "shell": {
+    //        "program": "sh"
+    //      }
+    // 3. A program with arguments:
+    //     "shell": {
+    //         "with_arguments": {
+    //           "program": "/bin/bash",
+    //           "arguments": ["--login"]
+    //         }
+    //     }
+    "shell": "system"
   }
 ]

crates/project/src/project.rs 🔗

@@ -110,7 +110,7 @@ use std::{
 };
 use task::{
     static_source::{StaticSource, TrackedFile},
-    RevealStrategy, TaskContext, TaskTemplate, TaskVariables, VariableName,
+    HideStrategy, RevealStrategy, Shell, TaskContext, TaskTemplate, TaskVariables, VariableName,
 };
 use terminals::Terminals;
 use text::{Anchor, BufferId, LineEnding};
@@ -9587,9 +9587,25 @@ impl Project {
                     use_new_terminal: template.use_new_terminal,
                     allow_concurrent_runs: template.allow_concurrent_runs,
                     reveal: match template.reveal {
-                        RevealStrategy::Always => proto::RevealStrategy::Always as i32,
-                        RevealStrategy::Never => proto::RevealStrategy::Never as i32,
+                        RevealStrategy::Always => proto::RevealStrategy::RevealAlways as i32,
+                        RevealStrategy::Never => proto::RevealStrategy::RevealNever as i32,
                     },
+                    hide: match template.hide {
+                        HideStrategy::Always => proto::HideStrategy::HideAlways as i32,
+                        HideStrategy::Never => proto::HideStrategy::HideNever as i32,
+                        HideStrategy::OnSuccess => proto::HideStrategy::HideOnSuccess as i32,
+                    },
+                    shell: Some(proto::Shell {
+                        shell_type: Some(match template.shell {
+                            Shell::System => proto::shell::ShellType::System(proto::System {}),
+                            Shell::Program(program) => proto::shell::ShellType::Program(program),
+                            Shell::WithArguments { program, args } => {
+                                proto::shell::ShellType::WithArguments(
+                                    proto::shell::WithArguments { program, args },
+                                )
+                            }
+                        }),
+                    }),
                     tags: template.tags,
                 });
                 proto::TemplatePair { kind, template }
@@ -10628,10 +10644,31 @@ impl Project {
 
                     let proto_template = template_pair.template?;
                     let reveal = match proto::RevealStrategy::from_i32(proto_template.reveal)
-                        .unwrap_or(proto::RevealStrategy::Always)
+                        .unwrap_or(proto::RevealStrategy::RevealAlways)
                     {
-                        proto::RevealStrategy::Always => RevealStrategy::Always,
-                        proto::RevealStrategy::Never => RevealStrategy::Never,
+                        proto::RevealStrategy::RevealAlways => RevealStrategy::Always,
+                        proto::RevealStrategy::RevealNever => RevealStrategy::Never,
+                    };
+                    let hide = match proto::HideStrategy::from_i32(proto_template.hide)
+                        .unwrap_or(proto::HideStrategy::HideNever)
+                    {
+                        proto::HideStrategy::HideAlways => HideStrategy::Always,
+                        proto::HideStrategy::HideNever => HideStrategy::Never,
+                        proto::HideStrategy::HideOnSuccess => HideStrategy::OnSuccess,
+                    };
+                    let shell = match proto_template
+                        .shell
+                        .and_then(|shell| shell.shell_type)
+                        .unwrap_or(proto::shell::ShellType::System(proto::System {}))
+                    {
+                        proto::shell::ShellType::System(_) => Shell::System,
+                        proto::shell::ShellType::Program(program) => Shell::Program(program),
+                        proto::shell::ShellType::WithArguments(with_arguments) => {
+                            Shell::WithArguments {
+                                program: with_arguments.program,
+                                args: with_arguments.args,
+                            }
+                        }
                     };
                     let task_template = TaskTemplate {
                         label: proto_template.label,
@@ -10642,6 +10679,8 @@ impl Project {
                         use_new_terminal: proto_template.use_new_terminal,
                         allow_concurrent_runs: proto_template.allow_concurrent_runs,
                         reveal,
+                        hide,
+                        shell,
                         tags: proto_template.tags,
                     };
                     Some((task_source_kind, task_template))

crates/project/src/terminals.rs 🔗

@@ -13,9 +13,9 @@ use std::{
     io::Write,
     path::{Path, PathBuf},
 };
-use task::{SpawnInTerminal, TerminalWorkDir};
+use task::{Shell, SpawnInTerminal, TerminalWorkDir};
 use terminal::{
-    terminal_settings::{self, Shell, TerminalSettings, VenvSettingsContent},
+    terminal_settings::{self, TerminalSettings, VenvSettingsContent},
     TaskState, TaskStatus, Terminal, TerminalBuilder,
 };
 use util::ResultExt;
@@ -131,6 +131,7 @@ impl Project {
                         full_label: spawn_task.full_label,
                         label: spawn_task.label,
                         command_label: spawn_task.command_label,
+                        hide: spawn_task.hide,
                         status: TaskStatus::Running,
                         completion_rx,
                     }),
@@ -155,6 +156,7 @@ impl Project {
                             full_label: spawn_task.full_label,
                             label: spawn_task.label,
                             command_label: spawn_task.command_label,
+                            hide: spawn_task.hide,
                             status: TaskStatus::Running,
                             completion_rx,
                         }),

crates/proto/proto/zed.proto 🔗

@@ -2284,12 +2284,35 @@ message TaskTemplate {
     bool use_new_terminal = 6;
     bool allow_concurrent_runs = 7;
     RevealStrategy reveal = 8;
+    HideStrategy hide = 10;
     repeated string tags = 9;
+    Shell shell = 11;
 }
 
+message Shell {
+    message WithArguments {
+        string program = 1;
+        repeated string args = 2;
+    }
+
+    oneof shell_type {
+        System system = 1;
+        string program = 2;
+        WithArguments with_arguments = 3;
+    }
+}
+
+message System {}
+
 enum RevealStrategy {
-    Always = 0;
-    Never = 1;
+    RevealAlways = 0;
+    RevealNever = 1;
+}
+
+enum HideStrategy {
+    HideAlways = 0;
+    HideNever = 1;
+    HideOnSuccess = 2;
 }
 
 message TaskSourceKind {

crates/recent_projects/src/dev_servers.rs 🔗

@@ -20,6 +20,7 @@ use rpc::{
     proto::{CreateDevServerResponse, DevServerStatus},
     ErrorCode, ErrorExt,
 };
+use task::HideStrategy;
 use task::RevealStrategy;
 use task::SpawnInTerminal;
 use task::TerminalWorkDir;
@@ -1191,10 +1192,12 @@ pub async fn spawn_ssh_task(
                         ssh_command: ssh_connection_string,
                         path: None,
                     }),
-                    env: Default::default(),
                     use_new_terminal: true,
                     allow_concurrent_runs: false,
                     reveal: RevealStrategy::Always,
+                    hide: HideStrategy::Never,
+                    env: Default::default(),
+                    shell: Default::default(),
                 },
                 cx,
             )

crates/remote/src/ssh_session.rs 🔗

@@ -412,11 +412,11 @@ impl ProtoClient for SshSession {
 impl SshClientState {
     #[cfg(not(unix))]
     async fn new(
-        user: String,
-        host: String,
-        port: u16,
-        delegate: Arc<dyn SshClientDelegate>,
-        cx: &mut AsyncAppContext,
+        _user: String,
+        _host: String,
+        _port: u16,
+        _delegate: Arc<dyn SshClientDelegate>,
+        _cx: &mut AsyncAppContext,
     ) -> Result<Self> {
         Err(anyhow!("ssh is not supported on this platform"))
     }

crates/task/src/lib.rs 🔗

@@ -7,12 +7,13 @@ mod vscode_format;
 
 use collections::{hash_map, HashMap, HashSet};
 use gpui::SharedString;
+use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::path::PathBuf;
 use std::str::FromStr;
 use std::{borrow::Cow, path::Path};
 
-pub use task_template::{RevealStrategy, TaskTemplate, TaskTemplates};
+pub use task_template::{HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates};
 pub use vscode_format::VsCodeTaskFile;
 
 /// Task identifier, unique within the application.
@@ -78,6 +79,10 @@ pub struct SpawnInTerminal {
     pub allow_concurrent_runs: bool,
     /// What to do with the terminal pane and tab, after the command was started.
     pub reveal: RevealStrategy,
+    /// What to do with the terminal pane and tab, after the command had finished.
+    pub hide: HideStrategy,
+    /// Which shell to use when spawning the task.
+    pub shell: Shell,
 }
 
 /// A final form of the [`TaskTemplate`], that got resolved with a particualar [`TaskContext`] and now is ready to spawn the actual task.
@@ -271,3 +276,21 @@ pub struct TaskContext {
 /// This is a new type representing a 'tag' on a 'runnable symbol', typically a test of main() function, found via treesitter.
 #[derive(Clone, Debug)]
 pub struct RunnableTag(pub SharedString);
+
+/// Shell configuration to open the terminal with.
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum Shell {
+    /// Use the system's default terminal configuration in /etc/passwd
+    #[default]
+    System,
+    /// Use a specific program with no arguments.
+    Program(String),
+    /// Use a specific program with arguments.
+    WithArguments {
+        /// The program to run.
+        program: String,
+        /// The arguments to pass to the program.
+        args: Vec<String>,
+    },
+}

crates/task/src/task_template.rs 🔗

@@ -8,7 +8,7 @@ use sha2::{Digest, Sha256};
 use util::{truncate_and_remove_front, ResultExt};
 
 use crate::{
-    ResolvedTask, SpawnInTerminal, TaskContext, TaskId, TerminalWorkDir, VariableName,
+    ResolvedTask, Shell, SpawnInTerminal, TaskContext, TaskId, TerminalWorkDir, VariableName,
     ZED_VARIABLE_NAME_PREFIX,
 };
 
@@ -45,10 +45,18 @@ pub struct TaskTemplate {
     /// * `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 had finished:
+    /// * `never` — do nothing when the command finishes (default)
+    /// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it
+    /// * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`.
+    #[serde(default)]
+    pub hide: HideStrategy,
     /// Represents the tags which this template attaches to. Adding this removes this task from other UI.
     #[serde(default)]
     pub tags: Vec<String>,
+    /// Which shell to use when spawning the task.
+    #[serde(default)]
+    pub shell: Shell,
 }
 
 /// What to do with the terminal pane and tab, after the command was started.
@@ -62,6 +70,19 @@ pub enum RevealStrategy {
     Never,
 }
 
+/// What to do with the terminal pane and tab, after the command has finished.
+#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum HideStrategy {
+    /// Do nothing when the command finishes.
+    #[default]
+    Never,
+    /// Always hide the terminal tab, hide the pane also if it was the last tab in it.
+    Always,
+    /// Hide the terminal tab on task success only, otherwise behaves similar to `Always`.
+    OnSuccess,
+}
+
 /// A group of Tasks defined in a JSON file.
 #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 pub struct TaskTemplates(pub Vec<TaskTemplate>);
@@ -194,6 +215,8 @@ impl TaskTemplate {
                 use_new_terminal: self.use_new_terminal,
                 allow_concurrent_runs: self.allow_concurrent_runs,
                 reveal: self.reveal,
+                hide: self.hide,
+                shell: self.shell.clone(),
             }),
         })
     }

crates/terminal/src/terminal.rs 🔗

@@ -39,8 +39,8 @@ use pty_info::PtyProcessInfo;
 use serde::{Deserialize, Serialize};
 use settings::Settings;
 use smol::channel::{Receiver, Sender};
-use task::TaskId;
-use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings};
+use task::{HideStrategy, Shell, TaskId};
+use terminal_settings::{AlternateScroll, TerminalBlink, TerminalSettings};
 use theme::{ActiveTheme, Theme};
 use util::truncate_and_trailoff;
 
@@ -612,6 +612,7 @@ pub struct TaskState {
     pub command_label: String,
     pub status: TaskStatus,
     pub completion_rx: Receiver<()>,
+    pub hide: HideStrategy,
 }
 
 /// A status of the current terminal tab's task.
@@ -1567,32 +1568,43 @@ impl Terminal {
             }
         };
 
-        let (task_line, command_line) = task_summary(task, error_code);
+        let (finished_successfully, task_line, command_line) = task_summary(task, error_code);
         // SAFETY: the invocation happens on non `TaskStatus::Running` tasks, once,
         // after either `AlacTermEvent::Exit` or `AlacTermEvent::ChildExit` events that are spawned
         // when Zed task finishes and no more output is made.
         // After the task summary is output once, no more text is appended to the terminal.
         unsafe { append_text_to_term(&mut self.term.lock(), &[&task_line, &command_line]) };
+        match task.hide {
+            HideStrategy::Never => {}
+            HideStrategy::Always => {
+                cx.emit(Event::CloseTerminal);
+            }
+            HideStrategy::OnSuccess => {
+                if finished_successfully {
+                    cx.emit(Event::CloseTerminal);
+                }
+            }
+        }
     }
 }
 
 const TASK_DELIMITER: &str = "⏵ ";
-fn task_summary(task: &TaskState, error_code: Option<i32>) -> (String, String) {
+fn task_summary(task: &TaskState, error_code: Option<i32>) -> (bool, String, String) {
     let escaped_full_label = task.full_label.replace("\r\n", "\r").replace('\n', "\r");
-    let task_line = match error_code {
+    let (success, task_line) = match error_code {
         Some(0) => {
-            format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished successfully")
+            (true, format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished successfully"))
         }
         Some(error_code) => {
-            format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished with non-zero error code: {error_code}")
+            (false, format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished with non-zero error code: {error_code}"))
         }
         None => {
-            format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished")
+            (false, format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished"))
         }
     };
     let escaped_command_label = task.command_label.replace("\r\n", "\r").replace('\n', "\r");
     let command_line = format!("{TASK_DELIMITER}Command: '{escaped_command_label}'");
-    (task_line, command_line)
+    (success, task_line, command_line)
 }
 
 /// Appends a stringified task summary to the terminal, after its output.

crates/terminal/src/terminal_settings.rs 🔗

@@ -9,6 +9,7 @@ use serde_derive::{Deserialize, Serialize};
 use serde_json::Value;
 use settings::{SettingsJsonSchemaParams, SettingsSources};
 use std::path::PathBuf;
+use task::Shell;
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
@@ -256,60 +257,6 @@ pub enum TerminalBlink {
     On,
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum Shell {
-    /// Use the system's default terminal configuration in /etc/passwd
-    System,
-    Program(String),
-    WithArguments {
-        program: String,
-        args: Vec<String>,
-    },
-}
-
-impl Shell {
-    pub fn retrieve_system_shell() -> Option<String> {
-        #[cfg(not(target_os = "windows"))]
-        {
-            use anyhow::Context;
-            use util::ResultExt;
-
-            return std::env::var("SHELL")
-                .context("Error finding SHELL in env.")
-                .log_err();
-        }
-        // `alacritty_terminal` uses this as default on Windows. See:
-        // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
-        #[cfg(target_os = "windows")]
-        return Some("powershell".to_owned());
-    }
-
-    /// Convert unix-shell variable syntax to windows-shell syntax.
-    /// `powershell` and `cmd` are considered valid here.
-    #[cfg(target_os = "windows")]
-    pub fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
-        match shell_type {
-            WindowsShellType::Powershell => to_powershell_variable(input),
-            WindowsShellType::Cmd => to_cmd_variable(input),
-            WindowsShellType::Other => input,
-        }
-    }
-
-    #[cfg(target_os = "windows")]
-    pub fn to_windows_shell_type(shell: &str) -> WindowsShellType {
-        if shell == "powershell" || shell.ends_with("powershell.exe") {
-            WindowsShellType::Powershell
-        } else if shell == "cmd" || shell.ends_with("cmd.exe") {
-            WindowsShellType::Cmd
-        } else {
-            // Someother shell detected, the user might install and use a
-            // unix-like shell.
-            WindowsShellType::Other
-        }
-    }
-}
-
 #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum AlternateScroll {
@@ -341,55 +288,3 @@ pub struct ToolbarContent {
     /// Default: true
     pub title: Option<bool>,
 }
-
-#[cfg(target_os = "windows")]
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum WindowsShellType {
-    Powershell,
-    Cmd,
-    Other,
-}
-
-/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
-#[inline]
-#[cfg(target_os = "windows")]
-fn to_cmd_variable(input: String) -> String {
-    if let Some(var_str) = input.strip_prefix("${") {
-        if var_str.find(':').is_none() {
-            // If the input starts with "${", remove the trailing "}"
-            format!("%{}%", &var_str[..var_str.len() - 1])
-        } else {
-            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
-            // which will result in the task failing to run in such cases.
-            input
-        }
-    } else if let Some(var_str) = input.strip_prefix('$') {
-        // If the input starts with "$", directly append to "$env:"
-        format!("%{}%", var_str)
-    } else {
-        // If no prefix is found, return the input as is
-        input
-    }
-}
-
-/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
-#[inline]
-#[cfg(target_os = "windows")]
-fn to_powershell_variable(input: String) -> String {
-    if let Some(var_str) = input.strip_prefix("${") {
-        if var_str.find(':').is_none() {
-            // If the input starts with "${", remove the trailing "}"
-            format!("$env:{}", &var_str[..var_str.len() - 1])
-        } else {
-            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
-            // which will result in the task failing to run in such cases.
-            input
-        }
-    } else if let Some(var_str) = input.strip_prefix('$') {
-        // If the input starts with "$", directly append to "$env:"
-        format!("$env:{}", var_str)
-    } else {
-        // If no prefix is found, return the input as is
-        input
-    }
-}

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -14,9 +14,9 @@ use project::{Fs, ProjectEntryId};
 use search::{buffer_search::DivRegistrar, BufferSearchBar};
 use serde::{Deserialize, Serialize};
 use settings::Settings;
-use task::{RevealStrategy, SpawnInTerminal, TaskId, TerminalWorkDir};
+use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId, TerminalWorkDir};
 use terminal::{
-    terminal_settings::{Shell, TerminalDockPosition, TerminalSettings},
+    terminal_settings::{TerminalDockPosition, TerminalSettings},
     Terminal,
 };
 use ui::{
@@ -363,15 +363,15 @@ impl TerminalPanel {
     fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
         let mut spawn_task = spawn_in_terminal.clone();
         // 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() {
-            Shell::System => Shell::retrieve_system_shell().map(|shell| (shell, Vec::new())),
+        let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() {
+            Shell::System => retrieve_system_shell().map(|shell| (shell, Vec::new())),
             Shell::Program(shell) => Some((shell, Vec::new())),
             Shell::WithArguments { program, args } => Some((program, args)),
         }) else {
             return;
         };
         #[cfg(target_os = "windows")]
-        let windows_shell_type = Shell::to_windows_shell_type(&shell);
+        let windows_shell_type = to_windows_shell_type(&shell);
 
         #[cfg(not(target_os = "windows"))]
         {
@@ -379,7 +379,7 @@ impl TerminalPanel {
         }
         #[cfg(target_os = "windows")]
         {
-            use terminal::terminal_settings::WindowsShellType;
+            use crate::terminal_panel::WindowsShellType;
 
             match windows_shell_type {
                 WindowsShellType::Powershell => {
@@ -404,7 +404,7 @@ impl TerminalPanel {
                 #[cfg(not(target_os = "windows"))]
                 command.push_str(&arg);
                 #[cfg(target_os = "windows")]
-                command.push_str(&Shell::to_windows_shell_variable(windows_shell_type, arg));
+                command.push_str(&to_windows_shell_variable(windows_shell_type, arg));
                 command
             });
 
@@ -412,7 +412,7 @@ impl TerminalPanel {
         user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
         #[cfg(target_os = "windows")]
         {
-            use terminal::terminal_settings::WindowsShellType;
+            use crate::terminal_panel::WindowsShellType;
 
             match windows_shell_type {
                 WindowsShellType::Powershell => {
@@ -845,3 +845,93 @@ struct SerializedTerminalPanel {
     width: Option<Pixels>,
     height: Option<Pixels>,
 }
+
+fn retrieve_system_shell() -> Option<String> {
+    #[cfg(not(target_os = "windows"))]
+    {
+        use anyhow::Context;
+        use util::ResultExt;
+
+        return std::env::var("SHELL")
+            .context("Error finding SHELL in env.")
+            .log_err();
+    }
+    // `alacritty_terminal` uses this as default on Windows. See:
+    // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
+    #[cfg(target_os = "windows")]
+    return Some("powershell".to_owned());
+}
+
+#[cfg(target_os = "windows")]
+fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
+    match shell_type {
+        WindowsShellType::Powershell => to_powershell_variable(input),
+        WindowsShellType::Cmd => to_cmd_variable(input),
+        WindowsShellType::Other => input,
+    }
+}
+
+#[cfg(target_os = "windows")]
+fn to_windows_shell_type(shell: &str) -> WindowsShellType {
+    if shell == "powershell" || shell.ends_with("powershell.exe") {
+        WindowsShellType::Powershell
+    } else if shell == "cmd" || shell.ends_with("cmd.exe") {
+        WindowsShellType::Cmd
+    } else {
+        // Someother shell detected, the user might install and use a
+        // unix-like shell.
+        WindowsShellType::Other
+    }
+}
+
+/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
+#[inline]
+#[cfg(target_os = "windows")]
+fn to_cmd_variable(input: String) -> String {
+    if let Some(var_str) = input.strip_prefix("${") {
+        if var_str.find(':').is_none() {
+            // If the input starts with "${", remove the trailing "}"
+            format!("%{}%", &var_str[..var_str.len() - 1])
+        } else {
+            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
+            // which will result in the task failing to run in such cases.
+            input
+        }
+    } else if let Some(var_str) = input.strip_prefix('$') {
+        // If the input starts with "$", directly append to "$env:"
+        format!("%{}%", var_str)
+    } else {
+        // If no prefix is found, return the input as is
+        input
+    }
+}
+
+/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
+#[inline]
+#[cfg(target_os = "windows")]
+fn to_powershell_variable(input: String) -> String {
+    if let Some(var_str) = input.strip_prefix("${") {
+        if var_str.find(':').is_none() {
+            // If the input starts with "${", remove the trailing "}"
+            format!("$env:{}", &var_str[..var_str.len() - 1])
+        } else {
+            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
+            // which will result in the task failing to run in such cases.
+            input
+        }
+    } else if let Some(var_str) = input.strip_prefix('$') {
+        // If the input starts with "$", directly append to "$env:"
+        format!("$env:{}", var_str)
+    } else {
+        // If no prefix is found, return the input as is
+        input
+    }
+}
+
+#[cfg(target_os = "windows")]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum WindowsShellType {
+    Powershell,
+    Cmd,
+    Other,
+}

crates/workspace/src/tasks.rs 🔗

@@ -41,6 +41,6 @@ pub fn schedule_resolved_task(
                 })
             });
         }
-        cx.emit(crate::Event::SpawnTask(spawn_in_terminal));
+        cx.emit(crate::Event::SpawnTask(Box::new(spawn_in_terminal)));
     }
 }

crates/workspace/src/workspace.rs 🔗

@@ -660,7 +660,7 @@ pub enum Event {
     ActiveItemChanged,
     ContactRequestedJoin(u64),
     WorkspaceCreated(WeakView<Workspace>),
-    SpawnTask(SpawnInTerminal),
+    SpawnTask(Box<SpawnInTerminal>),
     OpenBundledFile {
         text: Cow<'static, str>,
         title: &'static str,

crates/zed/src/zed.rs 🔗

@@ -1000,7 +1000,7 @@ mod tests {
         path::{Path, PathBuf},
         time::Duration,
     };
-    use task::{RevealStrategy, SpawnInTerminal};
+    use task::{HideStrategy, RevealStrategy, Shell, SpawnInTerminal};
     use theme::{ThemeRegistry, ThemeSettings};
     use workspace::{
         item::{Item, ItemHandle},
@@ -3349,6 +3349,8 @@ mod tests {
             use_new_terminal: false,
             allow_concurrent_runs: false,
             reveal: RevealStrategy::Always,
+            hide: HideStrategy::Never,
+            shell: Shell::System,
         };
         let project = Project::test(app_state.fs.clone(), [project_root.path()], cx).await;
         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
@@ -3356,7 +3358,7 @@ mod tests {
         cx.update(|cx| {
             window
                 .update(cx, |_workspace, cx| {
-                    cx.emit(workspace::Event::SpawnTask(spawn_in_terminal));
+                    cx.emit(workspace::Event::SpawnTask(Box::new(spawn_in_terminal)));
                 })
                 .unwrap();
         });

docs/src/tasks.md 🔗

@@ -19,7 +19,28 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
     // 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"
+    "reveal": "always",
+    // What to do with the terminal pane and tab, after the command had finished:
+    // * `never` — Do nothing when the command finishes (default)
+    // * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it
+    // * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`
+    "hide": "never",
+    // Which shell to use when running a task inside the terminal.
+    // May take 3 values:
+    // 1. (default) Use the system's default terminal configuration in /etc/passwd
+    //      "shell": "system"
+    // 2. A program:
+    //      "shell": {
+    //        "program": "sh"
+    //      }
+    // 3. A program with arguments:
+    //     "shell": {
+    //         "with_arguments": {
+    //           "program": "/bin/bash",
+    //           "arguments": ["--login"]
+    //         }
+    //     }
+    "shell": "system"
   }
 ]
 ```