pub(crate) mod term;
use crossterm::{
cursor::MoveTo,
style::Attribute,
terminal::{Clear, ClearType},
};
use std::{convert::TryFrom, io};
use crate::{AlternateScreenPagingError, Pager};
#[cfg(feature = "search")]
use crate::search::highlight_line_matches;
pub(crate) fn draw(
out: &mut impl io::Write,
mut pager: &mut Pager,
) -> Result<(), AlternateScreenPagingError> {
if pager.run_no_overflow && pager.num_lines() <= pager.rows {
return write_lines(out, &mut pager);
}
write!(out, "{}{}", Clear(ClearType::All), MoveTo(0, 0))?;
write_lines(out, &mut pager)?;
let prompt = pager
.message
.0
.as_ref()
.map_or_else(|| pager.prompt.clone(), std::clone::Clone::clone);
{
write!(
out,
"{mv}\r{rev}{prompt}{reset}",
mv = MoveTo(0, u16::try_from(pager.rows).unwrap()),
rev = Attribute::Reverse,
prompt = prompt.first().unwrap(),
reset = Attribute::Reset,
)?;
}
out.flush().map_err(AlternateScreenPagingError::Draw)
}
pub(crate) fn write_lines(
out: &mut impl io::Write,
mut pager: &mut Pager,
) -> Result<(), AlternateScreenPagingError> {
let line_count = pager.num_lines();
let rows = pager.rows.saturating_sub(1);
let lower_mark = pager.upper_mark.saturating_add(rows.min(line_count));
if lower_mark > line_count {
pager.upper_mark = if line_count < pager.rows {
0
} else {
line_count.saturating_sub(rows)
};
}
let displayed_lines = match pager.line_numbers {
LineNumbers::AlwaysOff | LineNumbers::Disabled => {
#[cfg_attr(not(feature = "search"), allow(unused_mut))]
let mut lines = pager
.get_flattened_lines()
.skip(pager.upper_mark)
.take(rows.min(line_count))
.collect::<Vec<String>>();
#[cfg(feature = "search")]
if let Some(st) = &pager.search_term {
for mut line in &mut lines {
highlight_line_matches(&mut line, st);
}
}
lines
}
LineNumbers::AlwaysOn | LineNumbers::Enabled => {
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
let len_line_number = (line_count as f64).log10().floor() as usize + 1;
annotate_line_numbers(
pager.get_lines(),
len_line_number,
pager.cols,
#[cfg(feature = "search")]
&pager.search_term,
)
.iter()
.skip(pager.upper_mark)
.take(rows.min(line_count))
.map(ToOwned::to_owned)
.collect()
}
};
writeln!(out, "\r{}", displayed_lines.join("\n\r"))?;
Ok(())
}
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum LineNumbers {
AlwaysOn,
Enabled,
Disabled,
AlwaysOff,
}
impl LineNumbers {
#[allow(dead_code)]
fn is_invertible(self) -> bool {
matches!(self, Self::Enabled | Self::Disabled)
}
}
impl std::ops::Not for LineNumbers {
type Output = Self;
fn not(self) -> Self::Output {
use LineNumbers::{Disabled, Enabled};
match self {
Enabled => Disabled,
Disabled => Enabled,
ln => ln,
}
}
}
fn annotate_line_numbers(
mut lines: Vec<Vec<String>>,
len_line_number: usize,
cols: usize,
#[cfg(feature = "search")] search_term: &Option<regex::Regex>,
) -> Vec<String> {
let padding = len_line_number + 3;
for (idx, line) in lines.iter_mut().enumerate() {
crate::rewrap(line, cols.saturating_sub(padding));
#[cfg_attr(not(feature = "search"), allow(unused_mut))]
for mut row in line.iter_mut() {
#[cfg(feature = "search")]
if let Some(st) = search_term {
highlight_line_matches(&mut row, st);
}
let fmt_numbers = if cfg!(not(test)) {
format!(
" {bold}{number: >len$}.{reset} ",
bold = crossterm::style::Attribute::Bold,
number = idx + 1,
len = len_line_number,
reset = crossterm::style::Attribute::Reset
)
} else {
format!(
" {number: >len$}. ",
number = idx + 1,
len = len_line_number,
)
};
row.insert_str(0, &fmt_numbers);
}
}
lines.iter().flatten().map(ToOwned::to_owned).collect()
}
#[cfg(test)]
mod tests;