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