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}