Merge branch 'main' into drag-and-drop

K Simmons created

Change summary

crates/terminal/src/connected_el.rs            | 895 -------------------
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        | 904 +++++++++++++++----
crates/terminal/src/terminal_view.rs           | 794 ++++++++---------
7 files changed, 1,570 insertions(+), 2,001 deletions(-)

Detailed changes

crates/terminal/src/connected_el.rs 🔗

@@ -1,895 +0,0 @@
-use alacritty_terminal::{
-    ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
-    grid::Dimensions,
-    index::Point,
-    selection::SelectionRange,
-    term::{
-        cell::{Cell, Flags},
-        TermMode,
-    },
-};
-use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
-use gpui::{
-    color::Color,
-    fonts::{Properties, Style::Italic, TextStyle, Underline, Weight},
-    geometry::{
-        rect::RectF,
-        vector::{vec2f, Vector2F},
-    },
-    serde_json::json,
-    text_layout::{Line, RunStyle},
-    Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseRegion,
-    PaintContext, Quad, TextLayoutCache, WeakModelHandle, WeakViewHandle,
-};
-use itertools::Itertools;
-use ordered_float::OrderedFloat;
-use settings::Settings;
-use theme::TerminalStyle;
-use util::ResultExt;
-
-use std::fmt::Debug;
-use std::{
-    mem,
-    ops::{Deref, Range},
-};
-
-use crate::{
-    connected_view::{ConnectedView, DeployContextMenu},
-    mappings::colors::convert_color,
-    Terminal, TerminalSize,
-};
-
-///The information generated during layout that is nescessary for painting
-pub struct LayoutState {
-    cells: Vec<LayoutCell>,
-    rects: Vec<LayoutRect>,
-    highlights: Vec<RelativeHighlightedRange>,
-    cursor: Option<Cursor>,
-    background_color: Color,
-    selection_color: Color,
-    size: TerminalSize,
-    mode: TermMode,
-}
-
-#[derive(Debug)]
-struct IndexedCell {
-    point: Point,
-    cell: Cell,
-}
-
-impl Deref for IndexedCell {
-    type Target = Cell;
-
-    #[inline]
-    fn deref(&self) -> &Cell {
-        &self.cell
-    }
-}
-
-///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
-struct DisplayCursor {
-    line: i32,
-    col: usize,
-}
-
-impl DisplayCursor {
-    fn from(cursor_point: Point, display_offset: usize) -> Self {
-        Self {
-            line: cursor_point.line.0 + display_offset as i32,
-            col: cursor_point.column.0,
-        }
-    }
-
-    pub fn line(&self) -> i32 {
-        self.line
-    }
-
-    pub fn col(&self) -> usize {
-        self.col
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-struct LayoutCell {
-    point: Point<i32, i32>,
-    text: Line,
-}
-
-impl LayoutCell {
-    fn new(point: Point<i32, i32>, text: Line) -> LayoutCell {
-        LayoutCell { point, text }
-    }
-
-    fn paint(
-        &self,
-        origin: Vector2F,
-        layout: &LayoutState,
-        visible_bounds: RectF,
-        cx: &mut PaintContext,
-    ) {
-        let pos = {
-            let point = self.point;
-            vec2f(
-                (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
-                origin.y() + point.line as f32 * layout.size.line_height,
-            )
-        };
-
-        self.text
-            .paint(pos, visible_bounds, layout.size.line_height, cx);
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-struct LayoutRect {
-    point: Point<i32, i32>,
-    num_of_cells: usize,
-    color: Color,
-}
-
-impl LayoutRect {
-    fn new(point: Point<i32, i32>, num_of_cells: usize, color: Color) -> LayoutRect {
-        LayoutRect {
-            point,
-            num_of_cells,
-            color,
-        }
-    }
-
-    fn extend(&self) -> Self {
-        LayoutRect {
-            point: self.point,
-            num_of_cells: self.num_of_cells + 1,
-            color: self.color,
-        }
-    }
-
-    fn paint(&self, origin: Vector2F, layout: &LayoutState, cx: &mut PaintContext) {
-        let position = {
-            let point = self.point;
-            vec2f(
-                (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
-                origin.y() + point.line as f32 * layout.size.line_height,
-            )
-        };
-        let size = vec2f(
-            (layout.size.cell_width * self.num_of_cells as f32).ceil(),
-            layout.size.line_height,
-        );
-
-        cx.scene.push_quad(Quad {
-            bounds: RectF::new(position, size),
-            background: Some(self.color),
-            border: Default::default(),
-            corner_radius: 0.,
-        })
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-struct RelativeHighlightedRange {
-    line_index: usize,
-    range: Range<usize>,
-}
-
-impl RelativeHighlightedRange {
-    fn new(line_index: usize, range: Range<usize>) -> Self {
-        RelativeHighlightedRange { line_index, range }
-    }
-
-    fn to_highlighted_range_line(
-        &self,
-        origin: Vector2F,
-        layout: &LayoutState,
-    ) -> HighlightedRangeLine {
-        let start_x = origin.x() + self.range.start as f32 * layout.size.cell_width;
-        let end_x =
-            origin.x() + self.range.end as f32 * layout.size.cell_width + layout.size.cell_width;
-
-        HighlightedRangeLine { start_x, end_x }
-    }
-}
-
-///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 {
-    terminal: WeakModelHandle<Terminal>,
-    view: WeakViewHandle<ConnectedView>,
-    modal: bool,
-    focused: bool,
-    cursor_visible: bool,
-}
-
-impl TerminalEl {
-    pub fn new(
-        view: WeakViewHandle<ConnectedView>,
-        terminal: WeakModelHandle<Terminal>,
-        modal: bool,
-        focused: bool,
-        cursor_visible: bool,
-    ) -> TerminalEl {
-        TerminalEl {
-            view,
-            terminal,
-            modal,
-            focused,
-            cursor_visible,
-        }
-    }
-
-    fn layout_grid(
-        grid: Vec<IndexedCell>,
-        text_style: &TextStyle,
-        terminal_theme: &TerminalStyle,
-        text_layout_cache: &TextLayoutCache,
-        font_cache: &FontCache,
-        modal: bool,
-        selection_range: Option<SelectionRange>,
-    ) -> (
-        Vec<LayoutCell>,
-        Vec<LayoutRect>,
-        Vec<RelativeHighlightedRange>,
-    ) {
-        let mut cells = vec![];
-        let mut rects = vec![];
-        let mut highlight_ranges = vec![];
-
-        let mut cur_rect: Option<LayoutRect> = None;
-        let mut cur_alac_color = None;
-        let mut highlighted_range = None;
-
-        let linegroups = grid.into_iter().group_by(|i| i.point.line);
-        for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
-            for (x_index, cell) in line.enumerate() {
-                let mut fg = cell.fg;
-                let mut bg = cell.bg;
-                if cell.flags.contains(Flags::INVERSE) {
-                    mem::swap(&mut fg, &mut bg);
-                }
-
-                //Increase selection range
-                {
-                    if selection_range
-                        .map(|range| range.contains(cell.point))
-                        .unwrap_or(false)
-                    {
-                        let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
-                        range.end = range.end.max(x_index);
-                        highlighted_range = Some(range);
-                    }
-                }
-
-                //Expand background rect range
-                {
-                    if matches!(bg, Named(NamedColor::Background)) {
-                        //Continue to next cell, resetting variables if nescessary
-                        cur_alac_color = None;
-                        if let Some(rect) = cur_rect {
-                            rects.push(rect);
-                            cur_rect = None
-                        }
-                    } else {
-                        match cur_alac_color {
-                            Some(cur_color) => {
-                                if bg == cur_color {
-                                    cur_rect = cur_rect.take().map(|rect| rect.extend());
-                                } else {
-                                    cur_alac_color = Some(bg);
-                                    if cur_rect.is_some() {
-                                        rects.push(cur_rect.take().unwrap());
-                                    }
-                                    cur_rect = Some(LayoutRect::new(
-                                        Point::new(line_index as i32, cell.point.column.0 as i32),
-                                        1,
-                                        convert_color(&bg, &terminal_theme.colors, modal),
-                                    ));
-                                }
-                            }
-                            None => {
-                                cur_alac_color = Some(bg);
-                                cur_rect = Some(LayoutRect::new(
-                                    Point::new(line_index as i32, cell.point.column.0 as i32),
-                                    1,
-                                    convert_color(&bg, &terminal_theme.colors, modal),
-                                ));
-                            }
-                        }
-                    }
-                }
-
-                //Layout current cell text
-                {
-                    let cell_text = &cell.c.to_string();
-                    if cell_text != " " {
-                        let cell_style = TerminalEl::cell_style(
-                            &cell,
-                            fg,
-                            terminal_theme,
-                            text_style,
-                            font_cache,
-                            modal,
-                        );
-
-                        let layout_cell = text_layout_cache.layout_str(
-                            cell_text,
-                            text_style.font_size,
-                            &[(cell_text.len(), cell_style)],
-                        );
-
-                        cells.push(LayoutCell::new(
-                            Point::new(line_index as i32, cell.point.column.0 as i32),
-                            layout_cell,
-                        ))
-                    }
-                };
-            }
-
-            if highlighted_range.is_some() {
-                highlight_ranges.push(RelativeHighlightedRange::new(
-                    line_index,
-                    highlighted_range.take().unwrap(),
-                ))
-            }
-
-            if cur_rect.is_some() {
-                rects.push(cur_rect.take().unwrap());
-            }
-        }
-        (cells, rects, highlight_ranges)
-    }
-
-    // Compute the cursor position and expected block width, may return a zero width if x_for_index returns
-    // the same position for sequential indexes. Use em_width instead
-    fn shape_cursor(
-        cursor_point: DisplayCursor,
-        size: TerminalSize,
-        text_fragment: &Line,
-    ) -> Option<(Vector2F, f32)> {
-        if cursor_point.line() < size.total_lines() as i32 {
-            let cursor_width = if text_fragment.width() == 0. {
-                size.cell_width()
-            } else {
-                text_fragment.width()
-            };
-
-            //Cursor should always surround as much of the text as possible,
-            //hence when on pixel boundaries round the origin down and the width up
-            Some((
-                vec2f(
-                    (cursor_point.col() as f32 * size.cell_width()).floor(),
-                    (cursor_point.line() as f32 * size.line_height()).floor(),
-                ),
-                cursor_width.ceil(),
-            ))
-        } else {
-            None
-        }
-    }
-
-    ///Convert the Alacritty cell styles to GPUI text styles and background color
-    fn cell_style(
-        indexed: &IndexedCell,
-        fg: AnsiColor,
-        style: &TerminalStyle,
-        text_style: &TextStyle,
-        font_cache: &FontCache,
-        modal: bool,
-    ) -> RunStyle {
-        let flags = indexed.cell.flags;
-        let fg = convert_color(&fg, &style.colors, modal);
-
-        let underline = flags
-            .intersects(Flags::ALL_UNDERLINES)
-            .then(|| Underline {
-                color: Some(fg),
-                squiggly: flags.contains(Flags::UNDERCURL),
-                thickness: OrderedFloat(1.),
-            })
-            .unwrap_or_default();
-
-        let mut properties = Properties::new();
-        if indexed
-            .flags
-            .intersects(Flags::BOLD | Flags::BOLD_ITALIC | Flags::DIM_BOLD)
-        {
-            properties = *properties.weight(Weight::BOLD);
-        }
-        if indexed.flags.intersects(Flags::ITALIC | Flags::BOLD_ITALIC) {
-            properties = *properties.style(Italic);
-        }
-
-        let font_id = font_cache
-            .select_font(text_style.font_family_id, &properties)
-            .unwrap_or(text_style.font_id);
-
-        RunStyle {
-            color: fg,
-            font_id,
-            underline,
-        }
-    }
-
-    fn generic_button_handler<E>(
-        connection: WeakModelHandle<Terminal>,
-        origin: Vector2F,
-        f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext<Terminal>),
-    ) -> impl Fn(E, &mut EventContext) {
-        move |event, cx| {
-            cx.focus_parent_view();
-            if let Some(conn_handle) = connection.upgrade(cx.app) {
-                conn_handle.update(cx.app, |terminal, cx| {
-                    f(terminal, origin, event, cx);
-
-                    cx.notify();
-                })
-            }
-        }
-    }
-
-    fn attach_mouse_handlers(
-        &self,
-        origin: Vector2F,
-        view_id: usize,
-        visible_bounds: RectF,
-        mode: TermMode,
-        cx: &mut PaintContext,
-    ) {
-        let connection = self.terminal;
-
-        let mut region = MouseRegion::new(view_id, None, visible_bounds);
-
-        // Terminal Emulator controlled behavior:
-        region = region
-            // Start selections
-            .on_down(
-                MouseButton::Left,
-                TerminalEl::generic_button_handler(
-                    connection,
-                    origin,
-                    move |terminal, origin, e, _cx| {
-                        terminal.mouse_down(&e, origin);
-                    },
-                ),
-            )
-            // Update drag selections
-            .on_drag(MouseButton::Left, move |event, cx| {
-                if cx.is_parent_view_focused() {
-                    if let Some(conn_handle) = connection.upgrade(cx.app) {
-                        conn_handle.update(cx.app, |terminal, cx| {
-                            terminal.mouse_drag(event, origin);
-                            cx.notify();
-                        })
-                    }
-                }
-            })
-            // Copy on up behavior
-            .on_up(
-                MouseButton::Left,
-                TerminalEl::generic_button_handler(
-                    connection,
-                    origin,
-                    move |terminal, origin, e, _cx| {
-                        terminal.mouse_up(&e, origin);
-                    },
-                ),
-            )
-            // Handle click based selections
-            .on_click(
-                MouseButton::Left,
-                TerminalEl::generic_button_handler(
-                    connection,
-                    origin,
-                    move |terminal, origin, e, _cx| {
-                        terminal.left_click(&e, origin);
-                    },
-                ),
-            )
-            // Context menu
-            .on_click(MouseButton::Right, move |e, cx| {
-                let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx.app) {
-                    conn_handle.update(cx.app, |terminal, _cx| terminal.mouse_mode(e.shift))
-                } else {
-                    // If we can't get the model handle, probably can't deploy the context menu
-                    true
-                };
-                if !mouse_mode {
-                    cx.dispatch_action(DeployContextMenu {
-                        position: e.position,
-                    });
-                }
-            });
-
-        // Mouse mode handlers:
-        // All mouse modes need the extra click handlers
-        if mode.intersects(TermMode::MOUSE_MODE) {
-            region = region
-                .on_down(
-                    MouseButton::Right,
-                    TerminalEl::generic_button_handler(
-                        connection,
-                        origin,
-                        move |terminal, origin, e, _cx| {
-                            terminal.mouse_down(&e, origin);
-                        },
-                    ),
-                )
-                .on_down(
-                    MouseButton::Middle,
-                    TerminalEl::generic_button_handler(
-                        connection,
-                        origin,
-                        move |terminal, origin, e, _cx| {
-                            terminal.mouse_down(&e, origin);
-                        },
-                    ),
-                )
-                .on_up(
-                    MouseButton::Right,
-                    TerminalEl::generic_button_handler(
-                        connection,
-                        origin,
-                        move |terminal, origin, e, _cx| {
-                            terminal.mouse_up(&e, origin);
-                        },
-                    ),
-                )
-                .on_up(
-                    MouseButton::Middle,
-                    TerminalEl::generic_button_handler(
-                        connection,
-                        origin,
-                        move |terminal, origin, e, _cx| {
-                            terminal.mouse_up(&e, origin);
-                        },
-                    ),
-                )
-        }
-        //Mouse move manages both dragging and motion events
-        if mode.intersects(TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION) {
-            region = region
-                //TODO: This does not fire on right-mouse-down-move events.
-                .on_move(move |event, cx| {
-                    if cx.is_parent_view_focused() {
-                        if let Some(conn_handle) = connection.upgrade(cx.app) {
-                            conn_handle.update(cx.app, |terminal, cx| {
-                                terminal.mouse_move(&event, origin);
-                                cx.notify();
-                            })
-                        }
-                    }
-                })
-        }
-
-        cx.scene.push_mouse_region(region);
-    }
-
-    ///Configures a text style from the current settings.
-    pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
-        // Pull the font family from settings properly overriding
-        let family_id = settings
-            .terminal_overrides
-            .font_family
-            .as_ref()
-            .or(settings.terminal_defaults.font_family.as_ref())
-            .and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
-            .unwrap_or(settings.buffer_font_family);
-
-        let font_size = settings
-            .terminal_overrides
-            .font_size
-            .or(settings.terminal_defaults.font_size)
-            .unwrap_or(settings.buffer_font_size);
-
-        let font_id = font_cache
-            .select_font(family_id, &Default::default())
-            .unwrap();
-
-        TextStyle {
-            color: settings.theme.editor.text_color,
-            font_family_id: family_id,
-            font_family_name: font_cache.family_name(family_id).unwrap(),
-            font_id,
-            font_size,
-            font_properties: Default::default(),
-            underline: Default::default(),
-        }
-    }
-}
-
-impl Element for TerminalEl {
-    type LayoutState = LayoutState;
-    type PaintState = ();
-
-    fn layout(
-        &mut self,
-        constraint: gpui::SizeConstraint,
-        cx: &mut gpui::LayoutContext,
-    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
-        let settings = cx.global::<Settings>();
-        let font_cache = cx.font_cache();
-
-        //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 selection_color = settings.theme.editor.selection.selection;
-        let dimensions = {
-            let line_height = font_cache.line_height(text_style.font_size);
-            let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
-            TerminalSize::new(line_height, cell_width, constraint.max)
-        };
-
-        let background_color = if self.modal {
-            terminal_theme.colors.modal_background
-        } else {
-            terminal_theme.colors.background
-        };
-
-        let (cells, selection, cursor, display_offset, cursor_text, mode) = self
-            .terminal
-            .upgrade(cx)
-            .unwrap()
-            .update(cx.app, |terminal, mcx| {
-                terminal.set_size(dimensions);
-                terminal.render_lock(mcx, |content, cursor_text| {
-                    let mut cells = vec![];
-                    cells.extend(
-                        content
-                            .display_iter
-                            //TODO: Add this once there's a way to retain empty lines
-                            // .filter(|ic| {
-                            //     !ic.flags.contains(Flags::HIDDEN)
-                            //         && !(ic.bg == Named(NamedColor::Background)
-                            //             && ic.c == ' '
-                            //             && !ic.flags.contains(Flags::INVERSE))
-                            // })
-                            .map(|ic| IndexedCell {
-                                point: ic.point,
-                                cell: ic.cell.clone(),
-                            }),
-                    );
-                    (
-                        cells,
-                        content.selection,
-                        content.cursor,
-                        content.display_offset,
-                        cursor_text,
-                        content.mode,
-                    )
-                })
-            });
-
-        let (cells, rects, highlights) = TerminalEl::layout_grid(
-            cells,
-            &text_style,
-            &terminal_theme,
-            cx.text_layout_cache,
-            cx.font_cache(),
-            self.modal,
-            selection,
-        );
-
-        //Layout cursor. Rectangle is used for IME, so we should lay it out even
-        //if we don't end up showing it.
-        let cursor = if let AlacCursorShape::Hidden = cursor.shape {
-            None
-        } else {
-            let cursor_point = DisplayCursor::from(cursor.point, display_offset);
-            let cursor_text = {
-                let str_trxt = cursor_text.to_string();
-
-                let color = if self.focused {
-                    terminal_theme.colors.background
-                } else {
-                    terminal_theme.colors.foreground
-                };
-
-                cx.text_layout_cache.layout_str(
-                    &str_trxt,
-                    text_style.font_size,
-                    &[(
-                        str_trxt.len(),
-                        RunStyle {
-                            font_id: text_style.font_id,
-                            color,
-                            underline: Default::default(),
-                        },
-                    )],
-                )
-            };
-
-            TerminalEl::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,
-                        AlacCursorShape::Block => CursorShape::Block,
-                        AlacCursorShape::Underline => CursorShape::Underscore,
-                        AlacCursorShape::Beam => CursorShape::Bar,
-                        AlacCursorShape::HollowBlock => CursorShape::Hollow,
-                        //This case is handled in the if wrapping the whole cursor layout
-                        AlacCursorShape::Hidden => unreachable!(),
-                    };
-
-                    Cursor::new(
-                        cursor_position,
-                        block_width,
-                        dimensions.line_height,
-                        terminal_theme.colors.cursor,
-                        shape,
-                        Some(cursor_text),
-                    )
-                },
-            )
-        };
-
-        //Done!
-        (
-            constraint.max,
-            LayoutState {
-                cells,
-                cursor,
-                background_color,
-                selection_color,
-                size: dimensions,
-                rects,
-                highlights,
-                mode,
-            },
-        )
-    }
-
-    fn paint(
-        &mut self,
-        bounds: gpui::geometry::rect::RectF,
-        visible_bounds: gpui::geometry::rect::RectF,
-        layout: &mut Self::LayoutState,
-        cx: &mut gpui::PaintContext,
-    ) -> Self::PaintState {
-        //Setup element stuff
-        let clip_bounds = Some(visible_bounds);
-
-        cx.paint_layer(clip_bounds, |cx| {
-            let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
-
-            //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
-            self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.mode, cx);
-
-            cx.paint_layer(clip_bounds, |cx| {
-                //Start with a background color
-                cx.scene.push_quad(Quad {
-                    bounds: RectF::new(bounds.origin(), bounds.size()),
-                    background: Some(layout.background_color),
-                    border: Default::default(),
-                    corner_radius: 0.,
-                });
-
-                for rect in &layout.rects {
-                    rect.paint(origin, layout, cx)
-                }
-            });
-
-            //Draw Selection
-            cx.paint_layer(clip_bounds, |cx| {
-                let start_y = layout.highlights.get(0).map(|highlight| {
-                    origin.y() + highlight.line_index as f32 * layout.size.line_height
-                });
-
-                if let Some(y) = start_y {
-                    let range_lines = layout
-                        .highlights
-                        .iter()
-                        .map(|relative_highlight| {
-                            relative_highlight.to_highlighted_range_line(origin, layout)
-                        })
-                        .collect::<Vec<HighlightedRangeLine>>();
-
-                    let hr = HighlightedRange {
-                        start_y: y, //Need to change this
-                        line_height: layout.size.line_height,
-                        lines: range_lines,
-                        color: layout.selection_color,
-                        //Copied from editor. TODO: move to theme or something
-                        corner_radius: 0.15 * layout.size.line_height,
-                    };
-                    hr.paint(bounds, cx.scene);
-                }
-            });
-
-            //Draw the text cells
-            cx.paint_layer(clip_bounds, |cx| {
-                for cell in &layout.cells {
-                    cell.paint(origin, layout, visible_bounds, cx);
-                }
-            });
-
-            //Draw cursor
-            if self.cursor_visible {
-                if let Some(cursor) = &layout.cursor {
-                    cx.paint_layer(clip_bounds, |cx| {
-                        cursor.paint(origin, cx);
-                    })
-                }
-            }
-        });
-    }
-
-    fn dispatch_event(
-        &mut self,
-        event: &gpui::Event,
-        bounds: gpui::geometry::rect::RectF,
-        visible_bounds: gpui::geometry::rect::RectF,
-        layout: &mut Self::LayoutState,
-        _paint: &mut Self::PaintState,
-        cx: &mut gpui::EventContext,
-    ) -> bool {
-        match event {
-            Event::ScrollWheel(e) => visible_bounds
-                .contains_point(e.position)
-                .then(|| {
-                    let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
-
-                    if let Some(terminal) = self.terminal.upgrade(cx.app) {
-                        terminal.update(cx.app, |term, _| term.scroll(e, origin));
-                        cx.notify();
-                    }
-                })
-                .is_some(),
-            Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
-                if !cx.is_parent_view_focused() {
-                    return false;
-                }
-
-                if let Some(view) = self.view.upgrade(cx.app) {
-                    view.update(cx.app, |view, cx| {
-                        view.clear_bel(cx);
-                        view.pause_cursor_blinking(cx);
-                    })
-                }
-
-                self.terminal
-                    .upgrade(cx.app)
-                    .map(|model_handle| {
-                        model_handle.update(cx.app, |term, _| term.try_keystroke(keystroke))
-                    })
-                    .unwrap_or(false)
-            }
-            _ => false,
-        }
-    }
-
-    fn metadata(&self) -> Option<&dyn std::any::Any> {
-        None
-    }
-
-    fn debug(
-        &self,
-        _bounds: gpui::geometry::rect::RectF,
-        _layout: &Self::LayoutState,
-        _paint: &Self::PaintState,
-        _cx: &gpui::DebugContext,
-    ) -> gpui::serde_json::Value {
-        json!({
-            "type": "TerminalElement",
-        })
-    }
-
-    fn rect_for_text_range(
-        &self,
-        _: Range<usize>,
-        bounds: RectF,
-        _: RectF,
-        layout: &Self::LayoutState,
-        _: &Self::PaintState,
-        _: &gpui::MeasurementContext,
-    ) -> Option<RectF> {
-        // Use the same origin that's passed to `Cursor::paint` in the paint
-        // method bove.
-        let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
-
-        // TODO - Why is it necessary to move downward one line to get correct
-        // positioning? I would think that we'd want the same rect that is
-        // painted for the cursor.
-        origin += vec2f(0., layout.size.line_height);
-
-        Some(layout.cursor.as_ref()?.bounding_rect(origin))
-    }
-}

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::{
@@ -53,7 +53,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/terminal_element.rs 🔗

@@ -1,27 +1,25 @@
 use alacritty_terminal::{
-    grid::{Dimensions, GridIterator, Indexed, Scroll},
-    index::{Column as GridCol, Line as GridLine, Point, Side},
-    selection::{Selection, SelectionRange, SelectionType},
-    sync::FairMutex,
+    ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
+    grid::Dimensions,
+    index::Point,
+    selection::SelectionRange,
     term::{
         cell::{Cell, Flags},
-        SizeInfo,
+        TermMode,
     },
-    Term,
 };
 use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
 use gpui::{
     color::Color,
-    elements::*,
-    fonts::{TextStyle, Underline},
+    fonts::{Properties, Style::Italic, TextStyle, Underline, Weight},
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    json::json,
+    serde_json::json,
     text_layout::{Line, RunStyle},
-    Event, FontCache, KeyDownEvent, MouseButton, MouseRegion, PaintContext, Quad, ScrollWheelEvent,
-    SizeConstraint, TextLayoutCache, WeakModelHandle,
+    Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseRegion,
+    PaintContext, Quad, TextLayoutCache, WeakModelHandle, WeakViewHandle,
 };
 use itertools::Itertools;
 use ordered_float::OrderedFloat;
@@ -29,90 +27,576 @@ use settings::Settings;
 use theme::TerminalStyle;
 use util::ResultExt;
 
-use std::{cmp::min, ops::Range, sync::Arc};
-use std::{fmt::Debug, ops::Sub};
-
-use crate::{color_translation::convert_color, connection::TerminalConnection, ZedListener};
+use std::fmt::Debug;
+use std::{
+    mem,
+    ops::{Deref, Range},
+};
 
-///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
-///Scroll multiplier that is set to 3 by default. This will be removed when I
-///Implement scroll bars.
-const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
+use crate::{
+    mappings::colors::convert_color,
+    terminal_view::{DeployContextMenu, TerminalView},
+    Terminal, TerminalSize,
+};
 
-///Used to display the grid as passed to Alacritty and the TTY.
-///Useful for debugging inconsistencies between behavior and display
-#[cfg(debug_assertions)]
-const DEBUG_GRID: bool = false;
+///The information generated during layout that is nescessary for painting
+pub struct LayoutState {
+    cells: Vec<LayoutCell>,
+    rects: Vec<LayoutRect>,
+    highlights: Vec<RelativeHighlightedRange>,
+    cursor: Option<Cursor>,
+    background_color: Color,
+    selection_color: Color,
+    size: TerminalSize,
+    mode: TermMode,
+}
 
-///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<TerminalConnection>,
-    view_id: usize,
-    modal: bool,
+#[derive(Debug)]
+struct IndexedCell {
+    point: Point,
+    cell: Cell,
 }
 
-///New type pattern so I don't mix these two up
-struct CellWidth(f32);
-struct LineHeight(f32);
+impl Deref for IndexedCell {
+    type Target = Cell;
 
-struct LayoutLine {
-    cells: Vec<LayoutCell>,
-    highlighted_range: Option<Range<usize>>,
+    #[inline]
+    fn deref(&self) -> &Cell {
+        &self.cell
+    }
 }
 
-///New type pattern to ensure that we use adjusted mouse positions throughout the code base, rather than
-struct PaneRelativePos(Vector2F);
+///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
+struct DisplayCursor {
+    line: i32,
+    col: usize,
+}
+
+impl DisplayCursor {
+    fn from(cursor_point: Point, display_offset: usize) -> Self {
+        Self {
+            line: cursor_point.line.0 + display_offset as i32,
+            col: cursor_point.column.0,
+        }
+    }
+
+    pub fn line(&self) -> i32 {
+        self.line
+    }
 
-///Functionally the constructor for the PaneRelativePos type, mutates the mouse_position
-fn relative_pos(mouse_position: Vector2F, origin: Vector2F) -> PaneRelativePos {
-    PaneRelativePos(mouse_position.sub(origin)) //Avoid the extra allocation by mutating
+    pub fn col(&self) -> usize {
+        self.col
+    }
 }
 
 #[derive(Clone, Debug, Default)]
 struct LayoutCell {
     point: Point<i32, i32>,
-    text: Line, //NOTE TO SELF THIS IS BAD PERFORMANCE RN!
-    background_color: Color,
+    text: Line,
 }
 
 impl LayoutCell {
-    fn new(point: Point<i32, i32>, text: Line, background_color: Color) -> LayoutCell {
-        LayoutCell {
+    fn new(point: Point<i32, i32>, text: Line) -> LayoutCell {
+        LayoutCell { point, text }
+    }
+
+    fn paint(
+        &self,
+        origin: Vector2F,
+        layout: &LayoutState,
+        visible_bounds: RectF,
+        cx: &mut PaintContext,
+    ) {
+        let pos = {
+            let point = self.point;
+            vec2f(
+                (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
+                origin.y() + point.line as f32 * layout.size.line_height,
+            )
+        };
+
+        self.text
+            .paint(pos, visible_bounds, layout.size.line_height, cx);
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+struct LayoutRect {
+    point: Point<i32, i32>,
+    num_of_cells: usize,
+    color: Color,
+}
+
+impl LayoutRect {
+    fn new(point: Point<i32, i32>, num_of_cells: usize, color: Color) -> LayoutRect {
+        LayoutRect {
             point,
-            text,
-            background_color,
+            num_of_cells,
+            color,
         }
     }
+
+    fn extend(&self) -> Self {
+        LayoutRect {
+            point: self.point,
+            num_of_cells: self.num_of_cells + 1,
+            color: self.color,
+        }
+    }
+
+    fn paint(&self, origin: Vector2F, layout: &LayoutState, cx: &mut PaintContext) {
+        let position = {
+            let point = self.point;
+            vec2f(
+                (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
+                origin.y() + point.line as f32 * layout.size.line_height,
+            )
+        };
+        let size = vec2f(
+            (layout.size.cell_width * self.num_of_cells as f32).ceil(),
+            layout.size.line_height,
+        );
+
+        cx.scene.push_quad(Quad {
+            bounds: RectF::new(position, size),
+            background: Some(self.color),
+            border: Default::default(),
+            corner_radius: 0.,
+        })
+    }
 }
 
-///The information generated during layout that is nescessary for painting
-pub struct LayoutState {
-    layout_lines: Vec<LayoutLine>,
-    line_height: LineHeight,
-    em_width: CellWidth,
-    cursor: Option<Cursor>,
-    background_color: Color,
-    cur_size: SizeInfo,
-    terminal: Arc<FairMutex<Term<ZedListener>>>,
-    selection_color: Color,
+#[derive(Clone, Debug, Default)]
+struct RelativeHighlightedRange {
+    line_index: usize,
+    range: Range<usize>,
 }
 
-impl TerminalEl {
+impl RelativeHighlightedRange {
+    fn new(line_index: usize, range: Range<usize>) -> Self {
+        RelativeHighlightedRange { line_index, range }
+    }
+
+    fn to_highlighted_range_line(
+        &self,
+        origin: Vector2F,
+        layout: &LayoutState,
+    ) -> HighlightedRangeLine {
+        let start_x = origin.x() + self.range.start as f32 * layout.size.cell_width;
+        let end_x =
+            origin.x() + self.range.end as f32 * layout.size.cell_width + layout.size.cell_width;
+
+        HighlightedRangeLine { start_x, end_x }
+    }
+}
+
+///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 TerminalElement {
+    terminal: WeakModelHandle<Terminal>,
+    view: WeakViewHandle<TerminalView>,
+    modal: bool,
+    focused: bool,
+    cursor_visible: bool,
+}
+
+impl TerminalElement {
     pub fn new(
-        view_id: usize,
-        connection: WeakModelHandle<TerminalConnection>,
+        view: WeakViewHandle<TerminalView>,
+        terminal: WeakModelHandle<Terminal>,
         modal: bool,
-    ) -> TerminalEl {
-        TerminalEl {
-            view_id,
-            connection,
+        focused: bool,
+        cursor_visible: bool,
+    ) -> TerminalElement {
+        TerminalElement {
+            view,
+            terminal,
             modal,
+            focused,
+            cursor_visible,
+        }
+    }
+
+    fn layout_grid(
+        grid: Vec<IndexedCell>,
+        text_style: &TextStyle,
+        terminal_theme: &TerminalStyle,
+        text_layout_cache: &TextLayoutCache,
+        font_cache: &FontCache,
+        modal: bool,
+        selection_range: Option<SelectionRange>,
+    ) -> (
+        Vec<LayoutCell>,
+        Vec<LayoutRect>,
+        Vec<RelativeHighlightedRange>,
+    ) {
+        let mut cells = vec![];
+        let mut rects = vec![];
+        let mut highlight_ranges = vec![];
+
+        let mut cur_rect: Option<LayoutRect> = None;
+        let mut cur_alac_color = None;
+        let mut highlighted_range = None;
+
+        let linegroups = grid.into_iter().group_by(|i| i.point.line);
+        for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
+            for (x_index, cell) in line.enumerate() {
+                let mut fg = cell.fg;
+                let mut bg = cell.bg;
+                if cell.flags.contains(Flags::INVERSE) {
+                    mem::swap(&mut fg, &mut bg);
+                }
+
+                //Increase selection range
+                {
+                    if selection_range
+                        .map(|range| range.contains(cell.point))
+                        .unwrap_or(false)
+                    {
+                        let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
+                        range.end = range.end.max(x_index);
+                        highlighted_range = Some(range);
+                    }
+                }
+
+                //Expand background rect range
+                {
+                    if matches!(bg, Named(NamedColor::Background)) {
+                        //Continue to next cell, resetting variables if nescessary
+                        cur_alac_color = None;
+                        if let Some(rect) = cur_rect {
+                            rects.push(rect);
+                            cur_rect = None
+                        }
+                    } else {
+                        match cur_alac_color {
+                            Some(cur_color) => {
+                                if bg == cur_color {
+                                    cur_rect = cur_rect.take().map(|rect| rect.extend());
+                                } else {
+                                    cur_alac_color = Some(bg);
+                                    if cur_rect.is_some() {
+                                        rects.push(cur_rect.take().unwrap());
+                                    }
+                                    cur_rect = Some(LayoutRect::new(
+                                        Point::new(line_index as i32, cell.point.column.0 as i32),
+                                        1,
+                                        convert_color(&bg, &terminal_theme.colors, modal),
+                                    ));
+                                }
+                            }
+                            None => {
+                                cur_alac_color = Some(bg);
+                                cur_rect = Some(LayoutRect::new(
+                                    Point::new(line_index as i32, cell.point.column.0 as i32),
+                                    1,
+                                    convert_color(&bg, &terminal_theme.colors, modal),
+                                ));
+                            }
+                        }
+                    }
+                }
+
+                //Layout current cell text
+                {
+                    let cell_text = &cell.c.to_string();
+                    if cell_text != " " {
+                        let cell_style = TerminalElement::cell_style(
+                            &cell,
+                            fg,
+                            terminal_theme,
+                            text_style,
+                            font_cache,
+                            modal,
+                        );
+
+                        let layout_cell = text_layout_cache.layout_str(
+                            cell_text,
+                            text_style.font_size,
+                            &[(cell_text.len(), cell_style)],
+                        );
+
+                        cells.push(LayoutCell::new(
+                            Point::new(line_index as i32, cell.point.column.0 as i32),
+                            layout_cell,
+                        ))
+                    }
+                };
+            }
+
+            if highlighted_range.is_some() {
+                highlight_ranges.push(RelativeHighlightedRange::new(
+                    line_index,
+                    highlighted_range.take().unwrap(),
+                ))
+            }
+
+            if cur_rect.is_some() {
+                rects.push(cur_rect.take().unwrap());
+            }
+        }
+        (cells, rects, highlight_ranges)
+    }
+
+    // Compute the cursor position and expected block width, may return a zero width if x_for_index returns
+    // the same position for sequential indexes. Use em_width instead
+    fn shape_cursor(
+        cursor_point: DisplayCursor,
+        size: TerminalSize,
+        text_fragment: &Line,
+    ) -> Option<(Vector2F, f32)> {
+        if cursor_point.line() < size.total_lines() as i32 {
+            let cursor_width = if text_fragment.width() == 0. {
+                size.cell_width()
+            } else {
+                text_fragment.width()
+            };
+
+            //Cursor should always surround as much of the text as possible,
+            //hence when on pixel boundaries round the origin down and the width up
+            Some((
+                vec2f(
+                    (cursor_point.col() as f32 * size.cell_width()).floor(),
+                    (cursor_point.line() as f32 * size.line_height()).floor(),
+                ),
+                cursor_width.ceil(),
+            ))
+        } else {
+            None
+        }
+    }
+
+    ///Convert the Alacritty cell styles to GPUI text styles and background color
+    fn cell_style(
+        indexed: &IndexedCell,
+        fg: AnsiColor,
+        style: &TerminalStyle,
+        text_style: &TextStyle,
+        font_cache: &FontCache,
+        modal: bool,
+    ) -> RunStyle {
+        let flags = indexed.cell.flags;
+        let fg = convert_color(&fg, &style.colors, modal);
+
+        let underline = flags
+            .intersects(Flags::ALL_UNDERLINES)
+            .then(|| Underline {
+                color: Some(fg),
+                squiggly: flags.contains(Flags::UNDERCURL),
+                thickness: OrderedFloat(1.),
+            })
+            .unwrap_or_default();
+
+        let mut properties = Properties::new();
+        if indexed
+            .flags
+            .intersects(Flags::BOLD | Flags::BOLD_ITALIC | Flags::DIM_BOLD)
+        {
+            properties = *properties.weight(Weight::BOLD);
+        }
+        if indexed.flags.intersects(Flags::ITALIC | Flags::BOLD_ITALIC) {
+            properties = *properties.style(Italic);
+        }
+
+        let font_id = font_cache
+            .select_font(text_style.font_family_id, &properties)
+            .unwrap_or(text_style.font_id);
+
+        RunStyle {
+            color: fg,
+            font_id,
+            underline,
+        }
+    }
+
+    fn generic_button_handler<E>(
+        connection: WeakModelHandle<Terminal>,
+        origin: Vector2F,
+        f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext<Terminal>),
+    ) -> impl Fn(E, &mut EventContext) {
+        move |event, cx| {
+            cx.focus_parent_view();
+            if let Some(conn_handle) = connection.upgrade(cx.app) {
+                conn_handle.update(cx.app, |terminal, cx| {
+                    f(terminal, origin, event, cx);
+
+                    cx.notify();
+                })
+            }
+        }
+    }
+
+    fn attach_mouse_handlers(
+        &self,
+        origin: Vector2F,
+        view_id: usize,
+        visible_bounds: RectF,
+        mode: TermMode,
+        cx: &mut PaintContext,
+    ) {
+        let connection = self.terminal;
+
+        let mut region = MouseRegion::new(view_id, None, visible_bounds);
+
+        // Terminal Emulator controlled behavior:
+        region = region
+            // Start selections
+            .on_down(
+                MouseButton::Left,
+                TerminalElement::generic_button_handler(
+                    connection,
+                    origin,
+                    move |terminal, origin, e, _cx| {
+                        terminal.mouse_down(&e, origin);
+                    },
+                ),
+            )
+            // Update drag selections
+            .on_drag(MouseButton::Left, move |event, cx| {
+                if cx.is_parent_view_focused() {
+                    if let Some(conn_handle) = connection.upgrade(cx.app) {
+                        conn_handle.update(cx.app, |terminal, cx| {
+                            terminal.mouse_drag(event, origin);
+                            cx.notify();
+                        })
+                    }
+                }
+            })
+            // Copy on up behavior
+            .on_up(
+                MouseButton::Left,
+                TerminalElement::generic_button_handler(
+                    connection,
+                    origin,
+                    move |terminal, origin, e, _cx| {
+                        terminal.mouse_up(&e, origin);
+                    },
+                ),
+            )
+            // Handle click based selections
+            .on_click(
+                MouseButton::Left,
+                TerminalElement::generic_button_handler(
+                    connection,
+                    origin,
+                    move |terminal, origin, e, _cx| {
+                        terminal.left_click(&e, origin);
+                    },
+                ),
+            )
+            // Context menu
+            .on_click(MouseButton::Right, move |e, cx| {
+                let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx.app) {
+                    conn_handle.update(cx.app, |terminal, _cx| terminal.mouse_mode(e.shift))
+                } else {
+                    // If we can't get the model handle, probably can't deploy the context menu
+                    true
+                };
+                if !mouse_mode {
+                    cx.dispatch_action(DeployContextMenu {
+                        position: e.position,
+                    });
+                }
+            });
+
+        // Mouse mode handlers:
+        // All mouse modes need the extra click handlers
+        if mode.intersects(TermMode::MOUSE_MODE) {
+            region = region
+                .on_down(
+                    MouseButton::Right,
+                    TerminalElement::generic_button_handler(
+                        connection,
+                        origin,
+                        move |terminal, origin, e, _cx| {
+                            terminal.mouse_down(&e, origin);
+                        },
+                    ),
+                )
+                .on_down(
+                    MouseButton::Middle,
+                    TerminalElement::generic_button_handler(
+                        connection,
+                        origin,
+                        move |terminal, origin, e, _cx| {
+                            terminal.mouse_down(&e, origin);
+                        },
+                    ),
+                )
+                .on_up(
+                    MouseButton::Right,
+                    TerminalElement::generic_button_handler(
+                        connection,
+                        origin,
+                        move |terminal, origin, e, _cx| {
+                            terminal.mouse_up(&e, origin);
+                        },
+                    ),
+                )
+                .on_up(
+                    MouseButton::Middle,
+                    TerminalElement::generic_button_handler(
+                        connection,
+                        origin,
+                        move |terminal, origin, e, _cx| {
+                            terminal.mouse_up(&e, origin);
+                        },
+                    ),
+                )
+        }
+        //Mouse move manages both dragging and motion events
+        if mode.intersects(TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION) {
+            region = region
+                //TODO: This does not fire on right-mouse-down-move events.
+                .on_move(move |event, cx| {
+                    if cx.is_parent_view_focused() {
+                        if let Some(conn_handle) = connection.upgrade(cx.app) {
+                            conn_handle.update(cx.app, |terminal, cx| {
+                                terminal.mouse_move(&event, origin);
+                                cx.notify();
+                            })
+                        }
+                    }
+                })
+        }
+
+        cx.scene.push_mouse_region(region);
+    }
+
+    ///Configures a text style from the current settings.
+    pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
+        // Pull the font family from settings properly overriding
+        let family_id = settings
+            .terminal_overrides
+            .font_family
+            .as_ref()
+            .or(settings.terminal_defaults.font_family.as_ref())
+            .and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
+            .unwrap_or(settings.buffer_font_family);
+
+        let font_size = settings
+            .terminal_overrides
+            .font_size
+            .or(settings.terminal_defaults.font_size)
+            .unwrap_or(settings.buffer_font_size);
+
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+
+        TextStyle {
+            color: settings.theme.editor.text_color,
+            font_family_id: family_id,
+            font_family_name: font_cache.family_name(family_id).unwrap(),
+            font_id,
+            font_size,
+            font_properties: Default::default(),
+            underline: Default::default(),
         }
     }
 }
 
-impl Element for TerminalEl {
+impl Element for TerminalElement {
     type LayoutState = LayoutState;
     type PaintState = ();
 
@@ -121,101 +605,134 @@ impl Element for TerminalEl {
         constraint: gpui::SizeConstraint,
         cx: &mut gpui::LayoutContext,
     ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
-        //Settings immutably borrows cx here for the settings and font cache
-        //and we need to modify the cx to resize the terminal. So instead of
-        //storing Settings or the font_cache(), we toss them ASAP and then reborrow later
-        let text_style = make_text_style(cx.font_cache(), cx.global::<Settings>());
-        let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size));
-        let cell_width = CellWidth(
-            cx.font_cache()
-                .em_advance(text_style.font_id, text_style.font_size),
-        );
-        let connection_handle = self.connection.upgrade(cx).unwrap();
-
-        //Tell the view our new size. Requires a mutable borrow of cx and the view
-        let cur_size = make_new_size(constraint, &cell_width, &line_height);
-        //Note that set_size locks and mutates the terminal.
-        connection_handle.update(cx.app, |connection, _| connection.set_size(cur_size));
-
-        let (selection_color, terminal_theme) = {
-            let theme = &(cx.global::<Settings>()).theme;
-            (theme.editor.selection.selection, &theme.terminal)
+        let settings = cx.global::<Settings>();
+        let font_cache = cx.font_cache();
+
+        //Setup layout information
+        let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
+        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);
+            let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
+            TerminalSize::new(line_height, cell_width, constraint.max)
         };
 
-        let terminal_mutex = connection_handle.read(cx).term.clone();
-        let term = terminal_mutex.lock();
-        let grid = term.grid();
-        let cursor_point = grid.cursor.point;
-        let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string();
+        let background_color = if self.modal {
+            terminal_theme.colors.modal_background
+        } else {
+            terminal_theme.colors.background
+        };
 
-        let content = term.renderable_content();
+        let (cells, selection, cursor, display_offset, cursor_text, mode) = self
+            .terminal
+            .upgrade(cx)
+            .unwrap()
+            .update(cx.app, |terminal, mcx| {
+                terminal.set_size(dimensions);
+                terminal.render_lock(mcx, |content, cursor_text| {
+                    let mut cells = vec![];
+                    cells.extend(
+                        content
+                            .display_iter
+                            //TODO: Add this once there's a way to retain empty lines
+                            // .filter(|ic| {
+                            //     !ic.flags.contains(Flags::HIDDEN)
+                            //         && !(ic.bg == Named(NamedColor::Background)
+                            //             && ic.c == ' '
+                            //             && !ic.flags.contains(Flags::INVERSE))
+                            // })
+                            .map(|ic| IndexedCell {
+                                point: ic.point,
+                                cell: ic.cell.clone(),
+                            }),
+                    );
+                    (
+                        cells,
+                        content.selection,
+                        content.cursor,
+                        content.display_offset,
+                        cursor_text,
+                        content.mode,
+                    )
+                })
+            });
 
-        let layout_lines = layout_lines(
-            content.display_iter,
+        let (cells, rects, highlights) = TerminalElement::layout_grid(
+            cells,
             &text_style,
-            terminal_theme,
+            &terminal_theme,
             cx.text_layout_cache,
+            cx.font_cache(),
             self.modal,
-            content.selection,
+            selection,
         );
 
-        let block_text = cx.text_layout_cache.layout_str(
-            &cursor_text,
-            text_style.font_size,
-            &[(
-                cursor_text.len(),
-                RunStyle {
-                    font_id: text_style.font_id,
-                    color: terminal_theme.colors.background,
-                    underline: Default::default(),
-                },
-            )],
-        );
+        //Layout cursor. Rectangle is used for IME, so we should lay it out even
+        //if we don't end up showing it.
+        let cursor = if let AlacCursorShape::Hidden = cursor.shape {
+            None
+        } else {
+            let cursor_point = DisplayCursor::from(cursor.point, display_offset);
+            let cursor_text = {
+                let str_trxt = cursor_text.to_string();
+
+                let color = if self.focused {
+                    terminal_theme.colors.background
+                } else {
+                    terminal_theme.colors.foreground
+                };
 
-        let cursor = get_cursor_shape(
-            content.cursor.point.line.0 as usize,
-            content.cursor.point.column.0 as usize,
-            content.display_offset,
-            &line_height,
-            &cell_width,
-            cur_size.total_lines(),
-            &block_text,
-        )
-        .map(move |(cursor_position, block_width)| {
-            let block_width = if block_width != 0.0 {
-                block_width
-            } else {
-                cell_width.0
+                cx.text_layout_cache.layout_str(
+                    &str_trxt,
+                    text_style.font_size,
+                    &[(
+                        str_trxt.len(),
+                        RunStyle {
+                            font_id: text_style.font_id,
+                            color,
+                            underline: Default::default(),
+                        },
+                    )],
+                )
             };
 
-            Cursor::new(
-                cursor_position,
-                block_width,
-                line_height.0,
-                terminal_theme.colors.cursor,
-                CursorShape::Block,
-                Some(block_text.clone()),
-            )
-        });
-        drop(term);
+            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,
+                        AlacCursorShape::Block => CursorShape::Block,
+                        AlacCursorShape::Underline => CursorShape::Underscore,
+                        AlacCursorShape::Beam => CursorShape::Bar,
+                        AlacCursorShape::HollowBlock => CursorShape::Hollow,
+                        //This case is handled in the if wrapping the whole cursor layout
+                        AlacCursorShape::Hidden => unreachable!(),
+                    };
 
-        let background_color = if self.modal {
-            terminal_theme.colors.modal_background
-        } else {
-            terminal_theme.colors.background
+                    Cursor::new(
+                        cursor_position,
+                        block_width,
+                        dimensions.line_height,
+                        terminal_theme.colors.cursor,
+                        shape,
+                        Some(cursor_text),
+                    )
+                },
+            )
         };
 
+        //Done!
         (
             constraint.max,
             LayoutState {
-                layout_lines,
-                line_height,
-                em_width: cell_width,
+                cells,
                 cursor,
-                cur_size,
                 background_color,
-                terminal: terminal_mutex,
                 selection_color,
+                size: dimensions,
+                rects,
+                highlights,
+                mode,
             },
         )
     }
@@ -231,18 +748,10 @@ impl Element for TerminalEl {
         let clip_bounds = Some(visible_bounds);
 
         cx.paint_layer(clip_bounds, |cx| {
-            let cur_size = layout.cur_size.clone();
-            let origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
+            let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
 
             //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
-            attach_mouse_handlers(
-                origin,
-                cur_size,
-                self.view_id,
-                &layout.terminal,
-                visible_bounds,
-                cx,
-            );
+            self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.mode, cx);
 
             cx.paint_layer(clip_bounds, |cx| {
                 //Start with a background color
@@ -253,99 +762,52 @@ impl Element for TerminalEl {
                     corner_radius: 0.,
                 });
 
-                //Draw cell backgrounds
-                for layout_line in &layout.layout_lines {
-                    for layout_cell in &layout_line.cells {
-                        let position = vec2f(
-                            (origin.x() + layout_cell.point.column as f32 * layout.em_width.0)
-                                .floor(),
-                            origin.y() + layout_cell.point.line as f32 * layout.line_height.0,
-                        );
-                        let size = vec2f(layout.em_width.0.ceil(), layout.line_height.0);
-
-                        cx.scene.push_quad(Quad {
-                            bounds: RectF::new(position, size),
-                            background: Some(layout_cell.background_color),
-                            border: Default::default(),
-                            corner_radius: 0.,
-                        })
-                    }
+                for rect in &layout.rects {
+                    rect.paint(origin, layout, cx)
                 }
             });
 
             //Draw Selection
             cx.paint_layer(clip_bounds, |cx| {
-                let mut highlight_y = None;
-                let highlight_lines = layout
-                    .layout_lines
-                    .iter()
-                    .filter_map(|line| {
-                        if let Some(range) = &line.highlighted_range {
-                            if let None = highlight_y {
-                                highlight_y = Some(
-                                    origin.y()
-                                        + line.cells[0].point.line as f32 * layout.line_height.0,
-                                );
-                            }
-                            let start_x = origin.x()
-                                + line.cells[range.start].point.column as f32 * layout.em_width.0;
-                            let end_x = origin.x()
-                                + line.cells[range.end].point.column as f32 * layout.em_width.0
-                                + layout.em_width.0;
-
-                            return Some(HighlightedRangeLine { start_x, end_x });
-                        } else {
-                            return None;
-                        }
-                    })
-                    .collect::<Vec<HighlightedRangeLine>>();
+                let start_y = layout.highlights.get(0).map(|highlight| {
+                    origin.y() + highlight.line_index as f32 * layout.size.line_height
+                });
+
+                if let Some(y) = start_y {
+                    let range_lines = layout
+                        .highlights
+                        .iter()
+                        .map(|relative_highlight| {
+                            relative_highlight.to_highlighted_range_line(origin, layout)
+                        })
+                        .collect::<Vec<HighlightedRangeLine>>();
 
-                if let Some(y) = highlight_y {
                     let hr = HighlightedRange {
                         start_y: y, //Need to change this
-                        line_height: layout.line_height.0,
-                        lines: highlight_lines,
+                        line_height: layout.size.line_height,
+                        lines: range_lines,
                         color: layout.selection_color,
                         //Copied from editor. TODO: move to theme or something
-                        corner_radius: 0.15 * layout.line_height.0,
+                        corner_radius: 0.15 * layout.size.line_height,
                     };
                     hr.paint(bounds, cx.scene);
                 }
             });
 
+            //Draw the text cells
             cx.paint_layer(clip_bounds, |cx| {
-                for layout_line in &layout.layout_lines {
-                    for layout_cell in &layout_line.cells {
-                        let point = layout_cell.point;
-
-                        //Don't actually know the start_x for a line, until here:
-                        let cell_origin = vec2f(
-                            (origin.x() + point.column as f32 * layout.em_width.0).floor(),
-                            origin.y() + point.line as f32 * layout.line_height.0,
-                        );
-
-                        layout_cell.text.paint(
-                            cell_origin,
-                            visible_bounds,
-                            layout.line_height.0,
-                            cx,
-                        );
-                    }
+                for cell in &layout.cells {
+                    cell.paint(origin, layout, visible_bounds, cx);
                 }
             });
 
             //Draw cursor
-            if let Some(cursor) = &layout.cursor {
-                cx.paint_layer(clip_bounds, |cx| {
-                    cursor.paint(origin, cx);
-                })
-            }
-
-            #[cfg(debug_assertions)]
-            if DEBUG_GRID {
-                cx.paint_layer(clip_bounds, |cx| {
-                    draw_debug_grid(bounds, layout, cx);
-                })
+            if self.cursor_visible {
+                if let Some(cursor) = &layout.cursor {
+                    cx.paint_layer(clip_bounds, |cx| {
+                        cursor.paint(origin, cx);
+                    })
+                }
             }
         });
     }

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
     }
 }