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
 28pub enum Event {
 29    Close,
 30    DockPositionChanged,
 31    ZoomIn,
 32    ZoomOut,
 33    Focus,
 34}
 35
 36pub struct TerminalPanel {
 37    pane: ViewHandle<Pane>,
 38    fs: Arc<dyn Fs>,
 39    workspace: WeakViewHandle<Workspace>,
 40    width: Option<f32>,
 41    height: Option<f32>,
 42    pending_serialization: Task<Option<()>>,
 43    _subscriptions: Vec<Subscription>,
 44}
 45
 46impl TerminalPanel {
 47    fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
 48        let weak_self = cx.weak_handle();
 49        let pane = cx.add_view(|cx| {
 50            let window_id = cx.window_id();
 51            let mut pane = Pane::new(
 52                workspace.weak_handle(),
 53                workspace.project().clone(),
 54                workspace.app_state().background_actions,
 55                Default::default(),
 56                cx,
 57            );
 58            pane.set_can_split(false, cx);
 59            pane.set_can_navigate(false, cx);
 60            pane.on_can_drop(move |drag_and_drop, cx| {
 61                drag_and_drop
 62                    .currently_dragged::<DraggedItem>(window_id)
 63                    .map_or(false, |(_, item)| {
 64                        item.handle.act_as::<TerminalView>(cx).is_some()
 65                    })
 66            });
 67            pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
 68                let this = weak_self.clone();
 69                Flex::row()
 70                    .with_child(Pane::render_tab_bar_button(
 71                        0,
 72                        "icons/plus_12.svg",
 73                        false,
 74                        Some((
 75                            "New Terminal".into(),
 76                            Some(Box::new(workspace::NewTerminal)),
 77                        )),
 78                        cx,
 79                        move |_, cx| {
 80                            let this = this.clone();
 81                            cx.window_context().defer(move |cx| {
 82                                if let Some(this) = this.upgrade(cx) {
 83                                    this.update(cx, |this, cx| {
 84                                        this.add_terminal(cx);
 85                                    });
 86                                }
 87                            })
 88                        },
 89                        None,
 90                    ))
 91                    .with_child(Pane::render_tab_bar_button(
 92                        1,
 93                        if pane.is_zoomed() {
 94                            "icons/minimize_8.svg"
 95                        } else {
 96                            "icons/maximize_8.svg"
 97                        },
 98                        pane.is_zoomed(),
 99                        Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
100                        cx,
101                        move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
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        }
223    }
224
225    fn new_terminal(
226        workspace: &mut Workspace,
227        _: &workspace::NewTerminal,
228        cx: &mut ViewContext<Workspace>,
229    ) {
230        let Some(this) = workspace.focus_panel::<Self>(cx) else {
231            return;
232        };
233
234        this.update(cx, |this, cx| this.add_terminal(cx))
235    }
236
237    fn add_terminal(&mut self, cx: &mut ViewContext<Self>) {
238        let workspace = self.workspace.clone();
239        cx.spawn(|this, mut cx| async move {
240            let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
241            workspace.update(&mut cx, |workspace, cx| {
242                let working_directory_strategy = settings::get::<TerminalSettings>(cx)
243                    .working_directory
244                    .clone();
245                let working_directory =
246                    crate::get_working_directory(workspace, cx, working_directory_strategy);
247                let window_id = cx.window_id();
248                if let Some(terminal) = workspace.project().update(cx, |project, cx| {
249                    project
250                        .create_terminal(working_directory, window_id, cx)
251                        .log_err()
252                }) {
253                    let terminal =
254                        Box::new(cx.add_view(|cx| {
255                            TerminalView::new(terminal, workspace.database_id(), cx)
256                        }));
257                    pane.update(cx, |pane, cx| {
258                        let focus = pane.has_focus();
259                        pane.add_item(terminal, true, focus, None, cx);
260                    });
261                }
262            })?;
263            this.update(&mut cx, |this, cx| this.serialize(cx))?;
264            anyhow::Ok(())
265        })
266        .detach_and_log_err(cx);
267    }
268
269    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
270        let items = self
271            .pane
272            .read(cx)
273            .items()
274            .map(|item| item.id())
275            .collect::<Vec<_>>();
276        let active_item_id = self.pane.read(cx).active_item().map(|item| item.id());
277        let height = self.height;
278        let width = self.width;
279        self.pending_serialization = cx.background().spawn(
280            async move {
281                KEY_VALUE_STORE
282                    .write_kvp(
283                        TERMINAL_PANEL_KEY.into(),
284                        serde_json::to_string(&SerializedTerminalPanel {
285                            items,
286                            active_item_id,
287                            height,
288                            width,
289                        })?,
290                    )
291                    .await?;
292                anyhow::Ok(())
293            }
294            .log_err(),
295        );
296    }
297}
298
299impl Entity for TerminalPanel {
300    type Event = Event;
301}
302
303impl View for TerminalPanel {
304    fn ui_name() -> &'static str {
305        "TerminalPanel"
306    }
307
308    fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
309        ChildView::new(&self.pane, cx).into_any()
310    }
311
312    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
313        if cx.is_self_focused() {
314            cx.focus(&self.pane);
315        }
316    }
317}
318
319impl Panel for TerminalPanel {
320    fn position(&self, cx: &WindowContext) -> DockPosition {
321        match settings::get::<TerminalSettings>(cx).dock {
322            TerminalDockPosition::Left => DockPosition::Left,
323            TerminalDockPosition::Bottom => DockPosition::Bottom,
324            TerminalDockPosition::Right => DockPosition::Right,
325        }
326    }
327
328    fn position_is_valid(&self, _: DockPosition) -> bool {
329        true
330    }
331
332    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
333        settings::update_settings_file::<TerminalSettings>(self.fs.clone(), cx, move |settings| {
334            let dock = match position {
335                DockPosition::Left => TerminalDockPosition::Left,
336                DockPosition::Bottom => TerminalDockPosition::Bottom,
337                DockPosition::Right => TerminalDockPosition::Right,
338            };
339            settings.dock = Some(dock);
340        });
341    }
342
343    fn size(&self, cx: &WindowContext) -> f32 {
344        let settings = settings::get::<TerminalSettings>(cx);
345        match self.position(cx) {
346            DockPosition::Left | DockPosition::Right => {
347                self.width.unwrap_or_else(|| settings.default_width)
348            }
349            DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
350        }
351    }
352
353    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
354        match self.position(cx) {
355            DockPosition::Left | DockPosition::Right => self.width = Some(size),
356            DockPosition::Bottom => self.height = Some(size),
357        }
358        self.serialize(cx);
359        cx.notify();
360    }
361
362    fn should_zoom_in_on_event(event: &Event) -> bool {
363        matches!(event, Event::ZoomIn)
364    }
365
366    fn should_zoom_out_on_event(event: &Event) -> bool {
367        matches!(event, Event::ZoomOut)
368    }
369
370    fn is_zoomed(&self, cx: &WindowContext) -> bool {
371        self.pane.read(cx).is_zoomed()
372    }
373
374    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
375        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
376    }
377
378    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
379        if active && self.pane.read(cx).items_len() == 0 {
380            self.add_terminal(cx)
381        }
382    }
383
384    fn icon_path(&self) -> &'static str {
385        "icons/terminal_12.svg"
386    }
387
388    fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
389        ("Terminal Panel".into(), Some(Box::new(ToggleFocus)))
390    }
391
392    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
393        let count = self.pane.read(cx).items_len();
394        if count == 0 {
395            None
396        } else {
397            Some(count.to_string())
398        }
399    }
400
401    fn should_change_position_on_event(event: &Self::Event) -> bool {
402        matches!(event, Event::DockPositionChanged)
403    }
404
405    fn should_activate_on_event(_: &Self::Event) -> bool {
406        false
407    }
408
409    fn should_close_on_event(event: &Event) -> bool {
410        matches!(event, Event::Close)
411    }
412
413    fn has_focus(&self, cx: &WindowContext) -> bool {
414        self.pane.read(cx).has_focus()
415    }
416
417    fn is_focus_event(event: &Self::Event) -> bool {
418        matches!(event, Event::Focus)
419    }
420}
421
422#[derive(Serialize, Deserialize)]
423struct SerializedTerminalPanel {
424    items: Vec<usize>,
425    active_item_id: Option<usize>,
426    width: Option<f32>,
427    height: Option<f32>,
428}