diff options
author | Christian Duerr <contact@christianduerr.com> | 2020-03-18 02:35:08 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-18 02:35:08 +0000 |
commit | 1a8cd172e520e493bacc9c6a2ae6f80de086eaa3 (patch) | |
tree | 0b837f1f52f72fe00e258afc34094d60b5d18f04 /alacritty_terminal/src/term | |
parent | 64db7d3daaed4e06fb8292227622bbc4cdaa2cf0 (diff) | |
download | alacritty-1a8cd172e520e493bacc9c6a2ae6f80de086eaa3.tar.gz alacritty-1a8cd172e520e493bacc9c6a2ae6f80de086eaa3.zip |
Add modal keyboard motion mode
This implements a basic mode for navigating inside of Alacritty's
history with keyboard bindings. They're bound by default to vi's motion
shortcuts but are fully customizable. Since this relies on key bindings
only single key bindings are currently supported (so no `ge`, or
repetition).
Other than navigating the history and moving the viewport, this mode
should enable making use of all available selection modes to copy
content to the clipboard and launch URLs below the cursor.
This also changes the rendering of the block cursor at the side of
selections, since previously it could be inverted to be completely
invisible. Since that would have caused some troubles with this keyboard
selection mode, the block cursor now is no longer inverted when it is at
the edges of a selection.
Fixes #262.
Diffstat (limited to 'alacritty_terminal/src/term')
-rw-r--r-- | alacritty_terminal/src/term/mod.rs | 456 |
1 files changed, 295 insertions, 161 deletions
diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index ac5e56b5..89c3723f 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -31,10 +31,11 @@ use crate::event::{Event, EventListener}; use crate::grid::{ BidirectionalIterator, DisplayIter, Grid, GridCell, IndexRegion, Indexed, Scroll, }; -use crate::index::{self, Column, IndexRange, Line, Point}; +use crate::index::{self, Column, IndexRange, Line, Point, Side}; use crate::selection::{Selection, SelectionRange}; use crate::term::cell::{Cell, Flags, LineLength}; use crate::term::color::Rgb; +use crate::vi_mode::{ViModeCursor, ViMotion}; pub mod cell; pub mod color; @@ -180,7 +181,17 @@ impl<T> Search for Term<T> { } } -/// A key for caching cursor glyphs +/// Cursor storing all information relevant for rendering. +#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)] +struct RenderableCursor { + text_color: Option<Rgb>, + cursor_color: Option<Rgb>, + key: CursorKey, + point: Point, + rendered: bool, +} + +/// A key for caching cursor glyphs. #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)] pub struct CursorKey { pub style: CursorStyle, @@ -198,10 +209,7 @@ pub struct CursorKey { pub struct RenderableCellsIter<'a, C> { inner: DisplayIter<'a, Cell>, grid: &'a Grid<Cell>, - cursor: &'a Point, - cursor_offset: usize, - cursor_key: Option<CursorKey>, - cursor_style: CursorStyle, + cursor: RenderableCursor, config: &'a Config<C>, colors: &'a color::List, selection: Option<SelectionRange<Line>>, @@ -216,12 +224,10 @@ impl<'a, C> RenderableCellsIter<'a, C> { term: &'b Term<T>, config: &'b Config<C>, selection: Option<SelectionRange>, - mut cursor_style: CursorStyle, ) -> RenderableCellsIter<'b, C> { let grid = &term.grid; let num_cols = grid.num_cols(); - let cursor_offset = grid.num_lines().0 - term.cursor.point.line.0 - 1; let inner = grid.display_iter(); let selection_range = selection.and_then(|span| { @@ -242,29 +248,13 @@ impl<'a, C> RenderableCellsIter<'a, C> { Some(SelectionRange::new(start, end, span.is_block)) }); - // Load cursor glyph - let cursor = &term.cursor.point; - let cursor_visible = term.mode.contains(TermMode::SHOW_CURSOR) && grid.contains(cursor); - let cursor_key = if cursor_visible { - let is_wide = - grid[cursor].flags.contains(Flags::WIDE_CHAR) && (cursor.col + 1) < num_cols; - Some(CursorKey { style: cursor_style, is_wide }) - } else { - // Use hidden cursor so text will not get inverted - cursor_style = CursorStyle::Hidden; - None - }; - RenderableCellsIter { - cursor, - cursor_offset, + cursor: term.renderable_cursor(config), grid, inner, selection: selection_range, config, colors: &term.colors, - cursor_key, - cursor_style, } } @@ -275,6 +265,18 @@ impl<'a, C> RenderableCellsIter<'a, C> { None => return false, }; + // Do not invert block cursor at selection boundaries + if self.cursor.key.style == CursorStyle::Block + && self.cursor.point == point + && (selection.start == point + || selection.end == point + || (selection.is_block + && ((selection.start.line == point.line && selection.end.col == point.col) + || (selection.end.line == point.line && selection.start.col == point.col)))) + { + return false; + } + // Point itself is selected if selection.contains(point.col, point.line) { return true; @@ -442,43 +444,46 @@ impl<'a, C> Iterator for RenderableCellsIter<'a, C> { #[inline] fn next(&mut self) -> Option<Self::Item> { loop { - if self.cursor_offset == self.inner.offset() && self.inner.column() == self.cursor.col { - let selected = self.is_selected(Point::new(self.cursor.line, self.cursor.col)); + if self.cursor.point.line == self.inner.line() + && self.cursor.point.col == self.inner.column() + { + let selected = self.is_selected(self.cursor.point); + + // Handle cell below cursor + if self.cursor.rendered { + let mut cell = + RenderableCell::new(self.config, self.colors, self.inner.next()?, selected); - // Handle cursor - if let Some(cursor_key) = self.cursor_key.take() { + if self.cursor.key.style == CursorStyle::Block { + mem::swap(&mut cell.bg, &mut cell.fg); + + if let Some(color) = self.cursor.text_color { + cell.fg = color; + } + } + + return Some(cell); + } else { + // Handle cursor + self.cursor.rendered = true; + + let buffer_point = self.grid.visible_to_buffer(self.cursor.point); let cell = Indexed { - inner: self.grid[self.cursor], - column: self.cursor.col, - // Using `self.cursor.line` leads to inconsitent cursor position when - // scrolling. See https://github.com/alacritty/alacritty/issues/2570 for more - // info. - line: self.inner.line(), + inner: self.grid[buffer_point.line][buffer_point.col], + column: self.cursor.point.col, + line: self.cursor.point.line, }; let mut renderable_cell = RenderableCell::new(self.config, self.colors, cell, selected); - renderable_cell.inner = RenderableCellContent::Cursor(cursor_key); + renderable_cell.inner = RenderableCellContent::Cursor(self.cursor.key); - if let Some(color) = self.config.cursor_cursor_color() { - renderable_cell.fg = RenderableCell::compute_bg_rgb(self.colors, color); + if let Some(color) = self.cursor.cursor_color { + renderable_cell.fg = color; } return Some(renderable_cell); - } else { - let mut cell = - RenderableCell::new(self.config, self.colors, self.inner.next()?, selected); - - if self.cursor_style == CursorStyle::Block { - std::mem::swap(&mut cell.bg, &mut cell.fg); - - if let Some(color) = self.config.cursor_text_color() { - cell.fg = color; - } - } - - return Some(cell); } } else { let cell = self.inner.next()?; @@ -497,26 +502,27 @@ pub mod mode { use bitflags::bitflags; bitflags! { - pub struct TermMode: u16 { - const SHOW_CURSOR = 0b0000_0000_0000_0001; - const APP_CURSOR = 0b0000_0000_0000_0010; - const APP_KEYPAD = 0b0000_0000_0000_0100; - const MOUSE_REPORT_CLICK = 0b0000_0000_0000_1000; - const BRACKETED_PASTE = 0b0000_0000_0001_0000; - const SGR_MOUSE = 0b0000_0000_0010_0000; - const MOUSE_MOTION = 0b0000_0000_0100_0000; - const LINE_WRAP = 0b0000_0000_1000_0000; - const LINE_FEED_NEW_LINE = 0b0000_0001_0000_0000; - const ORIGIN = 0b0000_0010_0000_0000; - const INSERT = 0b0000_0100_0000_0000; - const FOCUS_IN_OUT = 0b0000_1000_0000_0000; - const ALT_SCREEN = 0b0001_0000_0000_0000; - const MOUSE_DRAG = 0b0010_0000_0000_0000; - const MOUSE_MODE = 0b0010_0000_0100_1000; - const UTF8_MOUSE = 0b0100_0000_0000_0000; - const ALTERNATE_SCROLL = 0b1000_0000_0000_0000; - const ANY = 0b1111_1111_1111_1111; + pub struct TermMode: u32 { const NONE = 0; + const SHOW_CURSOR = 0b0000_0000_0000_0000_0001; + const APP_CURSOR = 0b0000_0000_0000_0000_0010; + const APP_KEYPAD = 0b0000_0000_0000_0000_0100; + const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_1000; + const BRACKETED_PASTE = 0b0000_0000_0000_0001_0000; + const SGR_MOUSE = 0b0000_0000_0000_0010_0000; + const MOUSE_MOTION = 0b0000_0000_0000_0100_0000; + const LINE_WRAP = 0b0000_0000_0000_1000_0000; + const LINE_FEED_NEW_LINE = 0b0000_0000_0001_0000_0000; + const ORIGIN = 0b0000_0000_0010_0000_0000; + const INSERT = 0b0000_0000_0100_0000_0000; + const FOCUS_IN_OUT = 0b0000_0000_1000_0000_0000; + const ALT_SCREEN = 0b0000_0001_0000_0000_0000; + const MOUSE_DRAG = 0b0000_0010_0000_0000_0000; + const MOUSE_MODE = 0b0000_0010_0000_0100_1000; + const UTF8_MOUSE = 0b0000_0100_0000_0000_0000; + const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000; + const VI = 0b0001_0000_0000_0000_0000; + const ANY = std::u32::MAX; } } @@ -730,11 +736,69 @@ impl VisualBell { } } +/// Terminal size info. +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)] +pub struct SizeInfo { + /// Terminal window width. + pub width: f32, + + /// Terminal window height. + pub height: f32, + + /// Width of individual cell. + pub cell_width: f32, + + /// Height of individual cell. + pub cell_height: f32, + + /// Horizontal window padding. + pub padding_x: f32, + + /// Horizontal window padding. + pub padding_y: f32, + + /// DPI factor of the current window. + #[serde(default)] + pub dpr: f64, +} + +impl SizeInfo { + #[inline] + pub fn lines(&self) -> Line { + Line(((self.height - 2. * self.padding_y) / self.cell_height) as usize) + } + + #[inline] + pub fn cols(&self) -> Column { + Column(((self.width - 2. * self.padding_x) / self.cell_width) as usize) + } + + /// Check if coordinates are inside the terminal grid. + /// + /// The padding is not counted as part of the grid. + pub fn contains_point(&self, x: usize, y: usize) -> bool { + x < (self.width - self.padding_x) as usize + && x >= self.padding_x as usize + && y < (self.height - self.padding_y) as usize + && y >= self.padding_y as usize + } + + pub fn pixels_to_coords(&self, x: usize, y: usize) -> Point { + let col = Column(x.saturating_sub(self.padding_x as usize) / (self.cell_width as usize)); + let line = Line(y.saturating_sub(self.padding_y as usize) / (self.cell_height as usize)); + + Point { + line: min(line, Line(self.lines().saturating_sub(1))), + col: min(col, Column(self.cols().saturating_sub(1))), + } + } +} + pub struct Term<T> { - /// Terminal focus + /// Terminal focus. pub is_focused: bool, - /// The grid + /// The grid. grid: Grid<Cell>, /// Tracks if the next call to input will need to first handle wrapping. @@ -744,23 +808,25 @@ pub struct Term<T> { /// arrays. Without it we would have to sanitize cursor.col every time we used it. input_needs_wrap: bool, - /// Alternate grid + /// Alternate grid. alt_grid: Grid<Cell>, - /// Alt is active + /// Alt is active. alt: bool, - /// The cursor + /// The cursor. cursor: Cursor, - /// The graphic character set, out of `charsets`, which ASCII is currently - /// being mapped to + /// Cursor location for vi mode. + pub vi_mode_cursor: ViModeCursor, + + /// Index into `charsets`, pointing to what ASCII is currently being mapped to. active_charset: CharsetIndex, - /// Tabstops + /// Tabstops. tabs: TabStops, - /// Mode flags + /// Mode flags. mode: TermMode, /// Scroll region. @@ -772,33 +838,36 @@ pub struct Term<T> { pub visual_bell: VisualBell, - /// Saved cursor from main grid + /// Saved cursor from main grid. cursor_save: Cursor, - /// Saved cursor from alt grid + /// Saved cursor from alt grid. cursor_save_alt: Cursor, semantic_escape_chars: String, - /// Colors used for rendering + /// Colors used for rendering. colors: color::List, - /// Is color in `colors` modified or not + /// Is color in `colors` modified or not. color_modified: [bool; color::COUNT], - /// Original colors from config + /// Original colors from config. original_colors: color::List, - /// Current style of the cursor + /// Current style of the cursor. cursor_style: Option<CursorStyle>, - /// Default style for resetting the cursor + /// Default style for resetting the cursor. default_cursor_style: CursorStyle, + /// Style of the vi mode cursor. + vi_mode_cursor_style: Option<CursorStyle>, + /// Clipboard access coupled to the active window clipboard: Clipboard, - /// Proxy for sending events to the event loop + /// Proxy for sending events to the event loop. event_proxy: T, /// Current title of the window. @@ -815,64 +884,6 @@ pub struct Term<T> { title_stack: Vec<Option<String>>, } -/// Terminal size info -#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)] -pub struct SizeInfo { - /// Terminal window width - pub width: f32, - - /// Terminal window height - pub height: f32, - - /// Width of individual cell - pub cell_width: f32, - - /// Height of individual cell - pub cell_height: f32, - - /// Horizontal window padding - pub padding_x: f32, - - /// Horizontal window padding - pub padding_y: f32, - - /// DPI factor of the current window - #[serde(default)] - pub dpr: f64, -} - -impl SizeInfo { - #[inline] - pub fn lines(&self) -> Line { - Line(((self.height - 2. * self.padding_y) / self.cell_height) as usize) - } - - #[inline] - pub fn cols(&self) -> Column { - Column(((self.width - 2. * self.padding_x) / self.cell_width) as usize) - } - - /// Check if coordinates are inside the terminal grid. - /// - /// The padding is not counted as part of the grid. - pub fn contains_point(&self, x: usize, y: usize) -> bool { - x < (self.width - self.padding_x) as usize - && x >= self.padding_x as usize - && y < (self.height - self.padding_y) as usize - && y >= self.padding_y as usize - } - - pub fn pixels_to_coords(&self, x: usize, y: usize) -> Point { - let col = Column(x.saturating_sub(self.padding_x as usize) / (self.cell_width as usize)); - let line = Line(y.saturating_sub(self.padding_y as usize) / (self.cell_height as usize)); - - Point { - line: min(line, Line(self.lines().saturating_sub(1))), - col: min(col, Column(self.cols().saturating_sub(1))), - } - } -} - impl<T> Term<T> { pub fn selection(&self) -> &Option<Selection> { &self.grid.selection @@ -920,6 +931,7 @@ impl<T> Term<T> { alt: false, active_charset: Default::default(), cursor: Default::default(), + vi_mode_cursor: Default::default(), cursor_save: Default::default(), cursor_save_alt: Default::default(), tabs, @@ -931,6 +943,7 @@ impl<T> Term<T> { semantic_escape_chars: config.selection.semantic_escape_chars().to_owned(), cursor_style: None, default_cursor_style: config.cursor.style, + vi_mode_cursor_style: config.cursor.vi_mode_style, dynamic_title: config.dynamic_title(), clipboard, event_proxy, @@ -959,6 +972,7 @@ impl<T> Term<T> { self.mode.remove(TermMode::ALTERNATE_SCROLL); } self.default_cursor_style = config.cursor.style; + self.vi_mode_cursor_style = config.cursor.vi_mode_style; self.default_title = config.window.title.clone(); self.dynamic_title = config.dynamic_title(); @@ -1105,13 +1119,7 @@ impl<T> Term<T> { pub fn renderable_cells<'b, C>(&'b self, config: &'b Config<C>) -> RenderableCellsIter<'_, C> { let selection = self.grid.selection.as_ref().and_then(|s| s.to_range(self)); - let cursor = if self.is_focused || !config.cursor.unfocused_hollow() { - self.cursor_style.unwrap_or(self.default_cursor_style) - } else { - CursorStyle::HollowBlock - }; - - RenderableCellsIter::new(&self, config, selection, cursor) + RenderableCellsIter::new(&self, config, selection) } /// Resize terminal to new dimensions @@ -1129,12 +1137,12 @@ impl<T> Term<T> { self.grid.selection = None; self.alt_grid.selection = None; - // Should not allow less than 1 col, causes all sorts of checks to be required. + // Should not allow less than 2 cols, causes all sorts of checks to be required. if num_cols <= Column(1) { num_cols = Column(2); } - // Should not allow less than 1 line, causes all sorts of checks to be required. + // Should not allow less than 2 lines, causes all sorts of checks to be required. if num_lines <= Line(1) { num_lines = Line(2); } @@ -1178,6 +1186,8 @@ impl<T> Term<T> { self.cursor_save.point.line = min(self.cursor_save.point.line, num_lines - 1); self.cursor_save_alt.point.col = min(self.cursor_save_alt.point.col, num_cols - 1); self.cursor_save_alt.point.line = min(self.cursor_save_alt.point.line, num_lines - 1); + self.vi_mode_cursor.point.col = min(self.vi_mode_cursor.point.col, num_cols - 1); + self.vi_mode_cursor.point.line = min(self.vi_mode_cursor.point.line, num_lines - 1); // Recreate tabs list self.tabs.resize(self.grid.num_cols()); @@ -1200,7 +1210,7 @@ impl<T> Term<T> { } self.alt = !self.alt; - std::mem::swap(&mut self.grid, &mut self.alt_grid); + mem::swap(&mut self.grid, &mut self.alt_grid); } /// Scroll screen down @@ -1258,10 +1268,58 @@ impl<T> Term<T> { self.event_proxy.send_event(Event::Exit); } + #[inline] pub fn clipboard(&mut self) -> &mut Clipboard { &mut self.clipboard } + /// Toggle the vi mode. + #[inline] + pub fn toggle_vi_mode(&mut self) { + self.mode ^= TermMode::VI; + self.grid.selection = None; + + // Reset vi mode cursor position to match primary cursor + if self.mode.contains(TermMode::VI) { + let line = min(self.cursor.point.line + self.grid.display_offset(), self.lines() - 1); + self.vi_mode_cursor = ViModeCursor::new(Point::new(line, self.cursor.point.col)); + } + + self.dirty = true; + } + + /// Move vi mode cursor. + #[inline] + pub fn vi_motion(&mut self, motion: ViMotion) + where + T: EventListener, + { + // Require vi mode to be active + if !self.mode.contains(TermMode::VI) { + return; + } + + // Move cursor + self.vi_mode_cursor = self.vi_mode_cursor.motion(self, motion); + + // Update selection if one is active + let viewport_point = self.visible_to_buffer(self.vi_mode_cursor.point); + if let Some(selection) = &mut self.grid.selection { + // Do not extend empty selections started by single mouse click + if !selection.is_empty() { + selection.update(viewport_point, Side::Left); + selection.include_all(); + } + } + + self.dirty = true; + } + + #[inline] + pub fn semantic_escape_chars(&self) -> &str { + &self.semantic_escape_chars + } + /// Insert a linebreak at the current cursor position. #[inline] fn wrapline(&mut self) @@ -1297,6 +1355,65 @@ impl<T> Term<T> { cell.c = self.cursor.charsets[self.active_charset].map(c); cell } + + /// Get rendering information about the active cursor. + fn renderable_cursor<C>(&self, config: &Config<C>) -> RenderableCursor { + let vi_mode = self.mode.contains(TermMode::VI); + + // Cursor position + let mut point = if vi_mode { + self.vi_mode_cursor.point + } else { + let mut point = self.cursor.point; + point.line += self.grid.display_offset(); + point + }; + + // Cursor shape + let hidden = !self.mode.contains(TermMode::SHOW_CURSOR) || point.line >= self.lines(); + let cursor_style = if hidden && !vi_mode { + point.line = Line(0); + CursorStyle::Hidden + } else if !self.is_focused && config.cursor.unfocused_hollow() { + CursorStyle::HollowBlock + } else { + let cursor_style = self.cursor_style.unwrap_or(self.default_cursor_style); + + if vi_mode { + self.vi_mode_cursor_style.unwrap_or(cursor_style) + } else { + cursor_style + } + }; + + // Cursor colors + let (text_color, cursor_color) = if vi_mode { + (config.vi_mode_cursor_text_color(), config.vi_mode_cursor_cursor_color()) + } else { + let cursor_cursor_color = config.cursor_cursor_color().map(|c| self.colors[c]); + (config.cursor_text_color(), cursor_cursor_color) + }; + + // Expand across wide cell when inside wide char or spacer + let buffer_point = self.visible_to_buffer(point); + let cell = self.grid[buffer_point.line][buffer_point.col]; + let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) + && self.grid[buffer_point.line][buffer_point.col - 1].flags.contains(Flags::WIDE_CHAR) + { + point.col -= 1; + true + } else { + cell.flags.contains(Flags::WIDE_CHAR) + }; + + RenderableCursor { + text_color, + cursor_color, + key: CursorKey { style: cursor_style, is_wide }, + point, + rendered: false, + } + } } impl<T> TermInfo for Term<T> { @@ -2184,7 +2301,7 @@ mod tests { use crate::event::{Event, EventListener}; use crate::grid::{Grid, Scroll}; use crate::index::{Column, Line, Point, Side}; - use crate::selection::Selection; + use crate::selection::{Selection, SelectionType}; use crate::term::cell::{Cell, Flags}; use crate::term::{SizeInfo, Term}; @@ -2222,17 +2339,29 @@ mod tests { mem::swap(&mut term.semantic_escape_chars, &mut escape_chars); { - *term.selection_mut() = Some(Selection::semantic(Point { line: 2, col: Column(1) })); + *term.selection_mut() = Some(Selection::new( + SelectionType::Semantic, + Point { line: 2, col: Column(1) }, + Side::Left, + )); assert_eq!(term.selection_to_string(), Some(String::from("aa"))); } { - *term.selection_mut() = Some(Selection::semantic(Point { line: 2, col: Column(4) })); + *term.selection_mut() = Some(Selection::new( + SelectionType::Semantic, + Point { line: 2, col: Column(4) }, + Side::Left, + )); assert_eq!(term.selection_to_string(), Some(String::from("aaa"))); } { - *term.selection_mut() = Some(Selection::semantic(Point { line: 1, col: Column(1) })); + *term.selection_mut() = Some(Selection::new( + SelectionType::Semantic, + Point { line: 1, col: Column(1) }, + Side::Left, + )); assert_eq!(term.selection_to_string(), Some(String::from("aaa"))); } } @@ -2258,7 +2387,11 @@ mod tests { mem::swap(&mut term.grid, &mut grid); - *term.selection_mut() = Some(Selection::lines(Point { line: 0, col: Column(3) })); + *term.selection_mut() = Some(Selection::new( + SelectionType::Lines, + Point { line: 0, col: Column(3) }, + Side::Left, + )); assert_eq!(term.selection_to_string(), Some(String::from("\"aa\"a\n"))); } @@ -2285,7 +2418,8 @@ mod tests { mem::swap(&mut term.grid, &mut grid); - let mut selection = Selection::simple(Point { line: 2, col: Column(0) }, Side::Left); + let mut selection = + Selection::new(SelectionType::Simple, Point { line: 2, col: Column(0) }, Side::Left); selection.update(Point { line: 0, col: Column(2) }, Side::Right); *term.selection_mut() = Some(selection); assert_eq!(term.selection_to_string(), Some("aaa\n\naaa\n".into())); |