refactor: add comments & change some names

This commit is contained in:
graelo 2020-05-21 08:28:27 +02:00
parent 34d0bb5a35
commit 777a460ec9
2 changed files with 177 additions and 143 deletions

View file

@ -12,6 +12,8 @@ use std::fs::OpenOptions;
use std::io::prelude::*; use std::io::prelude::*;
use std::io::{self, Read}; use std::io::{self, Read};
// TODO: position as an enum ::Leading ::Trailing
fn app_args<'a>() -> clap::ArgMatches<'a> { fn app_args<'a>() -> clap::ArgMatches<'a> {
App::new("thumbs") App::new("thumbs")
.version(crate_version!()) .version(crate_version!())
@ -138,17 +140,18 @@ fn main() {
let select_foreground_color = colors::get_color(args.value_of("select_foreground_color").unwrap()); let select_foreground_color = colors::get_color(args.value_of("select_foreground_color").unwrap());
let select_background_color = colors::get_color(args.value_of("select_background_color").unwrap()); let select_background_color = colors::get_color(args.value_of("select_background_color").unwrap());
// Copy the pane contents (piped in via stdin) into a buffer, and split lines.
let mut buffer = String::new();
let stdin = io::stdin(); let stdin = io::stdin();
let mut handle = stdin.lock(); let mut handle = stdin.lock();
let mut output = String::new();
handle.read_to_string(&mut output).unwrap(); handle.read_to_string(&mut buffer).unwrap();
let lines = output.split('\n').collect::<Vec<&str>>(); let lines: Vec<&str> = buffer.split('\n').collect();
let mut state = state::State::new(&lines, alphabet, &regexp); let mut state = state::State::new(&lines, alphabet, &regexp);
let selected = { let selections = {
let mut viewbox = view::View::new( let mut viewbox = view::View::new(
&mut state, &mut state,
multi, multi,
@ -167,22 +170,28 @@ fn main() {
viewbox.present() viewbox.present()
}; };
if !selected.is_empty() { // Early exit, signaling tmux we had no selections.
let output = selected if selections.is_empty() {
.iter() ::std::process::exit(1);
.map(|(text, upcase)| { }
let upcase_value = if *upcase { "true" } else { "false" };
let mut output = format.to_string(); let output = selections
.iter()
.map(|(text, upcase)| {
let upcase_value = if *upcase { "true" } else { "false" };
output = str::replace(&output, "%U", upcase_value); let mut output = format.to_string();
output = str::replace(&output, "%H", text.as_str());
output
})
.collect::<Vec<_>>()
.join("\n");
if let Some(target) = target { output = str::replace(&output, "%U", upcase_value);
output = str::replace(&output, "%H", text.as_str());
output
})
.collect::<Vec<_>>()
.join("\n");
match target {
None => println!("{}", output),
Some(target) => {
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
.create(true) .create(true)
.truncate(true) .truncate(true)
@ -191,10 +200,6 @@ fn main() {
.expect("Unable to open the target file"); .expect("Unable to open the target file");
file.write(output.as_bytes()).unwrap(); file.write(output.as_bytes()).unwrap();
} else {
print!("{}", output);
} }
} else {
::std::process::exit(1);
} }
} }

View file

@ -62,97 +62,124 @@ impl<'a> View<'a> {
} }
} }
/// Move focus onto the previous hint.
pub fn prev(&mut self) { pub fn prev(&mut self) {
if self.skip > 0 { if self.skip > 0 {
self.skip -= 1; self.skip -= 1;
} }
} }
/// Move focus onto the next hint.
pub fn next(&mut self) { pub fn next(&mut self) {
if self.skip < self.matches.len() - 1 { if self.skip < self.matches.len() - 1 {
self.skip += 1; self.skip += 1;
} }
} }
fn make_hint_text(&self, hint: &str) -> String { // /// TODO remove
if self.contrast { // fn make_hint_text(&self, hint: &str) -> String {
format!("[{}]", hint) // if self.contrast {
} else { // format!("[{}]", hint)
hint.to_string() // } else {
} // hint.to_string()
} // }
// }
/// Render the view on stdout.
fn render(&self, stdout: &mut dyn Write) -> () { fn render(&self, stdout: &mut dyn Write) -> () {
write!(stdout, "{}", cursor::Hide).unwrap(); write!(stdout, "{}", cursor::Hide).unwrap();
// Trim all lines and render non-empty ones.
for (index, line) in self.state.lines.iter().enumerate() { for (index, line) in self.state.lines.iter().enumerate() {
let clean = line.trim_end_matches(|c: char| c.is_whitespace()); // remove trailing whitespaces
let cleaned_line = line.trim_end_matches(|c: char| c.is_whitespace());
if !clean.is_empty() { if cleaned_line.is_empty() {
let text = self.make_hint_text(line); continue; // Don't render empty lines.
print!("{goto}{text}", goto = cursor::Goto(1, index as u16 + 1), text = &text);
} }
// let text = self.make_hint_text(line);
// print!(
write!(
stdout,
"{goto}{text}",
goto = cursor::Goto(1, index as u16 + 1),
text = &cleaned_line,
)
.unwrap();
} }
let selected = self.matches.get(self.skip); // let focused = self.matches.get(self.skip);
for mat in self.matches.iter() { for (index, mat) in self.matches.iter().enumerate() {
let selected_color = if selected == Some(mat) { // 1. Render the match's text.
&self.select_foreground_color //
// To help identify it, the match thas has focus is rendered with a dedicated color.
// let (text_fg_color, text_bg_color) = if focused == Some(mat) {
let (text_fg_color, text_bg_color) = if index == self.skip {
(&self.select_foreground_color, &self.select_background_color)
} else { } else {
&self.foreground_color (&self.foreground_color, &self.background_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 // 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.
let line = &self.state.lines[mat.y as usize]; let line = &self.state.lines[mat.y as usize];
let prefix = &line[0..mat.x as usize]; let prefix = &line[0..mat.x as usize];
let extra = prefix.len() - prefix.chars().count(); let adjust = prefix.len() - prefix.chars().count();
let offset = (mat.x as u16) - (extra as u16); let offset = (mat.x as u16) - (adjust as u16);
let text = self.make_hint_text(mat.text); let text = &mat.text; //self.make_hint_text(mat.text);
print!( // Render just the match's text on top of existing content.
"{goto}{background}{foregroud}{text}{resetf}{resetb}", write!(
stdout,
"{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}",
goto = cursor::Goto(offset + 1, mat.y as u16 + 1), goto = cursor::Goto(offset + 1, mat.y as u16 + 1),
foregroud = color::Fg(**selected_color), fg_color = color::Fg(**text_fg_color),
background = color::Bg(**selected_background_color), bg_color = color::Bg(**text_bg_color),
resetf = color::Fg(color::Reset), fg_reset = color::Fg(color::Reset),
resetb = color::Bg(color::Reset), bg_reset = color::Bg(color::Reset),
text = &text text = &text,
); )
.unwrap();
// 2. Render the hint (e.g. ";k") on top of the text at the beginning or the end.
//
if let Some(ref hint) = mat.hint { if let Some(ref hint) = mat.hint {
let extra_position = if self.position == "left" { let extra_offset = if self.position == "left" {
0 0
} else { } else {
text.len() - mat.hint.clone().unwrap().len() text.len() - hint.len()
}; };
let text = self.make_hint_text(hint.as_str()); write!(
stdout,
print!( "{goto}{bg_color}{fg_color}{hint}{fg_reset}{bg_reset}",
"{goto}{background}{foregroud}{text}{resetf}{resetb}", goto = cursor::Goto(offset + extra_offset as u16 + 1, mat.y as u16 + 1),
goto = cursor::Goto(offset + extra_position as u16 + 1, mat.y as u16 + 1), fg_color = color::Fg(*self.hint_foreground_color),
foregroud = color::Fg(*self.hint_foreground_color), bg_color = color::Bg(*self.hint_background_color),
background = color::Bg(*self.hint_background_color), fg_reset = color::Fg(color::Reset),
resetf = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset),
resetb = color::Bg(color::Reset), hint = hint,
text = &text )
); .unwrap();
} }
} }
stdout.flush().unwrap(); stdout.flush().unwrap();
} }
/// Listen to keys entered on stdin
///
/// # Panics
/// This function panics if termion cannot read the entered keys on stdin.
/// This function also panics if the user types Insert on a line without hints.
fn listen(&mut self, stdin: &mut dyn Read, stdout: &mut dyn Write) -> CaptureEvent { fn listen(&mut self, stdin: &mut dyn Read, stdout: &mut dyn Write) -> CaptureEvent {
if self.matches.is_empty() { if self.matches.is_empty() {
return CaptureEvent::Exit return CaptureEvent::Exit;
} }
let mut chosen = vec![]; let mut chosen = vec![];
@ -168,81 +195,83 @@ impl<'a> View<'a> {
self.render(stdout); self.render(stdout);
loop { loop {
match stdin.keys().next() { // This is an option of a result of a key... Let's pop error cases first.
Some(key) => { let next_key = stdin.keys().next();
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 { if next_key.is_none() {
return CaptureEvent::Hint(chosen); // Nothing in the buffer. Wait for a bit...
} std::thread::sleep(std::time::Duration::from_millis(100));
} continue;
_ => 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 key_res = next_key.unwrap();
let lower_key = key.to_lowercase(); if let Err(err) = key_res {
// Termion not being able to read from stdin is an unrecoverable error.
panic!(err);
}
typed_hint.push_str(lower_key.as_str()); match key_res.unwrap() {
// Clears an ongoing multi-hint selection, or exit.
let selection = self.matches.iter().find(|mat| mat.hint == Some(typed_hint.clone())); Key::Esc => {
if self.multi && !typed_hint.is_empty() {
match selection { typed_hint.clear();
Some(mat) => { } else {
chosen.push((mat.text.to_string(), key != lower_key)); break;
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),
} }
} }
_ => {
// Nothing in the buffer. Wait for a bit... // TODO: What does this do?
std::thread::sleep(std::time::Duration::from_millis(100)); Key::Insert => match self.matches.iter().enumerate().find(|&(idx, _)| idx == self.skip) {
Some((_idx, mtch)) => {
chosen.push((mtch.text.to_string(), false));
if !self.multi {
return CaptureEvent::Hint(chosen);
}
}
None => panic!("Match not found?"),
},
// Move focus to next/prev hint.
Key::Up => self.prev(),
Key::Down => self.next(),
Key::Left => self.prev(),
Key::Right => self.next(),
// Pressing space finalizes an ongoing multi-hint selection.
// Others characters attempt the corresponding hint.
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);
let selection = self.matches.iter().find(|&mtch| mtch.hint == Some(typed_hint.clone()));
match selection {
Some(mtch) => {
chosen.push((mtch.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 keys are ignored.
_ => (),
} }
self.render(stdout); self.render(stdout);
@ -294,11 +323,11 @@ mod tests {
hint_foreground_color: colors::get_color("default"), hint_foreground_color: colors::get_color("default"),
}; };
let result = view.make_hint_text("a"); // let result = view.make_hint_text("a");
assert_eq!(result, "a".to_string()); // assert_eq!(result, "a".to_string());
view.contrast = true; // view.contrast = true;
let result = view.make_hint_text("a"); // let result = view.make_hint_text("a");
assert_eq!(result, "[a]".to_string()); // assert_eq!(result, "[a]".to_string());
} }
} }