From a8a4ea18365171e4df3c44727fc797fc5575708e Mon Sep 17 00:00:00 2001 From: Sherub Thakur Date: Mon, 28 Jun 2021 15:24:54 +0530 Subject: [PATCH] Extract screen painting stuff into a separate struct --- src/engine.rs | 142 ++++++++++------------------------- src/lib.rs | 2 + src/painter.rs | 199 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 241 insertions(+), 102 deletions(-) create mode 100644 src/painter.rs diff --git a/src/engine.rs b/src/engine.rs index 68d79bce..35c28372 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -2,6 +2,7 @@ use crate::{ clip_buffer::{get_default_clipboard, Clipboard}, default_emacs_keybindings, keybindings::{default_vi_insert_keybindings, default_vi_normal_keybindings, Keybindings}, + painter::Painter, prompt::{PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, PromptViMode}, Prompt, }; @@ -12,19 +13,12 @@ use crate::{ }; use crate::{EditCommand, EditMode, Signal, ViEngine}; use crossterm::{ - cursor, - cursor::{position, MoveTo, MoveToColumn, RestorePosition, SavePosition}, + cursor::position, event::{poll, read, Event, KeyCode, KeyEvent, KeyModifiers}, - style::{Color, Print, ResetColor, SetForegroundColor}, - terminal::{self, Clear, ClearType}, - QueueableCommand, Result, + terminal, Result, }; -use std::{ - collections::HashMap, - io::{stdout, Stdout, Write}, - time::Duration, -}; +use std::{collections::HashMap, io::stdout, time::Duration}; /// Line editor engine /// @@ -55,7 +49,7 @@ pub struct Reedline { history_search: Option, // This could be have more features in the future (fzf, configurable?) // Stdout - stdout: Stdout, + painter: Painter, // Keybindings keybindings: HashMap, @@ -84,7 +78,7 @@ impl Reedline { pub fn new() -> Reedline { let history = History::default(); let cut_buffer = Box::new(get_default_clipboard()); - let stdout = stdout(); + let painter = Painter::new(stdout()); let mut keybindings_hashmap = HashMap::new(); keybindings_hashmap.insert(EditMode::Emacs, default_emacs_keybindings()); keybindings_hashmap.insert(EditMode::ViInsert, default_vi_insert_keybindings()); @@ -95,7 +89,7 @@ impl Reedline { cut_buffer, history, history_search: None, - stdout, + painter, keybindings: keybindings_hashmap, edit_mode: EditMode::Emacs, need_full_repaint: false, @@ -177,13 +171,6 @@ impl Reedline { Ok(()) } - pub fn move_to(&mut self, column: u16, row: u16) -> Result<()> { - self.stdout.queue(MoveTo(column, row))?; - self.stdout.flush()?; - - Ok(()) - } - /// Wait for input and provide the user with a specified [`Prompt`]. /// /// Returns a [`crossterm::Result`] in which the `Err` type is [`crossterm::ErrorKind`] @@ -201,23 +188,14 @@ impl Reedline { /// Writes `msg` to the terminal with a following carriage return and newline pub fn print_line(&mut self, msg: &str) -> Result<()> { - self.stdout - .queue(Print(msg))? - .queue(Print("\n"))? - .queue(MoveToColumn(1))?; - self.stdout.flush()?; - - Ok(()) + self.painter.paint_line(msg) } /// Goes to the beginning of the next line /// /// Also works in raw mode pub fn print_crlf(&mut self) -> Result<()> { - self.stdout.queue(Print("\n"))?.queue(MoveToColumn(1))?; - self.stdout.flush()?; - - Ok(()) + self.painter.paint_crlf() } /// **For debugging purposes only:** Track the terminal events observed by [`Reedline`] and print them. @@ -619,28 +597,7 @@ impl Reedline { /// Clear the screen by printing enough whitespace to start the prompt or /// other output back at the first line of the terminal. pub fn clear_screen(&mut self) -> Result<()> { - let (_, num_lines) = terminal::size()?; - for _ in 0..2 * num_lines { - self.stdout.queue(Print("\n"))?; - } - self.stdout.queue(MoveTo(0, 0))?; - self.stdout.flush()?; - Ok(()) - } - - /// Display the complete prompt including status indicators (e.g. pwd, time) - /// - /// Used at the beginning of each [`Reedline::read_line()`] call. - fn queue_prompt(&mut self, prompt: &dyn Prompt, screen_width: usize) -> Result<()> { - // print our prompt - let prompt_mode = self.prompt_edit_mode(); - - self.stdout - .queue(MoveToColumn(0))? - .queue(SetForegroundColor(prompt.get_prompt_color()))? - .queue(Print(prompt.render_prompt(screen_width)))? - .queue(Print(prompt.render_prompt_indicator(prompt_mode)))? - .queue(ResetColor)?; + self.painter.clear_screen()?; Ok(()) } @@ -652,11 +609,7 @@ impl Reedline { fn queue_prompt_indicator(&mut self, prompt: &dyn Prompt) -> Result<()> { // print our prompt let prompt_mode = self.prompt_edit_mode(); - self.stdout - .queue(MoveToColumn(0))? - .queue(SetForegroundColor(prompt.get_prompt_color()))? - .queue(Print(prompt.render_prompt_indicator(prompt_mode)))? - .queue(ResetColor)?; + self.painter.queue_prompt_indicator(prompt, prompt_mode)?; Ok(()) } @@ -665,8 +618,6 @@ impl Reedline { /// /// Requires coordinates where the input buffer begins after the prompt. fn buffer_paint(&mut self, prompt_offset: (u16, u16)) -> Result<()> { - let new_index = self.insertion_point().offset; - // Repaint logic: // // Start after the prompt @@ -675,17 +626,13 @@ impl Reedline { // Then draw the remainer of the buffer from above // Finally, reset the cursor to the saved position - // stdout.queue(Print(&engine.line_buffer[..new_index]))?; let insertion_line = self.insertion_line().to_string(); - self.stdout - .queue(MoveTo(prompt_offset.0, prompt_offset.1))?; - self.stdout.queue(Print(&insertion_line[0..new_index]))?; - self.stdout.queue(SavePosition)?; - self.stdout.queue(Print(&insertion_line[new_index..]))?; - self.stdout.queue(Clear(ClearType::FromCursorDown))?; - self.stdout.queue(RestorePosition)?; + let new_index = self.insertion_point().offset; + + self.painter + .queue_buffer(insertion_line, prompt_offset, new_index)?; - self.stdout.flush()?; + self.painter.flush()?; Ok(()) } @@ -694,20 +641,21 @@ impl Reedline { &mut self, prompt: &dyn Prompt, prompt_origin: (u16, u16), - terminal_width: u16, + terminal_size: (u16, u16), ) -> Result<(u16, u16)> { - self.stdout - .queue(cursor::Hide)? - .queue(MoveTo(prompt_origin.0, prompt_origin.1))?; - self.queue_prompt(prompt, terminal_width as usize)?; - // set where the input begins - self.stdout.queue(cursor::Show)?.flush()?; - - let prompt_offset = position()?; - self.buffer_paint(prompt_offset)?; - self.stdout.queue(cursor::Show)?.flush()?; + let prompt_mode = self.prompt_edit_mode(); + let insertion_line = self.insertion_line().to_string(); + let new_index = self.insertion_point().offset; + self.painter.repaint_everything( + prompt, + prompt_mode, + prompt_origin, + new_index, + insertion_line, + terminal_size, + ) - Ok(prompt_offset) + // Ok(prompt_offset) } /// Repaint logic for the history reverse search @@ -728,34 +676,24 @@ impl Reedline { }; let prompt_history_search = PromptHistorySearch::new(status, search.search_string.clone()); - let history_indicator = - prompt.render_prompt_history_search_indicator(prompt_history_search); // print search prompt - self.stdout - .queue(MoveToColumn(0))? - .queue(SetForegroundColor(Color::Blue))? - .queue(Print(history_indicator))? - .queue(ResetColor)?; + self.painter + .queue_history_search_indicator(prompt, prompt_history_search)?; match search.result { Some((history_index, offset)) => { let history_result = self.history.get_nth_newest(history_index).unwrap(); - self.stdout.queue(Print(&history_result[..offset]))?; - self.stdout.queue(SavePosition)?; - self.stdout.queue(Print(&history_result[offset..]))?; - self.stdout.queue(Clear(ClearType::UntilNewLine))?; - self.stdout.queue(RestorePosition)?; + self.painter.queue_history_results(history_result, offset)?; + self.painter.flush()?; } None => { - self.stdout.queue(Clear(ClearType::UntilNewLine))?; + self.painter.clear_until_newline()?; } } - self.stdout.flush()?; - Ok(()) } @@ -771,10 +709,10 @@ impl Reedline { if (column, row) == (0, 0) { (0, 0) } else if row + 1 == terminal_size.1 { - self.stdout.queue(Print("\r\n\r\n"))?.flush()?; + self.painter.paint_carrige_return()?; (0, row.saturating_sub(1)) } else if row + 2 == terminal_size.1 { - self.stdout.queue(Print("\r\n\r\n"))?.flush()?; + self.painter.paint_carrige_return()?; (0, row) } else { (0, row + 1) @@ -782,7 +720,7 @@ impl Reedline { }; // set where the input begins - let mut prompt_offset = self.full_repaint(prompt, prompt_origin, terminal_size.0)?; + let mut prompt_offset = self.full_repaint(prompt, prompt_origin, terminal_size)?; // Redraw if Ctrl-L was used if self.history_search.is_some() { @@ -887,20 +825,20 @@ impl Reedline { terminal_size = (width, height); // TODO properly adjusting prompt_origin on resizing while lines > 1 prompt_origin.1 = position()?.1.saturating_sub(1); - prompt_offset = self.full_repaint(prompt, prompt_origin, width)?; + prompt_offset = self.full_repaint(prompt, prompt_origin, terminal_size)?; continue; } } if self.history_search.is_some() { self.history_search_paint(prompt)?; } else if self.need_full_repaint { - prompt_offset = self.full_repaint(prompt, prompt_origin, terminal_size.0)?; + prompt_offset = self.full_repaint(prompt, prompt_origin, terminal_size)?; self.need_full_repaint = false; } else { self.buffer_paint(prompt_offset)?; } } else { - prompt_offset = self.full_repaint(prompt, prompt_origin, terminal_size.0)?; + prompt_offset = self.full_repaint(prompt, prompt_origin, terminal_size)?; } } } diff --git a/src/lib.rs b/src/lib.rs index 6432f4d0..d4d2bd0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,8 @@ mod clip_buffer; mod enums; pub use enums::{EditCommand, EditMode, Signal}; +mod painter; + mod engine; pub use engine::Reedline; diff --git a/src/painter.rs b/src/painter.rs new file mode 100644 index 00000000..bb7f2bc1 --- /dev/null +++ b/src/painter.rs @@ -0,0 +1,199 @@ +use crate::{ + prompt::{PromptEditMode, PromptHistorySearch}, + Prompt, +}; +use crossterm::{ + cursor::{self, position, MoveTo, MoveToColumn, RestorePosition, SavePosition}, + style::{Color, Print, ResetColor, SetForegroundColor}, + terminal::{self, Clear, ClearType}, + QueueableCommand, Result, +}; + +use std::io::{Stdout, Write}; + +pub struct Painter { + // Stdout + stdout: Stdout, +} + +impl Painter { + pub fn new(stdout: Stdout) -> Self { + Painter { stdout } + } + + pub fn queue_move_to(&mut self, column: u16, row: u16) -> Result<()> { + self.stdout.queue(cursor::MoveTo(column, row))?; + + Ok(()) + } + + /// Queue the complete prompt to display including status indicators (e.g. pwd, time) + /// + /// Used at the beginning of each [`Reedline::read_line()`] call. + pub fn queue_prompt( + &mut self, + prompt: &dyn Prompt, + prompt_mode: PromptEditMode, + terminal_size: (u16, u16), + ) -> Result<()> { + let (screen_width, _) = terminal_size; + + // print our prompt + self.stdout + .queue(MoveToColumn(0))? + .queue(SetForegroundColor(prompt.get_prompt_color()))? + .queue(Print(prompt.render_prompt(screen_width as usize)))? + .queue(Print(prompt.render_prompt_indicator(prompt_mode)))? + .queue(ResetColor)?; + + Ok(()) + } + + /// Queue prompt components preceding the buffer to display + /// + /// Used to restore the prompt indicator after a search etc. that affected + /// the prompt + pub fn queue_prompt_indicator( + &mut self, + prompt: &dyn Prompt, + prompt_mode: PromptEditMode, + ) -> Result<()> { + // print our prompt + self.stdout + .queue(MoveToColumn(0))? + .queue(SetForegroundColor(prompt.get_prompt_color()))? + .queue(Print(prompt.render_prompt_indicator(prompt_mode)))? + .queue(ResetColor)?; + + Ok(()) + } + + /// Repaint logic for the normal input prompt buffer + /// + /// Requires coordinates where the input buffer begins after the prompt. + pub fn queue_buffer( + &mut self, + buffer: String, + offset: (u16, u16), + cursor_index_in_buffer: usize, + ) -> Result<()> { + // Repaint logic: + // + // Start after the prompt + // Draw the string slice from 0 to the grapheme start left of insertion point + // Then, get the position on the screen + // Then draw the remainer of the buffer from above + // Finally, reset the cursor to the saved position + + self.stdout + .queue(MoveTo(offset.0, offset.1))? + .queue(Print(&buffer[0..cursor_index_in_buffer]))? + .queue(SavePosition)? + .queue(Print(&buffer[cursor_index_in_buffer..]))? + .queue(Clear(ClearType::FromCursorDown))? + .queue(RestorePosition)?; + + Ok(()) + } + + pub fn repaint_everything( + &mut self, + prompt: &dyn Prompt, + prompt_mode: PromptEditMode, + prompt_origin: (u16, u16), + cursor_position_in_buffer: usize, + buffer: String, + terminal_size: (u16, u16), + ) -> Result<(u16, u16)> { + self.stdout.queue(cursor::Hide)?; + self.queue_move_to(prompt_origin.0, prompt_origin.1)?; + self.queue_prompt(prompt, prompt_mode, terminal_size)?; + self.stdout.queue(cursor::Show)?; + self.flush()?; + // set where the input begins + let prompt_offset = position()?; + self.queue_buffer(buffer, prompt_offset, cursor_position_in_buffer)?; + self.stdout.queue(cursor::Show)?; + self.flush()?; + + Ok(prompt_offset) + } + + pub fn queue_history_search_indicator( + &mut self, + prompt: &dyn Prompt, + prompt_search: PromptHistorySearch, + ) -> Result<()> { + // print search prompt + self.stdout + .queue(MoveToColumn(0))? + .queue(SetForegroundColor(Color::Blue))? + .queue(Print( + prompt.render_prompt_history_search_indicator(prompt_search), + ))? + .queue(ResetColor)?; + + Ok(()) + } + + pub fn queue_history_results(&mut self, history_result: &String, offset: usize) -> Result<()> { + self.stdout + .queue(Print(&history_result[..offset]))? + .queue(SavePosition)? + .queue(Print(&history_result[offset..]))? + .queue(Clear(ClearType::UntilNewLine))? + .queue(RestorePosition)?; + + Ok(()) + } + + /// Writes `line` to the terminal with a following carriage return and newline + pub fn paint_line(&mut self, line: &str) -> Result<()> { + self.stdout + .queue(Print(line))? + .queue(Print("\n"))? + .queue(MoveToColumn(1))?; + self.stdout.flush()?; + + Ok(()) + } + + /// Goes to the beginning of the next line + /// + /// Also works in raw mode + pub fn paint_crlf(&mut self) -> Result<()> { + self.stdout.queue(Print("\n"))?.queue(MoveToColumn(1))?; + self.stdout.flush()?; + + Ok(()) + } + + // Printing carrige return + pub fn paint_carrige_return(&mut self) -> Result<()> { + self.stdout.queue(Print("\r\n\r\n"))?.flush() + } + + /// Clear the screen by printing enough whitespace to start the prompt or + /// other output back at the first line of the terminal. + pub fn clear_screen(&mut self) -> Result<()> { + let (_, num_lines) = terminal::size()?; + for _ in 0..2 * num_lines { + self.stdout.queue(Print("\n"))?; + } + self.stdout.queue(MoveTo(0, 0))?; + self.stdout.flush()?; + + Ok(()) + } + + pub fn clear_until_newline(&mut self) -> Result<()> { + self.stdout.queue(Clear(ClearType::UntilNewLine))?; + self.stdout.flush()?; + + Ok(()) + } + + pub fn flush(&mut self) -> Result<()> { + self.stdout.flush() + } +}