terminal_panel.rs

  1use std::{path::PathBuf, sync::Arc};
  2
  3use crate::TerminalView;
  4use db::kvp::KEY_VALUE_STORE;
  5use gpui::{
  6    actions, serde_json, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths,
  7    FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription,
  8    Task, View, ViewContext, VisualContext, WeakView, WindowContext,
  9};
 10use project::Fs;
 11use search::{buffer_search::DivRegistrar, BufferSearchBar};
 12use serde::{Deserialize, Serialize};
 13use settings::{Settings, SettingsStore};
 14use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings};
 15use ui::{h_stack, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip};
 16use util::{ResultExt, TryFutureExt};
 17use workspace::{
 18    dock::{DockPosition, Panel, PanelEvent},
 19    item::Item,
 20    pane,
 21    ui::Icon,
 22    Pane, Workspace,
 23};
 24
 25use anyhow::Result;
 26
 27const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
 28
 29actions!(terminal_panel, [ToggleFocus]);
 30
 31pub fn init(cx: &mut AppContext) {
 32    cx.observe_new_views(
 33        |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
 34            workspace.register_action(TerminalPanel::new_terminal);
 35            workspace.register_action(TerminalPanel::open_terminal);
 36            workspace.register_action(|workspace, _: &ToggleFocus, cx| {
 37                workspace.toggle_panel_focus::<TerminalPanel>(cx);
 38            });
 39        },
 40    )
 41    .detach();
 42}
 43
 44pub struct TerminalPanel {
 45    pane: View<Pane>,
 46    fs: Arc<dyn Fs>,
 47    workspace: WeakView<Workspace>,
 48    width: Option<Pixels>,
 49    height: Option<Pixels>,
 50    pending_serialization: Task<Option<()>>,
 51    _subscriptions: Vec<Subscription>,
 52}
 53
 54impl TerminalPanel {
 55    fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
 56        let terminal_panel = cx.view().downgrade();
 57        let pane = cx.new_view(|cx| {
 58            let mut pane = Pane::new(
 59                workspace.weak_handle(),
 60                workspace.project().clone(),
 61                Default::default(),
 62                Some(Arc::new(|a, cx| {
 63                    if let Some(tab) = a.downcast_ref::<workspace::pane::DraggedTab>() {
 64                        if let Some(item) = tab.pane.read(cx).item_for_index(tab.ix) {
 65                            return item.downcast::<TerminalView>().is_some();
 66                        }
 67                    }
 68                    if a.downcast_ref::<ExternalPaths>().is_some() {
 69                        return true;
 70                    }
 71
 72                    false
 73                })),
 74                cx,
 75            );
 76            pane.set_can_split(false, cx);
 77            pane.set_can_navigate(false, cx);
 78            pane.display_nav_history_buttons(false);
 79            pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
 80                let terminal_panel = terminal_panel.clone();
 81                h_stack()
 82                    .gap_2()
 83                    .child(
 84                        IconButton::new("plus", Icon::Plus)
 85                            .icon_size(IconSize::Small)
 86                            .on_click(move |_, cx| {
 87                                terminal_panel
 88                                    .update(cx, |panel, cx| panel.add_terminal(None, cx))
 89                                    .log_err();
 90                            })
 91                            .tooltip(|cx| Tooltip::text("New Terminal", cx)),
 92                    )
 93                    .child({
 94                        let zoomed = pane.is_zoomed();
 95                        IconButton::new("toggle_zoom", Icon::Maximize)
 96                            .icon_size(IconSize::Small)
 97                            .selected(zoomed)
 98                            .selected_icon(Icon::Minimize)
 99                            .on_click(cx.listener(|pane, _, cx| {
100                                pane.toggle_zoom(&workspace::ToggleZoom, cx);
101                            }))
102                            .tooltip(move |cx| {
103                                Tooltip::text(if zoomed { "Zoom Out" } else { "Zoom In" }, cx)
104                            })
105                    })
106                    .into_any_element()
107            });
108            let buffer_search_bar = cx.new_view(search::BufferSearchBar::new);
109            pane.toolbar()
110                .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
111            pane
112        });
113        let subscriptions = vec![
114            cx.observe(&pane, |_, _, cx| cx.notify()),
115            cx.subscribe(&pane, Self::handle_pane_event),
116        ];
117        let this = Self {
118            pane,
119            fs: workspace.app_state().fs.clone(),
120            workspace: workspace.weak_handle(),
121            pending_serialization: Task::ready(None),
122            width: None,
123            height: None,
124            _subscriptions: subscriptions,
125        };
126        let mut old_dock_position = this.position(cx);
127        cx.observe_global::<SettingsStore>(move |this, cx| {
128            let new_dock_position = this.position(cx);
129            if new_dock_position != old_dock_position {
130                old_dock_position = new_dock_position;
131                cx.emit(PanelEvent::ChangePosition);
132            }
133        })
134        .detach();
135        this
136    }
137
138    pub async fn load(
139        workspace: WeakView<Workspace>,
140        mut cx: AsyncWindowContext,
141    ) -> Result<View<Self>> {
142        let serialized_panel = cx
143            .background_executor()
144            .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
145            .await
146            .log_err()
147            .flatten()
148            .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
149            .transpose()
150            .log_err()
151            .flatten();
152
153        let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
154            let panel = cx.new_view(|cx| TerminalPanel::new(workspace, cx));
155            let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
156                panel.update(cx, |panel, cx| {
157                    cx.notify();
158                    panel.height = serialized_panel.height;
159                    panel.width = serialized_panel.width;
160                    panel.pane.update(cx, |_, cx| {
161                        serialized_panel
162                            .items
163                            .iter()
164                            .map(|item_id| {
165                                TerminalView::deserialize(
166                                    workspace.project().clone(),
167                                    workspace.weak_handle(),
168                                    workspace.database_id(),
169                                    *item_id,
170                                    cx,
171                                )
172                            })
173                            .collect::<Vec<_>>()
174                    })
175                })
176            } else {
177                Default::default()
178            };
179            let pane = panel.read(cx).pane.clone();
180            (panel, pane, items)
181        })?;
182
183        let pane = pane.downgrade();
184        let items = futures::future::join_all(items).await;
185        pane.update(&mut cx, |pane, cx| {
186            let active_item_id = serialized_panel
187                .as_ref()
188                .and_then(|panel| panel.active_item_id);
189            let mut active_ix = None;
190            for item in items {
191                if let Some(item) = item.log_err() {
192                    let item_id = item.entity_id().as_u64();
193                    pane.add_item(Box::new(item), false, false, None, cx);
194                    if Some(item_id) == active_item_id {
195                        active_ix = Some(pane.items_len() - 1);
196                    }
197                }
198            }
199
200            if let Some(active_ix) = active_ix {
201                pane.activate_item(active_ix, false, false, cx)
202            }
203        })?;
204
205        Ok(panel)
206    }
207
208    fn handle_pane_event(
209        &mut self,
210        _pane: View<Pane>,
211        event: &pane::Event,
212        cx: &mut ViewContext<Self>,
213    ) {
214        match event {
215            pane::Event::ActivateItem { .. } => self.serialize(cx),
216            pane::Event::RemoveItem { .. } => self.serialize(cx),
217            pane::Event::Remove => cx.emit(PanelEvent::Close),
218            pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
219            pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
220            pane::Event::Focus => cx.emit(PanelEvent::Focus),
221
222            pane::Event::AddItem { item } => {
223                if let Some(workspace) = self.workspace.upgrade() {
224                    let pane = self.pane.clone();
225                    workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
226                }
227            }
228
229            _ => {}
230        }
231    }
232
233    pub fn open_terminal(
234        workspace: &mut Workspace,
235        action: &workspace::OpenTerminal,
236        cx: &mut ViewContext<Workspace>,
237    ) {
238        let Some(this) = workspace.focus_panel::<Self>(cx) else {
239            return;
240        };
241
242        this.update(cx, |this, cx| {
243            this.add_terminal(Some(action.working_directory.clone()), cx)
244        })
245    }
246
247    ///Create a new Terminal in the current working directory or the user's home directory
248    fn new_terminal(
249        workspace: &mut Workspace,
250        _: &workspace::NewTerminal,
251        cx: &mut ViewContext<Workspace>,
252    ) {
253        let Some(this) = workspace.focus_panel::<Self>(cx) else {
254            return;
255        };
256
257        this.update(cx, |this, cx| this.add_terminal(None, cx))
258    }
259
260    fn add_terminal(&mut self, working_directory: Option<PathBuf>, cx: &mut ViewContext<Self>) {
261        let workspace = self.workspace.clone();
262        cx.spawn(|this, mut cx| async move {
263            let pane = this.update(&mut cx, |this, _| this.pane.clone())?;
264            workspace.update(&mut cx, |workspace, cx| {
265                let working_directory = if let Some(working_directory) = working_directory {
266                    Some(working_directory)
267                } else {
268                    let working_directory_strategy =
269                        TerminalSettings::get_global(cx).working_directory.clone();
270                    crate::get_working_directory(workspace, cx, working_directory_strategy)
271                };
272
273                let window = cx.window_handle();
274                if let Some(terminal) = workspace.project().update(cx, |project, cx| {
275                    project
276                        .create_terminal(working_directory, window, cx)
277                        .log_err()
278                }) {
279                    let terminal = Box::new(cx.new_view(|cx| {
280                        TerminalView::new(
281                            terminal,
282                            workspace.weak_handle(),
283                            workspace.database_id(),
284                            cx,
285                        )
286                    }));
287                    pane.update(cx, |pane, cx| {
288                        let focus = pane.has_focus(cx);
289                        pane.add_item(terminal, true, focus, None, cx);
290                    });
291                }
292            })?;
293            this.update(&mut cx, |this, cx| this.serialize(cx))?;
294            anyhow::Ok(())
295        })
296        .detach_and_log_err(cx);
297    }
298
299    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
300        let items = self
301            .pane
302            .read(cx)
303            .items()
304            .map(|item| item.item_id().as_u64())
305            .collect::<Vec<_>>();
306        let active_item_id = self
307            .pane
308            .read(cx)
309            .active_item()
310            .map(|item| item.item_id().as_u64());
311        let height = self.height;
312        let width = self.width;
313        self.pending_serialization = cx.background_executor().spawn(
314            async move {
315                KEY_VALUE_STORE
316                    .write_kvp(
317                        TERMINAL_PANEL_KEY.into(),
318                        serde_json::to_string(&SerializedTerminalPanel {
319                            items,
320                            active_item_id,
321                            height,
322                            width,
323                        })?,
324                    )
325                    .await?;
326                anyhow::Ok(())
327            }
328            .log_err(),
329        );
330    }
331}
332
333impl EventEmitter<PanelEvent> for TerminalPanel {}
334
335impl Render for TerminalPanel {
336    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
337        let mut registrar = DivRegistrar::new(
338            |panel, cx| {
339                panel
340                    .pane
341                    .read(cx)
342                    .toolbar()
343                    .read(cx)
344                    .item_of_type::<BufferSearchBar>()
345            },
346            cx,
347        );
348        BufferSearchBar::register_inner(&mut registrar);
349        registrar.into_div().size_full().child(self.pane.clone())
350    }
351}
352
353impl FocusableView for TerminalPanel {
354    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
355        self.pane.focus_handle(cx)
356    }
357}
358
359impl Panel for TerminalPanel {
360    fn position(&self, cx: &WindowContext) -> DockPosition {
361        match TerminalSettings::get_global(cx).dock {
362            TerminalDockPosition::Left => DockPosition::Left,
363            TerminalDockPosition::Bottom => DockPosition::Bottom,
364            TerminalDockPosition::Right => DockPosition::Right,
365        }
366    }
367
368    fn position_is_valid(&self, _: DockPosition) -> bool {
369        true
370    }
371
372    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
373        settings::update_settings_file::<TerminalSettings>(self.fs.clone(), cx, move |settings| {
374            let dock = match position {
375                DockPosition::Left => TerminalDockPosition::Left,
376                DockPosition::Bottom => TerminalDockPosition::Bottom,
377                DockPosition::Right => TerminalDockPosition::Right,
378            };
379            settings.dock = Some(dock);
380        });
381    }
382
383    fn size(&self, cx: &WindowContext) -> Pixels {
384        let settings = TerminalSettings::get_global(cx);
385        match self.position(cx) {
386            DockPosition::Left | DockPosition::Right => {
387                self.width.unwrap_or_else(|| settings.default_width)
388            }
389            DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
390        }
391    }
392
393    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
394        match self.position(cx) {
395            DockPosition::Left | DockPosition::Right => self.width = size,
396            DockPosition::Bottom => self.height = size,
397        }
398        self.serialize(cx);
399        cx.notify();
400    }
401
402    fn is_zoomed(&self, cx: &WindowContext) -> bool {
403        self.pane.read(cx).is_zoomed()
404    }
405
406    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
407        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
408    }
409
410    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
411        if active && self.pane.read(cx).items_len() == 0 {
412            self.add_terminal(None, cx)
413        }
414    }
415
416    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
417        let count = self.pane.read(cx).items_len();
418        if count == 0 {
419            None
420        } else {
421            Some(count.to_string())
422        }
423    }
424
425    fn persistent_name() -> &'static str {
426        "TerminalPanel"
427    }
428
429    fn icon(&self, _cx: &WindowContext) -> Option<Icon> {
430        Some(Icon::Terminal)
431    }
432
433    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
434        Some("Terminal Panel")
435    }
436
437    fn toggle_action(&self) -> Box<dyn gpui::Action> {
438        Box::new(ToggleFocus)
439    }
440}
441
442#[derive(Serialize, Deserialize)]
443struct SerializedTerminalPanel {
444    items: Vec<u64>,
445    active_item_id: Option<u64>,
446    width: Option<Pixels>,
447    height: Option<Pixels>,
448}