terminal_panel.rs

  1use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
  2
  3use crate::{default_working_directory, TerminalView};
  4use collections::{HashMap, HashSet};
  5use db::kvp::KEY_VALUE_STORE;
  6use futures::future::join_all;
  7use gpui::{
  8    actions, Action, AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EventEmitter,
  9    ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render,
 10    Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
 11};
 12use itertools::Itertools;
 13use project::{terminals::TerminalKind, Fs, ProjectEntryId};
 14use search::{buffer_search::DivRegistrar, BufferSearchBar};
 15use serde::{Deserialize, Serialize};
 16use settings::Settings;
 17use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId};
 18use terminal::{
 19    terminal_settings::{TerminalDockPosition, TerminalSettings},
 20    Terminal,
 21};
 22use ui::{
 23    h_flex, ButtonCommon, Clickable, ContextMenu, FluentBuilder, IconButton, IconSize, Selectable,
 24    Tooltip,
 25};
 26use util::{ResultExt, TryFutureExt};
 27use workspace::{
 28    dock::{DockPosition, Panel, PanelEvent},
 29    item::SerializableItem,
 30    pane,
 31    ui::IconName,
 32    DraggedTab, ItemId, NewTerminal, Pane, ToggleZoom, Workspace,
 33};
 34
 35use anyhow::Result;
 36
 37const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
 38
 39actions!(terminal_panel, [ToggleFocus]);
 40
 41pub fn init(cx: &mut AppContext) {
 42    cx.observe_new_views(
 43        |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
 44            workspace.register_action(TerminalPanel::new_terminal);
 45            workspace.register_action(TerminalPanel::open_terminal);
 46            workspace.register_action(|workspace, _: &ToggleFocus, cx| {
 47                if workspace
 48                    .panel::<TerminalPanel>(cx)
 49                    .as_ref()
 50                    .is_some_and(|panel| panel.read(cx).enabled)
 51                {
 52                    workspace.toggle_panel_focus::<TerminalPanel>(cx);
 53                }
 54            });
 55        },
 56    )
 57    .detach();
 58}
 59
 60pub struct TerminalPanel {
 61    pane: View<Pane>,
 62    fs: Arc<dyn Fs>,
 63    workspace: WeakView<Workspace>,
 64    width: Option<Pixels>,
 65    height: Option<Pixels>,
 66    pending_serialization: Task<Option<()>>,
 67    pending_terminals_to_add: usize,
 68    _subscriptions: Vec<Subscription>,
 69    deferred_tasks: HashMap<TaskId, Task<()>>,
 70    enabled: bool,
 71    additional_tab_bar_buttons: Vec<AnyView>,
 72}
 73
 74impl TerminalPanel {
 75    fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
 76        let pane = cx.new_view(|cx| {
 77            let mut pane = Pane::new(
 78                workspace.weak_handle(),
 79                workspace.project().clone(),
 80                Default::default(),
 81                None,
 82                NewTerminal.boxed_clone(),
 83                cx,
 84            );
 85            pane.set_can_split(false, cx);
 86            pane.set_can_navigate(false, cx);
 87            pane.display_nav_history_buttons(None);
 88            pane.set_should_display_tab_bar(|_| true);
 89
 90            let workspace = workspace.weak_handle();
 91            pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| {
 92                if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
 93                    let item = if &tab.pane == cx.view() {
 94                        pane.item_for_index(tab.ix)
 95                    } else {
 96                        tab.pane.read(cx).item_for_index(tab.ix)
 97                    };
 98                    if let Some(item) = item {
 99                        if item.downcast::<TerminalView>().is_some() {
100                            return ControlFlow::Continue(());
101                        } else if let Some(project_path) = item.project_path(cx) {
102                            if let Some(entry_path) = workspace
103                                .update(cx, |workspace, cx| {
104                                    workspace
105                                        .project()
106                                        .read(cx)
107                                        .absolute_path(&project_path, cx)
108                                })
109                                .log_err()
110                                .flatten()
111                            {
112                                add_paths_to_terminal(pane, &[entry_path], cx);
113                            }
114                        }
115                    }
116                } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
117                    if let Some(entry_path) = workspace
118                        .update(cx, |workspace, cx| {
119                            let project = workspace.project().read(cx);
120                            project
121                                .path_for_entry(entry_id, cx)
122                                .and_then(|project_path| project.absolute_path(&project_path, cx))
123                        })
124                        .log_err()
125                        .flatten()
126                    {
127                        add_paths_to_terminal(pane, &[entry_path], cx);
128                    }
129                } else if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
130                    add_paths_to_terminal(pane, paths.paths(), cx);
131                }
132
133                ControlFlow::Break(())
134            });
135            let buffer_search_bar = cx.new_view(search::BufferSearchBar::new);
136            pane.toolbar()
137                .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
138            pane
139        });
140        let subscriptions = vec![
141            cx.observe(&pane, |_, _, cx| cx.notify()),
142            cx.subscribe(&pane, Self::handle_pane_event),
143        ];
144        let project = workspace.project().read(cx);
145        let enabled = project.is_local() || project.supports_remote_terminal(cx);
146        let this = Self {
147            pane,
148            fs: workspace.app_state().fs.clone(),
149            workspace: workspace.weak_handle(),
150            pending_serialization: Task::ready(None),
151            width: None,
152            height: None,
153            pending_terminals_to_add: 0,
154            deferred_tasks: HashMap::default(),
155            _subscriptions: subscriptions,
156            enabled,
157            additional_tab_bar_buttons: Vec::new(),
158        };
159        this.apply_tab_bar_buttons(cx);
160        this
161    }
162
163    pub fn register_tab_bar_button(
164        &mut self,
165        button: impl Into<AnyView>,
166        cx: &mut ViewContext<Self>,
167    ) {
168        self.additional_tab_bar_buttons.push(button.into());
169        self.apply_tab_bar_buttons(cx);
170    }
171
172    fn apply_tab_bar_buttons(&self, cx: &mut ViewContext<Self>) {
173        let additional_buttons = self.additional_tab_bar_buttons.clone();
174        self.pane.update(cx, |pane, cx| {
175            pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
176                h_flex()
177                    .gap_2()
178                    .children(additional_buttons.clone())
179                    .child(
180                        IconButton::new("plus", IconName::Plus)
181                            .icon_size(IconSize::Small)
182                            .on_click(cx.listener(|pane, _, cx| {
183                                let focus_handle = pane.focus_handle(cx);
184                                let menu = ContextMenu::build(cx, |menu, _| {
185                                    menu.action(
186                                        "New Terminal",
187                                        workspace::NewTerminal.boxed_clone(),
188                                    )
189                                    .entry(
190                                        "Spawn task",
191                                        Some(tasks_ui::Spawn::modal().boxed_clone()),
192                                        move |cx| {
193                                            // We want the focus to go back to terminal panel once task modal is dismissed,
194                                            // hence we focus that first. Otherwise, we'd end up without a focused element, as
195                                            // context menu will be gone the moment we spawn the modal.
196                                            cx.focus(&focus_handle);
197                                            cx.dispatch_action(
198                                                tasks_ui::Spawn::modal().boxed_clone(),
199                                            );
200                                        },
201                                    )
202                                });
203                                cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| {
204                                    pane.new_item_menu = None;
205                                })
206                                .detach();
207                                pane.new_item_menu = Some(menu);
208                            }))
209                            .tooltip(|cx| Tooltip::text("New...", cx)),
210                    )
211                    .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
212                        el.child(Pane::render_menu_overlay(new_item_menu))
213                    })
214                    .child({
215                        let zoomed = pane.is_zoomed();
216                        IconButton::new("toggle_zoom", IconName::Maximize)
217                            .icon_size(IconSize::Small)
218                            .selected(zoomed)
219                            .selected_icon(IconName::Minimize)
220                            .on_click(cx.listener(|pane, _, cx| {
221                                pane.toggle_zoom(&workspace::ToggleZoom, cx);
222                            }))
223                            .tooltip(move |cx| {
224                                Tooltip::for_action(
225                                    if zoomed { "Zoom Out" } else { "Zoom In" },
226                                    &ToggleZoom,
227                                    cx,
228                                )
229                            })
230                    })
231                    .into_any_element()
232            });
233        });
234    }
235
236    pub async fn load(
237        workspace: WeakView<Workspace>,
238        mut cx: AsyncWindowContext,
239    ) -> Result<View<Self>> {
240        let serialized_panel = cx
241            .background_executor()
242            .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
243            .await
244            .log_err()
245            .flatten()
246            .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
247            .transpose()
248            .log_err()
249            .flatten();
250
251        let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
252            let panel = cx.new_view(|cx| TerminalPanel::new(workspace, cx));
253            let items = if let Some((serialized_panel, database_id)) =
254                serialized_panel.as_ref().zip(workspace.database_id())
255            {
256                panel.update(cx, |panel, cx| {
257                    cx.notify();
258                    panel.height = serialized_panel.height.map(|h| h.round());
259                    panel.width = serialized_panel.width.map(|w| w.round());
260                    panel.pane.update(cx, |_, cx| {
261                        serialized_panel
262                            .items
263                            .iter()
264                            .map(|item_id| {
265                                TerminalView::deserialize(
266                                    workspace.project().clone(),
267                                    workspace.weak_handle(),
268                                    database_id,
269                                    *item_id,
270                                    cx,
271                                )
272                            })
273                            .collect::<Vec<_>>()
274                    })
275                })
276            } else {
277                Vec::new()
278            };
279            let pane = panel.read(cx).pane.clone();
280            (panel, pane, items)
281        })?;
282
283        if let Some(workspace) = workspace.upgrade() {
284            panel
285                .update(&mut cx, |panel, cx| {
286                    panel._subscriptions.push(cx.subscribe(
287                        &workspace,
288                        |terminal_panel, _, e, cx| {
289                            if let workspace::Event::SpawnTask(spawn_in_terminal) = e {
290                                terminal_panel.spawn_task(spawn_in_terminal, cx);
291                            };
292                        },
293                    ))
294                })
295                .ok();
296        }
297
298        let pane = pane.downgrade();
299        let items = futures::future::join_all(items).await;
300        let mut alive_item_ids = Vec::new();
301        pane.update(&mut cx, |pane, cx| {
302            let active_item_id = serialized_panel
303                .as_ref()
304                .and_then(|panel| panel.active_item_id);
305            let mut active_ix = None;
306            for item in items {
307                if let Some(item) = item.log_err() {
308                    let item_id = item.entity_id().as_u64();
309                    pane.add_item(Box::new(item), false, false, None, cx);
310                    alive_item_ids.push(item_id as ItemId);
311                    if Some(item_id) == active_item_id {
312                        active_ix = Some(pane.items_len() - 1);
313                    }
314                }
315            }
316
317            if let Some(active_ix) = active_ix {
318                pane.activate_item(active_ix, false, false, cx)
319            }
320        })?;
321
322        // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
323        if let Some(workspace) = workspace.upgrade() {
324            let cleanup_task = workspace.update(&mut cx, |workspace, cx| {
325                workspace
326                    .database_id()
327                    .map(|workspace_id| TerminalView::cleanup(workspace_id, alive_item_ids, cx))
328            })?;
329            if let Some(task) = cleanup_task {
330                task.await.log_err();
331            }
332        }
333
334        Ok(panel)
335    }
336
337    fn handle_pane_event(
338        &mut self,
339        _pane: View<Pane>,
340        event: &pane::Event,
341        cx: &mut ViewContext<Self>,
342    ) {
343        match event {
344            pane::Event::ActivateItem { .. } => self.serialize(cx),
345            pane::Event::RemovedItem { .. } => self.serialize(cx),
346            pane::Event::Remove => cx.emit(PanelEvent::Close),
347            pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
348            pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
349
350            pane::Event::AddItem { item } => {
351                if let Some(workspace) = self.workspace.upgrade() {
352                    let pane = self.pane.clone();
353                    workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
354                }
355            }
356
357            _ => {}
358        }
359    }
360
361    pub fn open_terminal(
362        workspace: &mut Workspace,
363        action: &workspace::OpenTerminal,
364        cx: &mut ViewContext<Workspace>,
365    ) {
366        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
367            return;
368        };
369
370        terminal_panel
371            .update(cx, |panel, cx| {
372                panel.add_terminal(
373                    TerminalKind::Shell(Some(action.working_directory.clone())),
374                    RevealStrategy::Always,
375                    cx,
376                )
377            })
378            .detach_and_log_err(cx);
379    }
380
381    fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
382        let mut spawn_task = spawn_in_terminal.clone();
383        // Set up shell args unconditionally, as tasks are always spawned inside of a shell.
384        let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() {
385            Shell::System => retrieve_system_shell().map(|shell| (shell, Vec::new())),
386            Shell::Program(shell) => Some((shell, Vec::new())),
387            Shell::WithArguments { program, args } => Some((program, args)),
388        }) else {
389            return;
390        };
391        #[cfg(target_os = "windows")]
392        let windows_shell_type = to_windows_shell_type(&shell);
393
394        #[cfg(not(target_os = "windows"))]
395        {
396            spawn_task.command_label = format!("{shell} -i -c `{}`", spawn_task.command_label);
397        }
398        #[cfg(target_os = "windows")]
399        {
400            use crate::terminal_panel::WindowsShellType;
401
402            match windows_shell_type {
403                WindowsShellType::Powershell => {
404                    spawn_task.command_label = format!("{shell} -C `{}`", spawn_task.command_label)
405                }
406                WindowsShellType::Cmd => {
407                    spawn_task.command_label = format!("{shell} /C `{}`", spawn_task.command_label)
408                }
409                WindowsShellType::Other => {
410                    spawn_task.command_label =
411                        format!("{shell} -i -c `{}`", spawn_task.command_label)
412                }
413            }
414        }
415
416        let task_command = std::mem::replace(&mut spawn_task.command, shell);
417        let task_args = std::mem::take(&mut spawn_task.args);
418        let combined_command = task_args
419            .into_iter()
420            .fold(task_command, |mut command, arg| {
421                command.push(' ');
422                #[cfg(not(target_os = "windows"))]
423                command.push_str(&arg);
424                #[cfg(target_os = "windows")]
425                command.push_str(&to_windows_shell_variable(windows_shell_type, arg));
426                command
427            });
428
429        #[cfg(not(target_os = "windows"))]
430        user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
431        #[cfg(target_os = "windows")]
432        {
433            use crate::terminal_panel::WindowsShellType;
434
435            match windows_shell_type {
436                WindowsShellType::Powershell => {
437                    user_args.extend(["-C".to_owned(), combined_command])
438                }
439                WindowsShellType::Cmd => user_args.extend(["/C".to_owned(), combined_command]),
440                WindowsShellType::Other => {
441                    user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command])
442                }
443            }
444        }
445        spawn_task.args = user_args;
446        let spawn_task = spawn_task;
447
448        let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
449        let use_new_terminal = spawn_in_terminal.use_new_terminal;
450
451        if allow_concurrent_runs && use_new_terminal {
452            self.spawn_in_new_terminal(spawn_task, cx)
453                .detach_and_log_err(cx);
454            return;
455        }
456
457        let terminals_for_task = self.terminals_for_task(&spawn_in_terminal.full_label, cx);
458        if terminals_for_task.is_empty() {
459            self.spawn_in_new_terminal(spawn_task, cx)
460                .detach_and_log_err(cx);
461            return;
462        }
463        let (existing_item_index, existing_terminal) = terminals_for_task
464            .last()
465            .expect("covered no terminals case above")
466            .clone();
467        if allow_concurrent_runs {
468            debug_assert!(
469                !use_new_terminal,
470                "Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
471            );
472            self.replace_terminal(spawn_task, existing_item_index, existing_terminal, cx);
473        } else {
474            self.deferred_tasks.insert(
475                spawn_in_terminal.id.clone(),
476                cx.spawn(|terminal_panel, mut cx| async move {
477                    wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
478                    terminal_panel
479                        .update(&mut cx, |terminal_panel, cx| {
480                            if use_new_terminal {
481                                terminal_panel
482                                    .spawn_in_new_terminal(spawn_task, cx)
483                                    .detach_and_log_err(cx);
484                            } else {
485                                terminal_panel.replace_terminal(
486                                    spawn_task,
487                                    existing_item_index,
488                                    existing_terminal,
489                                    cx,
490                                );
491                            }
492                        })
493                        .ok();
494                }),
495            );
496        }
497    }
498
499    pub fn spawn_in_new_terminal(
500        &mut self,
501        spawn_task: SpawnInTerminal,
502        cx: &mut ViewContext<Self>,
503    ) -> Task<Result<Model<Terminal>>> {
504        let reveal = spawn_task.reveal;
505        self.add_terminal(TerminalKind::Task(spawn_task), reveal, cx)
506    }
507
508    /// Create a new Terminal in the current working directory or the user's home directory
509    fn new_terminal(
510        workspace: &mut Workspace,
511        _: &workspace::NewTerminal,
512        cx: &mut ViewContext<Workspace>,
513    ) {
514        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
515            return;
516        };
517
518        let kind = TerminalKind::Shell(default_working_directory(workspace, cx));
519
520        terminal_panel
521            .update(cx, |this, cx| {
522                this.add_terminal(kind, RevealStrategy::Always, cx)
523            })
524            .detach_and_log_err(cx);
525    }
526
527    fn terminals_for_task(
528        &self,
529        label: &str,
530        cx: &mut AppContext,
531    ) -> Vec<(usize, View<TerminalView>)> {
532        self.pane
533            .read(cx)
534            .items()
535            .enumerate()
536            .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
537            .filter_map(|(index, terminal_view)| {
538                let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
539                if &task_state.full_label == label {
540                    Some((index, terminal_view))
541                } else {
542                    None
543                }
544            })
545            .collect()
546    }
547
548    fn activate_terminal_view(&self, item_index: usize, cx: &mut WindowContext) {
549        self.pane.update(cx, |pane, cx| {
550            pane.activate_item(item_index, true, true, cx)
551        })
552    }
553
554    fn add_terminal(
555        &mut self,
556        kind: TerminalKind,
557        reveal_strategy: RevealStrategy,
558        cx: &mut ViewContext<Self>,
559    ) -> Task<Result<Model<Terminal>>> {
560        if !self.enabled {
561            return Task::ready(Err(anyhow::anyhow!(
562                "terminal not yet supported for remote projects"
563            )));
564        }
565
566        let workspace = self.workspace.clone();
567        self.pending_terminals_to_add += 1;
568
569        cx.spawn(|terminal_panel, mut cx| async move {
570            let pane = terminal_panel.update(&mut cx, |this, _| this.pane.clone())?;
571            let result = workspace.update(&mut cx, |workspace, cx| {
572                let window = cx.window_handle();
573                let terminal = workspace
574                    .project()
575                    .update(cx, |project, cx| project.create_terminal(kind, window, cx))?;
576                let terminal_view = Box::new(cx.new_view(|cx| {
577                    TerminalView::new(
578                        terminal.clone(),
579                        workspace.weak_handle(),
580                        workspace.database_id(),
581                        cx,
582                    )
583                }));
584                pane.update(cx, |pane, cx| {
585                    let focus = pane.has_focus(cx);
586                    pane.add_item(terminal_view, true, focus, None, cx);
587                });
588
589                if reveal_strategy == RevealStrategy::Always {
590                    workspace.focus_panel::<Self>(cx);
591                }
592                Ok(terminal)
593            })?;
594            terminal_panel.update(&mut cx, |this, cx| {
595                this.pending_terminals_to_add = this.pending_terminals_to_add.saturating_sub(1);
596                this.serialize(cx)
597            })?;
598            result
599        })
600    }
601
602    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
603        let mut items_to_serialize = HashSet::default();
604        let items = self
605            .pane
606            .read(cx)
607            .items()
608            .filter_map(|item| {
609                let terminal_view = item.act_as::<TerminalView>(cx)?;
610                if terminal_view.read(cx).terminal().read(cx).task().is_some() {
611                    None
612                } else {
613                    let id = item.item_id().as_u64();
614                    items_to_serialize.insert(id);
615                    Some(id)
616                }
617            })
618            .collect::<Vec<_>>();
619        let active_item_id = self
620            .pane
621            .read(cx)
622            .active_item()
623            .map(|item| item.item_id().as_u64())
624            .filter(|active_id| items_to_serialize.contains(active_id));
625        let height = self.height;
626        let width = self.width;
627        self.pending_serialization = cx.background_executor().spawn(
628            async move {
629                KEY_VALUE_STORE
630                    .write_kvp(
631                        TERMINAL_PANEL_KEY.into(),
632                        serde_json::to_string(&SerializedTerminalPanel {
633                            items,
634                            active_item_id,
635                            height,
636                            width,
637                        })?,
638                    )
639                    .await?;
640                anyhow::Ok(())
641            }
642            .log_err(),
643        );
644    }
645
646    fn replace_terminal(
647        &self,
648        spawn_task: SpawnInTerminal,
649        terminal_item_index: usize,
650        terminal_to_replace: View<TerminalView>,
651        cx: &mut ViewContext<'_, Self>,
652    ) -> Option<()> {
653        let project = self
654            .workspace
655            .update(cx, |workspace, _| workspace.project().clone())
656            .ok()?;
657
658        let reveal = spawn_task.reveal;
659        let window = cx.window_handle();
660        let new_terminal = project.update(cx, |project, cx| {
661            project
662                .create_terminal(TerminalKind::Task(spawn_task), window, cx)
663                .log_err()
664        })?;
665        terminal_to_replace.update(cx, |terminal_to_replace, cx| {
666            terminal_to_replace.set_terminal(new_terminal, cx);
667        });
668
669        match reveal {
670            RevealStrategy::Always => {
671                self.activate_terminal_view(terminal_item_index, cx);
672                let task_workspace = self.workspace.clone();
673                cx.spawn(|_, mut cx| async move {
674                    task_workspace
675                        .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
676                        .ok()
677                })
678                .detach();
679            }
680            RevealStrategy::Never => {}
681        }
682
683        Some(())
684    }
685
686    fn has_no_terminals(&self, cx: &WindowContext) -> bool {
687        self.pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
688    }
689}
690
691async fn wait_for_terminals_tasks(
692    terminals_for_task: Vec<(usize, View<TerminalView>)>,
693    cx: &mut AsyncWindowContext,
694) {
695    let pending_tasks = terminals_for_task.iter().filter_map(|(_, terminal)| {
696        terminal
697            .update(cx, |terminal_view, cx| {
698                terminal_view
699                    .terminal()
700                    .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
701            })
702            .ok()
703    });
704    let _: Vec<()> = join_all(pending_tasks).await;
705}
706
707fn add_paths_to_terminal(pane: &mut Pane, paths: &[PathBuf], cx: &mut ViewContext<'_, Pane>) {
708    if let Some(terminal_view) = pane
709        .active_item()
710        .and_then(|item| item.downcast::<TerminalView>())
711    {
712        cx.focus_view(&terminal_view);
713        let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
714        new_text.push(' ');
715        terminal_view.update(cx, |terminal_view, cx| {
716            terminal_view.terminal().update(cx, |terminal, _| {
717                terminal.paste(&new_text);
718            });
719        });
720    }
721}
722
723impl EventEmitter<PanelEvent> for TerminalPanel {}
724
725impl Render for TerminalPanel {
726    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
727        let mut registrar = DivRegistrar::new(
728            |panel, cx| {
729                panel
730                    .pane
731                    .read(cx)
732                    .toolbar()
733                    .read(cx)
734                    .item_of_type::<BufferSearchBar>()
735            },
736            cx,
737        );
738        BufferSearchBar::register(&mut registrar);
739        registrar.into_div().size_full().child(self.pane.clone())
740    }
741}
742
743impl FocusableView for TerminalPanel {
744    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
745        self.pane.focus_handle(cx)
746    }
747}
748
749impl Panel for TerminalPanel {
750    fn position(&self, cx: &WindowContext) -> DockPosition {
751        match TerminalSettings::get_global(cx).dock {
752            TerminalDockPosition::Left => DockPosition::Left,
753            TerminalDockPosition::Bottom => DockPosition::Bottom,
754            TerminalDockPosition::Right => DockPosition::Right,
755        }
756    }
757
758    fn position_is_valid(&self, _: DockPosition) -> bool {
759        true
760    }
761
762    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
763        settings::update_settings_file::<TerminalSettings>(
764            self.fs.clone(),
765            cx,
766            move |settings, _| {
767                let dock = match position {
768                    DockPosition::Left => TerminalDockPosition::Left,
769                    DockPosition::Bottom => TerminalDockPosition::Bottom,
770                    DockPosition::Right => TerminalDockPosition::Right,
771                };
772                settings.dock = Some(dock);
773            },
774        );
775    }
776
777    fn size(&self, cx: &WindowContext) -> Pixels {
778        let settings = TerminalSettings::get_global(cx);
779        match self.position(cx) {
780            DockPosition::Left | DockPosition::Right => {
781                self.width.unwrap_or_else(|| settings.default_width)
782            }
783            DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
784        }
785    }
786
787    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
788        match self.position(cx) {
789            DockPosition::Left | DockPosition::Right => self.width = size,
790            DockPosition::Bottom => self.height = size,
791        }
792        self.serialize(cx);
793        cx.notify();
794    }
795
796    fn is_zoomed(&self, cx: &WindowContext) -> bool {
797        self.pane.read(cx).is_zoomed()
798    }
799
800    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
801        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
802    }
803
804    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
805        if !active || !self.has_no_terminals(cx) {
806            return;
807        }
808        cx.defer(|this, cx| {
809            let Ok(kind) = this.workspace.update(cx, |workspace, cx| {
810                TerminalKind::Shell(default_working_directory(workspace, cx))
811            }) else {
812                return;
813            };
814
815            this.add_terminal(kind, RevealStrategy::Never, cx)
816                .detach_and_log_err(cx)
817        })
818    }
819
820    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
821        let count = self.pane.read(cx).items_len();
822        if count == 0 {
823            None
824        } else {
825            Some(count.to_string())
826        }
827    }
828
829    fn persistent_name() -> &'static str {
830        "TerminalPanel"
831    }
832
833    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
834        if (self.enabled || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button {
835            Some(IconName::Terminal)
836        } else {
837            None
838        }
839    }
840
841    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
842        Some("Terminal Panel")
843    }
844
845    fn toggle_action(&self) -> Box<dyn gpui::Action> {
846        Box::new(ToggleFocus)
847    }
848
849    fn pane(&self) -> Option<View<Pane>> {
850        Some(self.pane.clone())
851    }
852}
853
854#[derive(Serialize, Deserialize)]
855struct SerializedTerminalPanel {
856    items: Vec<u64>,
857    active_item_id: Option<u64>,
858    width: Option<Pixels>,
859    height: Option<Pixels>,
860}
861
862fn retrieve_system_shell() -> Option<String> {
863    #[cfg(not(target_os = "windows"))]
864    {
865        use anyhow::Context;
866        use util::ResultExt;
867
868        return std::env::var("SHELL")
869            .context("Error finding SHELL in env.")
870            .log_err();
871    }
872    // `alacritty_terminal` uses this as default on Windows. See:
873    // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
874    #[cfg(target_os = "windows")]
875    return Some("powershell".to_owned());
876}
877
878#[cfg(target_os = "windows")]
879fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
880    match shell_type {
881        WindowsShellType::Powershell => to_powershell_variable(input),
882        WindowsShellType::Cmd => to_cmd_variable(input),
883        WindowsShellType::Other => input,
884    }
885}
886
887#[cfg(target_os = "windows")]
888fn to_windows_shell_type(shell: &str) -> WindowsShellType {
889    if shell == "powershell" || shell.ends_with("powershell.exe") {
890        WindowsShellType::Powershell
891    } else if shell == "cmd" || shell.ends_with("cmd.exe") {
892        WindowsShellType::Cmd
893    } else {
894        // Someother shell detected, the user might install and use a
895        // unix-like shell.
896        WindowsShellType::Other
897    }
898}
899
900/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
901#[inline]
902#[cfg(target_os = "windows")]
903fn to_cmd_variable(input: String) -> String {
904    if let Some(var_str) = input.strip_prefix("${") {
905        if var_str.find(':').is_none() {
906            // If the input starts with "${", remove the trailing "}"
907            format!("%{}%", &var_str[..var_str.len() - 1])
908        } else {
909            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
910            // which will result in the task failing to run in such cases.
911            input
912        }
913    } else if let Some(var_str) = input.strip_prefix('$') {
914        // If the input starts with "$", directly append to "$env:"
915        format!("%{}%", var_str)
916    } else {
917        // If no prefix is found, return the input as is
918        input
919    }
920}
921
922/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
923#[inline]
924#[cfg(target_os = "windows")]
925fn to_powershell_variable(input: String) -> String {
926    if let Some(var_str) = input.strip_prefix("${") {
927        if var_str.find(':').is_none() {
928            // If the input starts with "${", remove the trailing "}"
929            format!("$env:{}", &var_str[..var_str.len() - 1])
930        } else {
931            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
932            // which will result in the task failing to run in such cases.
933            input
934        }
935    } else if let Some(var_str) = input.strip_prefix('$') {
936        // If the input starts with "$", directly append to "$env:"
937        format!("$env:{}", var_str)
938    } else {
939        // If no prefix is found, return the input as is
940        input
941    }
942}
943
944#[cfg(target_os = "windows")]
945#[derive(Debug, Clone, Copy, PartialEq, Eq)]
946enum WindowsShellType {
947    Powershell,
948    Cmd,
949    Other,
950}