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