terminal_panel.rs

   1use std::{cmp, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
   2
   3use crate::{
   4    default_working_directory,
   5    persistence::{
   6        deserialize_terminal_panel, serialize_pane_group, SerializedItems, SerializedTerminalPanel,
   7    },
   8    TerminalView,
   9};
  10use breadcrumbs::Breadcrumbs;
  11use collections::HashMap;
  12use db::kvp::KEY_VALUE_STORE;
  13use futures::future::join_all;
  14use gpui::{
  15    actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, EventEmitter,
  16    ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render,
  17    Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
  18};
  19use itertools::Itertools;
  20use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId};
  21use search::{buffer_search::DivRegistrar, BufferSearchBar};
  22use settings::Settings;
  23use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId};
  24use terminal::{
  25    terminal_settings::{TerminalDockPosition, TerminalSettings},
  26    Terminal,
  27};
  28use ui::{
  29    prelude::*, ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Selectable,
  30    Tooltip,
  31};
  32use util::{ResultExt, TryFutureExt};
  33use workspace::{
  34    dock::{DockPosition, Panel, PanelEvent},
  35    item::SerializableItem,
  36    move_item, pane,
  37    ui::IconName,
  38    ActivateNextPane, ActivatePane, ActivatePaneInDirection, ActivatePreviousPane, DraggedTab,
  39    ItemId, NewTerminal, Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight,
  40    SplitUp, SwapPaneInDirection, ToggleZoom, Workspace,
  41};
  42
  43use anyhow::Result;
  44use zed_actions::InlineAssist;
  45
  46const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
  47
  48actions!(terminal_panel, [ToggleFocus]);
  49
  50pub fn init(cx: &mut AppContext) {
  51    cx.observe_new_views(
  52        |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
  53            workspace.register_action(TerminalPanel::new_terminal);
  54            workspace.register_action(TerminalPanel::open_terminal);
  55            workspace.register_action(|workspace, _: &ToggleFocus, cx| {
  56                if workspace
  57                    .panel::<TerminalPanel>(cx)
  58                    .as_ref()
  59                    .is_some_and(|panel| panel.read(cx).enabled)
  60                {
  61                    workspace.toggle_panel_focus::<TerminalPanel>(cx);
  62                }
  63            });
  64        },
  65    )
  66    .detach();
  67}
  68
  69pub struct TerminalPanel {
  70    pub(crate) active_pane: View<Pane>,
  71    pub(crate) center: PaneGroup,
  72    fs: Arc<dyn Fs>,
  73    workspace: WeakView<Workspace>,
  74    pub(crate) width: Option<Pixels>,
  75    pub(crate) height: Option<Pixels>,
  76    pending_serialization: Task<Option<()>>,
  77    pending_terminals_to_add: usize,
  78    deferred_tasks: HashMap<TaskId, Task<()>>,
  79    enabled: bool,
  80    assistant_enabled: bool,
  81    assistant_tab_bar_button: Option<AnyView>,
  82}
  83
  84impl TerminalPanel {
  85    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
  86        let project = workspace.project();
  87        let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx);
  88        let center = PaneGroup::new(pane.clone());
  89        let enabled = project.read(cx).supports_terminal(cx);
  90        cx.focus_view(&pane);
  91        let terminal_panel = Self {
  92            center,
  93            active_pane: pane,
  94            fs: workspace.app_state().fs.clone(),
  95            workspace: workspace.weak_handle(),
  96            pending_serialization: Task::ready(None),
  97            width: None,
  98            height: None,
  99            pending_terminals_to_add: 0,
 100            deferred_tasks: HashMap::default(),
 101            enabled,
 102            assistant_enabled: false,
 103            assistant_tab_bar_button: None,
 104        };
 105        terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx);
 106        terminal_panel
 107    }
 108
 109    pub fn asssistant_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
 110        self.assistant_enabled = enabled;
 111        if enabled {
 112            let focus_handle = self
 113                .active_pane
 114                .read(cx)
 115                .active_item()
 116                .map(|item| item.focus_handle(cx))
 117                .unwrap_or(self.focus_handle(cx));
 118            self.assistant_tab_bar_button = Some(
 119                cx.new_view(move |_| InlineAssistTabBarButton { focus_handle })
 120                    .into(),
 121            );
 122        } else {
 123            self.assistant_tab_bar_button = None;
 124        }
 125        for pane in self.center.panes() {
 126            self.apply_tab_bar_buttons(pane, cx);
 127        }
 128    }
 129
 130    fn apply_tab_bar_buttons(&self, terminal_pane: &View<Pane>, cx: &mut ViewContext<Self>) {
 131        let assistant_tab_bar_button = self.assistant_tab_bar_button.clone();
 132        terminal_pane.update(cx, |pane, cx| {
 133            pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
 134                let split_context = pane
 135                    .active_item()
 136                    .and_then(|item| item.downcast::<TerminalView>())
 137                    .map(|terminal_view| terminal_view.read(cx).focus_handle.clone());
 138                if !pane.has_focus(cx) && !pane.context_menu_focused(cx) {
 139                    return (None, None);
 140                }
 141                let focus_handle = pane.focus_handle(cx);
 142                let right_children = h_flex()
 143                    .gap(DynamicSpacing::Base02.rems(cx))
 144                    .child(
 145                        PopoverMenu::new("terminal-tab-bar-popover-menu")
 146                            .trigger(
 147                                IconButton::new("plus", IconName::Plus)
 148                                    .icon_size(IconSize::Small)
 149                                    .tooltip(|cx| Tooltip::text("New…", cx)),
 150                            )
 151                            .anchor(AnchorCorner::TopRight)
 152                            .with_handle(pane.new_item_context_menu_handle.clone())
 153                            .menu(move |cx| {
 154                                let focus_handle = focus_handle.clone();
 155                                let menu = ContextMenu::build(cx, |menu, _| {
 156                                    menu.context(focus_handle.clone())
 157                                        .action(
 158                                            "New Terminal",
 159                                            workspace::NewTerminal.boxed_clone(),
 160                                        )
 161                                        // We want the focus to go back to terminal panel once task modal is dismissed,
 162                                        // hence we focus that first. Otherwise, we'd end up without a focused element, as
 163                                        // context menu will be gone the moment we spawn the modal.
 164                                        .action(
 165                                            "Spawn task",
 166                                            zed_actions::Spawn::modal().boxed_clone(),
 167                                        )
 168                                });
 169
 170                                Some(menu)
 171                            }),
 172                    )
 173                    .children(assistant_tab_bar_button.clone())
 174                    .child(
 175                        PopoverMenu::new("terminal-pane-tab-bar-split")
 176                            .trigger(
 177                                IconButton::new("terminal-pane-split", IconName::Split)
 178                                    .icon_size(IconSize::Small)
 179                                    .tooltip(|cx| Tooltip::text("Split Pane", cx)),
 180                            )
 181                            .anchor(AnchorCorner::TopRight)
 182                            .with_handle(pane.split_item_context_menu_handle.clone())
 183                            .menu({
 184                                let split_context = split_context.clone();
 185                                move |cx| {
 186                                    ContextMenu::build(cx, |menu, _| {
 187                                        menu.when_some(
 188                                            split_context.clone(),
 189                                            |menu, split_context| menu.context(split_context),
 190                                        )
 191                                        .action("Split Right", SplitRight.boxed_clone())
 192                                        .action("Split Left", SplitLeft.boxed_clone())
 193                                        .action("Split Up", SplitUp.boxed_clone())
 194                                        .action("Split Down", SplitDown.boxed_clone())
 195                                    })
 196                                    .into()
 197                                }
 198                            }),
 199                    )
 200                    .child({
 201                        let zoomed = pane.is_zoomed();
 202                        IconButton::new("toggle_zoom", IconName::Maximize)
 203                            .icon_size(IconSize::Small)
 204                            .selected(zoomed)
 205                            .selected_icon(IconName::Minimize)
 206                            .on_click(cx.listener(|pane, _, cx| {
 207                                pane.toggle_zoom(&workspace::ToggleZoom, cx);
 208                            }))
 209                            .tooltip(move |cx| {
 210                                Tooltip::for_action(
 211                                    if zoomed { "Zoom Out" } else { "Zoom In" },
 212                                    &ToggleZoom,
 213                                    cx,
 214                                )
 215                            })
 216                    })
 217                    .into_any_element()
 218                    .into();
 219                (None, right_children)
 220            });
 221        });
 222    }
 223
 224    pub async fn load(
 225        workspace: WeakView<Workspace>,
 226        mut cx: AsyncWindowContext,
 227    ) -> Result<View<Self>> {
 228        let serialized_panel = cx
 229            .background_executor()
 230            .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
 231            .await
 232            .log_err()
 233            .flatten()
 234            .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
 235            .transpose()
 236            .log_err()
 237            .flatten();
 238
 239        let terminal_panel = workspace
 240            .update(&mut cx, |workspace, cx| {
 241                match serialized_panel.zip(workspace.database_id()) {
 242                    Some((serialized_panel, database_id)) => deserialize_terminal_panel(
 243                        workspace.weak_handle(),
 244                        workspace.project().clone(),
 245                        database_id,
 246                        serialized_panel,
 247                        cx,
 248                    ),
 249                    None => Task::ready(Ok(cx.new_view(|cx| TerminalPanel::new(workspace, cx)))),
 250                }
 251            })?
 252            .await?;
 253
 254        if let Some(workspace) = workspace.upgrade() {
 255            terminal_panel
 256                .update(&mut cx, |_, cx| {
 257                    cx.subscribe(&workspace, |terminal_panel, _, e, cx| {
 258                        if let workspace::Event::SpawnTask(spawn_in_terminal) = e {
 259                            terminal_panel.spawn_task(spawn_in_terminal, cx);
 260                        };
 261                    })
 262                    .detach();
 263                })
 264                .ok();
 265        }
 266
 267        // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
 268        if let Some(workspace) = workspace.upgrade() {
 269            let cleanup_task = workspace.update(&mut cx, |workspace, cx| {
 270                let alive_item_ids = terminal_panel
 271                    .read(cx)
 272                    .center
 273                    .panes()
 274                    .into_iter()
 275                    .flat_map(|pane| pane.read(cx).items())
 276                    .map(|item| item.item_id().as_u64() as ItemId)
 277                    .collect();
 278                workspace
 279                    .database_id()
 280                    .map(|workspace_id| TerminalView::cleanup(workspace_id, alive_item_ids, cx))
 281            })?;
 282            if let Some(task) = cleanup_task {
 283                task.await.log_err();
 284            }
 285        }
 286
 287        Ok(terminal_panel)
 288    }
 289
 290    fn handle_pane_event(
 291        &mut self,
 292        pane: View<Pane>,
 293        event: &pane::Event,
 294        cx: &mut ViewContext<Self>,
 295    ) {
 296        match event {
 297            pane::Event::ActivateItem { .. } => self.serialize(cx),
 298            pane::Event::RemovedItem { .. } => self.serialize(cx),
 299            pane::Event::Remove { focus_on_pane } => {
 300                let pane_count_before_removal = self.center.panes().len();
 301                let _removal_result = self.center.remove(&pane);
 302                if pane_count_before_removal == 1 {
 303                    self.center.first_pane().update(cx, |pane, cx| {
 304                        pane.set_zoomed(false, cx);
 305                    });
 306                    cx.emit(PanelEvent::Close);
 307                } else {
 308                    if let Some(focus_on_pane) =
 309                        focus_on_pane.as_ref().or_else(|| self.center.panes().pop())
 310                    {
 311                        focus_on_pane.focus_handle(cx).focus(cx);
 312                    }
 313                }
 314            }
 315            pane::Event::ZoomIn => {
 316                for pane in self.center.panes() {
 317                    pane.update(cx, |pane, cx| {
 318                        pane.set_zoomed(true, cx);
 319                    })
 320                }
 321                cx.emit(PanelEvent::ZoomIn);
 322                cx.notify();
 323            }
 324            pane::Event::ZoomOut => {
 325                for pane in self.center.panes() {
 326                    pane.update(cx, |pane, cx| {
 327                        pane.set_zoomed(false, cx);
 328                    })
 329                }
 330                cx.emit(PanelEvent::ZoomOut);
 331                cx.notify();
 332            }
 333            pane::Event::AddItem { item } => {
 334                if let Some(workspace) = self.workspace.upgrade() {
 335                    workspace.update(cx, |workspace, cx| {
 336                        item.added_to_pane(workspace, pane.clone(), cx)
 337                    })
 338                }
 339                self.serialize(cx);
 340            }
 341            pane::Event::Split(direction) => {
 342                let new_pane = self.new_pane_with_cloned_active_terminal(cx);
 343                let pane = pane.clone();
 344                let direction = *direction;
 345                cx.spawn(move |terminal_panel, mut cx| async move {
 346                    let Some(new_pane) = new_pane.await else {
 347                        return;
 348                    };
 349                    terminal_panel
 350                        .update(&mut cx, |terminal_panel, cx| {
 351                            terminal_panel
 352                                .center
 353                                .split(&pane, &new_pane, direction)
 354                                .log_err();
 355                            cx.focus_view(&new_pane);
 356                        })
 357                        .ok();
 358                })
 359                .detach();
 360            }
 361            pane::Event::Focus => {
 362                self.active_pane = pane.clone();
 363            }
 364
 365            _ => {}
 366        }
 367    }
 368
 369    fn new_pane_with_cloned_active_terminal(
 370        &mut self,
 371        cx: &mut ViewContext<Self>,
 372    ) -> Task<Option<View<Pane>>> {
 373        let Some(workspace) = self.workspace.clone().upgrade() else {
 374            return Task::ready(None);
 375        };
 376        let database_id = workspace.read(cx).database_id();
 377        let weak_workspace = self.workspace.clone();
 378        let project = workspace.read(cx).project().clone();
 379        let working_directory = self
 380            .active_pane
 381            .read(cx)
 382            .active_item()
 383            .and_then(|item| item.downcast::<TerminalView>())
 384            .and_then(|terminal_view| {
 385                terminal_view
 386                    .read(cx)
 387                    .terminal()
 388                    .read(cx)
 389                    .working_directory()
 390            })
 391            .or_else(|| default_working_directory(workspace.read(cx), cx));
 392        let kind = TerminalKind::Shell(working_directory);
 393        let window = cx.window_handle();
 394        cx.spawn(move |terminal_panel, mut cx| async move {
 395            let terminal = project
 396                .update(&mut cx, |project, cx| {
 397                    project.create_terminal(kind, window, cx)
 398                })
 399                .log_err()?
 400                .await
 401                .log_err()?;
 402
 403            let terminal_view = Box::new(
 404                cx.new_view(|cx| {
 405                    TerminalView::new(terminal.clone(), weak_workspace.clone(), database_id, cx)
 406                })
 407                .ok()?,
 408            );
 409            let pane = terminal_panel
 410                .update(&mut cx, |terminal_panel, cx| {
 411                    let pane = new_terminal_pane(
 412                        weak_workspace,
 413                        project,
 414                        terminal_panel.active_pane.read(cx).is_zoomed(),
 415                        cx,
 416                    );
 417                    terminal_panel.apply_tab_bar_buttons(&pane, cx);
 418                    pane
 419                })
 420                .ok()?;
 421
 422            pane.update(&mut cx, |pane, cx| {
 423                pane.add_item(terminal_view, true, true, None, cx);
 424            })
 425            .ok()?;
 426
 427            Some(pane)
 428        })
 429    }
 430
 431    pub fn open_terminal(
 432        workspace: &mut Workspace,
 433        action: &workspace::OpenTerminal,
 434        cx: &mut ViewContext<Workspace>,
 435    ) {
 436        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 437            return;
 438        };
 439
 440        terminal_panel
 441            .update(cx, |panel, cx| {
 442                panel.add_terminal(
 443                    TerminalKind::Shell(Some(action.working_directory.clone())),
 444                    RevealStrategy::Always,
 445                    cx,
 446                )
 447            })
 448            .detach_and_log_err(cx);
 449    }
 450
 451    fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
 452        let mut spawn_task = spawn_in_terminal.clone();
 453        // Set up shell args unconditionally, as tasks are always spawned inside of a shell.
 454        let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() {
 455            Shell::System => {
 456                match self
 457                    .workspace
 458                    .update(cx, |workspace, cx| workspace.project().read(cx).is_local())
 459                {
 460                    Ok(local) => {
 461                        if local {
 462                            retrieve_system_shell().map(|shell| (shell, Vec::new()))
 463                        } else {
 464                            Some(("\"${SHELL:-sh}\"".to_string(), Vec::new()))
 465                        }
 466                    }
 467                    Err(_no_window_e) => return,
 468                }
 469            }
 470            Shell::Program(shell) => Some((shell, Vec::new())),
 471            Shell::WithArguments { program, args, .. } => Some((program, args)),
 472        }) else {
 473            return;
 474        };
 475        #[cfg(target_os = "windows")]
 476        let windows_shell_type = to_windows_shell_type(&shell);
 477
 478        #[cfg(not(target_os = "windows"))]
 479        {
 480            spawn_task.command_label = format!("{shell} -i -c '{}'", spawn_task.command_label);
 481        }
 482        #[cfg(target_os = "windows")]
 483        {
 484            use crate::terminal_panel::WindowsShellType;
 485
 486            match windows_shell_type {
 487                WindowsShellType::Powershell => {
 488                    spawn_task.command_label = format!("{shell} -C '{}'", spawn_task.command_label)
 489                }
 490                WindowsShellType::Cmd => {
 491                    spawn_task.command_label = format!("{shell} /C '{}'", spawn_task.command_label)
 492                }
 493                WindowsShellType::Other => {
 494                    spawn_task.command_label =
 495                        format!("{shell} -i -c '{}'", spawn_task.command_label)
 496                }
 497            }
 498        }
 499
 500        let task_command = std::mem::replace(&mut spawn_task.command, shell);
 501        let task_args = std::mem::take(&mut spawn_task.args);
 502        let combined_command = task_args
 503            .into_iter()
 504            .fold(task_command, |mut command, arg| {
 505                command.push(' ');
 506                #[cfg(not(target_os = "windows"))]
 507                command.push_str(&arg);
 508                #[cfg(target_os = "windows")]
 509                command.push_str(&to_windows_shell_variable(windows_shell_type, arg));
 510                command
 511            });
 512
 513        #[cfg(not(target_os = "windows"))]
 514        user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
 515        #[cfg(target_os = "windows")]
 516        {
 517            use crate::terminal_panel::WindowsShellType;
 518
 519            match windows_shell_type {
 520                WindowsShellType::Powershell => {
 521                    user_args.extend(["-C".to_owned(), combined_command])
 522                }
 523                WindowsShellType::Cmd => user_args.extend(["/C".to_owned(), combined_command]),
 524                WindowsShellType::Other => {
 525                    user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command])
 526                }
 527            }
 528        }
 529        spawn_task.args = user_args;
 530        let spawn_task = spawn_task;
 531
 532        let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
 533        let use_new_terminal = spawn_in_terminal.use_new_terminal;
 534
 535        if allow_concurrent_runs && use_new_terminal {
 536            self.spawn_in_new_terminal(spawn_task, cx)
 537                .detach_and_log_err(cx);
 538            return;
 539        }
 540
 541        let terminals_for_task = self.terminals_for_task(&spawn_in_terminal.full_label, cx);
 542        if terminals_for_task.is_empty() {
 543            self.spawn_in_new_terminal(spawn_task, cx)
 544                .detach_and_log_err(cx);
 545            return;
 546        }
 547        let (existing_item_index, task_pane, existing_terminal) = terminals_for_task
 548            .last()
 549            .expect("covered no terminals case above")
 550            .clone();
 551        let id = spawn_in_terminal.id.clone();
 552        cx.spawn(move |this, mut cx| async move {
 553            if allow_concurrent_runs {
 554                debug_assert!(
 555                    !use_new_terminal,
 556                    "Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
 557                );
 558                this.update(&mut cx, |this, cx| {
 559                    this.replace_terminal(
 560                        spawn_task,
 561                        task_pane,
 562                        existing_item_index,
 563                        existing_terminal,
 564                        cx,
 565                    )
 566                })?
 567                .await;
 568            } else {
 569                this.update(&mut cx, |this, cx| {
 570                    this.deferred_tasks.insert(
 571                        id,
 572                        cx.spawn(|terminal_panel, mut cx| async move {
 573                            wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
 574                            let Ok(Some(new_terminal_task)) =
 575                                terminal_panel.update(&mut cx, |terminal_panel, cx| {
 576                                    if use_new_terminal {
 577                                        terminal_panel
 578                                            .spawn_in_new_terminal(spawn_task, cx)
 579                                            .detach_and_log_err(cx);
 580                                        None
 581                                    } else {
 582                                        Some(terminal_panel.replace_terminal(
 583                                            spawn_task,
 584                                            task_pane,
 585                                            existing_item_index,
 586                                            existing_terminal,
 587                                            cx,
 588                                        ))
 589                                    }
 590                                })
 591                            else {
 592                                return;
 593                            };
 594                            new_terminal_task.await;
 595                        }),
 596                    );
 597                })
 598                .ok();
 599            }
 600            anyhow::Result::<_, anyhow::Error>::Ok(())
 601        })
 602        .detach()
 603    }
 604
 605    pub fn spawn_in_new_terminal(
 606        &mut self,
 607        spawn_task: SpawnInTerminal,
 608        cx: &mut ViewContext<Self>,
 609    ) -> Task<Result<Model<Terminal>>> {
 610        let reveal = spawn_task.reveal;
 611        self.add_terminal(TerminalKind::Task(spawn_task), reveal, cx)
 612    }
 613
 614    /// Create a new Terminal in the current working directory or the user's home directory
 615    fn new_terminal(
 616        workspace: &mut Workspace,
 617        _: &workspace::NewTerminal,
 618        cx: &mut ViewContext<Workspace>,
 619    ) {
 620        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 621            return;
 622        };
 623
 624        let kind = TerminalKind::Shell(default_working_directory(workspace, cx));
 625
 626        terminal_panel
 627            .update(cx, |this, cx| {
 628                this.add_terminal(kind, RevealStrategy::Always, cx)
 629            })
 630            .detach_and_log_err(cx);
 631    }
 632
 633    fn terminals_for_task(
 634        &self,
 635        label: &str,
 636        cx: &mut AppContext,
 637    ) -> Vec<(usize, View<Pane>, View<TerminalView>)> {
 638        self.center
 639            .panes()
 640            .into_iter()
 641            .flat_map(|pane| {
 642                pane.read(cx)
 643                    .items()
 644                    .enumerate()
 645                    .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
 646                    .filter_map(|(index, terminal_view)| {
 647                        let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
 648                        if &task_state.full_label == label {
 649                            Some((index, terminal_view))
 650                        } else {
 651                            None
 652                        }
 653                    })
 654                    .map(|(index, terminal_view)| (index, pane.clone(), terminal_view))
 655            })
 656            .collect()
 657    }
 658
 659    fn activate_terminal_view(
 660        &self,
 661        pane: &View<Pane>,
 662        item_index: usize,
 663        focus: bool,
 664        cx: &mut WindowContext,
 665    ) {
 666        pane.update(cx, |pane, cx| {
 667            pane.activate_item(item_index, true, focus, cx)
 668        })
 669    }
 670
 671    fn add_terminal(
 672        &mut self,
 673        kind: TerminalKind,
 674        reveal_strategy: RevealStrategy,
 675        cx: &mut ViewContext<Self>,
 676    ) -> Task<Result<Model<Terminal>>> {
 677        if !self.enabled {
 678            return Task::ready(Err(anyhow::anyhow!(
 679                "terminal not yet supported for remote projects"
 680            )));
 681        }
 682
 683        let workspace = self.workspace.clone();
 684        self.pending_terminals_to_add += 1;
 685
 686        cx.spawn(|terminal_panel, mut cx| async move {
 687            let pane = terminal_panel.update(&mut cx, |this, _| this.active_pane.clone())?;
 688            let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
 689            let window = cx.window_handle();
 690            let terminal = project
 691                .update(&mut cx, |project, cx| {
 692                    project.create_terminal(kind, window, cx)
 693                })?
 694                .await?;
 695            let result = workspace.update(&mut cx, |workspace, cx| {
 696                let terminal_view = Box::new(cx.new_view(|cx| {
 697                    TerminalView::new(
 698                        terminal.clone(),
 699                        workspace.weak_handle(),
 700                        workspace.database_id(),
 701                        cx,
 702                    )
 703                }));
 704                pane.update(cx, |pane, cx| {
 705                    let focus = pane.has_focus(cx);
 706                    pane.add_item(terminal_view, true, focus, None, cx);
 707                });
 708
 709                match reveal_strategy {
 710                    RevealStrategy::Always => {
 711                        workspace.focus_panel::<Self>(cx);
 712                    }
 713                    RevealStrategy::NoFocus => {
 714                        workspace.open_panel::<Self>(cx);
 715                    }
 716                    RevealStrategy::Never => {}
 717                }
 718                Ok(terminal)
 719            })?;
 720            terminal_panel.update(&mut cx, |this, cx| {
 721                this.pending_terminals_to_add = this.pending_terminals_to_add.saturating_sub(1);
 722                this.serialize(cx)
 723            })?;
 724            result
 725        })
 726    }
 727
 728    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 729        let height = self.height;
 730        let width = self.width;
 731        self.pending_serialization = cx.spawn(|terminal_panel, mut cx| async move {
 732            cx.background_executor()
 733                .timer(Duration::from_millis(50))
 734                .await;
 735            let terminal_panel = terminal_panel.upgrade()?;
 736            let items = terminal_panel
 737                .update(&mut cx, |terminal_panel, cx| {
 738                    SerializedItems::WithSplits(serialize_pane_group(
 739                        &terminal_panel.center,
 740                        &terminal_panel.active_pane,
 741                        cx,
 742                    ))
 743                })
 744                .ok()?;
 745            cx.background_executor()
 746                .spawn(
 747                    async move {
 748                        KEY_VALUE_STORE
 749                            .write_kvp(
 750                                TERMINAL_PANEL_KEY.into(),
 751                                serde_json::to_string(&SerializedTerminalPanel {
 752                                    items,
 753                                    active_item_id: None,
 754                                    height,
 755                                    width,
 756                                })?,
 757                            )
 758                            .await?;
 759                        anyhow::Ok(())
 760                    }
 761                    .log_err(),
 762                )
 763                .await;
 764            Some(())
 765        });
 766    }
 767
 768    fn replace_terminal(
 769        &self,
 770        spawn_task: SpawnInTerminal,
 771        task_pane: View<Pane>,
 772        terminal_item_index: usize,
 773        terminal_to_replace: View<TerminalView>,
 774        cx: &mut ViewContext<'_, Self>,
 775    ) -> Task<Option<()>> {
 776        let reveal = spawn_task.reveal;
 777        let window = cx.window_handle();
 778        let task_workspace = self.workspace.clone();
 779        cx.spawn(move |this, mut cx| async move {
 780            let project = this
 781                .update(&mut cx, |this, cx| {
 782                    this.workspace
 783                        .update(cx, |workspace, _| workspace.project().clone())
 784                        .ok()
 785                })
 786                .ok()
 787                .flatten()?;
 788            let new_terminal = project
 789                .update(&mut cx, |project, cx| {
 790                    project.create_terminal(TerminalKind::Task(spawn_task), window, cx)
 791                })
 792                .ok()?
 793                .await
 794                .log_err()?;
 795            terminal_to_replace
 796                .update(&mut cx, |terminal_to_replace, cx| {
 797                    terminal_to_replace.set_terminal(new_terminal, cx);
 798                })
 799                .ok()?;
 800
 801            match reveal {
 802                RevealStrategy::Always => {
 803                    this.update(&mut cx, |this, cx| {
 804                        this.activate_terminal_view(&task_pane, terminal_item_index, true, cx)
 805                    })
 806                    .ok()?;
 807
 808                    cx.spawn(|mut cx| async move {
 809                        task_workspace
 810                            .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
 811                            .ok()
 812                    })
 813                    .detach();
 814                }
 815                RevealStrategy::NoFocus => {
 816                    this.update(&mut cx, |this, cx| {
 817                        this.activate_terminal_view(&task_pane, terminal_item_index, false, cx)
 818                    })
 819                    .ok()?;
 820
 821                    cx.spawn(|mut cx| async move {
 822                        task_workspace
 823                            .update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
 824                            .ok()
 825                    })
 826                    .detach();
 827                }
 828                RevealStrategy::Never => {}
 829            }
 830
 831            Some(())
 832        })
 833    }
 834
 835    fn has_no_terminals(&self, cx: &WindowContext) -> bool {
 836        self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
 837    }
 838
 839    pub fn assistant_enabled(&self) -> bool {
 840        self.assistant_enabled
 841    }
 842}
 843
 844pub fn new_terminal_pane(
 845    workspace: WeakView<Workspace>,
 846    project: Model<Project>,
 847    zoomed: bool,
 848    cx: &mut ViewContext<TerminalPanel>,
 849) -> View<Pane> {
 850    let is_local = project.read(cx).is_local();
 851    let terminal_panel = cx.view().clone();
 852    let pane = cx.new_view(|cx| {
 853        let mut pane = Pane::new(
 854            workspace.clone(),
 855            project.clone(),
 856            Default::default(),
 857            None,
 858            NewTerminal.boxed_clone(),
 859            cx,
 860        );
 861        pane.set_zoomed(zoomed, cx);
 862        pane.set_can_navigate(false, cx);
 863        pane.display_nav_history_buttons(None);
 864        pane.set_should_display_tab_bar(|_| true);
 865        pane.set_zoom_out_on_close(false);
 866
 867        let terminal_panel_for_split_check = terminal_panel.clone();
 868        pane.set_can_split(Some(Arc::new(move |pane, dragged_item, cx| {
 869            if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
 870                let current_pane = cx.view().clone();
 871                let can_drag_away =
 872                    terminal_panel_for_split_check.update(cx, |terminal_panel, _| {
 873                        let current_panes = terminal_panel.center.panes();
 874                        !current_panes.contains(&&tab.pane)
 875                            || current_panes.len() > 1
 876                            || (tab.pane != current_pane || pane.items_len() > 1)
 877                    });
 878                if can_drag_away {
 879                    let item = if tab.pane == current_pane {
 880                        pane.item_for_index(tab.ix)
 881                    } else {
 882                        tab.pane.read(cx).item_for_index(tab.ix)
 883                    };
 884                    if let Some(item) = item {
 885                        return item.downcast::<TerminalView>().is_some();
 886                    }
 887                }
 888            }
 889            false
 890        })));
 891
 892        let buffer_search_bar = cx.new_view(search::BufferSearchBar::new);
 893        let breadcrumbs = cx.new_view(|_| Breadcrumbs::new());
 894        pane.toolbar().update(cx, |toolbar, cx| {
 895            toolbar.add_item(buffer_search_bar, cx);
 896            toolbar.add_item(breadcrumbs, cx);
 897        });
 898
 899        pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| {
 900            if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
 901                let this_pane = cx.view().clone();
 902                let item = if tab.pane == this_pane {
 903                    pane.item_for_index(tab.ix)
 904                } else {
 905                    tab.pane.read(cx).item_for_index(tab.ix)
 906                };
 907                if let Some(item) = item {
 908                    if item.downcast::<TerminalView>().is_some() {
 909                        let source = tab.pane.clone();
 910                        let item_id_to_move = item.item_id();
 911
 912                        let new_split_pane = pane
 913                            .drag_split_direction()
 914                            .map(|split_direction| {
 915                                terminal_panel.update(cx, |terminal_panel, cx| {
 916                                    let is_zoomed = if terminal_panel.active_pane == this_pane {
 917                                        pane.is_zoomed()
 918                                    } else {
 919                                        terminal_panel.active_pane.read(cx).is_zoomed()
 920                                    };
 921                                    let new_pane = new_terminal_pane(
 922                                        workspace.clone(),
 923                                        project.clone(),
 924                                        is_zoomed,
 925                                        cx,
 926                                    );
 927                                    terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
 928                                    terminal_panel.center.split(
 929                                        &this_pane,
 930                                        &new_pane,
 931                                        split_direction,
 932                                    )?;
 933                                    anyhow::Ok(new_pane)
 934                                })
 935                            })
 936                            .transpose();
 937
 938                        match new_split_pane {
 939                            // Source pane may be the one currently updated, so defer the move.
 940                            Ok(Some(new_pane)) => cx
 941                                .spawn(|_, mut cx| async move {
 942                                    cx.update(|cx| {
 943                                        move_item(
 944                                            &source,
 945                                            &new_pane,
 946                                            item_id_to_move,
 947                                            new_pane.read(cx).active_item_index(),
 948                                            cx,
 949                                        );
 950                                    })
 951                                    .ok();
 952                                })
 953                                .detach(),
 954                            // If we drop into existing pane or current pane,
 955                            // regular pane drop handler will take care of it,
 956                            // using the right tab index for the operation.
 957                            Ok(None) => return ControlFlow::Continue(()),
 958                            err @ Err(_) => {
 959                                err.log_err();
 960                                return ControlFlow::Break(());
 961                            }
 962                        };
 963                    } else if let Some(project_path) = item.project_path(cx) {
 964                        if let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
 965                        {
 966                            add_paths_to_terminal(pane, &[entry_path], cx);
 967                        }
 968                    }
 969                }
 970            } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
 971                if let Some(entry_path) = project
 972                    .read(cx)
 973                    .path_for_entry(entry_id, cx)
 974                    .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx))
 975                {
 976                    add_paths_to_terminal(pane, &[entry_path], cx);
 977                }
 978            } else if is_local {
 979                if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
 980                    add_paths_to_terminal(pane, paths.paths(), cx);
 981                }
 982            }
 983
 984            ControlFlow::Break(())
 985        });
 986
 987        pane
 988    });
 989
 990    cx.subscribe(&pane, TerminalPanel::handle_pane_event)
 991        .detach();
 992    cx.observe(&pane, |_, _, cx| cx.notify()).detach();
 993
 994    pane
 995}
 996
 997async fn wait_for_terminals_tasks(
 998    terminals_for_task: Vec<(usize, View<Pane>, View<TerminalView>)>,
 999    cx: &mut AsyncWindowContext,
1000) {
1001    let pending_tasks = terminals_for_task.iter().filter_map(|(_, _, terminal)| {
1002        terminal
1003            .update(cx, |terminal_view, cx| {
1004                terminal_view
1005                    .terminal()
1006                    .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1007            })
1008            .ok()
1009    });
1010    let _: Vec<()> = join_all(pending_tasks).await;
1011}
1012
1013fn add_paths_to_terminal(pane: &mut Pane, paths: &[PathBuf], cx: &mut ViewContext<'_, Pane>) {
1014    if let Some(terminal_view) = pane
1015        .active_item()
1016        .and_then(|item| item.downcast::<TerminalView>())
1017    {
1018        cx.focus_view(&terminal_view);
1019        let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
1020        new_text.push(' ');
1021        terminal_view.update(cx, |terminal_view, cx| {
1022            terminal_view.terminal().update(cx, |terminal, _| {
1023                terminal.paste(&new_text);
1024            });
1025        });
1026    }
1027}
1028
1029impl EventEmitter<PanelEvent> for TerminalPanel {}
1030
1031impl Render for TerminalPanel {
1032    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1033        let mut registrar = DivRegistrar::new(
1034            |panel, cx| {
1035                panel
1036                    .active_pane
1037                    .read(cx)
1038                    .toolbar()
1039                    .read(cx)
1040                    .item_of_type::<BufferSearchBar>()
1041            },
1042            cx,
1043        );
1044        BufferSearchBar::register(&mut registrar);
1045        let registrar = registrar.into_div();
1046        self.workspace
1047            .update(cx, |workspace, cx| {
1048                registrar.size_full().child(self.center.render(
1049                    workspace.project(),
1050                    &HashMap::default(),
1051                    None,
1052                    &self.active_pane,
1053                    workspace.zoomed_item(),
1054                    workspace.app_state(),
1055                    cx,
1056                ))
1057            })
1058            .ok()
1059            .map(|div| {
1060                div.on_action({
1061                    cx.listener(|terminal_panel, action: &ActivatePaneInDirection, cx| {
1062                        if let Some(pane) = terminal_panel.center.find_pane_in_direction(
1063                            &terminal_panel.active_pane,
1064                            action.0,
1065                            cx,
1066                        ) {
1067                            cx.focus_view(&pane);
1068                        } else {
1069                            terminal_panel
1070                                .workspace
1071                                .update(cx, |workspace, cx| {
1072                                    workspace.activate_pane_in_direction(action.0, cx)
1073                                })
1074                                .ok();
1075                        }
1076                    })
1077                })
1078                .on_action(
1079                    cx.listener(|terminal_panel, _action: &ActivateNextPane, cx| {
1080                        let panes = terminal_panel.center.panes();
1081                        if let Some(ix) = panes
1082                            .iter()
1083                            .position(|pane| **pane == terminal_panel.active_pane)
1084                        {
1085                            let next_ix = (ix + 1) % panes.len();
1086                            let next_pane = panes[next_ix].clone();
1087                            cx.focus_view(&next_pane);
1088                        }
1089                    }),
1090                )
1091                .on_action(
1092                    cx.listener(|terminal_panel, _action: &ActivatePreviousPane, cx| {
1093                        let panes = terminal_panel.center.panes();
1094                        if let Some(ix) = panes
1095                            .iter()
1096                            .position(|pane| **pane == terminal_panel.active_pane)
1097                        {
1098                            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1099                            let prev_pane = panes[prev_ix].clone();
1100                            cx.focus_view(&prev_pane);
1101                        }
1102                    }),
1103                )
1104                .on_action(cx.listener(|terminal_panel, action: &ActivatePane, cx| {
1105                    let panes = terminal_panel.center.panes();
1106                    if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1107                        cx.focus_view(&pane);
1108                    } else {
1109                        let new_pane = terminal_panel.new_pane_with_cloned_active_terminal(cx);
1110                        cx.spawn(|terminal_panel, mut cx| async move {
1111                            if let Some(new_pane) = new_pane.await {
1112                                terminal_panel
1113                                    .update(&mut cx, |terminal_panel, cx| {
1114                                        terminal_panel
1115                                            .center
1116                                            .split(
1117                                                &terminal_panel.active_pane,
1118                                                &new_pane,
1119                                                SplitDirection::Right,
1120                                            )
1121                                            .log_err();
1122                                        cx.focus_view(&new_pane);
1123                                    })
1124                                    .ok();
1125                            }
1126                        })
1127                        .detach();
1128                    }
1129                }))
1130                .on_action(cx.listener(
1131                    |terminal_panel, action: &SwapPaneInDirection, cx| {
1132                        if let Some(to) = terminal_panel
1133                            .center
1134                            .find_pane_in_direction(&terminal_panel.active_pane, action.0, cx)
1135                            .cloned()
1136                        {
1137                            terminal_panel
1138                                .center
1139                                .swap(&terminal_panel.active_pane.clone(), &to);
1140                            cx.notify();
1141                        }
1142                    },
1143                ))
1144            })
1145            .unwrap_or_else(|| div())
1146    }
1147}
1148
1149impl FocusableView for TerminalPanel {
1150    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1151        self.active_pane.focus_handle(cx)
1152    }
1153}
1154
1155impl Panel for TerminalPanel {
1156    fn position(&self, cx: &WindowContext) -> DockPosition {
1157        match TerminalSettings::get_global(cx).dock {
1158            TerminalDockPosition::Left => DockPosition::Left,
1159            TerminalDockPosition::Bottom => DockPosition::Bottom,
1160            TerminalDockPosition::Right => DockPosition::Right,
1161        }
1162    }
1163
1164    fn position_is_valid(&self, _: DockPosition) -> bool {
1165        true
1166    }
1167
1168    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1169        settings::update_settings_file::<TerminalSettings>(
1170            self.fs.clone(),
1171            cx,
1172            move |settings, _| {
1173                let dock = match position {
1174                    DockPosition::Left => TerminalDockPosition::Left,
1175                    DockPosition::Bottom => TerminalDockPosition::Bottom,
1176                    DockPosition::Right => TerminalDockPosition::Right,
1177                };
1178                settings.dock = Some(dock);
1179            },
1180        );
1181    }
1182
1183    fn size(&self, cx: &WindowContext) -> Pixels {
1184        let settings = TerminalSettings::get_global(cx);
1185        match self.position(cx) {
1186            DockPosition::Left | DockPosition::Right => {
1187                self.width.unwrap_or(settings.default_width)
1188            }
1189            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1190        }
1191    }
1192
1193    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1194        match self.position(cx) {
1195            DockPosition::Left | DockPosition::Right => self.width = size,
1196            DockPosition::Bottom => self.height = size,
1197        }
1198        self.serialize(cx);
1199        cx.notify();
1200    }
1201
1202    fn is_zoomed(&self, cx: &WindowContext) -> bool {
1203        self.active_pane.read(cx).is_zoomed()
1204    }
1205
1206    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1207        for pane in self.center.panes() {
1208            pane.update(cx, |pane, cx| {
1209                pane.set_zoomed(zoomed, cx);
1210            })
1211        }
1212        cx.notify();
1213    }
1214
1215    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1216        if !active || !self.has_no_terminals(cx) {
1217            return;
1218        }
1219        cx.defer(|this, cx| {
1220            let Ok(kind) = this.workspace.update(cx, |workspace, cx| {
1221                TerminalKind::Shell(default_working_directory(workspace, cx))
1222            }) else {
1223                return;
1224            };
1225
1226            this.add_terminal(kind, RevealStrategy::Never, cx)
1227                .detach_and_log_err(cx)
1228        })
1229    }
1230
1231    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
1232        let count = self
1233            .center
1234            .panes()
1235            .into_iter()
1236            .map(|pane| pane.read(cx).items_len())
1237            .sum::<usize>();
1238        if count == 0 {
1239            None
1240        } else {
1241            Some(count.to_string())
1242        }
1243    }
1244
1245    fn persistent_name() -> &'static str {
1246        "TerminalPanel"
1247    }
1248
1249    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1250        if (self.enabled || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button {
1251            Some(IconName::Terminal)
1252        } else {
1253            None
1254        }
1255    }
1256
1257    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1258        Some("Terminal Panel")
1259    }
1260
1261    fn toggle_action(&self) -> Box<dyn gpui::Action> {
1262        Box::new(ToggleFocus)
1263    }
1264
1265    fn pane(&self) -> Option<View<Pane>> {
1266        Some(self.active_pane.clone())
1267    }
1268}
1269
1270struct InlineAssistTabBarButton {
1271    focus_handle: FocusHandle,
1272}
1273
1274impl Render for InlineAssistTabBarButton {
1275    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1276        let focus_handle = self.focus_handle.clone();
1277        IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
1278            .icon_size(IconSize::Small)
1279            .on_click(cx.listener(|_, _, cx| {
1280                cx.dispatch_action(InlineAssist::default().boxed_clone());
1281            }))
1282            .tooltip(move |cx| {
1283                Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx)
1284            })
1285    }
1286}
1287
1288fn retrieve_system_shell() -> Option<String> {
1289    #[cfg(not(target_os = "windows"))]
1290    {
1291        use anyhow::Context;
1292        use util::ResultExt;
1293
1294        std::env::var("SHELL")
1295            .context("Error finding SHELL in env.")
1296            .log_err()
1297    }
1298    // `alacritty_terminal` uses this as default on Windows. See:
1299    // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
1300    #[cfg(target_os = "windows")]
1301    return Some("powershell".to_owned());
1302}
1303
1304#[cfg(target_os = "windows")]
1305fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
1306    match shell_type {
1307        WindowsShellType::Powershell => to_powershell_variable(input),
1308        WindowsShellType::Cmd => to_cmd_variable(input),
1309        WindowsShellType::Other => input,
1310    }
1311}
1312
1313#[cfg(target_os = "windows")]
1314fn to_windows_shell_type(shell: &str) -> WindowsShellType {
1315    if shell == "powershell"
1316        || shell.ends_with("powershell.exe")
1317        || shell == "pwsh"
1318        || shell.ends_with("pwsh.exe")
1319    {
1320        WindowsShellType::Powershell
1321    } else if shell == "cmd" || shell.ends_with("cmd.exe") {
1322        WindowsShellType::Cmd
1323    } else {
1324        // Someother shell detected, the user might install and use a
1325        // unix-like shell.
1326        WindowsShellType::Other
1327    }
1328}
1329
1330/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
1331#[inline]
1332#[cfg(target_os = "windows")]
1333fn to_cmd_variable(input: String) -> String {
1334    if let Some(var_str) = input.strip_prefix("${") {
1335        if var_str.find(':').is_none() {
1336            // If the input starts with "${", remove the trailing "}"
1337            format!("%{}%", &var_str[..var_str.len() - 1])
1338        } else {
1339            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
1340            // which will result in the task failing to run in such cases.
1341            input
1342        }
1343    } else if let Some(var_str) = input.strip_prefix('$') {
1344        // If the input starts with "$", directly append to "$env:"
1345        format!("%{}%", var_str)
1346    } else {
1347        // If no prefix is found, return the input as is
1348        input
1349    }
1350}
1351
1352/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
1353#[inline]
1354#[cfg(target_os = "windows")]
1355fn to_powershell_variable(input: String) -> String {
1356    if let Some(var_str) = input.strip_prefix("${") {
1357        if var_str.find(':').is_none() {
1358            // If the input starts with "${", remove the trailing "}"
1359            format!("$env:{}", &var_str[..var_str.len() - 1])
1360        } else {
1361            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
1362            // which will result in the task failing to run in such cases.
1363            input
1364        }
1365    } else if let Some(var_str) = input.strip_prefix('$') {
1366        // If the input starts with "$", directly append to "$env:"
1367        format!("$env:{}", var_str)
1368    } else {
1369        // If no prefix is found, return the input as is
1370        input
1371    }
1372}
1373
1374#[cfg(target_os = "windows")]
1375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1376enum WindowsShellType {
1377    Powershell,
1378    Cmd,
1379    Other,
1380}