Merge pull request #1555 from zed-industries/terminal-renaming

Mikayla Maki created

Renamed all the terminal files

Change summary

crates/terminal/src/connected_view.rs          | 449 -----------
crates/terminal/src/modal.rs                   |  10 
crates/terminal/src/terminal.rs                |   6 
crates/terminal/src/terminal_container_view.rs | 513 ++++++++++++
crates/terminal/src/terminal_element.rs        |  38 
crates/terminal/src/terminal_view.rs           | 794 +++++++++----------
6 files changed, 906 insertions(+), 904 deletions(-)

Detailed changes

crates/terminal/src/connected_view.rs 🔗

@@ -1,449 +0,0 @@
-use std::time::Duration;
-
-use alacritty_terminal::term::TermMode;
-use context_menu::{ContextMenu, ContextMenuItem};
-use gpui::{
-    actions,
-    elements::{ChildView, ParentElement, Stack},
-    geometry::vector::Vector2F,
-    impl_internal_actions,
-    keymap::Keystroke,
-    AnyViewHandle, AppContext, Element, ElementBox, ModelHandle, MutableAppContext, View,
-    ViewContext, ViewHandle,
-};
-use settings::{Settings, TerminalBlink};
-use smol::Timer;
-use workspace::pane;
-
-use crate::{connected_el::TerminalEl, Event, Terminal};
-
-const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
-
-///Event to transmit the scroll from the element to the view
-#[derive(Clone, Debug, PartialEq)]
-pub struct ScrollTerminal(pub i32);
-
-#[derive(Clone, PartialEq)]
-pub struct DeployContextMenu {
-    pub position: Vector2F,
-}
-
-actions!(
-    terminal,
-    [
-        Up,
-        Down,
-        CtrlC,
-        Escape,
-        Enter,
-        Clear,
-        Copy,
-        Paste,
-        ShowCharacterPalette,
-    ]
-);
-impl_internal_actions!(project_panel, [DeployContextMenu]);
-
-pub fn init(cx: &mut MutableAppContext) {
-    //Global binding overrrides
-    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 views
-    cx.add_action(ConnectedView::deploy_context_menu);
-    cx.add_action(ConnectedView::copy);
-    cx.add_action(ConnectedView::paste);
-    cx.add_action(ConnectedView::clear);
-    cx.add_action(ConnectedView::show_character_palette);
-}
-
-///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,
-    // Only for styling purposes. Doesn't effect behavior
-    modal: bool,
-    context_menu: ViewHandle<ContextMenu>,
-    blink_state: bool,
-    blinking_on: bool,
-    blinking_paused: bool,
-    blink_epoch: usize,
-}
-
-impl ConnectedView {
-    pub fn from_terminal(
-        terminal: ModelHandle<Terminal>,
-        modal: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
-        cx.subscribe(&terminal, |this, _, event, cx| match event {
-            Event::Wakeup => {
-                if !cx.is_self_focused() {
-                    this.has_new_content = true;
-                    cx.notify();
-                    cx.emit(Event::Wakeup);
-                }
-            }
-            Event::Bell => {
-                this.has_bell = true;
-                cx.emit(Event::Wakeup);
-            }
-            Event::BlinkChanged => this.blinking_on = !this.blinking_on,
-            _ => cx.emit(*event),
-        })
-        .detach();
-
-        Self {
-            terminal,
-            has_new_content: true,
-            has_bell: false,
-            modal,
-            context_menu: cx.add_view(ContextMenu::new),
-            blink_state: true,
-            blinking_on: false,
-            blinking_paused: false,
-            blink_epoch: 0,
-        }
-    }
-
-    pub fn handle(&self) -> ModelHandle<Terminal> {
-        self.terminal.clone()
-    }
-
-    pub fn has_new_content(&self) -> bool {
-        self.has_new_content
-    }
-
-    pub fn has_bell(&self) -> bool {
-        self.has_bell
-    }
-
-    pub fn clear_bel(&mut self, cx: &mut ViewContext<ConnectedView>) {
-        self.has_bell = false;
-        cx.emit(Event::Wakeup);
-    }
-
-    pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
-        let menu_entries = vec![
-            ContextMenuItem::item("Clear Buffer", Clear),
-            ContextMenuItem::item("Close Terminal", pane::CloseActiveItem),
-        ];
-
-        self.context_menu
-            .update(cx, |menu, cx| menu.show(action.position, menu_entries, cx));
-
-        cx.notify();
-    }
-
-    fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
-        if !self
-            .terminal
-            .read(cx)
-            .last_mode
-            .contains(TermMode::ALT_SCREEN)
-        {
-            cx.show_character_palette();
-        } else {
-            self.terminal.update(cx, |term, _| {
-                term.try_keystroke(&Keystroke::parse("ctrl-cmd-space").unwrap())
-            });
-        }
-    }
-
-    fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
-        self.terminal.update(cx, |term, _| term.clear());
-        cx.notify();
-    }
-
-    pub fn should_show_cursor(
-        &self,
-        focused: bool,
-        cx: &mut gpui::RenderContext<'_, Self>,
-    ) -> bool {
-        //Don't blink the cursor when not focused, blinking is disabled, or paused
-        if !focused
-            || !self.blinking_on
-            || self.blinking_paused
-            || self
-                .terminal
-                .read(cx)
-                .last_mode
-                .contains(TermMode::ALT_SCREEN)
-        {
-            return true;
-        }
-
-        let setting = {
-            let settings = cx.global::<Settings>();
-            settings
-                .terminal_overrides
-                .blinking
-                .clone()
-                .unwrap_or(TerminalBlink::TerminalControlled)
-        };
-
-        match setting {
-            //If the user requested to never blink, don't blink it.
-            TerminalBlink::Off => true,
-            //If the terminal is controlling it, check terminal mode
-            TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state,
-        }
-    }
-
-    fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
-        if epoch == self.blink_epoch && !self.blinking_paused {
-            self.blink_state = !self.blink_state;
-            cx.notify();
-
-            let epoch = self.next_blink_epoch();
-            cx.spawn(|this, mut cx| {
-                let this = this.downgrade();
-                async move {
-                    Timer::after(CURSOR_BLINK_INTERVAL).await;
-                    if let Some(this) = this.upgrade(&cx) {
-                        this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
-                    }
-                }
-            })
-            .detach();
-        }
-    }
-
-    pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
-        self.blink_state = true;
-        cx.notify();
-
-        let epoch = self.next_blink_epoch();
-        cx.spawn(|this, mut cx| {
-            let this = this.downgrade();
-            async move {
-                Timer::after(CURSOR_BLINK_INTERVAL).await;
-                if let Some(this) = this.upgrade(&cx) {
-                    this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
-                }
-            }
-        })
-        .detach();
-    }
-
-    fn next_blink_epoch(&mut self) -> usize {
-        self.blink_epoch += 1;
-        self.blink_epoch
-    }
-
-    fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
-        if epoch == self.blink_epoch {
-            self.blinking_paused = false;
-            self.blink_cursors(epoch, cx);
-        }
-    }
-
-    ///Attempt to paste the clipboard into the terminal
-    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
-        self.terminal.update(cx, |term, _| term.copy())
-    }
-
-    ///Attempt to paste the clipboard into the terminal
-    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
-        if let Some(item) = cx.read_from_clipboard() {
-            self.terminal
-                .update(cx, |terminal, _cx| terminal.paste(item.text()));
-        }
-    }
-
-    ///Synthesize the keyboard event corresponding to 'up'
-    fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
-        self.clear_bel(cx);
-        self.terminal.update(cx, |term, _| {
-            term.try_keystroke(&Keystroke::parse("up").unwrap())
-        });
-    }
-
-    ///Synthesize the keyboard event corresponding to 'down'
-    fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
-        self.clear_bel(cx);
-        self.terminal.update(cx, |term, _| {
-            term.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.clear_bel(cx);
-        self.terminal.update(cx, |term, _| {
-            term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap())
-        });
-    }
-
-    ///Synthesize the keyboard event corresponding to 'escape'
-    fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
-        self.clear_bel(cx);
-        self.terminal.update(cx, |term, _| {
-            term.try_keystroke(&Keystroke::parse("escape").unwrap())
-        });
-    }
-
-    ///Synthesize the keyboard event corresponding to 'enter'
-    fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
-        self.clear_bel(cx);
-        self.terminal.update(cx, |term, _| {
-            term.try_keystroke(&Keystroke::parse("enter").unwrap())
-        });
-    }
-}
-
-impl View for ConnectedView {
-    fn ui_name() -> &'static str {
-        "Terminal"
-    }
-
-    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
-        let terminal_handle = self.terminal.clone().downgrade();
-
-        let self_id = cx.view_id();
-        let focused = cx
-            .focused_view_id(cx.window_id())
-            .filter(|view_id| *view_id == self_id)
-            .is_some();
-
-        Stack::new()
-            .with_child(
-                TerminalEl::new(
-                    cx.handle(),
-                    terminal_handle,
-                    self.modal,
-                    focused,
-                    self.should_show_cursor(focused, cx),
-                )
-                .contained()
-                .boxed(),
-            )
-            .with_child(ChildView::new(&self.context_menu).boxed())
-            .boxed()
-    }
-
-    fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        self.has_new_content = false;
-        self.terminal.read(cx).focus_in();
-        self.blink_cursors(self.blink_epoch, cx);
-        cx.notify();
-    }
-
-    fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        self.terminal.read(cx).focus_out();
-        cx.notify();
-    }
-
-    //IME stuff
-    fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
-        if self
-            .terminal
-            .read(cx)
-            .last_mode
-            .contains(TermMode::ALT_SCREEN)
-        {
-            None
-        } else {
-            Some(0..0)
-        }
-    }
-
-    fn replace_text_in_range(
-        &mut self,
-        _: Option<std::ops::Range<usize>>,
-        text: &str,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.terminal.update(cx, |terminal, _| {
-            terminal.input(text.into());
-        });
-    }
-
-    fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context {
-        let mut context = Self::default_keymap_context();
-        if self.modal {
-            context.set.insert("ModalTerminal".into());
-        }
-        let mode = self.terminal.read(cx).last_mode;
-        context.map.insert(
-            "screen".to_string(),
-            (if mode.contains(TermMode::ALT_SCREEN) {
-                "alt"
-            } else {
-                "normal"
-            })
-            .to_string(),
-        );
-
-        if mode.contains(TermMode::APP_CURSOR) {
-            context.set.insert("DECCKM".to_string());
-        }
-        if mode.contains(TermMode::APP_KEYPAD) {
-            context.set.insert("DECPAM".to_string());
-        }
-        //Note the ! here
-        if !mode.contains(TermMode::APP_KEYPAD) {
-            context.set.insert("DECPNM".to_string());
-        }
-        if mode.contains(TermMode::SHOW_CURSOR) {
-            context.set.insert("DECTCEM".to_string());
-        }
-        if mode.contains(TermMode::LINE_WRAP) {
-            context.set.insert("DECAWM".to_string());
-        }
-        if mode.contains(TermMode::ORIGIN) {
-            context.set.insert("DECOM".to_string());
-        }
-        if mode.contains(TermMode::INSERT) {
-            context.set.insert("IRM".to_string());
-        }
-        //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
-        if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
-            context.set.insert("LNM".to_string());
-        }
-        if mode.contains(TermMode::FOCUS_IN_OUT) {
-            context.set.insert("report_focus".to_string());
-        }
-        if mode.contains(TermMode::ALTERNATE_SCROLL) {
-            context.set.insert("alternate_scroll".to_string());
-        }
-        if mode.contains(TermMode::BRACKETED_PASTE) {
-            context.set.insert("bracketed_paste".to_string());
-        }
-        if mode.intersects(TermMode::MOUSE_MODE) {
-            context.set.insert("any_mouse_reporting".to_string());
-        }
-        {
-            let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
-                "click"
-            } else if mode.contains(TermMode::MOUSE_DRAG) {
-                "drag"
-            } else if mode.contains(TermMode::MOUSE_MOTION) {
-                "motion"
-            } else {
-                "off"
-            };
-            context
-                .map
-                .insert("mouse_reporting".to_string(), mouse_reporting.to_string());
-        }
-        {
-            let format = if mode.contains(TermMode::SGR_MOUSE) {
-                "sgr"
-            } else if mode.contains(TermMode::UTF8_MOUSE) {
-                "utf8"
-            } else {
-                "normal"
-            };
-            context
-                .map
-                .insert("mouse_format".to_string(), format.to_string());
-        }
-        context
-    }
-}

