terminal_panel.rs

  1use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
  2
  3use crate::TerminalView;
  4use collections::{HashMap, HashSet};
  5use db::kvp::KEY_VALUE_STORE;
  6use futures::future::join_all;
  7use gpui::{
  8    actions, Action, 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::{Fs, ProjectEntryId};
 14use search::{buffer_search::DivRegistrar, BufferSearchBar};
 15use serde::{Deserialize, Serialize};
 16use settings::Settings;
 17use task::{RevealStrategy, SpawnInTerminal, TaskId, TerminalWorkDir};
 18use terminal::{
 19    terminal_settings::{Shell, 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::Item,
 30    pane,
 31    ui::IconName,
 32    DraggedTab, 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                workspace.toggle_panel_focus::<TerminalPanel>(cx);
 48            });
 49        },
 50    )
 51    .detach();
 52}
 53
 54pub struct TerminalPanel {
 55    pane: View<Pane>,
 56    fs: Arc<dyn Fs>,
 57    workspace: WeakView<Workspace>,
 58    width: Option<Pixels>,
 59    height: Option<Pixels>,
 60    pending_serialization: Task<Option<()>>,
 61    pending_terminals_to_add: usize,
 62    _subscriptions: Vec<Subscription>,
 63    deferred_tasks: HashMap<TaskId, Task<()>>,
 64}
 65
 66impl TerminalPanel {
 67    fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
 68        let pane = cx.new_view(|cx| {
 69            let mut pane = Pane::new(
 70                workspace.weak_handle(),
 71                workspace.project().clone(),
 72                Default::default(),
 73                None,
 74                NewTerminal.boxed_clone(),
 75                cx,
 76            );
 77            pane.set_can_split(false, cx);
 78            pane.set_can_navigate(false, cx);
 79            pane.display_nav_history_buttons(None);
 80            pane.set_should_display_tab_bar(|_| true);
 81            pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
 82                h_flex()
 83                    .gap_2()
 84                    .child(
 85                        IconButton::new("plus", IconName::Plus)
 86                            .icon_size(IconSize::Small)
 87                            .on_click(cx.listener(|pane, _, cx| {
 88                                let focus_handle = pane.focus_handle(cx);
 89                                let menu = ContextMenu::build(cx, |menu, _| {
 90                                    menu.action(
 91                                        "New Terminal",
 92                                        workspace::NewTerminal.boxed_clone(),
 93                                    )
 94                                    .entry(
 95                                        "Spawn task",
 96                                        Some(tasks_ui::Spawn::modal().boxed_clone()),
 97                                        move |cx| {
 98                                            // We want the focus to go back to terminal panel once task modal is dismissed,
 99                                            // hence we focus that first. Otherwise, we'd end up without a focused element, as
100                                            // context menu will be gone the moment we spawn the modal.
101                                            cx.focus(&focus_handle);
102                                            cx.dispatch_action(
103                                                tasks_ui::Spawn::modal().boxed_clone(),
104                                            );
105                                        },
106                                    )
107                                });
108                                cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| {
109                                    pane.new_item_menu = None;
110                                })
111                                .detach();
112                                pane.new_item_menu = Some(menu);
113                            }))
114                            .tooltip(|cx| Tooltip::text("New...", cx)),
115                    )
116                    .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
117                        el.child(Pane::render_menu_overlay(new_item_menu))
118                    })
119                    .child({
120                        let zoomed = pane.is_zoomed();
121                        IconButton::new("toggle_zoom", IconName::Maximize)
122                            .icon_size(IconSize::Small)
123                            .selected(zoomed)
124                            .selected_icon(IconName::Minimize)
125                            .on_click(cx.listener(|pane, _, cx| {
126                                pane.toggle_zoom(&workspace::ToggleZoom, cx);
127                            }))
128                            .tooltip(move |cx| {
129                                Tooltip::for_action(
130                                    if zoomed { "Zoom Out" } else { "Zoom In" },
131                                    &ToggleZoom,
132                                    cx,
133                                )
134                            })
135                    })
136                    .into_any_element()
137            });
138
139            let workspace = workspace.weak_handle();
140            pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| {
141                if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
142                    let item = if &tab.pane == cx.view() {
143                        pane.item_for_index(tab.ix)
144                    } else {
145                        tab.pane.read(cx).item_for_index(tab.ix)
146                    };
147                    if let Some(item) = item {
148                        if item.downcast::<TerminalView>().is_some() {
149                            return ControlFlow::Continue(());
150                        } else if let Some(project_path) = item.project_path(cx) {
151                            if let Some(entry_path) = workspace
152                                .update(cx, |workspace, cx| {
153                                    workspace
154                                        .project()
155                                        .read(cx)
156                                        .absolute_path(&project_path, cx)
157                                })
158                                .log_err()
159                                .flatten()
160                            {
161                                add_paths_to_terminal(pane, &[entry_path], cx);
162                            }
163                        }
164                    }
165                } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
166                    if let Some(entry_path) = workspace
167                        .update(cx, |workspace, cx| {
168                            let project = workspace.project().read(cx);
169                            project
170                                .path_for_entry(entry_id, cx)
171                                .and_then(|project_path| project.absolute_path(&project_path, cx))
172                        })
173                        .log_err()
174                        .flatten()
175                    {
176                        add_paths_to_terminal(pane, &[entry_path], cx);
177                    }
178                } else if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
179                    add_paths_to_terminal(pane, paths.paths(), cx);
180                }
181
182                ControlFlow::Break(())
183            });
184            let buffer_search_bar = cx.new_view(search::BufferSearchBar::new);
185            pane.toolbar()
186                .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
187            pane
188        });
189        let subscriptions = vec![
190            cx.observe(&pane, |_, _, cx| cx.notify()),
191            cx.subscribe(&pane, Self::handle_pane_event),
192        ];
193        let this = Self {
194            pane,
195            fs: workspace.app_state().fs.clone(),
196            workspace: workspace.weak_handle(),
197            pending_serialization: Task::ready(None),
198            width: None,
199            height: None,
200            pending_terminals_to_add: 0,
201            deferred_tasks: HashMap::default(),
202            _subscriptions: subscriptions,
203        };
204        this
205    }
206
207    pub async fn load(
208        workspace: WeakView<Workspace>,
209        mut cx: AsyncWindowContext,
210    ) -> Result<View<Self>> {
211        let serialized_panel = cx
212            .background_executor()
213            .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
214            .await
215            .log_err()
216            .flatten()
217            .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
218            .transpose()
219            .log_err()
220            .flatten();
221
222        let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
223            let panel = cx.new_view(|cx| TerminalPanel::new(workspace, cx));
224            let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
225                panel.update(cx, |panel, cx| {
226                    cx.notify();
227                    panel.height = serialized_panel.height.map(|h| h.round());
228                    panel.width = serialized_panel.width.map(|w| w.round());
229                    panel.pane.update(cx, |_, cx| {
230                        serialized_panel
231                            .items
232                            .iter()
233                            .map(|item_id| {
234                                TerminalView::deserialize(
235                                    workspace.project().clone(),
236                                    workspace.weak_handle(),
237                                    workspace.database_id(),
238                                    *item_id,
239                                    cx,
240                                )
241                            })
242                            .collect::<Vec<_>>()
243                    })
244                })
245            } else {
246                Vec::new()
247            };
248            let pane = panel.read(cx).pane.clone();
249            (panel, pane, items)
250        })?;
251
252        if let Some(workspace) = workspace.upgrade() {
253            panel
254                .update(&mut cx, |panel, cx| {
255                    panel._subscriptions.push(cx.subscribe(
256                        &workspace,
257                        |terminal_panel, _, e, cx| {
258                            if let workspace::Event::SpawnTask(spawn_in_terminal) = e {
259                                terminal_panel.spawn_task(spawn_in_terminal, cx);
260                            };
261                        },
262                    ))
263                })
264                .ok();
265        }
266
267        let pane = pane.downgrade();
268        let items = futures::future::join_all(items).await;
269        pane.update(&mut cx, |pane, cx| {
270            let active_item_id = serialized_panel
271                .as_ref()
272                .and_then(|panel| panel.active_item_id);
273            let mut active_ix = None;
274            for item in items {
275                if let Some(item) = item.log_err() {
276                    let item_id = item.entity_id().as_u64();
277                    pane.add_item(Box::new(item), false, false, None, cx);
278                    if Some(item_id) == active_item_id {
279                        active_ix = Some(pane.items_len() - 1);
280                    }
281                }
282            }
283
284            if let Some(active_ix) = active_ix {
285                pane.activate_item(active_ix, false, false, cx)
286            }
287        })?;
288
289        Ok(panel)
290    }
291
292    fn handle_pane_event(
293        &mut self,
294        _pane: View<Pane>,
295        event: &pane::Event,
296        cx: &mut ViewContext<Self>,
297    ) {
298        match event {
299            pane::Event::ActivateItem { .. } => self.serialize(cx),
300            pane::Event::RemoveItem { .. } => self.serialize(cx),
301            pane::Event::Remove => cx.emit(PanelEvent::Close),
302            pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
303            pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
304
305            pane::Event::AddItem { item } => {
306                if let Some(workspace) = self.workspace.upgrade() {
307                    let pane = self.pane.clone();
308                    workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
309                }
310            }
311
312            _ => {}
313        }
314    }
315
316    pub fn open_terminal(
317        workspace: &mut Workspace,
318        action: &workspace::OpenTerminal,
319        cx: &mut ViewContext<Workspace>,
320    ) {
321        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
322            return;
323        };
324
325        let terminal_work_dir = workspace
326            .project()
327            .read(cx)
328            .terminal_work_dir_for(Some(&action.working_directory), cx);
329
330        terminal_panel
331            .update(cx, |panel, cx| {
332                panel.add_terminal(terminal_work_dir, None, RevealStrategy::Always, cx)
333            })
334            .detach_and_log_err(cx);
335    }
336
337    fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
338        let mut spawn_task = spawn_in_terminal.clone();
339        // Set up shell args unconditionally, as tasks are always spawned inside of a shell.
340        let Some((shell, mut user_args)) = (match TerminalSettings::get_global(cx).shell.clone() {
341            Shell::System => std::env::var("SHELL").ok().map(|shell| (shell, Vec::new())),
342            Shell::Program(shell) => Some((shell, Vec::new())),
343            Shell::WithArguments { program, args } => Some((program, args)),
344        }) else {
345            return;
346        };
347
348        spawn_task.command_label = format!("{shell} -i -c `{}`", spawn_task.command_label);
349        let task_command = std::mem::replace(&mut spawn_task.command, shell);
350        let task_args = std::mem::take(&mut spawn_task.args);
351        let combined_command = task_args
352            .into_iter()
353            .fold(task_command, |mut command, arg| {
354                command.push(' ');
355                command.push_str(&arg);
356                command
357            });
358        user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
359        spawn_task.args = user_args;
360        let spawn_task = spawn_task;
361
362        let reveal = spawn_task.reveal;
363        let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
364        let use_new_terminal = spawn_in_terminal.use_new_terminal;
365
366        if allow_concurrent_runs && use_new_terminal {
367            self.spawn_in_new_terminal(spawn_task, cx)
368                .detach_and_log_err(cx);
369            return;
370        }
371
372        let terminals_for_task = self.terminals_for_task(&spawn_in_terminal.full_label, cx);
373        if terminals_for_task.is_empty() {
374            self.spawn_in_new_terminal(spawn_task, cx)
375                .detach_and_log_err(cx);
376            return;
377        }
378        let (existing_item_index, existing_terminal) = terminals_for_task
379            .last()
380            .expect("covered no terminals case above")
381            .clone();
382        if allow_concurrent_runs {
383            debug_assert!(
384                !use_new_terminal,
385                "Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
386            );
387            self.replace_terminal(spawn_task, existing_item_index, existing_terminal, cx);
388        } else {
389            self.deferred_tasks.insert(
390                spawn_in_terminal.id.clone(),
391                cx.spawn(|terminal_panel, mut cx| async move {
392                    wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
393                    terminal_panel
394                        .update(&mut cx, |terminal_panel, cx| {
395                            if use_new_terminal {
396                                terminal_panel
397                                    .spawn_in_new_terminal(spawn_task, cx)
398                                    .detach_and_log_err(cx);
399                            } else {
400                                terminal_panel.replace_terminal(
401                                    spawn_task,
402                                    existing_item_index,
403                                    existing_terminal,
404                                    cx,
405                                );
406                            }
407                        })
408                        .ok();
409                }),
410            );
411
412            match reveal {
413                RevealStrategy::Always => {
414                    self.activate_terminal_view(existing_item_index, cx);
415                    let task_workspace = self.workspace.clone();
416                    cx.spawn(|_, mut cx| async move {
417                        task_workspace
418                            .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
419                            .ok()
420                    })
421                    .detach();
422                }
423                RevealStrategy::Never => {}
424            }
425        }
426    }
427
428    pub fn spawn_in_new_terminal(
429        &mut self,
430        spawn_task: SpawnInTerminal,
431        cx: &mut ViewContext<Self>,
432    ) -> Task<Result<Model<Terminal>>> {
433        let reveal = spawn_task.reveal;
434        self.add_terminal(spawn_task.cwd.clone(), Some(spawn_task), reveal, cx)
435    }
436
437    /// Create a new Terminal in the current working directory or the user's home directory
438    fn new_terminal(
439        workspace: &mut Workspace,
440        _: &workspace::NewTerminal,
441        cx: &mut ViewContext<Workspace>,
442    ) {
443        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
444            return;
445        };
446
447        terminal_panel
448            .update(cx, |this, cx| {
449                this.add_terminal(None, None, RevealStrategy::Always, cx)
450            })
451            .detach_and_log_err(cx);
452    }
453
454    fn terminals_for_task(
455        &self,
456        label: &str,
457        cx: &mut AppContext,
458    ) -> Vec<(usize, View<TerminalView>)> {
459        self.pane
460            .read(cx)
461            .items()
462            .enumerate()
463            .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
464            .filter_map(|(index, terminal_view)| {
465                let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
466                if &task_state.full_label == label {
467                    Some((index, terminal_view))
468                } else {
469                    None
470                }
471            })
472            .collect()
473    }
474
475    fn activate_terminal_view(&self, item_index: usize, cx: &mut WindowContext) {
476        self.pane.update(cx, |pane, cx| {
477            pane.activate_item(item_index, true, true, cx)
478        })
479    }
480
481    fn add_terminal(
482        &mut self,
483        working_directory: Option<TerminalWorkDir>,
484        spawn_task: Option<SpawnInTerminal>,
485        reveal_strategy: RevealStrategy,
486        cx: &mut ViewContext<Self>,
487    ) -> Task<Result<Model<Terminal>>> {
488        let workspace = self.workspace.clone();
489        self.pending_terminals_to_add += 1;
490
491        cx.spawn(|terminal_panel, mut cx| async move {
492            let pane = terminal_panel.update(&mut cx, |this, _| this.pane.clone())?;
493            let result = workspace.update(&mut cx, |workspace, cx| {
494                let working_directory = if let Some(working_directory) = working_directory {
495                    Some(working_directory)
496                } else {
497                    let working_directory_strategy =
498                        TerminalSettings::get_global(cx).working_directory.clone();
499                    crate::get_working_directory(workspace, cx, working_directory_strategy)
500                };
501
502                let window = cx.window_handle();
503                let terminal = workspace.project().update(cx, |project, cx| {
504                    project.create_terminal(working_directory, spawn_task, window, cx)
505                })?;
506                let terminal_view = Box::new(cx.new_view(|cx| {
507                    TerminalView::new(
508                        terminal.clone(),
509                        workspace.weak_handle(),
510                        workspace.database_id(),
511                        cx,
512                    )
513                }));
514                pane.update(cx, |pane, cx| {
515                    let focus = pane.has_focus(cx);
516                    pane.add_item(terminal_view, true, focus, None, cx);
517                });
518
519                if reveal_strategy == RevealStrategy::Always {
520                    workspace.focus_panel::<Self>(cx);
521                }
522                Ok(terminal)
523            })?;
524            terminal_panel.update(&mut cx, |this, cx| {
525                this.pending_terminals_to_add = this.pending_terminals_to_add.saturating_sub(1);
526                this.serialize(cx)
527            })?;
528            result
529        })
530    }
531
532    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
533        let mut items_to_serialize = HashSet::default();
534        let items = self
535            .pane
536            .read(cx)
537            .items()
538            .filter_map(|item| {
539                let terminal_view = item.act_as::<TerminalView>(cx)?;
540                if terminal_view.read(cx).terminal().read(cx).task().is_some() {
541                    None
542                } else {
543                    let id = item.item_id().as_u64();
544                    items_to_serialize.insert(id);
545                    Some(id)
546                }
547            })
548            .collect::<Vec<_>>();
549        let active_item_id = self
550            .pane
551            .read(cx)
552            .active_item()
553            .map(|item| item.item_id().as_u64())
554            .filter(|active_id| items_to_serialize.contains(active_id));
555        let height = self.height;
556        let width = self.width;
557        self.pending_serialization = cx.background_executor().spawn(
558            async move {
559                KEY_VALUE_STORE
560                    .write_kvp(
561                        TERMINAL_PANEL_KEY.into(),
562                        serde_json::to_string(&SerializedTerminalPanel {
563                            items,
564                            active_item_id,
565                            height,
566                            width,
567                        })?,
568                    )
569                    .await?;
570                anyhow::Ok(())
571            }
572            .log_err(),
573        );
574    }
575
576    fn replace_terminal(
577        &self,
578        spawn_task: SpawnInTerminal,
579        terminal_item_index: usize,
580        terminal_to_replace: View<TerminalView>,
581        cx: &mut ViewContext<'_, Self>,
582    ) -> Option<()> {
583        let project = self
584            .workspace
585            .update(cx, |workspace, _| workspace.project().clone())
586            .ok()?;
587
588        let reveal = spawn_task.reveal;
589        let window = cx.window_handle();
590        let new_terminal = project.update(cx, |project, cx| {
591            project
592                .create_terminal(spawn_task.cwd.clone(), Some(spawn_task), window, cx)
593                .log_err()
594        })?;
595        terminal_to_replace.update(cx, |terminal_to_replace, cx| {
596            terminal_to_replace.set_terminal(new_terminal, cx);
597        });
598
599        match reveal {
600            RevealStrategy::Always => {
601                self.activate_terminal_view(terminal_item_index, cx);
602                let task_workspace = self.workspace.clone();
603                cx.spawn(|_, mut cx| async move {
604                    task_workspace
605                        .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
606                        .ok()
607                })
608                .detach();
609            }
610            RevealStrategy::Never => {}
611        }
612
613        Some(())
614    }
615
616    pub fn pane(&self) -> &View<Pane> {
617        &self.pane
618    }
619
620    fn has_no_terminals(&mut self, cx: &mut ViewContext<'_, Self>) -> bool {
621        self.pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
622    }
623}
624
625async fn wait_for_terminals_tasks(
626    terminals_for_task: Vec<(usize, View<TerminalView>)>,
627    cx: &mut AsyncWindowContext,
628) {
629    let pending_tasks = terminals_for_task.iter().filter_map(|(_, terminal)| {
630        terminal
631            .update(cx, |terminal_view, cx| {
632                terminal_view
633                    .terminal()
634                    .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
635            })
636            .ok()
637    });
638    let _: Vec<()> = join_all(pending_tasks).await;
639}
640
641fn add_paths_to_terminal(pane: &mut Pane, paths: &[PathBuf], cx: &mut ViewContext<'_, Pane>) {
642    if let Some(terminal_view) = pane
643        .active_item()
644        .and_then(|item| item.downcast::<TerminalView>())
645    {
646        cx.focus_view(&terminal_view);
647        let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
648        new_text.push(' ');
649        terminal_view.update(cx, |terminal_view, cx| {
650            terminal_view.terminal().update(cx, |terminal, _| {
651                terminal.paste(&new_text);
652            });
653        });
654    }
655}
656
657impl EventEmitter<PanelEvent> for TerminalPanel {}
658
659impl Render for TerminalPanel {
660    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
661        let mut registrar = DivRegistrar::new(
662            |panel, cx| {
663                panel
664                    .pane
665                    .read(cx)
666                    .toolbar()
667                    .read(cx)
668                    .item_of_type::<BufferSearchBar>()
669            },
670            cx,
671        );
672        BufferSearchBar::register(&mut registrar);
673        registrar.into_div().size_full().child(self.pane.clone())
674    }
675}
676
677impl FocusableView for TerminalPanel {
678    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
679        self.pane.focus_handle(cx)
680    }
681}
682
683impl Panel for TerminalPanel {
684    fn position(&self, cx: &WindowContext) -> DockPosition {
685        match TerminalSettings::get_global(cx).dock {
686            TerminalDockPosition::Left => DockPosition::Left,
687            TerminalDockPosition::Bottom => DockPosition::Bottom,
688            TerminalDockPosition::Right => DockPosition::Right,
689        }
690    }
691
692    fn position_is_valid(&self, _: DockPosition) -> bool {
693        true
694    }
695
696    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
697        settings::update_settings_file::<TerminalSettings>(self.fs.clone(), cx, move |settings| {
698            let dock = match position {
699                DockPosition::Left => TerminalDockPosition::Left,
700                DockPosition::Bottom => TerminalDockPosition::Bottom,
701                DockPosition::Right => TerminalDockPosition::Right,
702            };
703            settings.dock = Some(dock);
704        });
705    }
706
707    fn size(&self, cx: &WindowContext) -> Pixels {
708        let settings = TerminalSettings::get_global(cx);
709        match self.position(cx) {
710            DockPosition::Left | DockPosition::Right => {
711                self.width.unwrap_or_else(|| settings.default_width)
712            }
713            DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
714        }
715    }
716
717    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
718        match self.position(cx) {
719            DockPosition::Left | DockPosition::Right => self.width = size,
720            DockPosition::Bottom => self.height = size,
721        }
722        self.serialize(cx);
723        cx.notify();
724    }
725
726    fn is_zoomed(&self, cx: &WindowContext) -> bool {
727        self.pane.read(cx).is_zoomed()
728    }
729
730    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
731        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
732    }
733
734    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
735        if active && self.has_no_terminals(cx) {
736            self.add_terminal(None, None, RevealStrategy::Never, cx)
737                .detach_and_log_err(cx)
738        }
739    }
740
741    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
742        let count = self.pane.read(cx).items_len();
743        if count == 0 {
744            None
745        } else {
746            Some(count.to_string())
747        }
748    }
749
750    fn persistent_name() -> &'static str {
751        "TerminalPanel"
752    }
753
754    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
755        TerminalSettings::get_global(cx)
756            .button
757            .then(|| IconName::Terminal)
758    }
759
760    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
761        Some("Terminal Panel")
762    }
763
764    fn toggle_action(&self) -> Box<dyn gpui::Action> {
765        Box::new(ToggleFocus)
766    }
767}
768
769#[derive(Serialize, Deserialize)]
770struct SerializedTerminalPanel {
771    items: Vec<u64>,
772    active_item_id: Option<u64>,
773    width: Option<Pixels>,
774    height: Option<Pixels>,
775}