tmux-thumbs/src/view.rs

315 lines
8.7 KiB
Rust

use super::*;
use std::char;
use std::io::{stdout, Read, Write};
use termion::async_stdin;
use termion::event::Key;
use termion::input::TermRead;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use termion::{color, cursor};
pub struct View<'a> {
state: &'a mut state::State<'a>,
skip: usize,
multi: bool,
contrast: bool,
position: &'a str,
matches: Vec<state::Match<'a>>,
select_foreground_color: Box<&'a dyn color::Color>,
select_background_color: Box<&'a dyn color::Color>,
foreground_color: Box<&'a dyn color::Color>,
background_color: Box<&'a dyn color::Color>,
hint_background_color: Box<&'a dyn color::Color>,
hint_foreground_color: Box<&'a dyn color::Color>,
}
enum CaptureEvent {
Exit,
Hint(Vec<(String, bool)>),
}
impl<'a> View<'a> {
pub fn new(
state: &'a mut state::State<'a>,
multi: bool,
reverse: bool,
unique: bool,
contrast: bool,
position: &'a str,
select_foreground_color: Box<&'a dyn color::Color>,
select_background_color: Box<&'a dyn color::Color>,
foreground_color: Box<&'a dyn color::Color>,
background_color: Box<&'a dyn color::Color>,
hint_foreground_color: Box<&'a dyn color::Color>,
hint_background_color: Box<&'a dyn color::Color>,
) -> View<'a> {
let matches = state.matches(reverse, unique);
let skip = if reverse { matches.len() - 1 } else { 0 };
View {
state,
skip,
multi,
contrast,
position,
matches,
select_foreground_color,
select_background_color,
foreground_color,
background_color,
hint_foreground_color,
hint_background_color,
}
}
pub fn prev(&mut self) {
if self.skip > 0 {
self.skip -= 1;
}
}
pub fn next(&mut self) {
if self.skip < self.matches.len() - 1 {
self.skip += 1;
}
}
fn make_hint_text(&self, hint: &str) -> String {
if self.contrast {
format!("[{}]", hint)
} else {
hint.to_string()
}
}
fn render(&self, stdout: &mut dyn Write) -> () {
write!(stdout, "{}", cursor::Hide).unwrap();
for (index, line) in self.state.lines.iter().enumerate() {
let clean = line.trim_end_matches(|c: char| c.is_whitespace());
if !clean.is_empty() {
print!("{goto}{text}", goto = cursor::Goto(1, index as u16 + 1), text = line);
}
}
let selected = self.matches.get(self.skip);
for mat in self.matches.iter() {
let selected_color = if selected == Some(mat) {
&self.select_foreground_color
} else {
&self.foreground_color
};
let selected_background_color = if selected == Some(mat) {
&self.select_background_color
} else {
&self.background_color
};
// Find long utf sequences and extract it from mat.x
let line = &self.state.lines[mat.y as usize];
let prefix = &line[0..mat.x as usize];
let extra = prefix.len() - prefix.chars().count();
let offset = (mat.x as u16) - (extra as u16);
let text = self.make_hint_text(mat.text);
print!(
"{goto}{background}{foregroud}{text}{resetf}{resetb}",
goto = cursor::Goto(offset + 1, mat.y as u16 + 1),
foregroud = color::Fg(**selected_color),
background = color::Bg(**selected_background_color),
resetf = color::Fg(color::Reset),
resetb = color::Bg(color::Reset),
text = &text
);
if let Some(ref hint) = mat.hint {
let extra_position = if self.position == "left" {
0
} else if self.position == "off_left" {
0 - mat.hint.clone().unwrap().len()
} else if self.position == "off_right" {
text.len()
} else {
text.len() - mat.hint.clone().unwrap().len()
};
let text = self.make_hint_text(hint.as_str());
let final_position = if (offset as i32 + extra_position as i32) < 0 {
0
} else {
offset + extra_position as u16
};
print!(
"{goto}{background}{foregroud}{text}{resetf}{resetb}",
goto = cursor::Goto(final_position as u16 + 1, mat.y as u16 + 1),
foregroud = color::Fg(*self.hint_foreground_color),
background = color::Bg(*self.hint_background_color),
resetf = color::Fg(color::Reset),
resetb = color::Bg(color::Reset),
text = &text
);
}
}
stdout.flush().unwrap();
}
fn listen(&mut self, stdin: &mut dyn Read, stdout: &mut dyn Write) -> CaptureEvent {
if self.matches.is_empty() {
return CaptureEvent::Exit;
}
let mut chosen = vec![];
let mut typed_hint: String = "".to_owned();
let longest_hint = self
.matches
.iter()
.filter_map(|m| m.hint.clone())
.max_by(|x, y| x.len().cmp(&y.len()))
.unwrap()
.clone();
self.render(stdout);
loop {
match stdin.keys().next() {
Some(key) => {
match key {
Ok(key) => {
match key {
Key::Esc => {
if self.multi && !typed_hint.is_empty() {
typed_hint.clear();
} else {
break;
}
}
Key::Insert => match self.matches.iter().enumerate().find(|&h| h.0 == self.skip) {
Some(hm) => {
chosen.push((hm.1.text.to_string(), false));
if !self.multi {
return CaptureEvent::Hint(chosen);
}
}
_ => panic!("Match not found?"),
},
Key::Up => {
self.prev();
}
Key::Down => {
self.next();
}
Key::Left => {
self.prev();
}
Key::Right => {
self.next();
}
Key::Char(ch) => {
if ch == ' ' && self.multi {
return CaptureEvent::Hint(chosen);
}
let key = ch.to_string();
let lower_key = key.to_lowercase();
typed_hint.push_str(lower_key.as_str());
let selection = self.matches.iter().find(|mat| mat.hint == Some(typed_hint.clone()));
match selection {
Some(mat) => {
chosen.push((mat.text.to_string(), key != lower_key));
if self.multi {
typed_hint.clear();
} else {
return CaptureEvent::Hint(chosen);
}
}
None => {
if !self.multi && typed_hint.len() >= longest_hint.len() {
break;
}
}
}
}
_ => {
// Unknown key
}
}
}
Err(err) => panic!(err),
}
stdin.keys().for_each(|_| { /* Skip the rest of stdin buffer */ })
}
_ => {
// Nothing in the buffer. Wait for a bit...
std::thread::sleep(std::time::Duration::from_millis(50));
continue; // don't render again if nothing new to show
}
}
self.render(stdout);
}
CaptureEvent::Exit
}
pub fn present(&mut self) -> Vec<(String, bool)> {
let mut stdin = async_stdin();
let mut stdout = AlternateScreen::from(stdout().into_raw_mode().unwrap());
let hints = match self.listen(&mut stdin, &mut stdout) {
CaptureEvent::Exit => vec![],
CaptureEvent::Hint(chosen) => chosen,
};
write!(stdout, "{}", cursor::Show).unwrap();
hints
}
}
#[cfg(test)]
mod tests {
use super::*;
fn split(output: &str) -> Vec<&str> {
output.split("\n").collect::<Vec<&str>>()
}
#[test]
fn hint_text() {
let lines = split("lorem 127.0.0.1 lorem");
let custom = [].to_vec();
let mut state = state::State::new(&lines, "abcd", &custom);
let mut view = View {
state: &mut state,
skip: 0,
multi: false,
contrast: false,
position: &"",
matches: vec![],
select_foreground_color: colors::get_color("default"),
select_background_color: colors::get_color("default"),
foreground_color: colors::get_color("default"),
background_color: colors::get_color("default"),
hint_background_color: colors::get_color("default"),
hint_foreground_color: colors::get_color("default"),
};
let result = view.make_hint_text("a");
assert_eq!(result, "a".to_string());
view.contrast = true;
let result = view.make_hint_text("a");
assert_eq!(result, "[a]".to_string());
}
}