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 belongs_to_this_pane = tab.pane == this_pane;
 903                let item = if belongs_to_this_pane {
 904                    pane.item_for_index(tab.ix)
 905                } else {
 906                    tab.pane.read(cx).item_for_index(tab.ix)
 907                };
 908                if let Some(item) = item {
 909                    if item.downcast::<TerminalView>().is_some() {
 910                        let source = tab.pane.clone();
 911                        let item_id_to_move = item.item_id();
 912
 913                        let new_pane = pane.drag_split_direction().and_then(|split_direction| {
 914                            terminal_panel.update(cx, |terminal_panel, cx| {
 915                                let is_zoomed = if terminal_panel.active_pane == this_pane {
 916                                    pane.is_zoomed()
 917                                } else {
 918                                    terminal_panel.active_pane.read(cx).is_zoomed()
 919                                };
 920                                let new_pane = new_terminal_pane(
 921                                    workspace.clone(),
 922                                    project.clone(),
 923                                    is_zoomed,
 924                                    cx,
 925                                );
 926                                terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
 927                                terminal_panel
 928                                    .center
 929                                    .split(&this_pane, &new_pane, split_direction)
 930                                    .log_err()?;
 931                                Some(new_pane)
 932                            })
 933                        });
 934
 935                        let destination;
 936                        let destination_index;
 937                        if let Some(new_pane) = new_pane {
 938                            destination_index = new_pane.read(cx).active_item_index();
 939                            destination = new_pane;
 940                        } else if belongs_to_this_pane {
 941                            return ControlFlow::Break(());
 942                        } else {
 943                            destination = cx.view().clone();
 944                            destination_index = pane.active_item_index();
 945                        }
 946                        // Destination pane may be the one currently updated, so defer the move.
 947                        cx.spawn(|_, mut cx| async move {
 948                            cx.update(|cx| {
 949                                move_item(
 950                                    &source,
 951                                    &destination,
 952                                    item_id_to_move,
 953                                    destination_index,
 954                                    cx,
 955                                );
 956                            })
 957                            .ok();
 958                        })
 959                        .detach();
 960                    } else if let Some(project_path) = item.project_path(cx) {
 961                        if let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
 962                        {
 963                            add_paths_to_terminal(pane, &[entry_path], cx);
 964                        }
 965                    }
 966                }
 967            } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
 968                if let Some(entry_path) = project
 969                    .read(cx)
 970                    .path_for_entry(entry_id, cx)
 971                    .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx))
 972                {
 973                    add_paths_to_terminal(pane, &[entry_path], cx);
 974                }
 975            } else if is_local {
 976                if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
 977                    add_paths_to_terminal(pane, paths.paths(), cx);
 978                }
 979            }
 980
 981            ControlFlow::Break(())
 982        });
 983
 984        pane
 985    });
 986
 987    cx.subscribe(&pane, TerminalPanel::handle_pane_event)
 988        .detach();
 989    cx.observe(&pane, |_, _, cx| cx.notify()).detach();
 990
 991    pane
 992}
 993
 994async fn wait_for_terminals_tasks(
 995    terminals_for_task: Vec<(usize, View<Pane>, View<TerminalView>)>,
 996    cx: &mut AsyncWindowContext,
 997) {
 998    let pending_tasks = terminals_for_task.iter().filter_map(|(_, _, terminal)| {
 999        terminal
1000            .update(cx, |terminal_view, cx| {
1001                terminal_view
1002                    .terminal()
1003                    .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1004            })
1005            .ok()
1006    });
1007    let _: Vec<()> = join_all(pending_tasks).await;
1008}
1009
1010fn add_paths_to_terminal(pane: &mut Pane, paths: &[PathBuf], cx: &mut ViewContext<'_, Pane>) {
1011    if let Some(terminal_view) = pane
1012        .active_item()
1013        .and_then(|item| item.downcast::<TerminalView>())
1014    {
1015        cx.focus_view(&terminal_view);
1016        let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
1017        new_text.push(' ');
1018        terminal_view.update(cx, |terminal_view, cx| {
1019            terminal_view.terminal().update(cx, |terminal, _| {
1020                terminal.paste(&new_text);
1021            });
1022        });
1023    }
1024}
1025
1026impl EventEmitter<PanelEvent> for TerminalPanel {}
1027
1028impl Render for TerminalPanel {
1029    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1030        let mut registrar = DivRegistrar::new(
1031            |panel, cx| {
1032                panel
1033                    .active_pane
1034                    .read(cx)
1035                    .toolbar()
1036                    .read(cx)
1037                    .item_of_type::<BufferSearchBar>()
1038            },
1039            cx,
1040        );
1041        BufferSearchBar::register(&mut registrar);
1042        let registrar = registrar.into_div();
1043        self.workspace
1044            .update(cx, |workspace, cx| {
1045                registrar.size_full().child(self.center.render(
1046                    workspace.project(),
1047                    &HashMap::default(),
1048                    None,
1049                    &self.active_pane,
1050                    workspace.zoomed_item(),
1051                    workspace.app_state(),
1052                    cx,
1053                ))
1054            })
1055            .ok()
1056            .map(|div| {
1057                div.on_action({
1058                    cx.listener(|terminal_panel, action: &ActivatePaneInDirection, cx| {
1059                        if let Some(pane) = terminal_panel.center.find_pane_in_direction(
1060                            &terminal_panel.active_pane,
1061                            action.0,
1062                            cx,
1063                        ) {
1064                            cx.focus_view(&pane);
1065                        } else {
1066                            terminal_panel
1067                                .workspace
1068                                .update(cx, |workspace, cx| {
1069                                    workspace.activate_pane_in_direction(action.0, cx)
1070                                })
1071                                .ok();
1072                        }
1073                    })
1074                })
1075                .on_action(
1076                    cx.listener(|terminal_panel, _action: &ActivateNextPane, cx| {
1077                        let panes = terminal_panel.center.panes();
1078                        if let Some(ix) = panes
1079                            .iter()
1080                            .position(|pane| **pane == terminal_panel.active_pane)
1081                        {
1082                            let next_ix = (ix + 1) % panes.len();
1083                            let next_pane = panes[next_ix].clone();
1084                            cx.focus_view(&next_pane);
1085                        }
1086                    }),
1087                )
1088                .on_action(
1089                    cx.listener(|terminal_panel, _action: &ActivatePreviousPane, cx| {
1090                        let panes = terminal_panel.center.panes();
1091                        if let Some(ix) = panes
1092                            .iter()
1093                            .position(|pane| **pane == terminal_panel.active_pane)
1094                        {
1095                            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1096                            let prev_pane = panes[prev_ix].clone();
1097                            cx.focus_view(&prev_pane);
1098                        }
1099                    }),
1100                )
1101                .on_action(cx.listener(|terminal_panel, action: &ActivatePane, cx| {
1102                    let panes = terminal_panel.center.panes();
1103                    if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1104                        cx.focus_view(&pane);
1105                    } else {
1106                        let new_pane = terminal_panel.new_pane_with_cloned_active_terminal(cx);
1107                        cx.spawn(|terminal_panel, mut cx| async move {
1108                            if let Some(new_pane) = new_pane.await {
1109                                terminal_panel
1110                                    .update(&mut cx, |terminal_panel, cx| {
1111                                        terminal_panel
1112                                            .center
1113                                            .split(
1114                                                &terminal_panel.active_pane,
1115                                                &new_pane,
1116                                                SplitDirection::Right,
1117                                            )
1118                                            .log_err();
1119                                        cx.focus_view(&new_pane);
1120                                    })
1121                                    .ok();
1122                            }
1123                        })
1124                        .detach();
1125                    }
1126                }))
1127                .on_action(cx.listener(
1128                    |terminal_panel, action: &SwapPaneInDirection, cx| {
1129                        if let Some(to) = terminal_panel
1130                            .center
1131                            .find_pane_in_direction(&terminal_panel.active_pane, action.0, cx)
1132                            .cloned()
1133                        {
1134                            terminal_panel
1135                                .center
1136                                .swap(&terminal_panel.active_pane.clone(), &to);
1137                            cx.notify();
1138                        }
1139                    },
1140                ))
1141            })
1142            .unwrap_or_else(|| div())
1143    }
1144}
1145
1146impl FocusableView for TerminalPanel {
1147    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1148        self.active_pane.focus_handle(cx)
1149    }
1150}
1151
1152impl Panel for TerminalPanel {
1153    fn position(&self, cx: &WindowContext) -> DockPosition {
1154        match TerminalSettings::get_global(cx).dock {
1155            TerminalDockPosition::Left => DockPosition::Left,
1156            TerminalDockPosition::Bottom => DockPosition::Bottom,
1157            TerminalDockPosition::Right => DockPosition::Right,
1158        }
1159    }
1160
1161    fn position_is_valid(&self, _: DockPosition) -> bool {
1162        true
1163    }
1164
1165    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1166        settings::update_settings_file::<TerminalSettings>(
1167            self.fs.clone(),
1168            cx,
1169            move |settings, _| {
1170                let dock = match position {
1171                    DockPosition::Left => TerminalDockPosition::Left,
1172                    DockPosition::Bottom => TerminalDockPosition::Bottom,
1173                    DockPosition::Right => TerminalDockPosition::Right,
1174                };
1175                settings.dock = Some(dock);
1176            },
1177        );
1178    }
1179
1180    fn size(&self, cx: &WindowContext) -> Pixels {
1181        let settings = TerminalSettings::get_global(cx);
1182        match self.position(cx) {
1183            DockPosition::Left | DockPosition::Right => {
1184                self.width.unwrap_or(settings.default_width)
1185            }
1186            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1187        }
1188    }
1189
1190    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1191        match self.position(cx) {
1192            DockPosition::Left | DockPosition::Right => self.width = size,
1193            DockPosition::Bottom => self.height = size,
1194        }
1195        self.serialize(cx);
1196        cx.notify();
1197    }
1198
1199    fn is_zoomed(&self, cx: &WindowContext) -> bool {
1200        self.active_pane.read(cx).is_zoomed()
1201    }
1202
1203    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1204        for pane in self.center.panes() {
1205            pane.update(cx, |pane, cx| {
1206                pane.set_zoomed(zoomed, cx);
1207            })
1208        }
1209        cx.notify();
1210    }
1211
1212    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1213        if !active || !self.has_no_terminals(cx) {
1214            return;
1215        }
1216        cx.defer(|this, cx| {
1217            let Ok(kind) = this.workspace.update(cx, |workspace, cx| {
1218                TerminalKind::Shell(default_working_directory(workspace, cx))
1219            }) else {
1220                return;
1221            };
1222
1223            this.add_terminal(kind, RevealStrategy::Never, cx)
1224                .detach_and_log_err(cx)
1225        })
1226    }
1227
1228    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
1229        let count = self
1230            .center
1231            .panes()
1232            .into_iter()
1233            .map(|pane| pane.read(cx).items_len())
1234            .sum::<usize>();
1235        if count == 0 {
1236            None
1237        } else {
1238            Some(count.to_string())
1239        }
1240    }
1241
1242    fn persistent_name() -> &'static str {
1243        "TerminalPanel"
1244    }
1245
1246    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1247        if (self.enabled || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button {
1248            Some(IconName::Terminal)
1249        } else {
1250            None
1251        }
1252    }
1253
1254    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1255        Some("Terminal Panel")
1256    }
1257
1258    fn toggle_action(&self) -> Box<dyn gpui::Action> {
1259        Box::new(ToggleFocus)
1260    }
1261
1262    fn pane(&self) -> Option<View<Pane>> {
1263        Some(self.active_pane.clone())
1264    }
1265}
1266
1267struct InlineAssistTabBarButton {
1268    focus_handle: FocusHandle,
1269}
1270
1271impl Render for InlineAssistTabBarButton {
1272    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1273        let focus_handle = self.focus_handle.clone();
1274        IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
1275            .icon_size(IconSize::Small)
1276            .on_click(cx.listener(|_, _, cx| {
1277                cx.dispatch_action(InlineAssist::default().boxed_clone());
1278            }))
1279            .tooltip(move |cx| {
1280                Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx)
1281            })
1282    }
1283}
1284
1285fn retrieve_system_shell() -> Option<String> {
1286    #[cfg(not(target_os = "windows"))]
1287    {
1288        use anyhow::Context;
1289        use util::ResultExt;
1290
1291        std::env::var("SHELL")
1292            .context("Error finding SHELL in env.")
1293            .log_err()
1294    }
1295    // `alacritty_terminal` uses this as default on Windows. See:
1296    // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
1297    #[cfg(target_os = "windows")]
1298    return Some("powershell".to_owned());
1299}
1300
1301#[cfg(target_os = "windows")]
1302fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
1303    match shell_type {
1304        WindowsShellType::Powershell => to_powershell_variable(input),
1305        WindowsShellType::Cmd => to_cmd_variable(input),
1306        WindowsShellType::Other => input,
1307    }
1308}
1309
1310#[cfg(target_os = "windows")]
1311fn to_windows_shell_type(shell: &str) -> WindowsShellType {
1312    if shell == "powershell"
1313        || shell.ends_with("powershell.exe")
1314        || shell == "pwsh"
1315        || shell.ends_with("pwsh.exe")
1316    {
1317        WindowsShellType::Powershell
1318    } else if shell == "cmd" || shell.ends_with("cmd.exe") {
1319        WindowsShellType::Cmd
1320    } else {
1321        // Someother shell detected, the user might install and use a
1322        // unix-like shell.
1323        WindowsShellType::Other
1324    }
1325}
1326
1327/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
1328#[inline]
1329#[cfg(target_os = "windows")]
1330fn to_cmd_variable(input: String) -> String {
1331    if let Some(var_str) = input.strip_prefix("${") {
1332        if var_str.find(':').is_none() {
1333            // If the input starts with "${", remove the trailing "}"
1334            format!("%{}%", &var_str[..var_str.len() - 1])
1335        } else {
1336            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
1337            // which will result in the task failing to run in such cases.
1338            input
1339        }
1340    } else if let Some(var_str) = input.strip_prefix('$') {
1341        // If the input starts with "$", directly append to "$env:"
1342        format!("%{}%", var_str)
1343    } else {
1344        // If no prefix is found, return the input as is
1345        input
1346    }
1347}
1348
1349/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
1350#[inline]
1351#[cfg(target_os = "windows")]
1352fn to_powershell_variable(input: String) -> String {
1353    if let Some(var_str) = input.strip_prefix("${") {
1354        if var_str.find(':').is_none() {
1355            // If the input starts with "${", remove the trailing "}"
1356            format!("$env:{}", &var_str[..var_str.len() - 1])
1357        } else {
1358            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
1359            // which will result in the task failing to run in such cases.
1360            input
1361        }
1362    } else if let Some(var_str) = input.strip_prefix('$') {
1363        // If the input starts with "$", directly append to "$env:"
1364        format!("$env:{}", var_str)
1365    } else {
1366        // If no prefix is found, return the input as is
1367        input
1368    }
1369}
1370
1371#[cfg(target_os = "windows")]
1372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1373enum WindowsShellType {
1374    Powershell,
1375    Cmd,
1376    Other,
1377}