tmux-copyrat/src/ui.rs

1110 lines
38 KiB
Rust
Raw Normal View History

2020-06-02 20:03:16 +02:00
use std::char;
2020-05-28 09:24:33 +02:00
use std::io;
2020-05-27 10:04:42 +02:00
use std::str::FromStr;
2020-06-01 09:33:40 +02:00
use clap::Clap;
use sequence_trie::SequenceTrie;
2020-05-28 09:24:33 +02:00
use termion::{self, color, cursor, event, style};
2020-06-02 20:03:16 +02:00
2020-05-31 22:45:36 +02:00
use crate::error::ParseError;
2020-06-01 09:33:40 +02:00
use crate::{colors, model};
2020-05-31 22:45:36 +02:00
2020-06-01 15:25:54 +02:00
pub struct Ui<'a> {
2020-06-01 09:33:40 +02:00
model: &'a mut model::Model<'a>,
2020-06-02 13:10:48 +02:00
term_width: u16,
line_offsets: Vec<usize>,
2020-06-01 09:33:40 +02:00
matches: Vec<model::Match<'a>>,
2020-05-31 22:45:36 +02:00
lookup_trie: SequenceTrie<char, usize>,
2020-05-25 23:06:00 +02:00
focus_index: usize,
2020-06-01 10:06:50 +02:00
focus_wrap_around: bool,
2020-06-01 15:25:54 +02:00
rendering_colors: &'a UiColors,
2020-06-01 15:34:54 +02:00
hint_alignment: &'a HintAlignment,
2020-05-25 23:06:00 +02:00
hint_style: Option<HintStyle>,
2020-05-23 09:30:09 +02:00
}
2020-06-01 15:25:54 +02:00
impl<'a> Ui<'a> {
2020-05-25 23:06:00 +02:00
pub fn new(
2020-06-01 09:33:40 +02:00
model: &'a mut model::Model<'a>,
2020-05-26 08:11:45 +02:00
unique_hint: bool,
2020-06-01 10:06:50 +02:00
focus_wrap_around: bool,
2020-06-01 15:25:54 +02:00
rendering_colors: &'a UiColors,
2020-06-01 15:34:54 +02:00
hint_alignment: &'a HintAlignment,
2020-05-25 23:06:00 +02:00
hint_style: Option<HintStyle>,
2020-06-01 15:25:54 +02:00
) -> Ui<'a> {
2020-06-01 09:33:40 +02:00
let matches = model.matches(unique_hint);
let lookup_trie = model::Model::build_lookup_trie(&matches);
let focus_index = if model.reverse { matches.len() - 1 } else { 0 };
2020-05-25 23:06:00 +02:00
let (term_width, _) = termion::terminal_size().unwrap_or((80u16, 30u16)); // .expect("Cannot read the terminal size.");
2020-06-02 13:10:48 +02:00
let line_offsets = get_line_offsets(&model.lines, term_width);
2020-06-01 15:25:54 +02:00
Ui {
2020-06-01 09:33:40 +02:00
model,
2020-06-02 13:10:48 +02:00
term_width,
line_offsets,
2020-05-25 23:06:00 +02:00
matches,
2020-05-31 22:45:36 +02:00
lookup_trie,
2020-05-25 23:06:00 +02:00
focus_index,
2020-06-01 10:06:50 +02:00
focus_wrap_around,
2020-05-25 23:06:00 +02:00
rendering_colors,
2020-06-01 15:34:54 +02:00
hint_alignment,
2020-05-25 23:06:00 +02:00
hint_style,
}
2020-06-02 20:03:16 +02:00
}
2020-06-02 13:10:48 +02:00
/// Convert the `Match` text into the coordinates of the wrapped lines.
///
/// Compute the new x offset of the text as the remainder of the line width
/// (e.g. the match could start at offset 120 in a 80-width terminal, the new
/// offset being 40).
///
/// Compute the new y offset of the text as the initial y offset plus any
/// additional offset due to previous split lines. This is obtained thanks to
/// the `offset_per_line` member.
pub fn map_coords_to_wrapped_space(&self, offset_x: usize, offset_y: usize) -> (usize, usize) {
let line_width = self.term_width as usize;
let new_offset_x = offset_x % line_width;
let new_offset_y =
self.line_offsets.get(offset_y as usize).unwrap() + offset_x / line_width;
(new_offset_x, new_offset_y)
}
2020-06-01 11:57:23 +02:00
/// Move focus onto the previous hint, returning both the index of the
/// previously focused match, and the index of the newly focused one.
fn prev_focus_index(&mut self) -> (usize, usize) {
let old_index = self.focus_index;
2020-06-01 10:06:50 +02:00
if self.focus_wrap_around {
if self.focus_index == 0 {
self.focus_index = self.matches.len() - 1;
} else {
self.focus_index -= 1;
}
} else {
if self.focus_index > 0 {
self.focus_index -= 1;
}
2020-05-25 23:06:00 +02:00
}
2020-06-01 11:57:23 +02:00
let new_index = self.focus_index;
(old_index, new_index)
2020-06-02 20:03:16 +02:00
}
2020-06-01 11:57:23 +02:00
/// Move focus onto the next hint, returning both the index of the
/// previously focused match, and the index of the newly focused one.
fn next_focus_index(&mut self) -> (usize, usize) {
let old_index = self.focus_index;
2020-06-01 10:06:50 +02:00
if self.focus_wrap_around {
if self.focus_index == self.matches.len() - 1 {
self.focus_index = 0;
} else {
self.focus_index += 1;
}
} else {
if self.focus_index < self.matches.len() - 1 {
self.focus_index += 1;
}
2020-05-25 23:06:00 +02:00
}
2020-06-01 11:57:23 +02:00
let new_index = self.focus_index;
(old_index, new_index)
}
/// Returns screen offset of a given `Match`.
///
/// If multibyte characters occur before the hint (in the "prefix"), then
/// their compouding takes less space on screen when printed: for
/// instance ´ + e = é. Consequently the hint offset has to be adjusted
/// to the left.
fn match_offsets(&self, mat: &model::Match<'a>) -> (usize, usize) {
let offset_x = {
let line = &self.model.lines[mat.y as usize];
let prefix = &line[0..mat.x as usize];
let adjust = prefix.len() - prefix.chars().count();
(mat.x as usize) - (adjust)
};
let offset_y = mat.y as usize;
(offset_x, offset_y)
2020-06-02 20:03:16 +02:00
}
2020-05-25 23:06:00 +02:00
2020-06-01 09:33:40 +02:00
/// Render entire model lines on provided writer.
2020-05-25 23:06:00 +02:00
///
/// This renders the basic content on which matches and hints can be rendered.
///
/// # Notes
/// - All trailing whitespaces are trimmed, empty lines are skipped.
/// - This writes directly on the writer, avoiding extra allocation.
2020-06-02 13:10:48 +02:00
fn render_base_text(
stdout: &mut dyn io::Write,
lines: &Vec<&str>,
line_offsets: &Vec<usize>,
colors: &UiColors,
) -> () {
write!(
stdout,
"{bg_color}{fg_color}",
fg_color = color::Fg(colors.text_fg.as_ref()),
bg_color = color::Bg(colors.text_bg.as_ref()),
)
.unwrap();
2020-06-02 13:10:48 +02:00
for (line_index, line) in lines.iter().enumerate() {
2020-05-25 23:06:00 +02:00
let trimmed_line = line.trim_end();
if !trimmed_line.is_empty() {
2020-06-02 13:10:48 +02:00
let offset_y: usize =
*(line_offsets.get(line_index)).expect("Cannot get offset_per_line.");
2020-05-25 23:06:00 +02:00
write!(
stdout,
"{goto}{text}",
2020-06-02 13:10:48 +02:00
goto = cursor::Goto(1, offset_y as u16 + 1),
2020-05-25 23:06:00 +02:00
text = &trimmed_line,
)
.unwrap();
}
}
write!(
stdout,
"{fg_reset}{bg_reset}",
fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset),
)
.unwrap();
2020-05-22 15:03:39 +02:00
}
2020-05-25 23:06:00 +02:00
2020-05-29 20:36:27 +02:00
/// Render the Match's `text` field on provided writer using the `match_*g` color.
2020-05-25 23:06:00 +02:00
///
2020-05-29 20:36:27 +02:00
/// If a Mach is "focused", it is then rendered with the `focused_*g` colors.
2020-05-25 23:06:00 +02:00
///
/// # Note
2020-06-01 11:57:23 +02:00
///
2020-05-25 23:06:00 +02:00
/// This writes directly on the writer, avoiding extra allocation.
fn render_matched_text(
2020-05-28 09:24:33 +02:00
stdout: &mut dyn io::Write,
2020-05-25 23:06:00 +02:00
text: &str,
focused: bool,
offset: (usize, usize),
2020-06-01 15:25:54 +02:00
colors: &UiColors,
2020-05-25 23:06:00 +02:00
) {
// To help identify it, the match thas has focus is rendered with a dedicated color.
2020-05-29 20:36:27 +02:00
let (fg_color, bg_color) = if focused {
2020-05-25 23:06:00 +02:00
(&colors.focused_fg, &colors.focused_bg)
} else {
(&colors.match_fg, &colors.match_bg)
};
// Render just the Match's text on top of existing content.
2020-05-22 15:03:39 +02:00
write!(
stdout,
2020-05-25 23:06:00 +02:00
"{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}",
2020-05-22 15:03:39 +02:00
goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1),
2020-05-29 20:36:27 +02:00
fg_color = color::Fg(fg_color.as_ref()),
bg_color = color::Bg(bg_color.as_ref()),
2020-05-25 23:06:00 +02:00
fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset),
text = &text,
)
.unwrap();
2020-06-02 20:03:16 +02:00
}
2020-05-25 23:06:00 +02:00
/// Render a Match's `hint` field on the provided writer.
///
/// This renders the hint according to some provided style:
/// - just colors
/// - underlined with colors
/// - surrounding the hint's text with some delimiters, see
/// `HintStyle::Delimited`.
///
/// # Note
/// This writes directly on the writer, avoiding extra allocation.
fn render_matched_hint(
2020-05-28 09:24:33 +02:00
stdout: &mut dyn io::Write,
2020-05-25 23:06:00 +02:00
hint_text: &str,
offset: (usize, usize),
2020-06-01 15:25:54 +02:00
colors: &UiColors,
2020-05-25 23:06:00 +02:00
hint_style: &Option<HintStyle>,
) {
let fg_color = color::Fg(colors.hint_fg.as_ref());
let bg_color = color::Bg(colors.hint_bg.as_ref());
let fg_reset = color::Fg(color::Reset);
let bg_reset = color::Bg(color::Reset);
2020-06-02 11:17:35 +02:00
let goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1);
2020-05-25 23:06:00 +02:00
match hint_style {
None => {
write!(
stdout,
"{goto}{bg_color}{fg_color}{hint}{fg_reset}{bg_reset}",
2020-06-02 11:17:35 +02:00
goto = goto,
2020-05-25 23:06:00 +02:00
fg_color = fg_color,
bg_color = bg_color,
fg_reset = fg_reset,
bg_reset = bg_reset,
hint = hint_text,
)
.unwrap();
}
Some(hint_style) => match hint_style {
2020-05-29 16:23:46 +02:00
HintStyle::Bold => {
write!(
stdout,
"{goto}{bg_color}{fg_color}{sty}{hint}{sty_reset}{fg_reset}{bg_reset}",
2020-06-02 11:17:35 +02:00
goto = goto,
2020-05-29 16:23:46 +02:00
fg_color = fg_color,
bg_color = bg_color,
fg_reset = fg_reset,
bg_reset = bg_reset,
sty = style::Bold,
sty_reset = style::NoBold,
hint = hint_text,
)
.unwrap();
}
2020-05-28 07:07:51 +02:00
HintStyle::Italic => {
write!(
stdout,
"{goto}{bg_color}{fg_color}{sty}{hint}{sty_reset}{fg_reset}{bg_reset}",
2020-06-02 11:17:35 +02:00
goto = goto,
2020-05-28 07:07:51 +02:00
fg_color = fg_color,
bg_color = bg_color,
fg_reset = fg_reset,
bg_reset = bg_reset,
sty = style::Italic,
sty_reset = style::NoItalic,
hint = hint_text,
)
.unwrap();
}
2020-05-25 23:06:00 +02:00
HintStyle::Underline => {
write!(
stdout,
"{goto}{bg_color}{fg_color}{sty}{hint}{sty_reset}{fg_reset}{bg_reset}",
2020-06-02 11:17:35 +02:00
goto = goto,
2020-05-25 23:06:00 +02:00
fg_color = fg_color,
bg_color = bg_color,
fg_reset = fg_reset,
bg_reset = bg_reset,
sty = style::Underline,
sty_reset = style::NoUnderline,
hint = hint_text,
)
.unwrap();
}
HintStyle::Surround(opening, closing) => {
write!(
stdout,
"{goto}{bg_color}{fg_color}{bra}{hint}{bra_close}{fg_reset}{bg_reset}",
2020-06-02 11:17:35 +02:00
goto = goto,
2020-05-25 23:06:00 +02:00
fg_color = fg_color,
bg_color = bg_color,
fg_reset = fg_reset,
bg_reset = bg_reset,
bra = opening,
bra_close = closing,
hint = hint_text,
)
.unwrap();
}
},
}
2020-06-02 20:03:16 +02:00
}
2020-06-01 11:57:23 +02:00
/// Convenience function that renders both the matched text and its hint,
/// if focused.
fn render_match(&self, stdout: &mut dyn io::Write, mat: &model::Match<'a>, focused: bool) {
let text = mat.text;
let (offset_x, offset_y) = self.match_offsets(mat);
2020-06-02 13:10:48 +02:00
let (offset_x, offset_y) = self.map_coords_to_wrapped_space(offset_x, offset_y);
2020-06-01 11:57:23 +02:00
2020-06-01 15:25:54 +02:00
Ui::render_matched_text(
2020-06-01 11:57:23 +02:00
stdout,
text,
focused,
(offset_x, offset_y),
&self.rendering_colors,
);
if !focused {
// If not focused, render the hint (e.g. "eo") as an overlay on
// top of the rendered matched text, aligned at its leading or the
// trailing edge.
let extra_offset = match self.hint_alignment {
HintAlignment::Leading => 0,
HintAlignment::Trailing => text.len() - mat.hint.len(),
};
2020-06-01 15:25:54 +02:00
Ui::render_matched_hint(
2020-06-01 11:57:23 +02:00
stdout,
&mat.hint,
(offset_x + extra_offset, offset_y),
&self.rendering_colors,
&self.hint_style,
);
}
}
2020-06-01 15:25:54 +02:00
/// Full nender the Ui on the provided writer.
2020-05-25 23:06:00 +02:00
///
/// This renders in 3 phases:
/// - all lines are rendered verbatim
/// - each Match's `text` is rendered as an overlay on top of it
/// - each Match's `hint` text is rendered as a final overlay
///
/// Depending on the value of `self.hint_alignment`, the hint can be rendered
/// on the leading edge of the underlying Match's `text`,
/// or on the trailing edge.
///
/// # Note
/// Multibyte characters are taken into account, so that the Match's `text`
/// and `hint` are rendered in their proper position.
2020-05-29 20:36:27 +02:00
fn full_render(&self, stdout: &mut dyn io::Write) -> () {
2020-05-25 23:06:00 +02:00
// 1. Trim all lines and render non-empty ones.
2020-06-02 13:10:48 +02:00
Ui::render_base_text(
stdout,
&self.model.lines,
&self.line_offsets,
&self.rendering_colors,
);
2020-05-25 23:06:00 +02:00
for (index, mat) in self.matches.iter().enumerate() {
let focused = index == self.focus_index;
2020-06-01 11:57:23 +02:00
self.render_match(stdout, mat, focused);
}
2020-05-25 23:06:00 +02:00
2020-06-01 11:57:23 +02:00
stdout.flush().unwrap();
}
2020-05-25 23:06:00 +02:00
2020-06-01 11:57:23 +02:00
/// Render the previous match with its hint, and render the newly focused
/// match without its hint. This is more efficient than a full render.
fn diff_render(
&self,
stdout: &mut dyn io::Write,
old_focus_index: usize,
new_focus_index: usize,
) {
// Render the previously focused match as non-focused
let mat = self.matches.get(old_focus_index).unwrap();
let focused = false;
self.render_match(stdout, mat, focused);
2020-06-01 11:57:23 +02:00
// Render the previously focused match as non-focused
let mat = self.matches.get(new_focus_index).unwrap();
let focused = true;
self.render_match(stdout, mat, focused);
2020-05-25 23:06:00 +02:00
stdout.flush().unwrap();
2020-06-02 20:03:16 +02:00
}
2020-05-29 18:43:17 +02:00
/// Listen to keys entered on stdin, moving focus accordingly, or
/// selecting one match.
2020-05-25 23:06:00 +02:00
///
/// # Panics
2020-05-29 18:43:17 +02:00
///
/// - This function panics if termion cannot read the entered keys on stdin.
fn listen(&mut self, reader: &mut dyn io::Read, writer: &mut dyn io::Write) -> Event {
2020-05-28 09:24:33 +02:00
use termion::input::TermRead; // Trait for `reader.keys().next()`.
2020-05-25 23:06:00 +02:00
if self.matches.is_empty() {
2020-05-29 18:43:17 +02:00
return Event::Exit;
}
2020-05-29 18:43:17 +02:00
let mut typed_hint = String::new();
2020-05-29 20:36:27 +02:00
self.full_render(writer);
2020-05-25 23:06:00 +02:00
loop {
// This is an option of a result of a key... Let's pop error cases first.
2020-05-28 09:24:33 +02:00
let next_key = reader.keys().next();
2020-05-25 23:06:00 +02:00
if next_key.is_none() {
// Nothing in the buffer. Wait for a bit...
std::thread::sleep(std::time::Duration::from_millis(25));
2020-05-25 23:06:00 +02:00
continue;
}
2020-05-25 23:06:00 +02:00
let key_res = next_key.unwrap();
if let Err(err) = key_res {
// Termion not being able to read from stdin is an unrecoverable error.
panic!(err);
}
2020-05-25 23:06:00 +02:00
match key_res.unwrap() {
2020-05-28 09:24:33 +02:00
event::Key::Esc => {
2020-05-29 18:43:17 +02:00
break;
2020-05-25 23:06:00 +02:00
}
// Move focus to next/prev match.
2020-06-01 11:57:23 +02:00
event::Key::Up => {
let (old_index, focused_index) = self.prev_focus_index();
self.diff_render(writer, old_index, focused_index);
}
event::Key::Down => {
let (old_index, focused_index) = self.next_focus_index();
self.diff_render(writer, old_index, focused_index);
}
event::Key::Left => {
let (old_index, focused_index) = self.prev_focus_index();
self.diff_render(writer, old_index, focused_index);
}
event::Key::Right => {
let (old_index, focused_index) = self.next_focus_index();
self.diff_render(writer, old_index, focused_index);
}
2020-05-30 22:23:33 +02:00
event::Key::Char(_ch @ 'n') => {
2020-06-01 11:57:23 +02:00
let (old_index, focused_index) = if self.model.reverse {
self.prev_focus_index()
2020-05-30 22:23:33 +02:00
} else {
2020-06-01 11:57:23 +02:00
self.next_focus_index()
};
self.diff_render(writer, old_index, focused_index);
2020-05-30 22:23:33 +02:00
}
event::Key::Char(_ch @ 'N') => {
2020-06-01 11:57:23 +02:00
let (old_index, focused_index) = if self.model.reverse {
self.next_focus_index()
2020-05-30 22:23:33 +02:00
} else {
2020-06-01 11:57:23 +02:00
self.prev_focus_index()
};
self.diff_render(writer, old_index, focused_index);
2020-05-30 22:23:33 +02:00
}
2020-05-30 19:28:54 +02:00
2020-06-01 11:57:23 +02:00
// Yank/copy
2020-06-01 10:33:48 +02:00
event::Key::Char(_ch @ 'y') => {
let text = self.matches.get(self.focus_index).unwrap().text;
return Event::Match((text.to_string(), false));
}
event::Key::Char(_ch @ 'Y') => {
let text = self.matches.get(self.focus_index).unwrap().text;
return Event::Match((text.to_string(), true));
}
2020-05-29 18:43:17 +02:00
// TODO: use a Trie or another data structure to determine
// if the entered key belongs to a longer hint.
// Attempts at finding a match with a corresponding hint.
2020-05-28 09:24:33 +02:00
event::Key::Char(ch) => {
2020-05-25 23:06:00 +02:00
let key = ch.to_string();
let lower_key = key.to_lowercase();
typed_hint.push_str(&lower_key);
let node = self
2020-05-31 22:45:36 +02:00
.lookup_trie
.get_node(&typed_hint.chars().collect::<Vec<char>>());
if node.is_none() {
// An unknown key was entered.
return Event::Exit;
}
let node = node.unwrap();
if node.is_leaf() {
// The last key of a hint was entered.
let match_index = node.value().expect(
"By construction, the Lookup Trie should have a value for each leaf.",
);
let mat = self.matches.get(*match_index).expect("By construction, the value in a leaf should correspond to an existing hint.");
let text = mat.text.to_string();
let uppercased = key != lower_key;
return Event::Match((text, uppercased));
} else {
// The prefix of a hint was entered, but we
// still need more keys.
continue;
2020-05-25 23:06:00 +02:00
}
}
// Unknown keys are ignored.
_ => (),
}
2020-06-01 11:57:23 +02:00
// End of event processing loop.
2020-05-25 23:06:00 +02:00
}
2020-06-02 20:03:16 +02:00
2020-05-29 18:43:17 +02:00
Event::Exit
2020-06-02 20:03:16 +02:00
}
2020-06-01 15:25:54 +02:00
/// Configure the terminal and display the `Ui`.
2020-05-29 18:43:17 +02:00
///
/// - Setup steps: switch to alternate screen, switch to raw mode, hide the cursor.
/// - Teardown steps: show cursor, back to main screen.
pub fn present(&mut self) -> Option<(String, bool)> {
2020-05-28 09:24:33 +02:00
use std::io::Write;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
let mut stdin = termion::async_stdin();
let mut stdout = AlternateScreen::from(
io::stdout()
.into_raw_mode()
.expect("Cannot access alternate screen."),
);
2020-06-02 20:03:16 +02:00
2020-05-29 18:43:17 +02:00
write!(stdout, "{}", cursor::Hide).unwrap();
let selection = match self.listen(&mut stdin, &mut stdout) {
Event::Exit => None,
Event::Match((text, uppercased)) => Some((text, uppercased)),
2020-05-25 23:06:00 +02:00
};
2020-06-02 20:03:16 +02:00
2020-05-25 23:06:00 +02:00
write!(stdout, "{}", cursor::Show).unwrap();
2020-06-02 20:03:16 +02:00
2020-05-29 18:43:17 +02:00
selection
2020-05-25 23:06:00 +02:00
}
2020-06-02 20:03:16 +02:00
}
2020-06-02 13:10:48 +02:00
/// Compute each line's actual y offset if displayed in a terminal of width
/// `term_width`.
fn get_line_offsets(lines: &Vec<&str>, term_width: u16) -> Vec<usize> {
lines
.iter()
.scan(0, |offset, &line| {
let value = *offset;
// amount of extra y space taken by this line
let extra = line.trim_end().len() / term_width as usize;
*offset = *offset + 1 + extra;
Some(value)
})
.collect()
}
2020-06-01 10:14:16 +02:00
/// Describes if, during rendering, a hint should aligned to the leading edge of
/// the matched text, or to its trailing edge.
#[derive(Debug, Clap)]
pub enum HintAlignment {
Leading,
Trailing,
}
impl FromStr for HintAlignment {
type Err = ParseError;
fn from_str(s: &str) -> Result<HintAlignment, ParseError> {
match s {
"leading" => Ok(HintAlignment::Leading),
"trailing" => Ok(HintAlignment::Trailing),
_ => Err(ParseError::ExpectedString(String::from(
"leading or trailing",
))),
}
}
}
/// Describes the style of contrast to be used during rendering of the hint's
/// text.
///
/// # Note
/// In practice, this is wrapped in an `Option`, so that the hint's text can be rendered with no style.
pub enum HintStyle {
/// The hint's text will be bold (leveraging `termion::style::Bold`).
Bold,
/// The hint's text will be italicized (leveraging `termion::style::Italic`).
Italic,
/// The hint's text will be underlined (leveraging `termion::style::Underline`).
Underline,
/// The hint's text will be surrounded by these chars.
Surround(char, char),
}
2020-06-01 15:25:54 +02:00
/// Returned value after the `Ui` has finished listening to events.
2020-06-01 10:14:16 +02:00
enum Event {
/// Exit with no selected matches,
Exit,
/// A vector of matched text and whether it was selected with uppercase.
Match((String, bool)),
}
2020-06-02 20:03:16 +02:00
#[cfg(test)]
mod tests {
2020-05-25 23:06:00 +02:00
use super::*;
use crate::alphabets;
2020-06-02 20:03:16 +02:00
2020-05-25 23:06:00 +02:00
#[test]
fn test_render_all_lines() {
let content = "some text
2020-05-22 15:03:39 +02:00
* e006b06 - (12 days ago) swapper: Make quotes
path: /usr/local/bin/git
path: /usr/local/bin/cargo";
2020-05-25 23:06:00 +02:00
let lines: Vec<&str> = content.split('\n').collect();
2020-06-02 13:10:48 +02:00
let line_offsets: Vec<usize> = (0..lines.len()).collect();
2020-06-01 15:25:54 +02:00
let colors = UiColors {
2020-05-29 20:36:27 +02:00
text_fg: Box::new(color::Black),
text_bg: Box::new(color::White),
focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan),
};
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
let mut writer = vec![];
2020-06-02 13:10:48 +02:00
Ui::render_base_text(&mut writer, &lines, &line_offsets, &colors);
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
let goto1 = cursor::Goto(1, 1);
let goto2 = cursor::Goto(1, 2);
let goto3 = cursor::Goto(1, 3);
let goto6 = cursor::Goto(1, 6);
assert_eq!(
2020-05-29 20:36:27 +02:00
writer,
format!(
"{bg}{fg}{g1}some text{g2}* e006b06 - (12 days ago) swapper: Make quotes{g3}path: /usr/local/bin/git{g6}path: /usr/local/bin/cargo{fg_reset}{bg_reset}",
2020-05-29 20:36:27 +02:00
g1 = goto1, g2 = goto2, g3 = goto3, g6 = goto6,
fg = color::Fg(colors.text_fg.as_ref()),
bg = color::Bg(colors.text_bg.as_ref()),
fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset),
)
.as_bytes()
);
2020-05-25 23:06:00 +02:00
}
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
#[test]
fn test_render_focused_matched_text() {
let mut writer = vec![];
let text = "https://en.wikipedia.org/wiki/Barcelona";
let focused = true;
let offset: (usize, usize) = (3, 1);
2020-06-01 15:25:54 +02:00
let colors = UiColors {
2020-05-29 20:36:27 +02:00
text_fg: Box::new(color::Black),
text_bg: Box::new(color::White),
2020-05-25 23:06:00 +02:00
focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan),
};
2020-05-22 15:03:39 +02:00
2020-06-01 15:25:54 +02:00
Ui::render_matched_text(&mut writer, text, focused, offset, &colors);
2020-05-25 23:06:00 +02:00
assert_eq!(
writer,
format!(
"{goto}{bg}{fg}{text}{fg_reset}{bg_reset}",
goto = cursor::Goto(4, 2),
fg = color::Fg(colors.focused_fg.as_ref()),
bg = color::Bg(colors.focused_bg.as_ref()),
fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset),
text = &text,
)
.as_bytes()
);
}
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
#[test]
fn test_render_matched_text() {
let mut writer = vec![];
let text = "https://en.wikipedia.org/wiki/Barcelona";
let focused = false;
let offset: (usize, usize) = (3, 1);
2020-06-01 15:25:54 +02:00
let colors = UiColors {
2020-05-29 20:36:27 +02:00
text_fg: Box::new(color::Black),
text_bg: Box::new(color::White),
2020-05-25 23:06:00 +02:00
focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan),
};
2020-06-01 15:25:54 +02:00
Ui::render_matched_text(&mut writer, text, focused, offset, &colors);
2020-05-25 23:06:00 +02:00
assert_eq!(
writer,
format!(
"{goto}{bg}{fg}{text}{fg_reset}{bg_reset}",
goto = cursor::Goto(4, 2),
fg = color::Fg(colors.match_fg.as_ref()),
bg = color::Bg(colors.match_bg.as_ref()),
fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset),
text = &text,
)
.as_bytes()
);
}
#[test]
fn test_render_unstyled_matched_hint() {
let mut writer = vec![];
let hint_text = "eo";
let offset: (usize, usize) = (3, 1);
2020-06-01 15:25:54 +02:00
let colors = UiColors {
2020-05-29 20:36:27 +02:00
text_fg: Box::new(color::Black),
text_bg: Box::new(color::White),
2020-05-25 23:06:00 +02:00
focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan),
};
let extra_offset = 0;
let hint_style = None;
2020-06-01 15:25:54 +02:00
Ui::render_matched_hint(
2020-05-25 23:06:00 +02:00
&mut writer,
hint_text,
(offset.0 + extra_offset, offset.1),
&colors,
&hint_style,
);
assert_eq!(
writer,
format!(
"{goto}{bg}{fg}{text}{fg_reset}{bg_reset}",
goto = cursor::Goto(4, 2),
fg = color::Fg(colors.hint_fg.as_ref()),
bg = color::Bg(colors.hint_bg.as_ref()),
fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset),
text = "eo",
)
.as_bytes()
);
}
#[test]
fn test_render_underlined_matched_hint() {
let mut writer = vec![];
let hint_text = "eo";
let offset: (usize, usize) = (3, 1);
2020-06-01 15:25:54 +02:00
let colors = UiColors {
2020-05-29 20:36:27 +02:00
text_fg: Box::new(color::Black),
text_bg: Box::new(color::White),
2020-05-25 23:06:00 +02:00
focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan),
};
let extra_offset = 0;
let hint_style = Some(HintStyle::Underline);
2020-06-01 15:25:54 +02:00
Ui::render_matched_hint(
2020-05-25 23:06:00 +02:00
&mut writer,
hint_text,
(offset.0 + extra_offset, offset.1),
&colors,
&hint_style,
);
assert_eq!(
writer,
format!(
"{goto}{bg}{fg}{sty}{text}{sty_reset}{fg_reset}{bg_reset}",
goto = cursor::Goto(4, 2),
fg = color::Fg(colors.hint_fg.as_ref()),
bg = color::Bg(colors.hint_bg.as_ref()),
fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset),
sty = style::Underline,
sty_reset = style::NoUnderline,
text = "eo",
)
.as_bytes()
);
}
#[test]
fn test_render_bracketed_matched_hint() {
let mut writer = vec![];
let hint_text = "eo";
let offset: (usize, usize) = (3, 1);
2020-06-01 15:25:54 +02:00
let colors = UiColors {
2020-05-29 20:36:27 +02:00
text_fg: Box::new(color::Black),
text_bg: Box::new(color::White),
2020-05-25 23:06:00 +02:00
focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan),
};
let extra_offset = 0;
let hint_style = Some(HintStyle::Surround('{', '}'));
2020-06-01 15:25:54 +02:00
Ui::render_matched_hint(
2020-05-25 23:06:00 +02:00
&mut writer,
hint_text,
(offset.0 + extra_offset, offset.1),
&colors,
&hint_style,
);
assert_eq!(
writer,
format!(
"{goto}{bg}{fg}{bra}{text}{bra_close}{fg_reset}{bg_reset}",
goto = cursor::Goto(4, 2),
fg = color::Fg(colors.hint_fg.as_ref()),
bg = color::Bg(colors.hint_bg.as_ref()),
fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset),
bra = '{',
bra_close = '}',
text = "eo",
)
.as_bytes()
);
}
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
#[test]
/// Simulates rendering without any match.
fn test_render_full_without_matches() {
let content = "lorem 127.0.0.1 lorem
2020-05-22 15:03:39 +02:00
Barcelona https://en.wikipedia.org/wiki/Barcelona - ";
let named_pat = vec![];
let custom_regexes = vec![];
2020-05-25 23:06:00 +02:00
let alphabet = alphabets::Alphabet("abcd".to_string());
let mut model = model::Model::new(content, &alphabet, &named_pat, &custom_regexes, false);
2020-06-02 13:10:48 +02:00
let term_width: u16 = 80;
let line_offsets = get_line_offsets(&model.lines, term_width);
2020-06-01 15:25:54 +02:00
let rendering_colors = UiColors {
2020-05-29 20:36:27 +02:00
text_fg: Box::new(color::Black),
text_bg: Box::new(color::White),
2020-05-25 23:06:00 +02:00
focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan),
};
let hint_alignment = HintAlignment::Leading;
2020-06-01 15:25:54 +02:00
// create a Ui without any match
let ui = Ui {
2020-06-01 09:33:40 +02:00
model: &mut model,
2020-06-02 13:10:48 +02:00
term_width,
line_offsets,
2020-05-25 23:06:00 +02:00
matches: vec![], // no matches
2020-05-31 22:45:36 +02:00
lookup_trie: SequenceTrie::new(),
2020-05-25 23:06:00 +02:00
focus_index: 0,
2020-06-01 10:06:50 +02:00
focus_wrap_around: false,
2020-05-25 23:06:00 +02:00
rendering_colors: &rendering_colors,
2020-06-01 15:34:54 +02:00
hint_alignment: &hint_alignment,
2020-05-25 23:06:00 +02:00
hint_style: None,
};
let mut writer = vec![];
2020-06-01 15:25:54 +02:00
ui.full_render(&mut writer);
2020-05-25 23:06:00 +02:00
let goto1 = cursor::Goto(1, 1);
let goto3 = cursor::Goto(1, 3);
let expected = format!(
"{bg}{fg}{goto1}lorem 127.0.0.1 lorem\
{goto3}Barcelona https://en.wikipedia.org/wiki/Barcelona -{fg_reset}{bg_reset}",
2020-05-25 23:06:00 +02:00
goto1 = goto1,
goto3 = goto3,
2020-05-29 20:36:27 +02:00
fg = color::Fg(rendering_colors.text_fg.as_ref()),
bg = color::Bg(rendering_colors.text_bg.as_ref()),
fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset),
2020-05-25 23:06:00 +02:00
);
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
// println!("{:?}", writer);
// println!("{:?}", expected.as_bytes());
2020-05-22 15:03:39 +02:00
2020-06-01 15:25:54 +02:00
// println!("matches: {}", ui.matches.len());
2020-05-25 23:06:00 +02:00
// println!("lines: {}", lines.len());
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
assert_eq!(writer, expected.as_bytes());
}
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
#[test]
/// Simulates rendering with matches.
fn test_render_full_with_matches() {
let content = "lorem 127.0.0.1 lorem
2020-05-22 15:03:39 +02:00
Barcelona https://en.wikipedia.org/wiki/Barcelona - ";
let named_pat = vec![];
let custom_regexes = vec![];
2020-05-25 23:06:00 +02:00
let alphabet = alphabets::Alphabet("abcd".to_string());
2020-05-30 22:23:33 +02:00
let reverse = true;
let mut model = model::Model::new(content, &alphabet, &named_pat, &custom_regexes, reverse);
2020-05-26 08:11:45 +02:00
let unique_hint = false;
2020-06-01 10:06:50 +02:00
let wrap_around = false;
2020-05-25 23:06:00 +02:00
2020-06-01 15:25:54 +02:00
let rendering_colors = UiColors {
2020-05-29 20:36:27 +02:00
text_fg: Box::new(color::Black),
text_bg: Box::new(color::White),
2020-05-25 23:06:00 +02:00
focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan),
};
let hint_alignment = HintAlignment::Leading;
let hint_style = None;
2020-06-01 15:25:54 +02:00
let ui = Ui::new(
2020-06-01 09:33:40 +02:00
&mut model,
2020-05-26 08:11:45 +02:00
unique_hint,
2020-06-01 10:06:50 +02:00
wrap_around,
2020-05-25 23:06:00 +02:00
&rendering_colors,
2020-06-01 15:34:54 +02:00
&hint_alignment,
2020-05-25 23:06:00 +02:00
hint_style,
);
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
let mut writer = vec![];
2020-06-01 15:25:54 +02:00
ui.full_render(&mut writer);
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
let expected_content = {
let goto1 = cursor::Goto(1, 1);
let goto3 = cursor::Goto(1, 3);
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
format!(
"{bg}{fg}{goto1}lorem 127.0.0.1 lorem\
{goto3}Barcelona https://en.wikipedia.org/wiki/Barcelona -{fg_reset}{bg_reset}",
2020-05-25 23:06:00 +02:00
goto1 = goto1,
goto3 = goto3,
2020-05-29 20:36:27 +02:00
fg = color::Fg(rendering_colors.text_fg.as_ref()),
bg = color::Bg(rendering_colors.text_bg.as_ref()),
fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset)
2020-05-25 23:06:00 +02:00
)
};
2020-06-02 20:03:16 +02:00
2020-05-25 23:06:00 +02:00
let expected_match1_text = {
let goto7_1 = cursor::Goto(7, 1);
format!(
"{goto7_1}{match_bg}{match_fg}127.0.0.1{fg_reset}{bg_reset}",
goto7_1 = goto7_1,
match_fg = color::Fg(rendering_colors.match_fg.as_ref()),
match_bg = color::Bg(rendering_colors.match_bg.as_ref()),
fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset)
)
};
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
let expected_match1_hint = {
let goto7_1 = cursor::Goto(7, 1);
format!(
"{goto7_1}{hint_bg}{hint_fg}b{fg_reset}{bg_reset}",
goto7_1 = goto7_1,
hint_fg = color::Fg(rendering_colors.hint_fg.as_ref()),
hint_bg = color::Bg(rendering_colors.hint_bg.as_ref()),
fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset)
)
};
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
let expected_match2_text = {
let goto11_3 = cursor::Goto(11, 3);
format!(
2020-05-23 09:30:09 +02:00
"{goto11_3}{focus_bg}{focus_fg}https://en.wikipedia.org/wiki/Barcelona{fg_reset}{bg_reset}",
2020-05-22 15:03:39 +02:00
goto11_3 = goto11_3,
2020-05-24 21:02:11 +02:00
focus_fg = color::Fg(rendering_colors.focused_fg.as_ref()),
focus_bg = color::Bg(rendering_colors.focused_bg.as_ref()),
2020-05-22 15:03:39 +02:00
fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset)
)
2020-05-25 23:06:00 +02:00
};
// Because reverse is true, this second match is focused,
// then the hint should not be rendered.
2020-05-25 23:06:00 +02:00
// let expected_match2_hint = {
// let goto11_3 = cursor::Goto(11, 3);
// format!(
// "{goto11_3}{hint_bg}{hint_fg}a{fg_reset}{bg_reset}",
// goto11_3 = goto11_3,
// hint_fg = color::Fg(rendering_colors.hint_fg.as_ref()),
// hint_bg = color::Bg(rendering_colors.hint_bg.as_ref()),
// fg_reset = color::Fg(color::Reset),
// bg_reset = color::Bg(color::Reset)
// )
// };
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
let expected = [
expected_content,
expected_match1_text,
expected_match1_hint,
expected_match2_text,
// expected_match2_hint,
2020-05-25 23:06:00 +02:00
]
.concat();
2020-05-22 15:03:39 +02:00
2020-05-25 23:06:00 +02:00
// println!("{:?}", writer);
// println!("{:?}", expected.as_bytes());
// let diff_point = writer
// .iter()
// .zip(expected.as_bytes().iter())
// .enumerate()
// .find(|(_idx, (&l, &r))| l != r);
// println!("{:?}", diff_point);
2020-06-01 15:25:54 +02:00
assert_eq!(2, ui.matches.len());
2020-05-25 23:06:00 +02:00
assert_eq!(writer, expected.as_bytes());
}
2020-06-02 20:03:16 +02:00
}
2020-06-01 10:14:16 +02:00
/// Holds color-related data, for clarity.
///
/// - `focus_*` colors are used to render the currently focused matched text.
/// - `normal_*` colors are used to render other matched text.
/// - `hint_*` colors are used to render the hints.
#[derive(Clap, Debug)]
2020-06-01 15:25:54 +02:00
pub struct UiColors {
2020-06-01 10:14:16 +02:00
/// Foreground color for base text.
#[clap(long, default_value = "bright-cyan", parse(try_from_str = colors::parse_color))]
pub text_fg: Box<dyn color::Color>,
/// Background color for base text.
#[clap(long, default_value = "bright-white", parse(try_from_str = colors::parse_color))]
pub text_bg: Box<dyn color::Color>,
/// Foreground color for matches.
#[clap(long, default_value = "yellow",
parse(try_from_str = colors::parse_color))]
pub match_fg: Box<dyn color::Color>,
/// Background color for matches.
#[clap(long, default_value = "bright-white",
parse(try_from_str = colors::parse_color))]
pub match_bg: Box<dyn color::Color>,
/// Foreground color for the focused match.
#[clap(long, default_value = "magenta",
parse(try_from_str = colors::parse_color))]
pub focused_fg: Box<dyn color::Color>,
/// Background color for the focused match.
#[clap(long, default_value = "bright-white",
parse(try_from_str = colors::parse_color))]
pub focused_bg: Box<dyn color::Color>,
/// Foreground color for hints.
#[clap(long, default_value = "white",
parse(try_from_str = colors::parse_color))]
pub hint_fg: Box<dyn color::Color>,
/// Background color for hints.
#[clap(long, default_value = "magenta",
parse(try_from_str = colors::parse_color))]
pub hint_bg: Box<dyn color::Color>,
}