terminal_panel.rs

  1use std::sync::Arc;
  2
  3use crate::TerminalView;
  4use db::kvp::KEY_VALUE_STORE;
  5use gpui::{
  6    actions, anyhow::Result, elements::*, serde_json, Action, AppContext, AsyncAppContext, Entity,
  7    Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
  8};
  9use project::Fs;
 10use serde::{Deserialize, Serialize};
 11use settings::SettingsStore;
 12use terminal::{TerminalDockPosition, TerminalSettings};
 13use util::{ResultExt, TryFutureExt};
 14use workspace::{
 15    dock::{DockPosition, Panel},
 16    item::Item,
 17    pane, DraggedItem, Pane, Workspace,
 18};
 19
 20const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
 21
 22actions!(terminal_panel, [ToggleFocus]);
 23
 24pub fn init(cx: &mut AppContext) {
 25    cx.add_action(TerminalPanel::new_terminal);
 26}
 27
 28#[derive(Debug)]
 29pub enum Event {
 30    Close,
 31    DockPositionChanged,
 32    ZoomIn,
 33    ZoomOut,
 34    Focus,
 35}
 36
 37pub struct TerminalPanel {
 38    pane: ViewHandle<Pane>,
 39    fs: Arc<dyn Fs>,
 40    workspace: WeakViewHandle<Workspace>,
 41    width: Option<f32>,
 42    height: Option<f32>,
 43    pending_serialization: Task<Option<()>>,
 44    _subscriptions: Vec<Subscription>,
 45}
 46
 47impl TerminalPanel {
 48    fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
 49        let weak_self = cx.weak_handle();
 50        let pane = cx.add_view(|cx| {
 51            let window = cx.window();
 52            let mut pane = Pane::new(
 53                workspace.weak_handle(),
 54                workspace.project().clone(),
 55                workspace.app_state().background_actions,
 56                Default::default(),
 57                cx,
 58            );
 59            pane.set_can_split(false, cx);
 60            pane.set_can_navigate(false, cx);
 61            pane.on_can_drop(move |drag_and_drop, cx| {
 62                drag_and_drop
 63                    .currently_dragged::<DraggedItem>(window)
 64                    .map_or(false, |(_, item)| {
 65                        item.handle.act_as::<TerminalView>(cx).is_some()
 66                    })
 67            });
 68            pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
 69                let this = weak_self.clone();
 70                Flex::row()
 71                    .with_child(Pane::render_tab_bar_button(
 72                        0,
 73                        "icons/plus_12.svg",
 74                        false,
 75                        Some(("New Terminal", Some(Box::new(workspace::NewTerminal)))),
 76                        cx,
 77                        move |_, cx| {
 78                            let this = this.clone();
 79                            cx.window_context().defer(move |cx| {
 80                                if let Some(this) = this.upgrade(cx) {
 81                                    this.update(cx, |this, cx| {
 82                                        this.add_terminal(cx);
 83                                    });
 84                                }
 85                            })
 86                        },
 87                        |_, _| {},
 88                        None,
 89                    ))
 90                    .with_child(Pane::render_tab_bar_button(
 91                        1,
 92                        if pane.is_zoomed() {
 93                            "icons/minimize_8.svg"
 94                        } else {
 95                            "icons/maximize_8.svg"
 96                        },
 97                        pane.is_zoomed(),
 98                        Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
 99                        cx,
100                        move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
101                        |_, _| {},
102                        None,
103                    ))
104                    .into_any()
105            });
106            let buffer_search_bar = cx.add_view(search::BufferSearchBar::new);
107            pane.toolbar()
108                .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
109            pane
110        });
111        let subscriptions = vec![
112            cx.observe(&pane, |_, _, cx| cx.notify()),
113            cx.subscribe(&pane, Self::handle_pane_event),
114        ];
115        let this = Self {
116            pane,
117            fs: workspace.app_state().fs.clone(),
118            workspace: workspace.weak_handle(),
119            pending_serialization: Task::ready(None),
120            width: None,
121            height: None,
122            _subscriptions: subscriptions,
123        };
124        let mut old_dock_position = this.position(cx);
125        cx.observe_global::<SettingsStore, _>(move |this, cx| {
126            let new_dock_position = this.position(cx);
127            if new_dock_position != old_dock_position {
128                old_dock_position = new_dock_position;
129                cx.emit(Event::DockPositionChanged);
130            }
131        })
132        .detach();
133        this
134    }
135
136    pub fn load(
137        workspace: WeakViewHandle<Workspace>,
138        cx: AsyncAppContext,
139    ) -> Task<Result<ViewHandle<Self>>> {
140        cx.spawn(|mut cx| async move {
141            let serialized_panel = if let Some(panel) = cx
142                .background()
143                .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
144                .await
145                .log_err()
146                .flatten()
147            {
148                Some(serde_json::from_str::<SerializedTerminalPanel>(&panel)?)
149            } else {
150                None
151            };
152            let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
153                let panel = cx.add_view(|cx| TerminalPanel::new(workspace, cx));
154                let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
155                    panel.update(cx, |panel, cx| {
156                        cx.notify();
157                        panel.height = serialized_panel.height;
158                        panel.width = serialized_panel.width;
159                        panel.pane.update(cx, |_, cx| {
160                            serialized_panel
161                                .items
162                                .iter()
163                                .map(|item_id| {
164                                    TerminalView::deserialize(
165                                        workspace.project().clone(),
166                                        workspace.weak_handle(),
167                                        workspace.database_id(),
168                                        *item_id,
169                                        cx,
170                                    )
171                                })
172                                .collect::<Vec<_>>()
173                        })
174                    })
175                } else {
176                    Default::default()
177                };
178                let pane = panel.read(cx).pane.clone();
179                (panel, pane, items)
180            })?;
181
182            let pane = pane.downgrade();
183            let items = futures::future::join_all(items).await;
184            pane.update(&mut cx, |pane, cx| {
185                let active_item_id = serialized_panel
186                    .as_ref()
187                    .and_then(|panel| panel.active_item_id);
188                let mut active_ix = None;
189                for item in items {
190                    if let Some(item) = item.log_err() {
191                        let item_id = item.id();
192                        pane.add_item(Box::new(item), false, false, None, cx);
193                        if Some(item_id) == active_item_id {
194                            active_ix = Some(pane.items_len() - 1);
195                        }
196                    }
197                }
198
199                if let Some(active_ix) = active_ix {
200                    pane.activate_item(active_ix, false, false, cx)
201                }
202            })?;
203
204            Ok(panel)
205        })
206    }
207
208    fn handle_pane_event(
209        &mut self,
210        _pane: ViewHandle<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(Event::Close),
218            pane::Event::ZoomIn => cx.emit(Event::ZoomIn),
219            pane::Event::ZoomOut => cx.emit(Event::ZoomOut),
220            pane::Event::Focus => cx.emit(Event::Focus),
221
222            pane::Event::AddItem { item } => {
223                if let Some(workspace) = self.workspace.upgrade(cx) {
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    fn new_terminal(
234        workspace: &mut Workspace,
235        _: &workspace::NewTerminal,
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| this.add_terminal(cx))
243    }
244
245    fn add_terminal(&mut self, cx: &mut ViewContext<Self>) {
246        let workspace = self.workspace.clone();
247        cx.spawn(|this, mut cx| async move {
248            let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
249            workspace.update(&mut cx, |workspace, cx| {
250                let working_directory_strategy = settings::get::<TerminalSettings>(cx)
251                    .working_directory
252                    .clone();
253                let working_directory =
254                    crate::get_working_directory(workspace, cx, working_directory_strategy);
255                let window = cx.window();
256                if let Some(terminal) = workspace.project().update(cx, |project, cx| {
257                    project
258                        .create_terminal(working_directory, window, cx)
259                        .log_err()
260                }) {
261                    let terminal = Box::new(cx.add_view(|cx| {
262                        TerminalView::new(
263                            terminal,
264                            workspace.weak_handle(),
265                            workspace.database_id(),
266                            cx,
267                        )
268                    }));
269                    pane.update(cx, |pane, cx| {
270                        let focus = pane.has_focus();
271                        pane.add_item(terminal, true, focus, None, cx);
272                    });
273                }
274            })?;
275            this.update(&mut cx, |this, cx| this.serialize(cx))?;
276            anyhow::Ok(())
277        })
278        .detach_and_log_err(cx);
279    }
280
281    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
282        let items = self
283            .pane
284            .read(cx)
285            .items()
286            .map(|item| item.id())
287            .collect::<Vec<_>>();
288        let active_item_id = self.pane.read(cx).active_item().map(|item| item.id());
289        let height = self.height;
290        let width = self.width;
291        self.pending_serialization = cx.background().spawn(
292            async move {
293                KEY_VALUE_STORE
294                    .write_kvp(
295                        TERMINAL_PANEL_KEY.into(),
296                        serde_json::to_string(&SerializedTerminalPanel {
297                            items,
298                            active_item_id,
299                            height,
300                            width,
301                        })?,
302                    )
303                    .await?;
304                anyhow::Ok(())
305            }
306            .log_err(),
307        );
308    }
309}
310
311impl Entity for TerminalPanel {
312    type Event = Event;
313}
314
315impl View for TerminalPanel {
316    fn ui_name() -> &'static str {
317        "TerminalPanel"
318    }
319
320    fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
321        ChildView::new(&self.pane, cx).into_any()
322    }
323
324    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
325        if cx.is_self_focused() {
326            cx.focus(&self.pane);
327        }
328    }
329}
330
331impl Panel for TerminalPanel {
332    fn position(&self, cx: &WindowContext) -> DockPosition {
333        match settings::get::<TerminalSettings>(cx).dock {
334            TerminalDockPosition::Left => DockPosition::Left,
335            TerminalDockPosition::Bottom => DockPosition::Bottom,
336            TerminalDockPosition::Right => DockPosition::Right,
337        }
338    }
339
340    fn position_is_valid(&self, _: DockPosition) -> bool {
341        true
342    }
343
344    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
345        settings::update_settings_file::<TerminalSettings>(self.fs.clone(), cx, move |settings| {
346            let dock = match position {
347                DockPosition::Left => TerminalDockPosition::Left,
348                DockPosition::Bottom => TerminalDockPosition::Bottom,
349                DockPosition::Right => TerminalDockPosition::Right,
350            };
351            settings.dock = Some(dock);
352        });
353    }
354
355    fn size(&self, cx: &WindowContext) -> f32 {
356        let settings = settings::get::<TerminalSettings>(cx);
357        match self.position(cx) {
358            DockPosition::Left | DockPosition::Right => {
359                self.width.unwrap_or_else(|| settings.default_width)
360            }
361            DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
362        }
363    }
364
365    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
366        match self.position(cx) {
367            DockPosition::Left | DockPosition::Right => self.width = Some(size),
368            DockPosition::Bottom => self.height = Some(size),
369        }
370        self.serialize(cx);
371        cx.notify();
372    }
373
374    fn should_zoom_in_on_event(event: &Event) -> bool {
375        matches!(event, Event::ZoomIn)
376    }
377
378    fn should_zoom_out_on_event(event: &Event) -> bool {
379        matches!(event, Event::ZoomOut)
380    }
381
382    fn is_zoomed(&self, cx: &WindowContext) -> bool {
383        self.pane.read(cx).is_zoomed()
384    }
385
386    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
387        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
388    }
389
390    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
391        if active && self.pane.read(cx).items_len() == 0 {
392            self.add_terminal(cx)
393        }
394    }
395
396    fn icon_path(&self) -> &'static str {
397        "icons/terminal_12.svg"
398    }
399
400    fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
401        ("Terminal Panel".into(), Some(Box::new(ToggleFocus)))
402    }
403
404    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
405        let count = self.pane.read(cx).items_len();
406        if count == 0 {
407            None
408        } else {
409            Some(count.to_string())
410        }
411    }
412
413    fn should_change_position_on_event(event: &Self::Event) -> bool {
414        matches!(event, Event::DockPositionChanged)
415    }
416
417    fn should_activate_on_event(_: &Self::Event) -> bool {
418        false
419    }
420
421    fn should_close_on_event(event: &Event) -> bool {
422        matches!(event, Event::Close)
423    }
424
425    fn has_focus(&self, cx: &WindowContext) -> bool {
426        self.pane.read(cx).has_focus()
427    }
428
429    fn is_focus_event(event: &Self::Event) -> bool {
430        matches!(event, Event::Focus)
431    }
432}
433
434#[derive(Serialize, Deserialize)]
435struct SerializedTerminalPanel {
436    items: Vec<usize>,
437    active_item_id: Option<usize>,
438    width: Option<f32>,
439    height: Option<f32>,
440}