diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/connected_el.rs similarity index 57% rename from crates/terminal/src/terminal_element.rs rename to crates/terminal/src/connected_el.rs index 89618a9f3d..506e846e93 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/connected_el.rs @@ -1,5 +1,3 @@ -pub mod terminal_layout_context; - use alacritty_terminal::{ ansi::{Color::Named, NamedColor}, event::WindowSize, @@ -20,8 +18,7 @@ use gpui::{ json::json, text_layout::{Line, RunStyle}, Event, FontCache, KeyDownEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, - PaintContext, Quad, ScrollWheelEvent, SizeConstraint, TextLayoutCache, WeakModelHandle, - WeakViewHandle, + PaintContext, Quad, ScrollWheelEvent, TextLayoutCache, WeakModelHandle, WeakViewHandle, }; use itertools::Itertools; use ordered_float::OrderedFloat; @@ -32,34 +29,58 @@ use util::ResultExt; use std::{cmp::min, ops::Range}; use std::{fmt::Debug, ops::Sub}; -use crate::{color_translation::convert_color, connection::Terminal, ConnectedView}; - -use self::terminal_layout_context::TerminalLayoutData; +use crate::{mappings::colors::convert_color, model::Terminal, ConnectedView}; ///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.; -///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, - view: WeakViewHandle, - modal: bool, +///The information generated during layout that is nescessary for painting +pub struct LayoutState { + cells: Vec, + rects: Vec, + highlights: Vec, + cursor: Option, + background_color: Color, + selection_color: Color, + size: TermDimensions, +} + +///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, Copy, Debug)] -pub struct TerminalDimensions { - pub cell_width: f32, - pub line_height: f32, - pub height: f32, - pub width: f32, +pub struct TermDimensions { + cell_width: f32, + line_height: f32, + height: f32, + width: f32, } -impl TerminalDimensions { +impl TermDimensions { pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self { - TerminalDimensions { + TermDimensions { cell_width, line_height, width: size.x(), @@ -92,8 +113,7 @@ impl TerminalDimensions { } } -//TODO look at what TermSize is -impl Into for TerminalDimensions { +impl Into for TermDimensions { fn into(self) -> WindowSize { WindowSize { num_lines: self.num_lines() as u16, @@ -104,9 +124,9 @@ impl Into for TerminalDimensions { } } -impl Dimensions for TerminalDimensions { +impl Dimensions for TermDimensions { fn total_lines(&self) -> usize { - self.num_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer... + self.screen_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer... } fn screen_lines(&self) -> usize { @@ -214,15 +234,12 @@ impl RelativeHighlightedRange { } } -///The information generated during layout that is nescessary for painting -pub struct LayoutState { - cells: Vec, - rects: Vec, - highlights: Vec, - cursor: Option, - background_color: Color, - selection_color: Color, - size: TerminalDimensions, +///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, + view: WeakViewHandle, + modal: bool, } impl TerminalEl { @@ -238,12 +255,173 @@ impl TerminalEl { } } + fn layout_grid( + grid: GridIterator, + text_style: &TextStyle, + terminal_theme: &TerminalStyle, + text_layout_cache: &TextLayoutCache, + modal: bool, + selection_range: Option, + ) -> ( + Vec, + Vec, + Vec, + ) { + let mut cells = vec![]; + let mut rects = vec![]; + let mut highlight_ranges = vec![]; + + let mut cur_rect: Option = None; + let mut cur_alac_color = None; + let mut highlighted_range = None; + + let linegroups = grid.group_by(|i| i.point.line); + for (line_index, (_, line)) in linegroups.into_iter().enumerate() { + for (x_index, cell) in line.enumerate() { + //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!(cell.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 cell.bg == cur_color { + cur_rect = cur_rect.take().map(|rect| rect.extend()); + } else { + cur_alac_color = Some(cell.bg); + if let Some(_) = cur_rect { + 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(&cell.bg, &terminal_theme.colors, modal), + )); + } + } + None => { + cur_alac_color = Some(cell.bg); + cur_rect = Some(LayoutRect::new( + Point::new(line_index as i32, cell.point.column.0 as i32), + 1, + convert_color(&cell.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, terminal_theme, text_style, 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: TermDimensions, + 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() + }; + + Some(( + vec2f( + cursor_point.col() as f32 * size.cell_width(), + cursor_point.line() as f32 * size.line_height(), + ), + cursor_width, + )) + } else { + None + } + } + + ///Convert the Alacritty cell styles to GPUI text styles and background color + fn cell_style( + indexed: &Indexed<&Cell>, + style: &TerminalStyle, + text_style: &TextStyle, + modal: bool, + ) -> RunStyle { + let flags = indexed.cell.flags; + let fg = convert_color(&indexed.cell.fg, &style.colors, modal); + + let underline = flags + .contains(Flags::UNDERLINE) + .then(|| Underline { + color: Some(fg), + squiggly: false, + thickness: OrderedFloat(1.), + }) + .unwrap_or_default(); + + RunStyle { + color: fg, + font_id: text_style.font_id, + underline, + } + } + fn attach_mouse_handlers( &self, origin: Vector2F, view_id: usize, visible_bounds: RectF, - cur_size: TerminalDimensions, + cur_size: TermDimensions, cx: &mut PaintContext, ) { let mouse_down_connection = self.terminal.clone(); @@ -256,7 +434,7 @@ impl TerminalEl { move |MouseButtonEvent { position, .. }, cx| { if let Some(conn_handle) = mouse_down_connection.upgrade(cx.app) { conn_handle.update(cx.app, |terminal, cx| { - let (point, side) = mouse_to_cell_data( + let (point, side) = TerminalEl::mouse_to_cell_data( position, origin, cur_size, @@ -281,7 +459,7 @@ impl TerminalEl { cx.focus_parent_view(); if let Some(conn_handle) = click_connection.upgrade(cx.app) { conn_handle.update(cx.app, |terminal, cx| { - let (point, side) = mouse_to_cell_data( + let (point, side) = TerminalEl::mouse_to_cell_data( position, origin, cur_size, @@ -300,7 +478,7 @@ impl TerminalEl { move |_, MouseMovedEvent { position, .. }, cx| { if let Some(conn_handle) = drag_connection.upgrade(cx.app) { conn_handle.update(cx.app, |terminal, cx| { - let (point, side) = mouse_to_cell_data( + let (point, side) = TerminalEl::mouse_to_cell_data( position, origin, cur_size, @@ -316,6 +494,79 @@ impl TerminalEl { ), ); } + + ///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_else(|| 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(), + } + } + + pub fn mouse_to_cell_data( + pos: Vector2F, + origin: Vector2F, + cur_size: TermDimensions, + display_offset: usize, + ) -> (Point, alacritty_terminal::index::Direction) { + let pos = pos.sub(origin); + let point = { + let col = pos.x() / cur_size.cell_width; //TODO: underflow... + let col = min(GridCol(col as usize), cur_size.last_column()); + + let line = pos.y() / cur_size.line_height; + let line = min(line as i32, cur_size.bottommost_line().0); + + Point::new(GridLine(line - display_offset as i32), col) + }; + + //Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side() + let side = { + let x = pos.0.x() as usize; + let cell_x = + x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize; + let half_cell_width = (cur_size.cell_width / 2.0) as usize; + + let additional_padding = + (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width; + let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding; + //Width: Pixels or columns? + if cell_x > half_cell_width + // Edge case when mouse leaves the window. + || x as f32 >= end_of_grid + { + Side::Right + } else { + Side::Left + } + }; + + (point, side) + } } impl Element for TerminalEl { @@ -327,40 +578,74 @@ impl Element for TerminalEl { constraint: gpui::SizeConstraint, cx: &mut gpui::LayoutContext, ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - let layout = - TerminalLayoutData::new(cx.global::(), &cx.font_cache(), constraint.max); + let settings = cx.global::(); + let font_cache = &cx.font_cache(); + + //Setup layout information + let terminal_theme = &settings.theme.terminal; + 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); + TermDimensions::new(line_height, cell_width, constraint.max) + }; let terminal = self.terminal.upgrade(cx).unwrap().read(cx); let (cursor, cells, rects, highlights) = - terminal.render_lock(Some(layout.size.clone()), |content, cursor_text| { - let (cells, rects, highlights) = layout_grid( + terminal.render_lock(Some(dimensions.clone()), |content, cursor_text| { + let (cells, rects, highlights) = TerminalEl::layout_grid( content.display_iter, - &layout.text_style, - layout.terminal_theme, + &text_style, + terminal_theme, cx.text_layout_cache, self.modal, content.selection, ); //Layout cursor - let cursor = layout_cursor( - cursor_text, - cx.text_layout_cache, - &layout, - content.cursor.point, - content.display_offset, - constraint, - ); + let cursor = { + let cursor_point = + DisplayCursor::from(content.cursor.point, content.display_offset); + let cursor_text = { + let str_trxt = cursor_text.to_string(); + cx.text_layout_cache.layout_str( + &str_trxt, + text_style.font_size, + &[( + str_trxt.len(), + RunStyle { + font_id: text_style.font_id, + color: terminal_theme.colors.background, + underline: Default::default(), + }, + )], + ) + }; + + TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map( + move |(cursor_position, block_width)| { + Cursor::new( + cursor_position, + block_width, + dimensions.line_height, + terminal_theme.colors.cursor, + CursorShape::Block, + Some(cursor_text.clone()), + ) + }, + ) + }; (cursor, cells, rects, highlights) }); //Select background color let background_color = if self.modal { - layout.terminal_theme.colors.modal_background + terminal_theme.colors.modal_background } else { - layout.terminal_theme.colors.background + terminal_theme.colors.background }; //Done! @@ -370,8 +655,8 @@ impl Element for TerminalEl { cells, cursor, background_color, - selection_color: layout.selection_color, - size: layout.size, + selection_color, + size: dimensions, rects, highlights, }, @@ -385,13 +670,6 @@ impl Element for TerminalEl { layout: &mut Self::LayoutState, cx: &mut gpui::PaintContext, ) -> Self::PaintState { - /* - * For paint, I want to change how mouse events are handled: - * - Refactor the mouse handlers to push the grid cell actions into the connection - * - But keep the conversion from GPUI coordinates to grid cells in the Terminal element - * - Switch from directly painting things, to calling 'paint' on items produced by layout - */ - //Setup element stuff let clip_bounds = Some(visible_bounds); @@ -520,270 +798,6 @@ impl Element for TerminalEl { } } -///TODO: Fix cursor rendering with alacritty fork -fn layout_cursor( - cursor_text: char, - text_layout_cache: &TextLayoutCache, - tcx: &TerminalLayoutData, - cursor_point: Point, - display_offset: usize, - constraint: SizeConstraint, -) -> Option { - let cursor_text = layout_cursor_text(cursor_text, cursor_point, text_layout_cache, tcx); - get_cursor_shape( - cursor_point.line.0 as usize, - cursor_point.column.0 as usize, - display_offset, - tcx.size.line_height, - tcx.size.cell_width, - (constraint.max.y() / tcx.size.line_height) as usize, //TODO - &cursor_text, - ) - .map(move |(cursor_position, block_width)| { - let block_width = if block_width != 0.0 { - block_width - } else { - tcx.size.cell_width - }; - - Cursor::new( - cursor_position, - block_width, - tcx.size.line_height, - tcx.terminal_theme.colors.cursor, - CursorShape::Block, - Some(cursor_text.clone()), - ) - }) -} - -fn layout_cursor_text( - cursor_text: char, - _cursor_point: Point, - text_layout_cache: &TextLayoutCache, - tcx: &TerminalLayoutData, -) -> Line { - let cursor_text = cursor_text.to_string(); - text_layout_cache.layout_str( - &cursor_text, - tcx.text_style.font_size, - &[( - cursor_text.len(), - RunStyle { - font_id: tcx.text_style.font_id, - color: tcx.terminal_theme.colors.background, - underline: Default::default(), - }, - )], - ) -} - -pub fn mouse_to_cell_data( - pos: Vector2F, - origin: Vector2F, - cur_size: TerminalDimensions, - display_offset: usize, -) -> (Point, alacritty_terminal::index::Direction) { - let pos = pos.sub(origin); - let point = { - let col = pos.x() / cur_size.cell_width; //TODO: underflow... - let col = min(GridCol(col as usize), cur_size.last_column()); - - let line = pos.y() / cur_size.line_height; - let line = min(line as i32, cur_size.bottommost_line().0); - - Point::new(GridLine(line - display_offset as i32), col) - }; - - //Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side() - let side = { - let x = pos.0.x() as usize; - let cell_x = x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize; - let half_cell_width = (cur_size.cell_width / 2.0) as usize; - - let additional_padding = - (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width; - let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding; - //Width: Pixels or columns? - if cell_x > half_cell_width - // Edge case when mouse leaves the window. - || x as f32 >= end_of_grid - { - Side::Right - } else { - Side::Left - } - }; - - (point, side) -} - -fn layout_grid( - grid: GridIterator, - text_style: &TextStyle, - terminal_theme: &TerminalStyle, - text_layout_cache: &TextLayoutCache, - modal: bool, - selection_range: Option, -) -> ( - Vec, - Vec, - Vec, -) { - let mut cells = vec![]; - let mut rects = vec![]; - let mut highlight_ranges = vec![]; - - let mut cur_rect: Option = None; - let mut cur_alac_color = None; - let mut highlighted_range = None; - - let linegroups = grid.group_by(|i| i.point.line); - for (line_index, (_, line)) in linegroups.into_iter().enumerate() { - for (x_index, cell) in line.enumerate() { - //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!(cell.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 cell.bg == cur_color { - cur_rect = cur_rect.take().map(|rect| rect.extend()); - } else { - cur_alac_color = Some(cell.bg); - if let Some(_) = cur_rect { - 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(&cell.bg, &terminal_theme.colors, modal), - )); - } - } - None => { - cur_alac_color = Some(cell.bg); - cur_rect = Some(LayoutRect::new( - Point::new(line_index as i32, cell.point.column.0 as i32), - 1, - convert_color(&cell.bg, &terminal_theme.colors, modal), - )); - } - } - } - } - - //Layout current cell text - { - let cell_text = &cell.c.to_string(); - if cell_text != " " { - let cell_style = cell_style(&cell, terminal_theme, text_style, 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 -//TODO: This function is messy, too many arguments and too many ifs. Simplify. -fn get_cursor_shape( - line: usize, - line_index: usize, - display_offset: usize, - line_height: f32, - cell_width: f32, - total_lines: usize, - text_fragment: &Line, -) -> Option<(Vector2F, f32)> { - let cursor_line = line + display_offset; - if cursor_line <= total_lines { - let cursor_width = if text_fragment.width() == 0. { - cell_width - } else { - text_fragment.width() - }; - - Some(( - vec2f( - line_index as f32 * cell_width, - cursor_line as f32 * line_height, - ), - cursor_width, - )) - } else { - None - } -} - -///Convert the Alacritty cell styles to GPUI text styles and background color -fn cell_style( - indexed: &Indexed<&Cell>, - style: &TerminalStyle, - text_style: &TextStyle, - modal: bool, -) -> RunStyle { - let flags = indexed.cell.flags; - let fg = convert_color(&indexed.cell.fg, &style.colors, modal); - - let underline = flags - .contains(Flags::UNDERLINE) - .then(|| Underline { - color: Some(fg), - squiggly: false, - thickness: OrderedFloat(1.), - }) - .unwrap_or_default(); - - RunStyle { - color: fg, - font_id: text_style.font_id, - underline, - } -} - mod test { #[test] @@ -797,7 +811,7 @@ mod test { let origin_x = 10.; let origin_y = 20.; - let cur_size = crate::terminal_element::TerminalDimensions::new( + let cur_size = crate::connected_el::TermDimensions::new( line_height, cell_width, gpui::geometry::vector::vec2f(term_width, term_height), @@ -806,7 +820,7 @@ mod test { let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y); let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in let (point, _) = - crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0); + crate::connected_el::TerminalEl::mouse_to_cell_data(mouse_pos, origin, cur_size, 0); assert_eq!( point, alacritty_terminal::index::Point::new( diff --git a/crates/terminal/src/connected_view.rs b/crates/terminal/src/connected_view.rs new file mode 100644 index 0000000000..633bd70b5a --- /dev/null +++ b/crates/terminal/src/connected_view.rs @@ -0,0 +1,162 @@ +use gpui::{ + actions, keymap::Keystroke, ClipboardItem, Element, ElementBox, ModelHandle, MutableAppContext, + View, ViewContext, +}; + +use crate::{ + connected_el::TerminalEl, + model::{Event, Terminal}, +}; + +///Event to transmit the scroll from the element to the view +#[derive(Clone, Debug, PartialEq)] +pub struct ScrollTerminal(pub i32); + +actions!( + terminal, + [Up, Down, CtrlC, Escape, Enter, Clear, Copy, Paste,] +); + +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::copy); + cx.add_action(ConnectedView::paste); + cx.add_action(ConnectedView::clear); +} + +///A terminal view, maintains the PTY's file handles and communicates with the terminal +pub struct ConnectedView { + terminal: ModelHandle, + 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, +} + +impl ConnectedView { + pub fn from_terminal( + terminal: ModelHandle, + modal: bool, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); + cx.subscribe(&terminal, |this, _, event, cx| match event { + Event::Wakeup => { + if cx.is_self_focused() { + cx.notify() + } else { + this.has_new_content = true; + cx.emit(Event::TitleChanged); + } + } + Event::Bell => { + this.has_bell = true; + cx.emit(Event::TitleChanged); + } + _ => cx.emit(*event), + }) + .detach(); + + Self { + terminal, + has_new_content: true, + has_bell: false, + modal, + } + } + + pub fn handle(&self) -> ModelHandle { + 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) { + self.has_bell = false; + cx.emit(Event::TitleChanged); + } + + fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { + self.terminal.read(cx).clear(); + } + + ///Attempt to paste the clipboard into the terminal + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + self.terminal + .read(cx) + .copy() + .map(|text| cx.write_to_clipboard(ClipboardItem::new(text))); + } + + ///Attempt to paste the clipboard into the terminal + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + cx.read_from_clipboard().map(|item| { + self.terminal.read(cx).paste(item.text()); + }); + } + + ///Synthesize the keyboard event corresponding to 'up' + fn up(&mut self, _: &Up, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("up").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'down' + fn down(&mut self, _: &Down, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("down").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'ctrl-c' + fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("ctrl-c").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'escape' + fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("escape").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'enter' + fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("enter").unwrap()); + } +} + +impl View for ConnectedView { + fn ui_name() -> &'static str { + "Connected Terminal View" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let terminal_handle = self.terminal.clone().downgrade(); + TerminalEl::new(cx.handle(), terminal_handle, self.modal) + .contained() + .boxed() + } + + fn on_focus(&mut self, _cx: &mut ViewContext) { + self.has_new_content = false; + } +} diff --git a/crates/terminal/src/color_translation.rs b/crates/terminal/src/mappings/colors.rs similarity index 98% rename from crates/terminal/src/color_translation.rs rename to crates/terminal/src/mappings/colors.rs index 946a22d304..1a425ebaed 100644 --- a/crates/terminal/src/color_translation.rs +++ b/crates/terminal/src/mappings/colors.rs @@ -133,7 +133,7 @@ mod tests { fn test_rgb_for_index() { //Test every possible value in the color cube for i in 16..=231 { - let (r, g, b) = crate::color_translation::rgb_for_index(&(i as u8)); + let (r, g, b) = crate::mappings::colors::rgb_for_index(&(i as u8)); assert_eq!(i, 16 + 36 * r + 6 * g + b); } } diff --git a/crates/terminal/src/connection/keymappings.rs b/crates/terminal/src/mappings/keys.rs similarity index 99% rename from crates/terminal/src/connection/keymappings.rs rename to crates/terminal/src/mappings/keys.rs index a4d429843b..51d02d6bb2 100644 --- a/crates/terminal/src/connection/keymappings.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -1,15 +1,6 @@ use alacritty_terminal::term::TermMode; use gpui::keymap::Keystroke; -/* -Connection events still to do: -- Reporting mouse events correctly. -- Reporting scrolls -- Correctly bracketing a paste -- Storing changed colors -- Focus change sequence -*/ - #[derive(Debug)] pub enum Modifiers { None, diff --git a/crates/terminal/src/mappings/mod.rs b/crates/terminal/src/mappings/mod.rs new file mode 100644 index 0000000000..cde6c337ea --- /dev/null +++ b/crates/terminal/src/mappings/mod.rs @@ -0,0 +1,2 @@ +pub mod colors; +pub mod keys; diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal_view.rs similarity index 91% rename from crates/terminal/src/modal.rs rename to crates/terminal/src/modal_view.rs index c53f2a0469..ec5280befc 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal_view.rs @@ -2,7 +2,7 @@ use gpui::{ModelHandle, ViewContext}; use workspace::Workspace; use crate::{ - connection::Terminal, get_working_directory, DeployModal, Event, TerminalContent, TerminalView, + get_working_directory, model::Terminal, DeployModal, Event, TerminalContent, TerminalView, }; #[derive(Debug)] @@ -32,7 +32,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon let this = cx.add_view(|cx| TerminalView::new(working_directory, true, cx)); if let TerminalContent::Connected(connected) = &this.read(cx).content { - let terminal_handle = connected.read(cx).terminal.clone(); + let terminal_handle = connected.read(cx).handle(); cx.subscribe(&terminal_handle, on_event).detach(); // Set the global immediately if terminal construction was successful, // in case the user opens the command palette @@ -46,7 +46,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon // Terminal modal was dismissed. Store terminal if the terminal view is connected if let TerminalContent::Connected(connected) = &closed_terminal_handle.read(cx).content { - let terminal_handle = connected.read(cx).terminal.clone(); + let terminal_handle = connected.read(cx).handle(); // Set the global immediately if terminal construction was successful, // in case the user opens the command palette cx.set_global::>(Some(StoredTerminal( diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/model.rs similarity index 98% rename from crates/terminal/src/connection.rs rename to crates/terminal/src/model.rs index d41f4e8fad..852dc555c5 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/model.rs @@ -1,5 +1,3 @@ -mod keymappings; - use alacritty_terminal::{ ansi::{ClearMode, Handler}, config::{Config, Program, PtyConfig}, @@ -25,12 +23,13 @@ use thiserror::Error; use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext}; use crate::{ - color_translation::{get_color_at_index, to_alac_rgb}, - terminal_element::TerminalDimensions, + connected_el::TermDimensions, + mappings::{ + colors::{get_color_at_index, to_alac_rgb}, + keys::to_esc_str, + }, }; -use self::keymappings::to_esc_str; - const DEFAULT_TITLE: &str = "Terminal"; ///Upward flowing events, for changing the title and such @@ -132,7 +131,7 @@ impl TerminalBuilder { working_directory: Option, shell: Option, env: Option>, - initial_size: TerminalDimensions, + initial_size: TermDimensions, ) -> Result { let pty_config = { let alac_shell = shell.clone().and_then(|shell| match shell { @@ -364,7 +363,7 @@ impl Terminal { self.term.lock().selection = sel; } - pub fn render_lock(&self, new_size: Option, f: F) -> T + pub fn render_lock(&self, new_size: Option, f: F) -> T where F: FnOnce(RenderableContent, char) -> T, { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index ab543a22dc..ca10506cdd 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,64 +1,40 @@ -mod color_translation; -pub mod connection; -mod modal; -pub mod terminal_element; +pub mod connected_el; +pub mod connected_view; +pub mod mappings; +pub mod modal_view; +pub mod model; -use connection::{Event, Terminal, TerminalBuilder, TerminalError}; +use connected_view::ConnectedView; use dirs::home_dir; use gpui::{ - actions, elements::*, geometry::vector::vec2f, keymap::Keystroke, AnyViewHandle, AppContext, - ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, + actions, elements::*, geometry::vector::vec2f, AnyViewHandle, AppContext, Entity, ModelHandle, + MutableAppContext, View, ViewContext, ViewHandle, }; -use modal::deploy_modal; +use modal_view::deploy_modal; +use model::{Event, Terminal, TerminalBuilder, TerminalError}; +use connected_el::TermDimensions; use project::{LocalWorktree, Project, ProjectPath}; use settings::{Settings, WorkingDirectory}; use smallvec::SmallVec; use std::path::{Path, PathBuf}; -use terminal_element::{terminal_layout_context::TerminalLayoutData, TerminalDimensions}; use workspace::{Item, Workspace}; -use crate::terminal_element::TerminalEl; +use crate::connected_el::TerminalEl; const DEBUG_TERMINAL_WIDTH: f32 = 1000.; //This needs to be wide enough that the prompt can fill the whole space. const DEBUG_TERMINAL_HEIGHT: f32 = 200.; const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; -///Event to transmit the scroll from the element to the view -#[derive(Clone, Debug, PartialEq)] -pub struct ScrollTerminal(pub i32); - -actions!( - terminal, - [ - Deploy, - Up, - Down, - CtrlC, - Escape, - Enter, - Clear, - Copy, - Paste, - DeployModal - ] -); +actions!(terminal, [Deploy, DeployModal]); ///Initialize and register all of our action handlers 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 actions - cx.add_action(ConnectedView::deploy); - cx.add_action(ConnectedView::copy); - cx.add_action(ConnectedView::paste); - cx.add_action(ConnectedView::clear); + cx.add_action(TerminalView::deploy); cx.add_action(deploy_modal); + + connected_view::init(cx); } //Make terminal view an enum, that can give you views for the error and non-error states @@ -88,16 +64,6 @@ pub struct ErrorView { error: TerminalError, } -///A terminal view, maintains the PTY's file handles and communicates with the terminal -pub struct ConnectedView { - terminal: ModelHandle, - 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, -} - impl Entity for TerminalView { type Event = Event; } @@ -111,11 +77,18 @@ impl Entity for ErrorView { } impl TerminalView { + ///Create a new Terminal in the current working directory or the user's home directory + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + let working_directory = get_working_directory(workspace, cx); + let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx)); + workspace.add_item(Box::new(view), cx); + } + ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices ///To get the right working directory from a workspace, use: `get_wd_for_workspace()` fn new(working_directory: Option, modal: bool, cx: &mut ViewContext) -> Self { //The details here don't matter, the terminal will be resized on the first layout - let size_info = TerminalDimensions::new( + let size_info = TermDimensions::new( DEBUG_LINE_HEIGHT, DEBUG_CELL_WIDTH, vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), @@ -194,122 +167,6 @@ impl View for TerminalView { } } -impl ConnectedView { - fn from_terminal( - terminal: ModelHandle, - modal: bool, - cx: &mut ViewContext, - ) -> Self { - cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&terminal, |this, _, event, cx| match event { - Event::Wakeup => { - if cx.is_self_focused() { - cx.notify() - } else { - this.has_new_content = true; - cx.emit(Event::TitleChanged); - } - } - Event::Bell => { - this.has_bell = true; - cx.emit(Event::TitleChanged); - } - _ => cx.emit(*event), - }) - .detach(); - - Self { - terminal, - has_new_content: true, - has_bell: false, - modal, - } - } - - fn clear_bel(&mut self, cx: &mut ViewContext) { - self.has_bell = false; - cx.emit(Event::TitleChanged); - } - - ///Create a new Terminal in the current working directory or the user's home directory - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - let working_directory = get_working_directory(workspace, cx); - let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx)); - workspace.add_item(Box::new(view), cx); - } - - fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { - self.terminal.read(cx).clear(); - } - - ///Attempt to paste the clipboard into the terminal - fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - self.terminal - .read(cx) - .copy() - .map(|text| cx.write_to_clipboard(ClipboardItem::new(text))); - } - - ///Attempt to paste the clipboard into the terminal - fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { - cx.read_from_clipboard().map(|item| { - self.terminal.read(cx).paste(item.text()); - }); - } - - ///Synthesize the keyboard event corresponding to 'up' - fn up(&mut self, _: &Up, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("up").unwrap()); - } - - ///Synthesize the keyboard event corresponding to 'down' - fn down(&mut self, _: &Down, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("down").unwrap()); - } - - ///Synthesize the keyboard event corresponding to 'ctrl-c' - fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("ctrl-c").unwrap()); - } - - ///Synthesize the keyboard event corresponding to 'escape' - fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("escape").unwrap()); - } - - ///Synthesize the keyboard event corresponding to 'enter' - fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("enter").unwrap()); - } -} - -impl View for ConnectedView { - fn ui_name() -> &'static str { - "Connected Terminal View" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let terminal_handle = self.terminal.clone().downgrade(); - TerminalEl::new(cx.handle(), terminal_handle, self.modal) - .contained() - .boxed() - } - - fn on_focus(&mut self, _cx: &mut ViewContext) { - self.has_new_content = false; - } -} - impl View for ErrorView { fn ui_name() -> &'static str { "Terminal Error" @@ -317,7 +174,7 @@ impl View for ErrorView { fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { let settings = cx.global::(); - let style = TerminalLayoutData::make_text_style(cx.font_cache(), settings); + let style = TerminalEl::make_text_style(cx.font_cache(), settings); Label::new( format!( @@ -341,7 +198,7 @@ impl Item for TerminalView { ) -> ElementBox { let title = match &self.content { TerminalContent::Connected(connected) => { - connected.read(cx).terminal.read(cx).title.clone() + connected.read(cx).handle().read(cx).title.clone() } TerminalContent::Error(_) => "Terminal".to_string(), }; @@ -363,7 +220,7 @@ impl Item for TerminalView { if let TerminalContent::Connected(connected) = &self.content { let associated_directory = connected .read(cx) - .terminal + .handle() .read(cx) .associated_directory .clone(); @@ -418,7 +275,7 @@ impl Item for TerminalView { fn is_dirty(&self, cx: &gpui::AppContext) -> bool { if let TerminalContent::Connected(connected) = &self.content { - connected.read(cx).has_new_content + connected.read(cx).has_new_content() } else { false } @@ -426,7 +283,7 @@ impl Item for TerminalView { fn has_conflict(&self, cx: &AppContext) -> bool { if let TerminalContent::Connected(connected) = &self.content { - connected.read(cx).has_bell + connected.read(cx).has_bell() } else { false } diff --git a/crates/terminal/src/terminal_element/terminal_layout_context.rs b/crates/terminal/src/terminal_element/terminal_layout_context.rs deleted file mode 100644 index 6128d17828..0000000000 --- a/crates/terminal/src/terminal_element/terminal_layout_context.rs +++ /dev/null @@ -1,60 +0,0 @@ -use super::*; - -pub struct TerminalLayoutData<'a> { - pub text_style: TextStyle, - pub selection_color: Color, - pub terminal_theme: &'a TerminalStyle, - pub size: TerminalDimensions, -} - -impl<'a> TerminalLayoutData<'a> { - pub fn new(settings: &'a Settings, font_cache: &FontCache, constraint: Vector2F) -> Self { - let text_style = Self::make_text_style(font_cache, &settings); - let selection_color = settings.theme.editor.selection.selection; - let terminal_theme = &settings.theme.terminal; - - 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); - let dimensions = TerminalDimensions::new(line_height, cell_width, constraint); - - TerminalLayoutData { - size: dimensions, - text_style, - selection_color, - terminal_theme, - } - } - - ///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_else(|| 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(), - } - } -} diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index f9b99d60d8..e78939224b 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -8,8 +8,8 @@ use project::{Entry, Project, ProjectPath, Worktree}; use workspace::{AppState, Workspace}; use crate::{ - connection::{Terminal, TerminalBuilder}, - terminal_element::TerminalDimensions, + connected_el::TermDimensions, + model::{Terminal, TerminalBuilder}, DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, DEBUG_TERMINAL_WIDTH, }; @@ -22,7 +22,7 @@ impl<'a> TerminalTestContext<'a> { pub fn new(cx: &'a mut TestAppContext, term: bool) -> Self { cx.set_condition_duration(Some(Duration::from_secs(5))); - let size_info = TerminalDimensions::new( + let size_info = TermDimensions::new( DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),