diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index 45c02ebb2d..4c15c8b67c 100644 --- a/assets/settings/initial_tasks.json +++ b/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" } ] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 93c4129623..62a4030f34 100644 --- a/crates/project/src/project.rs +++ b/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)) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 58968b2385..2ca152b482 100644 --- a/crates/project/src/terminals.rs +++ b/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, }), diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index f4893a5579..3b22370312 100644 --- a/crates/proto/proto/zed.proto +++ b/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 { diff --git a/crates/recent_projects/src/dev_servers.rs b/crates/recent_projects/src/dev_servers.rs index a6a2e88e01..89d54f46b7 100644 --- a/crates/recent_projects/src/dev_servers.rs +++ b/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, ) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 4459a30bc9..aacd6ba169 100644 --- a/crates/remote/src/ssh_session.rs +++ b/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, - cx: &mut AsyncAppContext, + _user: String, + _host: String, + _port: u16, + _delegate: Arc, + _cx: &mut AsyncAppContext, ) -> Result { Err(anyhow!("ssh is not supported on this platform")) } diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 10b9b050a4..77c62638e2 100644 --- a/crates/task/src/lib.rs +++ b/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, + }, +} diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index fda245b4c5..403bab1b35 100644 --- a/crates/task/src/task_template.rs +++ b/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, + /// 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); @@ -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(), }), }) } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index ccd6d98137..8f9b512027 100644 --- a/crates/terminal/src/terminal.rs +++ b/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) -> (String, String) { +fn task_summary(task: &TaskState, error_code: Option) -> (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. diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 0114ce2c5f..7e31183b7b 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/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, - }, -} - -impl Shell { - pub fn retrieve_system_shell() -> Option { - #[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, } - -#[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 - } -} diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 08105970f5..5b34477d54 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/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) { 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, height: Option, } + +fn retrieve_system_shell() -> Option { + #[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, +} diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index f7c94c869b..98726096cb 100644 --- a/crates/workspace/src/tasks.rs +++ b/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))); } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ae6f6ebf85..fa5e7856db 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -660,7 +660,7 @@ pub enum Event { ActiveItemChanged, ContactRequestedJoin(u64), WorkspaceCreated(WeakView), - SpawnTask(SpawnInTerminal), + SpawnTask(Box), OpenBundledFile { text: Cow<'static, str>, title: &'static str, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index fd881dd7fa..34fb5d12ba 100644 --- a/crates/zed/src/zed.rs +++ b/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(); }); diff --git a/docs/src/tasks.md b/docs/src/tasks.md index c35b7bbb5a..05d4337162 100644 --- a/docs/src/tasks.md +++ b/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" } ] ```