crates/terminal/src/modal.rs 🔗

@@ -3,7 +3,9 @@ use settings::{Settings, WorkingDirectory};
 use workspace::Workspace;
 
 use crate::{
-    terminal_view::{get_working_directory, DeployModal, TerminalContent, TerminalView},
+    terminal_container_view::{
+        get_working_directory, DeployModal, TerminalContainer, TerminalContent,
+    },
     Event, Terminal,
 };
 
@@ -20,7 +22,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon
     if let Some(StoredTerminal(stored_terminal)) = possible_terminal {
         workspace.toggle_modal(cx, |_, 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.add_view(|cx| TerminalContainer::from_terminal(stored_terminal.clone(), true, cx))
         });
         // Toggle Modal will dismiss the terminal modal if it is currently shown, so we must
         // store the terminal back in the global
@@ -38,7 +40,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon
 
             let working_directory = get_working_directory(workspace, cx, wd_strategy);
 
-            let this = cx.add_view(|cx| TerminalView::new(working_directory, true, cx));
+            let this = cx.add_view(|cx| TerminalContainer::new(working_directory, true, cx));
 
             if let TerminalContent::Connected(connected) = &this.read(cx).content {
                 let terminal_handle = connected.read(cx).handle();
@@ -73,7 +75,7 @@ pub fn on_event(
     // Dismiss the modal if the terminal quit
     if let Event::CloseTerminal = event {
         cx.set_global::<Option<StoredTerminal>>(None);
-        if workspace.modal::<TerminalView>().is_some() {
+        if workspace.modal::<TerminalContainer>().is_some() {
             workspace.dismiss_modal(cx)
         }
     }

crates/terminal/src/terminal.rs 🔗

@@ -1,7 +1,7 @@
-pub mod connected_el;
-pub mod connected_view;
 pub mod mappings;
 pub mod modal;
+pub mod terminal_container_view;
+pub mod terminal_element;
 pub mod terminal_view;
 
 use alacritty_terminal::{
@@ -52,7 +52,7 @@ pub fn init(cx: &mut MutableAppContext) {
     }
 
     terminal_view::init(cx);
-    connected_view::init(cx);
+    terminal_container_view::init(cx);
 }
 
 ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable

crates/terminal/src/terminal_container_view.rs 🔗

@@ -0,0 +1,513 @@
+use crate::terminal_view::TerminalView;
+use crate::{Event, Terminal, TerminalBuilder, TerminalError};
+
+use dirs::home_dir;
+use gpui::{
+    actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, View,
+    ViewContext, ViewHandle,
+};
+use workspace::{Item, Workspace};
+
+use crate::TerminalSize;
+use project::{LocalWorktree, Project, ProjectPath};
+use settings::{AlternateScroll, Settings, WorkingDirectory};
+use smallvec::SmallVec;
+use std::path::{Path, PathBuf};
+
+use crate::terminal_element::TerminalElement;
+
+actions!(terminal, [DeployModal]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(TerminalContainer::deploy);
+}
+
+//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
+
+pub enum TerminalContent {
+    Connected(ViewHandle<TerminalView>),
+    Error(ViewHandle<ErrorView>),
+}
+
+impl TerminalContent {
+    fn handle(&self) -> AnyViewHandle {
+        match self {
+            Self::Connected(handle) => handle.into(),
+            Self::Error(handle) => handle.into(),
+        }
+    }
+}
+
+pub struct TerminalContainer {
+    modal: bool,
+    pub content: TerminalContent,
+    associated_directory: Option<PathBuf>,
+}
+
+pub struct ErrorView {
+    error: TerminalError,
+}
+
+impl Entity for TerminalContainer {
+    type Event = Event;
+}
+
+impl Entity for ErrorView {
+    type Event = Event;
+}
+
+impl TerminalContainer {
+    ///Create a new Terminal in the current working directory or the user's home directory
+    pub fn deploy(
+        workspace: &mut Workspace,
+        _: &workspace::NewTerminal,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let strategy = cx
+            .global::<Settings>()
+            .terminal_overrides
+            .working_directory
+            .clone()
+            .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
+
+        let working_directory = get_working_directory(workspace, cx, strategy);
+        let view = cx.add_view(|cx| TerminalContainer::new(working_directory, false, cx));
+        workspace.add_item(Box::new(view), cx);
+    }
+
+    ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices    
+    pub fn new(
+        working_directory: Option<PathBuf>,
+        modal: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        //The exact size here doesn't matter, the terminal will be resized on the first layout
+        let size_info = TerminalSize::default();
+
+        let settings = cx.global::<Settings>();
+        let shell = settings.terminal_overrides.shell.clone();
+        let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
+
+        //TODO: move this pattern to settings
+        let scroll = settings
+            .terminal_overrides
+            .alternate_scroll
+            .as_ref()
+            .unwrap_or(
+                settings
+                    .terminal_defaults
+                    .alternate_scroll
+                    .as_ref()
+                    .unwrap_or_else(|| &AlternateScroll::On),
+            );
+
+        let content = match TerminalBuilder::new(
+            working_directory.clone(),
+            shell,
+            envs,
+            size_info,
+            settings.terminal_overrides.blinking.clone(),
+            scroll,
+        ) {
+            Ok(terminal) => {
+                let terminal = cx.add_model(|cx| terminal.subscribe(cx));
+                let view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
+                cx.subscribe(&view, |_this, _content, event, cx| cx.emit(*event))
+                    .detach();
+                TerminalContent::Connected(view)
+            }
+            Err(error) => {
+                let view = cx.add_view(|_| ErrorView {
+                    error: error.downcast::<TerminalError>().unwrap(),
+                });
+                TerminalContent::Error(view)
+            }
+        };
+        cx.focus(content.handle());
+
+        TerminalContainer {
+            modal,
+            content,
+            associated_directory: working_directory,
+        }
+    }
+
+    pub fn from_terminal(
+        terminal: ModelHandle<Terminal>,
+        modal: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let connected_view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
+        TerminalContainer {
+            modal,
+            content: TerminalContent::Connected(connected_view),
+            associated_directory: None,
+        }
+    }
+}
+
+impl View for TerminalContainer {
+    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 {
+            child_view.boxed()
+        }
+    }
+
+    fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        if cx.is_self_focused() {
+            cx.focus(self.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
+    }
+}
+
+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 = TerminalElement::make_text_style(cx.font_cache(), settings);
+
+        //TODO:
+        //We want markdown style highlighting so we can format the program and working directory with ``
+        //We want a max-width of 75% with word-wrap
+        //We want to be able to select the text
+        //Want to be able to scroll if the error message is massive somehow (resiliency)
+
+        let program_text = {
+            match self.error.shell_to_string() {
+                Some(shell_txt) => format!("Shell Program: `{}`", shell_txt),
+                None => "No program specified".to_string(),
+            }
+        };
+
+        let directory_text = {
+            match self.error.directory.as_ref() {
+                Some(path) => format!("Working directory: `{}`", path.to_string_lossy()),
+                None => "No working directory specified".to_string(),
+            }
+        };
+
+        let error_text = self.error.source.to_string();
+
+        Flex::column()
+            .with_child(
+                Text::new("Failed to open the terminal.".to_string(), style.clone())
+                    .contained()
+                    .boxed(),
+            )
+            .with_child(Text::new(program_text, style.clone()).contained().boxed())
+            .with_child(Text::new(directory_text, style.clone()).contained().boxed())
+            .with_child(Text::new(error_text, style).contained().boxed())
+            .aligned()
+            .boxed()
+    }
+}
+
+impl Item for TerminalContainer {
+    fn tab_content(
+        &self,
+        _detail: Option<usize>,
+        tab_theme: &theme::Tab,
+        cx: &gpui::AppContext,
+    ) -> ElementBox {
+        let title = match &self.content {
+            TerminalContent::Connected(connected) => {
+                connected.read(cx).handle().read(cx).title.to_string()
+            }
+            TerminalContent::Error(_) => "Terminal".to_string(),
+        };
+
+        Flex::row()
+            .with_child(
+                Label::new(title, tab_theme.label.clone())
+                    .aligned()
+                    .contained()
+                    .boxed(),
+            )
+            .boxed()
+    }
+
+    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
+        //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
+        Some(TerminalContainer::new(
+            self.associated_directory.clone(),
+            false,
+            cx,
+        ))
+    }
+
+    fn project_path(&self, _cx: &gpui::AppContext) -> Option<ProjectPath> {
+        None
+    }
+
+    fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
+        SmallVec::new()
+    }
+
+    fn is_singleton(&self, _cx: &gpui::AppContext) -> bool {
+        false
+    }
+
+    fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
+
+    fn can_save(&self, _cx: &gpui::AppContext) -> bool {
+        false
+    }
+
+    fn save(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        unreachable!("save should not have been called");
+    }
+
+    fn save_as(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _abs_path: std::path::PathBuf,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        unreachable!("save_as should not have been called");
+    }
+
+    fn reload(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        gpui::Task::ready(Ok(()))
+    }
+
+    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, 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 {
+        matches!(event, &Event::TitleChanged | &Event::Wakeup)
+    }
+
+    fn should_close_item_on_event(event: &Self::Event) -> bool {
+        matches!(event, &Event::CloseTerminal)
+    }
+}
+
+///Get's the working directory for the given workspace, respecting the user's settings.
+pub fn get_working_directory(
+    workspace: &Workspace,
+    cx: &AppContext,
+    strategy: WorkingDirectory,
+) -> Option<PathBuf> {
+    let res = match strategy {
+        WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx)
+            .or_else(|| first_project_directory(workspace, cx)),
+        WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
+        WorkingDirectory::AlwaysHome => None,
+        WorkingDirectory::Always { directory } => {
+            shellexpand::full(&directory) //TODO handle this better
+                .ok()
+                .map(|dir| Path::new(&dir.to_string()).to_path_buf())
+                .filter(|dir| dir.is_dir())
+        }
+    };
+    res.or_else(home_dir)
+}
+
+///Get's the first project's home directory, or the home directory
+fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
+    workspace
+        .worktrees(cx)
+        .next()
+        .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
+        .and_then(get_path_from_wt)
+}
+
+///Gets the intuitively correct working directory from the given workspace
+///If there is an active entry for this project, returns that entry's worktree root.
+///If there's no active entry but there is a worktree, returns that worktrees root.
+///If either of these roots are files, or if there are any other query failures,
+///  returns the user's home directory
+fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
+    let project = workspace.project().read(cx);
+
+    project
+        .active_entry()
+        .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
+        .or_else(|| workspace.worktrees(cx).next())
+        .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
+        .and_then(get_path_from_wt)
+}
+
+fn get_path_from_wt(wt: &LocalWorktree) -> Option<PathBuf> {
+    wt.root_entry()
+        .filter(|re| re.is_dir())
+        .map(|_| wt.abs_path().to_path_buf())
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+    use gpui::TestAppContext;
+
+    use std::path::Path;
+
+    use crate::tests::terminal_test_context::TerminalTestContext;
+
+    ///Working directory calculation tests
+
+    ///No Worktrees in project -> home_dir()
+    #[gpui::test]
+    async fn no_worktree(cx: &mut TestAppContext) {
+        //Setup variables
+        let mut cx = TerminalTestContext::new(cx);
+        let (project, workspace) = cx.blank_workspace().await;
+        //Test
+        cx.cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            let active_entry = project.read(cx).active_entry();
+
+            //Make sure enviroment is as expeted
+            assert!(active_entry.is_none());
+            assert!(workspace.worktrees(cx).next().is_none());
+
+            let res = current_project_directory(workspace, cx);
+            assert_eq!(res, None);
+            let res = first_project_directory(workspace, cx);
+            assert_eq!(res, None);
+        });
+    }
+
+    ///No active entry, but a worktree, worktree is a file -> home_dir()
+    #[gpui::test]
+    async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
+        //Setup variables
+
+        let mut cx = TerminalTestContext::new(cx);
+        let (project, workspace) = cx.blank_workspace().await;
+        cx.create_file_wt(project.clone(), "/root.txt").await;
+
+        cx.cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            let active_entry = project.read(cx).active_entry();
+
+            //Make sure enviroment is as expeted
+            assert!(active_entry.is_none());
+            assert!(workspace.worktrees(cx).next().is_some());
+
+            let res = current_project_directory(workspace, cx);
+            assert_eq!(res, None);
+            let res = first_project_directory(workspace, cx);
+            assert_eq!(res, None);
+        });
+    }
+
+    //No active entry, but a worktree, worktree is a folder -> worktree_folder
+    #[gpui::test]
+    async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
+        //Setup variables
+        let mut cx = TerminalTestContext::new(cx);
+        let (project, workspace) = cx.blank_workspace().await;
+        let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root/").await;
+
+        //Test
+        cx.cx.update(|cx| {
+            let workspace = workspace.read(cx);
+            let active_entry = project.read(cx).active_entry();
+
+            assert!(active_entry.is_none());
+            assert!(workspace.worktrees(cx).next().is_some());
+
+            let res = current_project_directory(workspace, cx);
+            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
+            let res = first_project_directory(workspace, cx);
+            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
+        });
+    }
+
+    //Active entry with a work tree, worktree is a file -> home_dir()
+    #[gpui::test]
+    async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
+        //Setup variables
+        let mut cx = TerminalTestContext::new(cx);
+        let (project, workspace) = cx.blank_workspace().await;
+        let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await;
+        let (wt2, entry2) = cx.create_file_wt(project.clone(), "/root2.txt").await;
+        cx.insert_active_entry_for(wt2, entry2, project.clone());
+
+        //Test
+        cx.cx.update(|cx| {
+            let workspace = workspace.read(cx);
+            let active_entry = project.read(cx).active_entry();
+
+            assert!(active_entry.is_some());
+
+            let res = current_project_directory(workspace, cx);
+            assert_eq!(res, None);
+            let res = first_project_directory(workspace, cx);
+            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
+        });
+    }
+
+    //Active entry, with a worktree, worktree is a folder -> worktree_folder
+    #[gpui::test]
+    async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
+        //Setup variables
+        let mut cx = TerminalTestContext::new(cx);
+        let (project, workspace) = cx.blank_workspace().await;
+        let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await;
+        let (wt2, entry2) = cx.create_folder_wt(project.clone(), "/root2/").await;
+        cx.insert_active_entry_for(wt2, entry2, project.clone());
+
+        //Test
+        cx.cx.update(|cx| {
+            let workspace = workspace.read(cx);
+            let active_entry = project.read(cx).active_entry();
+
+            assert!(active_entry.is_some());
+
+            let res = current_project_directory(workspace, cx);
+            assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
+            let res = first_project_directory(workspace, cx);
+            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
+        });
+    }
+}

