diff --git a/src/core/ev_handler.rs b/src/core/ev_handler.rs index c10c76d..dd93563 100644 --- a/src/core/ev_handler.rs +++ b/src/core/ev_handler.rs @@ -45,7 +45,7 @@ pub fn handle_event( Event::UserInput(InputEvent::RestorePrompt) => { // Set the message to None and new messages to false as all messages have been shown p.message = None; - p.format_prompt(); + p.update_displayed_prompt(); } Event::UserInput(InputEvent::UpdateTermArea(c, r)) => { p.rows = r; @@ -94,7 +94,7 @@ pub fn handle_event( let compiled_regex = regex::Regex::new(&search_result.string).ok(); if compiled_regex.is_none() { p.message = Some("Invalid regular expression. Press Enter".to_owned()); - p.format_prompt(); + p.update_displayed_prompt(); } compiled_regex } else { @@ -112,7 +112,7 @@ pub fn handle_event( p.upper_mark = *p.search_idx.iter().nth(p.search_mark).unwrap(); } - p.format_prompt(); + p.update_displayed_prompt(); display::draw_full(&mut out, p)?; } #[cfg(feature = "search")] @@ -126,7 +126,7 @@ pub fn handle_event( p.upper_mark = *p.search_idx.iter().nth(p.search_mark).unwrap(); } - p.format_prompt(); + p.update_displayed_prompt(); } #[cfg(feature = "search")] Event::UserInput(InputEvent::PrevMatch | InputEvent::MoveToPrevMatch(1)) @@ -142,7 +142,7 @@ pub fn handle_event( // If the index is less than or equal to the upper_mark, then set y to the new upper_mark if *y < p.upper_mark { p.upper_mark = *y; - p.format_prompt(); + p.update_displayed_prompt(); } } } @@ -163,7 +163,7 @@ pub fn handle_event( p.upper_mark = *p.search_idx.iter().nth(p.search_mark).unwrap(); } } - p.format_prompt(); + p.update_displayed_prompt(); } #[cfg(feature = "search")] Event::UserInput(InputEvent::MoveToPrevMatch(n)) if p.search_term.is_some() => { @@ -177,7 +177,7 @@ pub fn handle_event( // If the index is less than or equal to the upper_mark, then set y to the new upper_mark if *y < p.upper_mark { p.upper_mark = *y; - p.format_prompt(); + p.update_displayed_prompt(); } } } @@ -204,7 +204,7 @@ pub fn handle_event( } else { p.message = Some(text.to_string()); } - p.format_prompt(); + p.update_displayed_prompt(); term::move_cursor(&mut out, 0, p.rows.try_into().unwrap(), false)?; if !p.running.lock().is_uninitialized() { super::utils::display::write_prompt( diff --git a/src/core/init.rs b/src/core/init.rs index 9d1a644..184bf12 100644 --- a/src/core/init.rs +++ b/src/core/init.rs @@ -348,17 +348,17 @@ fn event_reader( if let Some(iev) = input { if let InputEvent::Number(n) = iev { guard.prefix_num.push(n); - guard.format_prompt(); + guard.update_displayed_prompt(); } else if !guard.prefix_num.is_empty() { guard.prefix_num.clear(); - guard.format_prompt(); + guard.update_displayed_prompt(); } if let Err(TrySendError::Disconnected(_)) = evtx.try_send(Event::UserInput(iev)) { break; } } else if !guard.prefix_num.is_empty() { guard.prefix_num.clear(); - guard.format_prompt(); + guard.update_displayed_prompt(); } } } diff --git a/src/core/mod.rs b/src/core/mod.rs index f73a44d..78cec7d 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -4,6 +4,7 @@ pub mod events; pub mod init; pub mod utils; pub static RUNMODE: parking_lot::Mutex = parking_lot::const_mutex(RunMode::Uninitialized); +pub mod screen_line; /// Define the modes in which minus can run #[derive(Copy, Clone, PartialEq, Eq)] diff --git a/src/core/screen_line.rs b/src/core/screen_line.rs new file mode 100644 index 0000000..09a9bdc --- /dev/null +++ b/src/core/screen_line.rs @@ -0,0 +1,127 @@ +use crate::minus_core::utils::text::wrap_str; +use crate::LineNumbers; + +pub struct ScreenLine { + fmt_lines: Vec, + orig_text: String, + line_number: usize, + fmt_lines_count: usize, + terminated: bool, +} + +struct ScreenLineCreationOpts { + text: String, + cols: usize, + line_number: usize, + line_numbers: LineNumbers, + len_line_numbers: u16, + // prev_fmt_lines_count: usize, +} + +impl ScreenLine { + fn new(fmt_lines: Vec, orig_text: String, line_number: usize) -> Self { + let fmt_lines_count = fmt_lines.len(); + let terminated = orig_text.ends_with('\n'); + Self { + fmt_lines, + orig_text, + line_number, + fmt_lines_count, + terminated, + } + } + + fn new_from_string( + text: String, + cols: u16, + line_number: usize, + line_numbers: LineNumbers, + len_line_number: u16, + ) -> Self { + let fmt_lines = formatted_line(&text, len_line_number, line_number, line_numbers, cols); + Self::new(fmt_lines, text, line_number) + } +} + +pub fn formatted_line( + line: &str, + len_line_number: u16, + line_number: usize, + line_numbers: LineNumbers, + cols: u16, +) -> Vec { + assert!( + !line.contains('\n'), + "Newlines found in appending line {:?}", + line + ); + // Whether line numbers are active + let line_numbers = matches!(line_numbers, LineNumbers::Enabled | LineNumbers::AlwaysOn); + + // NOTE: Only relevant when line numbers are active + // Padding is the space that the actual line text will be shifted to accommodate for + // line numbers. This is equal to:- + // LineNumbers::EXTRA_PADDING + len_line_number + 1 (for '.') + // + // We reduce this from the number of available columns as this space cannot be used for + // actual line display when wrapping the lines + let padding = len_line_number + LineNumbers::EXTRA_PADDING + 1; + + // Wrap the line and return an iterator over all the rows + let mut enumerated_rows = if line_numbers { + wrap_str(line, cols.saturating_sub(padding + 2).into()).into_iter() + } else { + wrap_str(line, cols.into()).into_iter() + }; + + if line_numbers { + let mut formatted_rows = Vec::with_capacity(256); + + // Formatter for only when line numbers are active + // * If minus is run under test, ascii codes for making the numbers bol is not inserted because they add + // extra difficulty while writing tests + // * Line number is added only to the first row of a line. This makes a better UI overall + let formatter = |row: String, is_first_row: bool, idx: usize| { + format!( + "{bold}{number: >len$}{reset} {row}", + bold = if cfg!(not(test)) && is_first_row { + crossterm::style::Attribute::Bold.to_string() + } else { + String::new() + }, + number = if is_first_row { + (idx + 1).to_string() + "." + } else { + String::new() + }, + len = padding.into(), + reset = if cfg!(not(test)) && is_first_row { + crossterm::style::Attribute::Reset.to_string() + } else { + String::new() + }, + row = row + ) + }; + + // First format the first row separate from other rows, then the subsequent rows and finally join them + // This is because only the first row contains the line number and not the subsequent rows + let first_row = { + #[cfg_attr(not(feature = "search"), allow(unused_mut))] + let mut row = enumerated_rows.next().unwrap(); + formatter(row, true, line_number) + }; + formatted_rows.push(first_row); + + #[cfg_attr(not(feature = "search"), allow(unused_mut))] + #[cfg_attr(not(feature = "search"), allow(unused_variables))] + let mut rows_left = enumerated_rows + .map(|mut row| formatter(row, false, 0)) + .collect::>(); + formatted_rows.append(&mut rows_left); + + formatted_rows + } else { + enumerated_rows.collect() + } +} diff --git a/src/core/utils/display/tests.rs b/src/core/utils/display/tests.rs index 98aff1b..e07166e 100644 --- a/src/core/utils/display/tests.rs +++ b/src/core/utils/display/tests.rs @@ -424,7 +424,7 @@ fn draw_help_message() { let mut pager = PagerState::new().unwrap(); pager.lines = lines.to_string(); pager.line_numbers = LineNumbers::AlwaysOff; - pager.format_prompt(); + pager.update_displayed_prompt(); draw_full(&mut out, &mut pager).expect("Should have written"); @@ -468,7 +468,7 @@ mod draw_for_change_tests { ps.upper_mark = 0; ps.lines = lines; ps.format_lines(); - ps.format_prompt(); + ps.update_displayed_prompt(); ps } diff --git a/src/core/utils/text.rs b/src/core/utils/text.rs index 28980d5..d0312f3 100644 --- a/src/core/utils/text.rs +++ b/src/core/utils/text.rs @@ -335,7 +335,7 @@ pub fn formatted_line( // // We reduce this from the number of available columns as this space cannot be used for // actual line display when wrapping the lines - let padding = len_line_number + LineNumbers::EXTRA_PADDING + 1; + let padding = len_line_number + (>::into(LineNumbers::EXTRA_PADDING)) + 1; // Wrap the line and return an iterator over all the rows let mut enumerated_rows = if line_numbers { @@ -656,6 +656,80 @@ third line\n", } } +/// Reformat the inputted prompt to how it should be displayed +pub(crate) fn format_prompt( + prompt: String, + cols: usize, + prefix_num: &str, + message: Option, + #[cfg(feature = "search")] search_idx: BTreeSet, + #[cfg(feature = "search")] search_mark: usize, +) -> String { + const SEARCH_BG: &str = "\x1b[34m"; + const INPUT_BG: &str = "\x1b[33m"; + + // Allocate the string. Add extra space in case for the + // ANSI escape things if we do have characters typed and search showing + let mut format_string = String::with_capacity(cols + (SEARCH_BG.len() * 2) + 4); + + // Get the string that will contain the search index/match indicator + #[cfg(feature = "search")] + let mut search_str = String::new(); + #[cfg(feature = "search")] + if !search_idx.is_empty() { + search_str.push(' '); + search_str.push_str(&(search_mark + 1).to_string()); + search_str.push('/'); + search_str.push_str(&search_idx.len().to_string()); + search_str.push(' '); + } + + // And get the string that will contain the prefix_num + let mut prefix_str = String::new(); + if !prefix_num.is_empty() { + prefix_str.push(' '); + prefix_str.push_str(&prefix_num); + prefix_str.push(' '); + } + + // And lastly, the string that contains the prompt or msg + let prompt_str = message.as_ref().unwrap_or(&prompt); + + #[cfg(feature = "search")] + let search_len = search_str.len(); + #[cfg(not(feature = "search"))] + let search_len = 0; + + // Calculate how much extra padding in the middle we need between + // the prompt/message and the indicators on the right + let prefix_len = prefix_str.len(); + let extra_space = cols.saturating_sub(search_len + prefix_len + prompt_str.len()); + let dsp_prompt: &str = if extra_space == 0 { + &prompt_str[..cols - search_len - prefix_len] + } else { + prompt_str + }; + + // push the prompt/msg + format_string.push_str(dsp_prompt); + format_string.push_str(&" ".repeat(extra_space)); + + // add the prefix_num if it exists + if prefix_len > 0 { + format_string.push_str(INPUT_BG); + format_string.push_str(&prefix_str); + } + + // and add the search indicator stuff if it exists + #[cfg(feature = "search")] + if search_len > 0 { + format_string.push_str(SEARCH_BG); + format_string.push_str(&search_str); + } + + format_string +} + mod wrapping { // Test wrapping functions #[test] diff --git a/src/lib.rs b/src/lib.rs index 2e84499..90a3f74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -238,6 +238,7 @@ pub mod input; #[path = "core/mod.rs"] mod minus_core; mod pager; +pub mod screen; #[cfg(feature = "search")] pub mod search; mod state; @@ -246,16 +247,14 @@ mod static_pager; #[cfg(feature = "dynamic_output")] pub use dynamic_pager::dynamic_paging; -#[cfg(feature = "static_output")] -pub use static_pager::page_all; - +pub use error::MinusError; pub use minus_core::RunMode; +pub use pager::Pager; #[cfg(feature = "search")] pub use search::SearchMode; - -pub use error::MinusError; -pub use pager::Pager; pub use state::PagerState; +#[cfg(feature = "static_output")] +pub use static_pager::page_all; /// A convenient type for `Vec>` pub type ExitCallbacks = Vec>; @@ -305,7 +304,7 @@ pub enum LineNumbers { } impl LineNumbers { - const EXTRA_PADDING: usize = 5; + const EXTRA_PADDING: u16 = 5; /// Returns `true` if `self` can be inverted (i.e, `!self != self`), see /// the documentation for the variants to know if they are invertible or diff --git a/src/screen.rs b/src/screen.rs new file mode 100644 index 0000000..36aa8c0 --- /dev/null +++ b/src/screen.rs @@ -0,0 +1,61 @@ +use crossterm::{terminal, tty::IsTty}; +use std::{collections::BTreeSet, io::stdout}; + +use crate::{ + minus_core::{screen_line::ScreenLine, utils::text::format_prompt}, + MinusError, Result, +}; + +pub struct Screen { + screen_lines: Vec, + upper_mark: usize, + rows: usize, + cols: usize, + displayed_prompt: String, + prompt: String, +} + +impl Screen { + pub(crate) fn new() -> Result { + let (rows, cols); + if cfg!(test) { + // In tests, set number of columns to 80 and rows to 10 + cols = 80; + rows = 10; + } else if stdout().is_tty() { + // If a proper terminal is present, get size and set it + let size = terminal::size()?; + cols = size.0 as usize; + rows = size.1 as usize; + } else { + // For other cases beyond control + cols = 1; + rows = 1; + }; + + Ok(Self { + screen_lines: Vec::with_capacity(1024), + rows, + cols, + upper_mark: 1, + prompt: String::new(), + displayed_prompt: String::new(), + }) + } + + pub(crate) fn new_with_prompt(prompt: String) -> Result { + let mut screen = Self::new()?; + screen.prompt = prompt; + screen.displayed_prompt = format_prompt( + prompt, + screen.cols, + "", + None, + #[cfg(feature = "search")] + BTreeSet::new(), + #[cfg(feature = "search")] + 0, + ); + Ok(screen) + } +} diff --git a/src/state.rs b/src/state.rs index a6d7ca6..35c8d90 100644 --- a/src/state.rs +++ b/src/state.rs @@ -6,8 +6,9 @@ use crate::{ input::{self, HashedEventRegister}, minus_core::{ self, - utils::text::{self, AppendStyle}, + utils::text::{self, format_prompt, AppendStyle}, }, + screen::Screen, ExitStrategy, LineNumbers, }; use crossterm::{terminal, tty::IsTty}; @@ -73,7 +74,7 @@ pub struct PagerState { pub prefix_num: String, /// Describes whether minus is running and in which mode pub running: &'static Mutex, - + pub screen: Box, /// The output, flattened and formatted into the lines that should be displayed pub(crate) formatted_lines: Vec, /// Unterminated lines @@ -157,6 +158,7 @@ impl PagerState { .len() <= 5000 }); + let screen = Screen::new_with_prompt(prompt); let mut state = Self { lines: String::with_capacity(u16::MAX.into()), @@ -190,7 +192,7 @@ impl PagerState { lines_to_row_map: HashMap::new(), }; - state.format_prompt(); + state.update_displayed_prompt(); Ok(state) } @@ -243,76 +245,21 @@ impl PagerState { self.lines_to_row_map = format_result.lines_to_row_map; self.unterminated = format_result.num_unterminated; - self.format_prompt(); + self.update_displayed_prompt(); } /// Reformat the inputted prompt to how it should be displayed - pub(crate) fn format_prompt(&mut self) { - const SEARCH_BG: &str = "\x1b[34m"; - const INPUT_BG: &str = "\x1b[33m"; - - // Allocate the string. Add extra space in case for the - // ANSI escape things if we do have characters typed and search showing - let mut format_string = String::with_capacity(self.cols + (SEARCH_BG.len() * 2) + 4); - - // Get the string that will contain the search index/match indicator - #[cfg(feature = "search")] - let mut search_str = String::new(); - #[cfg(feature = "search")] - if !self.search_idx.is_empty() { - search_str.push(' '); - search_str.push_str(&(self.search_mark + 1).to_string()); - search_str.push('/'); - search_str.push_str(&self.search_idx.len().to_string()); - search_str.push(' '); - } - - // And get the string that will contain the prefix_num - let mut prefix_str = String::new(); - if !self.prefix_num.is_empty() { - prefix_str.push(' '); - prefix_str.push_str(&self.prefix_num); - prefix_str.push(' '); - } - - // And lastly, the string that contains the prompt or msg - let prompt_str = self.message.as_ref().unwrap_or(&self.prompt); - - #[cfg(feature = "search")] - let search_len = search_str.len(); - #[cfg(not(feature = "search"))] - let search_len = 0; - - // Calculate how much extra padding in the middle we need between - // the prompt/message and the indicators on the right - let prefix_len = prefix_str.len(); - let extra_space = self - .cols - .saturating_sub(search_len + prefix_len + prompt_str.len()); - let dsp_prompt: &str = if extra_space == 0 { - &prompt_str[..self.cols - search_len - prefix_len] - } else { - prompt_str - }; - - // push the prompt/msg - format_string.push_str(dsp_prompt); - format_string.push_str(&" ".repeat(extra_space)); - - // add the prefix_num if it exists - if prefix_len > 0 { - format_string.push_str(INPUT_BG); - format_string.push_str(&prefix_str); - } - - // and add the search indicator stuff if it exists - #[cfg(feature = "search")] - if search_len > 0 { - format_string.push_str(SEARCH_BG); - format_string.push_str(&search_str); - } - - self.displayed_prompt = format_string; + pub(crate) fn update_displayed_prompt(&mut self) { + self.displayed_prompt = format_prompt( + self.prompt, + self.cols, + &self.prefix_num, + self.message, + #[cfg(feature = "search")] + self.search_idx, + #[cfg(feature = "search")] + self.search_mark, + ); } /// Returns all the text within the bounds