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