crates/terminal/src/connected_el.rs → crates/terminal/src/terminal_element.rs 🔗

@@ -35,8 +35,8 @@ use std::{
 };
 
 use crate::{
-    connected_view::{ConnectedView, DeployContextMenu},
     mappings::colors::convert_color,
+    terminal_view::{DeployContextMenu, TerminalView},
     Terminal, TerminalSize,
 };
 
@@ -193,23 +193,23 @@ impl RelativeHighlightedRange {
 
 ///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 {
+pub struct TerminalElement {
     terminal: WeakModelHandle<Terminal>,
-    view: WeakViewHandle<ConnectedView>,
+    view: WeakViewHandle<TerminalView>,
     modal: bool,
     focused: bool,
     cursor_visible: bool,
 }
 
-impl TerminalEl {
+impl TerminalElement {
     pub fn new(
-        view: WeakViewHandle<ConnectedView>,
+        view: WeakViewHandle<TerminalView>,
         terminal: WeakModelHandle<Terminal>,
         modal: bool,
         focused: bool,
         cursor_visible: bool,
-    ) -> TerminalEl {
-        TerminalEl {
+    ) -> TerminalElement {
+        TerminalElement {
             view,
             terminal,
             modal,
@@ -302,7 +302,7 @@ impl TerminalEl {
                 {
                     let cell_text = &cell.c.to_string();
                     if cell_text != " " {
-                        let cell_style = TerminalEl::cell_style(
+                        let cell_style = TerminalElement::cell_style(
                             &cell,
                             fg,
                             terminal_theme,
@@ -444,7 +444,7 @@ impl TerminalEl {
             // Start selections
             .on_down(
                 MouseButton::Left,
-                TerminalEl::generic_button_handler(
+                TerminalElement::generic_button_handler(
                     connection,
                     origin,
                     move |terminal, origin, e, _cx| {
@@ -466,7 +466,7 @@ impl TerminalEl {
             // Copy on up behavior
             .on_up(
                 MouseButton::Left,
-                TerminalEl::generic_button_handler(
+                TerminalElement::generic_button_handler(
                     connection,
                     origin,
                     move |terminal, origin, e, _cx| {
@@ -477,7 +477,7 @@ impl TerminalEl {
             // Handle click based selections
             .on_click(
                 MouseButton::Left,
-                TerminalEl::generic_button_handler(
+                TerminalElement::generic_button_handler(
                     connection,
                     origin,
                     move |terminal, origin, e, _cx| {
@@ -507,7 +507,7 @@ impl TerminalEl {
             region = region
                 .on_down(
                     MouseButton::Right,
-                    TerminalEl::generic_button_handler(
+                    TerminalElement::generic_button_handler(
                         connection,
                         origin,
                         move |terminal, origin, e, _cx| {
@@ -517,7 +517,7 @@ impl TerminalEl {
                 )
                 .on_down(
                     MouseButton::Middle,
-                    TerminalEl::generic_button_handler(
+                    TerminalElement::generic_button_handler(
                         connection,
                         origin,
                         move |terminal, origin, e, _cx| {
@@ -527,7 +527,7 @@ impl TerminalEl {
                 )
                 .on_up(
                     MouseButton::Right,
-                    TerminalEl::generic_button_handler(
+                    TerminalElement::generic_button_handler(
                         connection,
                         origin,
                         move |terminal, origin, e, _cx| {
@@ -537,7 +537,7 @@ impl TerminalEl {
                 )
                 .on_up(
                     MouseButton::Middle,
-                    TerminalEl::generic_button_handler(
+                    TerminalElement::generic_button_handler(
                         connection,
                         origin,
                         move |terminal, origin, e, _cx| {
@@ -598,7 +598,7 @@ impl TerminalEl {
     }
 }
 
-impl Element for TerminalEl {
+impl Element for TerminalElement {
     type LayoutState = LayoutState;
     type PaintState = ();
 
@@ -612,7 +612,7 @@ impl Element for TerminalEl {
 
         //Setup layout information
         let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
-        let text_style = TerminalEl::make_text_style(font_cache, settings);
+        let text_style = TerminalElement::make_text_style(font_cache, settings);
         let selection_color = settings.theme.editor.selection.selection;
         let dimensions = {
             let line_height = font_cache.line_height(text_style.font_size);
@@ -660,7 +660,7 @@ impl Element for TerminalEl {
                 })
             });
 
-        let (cells, rects, highlights) = TerminalEl::layout_grid(
+        let (cells, rects, highlights) = TerminalElement::layout_grid(
             cells,
             &text_style,
             &terminal_theme,
@@ -699,7 +699,7 @@ impl Element for TerminalEl {
                 )
             };
 
-            TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map(
+            TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
                 move |(cursor_position, block_width)| {
                     let shape = match cursor.shape {
                         AlacCursorShape::Block if !self.focused => CursorShape::Hollow,

crates/terminal/src/terminal_view.rs 🔗

@@ -1,517 +1,453 @@
-use crate::connected_view::ConnectedView;
-use crate::{Event, Terminal, TerminalBuilder, TerminalError};
+use std::time::Duration;
 
-use dirs::home_dir;
+use alacritty_terminal::term::TermMode;
+use context_menu::{ContextMenu, ContextMenuItem};
 use gpui::{
-    actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, View,
+    actions,
+    elements::{ChildView, ParentElement, Stack},
+    geometry::vector::Vector2F,
+    impl_internal_actions,
+    keymap::Keystroke,
+    AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View,
     ViewContext, ViewHandle,
 };
-use workspace::{Item, Workspace};
+use settings::{Settings, TerminalBlink};
+use smol::Timer;
+use workspace::pane;
 
-use crate::TerminalSize;
-use project::{LocalWorktree, Project, ProjectPath};
-use settings::{AlternateScroll, Settings, WorkingDirectory};
-use smallvec::SmallVec;
-use std::path::{Path, PathBuf};
+use crate::{terminal_element::TerminalElement, Event, Terminal};
 
-use crate::connected_el::TerminalEl;
+const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 
-actions!(terminal, [DeployModal]);
+///Event to transmit the scroll from the element to the view
+#[derive(Clone, Debug, PartialEq)]
+pub struct ScrollTerminal(pub i32);
 
-pub fn init(cx: &mut MutableAppContext) {
-    cx.add_action(TerminalView::deploy);
+#[derive(Clone, PartialEq)]
+pub struct DeployContextMenu {
+    pub position: Vector2F,
 }
 
-//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
-
-pub enum TerminalContent {
-    Connected(ViewHandle<ConnectedView>),
-    Error(ViewHandle<ErrorView>),
-}
+actions!(
+    terminal,
+    [
+        Up,
+        Down,
+        CtrlC,
+        Escape,
+        Enter,
+        Clear,
+        Copy,
+        Paste,
+        ShowCharacterPalette,
+    ]
+);
+impl_internal_actions!(project_panel, [DeployContextMenu]);
 
-impl TerminalContent {
-    fn handle(&self) -> AnyViewHandle {
-        match self {
-            Self::Connected(handle) => handle.into(),
-            Self::Error(handle) => handle.into(),
-        }
-    }
+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);
+    //Useful terminal views
+    cx.add_action(TerminalView::deploy_context_menu);
+    cx.add_action(TerminalView::copy);
+    cx.add_action(TerminalView::paste);
+    cx.add_action(TerminalView::clear);
+    cx.add_action(TerminalView::show_character_palette);
 }
 
+///A terminal view, maintains the PTY's file handles and communicates with the terminal
 pub struct TerminalView {
+    terminal: ModelHandle<Terminal>,
+    has_new_content: bool,
+    //Currently using iTerm bell, show bell emoji in tab until input is received
+    has_bell: bool,
+    // Only for styling purposes. Doesn't effect behavior
     modal: bool,
-    pub content: TerminalContent,
-    associated_directory: Option<PathBuf>,
-}
-
-pub struct ErrorView {
-    error: TerminalError,
+    context_menu: ViewHandle<ContextMenu>,
+    blink_state: bool,
+    blinking_on: bool,
+    blinking_paused: bool,
+    blink_epoch: usize,
 }
 
 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 in the current working directory or the user's home directory
-    pub fn deploy(
-        workspace: &mut Workspace,
-        _: &workspace::NewTerminal,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        let strategy = cx
-            .global::<Settings>()
-            .terminal_overrides
-            .working_directory
-            .clone()
-            .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
-
-        let working_directory = get_working_directory(workspace, cx, strategy);
-        let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx));
-        workspace.add_item(Box::new(view), cx);
-    }
-
-    ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices    
-    pub fn new(
-        working_directory: Option<PathBuf>,
-        modal: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        //The exact size here doesn't matter, the terminal will be resized on the first layout
-        let size_info = TerminalSize::default();
-
-        let settings = cx.global::<Settings>();
-        let shell = settings.terminal_overrides.shell.clone();
-        let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
-
-        //TODO: move this pattern to settings
-        let scroll = settings
-            .terminal_overrides
-            .alternate_scroll
-            .as_ref()
-            .unwrap_or(
-                settings
-                    .terminal_defaults
-                    .alternate_scroll
-                    .as_ref()
-                    .unwrap_or_else(|| &AlternateScroll::On),
-            );
-
-        let content = match TerminalBuilder::new(
-            working_directory.clone(),
-            shell,
-            envs,
-            size_info,
-            settings.terminal_overrides.blinking.clone(),
-            scroll,
-        ) {
-            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))
-                    .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,
-            associated_directory: working_directory,
-        }
-    }
-
     pub 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 {
+        cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
+        cx.subscribe(&terminal, |this, _, event, cx| match event {
+            Event::Wakeup => {
+                if !cx.is_self_focused() {
+                    this.has_new_content = true;
+                    cx.notify();
+                    cx.emit(Event::Wakeup);
+                }
+            }
+            Event::Bell => {
+                this.has_bell = true;
+                cx.emit(Event::Wakeup);
+            }
+            Event::BlinkChanged => this.blinking_on = !this.blinking_on,
+            _ => cx.emit(*event),
+        })
+        .detach();
+
+        Self {
+            terminal,
+            has_new_content: true,
+            has_bell: false,
             modal,
-            content: TerminalContent::Connected(connected_view),
-            associated_directory: None,
+            context_menu: cx.add_view(ContextMenu::new),
+            blink_state: true,
+            blinking_on: false,
+            blinking_paused: false,
+            blink_epoch: 0,
         }
     }
-}
 
-impl View for TerminalView {
-    fn ui_name() -> &'static str {
-        "Terminal"
+    pub fn handle(&self) -> ModelHandle<Terminal> {
+        self.terminal.clone()
     }
 
-    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 {
-            child_view.boxed()
-        }
+    pub fn has_new_content(&self) -> bool {
+        self.has_new_content
     }
 
-    fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            cx.focus(self.content.handle());
-        }
+    pub fn has_bell(&self) -> bool {
+        self.has_bell
     }
 
-    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
+    pub fn clear_bel(&mut self, cx: &mut ViewContext<TerminalView>) {
+        self.has_bell = false;
+        cx.emit(Event::Wakeup);
     }
-}
 
-impl View for ErrorView {
-    fn ui_name() -> &'static str {
-        "Terminal Error"
-    }
+    pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
+        let menu_entries = vec![
+            ContextMenuItem::item("Clear Buffer", Clear),
+            ContextMenuItem::item("Close Terminal", pane::CloseActiveItem),
+        ];
 
-    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
-        let settings = cx.global::<Settings>();
-        let style = TerminalEl::make_text_style(cx.font_cache(), settings);
-
-        //TODO:
-        //We want markdown style highlighting so we can format the program and working directory with ``
-        //We want a max-width of 75% with word-wrap
-        //We want to be able to select the text
-        //Want to be able to scroll if the error message is massive somehow (resiliency)
-
-        let program_text = {
-            match self.error.shell_to_string() {
-                Some(shell_txt) => format!("Shell Program: `{}`", shell_txt),
-                None => "No program specified".to_string(),
-            }
-        };
+        self.context_menu
+            .update(cx, |menu, cx| menu.show(action.position, menu_entries, cx));
 
-        let directory_text = {
-            match self.error.directory.as_ref() {
-                Some(path) => format!("Working directory: `{}`", path.to_string_lossy()),
-                None => "No working directory specified".to_string(),
-            }
-        };
+        cx.notify();
+    }
 
-        let error_text = self.error.source.to_string();
+    fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
+        if !self
+            .terminal
+            .read(cx)
+            .last_mode
+            .contains(TermMode::ALT_SCREEN)
+        {
+            cx.show_character_palette();
+        } else {
+            self.terminal.update(cx, |term, _| {
+                term.try_keystroke(&Keystroke::parse("ctrl-cmd-space").unwrap())
+            });
+        }
+    }
 
-        Flex::column()
-            .with_child(
-                Text::new("Failed to open the terminal.".to_string(), style.clone())
-                    .contained()
-                    .boxed(),
-            )
-            .with_child(Text::new(program_text, style.clone()).contained().boxed())
-            .with_child(Text::new(directory_text, style.clone()).contained().boxed())
-            .with_child(Text::new(error_text, style).contained().boxed())
-            .aligned()
-            .boxed()
+    fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
+        self.terminal.update(cx, |term, _| term.clear());
+        cx.notify();
     }
-}
 
-impl Item for TerminalView {
-    fn tab_content(
+    pub fn should_show_cursor(
         &self,
-        _detail: Option<usize>,
-        tab_theme: &theme::Tab,
-        cx: &gpui::AppContext,
-    ) -> ElementBox {
-        let title = match &self.content {
-            TerminalContent::Connected(connected) => {
-                connected.read(cx).handle().read(cx).title.to_string()
-            }
-            TerminalContent::Error(_) => "Terminal".to_string(),
-        };
+        focused: bool,
+        cx: &mut gpui::RenderContext<'_, Self>,
+    ) -> bool {
+        //Don't blink the cursor when not focused, blinking is disabled, or paused
+        if !focused
+            || !self.blinking_on
+            || self.blinking_paused
+            || self
+                .terminal
+                .read(cx)
+                .last_mode
+                .contains(TermMode::ALT_SCREEN)
+        {
+            return true;
+        }
 
-        Flex::row()
-            .with_child(
-                Label::new(title, tab_theme.label.clone())
-                    .aligned()
-                    .contained()
-                    .boxed(),
-            )
-            .boxed()
-    }
+        let setting = {
+            let settings = cx.global::<Settings>();
+            settings
+                .terminal_overrides
+                .blinking
+                .clone()
+                .unwrap_or(TerminalBlink::TerminalControlled)
+        };
 
-    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
-        //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
-        Some(TerminalView::new(
-            self.associated_directory.clone(),
-            false,
-            cx,
-        ))
+        match setting {
+            //If the user requested to never blink, don't blink it.
+            TerminalBlink::Off => true,
+            //If the terminal is controlling it, check terminal mode
+            TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state,
+        }
     }
 
-    fn project_path(&self, _cx: &gpui::AppContext) -> Option<ProjectPath> {
-        None
+    fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
+        if epoch == self.blink_epoch && !self.blinking_paused {
+            self.blink_state = !self.blink_state;
+            cx.notify();
+
+            let epoch = self.next_blink_epoch();
+            cx.spawn(|this, mut cx| {
+                let this = this.downgrade();
+                async move {
+                    Timer::after(CURSOR_BLINK_INTERVAL).await;
+                    if let Some(this) = this.upgrade(&cx) {
+                        this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
+                    }
+                }
+            })
+            .detach();
+        }
     }
 
-    fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
-        SmallVec::new()
+    pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
+        self.blink_state = true;
+        cx.notify();
+
+        let epoch = self.next_blink_epoch();
+        cx.spawn(|this, mut cx| {
+            let this = this.downgrade();
+            async move {
+                Timer::after(CURSOR_BLINK_INTERVAL).await;
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
+                }
+            }
+        })
+        .detach();
     }
 
-    fn is_singleton(&self, _cx: &gpui::AppContext) -> bool {
-        false
+    fn next_blink_epoch(&mut self) -> usize {
+        self.blink_epoch += 1;
+        self.blink_epoch
     }
 
-    fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
-
-    fn can_save(&self, _cx: &gpui::AppContext) -> bool {
-        false
+    fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
+        if epoch == self.blink_epoch {
+            self.blinking_paused = false;
+            self.blink_cursors(epoch, cx);
+        }
     }
 
-    fn save(
-        &mut self,
-        _project: gpui::ModelHandle<Project>,
-        _cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        unreachable!("save should not have been called");
+    ///Attempt to paste the clipboard into the terminal
+    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
+        self.terminal.update(cx, |term, _| term.copy())
     }
 
-    fn save_as(
-        &mut self,
-        _project: gpui::ModelHandle<Project>,
-        _abs_path: std::path::PathBuf,
-        _cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        unreachable!("save_as should not have been called");
+    ///Attempt to paste the clipboard into the terminal
+    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+        if let Some(item) = cx.read_from_clipboard() {
+            self.terminal
+                .update(cx, |terminal, _cx| terminal.paste(item.text()));
+        }
     }
 
-    fn reload(
-        &mut self,
-        _project: gpui::ModelHandle<Project>,
-        _cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        gpui::Task::ready(Ok(()))
+    ///Synthesize the keyboard event corresponding to 'up'
+    fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
+        self.clear_bel(cx);
+        self.terminal.update(cx, |term, _| {
+            term.try_keystroke(&Keystroke::parse("up").unwrap())
+        });
     }
 
-    fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
-        if let TerminalContent::Connected(connected) = &self.content {
-            connected.read(cx).has_new_content()
-        } else {
-            false
-        }
+    ///Synthesize the keyboard event corresponding to 'down'
+    fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
+        self.clear_bel(cx);
+        self.terminal.update(cx, |term, _| {
+            term.try_keystroke(&Keystroke::parse("down").unwrap())
+        });
     }
 
-    fn has_conflict(&self, cx: &AppContext) -> bool {
-        if let TerminalContent::Connected(connected) = &self.content {
-            connected.read(cx).has_bell()
-        } else {
-            false
-        }
+    ///Synthesize the keyboard event corresponding to 'ctrl-c'
+    fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
+        self.clear_bel(cx);
+        self.terminal.update(cx, |term, _| {
+            term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap())
+        });
     }
 
-    fn should_update_tab_on_event(event: &Self::Event) -> bool {
-        matches!(event, &Event::TitleChanged | &Event::Wakeup)
+    ///Synthesize the keyboard event corresponding to 'escape'
+    fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
+        self.clear_bel(cx);
+        self.terminal.update(cx, |term, _| {
+            term.try_keystroke(&Keystroke::parse("escape").unwrap())
+        });
     }
 
-    fn should_close_item_on_event(event: &Self::Event) -> bool {
-        matches!(event, &Event::CloseTerminal)
+    ///Synthesize the keyboard event corresponding to 'enter'
+    fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
+        self.clear_bel(cx);
+        self.terminal.update(cx, |term, _| {
+            term.try_keystroke(&Keystroke::parse("enter").unwrap())
+        });
     }
 }
 
-///Get's the working directory for the given workspace, respecting the user's settings.
-pub fn get_working_directory(
-    workspace: &Workspace,
-    cx: &AppContext,
-    strategy: WorkingDirectory,
-) -> Option<PathBuf> {
-    let res = match strategy {
-        WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx)
-            .or_else(|| first_project_directory(workspace, cx)),
-        WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
-        WorkingDirectory::AlwaysHome => None,
-        WorkingDirectory::Always { directory } => {
-            shellexpand::full(&directory) //TODO handle this better
-                .ok()
-                .map(|dir| Path::new(&dir.to_string()).to_path_buf())
-                .filter(|dir| dir.is_dir())
-        }
-    };
-    res.or_else(home_dir)
-}
-
-///Get's the first project's home directory, or the home directory
-fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
-    workspace
-        .worktrees(cx)
-        .next()
-        .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
-        .and_then(get_path_from_wt)
-}
-
-///Gets the intuitively correct working directory from the given workspace
-///If there is an active entry for this project, returns that entry's worktree root.
-///If there's no active entry but there is a worktree, returns that worktrees root.
-///If either of these roots are files, or if there are any other query failures,
-///  returns the user's home directory
-fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
-    let project = workspace.project().read(cx);
-
-    project
-        .active_entry()
-        .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
-        .or_else(|| workspace.worktrees(cx).next())
-        .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
-        .and_then(get_path_from_wt)
-}
-
-fn get_path_from_wt(wt: &LocalWorktree) -> Option<PathBuf> {
-    wt.root_entry()
-        .filter(|re| re.is_dir())
-        .map(|_| wt.abs_path().to_path_buf())
-}
-
-#[cfg(test)]
-mod tests {
-
-    use super::*;
-    use gpui::TestAppContext;
-
-    use std::path::Path;
-
-    use crate::tests::terminal_test_context::TerminalTestContext;
-
-    ///Working directory calculation tests
-
-    ///No Worktrees in project -> home_dir()
-    #[gpui::test]
-    async fn no_worktree(cx: &mut TestAppContext) {
-        //Setup variables
-        let mut cx = TerminalTestContext::new(cx);
-        let (project, workspace) = cx.blank_workspace().await;
-        //Test
-        cx.cx.read(|cx| {
-            let workspace = workspace.read(cx);
-            let active_entry = project.read(cx).active_entry();
-
-            //Make sure enviroment is as expeted
-            assert!(active_entry.is_none());
-            assert!(workspace.worktrees(cx).next().is_none());
-
-            let res = current_project_directory(workspace, cx);
-            assert_eq!(res, None);
-            let res = first_project_directory(workspace, cx);
-            assert_eq!(res, None);
-        });
+impl View for TerminalView {
+    fn ui_name() -> &'static str {
+        "Terminal"
     }
 
-    ///No active entry, but a worktree, worktree is a file -> home_dir()
-    #[gpui::test]
-    async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
-        //Setup variables
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
+        let terminal_handle = self.terminal.clone().downgrade();
 
-        let mut cx = TerminalTestContext::new(cx);
-        let (project, workspace) = cx.blank_workspace().await;
-        cx.create_file_wt(project.clone(), "/root.txt").await;
+        let self_id = cx.view_id();
+        let focused = cx
+            .focused_view_id(cx.window_id())
+            .filter(|view_id| *view_id == self_id)
+            .is_some();
 
-        cx.cx.read(|cx| {
-            let workspace = workspace.read(cx);
-            let active_entry = project.read(cx).active_entry();
+        Stack::new()
+            .with_child(
+                TerminalElement::new(
+                    cx.handle(),
+                    terminal_handle,
+                    self.modal,
+                    focused,
+                    self.should_show_cursor(focused, cx),
+                )
+                .contained()
+                .boxed(),
+            )
+            .with_child(ChildView::new(&self.context_menu).boxed())
+            .boxed()
+    }
 
-            //Make sure enviroment is as expeted
-            assert!(active_entry.is_none());
-            assert!(workspace.worktrees(cx).next().is_some());
+    fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.has_new_content = false;
+        self.terminal.read(cx).focus_in();
+        self.blink_cursors(self.blink_epoch, cx);
+        cx.notify();
+    }
 
-            let res = current_project_directory(workspace, cx);
-            assert_eq!(res, None);
-            let res = first_project_directory(workspace, cx);
-            assert_eq!(res, None);
-        });
+    fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.terminal.read(cx).focus_out();
+        cx.notify();
     }
 
-    //No active entry, but a worktree, worktree is a folder -> worktree_folder
-    #[gpui::test]
-    async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
-        //Setup variables
-        let mut cx = TerminalTestContext::new(cx);
-        let (project, workspace) = cx.blank_workspace().await;
-        let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root/").await;
-
-        //Test
-        cx.cx.update(|cx| {
-            let workspace = workspace.read(cx);
-            let active_entry = project.read(cx).active_entry();
-
-            assert!(active_entry.is_none());
-            assert!(workspace.worktrees(cx).next().is_some());
-
-            let res = current_project_directory(workspace, cx);
-            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
-            let res = first_project_directory(workspace, cx);
-            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
-        });
+    //IME stuff
+    fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
+        if self
+            .terminal
+            .read(cx)
+            .last_mode
+            .contains(TermMode::ALT_SCREEN)
+        {
+            None
+        } else {
+            Some(0..0)
+        }
     }
 
-    //Active entry with a work tree, worktree is a file -> home_dir()
-    #[gpui::test]
-    async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
-        //Setup variables
-        let mut cx = TerminalTestContext::new(cx);
-        let (project, workspace) = cx.blank_workspace().await;
-        let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await;
-        let (wt2, entry2) = cx.create_file_wt(project.clone(), "/root2.txt").await;
-        cx.insert_active_entry_for(wt2, entry2, project.clone());
-
-        //Test
-        cx.cx.update(|cx| {
-            let workspace = workspace.read(cx);
-            let active_entry = project.read(cx).active_entry();
-
-            assert!(active_entry.is_some());
-
-            let res = current_project_directory(workspace, cx);
-            assert_eq!(res, None);
-            let res = first_project_directory(workspace, cx);
-            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
+    fn replace_text_in_range(
+        &mut self,
+        _: Option<std::ops::Range<usize>>,
+        text: &str,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.terminal.update(cx, |terminal, _| {
+            terminal.input(text.into());
         });
     }
 
-    //Active entry, with a worktree, worktree is a folder -> worktree_folder
-    #[gpui::test]
-    async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
-        //Setup variables
-        let mut cx = TerminalTestContext::new(cx);
-        let (project, workspace) = cx.blank_workspace().await;
-        let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await;
-        let (wt2, entry2) = cx.create_folder_wt(project.clone(), "/root2/").await;
-        cx.insert_active_entry_for(wt2, entry2, project.clone());
-
-        //Test
-        cx.cx.update(|cx| {
-            let workspace = workspace.read(cx);
-            let active_entry = project.read(cx).active_entry();
-
-            assert!(active_entry.is_some());
-
-            let res = current_project_directory(workspace, cx);
-            assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
-            let res = first_project_directory(workspace, cx);
-            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
-        });
+    fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context {
+        let mut context = Self::default_keymap_context();
+        if self.modal {
+            context.set.insert("ModalTerminal".into());
+        }
+        let mode = self.terminal.read(cx).last_mode;
+        context.map.insert(
+            "screen".to_string(),
+            (if mode.contains(TermMode::ALT_SCREEN) {
+                "alt"
+            } else {
+                "normal"
+            })
+            .to_string(),
+        );
+
+        if mode.contains(TermMode::APP_CURSOR) {
+            context.set.insert("DECCKM".to_string());
+        }
+        if mode.contains(TermMode::APP_KEYPAD) {
+            context.set.insert("DECPAM".to_string());
+        }
+        //Note the ! here
+        if !mode.contains(TermMode::APP_KEYPAD) {
+            context.set.insert("DECPNM".to_string());
+        }
+        if mode.contains(TermMode::SHOW_CURSOR) {
+            context.set.insert("DECTCEM".to_string());
+        }
+        if mode.contains(TermMode::LINE_WRAP) {
+            context.set.insert("DECAWM".to_string());
+        }
+        if mode.contains(TermMode::ORIGIN) {
+            context.set.insert("DECOM".to_string());
+        }
+        if mode.contains(TermMode::INSERT) {
+            context.set.insert("IRM".to_string());
+        }
+        //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
+        if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
+            context.set.insert("LNM".to_string());
+        }
+        if mode.contains(TermMode::FOCUS_IN_OUT) {
+            context.set.insert("report_focus".to_string());
+        }
+        if mode.contains(TermMode::ALTERNATE_SCROLL) {
+            context.set.insert("alternate_scroll".to_string());
+        }
+        if mode.contains(TermMode::BRACKETED_PASTE) {
+            context.set.insert("bracketed_paste".to_string());
+        }
+        if mode.intersects(TermMode::MOUSE_MODE) {
+            context.set.insert("any_mouse_reporting".to_string());
+        }
+        {
+            let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
+                "click"
+            } else if mode.contains(TermMode::MOUSE_DRAG) {
+                "drag"
+            } else if mode.contains(TermMode::MOUSE_MOTION) {
+                "motion"
+            } else {
+                "off"
+            };
+            context
+                .map
+                .insert("mouse_reporting".to_string(), mouse_reporting.to_string());
+        }
+        {
+            let format = if mode.contains(TermMode::SGR_MOUSE) {
+                "sgr"
+            } else if mode.contains(TermMode::UTF8_MOUSE) {
+                "utf8"
+            } else {
+                "normal"
+            };
+            context
+                .map
+                .insert("mouse_format".to_string(), format.to_string());
+        }
+        context
     }
 }