Finished erorr terminal refactoring

Mikayla Maki created

Change summary

crates/command_palette/src/command_palette.rs      |  14 
crates/file_finder/src/file_finder.rs              |  10 
crates/terminal/src/connection.rs                  |  12 
crates/terminal/src/modal.rs                       |  79 +-
crates/terminal/src/terminal.rs                    | 397 ++++++++-------
crates/terminal/src/terminal_element.rs            |  24 
crates/terminal/src/tests/terminal_test_context.rs |   6 
crates/workspace/src/workspace.rs                  |   6 
8 files changed, 282 insertions(+), 266 deletions(-)

Detailed changes

crates/command_palette/src/command_palette.rs 🔗

@@ -362,12 +362,7 @@ mod tests {
         });
 
         let palette = workspace.read_with(cx, |workspace, _| {
-            workspace
-                .modal()
-                .unwrap()
-                .clone()
-                .downcast::<CommandPalette>()
-                .unwrap()
+            workspace.modal::<CommandPalette>().unwrap()
         });
 
         palette
@@ -398,12 +393,7 @@ mod tests {
 
         // Assert editor command not present
         let palette = workspace.read_with(cx, |workspace, _| {
-            workspace
-                .modal()
-                .unwrap()
-                .clone()
-                .downcast::<CommandPalette>()
-                .unwrap()
+            workspace.modal::<CommandPalette>().unwrap()
         });
 
         palette

crates/file_finder/src/file_finder.rs 🔗

@@ -317,15 +317,7 @@ mod tests {
         let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         cx.dispatch_action(window_id, Toggle);
 
-        let finder = cx.read(|cx| {
-            workspace
-                .read(cx)
-                .modal()
-                .cloned()
-                .unwrap()
-                .downcast::<FileFinder>()
-                .unwrap()
-        });
+        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
         cx.dispatch_action(window_id, Input("b".into()));
         cx.dispatch_action(window_id, Input("n".into()));
         cx.dispatch_action(window_id, Input("a".into()));

crates/terminal/src/connection.rs 🔗

@@ -122,18 +122,18 @@ impl Display for TerminalError {
     }
 }
 
-pub struct DisconnectedPTY {
+pub struct TerminalBuilder {
     terminal: Terminal,
     events_rx: UnboundedReceiver<AlacTermEvent>,
 }
 
-impl DisconnectedPTY {
+impl TerminalBuilder {
     pub fn new(
         working_directory: Option<PathBuf>,
         shell: Option<Shell>,
         env: Option<HashMap<String, String>>,
         initial_size: TerminalDimensions,
-    ) -> Result<DisconnectedPTY> {
+    ) -> Result<TerminalBuilder> {
         let pty_config = {
             let alac_shell = shell.clone().and_then(|shell| match shell {
                 Shell::System => None,
@@ -214,13 +214,13 @@ impl DisconnectedPTY {
             associated_directory: working_directory,
         };
 
-        Ok(DisconnectedPTY {
+        Ok(TerminalBuilder {
             terminal,
             events_rx,
         })
     }
 
-    pub fn connect(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
+    pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
         cx.spawn_weak(|this, mut cx| async move {
             //Listen for terminal events
             while let Some(event) = self.events_rx.next().await {
@@ -435,7 +435,7 @@ impl Entity for Terminal {
 mod alacritty_unix {
     use alacritty_terminal::config::Program;
     use gpui::anyhow::{bail, Result};
-    use libc::{self};
+    use libc;
     use std::ffi::CStr;
     use std::mem::MaybeUninit;
     use std::ptr;

crates/terminal/src/modal.rs 🔗

@@ -1,60 +1,60 @@
 use gpui::{ModelHandle, ViewContext};
 use workspace::Workspace;
 
-use crate::{connection::Terminal, get_wd_for_workspace, DeployModal, Event, TerminalView};
+use crate::{
+    connection::Terminal, get_working_directory, DeployModal, Event, TerminalContent, TerminalView,
+};
 
 #[derive(Debug)]
-struct StoredConnection(ModelHandle<Terminal>);
+struct StoredTerminal(ModelHandle<Terminal>);
 
 pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
     // Pull the terminal connection out of the global if it has been stored
-    let possible_connection =
-        cx.update_default_global::<Option<StoredConnection>, _, _>(|possible_connection, _| {
+    let possible_terminal =
+        cx.update_default_global::<Option<StoredTerminal>, _, _>(|possible_connection, _| {
             possible_connection.take()
         });
 
-    if let Some(StoredConnection(stored_connection)) = possible_connection {
-        // Create a view from the stored connection
+    if let Some(StoredTerminal(stored_terminal)) = possible_terminal {
         workspace.toggle_modal(cx, |_, cx| {
-            cx.add_view(|cx| {
-                TerminalView::from_connection(
-                    crate::TerminalConnection(Ok(stored_connection.clone())),
-                    true,
-                    cx,
-                )
-            })
+            // Create a view from the stored connection if the terminal modal is not already shown
+            cx.add_view(|cx| TerminalView::from_terminal(stored_terminal.clone(), true, cx))
         });
-        cx.set_global::<Option<StoredConnection>>(Some(StoredConnection(
-            stored_connection.clone(),
-        )));
+        // Toggle Modal will dismiss the terminal modal if it is currently shown, so we must
+        // store the terminal back in the global
+        cx.set_global::<Option<StoredTerminal>>(Some(StoredTerminal(stored_terminal.clone())));
     } else {
         // No connection was stored, create a new terminal
         if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {
-            let wd = get_wd_for_workspace(workspace, cx);
+            // No terminal modal visible, construct a new one.
+            let working_directory = get_working_directory(workspace, cx);
 
-            //TODO fix this crash
-            let this = cx.add_view(|cx| TerminalView::new(wd, true, cx).unwrap());
+            let this = cx.add_view(|cx| TerminalView::new(working_directory, true, cx));
+
+            if let TerminalContent::Connected(connected) = &this.read(cx).content {
+                let terminal_handle = connected.read(cx).terminal.clone();
+                cx.subscribe(&terminal_handle, on_event).detach();
+                // Set the global immediately if terminal construction was successful,
+                // in case the user opens the command palette
+                cx.set_global::<Option<StoredTerminal>>(Some(StoredTerminal(
+                    terminal_handle.clone(),
+                )));
+            }
 
-            let connection_handle = this.read(cx).connection.0.as_ref().unwrap().clone();
-            cx.subscribe(&connection_handle, on_event).detach();
-            //Set the global immediately, in case the user opens the command palette
-            cx.set_global::<Option<StoredConnection>>(Some(StoredConnection(
-                connection_handle.clone(),
-            )));
             this
         }) {
-            let connection = closed_terminal_handle
-                .read(cx)
-                .connection
-                .0
-                .as_ref()
-                .unwrap()
-                .clone();
-            cx.set_global(Some(StoredConnection(connection)));
+            // Terminal modal was dismissed. Store terminal if the terminal view is connected
+            if let TerminalContent::Connected(connected) = &closed_terminal_handle.read(cx).content
+            {
+                let terminal_handle = connected.read(cx).terminal.clone();
+                // Set the global immediately if terminal construction was successful,
+                // in case the user opens the command palette
+                cx.set_global::<Option<StoredTerminal>>(Some(StoredTerminal(
+                    terminal_handle.clone(),
+                )));
+            }
         }
     }
-
-    //The problem is that the terminal modal is never re-stored.
 }
 
 pub fn on_event(
@@ -65,13 +65,8 @@ pub fn on_event(
 ) {
     // Dismiss the modal if the terminal quit
     if let Event::CloseTerminal = event {
-        cx.set_global::<Option<StoredConnection>>(None);
-        if workspace
-            .modal()
-            .cloned()
-            .and_then(|modal| modal.downcast::<TerminalView>())
-            .is_some()
-        {
+        cx.set_global::<Option<StoredTerminal>>(None);
+        if workspace.modal::<TerminalView>().is_some() {
             workspace.dismiss_modal(cx)
         }
     }

crates/terminal/src/terminal.rs 🔗

@@ -3,11 +3,11 @@ pub mod connection;
 mod modal;
 pub mod terminal_element;
 
-use connection::{DisconnectedPTY, Event, Terminal, TerminalError};
+use connection::{Event, Terminal, TerminalBuilder, TerminalError};
 use dirs::home_dir;
 use gpui::{
-    actions, elements::*, geometry::vector::vec2f, keymap::Keystroke, AppContext, ClipboardItem,
-    Entity, ModelHandle, MutableAppContext, View, ViewContext,
+    actions, elements::*, geometry::vector::vec2f, keymap::Keystroke, AnyViewHandle, AppContext,
+    ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle,
 };
 use modal::deploy_modal;
 
@@ -16,7 +16,6 @@ use settings::{Settings, WorkingDirectory};
 use smallvec::SmallVec;
 use std::path::{Path, PathBuf};
 use terminal_element::{terminal_layout_context::TerminalLayoutData, TerminalDimensions};
-use util::ResultExt;
 use workspace::{Item, Workspace};
 
 use crate::terminal_element::TerminalEl;
@@ -49,25 +48,49 @@ actions!(
 ///Initialize and register all of our action handlers
 pub fn init(cx: &mut MutableAppContext) {
     //Global binding overrrides
-    cx.add_action(TerminalView::ctrl_c);
-    cx.add_action(TerminalView::up);
-    cx.add_action(TerminalView::down);
-    cx.add_action(TerminalView::escape);
-    cx.add_action(TerminalView::enter);
+    cx.add_action(ConnectedView::ctrl_c);
+    cx.add_action(ConnectedView::up);
+    cx.add_action(ConnectedView::down);
+    cx.add_action(ConnectedView::escape);
+    cx.add_action(ConnectedView::enter);
     //Useful terminal actions
-    cx.add_action(TerminalView::deploy);
+    cx.add_action(ConnectedView::deploy);
+    cx.add_action(ConnectedView::copy);
+    cx.add_action(ConnectedView::paste);
+    cx.add_action(ConnectedView::clear);
     cx.add_action(deploy_modal);
-    cx.add_action(TerminalView::copy);
-    cx.add_action(TerminalView::paste);
-    cx.add_action(TerminalView::clear);
 }
 
-//New Type to make terminal connection's easier
-struct TerminalConnection(Result<ModelHandle<Terminal>, TerminalError>);
+//Make terminal view an enum, that can give you views for the error and non-error states
+//Take away all the result unwrapping in the current TerminalView by making it 'infallible'
+//Bubble up to deploy(_modal)() calls
+
+enum TerminalContent {
+    Connected(ViewHandle<ConnectedView>),
+    Error(ViewHandle<ErrorView>),
+}
+
+impl TerminalContent {
+    fn handle(&self) -> AnyViewHandle {
+        match self {
+            Self::Connected(handle) => handle.into(),
+            Self::Error(handle) => handle.into(),
+        }
+    }
+}
 
-///A terminal view, maintains the PTY's file handles and communicates with the terminal
 pub struct TerminalView {
-    connection: TerminalConnection,
+    modal: bool,
+    content: TerminalContent,
+}
+
+pub struct ErrorView {
+    error: TerminalError,
+}
+
+///A terminal view, maintains the PTY's file handles and communicates with the terminal
+pub struct ConnectedView {
+    terminal: ModelHandle<Terminal>,
     has_new_content: bool,
     //Currently using iTerm bell, show bell emoji in tab until input is received
     has_bell: bool,
@@ -79,14 +102,18 @@ impl Entity for TerminalView {
     type Event = Event;
 }
 
+impl Entity for ConnectedView {
+    type Event = Event;
+}
+
+impl Entity for ErrorView {
+    type Event = Event;
+}
+
 impl TerminalView {
     ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices
     ///To get the right working directory from a workspace, use: `get_wd_for_workspace()`
-    fn new(
-        working_directory: Option<PathBuf>,
-        modal: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<Self> {
+    fn new(working_directory: Option<PathBuf>, modal: bool, cx: &mut ViewContext<Self>) -> Self {
         //The details here don't matter, the terminal will be resized on the first layout
         let size_info = TerminalDimensions::new(
             DEBUG_LINE_HEIGHT,
@@ -98,214 +125,210 @@ impl TerminalView {
         let shell = settings.terminal_overrides.shell.clone();
         let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
 
-        let connection = DisconnectedPTY::new(working_directory, shell, envs, size_info)
-            .map(|pty| cx.add_model(|cx| pty.connect(cx)))
-            .map_err(|err| {
-                match err.downcast::<TerminalError>() {
-                    Ok(err) => err,
-                    Err(_) => unreachable!(), //This should never happen
-                }
-            });
-
-        if let Ok(_) = connection {
-            Some(TerminalView::from_connection(
-                TerminalConnection(connection),
-                modal,
-                cx,
-            ))
+        let content = match TerminalBuilder::new(working_directory, shell, envs, size_info) {
+            Ok(terminal) => {
+                let terminal = cx.add_model(|cx| terminal.subscribe(cx));
+                let view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx));
+                cx.subscribe(&view, |_this, _content, event, cx| cx.emit(event.clone()))
+                    .detach();
+                TerminalContent::Connected(view)
+            }
+            Err(error) => {
+                let view = cx.add_view(|_| ErrorView {
+                    error: error.downcast::<TerminalError>().unwrap(),
+                });
+                TerminalContent::Error(view)
+            }
+        };
+        cx.focus(content.handle());
+
+        TerminalView { modal, content }
+    }
+
+    fn from_terminal(
+        terminal: ModelHandle<Terminal>,
+        modal: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let connected_view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx));
+        TerminalView {
+            modal,
+            content: TerminalContent::Connected(connected_view),
+        }
+    }
+}
+
+impl View for TerminalView {
+    fn ui_name() -> &'static str {
+        "Terminal"
+    }
+
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
+        let child_view = match &self.content {
+            TerminalContent::Connected(connected) => ChildView::new(connected),
+            TerminalContent::Error(error) => ChildView::new(error),
+        };
+
+        if self.modal {
+            let settings = cx.global::<Settings>();
+            let container_style = settings.theme.terminal.modal_container;
+            child_view.contained().with_style(container_style).boxed()
         } else {
-            connection.log_err();
-            None
+            child_view.boxed()
+        }
+    }
+
+    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Activate);
+        cx.defer(|view, cx| {
+            cx.focus(view.content.handle());
+        });
+    }
+
+    fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
+        let mut context = Self::default_keymap_context();
+        if self.modal {
+            context.set.insert("ModalTerminal".into());
         }
+        context
     }
+}
 
-    fn from_connection(
-        connection: TerminalConnection,
+impl ConnectedView {
+    fn from_terminal(
+        terminal: ModelHandle<Terminal>,
         modal: bool,
         cx: &mut ViewContext<Self>,
-    ) -> TerminalView {
-        match connection.0.as_ref() {
-            Ok(conn) => {
-                cx.observe(conn, |_, _, cx| cx.notify()).detach();
-                cx.subscribe(conn, |this, _, event, cx| match event {
-                    Event::Wakeup => {
-                        if cx.is_self_focused() {
-                            cx.notify()
-                        } else {
-                            this.has_new_content = true;
-                            cx.emit(Event::TitleChanged);
-                        }
-                    }
-                    Event::Bell => {
-                        this.has_bell = true;
-                        cx.emit(Event::TitleChanged);
-                    }
-                    _ => cx.emit(*event),
-                })
-                .detach();
+    ) -> Self {
+        cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
+        cx.subscribe(&terminal, |this, _, event, cx| match event {
+            Event::Wakeup => {
+                if cx.is_self_focused() {
+                    cx.notify()
+                } else {
+                    this.has_new_content = true;
+                    cx.emit(Event::TitleChanged);
+                }
             }
-            Err(_) => { /* Leave it as is */ }
-        }
+            Event::Bell => {
+                this.has_bell = true;
+                cx.emit(Event::TitleChanged);
+            }
+            _ => cx.emit(*event),
+        })
+        .detach();
 
-        TerminalView {
-            connection,
+        Self {
+            terminal,
             has_new_content: true,
             has_bell: false,
             modal,
         }
     }
 
-    fn clear_bel(&mut self, cx: &mut ViewContext<Self>) {
+    fn clear_bel(&mut self, cx: &mut ViewContext<ConnectedView>) {
         self.has_bell = false;
         cx.emit(Event::TitleChanged);
     }
 
     ///Create a new Terminal in the current working directory or the user's home directory
     fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
-        let wd = get_wd_for_workspace(workspace, cx);
-        if let Some(view) = cx.add_option_view(|cx| TerminalView::new(wd, false, cx)) {
-            workspace.add_item(Box::new(view), cx);
-        }
+        let working_directory = get_working_directory(workspace, cx);
+        let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx));
+        workspace.add_item(Box::new(view), cx);
     }
 
     fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
-        self.connection
-            .0
-            .as_ref()
-            .map(|term_handle| term_handle.read(cx).clear())
-            .ok();
+        self.terminal.read(cx).clear();
     }
 
     ///Attempt to paste the clipboard into the terminal
     fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
-        self.connection
-            .0
-            .as_ref()
-            .map(|handle| handle.read(cx))
-            .map(|term| term.copy())
-            .map(|text| text.map(|text| cx.write_to_clipboard(ClipboardItem::new(text))))
-            .ok();
+        self.terminal
+            .read(cx)
+            .copy()
+            .map(|text| cx.write_to_clipboard(ClipboardItem::new(text)));
     }
 
     ///Attempt to paste the clipboard into the terminal
     fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
         cx.read_from_clipboard().map(|item| {
-            self.connection
-                .0
-                .as_ref()
-                .map(|handle| handle.read(cx))
-                .map(|term| term.paste(item.text()))
-                .ok();
+            self.terminal.read(cx).paste(item.text());
         });
     }
 
     ///Synthesize the keyboard event corresponding to 'up'
     fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
-        self.connection
-            .0
-            .as_ref()
-            .map(|handle| handle.read(cx))
-            .map(|term| term.try_keystroke(&Keystroke::parse("up").unwrap()))
-            .ok();
+        self.terminal
+            .read(cx)
+            .try_keystroke(&Keystroke::parse("up").unwrap());
     }
 
     ///Synthesize the keyboard event corresponding to 'down'
     fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
-        self.connection
-            .0
-            .as_ref()
-            .map(|handle| handle.read(cx))
-            .map(|term| term.try_keystroke(&Keystroke::parse("down").unwrap()))
-            .ok();
+        self.terminal
+            .read(cx)
+            .try_keystroke(&Keystroke::parse("down").unwrap());
     }
 
     ///Synthesize the keyboard event corresponding to 'ctrl-c'
     fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
-        self.connection
-            .0
-            .as_ref()
-            .map(|handle| handle.read(cx))
-            .map(|term| term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap()))
-            .ok();
+        self.terminal
+            .read(cx)
+            .try_keystroke(&Keystroke::parse("ctrl-c").unwrap());
     }
 
     ///Synthesize the keyboard event corresponding to 'escape'
     fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
-        self.connection
-            .0
-            .as_ref()
-            .map(|handle| handle.read(cx))
-            .map(|term| term.try_keystroke(&Keystroke::parse("escape").unwrap()))
-            .ok();
+        self.terminal
+            .read(cx)
+            .try_keystroke(&Keystroke::parse("escape").unwrap());
     }
 
     ///Synthesize the keyboard event corresponding to 'enter'
     fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
-        self.connection
-            .0
-            .as_ref()
-            .map(|handle| handle.read(cx))
-            .map(|term| term.try_keystroke(&Keystroke::parse("enter").unwrap()))
-            .ok();
+        self.terminal
+            .read(cx)
+            .try_keystroke(&Keystroke::parse("enter").unwrap());
     }
 }
 
-impl View for TerminalView {
+impl View for ConnectedView {
     fn ui_name() -> &'static str {
-        "Terminal"
+        "Connected Terminal View"
     }
 
     fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
-        let element = match self.connection.0.as_ref() {
-            Ok(handle) => {
-                let connection_handle = handle.clone().downgrade();
-                TerminalEl::new(cx.handle(), connection_handle, self.modal).contained()
-            }
-            Err(e) => {
-                let settings = cx.global::<Settings>();
-                let style = TerminalLayoutData::make_text_style(cx.font_cache(), settings);
-
-                Flex::column()
-                    .with_child(
-                        Flex::row()
-                            .with_child(
-                                Label::new(
-                                    format!(
-                                        "Failed to open the terminal. Info: \n{}",
-                                        e.to_string()
-                                    ),
-                                    style,
-                                )
-                                .boxed(),
-                            )
-                            .aligned()
-                            .boxed(),
-                    )
-                    .aligned()
-                    .contained()
-            }
-        };
-
-        if self.modal {
-            let settings = cx.global::<Settings>();
-            let container_style = settings.theme.terminal.modal_container;
-            element.with_style(container_style).boxed()
-        } else {
-            element.boxed()
-        }
+        let terminal_handle = self.terminal.clone().downgrade();
+        TerminalEl::new(cx.handle(), terminal_handle, self.modal)
+            .contained()
+            .boxed()
     }
 
-    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
-        cx.emit(Event::Activate);
+    fn on_focus(&mut self, _cx: &mut ViewContext<Self>) {
         self.has_new_content = false;
     }
+}
 
-    fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
-        let mut context = Self::default_keymap_context();
-        if self.modal {
-            context.set.insert("ModalTerminal".into());
-        }
-        context
+impl View for ErrorView {
+    fn ui_name() -> &'static str {
+        "Terminal Error"
+    }
+
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
+        let settings = cx.global::<Settings>();
+        let style = TerminalLayoutData::make_text_style(cx.font_cache(), settings);
+
+        Label::new(
+            format!(
+                "Failed to open the terminal. Info: \n{}",
+                self.error.to_string()
+            ),
+            style,
+        )
+        .aligned()
+        .contained()
+        .boxed()
     }
 }
 
@@ -316,9 +339,11 @@ impl Item for TerminalView {
         tab_theme: &theme::Tab,
         cx: &gpui::AppContext,
     ) -> ElementBox {
-        let title = match self.connection.0.as_ref() {
-            Ok(handle) => handle.read(cx).title.clone(),
-            Err(_) => "Terminal".to_string(),
+        let title = match &self.content {
+            TerminalContent::Connected(connected) => {
+                connected.read(cx).terminal.read(cx).title.clone()
+            }
+            TerminalContent::Error(_) => "Terminal".to_string(),
         };
 
         Flex::row()
@@ -335,13 +360,17 @@ impl Item for TerminalView {
         //From what I can tell, there's no  way to tell the current working
         //Directory of the terminal from outside the shell. There might be
         //solutions to this, but they are non-trivial and require more IPC
-
-        let wd = match self.connection.0.as_ref() {
-            Ok(term_handle) => term_handle.read(cx).associated_directory.clone(),
-            Err(e) => e.directory.clone(),
-        };
-
-        TerminalView::new(wd, false, cx)
+        if let TerminalContent::Connected(connected) = &self.content {
+            let associated_directory = connected
+                .read(cx)
+                .terminal
+                .read(cx)
+                .associated_directory
+                .clone();
+            Some(TerminalView::new(associated_directory, false, cx))
+        } else {
+            None
+        }
     }
 
     fn project_path(&self, _cx: &gpui::AppContext) -> Option<ProjectPath> {
@@ -387,12 +416,20 @@ impl Item for TerminalView {
         gpui::Task::ready(Ok(()))
     }
 
-    fn is_dirty(&self, _: &gpui::AppContext) -> bool {
-        self.has_new_content
+    fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
+        if let TerminalContent::Connected(connected) = &self.content {
+            connected.read(cx).has_new_content
+        } else {
+            false
+        }
     }
 
-    fn has_conflict(&self, _: &AppContext) -> bool {
-        self.has_bell
+    fn has_conflict(&self, cx: &AppContext) -> bool {
+        if let TerminalContent::Connected(connected) = &self.content {
+            connected.read(cx).has_bell
+        } else {
+            false
+        }
     }
 
     fn should_update_tab_on_event(event: &Self::Event) -> bool {
@@ -409,7 +446,7 @@ impl Item for TerminalView {
 }
 
 ///Get's the working directory for the given workspace, respecting the user's settings.
-fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
+fn get_working_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
     let wd_setting = cx
         .global::<Settings>()
         .terminal_overrides

crates/terminal/src/terminal_element.rs 🔗

@@ -32,7 +32,7 @@ use util::ResultExt;
 use std::{cmp::min, ops::Range};
 use std::{fmt::Debug, ops::Sub};
 
-use crate::{color_translation::convert_color, connection::Terminal, TerminalView};
+use crate::{color_translation::convert_color, connection::Terminal, ConnectedView};
 
 use self::terminal_layout_context::TerminalLayoutData;
 
@@ -44,8 +44,8 @@ const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
 ///The GPUI element that paints the terminal.
 ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
 pub struct TerminalEl {
-    connection: WeakModelHandle<Terminal>,
-    view: WeakViewHandle<TerminalView>,
+    terminal: WeakModelHandle<Terminal>,
+    view: WeakViewHandle<ConnectedView>,
     modal: bool,
 }
 
@@ -227,13 +227,13 @@ pub struct LayoutState {
 
 impl TerminalEl {
     pub fn new(
-        view: WeakViewHandle<TerminalView>,
-        connection: WeakModelHandle<Terminal>,
+        view: WeakViewHandle<ConnectedView>,
+        terminal: WeakModelHandle<Terminal>,
         modal: bool,
     ) -> TerminalEl {
         TerminalEl {
             view,
-            connection,
+            terminal,
             modal,
         }
     }
@@ -246,9 +246,9 @@ impl TerminalEl {
         cur_size: TerminalDimensions,
         cx: &mut PaintContext,
     ) {
-        let mouse_down_connection = self.connection.clone();
-        let click_connection = self.connection.clone();
-        let drag_connection = self.connection.clone();
+        let mouse_down_connection = self.terminal.clone();
+        let click_connection = self.terminal.clone();
+        let drag_connection = self.terminal.clone();
         cx.scene.push_mouse_region(
             MouseRegion::new(view_id, None, visible_bounds)
                 .on_down(
@@ -330,7 +330,7 @@ impl Element for TerminalEl {
         let layout =
             TerminalLayoutData::new(cx.global::<Settings>(), &cx.font_cache(), constraint.max);
 
-        let terminal = self.connection.upgrade(cx).unwrap().read(cx);
+        let terminal = self.terminal.upgrade(cx).unwrap().read(cx);
 
         let (cursor, cells, rects, highlights) =
             terminal.render_lock(Some(layout.size.clone()), |content, cursor_text| {
@@ -476,7 +476,7 @@ impl Element for TerminalEl {
                     let vertical_scroll =
                         (delta.y() / layout.size.line_height) * ALACRITTY_SCROLL_MULTIPLIER;
 
-                    self.connection.upgrade(cx.app).map(|terminal| {
+                    self.terminal.upgrade(cx.app).map(|terminal| {
                         terminal
                             .read(cx.app)
                             .scroll(Scroll::Delta(vertical_scroll.round() as i32));
@@ -493,7 +493,7 @@ impl Element for TerminalEl {
                     view.update(cx.app, |view, cx| view.clear_bel(cx))
                 }
 
-                self.connection
+                self.terminal
                     .upgrade(cx.app)
                     .map(|model_handle| model_handle.read(cx.app))
                     .map(|term| term.try_keystroke(keystroke))

crates/terminal/src/tests/terminal_test_context.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{geometry::vector::vec2f, AppContext, ModelHandle, ReadModelWith, Test
 use itertools::Itertools;
 
 use crate::{
-    connection::{DisconnectedPTY, Terminal},
+    connection::{Terminal, TerminalBuilder},
     terminal_element::TerminalDimensions,
     DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, DEBUG_TERMINAL_WIDTH,
 };
@@ -25,9 +25,9 @@ impl<'a> TerminalTestContext<'a> {
         );
 
         let connection = cx.add_model(|cx| {
-            DisconnectedPTY::new(None, None, None, size_info)
+            TerminalBuilder::new(None, None, None, size_info)
                 .unwrap()
-                .connect(cx)
+                .subscribe(cx)
         });
 
         TerminalTestContext { cx, connection }

crates/workspace/src/workspace.rs 🔗

@@ -1223,8 +1223,10 @@ impl Workspace {
         }
     }
 
-    pub fn modal(&self) -> Option<&AnyViewHandle> {
-        self.modal.as_ref()
+    pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
+        self.modal
+            .as_ref()
+            .and_then(|modal| modal.clone().downcast::<V>())
     }
 
     pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {