meli/src/components/utilities/pager.rs

801 lines
28 KiB
Rust

/*
* meli - pager
*
* Copyright 2020 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
use melib::text_processing::LineBreakText;
/// A pager for text.
/// `Pager` holds its own content in its own `CellBuffer` and when `draw` is called, it draws the
/// current view of the text. It is responsible for scrolling etc.
#[derive(Default, Debug, Clone)]
pub struct Pager {
text: String,
cursor: (usize, usize),
reflow: Reflow,
height: usize,
width: usize,
minimum_width: usize,
search: Option<SearchPattern>,
dirty: bool,
colors: ThemeAttribute,
initialised: bool,
show_scrollbar: bool,
content: CellBuffer,
raw: bool,
text_lines: Vec<String>,
line_breaker: LineBreakText,
movement: Option<PageMovement>,
id: ComponentId,
}
impl fmt::Display for Pager {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", Pager::DESCRIPTION)
}
}
impl Pager {
pub const DESCRIPTION: &'static str = "pager";
const PAGES_AHEAD_TO_RENDER_NO: usize = 16;
pub fn new(context: &Context) -> Self {
let mut ret = Pager::default();
ret.minimum_width = context.settings.pager.minimum_width;
ret.set_colors(crate::conf::value(context, "theme_default"))
.set_reflow(if context.settings.pager.split_long_lines {
Reflow::All
} else {
Reflow::No
});
ret
}
pub fn set_show_scrollbar(&mut self, new_val: bool) -> &mut Self {
self.show_scrollbar = new_val;
self
}
pub fn set_colors(&mut self, new_val: ThemeAttribute) -> &mut Self {
self.colors = new_val;
self
}
pub fn set_reflow(&mut self, new_val: Reflow) -> &mut Self {
self.reflow = new_val;
self
}
pub fn set_initialised(&mut self, new_val: bool) -> &mut Self {
self.initialised = new_val;
self
}
pub fn reflow(&self) -> Reflow {
self.reflow
}
pub fn update_from_str(&mut self, text: &str, mut width: Option<usize>) {
if let Some(ref mut width) = width.as_mut() {
if **width < self.minimum_width {
**width = self.minimum_width;
}
}
self.text = text.to_string();
self.text_lines.clear();
self.line_breaker = LineBreakText::new(self.text.clone(), self.reflow, width);
self.height = 0;
self.width = 0;
self.search = None;
self.set_dirty(true);
self.initialised = false;
self.cursor = (0, 0);
}
pub fn from_string(
text: String,
context: Option<&Context>,
cursor_pos: Option<usize>,
mut width: Option<usize>,
colors: ThemeAttribute,
) -> Self {
let pager_filter: Option<&String> = if let Some(context) = context {
context.settings.pager.filter.as_ref()
} else {
None
};
let pager_minimum_width: usize = if let Some(context) = context {
context.settings.pager.minimum_width
} else {
0
};
let reflow: Reflow = if let Some(context) = context {
if context.settings.pager.split_long_lines {
Reflow::All
} else {
Reflow::No
}
} else {
Reflow::All
};
if let Some(ref mut width) = width.as_mut() {
if **width < pager_minimum_width {
**width = pager_minimum_width;
}
}
if let Some(content) = pager_filter.and_then(|bin| {
use std::io::Write;
use std::process::{Command, Stdio};
let mut filter_child = Command::new("sh")
.args(&["-c", bin])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start pager filter process");
let stdin = filter_child.stdin.as_mut().expect("failed to open stdin");
stdin
.write_all(text.as_bytes())
.expect("Failed to write to stdin");
let out = filter_child
.wait_with_output()
.expect("Failed to wait on filter")
.stdout;
let mut dev_null = std::fs::File::open("/dev/null").ok()?;
let mut embedded = crate::terminal::embed::EmbedGrid::new();
embedded.set_terminal_size((80, 20));
for b in out {
embedded.process_byte(&mut dev_null, b);
}
Some(std::mem::replace(embedded.buffer_mut(), Default::default()))
}) {
return Pager::from_buf(content, cursor_pos);
}
Pager {
text,
text_lines: vec![],
reflow,
cursor: (0, cursor_pos.unwrap_or(0)),
height: 1,
width: 1,
minimum_width: pager_minimum_width,
initialised: false,
dirty: true,
id: ComponentId::new_v4(),
raw: false,
colors,
..Default::default()
}
}
pub fn from_buf(content: CellBuffer, cursor_pos: Option<usize>) -> Self {
let (width, height) = content.size();
Pager {
text: String::new(),
cursor: (0, cursor_pos.unwrap_or(0)),
height,
width,
dirty: true,
content,
raw: true,
initialised: false,
id: ComponentId::new_v4(),
..Default::default()
}
}
pub fn cursor_pos(&self) -> usize {
self.cursor.1
}
pub fn size(&self) -> (usize, usize) {
(self.width, self.height)
}
pub fn initialise(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
let mut width = width!(area);
if width < self.minimum_width {
width = self.minimum_width;
}
if !self.raw {
if self.line_breaker.width() != Some(width.saturating_sub(4)) {
let line_breaker = LineBreakText::new(
self.text.clone(),
self.reflow,
Some(width.saturating_sub(4)),
);
self.line_breaker = line_breaker;
self.text_lines.clear();
};
self.height = self.text_lines.len();
self.width = width;
if let Some(ref mut search) = self.search {
use melib::text_processing::search::KMP;
search.positions.clear();
for (y, l) in self.text_lines.iter().enumerate() {
search.positions.extend(
l.kmp_search(&search.pattern)
.into_iter()
.map(|offset| (y, offset)),
);
}
if let Some(pos) = search.positions.get(search.cursor) {
if self.cursor.1 > pos.0 || self.cursor.1 + height!(area) < pos.0 {
self.cursor.1 = pos.0.saturating_sub(3);
}
}
}
self.draw_lines_up_to(
grid,
area,
context,
self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * height!(area),
);
}
self.draw_page(grid, area, context);
self.initialised = true;
}
pub fn draw_lines_up_to(
&mut self,
_grid: &mut CellBuffer,
area: Area,
_context: &mut Context,
up_to: usize,
) {
if self.line_breaker.is_finished() {
return;
}
let old_lines_no = self.text_lines.len();
if up_to == 0 {
self.text_lines.extend(self.line_breaker.by_ref());
} else {
if old_lines_no >= up_to + height!(area) {
return;
}
let new_lines_no = (up_to + height!(area)) - old_lines_no;
self.text_lines
.extend(self.line_breaker.by_ref().take(new_lines_no));
};
let new_lines_no = self.text_lines.len() - old_lines_no;
if let Some(ref mut search) = self.search {
use melib::text_processing::search::KMP;
for (y, l) in self.text_lines.iter().enumerate().skip(old_lines_no) {
search.positions.extend(
l.kmp_search(&search.pattern)
.into_iter()
.map(|offset| (y, offset)),
);
}
}
self.height += new_lines_no;
}
fn draw_page(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.raw {
copy_area(
grid,
&self.content,
area,
(
(
std::cmp::min(
self.cursor.0.saturating_sub(width!(area)),
self.content.size().0.saturating_sub(width!(area)),
),
std::cmp::min(
self.cursor.1.saturating_sub(height!(area)),
self.content.size().1.saturating_sub(height!(area)),
),
),
pos_dec(self.content.size(), (1, 1)),
),
);
return;
}
let (mut upper_left, bottom_right) = area;
for l in self
.text_lines
.iter()
.skip(self.cursor.1)
.take(height!(area) + 1)
{
write_string_to_grid(
l,
grid,
self.colors.fg,
self.colors.bg,
Attr::DEFAULT,
(upper_left, bottom_right),
None,
);
if l.starts_with("⤷") {
grid[upper_left]
.set_fg(Color::Byte(240))
.set_attrs(Attr::BOLD);
}
upper_left = pos_inc(upper_left, (0, 1));
}
if get_y(upper_left) <= get_y(bottom_right) {
clear_area(
grid,
(upper_left, bottom_right),
crate::conf::value(context, "theme_default"),
);
}
let (upper_left, _bottom_right) = area;
#[cfg(feature = "regexp")]
{
for text_formatter in crate::conf::text_format_regexps(context, "pager.envelope.body") {
let t = grid.insert_tag(text_formatter.tag);
for (i, l) in self
.text_lines
.iter()
.skip(self.cursor.1)
.enumerate()
.take(height!(area) + 1)
{
let i = i + get_y(upper_left);
for (start, end) in text_formatter.regexp.find_iter(l) {
let start = start + get_x(upper_left);
let end = end + get_x(upper_left);
grid.set_tag(t, (start, i), (end, i));
}
}
}
}
let cursor_line = self.cursor.1;
if let Some(ref mut search) = self.search {
let results_attr = crate::conf::value(context, "pager.highlight_search");
let results_current_attr =
crate::conf::value(context, "pager.highlight_search_current");
search.cursor = std::cmp::min(search.positions.len().saturating_sub(1), search.cursor);
for (i, (y, x)) in search
.positions
.iter()
.enumerate()
.filter(|(_, (y, _))| *y >= cursor_line)
.take(height!(area) + 1)
{
let x = *x + get_x(upper_left);
let y = *y - cursor_line;
for c in grid.row_iter(
x..x + search.pattern.grapheme_width(),
y + get_y(upper_left),
) {
if i == search.cursor {
grid[c]
.set_fg(results_current_attr.fg)
.set_bg(results_current_attr.bg)
.set_attrs(results_current_attr.attrs);
} else {
grid[c]
.set_fg(results_attr.fg)
.set_bg(results_attr.bg)
.set_attrs(results_attr.attrs);
}
}
}
}
}
}
impl Component for Pager {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if !is_valid_area!(area) {
return;
}
if !self.is_dirty() {
return;
}
if !self.initialised {
self.initialise(grid, area, context);
}
self.dirty = false;
let height = height!(area);
if let Some(mvm) = self.movement.take() {
match mvm {
PageMovement::Up(amount) => {
self.cursor.1 = self.cursor.1.saturating_sub(amount);
}
PageMovement::PageUp(multiplier) => {
self.cursor.1 = self.cursor.1.saturating_sub(height * multiplier);
}
PageMovement::Down(amount) => {
if self.cursor.1 + amount + 1 < self.height {
self.cursor.1 += amount;
} else {
self.cursor.1 = self.height.saturating_sub(1);
}
self.draw_lines_up_to(
grid,
area,
context,
self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * height,
);
}
PageMovement::PageDown(multiplier) => {
if self.cursor.1 + height * multiplier + 1 < self.height {
self.cursor.1 += height * multiplier;
} else if self.cursor.1 + height * multiplier > self.height {
self.cursor.1 = self.height.saturating_sub(1);
} else {
self.cursor.1 = (self.height / height) * height;
}
self.draw_lines_up_to(
grid,
area,
context,
self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * height,
);
}
PageMovement::Right(amount) => {
if self.cursor.0 + amount + 1 < self.width {
self.cursor.0 += amount;
} else {
self.cursor.0 = self.width.saturating_sub(1);
}
}
PageMovement::Left(amount) => {
self.cursor.0 = self.cursor.0.saturating_sub(amount);
}
PageMovement::Home => {
self.cursor.1 = 0;
}
PageMovement::End => {
self.draw_lines_up_to(grid, area, context, 0);
self.cursor.1 = self.height.saturating_sub(1);
}
}
}
if self.height == 0 || self.width == 0 {
return;
}
if let Some(ref mut search) = self.search {
if !search.positions.is_empty() {
if let Some(mvm) = search.movement.take() {
match mvm {
PageMovement::Up(_) => {
if self.cursor.1 > search.positions[search.cursor].0 {
self.cursor.1 = search.positions[search.cursor].0;
}
}
PageMovement::Down(_) => {
if self.cursor.1 + height < search.positions[search.cursor].0 {
self.cursor.1 = search.positions[search.cursor].0;
}
}
_ => {}
}
}
}
}
clear_area(grid, area, crate::conf::value(context, "theme_default"));
let (mut cols, mut rows) = (width!(area), height!(area));
if cols < 2 || rows < 2 {
return;
}
let (width, height) = (self.line_breaker.width().unwrap_or(cols), self.height);
if self.show_scrollbar && rows < height {
cols -= 1;
rows -= 1;
} else if self.search.is_some() {
rows -= 1;
}
if self.show_scrollbar && cols < width {
rows -= 1;
}
self.cursor = (
std::cmp::min(width.saturating_sub(cols), self.cursor.0),
std::cmp::min(height.saturating_sub(rows), self.cursor.1),
);
self.draw_page(
grid,
(upper_left!(area), pos_inc(upper_left!(area), (cols, rows))),
context,
);
if self.show_scrollbar && rows < height {
ScrollBar::default().set_show_arrows(true).draw(
grid,
(
set_x(upper_left!(area), get_x(bottom_right!(area))),
bottom_right!(area),
),
context,
/* position */
self.cursor.1,
/* visible_rows */
rows,
/* length */
height,
);
}
if self.show_scrollbar && cols < width {
ScrollBar::default().set_show_arrows(true).draw_horizontal(
grid,
(
set_y(upper_left!(area), get_y(bottom_right!(area))),
bottom_right!(area),
),
context,
self.cursor.0,
cols,
width,
);
}
if (rows < height) || self.search.is_some() {
const RESULTS_STR: &str = "Results for ";
let shown_lines = self.cursor.1 + rows;
let total_lines = height;
if rows < height {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::Update {
id: self.id,
context: ScrollContext {
shown_lines,
total_lines,
has_more_lines: !self.line_breaker.is_finished(),
},
},
)));
} else {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
};
if let Some(ref search) = self.search {
let status_message = format!(
"{results_str}{search_pattern}: {current_pos}/{total_results}{has_more_lines}",
results_str = RESULTS_STR,
search_pattern = &search.pattern,
current_pos = if search.positions.is_empty() {
0
} else {
search.cursor + 1
},
total_results = search.positions.len(),
has_more_lines = if self.line_breaker.is_finished() {
""
} else {
"(+)"
}
);
let mut attribute = crate::conf::value(context, "status.bar");
if !context.settings.terminal.use_color() {
attribute.attrs |= Attr::REVERSE;
}
let (_, y) = write_string_to_grid(
&status_message,
grid,
attribute.fg,
attribute.bg,
attribute.attrs,
(
set_y(upper_left!(area), get_y(bottom_right!(area))),
bottom_right!(area),
),
None,
);
/* set search pattern to italics */
let start_x = get_x(upper_left!(area)) + RESULTS_STR.len();
for c in grid.row_iter(start_x..(start_x + search.pattern.grapheme_width()), y) {
grid[c].set_attrs(attribute.attrs | Attr::ITALICS);
}
};
}
context.dirty_areas.push_back(area);
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
let shortcuts = self.get_shortcuts(context);
match event {
UIEvent::ConfigReload { old_settings: _ } => {
self.set_colors(crate::conf::value(context, "theme_default"));
self.set_dirty(true);
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Self::DESCRIPTION]["scroll_up"]) =>
{
self.movement = Some(PageMovement::Up(1));
self.dirty = true;
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Self::DESCRIPTION]["scroll_down"]) =>
{
self.movement = Some(PageMovement::Down(1));
self.dirty = true;
return true;
}
UIEvent::Input(Key::Home) => {
self.movement = Some(PageMovement::Home);
self.dirty = true;
return true;
}
UIEvent::Input(Key::End) => {
self.movement = Some(PageMovement::End);
self.dirty = true;
return true;
}
UIEvent::Input(Key::Left) => {
self.movement = Some(PageMovement::Left(1));
self.dirty = true;
return true;
}
UIEvent::Input(Key::Right) => {
self.movement = Some(PageMovement::Right(1));
self.dirty = true;
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Self::DESCRIPTION]["page_up"]) =>
{
self.movement = Some(PageMovement::PageUp(1));
self.dirty = true;
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Self::DESCRIPTION]["page_down"]) =>
{
self.movement = Some(PageMovement::PageDown(1));
self.dirty = true;
return true;
}
UIEvent::ChangeMode(UIMode::Normal) => {
self.dirty = true;
}
UIEvent::Action(View(Pipe(ref bin, ref args))) => {
use std::io::Write;
use std::process::{Command, Stdio};
let mut command_obj = match Command::new(bin)
.args(args.as_slice())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
{
Ok(o) => o,
Err(e) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Could not pipe to {}: {}",
bin, e
)),
));
return true;
}
};
let stdin = command_obj.stdin.as_mut().expect("failed to open stdin");
stdin
.write_all(self.text.as_bytes())
.expect("Failed to write to stdin");
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!(
"Pager text piped to '{}{}{}'",
&bin,
if args.is_empty() { "" } else { " " },
args.join(" ")
))));
return true;
}
UIEvent::Action(Action::Listing(ListingAction::Search(pattern))) => {
self.search = Some(SearchPattern {
pattern: pattern.to_string(),
positions: vec![],
cursor: 0,
movement: Some(PageMovement::Home),
});
self.initialised = false;
self.dirty = true;
return true;
}
UIEvent::Input(Key::Char('n')) if self.search.is_some() => {
if let Some(ref mut search) = self.search {
search.movement = Some(PageMovement::Down(1));
search.cursor += 1;
} else {
unsafe {
std::hint::unreachable_unchecked();
}
}
self.initialised = false;
self.dirty = true;
return true;
}
UIEvent::Input(Key::Char('N')) if self.search.is_some() => {
if let Some(ref mut search) = self.search {
search.movement = Some(PageMovement::Up(1));
search.cursor = search.cursor.saturating_sub(1);
} else {
unsafe {
std::hint::unreachable_unchecked();
}
}
self.initialised = false;
self.dirty = true;
return true;
}
UIEvent::Input(Key::Esc) if self.search.is_some() => {
self.search = None;
self.initialised = false;
self.dirty = true;
return true;
}
UIEvent::Resize => {
self.initialised = false;
self.dirty = true;
}
UIEvent::VisibilityChange(false) => {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
_ => {}
}
false
}
fn is_dirty(&self) -> bool {
self.dirty
}
fn set_dirty(&mut self, value: bool) {
self.dirty = value;
}
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
let config_map: IndexMap<&'static str, Key> = context.settings.shortcuts.pager.key_values();
let mut ret: ShortcutMaps = Default::default();
ret.insert(Pager::DESCRIPTION, config_map);
ret
}
fn id(&self) -> ComponentId {
self.id
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
}