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