From 777a460ec943ec76c18bf82d20c5a41577b011ec Mon Sep 17 00:00:00 2001 From: graelo Date: Thu, 21 May 2020 08:28:27 +0200 Subject: [PATCH 01/40] refactor: add comments & change some names --- src/main.rs | 47 +++++---- src/view.rs | 273 +++++++++++++++++++++++++++++----------------------- 2 files changed, 177 insertions(+), 143 deletions(-) diff --git a/src/main.rs b/src/main.rs index eda20eb..a7f9508 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,8 @@ use std::fs::OpenOptions; use std::io::prelude::*; use std::io::{self, Read}; +// TODO: position as an enum ::Leading ::Trailing + fn app_args<'a>() -> clap::ArgMatches<'a> { App::new("thumbs") .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_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 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::>(); + let lines: Vec<&str> = buffer.split('\n').collect(); let mut state = state::State::new(&lines, alphabet, ®exp); - let selected = { + let selections = { let mut viewbox = view::View::new( &mut state, multi, @@ -167,22 +170,28 @@ fn main() { viewbox.present() }; - if !selected.is_empty() { - let output = selected - .iter() - .map(|(text, upcase)| { - let upcase_value = if *upcase { "true" } else { "false" }; + // Early exit, signaling tmux we had no selections. + if selections.is_empty() { + ::std::process::exit(1); + } - 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); - output = str::replace(&output, "%H", text.as_str()); - output - }) - .collect::>() - .join("\n"); + let mut output = format.to_string(); - if let Some(target) = target { + output = str::replace(&output, "%U", upcase_value); + output = str::replace(&output, "%H", text.as_str()); + output + }) + .collect::>() + .join("\n"); + + match target { + None => println!("{}", output), + Some(target) => { let mut file = OpenOptions::new() .create(true) .truncate(true) @@ -191,10 +200,6 @@ fn main() { .expect("Unable to open the target file"); file.write(output.as_bytes()).unwrap(); - } else { - print!("{}", output); } - } else { - ::std::process::exit(1); } } diff --git a/src/view.rs b/src/view.rs index 40bc3b8..d19994d 100644 --- a/src/view.rs +++ b/src/view.rs @@ -62,97 +62,124 @@ impl<'a> View<'a> { } } + /// Move focus onto the previous hint. pub fn prev(&mut self) { if self.skip > 0 { self.skip -= 1; } } + /// Move focus onto the next hint. 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() - } - } + // /// TODO remove + // fn make_hint_text(&self, hint: &str) -> String { + // if self.contrast { + // format!("[{}]", hint) + // } else { + // hint.to_string() + // } + // } + /// Render the view on stdout. fn render(&self, stdout: &mut dyn Write) -> () { write!(stdout, "{}", cursor::Hide).unwrap(); + // Trim all lines and render non-empty ones. 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() { - let text = self.make_hint_text(line); - - print!("{goto}{text}", goto = cursor::Goto(1, index as u16 + 1), text = &text); + if cleaned_line.is_empty() { + continue; // Don't render empty lines. } + + // 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() { - let selected_color = if selected == Some(mat) { - &self.select_foreground_color + for (index, mat) in self.matches.iter().enumerate() { + // 1. Render the match's text. + // + + // 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 { - &self.foreground_color - }; - let selected_background_color = if selected == Some(mat) { - &self.select_background_color - } else { - &self.background_color + (&self.foreground_color, &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 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); + let adjust = prefix.len() - prefix.chars().count(); + let offset = (mat.x as u16) - (adjust as u16); + let text = &mat.text; //self.make_hint_text(mat.text); - print!( - "{goto}{background}{foregroud}{text}{resetf}{resetb}", + // Render just the match's text on top of existing content. + write!( + stdout, + "{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}", 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 - ); + fg_color = color::Fg(**text_fg_color), + bg_color = color::Bg(**text_bg_color), + fg_reset = color::Fg(color::Reset), + bg_reset = color::Bg(color::Reset), + 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 { - let extra_position = if self.position == "left" { + let extra_offset = if self.position == "left" { 0 } else { - text.len() - mat.hint.clone().unwrap().len() + text.len() - hint.len() }; - let text = self.make_hint_text(hint.as_str()); - - print!( - "{goto}{background}{foregroud}{text}{resetf}{resetb}", - goto = cursor::Goto(offset + extra_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 - ); + write!( + stdout, + "{goto}{bg_color}{fg_color}{hint}{fg_reset}{bg_reset}", + goto = cursor::Goto(offset + extra_offset as u16 + 1, mat.y as u16 + 1), + fg_color = color::Fg(*self.hint_foreground_color), + bg_color = color::Bg(*self.hint_background_color), + fg_reset = color::Fg(color::Reset), + bg_reset = color::Bg(color::Reset), + hint = hint, + ) + .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 { if self.matches.is_empty() { - return CaptureEvent::Exit + return CaptureEvent::Exit; } let mut chosen = vec![]; @@ -168,81 +195,83 @@ impl<'a> View<'a> { 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)); + // This is an option of a result of a key... Let's pop error cases first. + let next_key = stdin.keys().next(); - 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); - } + if next_key.is_none() { + // Nothing in the buffer. Wait for a bit... + std::thread::sleep(std::time::Duration::from_millis(100)); + continue; + } - let key = ch.to_string(); - let lower_key = key.to_lowercase(); + 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); + } - 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), + match key_res.unwrap() { + // Clears an ongoing multi-hint selection, or exit. + Key::Esc => { + if self.multi && !typed_hint.is_empty() { + typed_hint.clear(); + } else { + break; } } - _ => { - // Nothing in the buffer. Wait for a bit... - std::thread::sleep(std::time::Duration::from_millis(100)); + + // TODO: What does this do? + 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); @@ -294,11 +323,11 @@ mod tests { hint_foreground_color: colors::get_color("default"), }; - let result = view.make_hint_text("a"); - assert_eq!(result, "a".to_string()); + // 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()); + // view.contrast = true; + // let result = view.make_hint_text("a"); + // assert_eq!(result, "[a]".to_string()); } } From 4eca53fd85123d02b350f4ad08ed38da7d235709 Mon Sep 17 00:00:00 2001 From: graelo Date: Fri, 22 May 2020 15:03:39 +0200 Subject: [PATCH 02/40] refactor(ui): better view & colors --- src/colors.rs | 14 + src/main.rs | 32 ++- src/view.rs | 772 ++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 665 insertions(+), 153 deletions(-) diff --git a/src/colors.rs b/src/colors.rs index be5f92d..2f213ee 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -15,6 +15,20 @@ pub fn get_color(color_name: &str) -> Box<&dyn color::Color> { } } +/// 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. +pub struct RenderingColors<'a> { + pub focus_fg_color: Box<&'a dyn color::Color>, + pub focus_bg_color: Box<&'a dyn color::Color>, + pub normal_fg_color: Box<&'a dyn color::Color>, + pub normal_bg_color: Box<&'a dyn color::Color>, + pub hint_fg_color: Box<&'a dyn color::Color>, + pub hint_bg_color: Box<&'a dyn color::Color>, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/main.rs b/src/main.rs index a7f9508..677905f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -151,20 +151,36 @@ fn main() { let mut state = state::State::new(&lines, alphabet, ®exp); + let rendering_edge = if position == "left" { + view::RenderingEdge::Leading + } else { + view::RenderingEdge::Trailing + }; + + let rendering_colors = colors::RenderingColors { + focus_fg_color: select_foreground_color, + focus_bg_color: select_background_color, + normal_fg_color: foreground_color, + normal_bg_color: background_color, + hint_fg_color: hint_foreground_color, + hint_bg_color: hint_background_color, + }; + + let contrast_style = if contrast { + Some(view::ContrastStyle::Surrounded('[', ']')) + } else { + None + }; + let selections = { let mut viewbox = view::View::new( &mut state, multi, reverse, unique, - contrast, - position, - select_foreground_color, - select_background_color, - foreground_color, - background_color, - hint_foreground_color, - hint_background_color, + rendering_edge, + &rendering_colors, + contrast_style, ); viewbox.present() diff --git a/src/view.rs b/src/view.rs index d19994d..16c99fc 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{colors, state}; use std::char; use std::io::{stdout, Read, Write}; use termion::async_stdin; @@ -6,25 +6,42 @@ use termion::event::Key; use termion::input::TermRead; use termion::raw::IntoRawMode; use termion::screen::AlternateScreen; -use termion::{color, cursor}; +use termion::{color, cursor, style}; pub struct View<'a> { state: &'a mut state::State<'a>, - skip: usize, - multi: bool, - contrast: bool, - position: &'a str, matches: Vec>, - 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>, + focus_index: usize, + multi: bool, + rendering_edge: RenderingEdge, + rendering_colors: &'a colors::RenderingColors<'a>, + contrast_style: Option, } +/// Describes if, during rendering, a hint should aligned to the leading edge of +/// the matched text, or to its trailing edge. +pub enum RenderingEdge { + Leading, + 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 ContrastStyle { + /// The hint's text will be underlined (leveraging `termion::style::Underline`). + Underlined, + /// The hint's text will be surrounded by these chars. + Surrounded(char, char), +} + +/// Returned value after the `View` has finished listening to events. enum CaptureEvent { + /// Exit with no selected matches, Exit, + /// A vector of matched text and whether it was selected with uppercase. Hint(Vec<(String, bool)>), } @@ -32,147 +49,232 @@ impl<'a> View<'a> { pub fn new( state: &'a mut state::State<'a>, multi: bool, - reverse: bool, + reversed: 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>, + rendering_edge: RenderingEdge, + rendering_colors: &'a colors::RenderingColors, + contrast_style: Option, ) -> View<'a> { - let matches = state.matches(reverse, unique); - let skip = if reverse { matches.len() - 1 } else { 0 }; + let matches = state.matches(reversed, unique); + let focus_index = if reversed { 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, + focus_index, + multi, + rendering_edge, + rendering_colors, + contrast_style, } } /// Move focus onto the previous hint. pub fn prev(&mut self) { - if self.skip > 0 { - self.skip -= 1; + if self.focus_index > 0 { + self.focus_index -= 1; } } /// Move focus onto the next hint. pub fn next(&mut self) { - if self.skip < self.matches.len() - 1 { - self.skip += 1; + if self.focus_index < self.matches.len() - 1 { + self.focus_index += 1; } } - // /// TODO remove - // fn make_hint_text(&self, hint: &str) -> String { - // if self.contrast { - // format!("[{}]", hint) - // } else { - // hint.to_string() - // } - // } + /// Render entire state lines on provided writer. + /// + /// 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. + fn render_lines(stdout: &mut dyn Write, lines: &Vec<&str>) -> () { + for (index, line) in lines.iter().enumerate() { + let trimmed_line = line.trim_end(); - /// Render the view on stdout. + if !trimmed_line.is_empty() { + write!( + stdout, + "{goto}{text}", + goto = cursor::Goto(1, index as u16 + 1), + text = &trimmed_line, + ) + .unwrap(); + } + } + } + + /// Render the Match's `text` field on provided writer. + /// + /// If a Mach is "focused", then it is rendered with a specific color. + /// + /// # Note + /// This writes directly on the writer, avoiding extra allocation. + fn render_matched_text( + stdout: &mut dyn Write, + text: &str, + focused: bool, + offset: (usize, usize), + colors: &colors::RenderingColors, + ) { + // To help identify it, the match thas has focus is rendered with a dedicated color. + let (text_fg_color, text_bg_color) = if focused { + (&colors.focus_fg_color, &colors.focus_bg_color) + } else { + (&colors.normal_fg_color, &colors.normal_bg_color) + }; + + // Render just the Match's text on top of existing content. + write!( + stdout, + "{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}", + goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + fg_color = color::Fg(**text_fg_color), + bg_color = color::Bg(**text_bg_color), + fg_reset = color::Fg(color::Reset), + bg_reset = color::Bg(color::Reset), + text = &text, + ) + .unwrap(); + } + + /// 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 + /// `ContrastStyle::Delimited`. + /// + /// # Note + /// This writes directly on the writer, avoiding extra allocation. + fn render_matched_hint( + stdout: &mut dyn Write, + hint_text: &str, + offset: (usize, usize), + colors: &colors::RenderingColors, + contrast_style: &Option, + ) { + let fg_color = color::Fg(*colors.hint_fg_color); + let bg_color = color::Bg(*colors.hint_bg_color); + let fg_reset = color::Fg(color::Reset); + let bg_reset = color::Bg(color::Reset); + + match contrast_style { + None => { + write!( + stdout, + "{goto}{bg_color}{fg_color}{hint}{fg_reset}{bg_reset}", + goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + fg_color = fg_color, + bg_color = bg_color, + fg_reset = fg_reset, + bg_reset = bg_reset, + hint = hint_text, + ) + .unwrap(); + } + Some(contrast_style) => match contrast_style { + ContrastStyle::Underlined => { + write!( + stdout, + "{goto}{bg_color}{fg_color}{sty}{hint}{sty_reset}{fg_reset}{bg_reset}", + goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + 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(); + } + ContrastStyle::Surrounded(opening, closing) => { + write!( + stdout, + "{goto}{bg_color}{fg_color}{bra}{hint}{bra_close}{fg_reset}{bg_reset}", + goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + 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(); + } + }, + } + } + + /// Render the view on the provided writer. + /// + /// 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.rendering_edge`, 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. fn render(&self, stdout: &mut dyn Write) -> () { write!(stdout, "{}", cursor::Hide).unwrap(); - // Trim all lines and render non-empty ones. - for (index, line) in self.state.lines.iter().enumerate() { - // remove trailing whitespaces - let cleaned_line = line.trim_end_matches(|c: char| c.is_whitespace()); - - if cleaned_line.is_empty() { - continue; // Don't render empty lines. - } - - // 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 focused = self.matches.get(self.skip); + // 1. Trim all lines and render non-empty ones. + View::render_lines(stdout, self.state.lines); for (index, mat) in self.matches.iter().enumerate() { - // 1. Render the match's text. - // - - // 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 { - (&self.foreground_color, &self.background_color) - }; + // 2. Render the match's text. // 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 prefix = &line[0..mat.x as usize]; - let adjust = prefix.len() - prefix.chars().count(); - let offset = (mat.x as u16) - (adjust as u16); - let text = &mat.text; //self.make_hint_text(mat.text); + let offset_x = { + let line = &self.state.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; - // Render just the match's text on top of existing content. - write!( - stdout, - "{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}", - goto = cursor::Goto(offset + 1, mat.y as u16 + 1), - fg_color = color::Fg(**text_fg_color), - bg_color = color::Bg(**text_bg_color), - fg_reset = color::Fg(color::Reset), - bg_reset = color::Bg(color::Reset), - text = &text, - ) - .unwrap(); + let text = &mat.text; - // 2. Render the hint (e.g. ";k") on top of the text at the beginning or the end. - // + let focused = index == self.focus_index; + + View::render_matched_text(stdout, text, focused, (offset_x, offset_y), &self.rendering_colors); + + // 3. Render the hint (e.g. "eo") as an overlay on top of the rendered matched text, + // aligned at its leading or the trailing edge. if let Some(ref hint) = mat.hint { - let extra_offset = if self.position == "left" { - 0 - } else { - text.len() - hint.len() + let extra_offset = match self.rendering_edge { + RenderingEdge::Leading => 0, + RenderingEdge::Trailing => text.len() - hint.len(), }; - write!( + View::render_matched_hint( stdout, - "{goto}{bg_color}{fg_color}{hint}{fg_reset}{bg_reset}", - goto = cursor::Goto(offset + extra_offset as u16 + 1, mat.y as u16 + 1), - fg_color = color::Fg(*self.hint_foreground_color), - bg_color = color::Bg(*self.hint_background_color), - fg_reset = color::Fg(color::Reset), - bg_reset = color::Bg(color::Reset), - hint = hint, - ) - .unwrap(); + hint, + (offset_x + extra_offset, offset_y), + &self.rendering_colors, + &self.contrast_style, + ); } } stdout.flush().unwrap(); } - /// Listen to keys entered on stdin + /// Listen to keys entered on stdin, moving focus accordingly, and selecting + /// one or multiple matches. /// /// # Panics /// This function panics if termion cannot read the entered keys on stdin. @@ -220,10 +322,12 @@ impl<'a> View<'a> { } } - // TODO: What does this do? - Key::Insert => match self.matches.iter().enumerate().find(|&(idx, _)| idx == self.skip) { - Some((_idx, mtch)) => { - chosen.push((mtch.text.to_string(), false)); + // In multi-selection mode, this appends the selected hint to the + // vector of selections. In normal mode, this returns with the hint + // selected. + Key::Insert => match self.matches.get(self.focus_index) { + Some(mat) => { + chosen.push((mat.text.to_string(), false)); if !self.multi { return CaptureEvent::Hint(chosen); @@ -232,14 +336,15 @@ impl<'a> View<'a> { None => panic!("Match not found?"), }, - // Move focus to next/prev hint. + // Move focus to next/prev match. 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. + // Pressing space finalizes an ongoing multi-hint selection (without + // selecting the focused match). Pressing other characters attempts at + // finding a match with a corresponding hint. Key::Char(ch) => { if ch == ' ' && self.multi { return CaptureEvent::Hint(chosen); @@ -250,11 +355,16 @@ impl<'a> View<'a> { typed_hint.push_str(&lower_key); - let selection = self.matches.iter().find(|&mtch| mtch.hint == Some(typed_hint.clone())); + // Find the match that corresponds to the entered key. + let selection = self + .matches + .iter() + // Avoid cloning typed_hint for comparison. + .find(|&mat| mat.hint.as_deref().unwrap_or_default() == &typed_hint); match selection { - Some(mtch) => { - chosen.push((mtch.text.to_string(), key != lower_key)); + Some(mat) => { + chosen.push((mat.text.to_string(), key != lower_key)); if self.multi { typed_hint.clear(); @@ -263,6 +373,8 @@ impl<'a> View<'a> { } } None => { + // TODO: use a Trie or another data structure to determine + // if the entered key belongs to a longer hint. if !self.multi && typed_hint.len() >= longest_hint.len() { break; } @@ -274,6 +386,8 @@ impl<'a> View<'a> { _ => (), } + // Render on stdout if we did not exit earlier (move focus, + // multi-selection). self.render(stdout); } @@ -299,35 +413,403 @@ impl<'a> View<'a> { mod tests { use super::*; - fn split(output: &str) -> Vec<&str> { - output.split("\n").collect::>() + #[test] + fn test_render_all_lines() { + let content = "some text +* e006b06 - (12 days ago) swapper: Make quotes +path: /usr/local/bin/git + + +path: /usr/local/bin/cargo"; + let lines: Vec<&str> = content.split('\n').collect(); + + let mut writer = vec![]; + View::render_lines(&mut writer, &lines); + + 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!( + writer, + format!( + "{}some text{}* e006b06 - (12 days ago) swapper: Make quotes{}path: /usr/local/bin/git{}path: /usr/local/bin/cargo", + goto1, goto2, goto3, goto6, + ) + .as_bytes() + ); } #[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"), + 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); + let colors = colors::RenderingColors { + focus_fg_color: Box::new(&(color::Red)), + focus_bg_color: Box::new(&(color::Blue)), + normal_fg_color: Box::new(&color::Green), + normal_bg_color: Box::new(&color::Magenta), + hint_fg_color: Box::new(&color::Yellow), + hint_bg_color: Box::new(&color::Cyan), }; - // let result = view.make_hint_text("a"); - // assert_eq!(result, "a".to_string()); + View::render_matched_text(&mut writer, text, focused, offset, &colors); - // view.contrast = true; - // let result = view.make_hint_text("a"); - // assert_eq!(result, "[a]".to_string()); + assert_eq!( + writer, + format!( + "{goto}{bg}{fg}{text}{fg_reset}{bg_reset}", + goto = cursor::Goto(4, 2), + fg = color::Fg(*colors.focus_fg_color), + bg = color::Bg(*colors.focus_bg_color), + fg_reset = color::Fg(color::Reset), + bg_reset = color::Bg(color::Reset), + text = &text, + ) + .as_bytes() + ); + } + + #[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); + let colors = colors::RenderingColors { + focus_fg_color: Box::new(&(color::Red)), + focus_bg_color: Box::new(&(color::Blue)), + normal_fg_color: Box::new(&color::Green), + normal_bg_color: Box::new(&color::Magenta), + hint_fg_color: Box::new(&color::Yellow), + hint_bg_color: Box::new(&color::Cyan), + }; + + View::render_matched_text(&mut writer, text, focused, offset, &colors); + + assert_eq!( + writer, + format!( + "{goto}{bg}{fg}{text}{fg_reset}{bg_reset}", + goto = cursor::Goto(4, 2), + fg = color::Fg(*colors.normal_fg_color), + bg = color::Bg(*colors.normal_bg_color), + 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); + let colors = colors::RenderingColors { + focus_fg_color: Box::new(&(color::Red)), + focus_bg_color: Box::new(&(color::Blue)), + normal_fg_color: Box::new(&color::Green), + normal_bg_color: Box::new(&color::Magenta), + hint_fg_color: Box::new(&color::Yellow), + hint_bg_color: Box::new(&color::Cyan), + }; + + let extra_offset = 0; + let contrast_style = None; + + View::render_matched_hint( + &mut writer, + hint_text, + (offset.0 + extra_offset, offset.1), + &colors, + &contrast_style, + ); + + assert_eq!( + writer, + format!( + "{goto}{bg}{fg}{text}{fg_reset}{bg_reset}", + goto = cursor::Goto(4, 2), + fg = color::Fg(*colors.hint_fg_color), + bg = color::Bg(*colors.hint_bg_color), + 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); + let colors = colors::RenderingColors { + focus_fg_color: Box::new(&(color::Red)), + focus_bg_color: Box::new(&(color::Blue)), + normal_fg_color: Box::new(&color::Green), + normal_bg_color: Box::new(&color::Magenta), + hint_fg_color: Box::new(&color::Yellow), + hint_bg_color: Box::new(&color::Cyan), + }; + + let extra_offset = 0; + let contrast_style = Some(ContrastStyle::Underlined); + + View::render_matched_hint( + &mut writer, + hint_text, + (offset.0 + extra_offset, offset.1), + &colors, + &contrast_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_color), + bg = color::Bg(*colors.hint_bg_color), + 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); + let colors = colors::RenderingColors { + focus_fg_color: Box::new(&(color::Red)), + focus_bg_color: Box::new(&(color::Blue)), + normal_fg_color: Box::new(&color::Green), + normal_bg_color: Box::new(&color::Magenta), + hint_fg_color: Box::new(&color::Yellow), + hint_bg_color: Box::new(&color::Cyan), + }; + + let extra_offset = 0; + let contrast_style = Some(ContrastStyle::Surrounded('{', '}')); + + View::render_matched_hint( + &mut writer, + hint_text, + (offset.0 + extra_offset, offset.1), + &colors, + &contrast_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_color), + bg = color::Bg(*colors.hint_bg_color), + fg_reset = color::Fg(color::Reset), + bg_reset = color::Bg(color::Reset), + bra = '{', + bra_close = '}', + text = "eo", + ) + .as_bytes() + ); + } + + #[test] + /// Simulates rendering without any match. + fn test_render_full_without_matches() { + let content = "lorem 127.0.0.1 lorem + +Barcelona https://en.wikipedia.org/wiki/Barcelona - "; + + let lines = content.split('\n').collect(); + + let custom_regexes = [].to_vec(); + let alphabet = "abcd"; + let mut state = state::State::new(&lines, alphabet, &custom_regexes); + let rendering_colors = colors::RenderingColors { + focus_fg_color: Box::new(&(color::Red)), + focus_bg_color: Box::new(&(color::Blue)), + normal_fg_color: Box::new(&color::Green), + normal_bg_color: Box::new(&color::Magenta), + hint_fg_color: Box::new(&color::Yellow), + hint_bg_color: Box::new(&color::Cyan), + }; + let rendering_edge = RenderingEdge::Leading; + + // create a view without any match + let view = View { + state: &mut state, + matches: vec![], // no matches + focus_index: 0, + multi: false, + rendering_edge, + rendering_colors: &rendering_colors, + contrast_style: None, + }; + + let mut writer = vec![]; + view.render(&mut writer); + + let hide = cursor::Hide; + let goto1 = cursor::Goto(1, 1); + let goto3 = cursor::Goto(1, 3); + + let expected = format!( + "{hide}{goto1}lorem 127.0.0.1 lorem\ + {goto3}Barcelona https://en.wikipedia.org/wiki/Barcelona -", + hide = hide, + goto1 = goto1, + goto3 = goto3, + ); + + // println!("{:?}", writer); + // println!("{:?}", expected.as_bytes()); + + // println!("matches: {}", view.matches.len()); + // println!("lines: {}", lines.len()); + + assert_eq!(writer, expected.as_bytes()); + } + + #[test] + /// Simulates rendering with matches. + fn test_render_full_with_matches() { + let content = "lorem 127.0.0.1 lorem + +Barcelona https://en.wikipedia.org/wiki/Barcelona - "; + + let lines = content.split('\n').collect(); + + let custom_regexes = [].to_vec(); + let alphabet = "abcd"; + let mut state = state::State::new(&lines, alphabet, &custom_regexes); + let multi = false; + let reversed = true; + let unique = false; + + let rendering_colors = colors::RenderingColors { + focus_fg_color: Box::new(&(color::Red)), + focus_bg_color: Box::new(&(color::Blue)), + normal_fg_color: Box::new(&color::Green), + normal_bg_color: Box::new(&color::Magenta), + hint_fg_color: Box::new(&color::Yellow), + hint_bg_color: Box::new(&color::Cyan), + }; + let rendering_edge = RenderingEdge::Leading; + let contrast_style = None; + + let view = View::new( + &mut state, + multi, + reversed, + unique, + rendering_edge, + &rendering_colors, + contrast_style, + ); + + let mut writer = vec![]; + view.render(&mut writer); + + let expected_content = { + let hide = cursor::Hide; + let goto1 = cursor::Goto(1, 1); + let goto3 = cursor::Goto(1, 3); + + format!( + "{hide}{goto1}lorem 127.0.0.1 lorem\ + {goto3}Barcelona https://en.wikipedia.org/wiki/Barcelona -", + hide = hide, + goto1 = goto1, + goto3 = goto3, + ) + }; + + let expected_match1_text = { + let goto7_1 = cursor::Goto(7, 1); + format!( + "{goto7_1}{normal_bg_color}{normal_fg_color}127.0.0.1{fg_reset}{bg_reset}", + goto7_1 = goto7_1, + normal_fg_color = color::Fg(*rendering_colors.normal_fg_color), + normal_bg_color = color::Bg(*rendering_colors.normal_bg_color), + fg_reset = color::Fg(color::Reset), + bg_reset = color::Bg(color::Reset) + ) + }; + + let expected_match1_hint = { + let goto7_1 = cursor::Goto(7, 1); + + format!( + "{goto7_1}{hint_bg_color}{hint_fg_color}b{fg_reset}{bg_reset}", + goto7_1 = goto7_1, + hint_fg_color = color::Fg(*rendering_colors.hint_fg_color), + hint_bg_color = color::Bg(*rendering_colors.hint_bg_color), + fg_reset = color::Fg(color::Reset), + bg_reset = color::Bg(color::Reset) + ) + }; + + let expected_match2_text = { + let goto11_3 = cursor::Goto(11, 3); + format!( + "{goto11_3}{focus_bg_color}{focus_fg_color}https://en.wikipedia.org/wiki/Barcelona{fg_reset}{bg_reset}", + goto11_3 = goto11_3, + focus_fg_color = color::Fg(*rendering_colors.focus_fg_color), + focus_bg_color = color::Bg(*rendering_colors.focus_bg_color), + fg_reset = color::Fg(color::Reset), + bg_reset = color::Bg(color::Reset) + ) + }; + + let expected_match2_hint = { + let goto11_3 = cursor::Goto(11, 3); + + format!( + "{goto11_3}{hint_bg_color}{hint_fg_color}a{fg_reset}{bg_reset}", + goto11_3 = goto11_3, + hint_fg_color = color::Fg(*rendering_colors.hint_fg_color), + hint_bg_color = color::Bg(*rendering_colors.hint_bg_color), + fg_reset = color::Fg(color::Reset), + bg_reset = color::Bg(color::Reset) + ) + }; + + let expected = [ + expected_content, + expected_match1_text, + expected_match1_hint, + expected_match2_text, + expected_match2_hint, + ] + .concat(); + + // 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); + + assert_eq!(2, view.matches.len()); + + assert_eq!(writer, expected.as_bytes()); } } From 0d45a2872ace9f1d6f9aaff5a8bcd6668aed7845 Mon Sep 17 00:00:00 2001 From: graelo Date: Sat, 23 May 2020 09:30:09 +0200 Subject: [PATCH 03/40] refactor: simpler names --- Cargo.lock | 118 +++++++++++++++++++++++++ Cargo.toml | 1 + src/colors.rs | 14 --- src/main.rs | 46 +++++++--- src/view.rs | 236 ++++++++++++++++++++++++++------------------------ 5 files changed, 276 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a95911..d17caff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,14 @@ dependencies = [ "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "hermit-abi" version = "0.1.12" @@ -73,6 +81,46 @@ name = "numtoa" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "proc-macro-error" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro-error-attr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", + "syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro2" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "redox_syscall" version = "0.1.56" @@ -107,6 +155,48 @@ name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "structopt" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt-derive 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "structopt-derive" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-error 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "termion" version = "1.5.5" @@ -140,19 +230,35 @@ version = "0.4.1" dependencies = [ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicode-width" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "vec_map" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "version_check" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "winapi" version = "0.3.8" @@ -178,21 +284,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum hermit-abi 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "61565ff7aaace3525556587bd2dc31d4a07071957be715e63ce7b1eccf51a8f4" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)" = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" "checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" "checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +"checksum proc-macro-error 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" +"checksum proc-macro-error-attr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" +"checksum proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "de40dd4ff82d9c9bab6dae29dbab1167e515f8df9ed17d2987cb6012db206933" +"checksum quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" "checksum regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" "checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +"checksum structopt 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "863246aaf5ddd0d6928dfeb1a9ca65f505599e4e1b399935ef7e75107516b4ef" +"checksum structopt-derive 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "d239ca4b13aee7a2142e6795cbd69e457665ff8037aed33b3effdc430d2f927a" +"checksum syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)" = "95b5f192649e48a5302a13f2feb224df883b98933222369e4b3b0fe2a5447269" +"checksum syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" "checksum termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" "checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" +"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +"checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 5ffbe36..96e2c6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ license = "MIT" termion = "1.5" regex = "1.3.1" clap = "2.33.0" +structopt = { version = "0.3", default-features = false } [[bin]] name = "thumbs" diff --git a/src/colors.rs b/src/colors.rs index 2f213ee..be5f92d 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -15,20 +15,6 @@ pub fn get_color(color_name: &str) -> Box<&dyn color::Color> { } } -/// 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. -pub struct RenderingColors<'a> { - pub focus_fg_color: Box<&'a dyn color::Color>, - pub focus_bg_color: Box<&'a dyn color::Color>, - pub normal_fg_color: Box<&'a dyn color::Color>, - pub normal_bg_color: Box<&'a dyn color::Color>, - pub hint_fg_color: Box<&'a dyn color::Color>, - pub hint_bg_color: Box<&'a dyn color::Color>, -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/main.rs b/src/main.rs index 677905f..0983e29 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,9 +11,27 @@ use clap::crate_version; use std::fs::OpenOptions; use std::io::prelude::*; use std::io::{self, Read}; +use structopt::StructOpt; // TODO: position as an enum ::Leading ::Trailing +/// A lightning fast version copy/pasting like vimium/vimperator. +#[derive(StructOpt, Debug)] +#[structopt(name = "thumbs")] +struct Opt { + /// Sets the alphabet. + #[structopt(short, long, default_value = "qwerty")] + alphabet: String, + + /// Sets the foreground color for matches. + #[structopt(long, default_value = "green")] + fg_color: String, + + /// Sets the background color for matches. + #[structopt(long, default_value = "black")] + bg_color: String, +} + fn app_args<'a>() -> clap::ArgMatches<'a> { App::new("thumbs") .version(crate_version!()) @@ -151,23 +169,23 @@ fn main() { let mut state = state::State::new(&lines, alphabet, ®exp); - let rendering_edge = if position == "left" { - view::RenderingEdge::Leading + let hint_alignment = if position == "left" { + view::HintAlignment::Leading } else { - view::RenderingEdge::Trailing + view::HintAlignment::Trailing }; - let rendering_colors = colors::RenderingColors { - focus_fg_color: select_foreground_color, - focus_bg_color: select_background_color, - normal_fg_color: foreground_color, - normal_bg_color: background_color, - hint_fg_color: hint_foreground_color, - hint_bg_color: hint_background_color, + let rendering_colors = view::ViewColors { + focus_fg: select_foreground_color, + focus_bg: select_background_color, + match_fg: foreground_color, + match_bg: background_color, + hint_fg: hint_foreground_color, + hint_bg: hint_background_color, }; - let contrast_style = if contrast { - Some(view::ContrastStyle::Surrounded('[', ']')) + let hint_style = if contrast { + Some(view::HintStyle::Surrounded('[', ']')) } else { None }; @@ -178,9 +196,9 @@ fn main() { multi, reverse, unique, - rendering_edge, + hint_alignment, &rendering_colors, - contrast_style, + hint_style, ); viewbox.present() diff --git a/src/view.rs b/src/view.rs index 16c99fc..6d18c38 100644 --- a/src/view.rs +++ b/src/view.rs @@ -13,14 +13,28 @@ pub struct View<'a> { matches: Vec>, focus_index: usize, multi: bool, - rendering_edge: RenderingEdge, - rendering_colors: &'a colors::RenderingColors<'a>, - contrast_style: Option, + hint_alignment: HintAlignment, + rendering_colors: &'a ViewColors<'a>, + hint_style: Option, +} + +/// 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. +pub struct ViewColors<'a> { + pub focus_fg: Box<&'a dyn color::Color>, + pub focus_bg: Box<&'a dyn color::Color>, + pub match_fg: Box<&'a dyn color::Color>, + pub match_bg: Box<&'a dyn color::Color>, + pub hint_fg: Box<&'a dyn color::Color>, + pub hint_bg: Box<&'a dyn color::Color>, } /// Describes if, during rendering, a hint should aligned to the leading edge of /// the matched text, or to its trailing edge. -pub enum RenderingEdge { +pub enum HintAlignment { Leading, Trailing, } @@ -30,7 +44,7 @@ pub enum RenderingEdge { /// /// # Note /// In practice, this is wrapped in an `Option`, so that the hint's text can be rendered with no style. -pub enum ContrastStyle { +pub enum HintStyle { /// The hint's text will be underlined (leveraging `termion::style::Underline`). Underlined, /// The hint's text will be surrounded by these chars. @@ -51,9 +65,9 @@ impl<'a> View<'a> { multi: bool, reversed: bool, unique: bool, - rendering_edge: RenderingEdge, - rendering_colors: &'a colors::RenderingColors, - contrast_style: Option, + hint_alignment: HintAlignment, + rendering_colors: &'a ViewColors, + hint_style: Option, ) -> View<'a> { let matches = state.matches(reversed, unique); let focus_index = if reversed { matches.len() - 1 } else { 0 }; @@ -63,9 +77,9 @@ impl<'a> View<'a> { matches, focus_index, multi, - rendering_edge, + hint_alignment, rendering_colors, - contrast_style, + hint_style, } } @@ -117,13 +131,13 @@ impl<'a> View<'a> { text: &str, focused: bool, offset: (usize, usize), - colors: &colors::RenderingColors, + colors: &ViewColors, ) { // To help identify it, the match thas has focus is rendered with a dedicated color. let (text_fg_color, text_bg_color) = if focused { - (&colors.focus_fg_color, &colors.focus_bg_color) + (&colors.focus_fg, &colors.focus_bg) } else { - (&colors.normal_fg_color, &colors.normal_bg_color) + (&colors.match_fg, &colors.match_bg) }; // Render just the Match's text on top of existing content. @@ -146,7 +160,7 @@ impl<'a> View<'a> { /// - just colors /// - underlined with colors /// - surrounding the hint's text with some delimiters, see - /// `ContrastStyle::Delimited`. + /// `HintStyle::Delimited`. /// /// # Note /// This writes directly on the writer, avoiding extra allocation. @@ -154,15 +168,15 @@ impl<'a> View<'a> { stdout: &mut dyn Write, hint_text: &str, offset: (usize, usize), - colors: &colors::RenderingColors, - contrast_style: &Option, + colors: &ViewColors, + hint_style: &Option, ) { - let fg_color = color::Fg(*colors.hint_fg_color); - let bg_color = color::Bg(*colors.hint_bg_color); + let fg_color = color::Fg(*colors.hint_fg); + let bg_color = color::Bg(*colors.hint_bg); let fg_reset = color::Fg(color::Reset); let bg_reset = color::Bg(color::Reset); - match contrast_style { + match hint_style { None => { write!( stdout, @@ -176,8 +190,8 @@ impl<'a> View<'a> { ) .unwrap(); } - Some(contrast_style) => match contrast_style { - ContrastStyle::Underlined => { + Some(hint_style) => match hint_style { + HintStyle::Underlined => { write!( stdout, "{goto}{bg_color}{fg_color}{sty}{hint}{sty_reset}{fg_reset}{bg_reset}", @@ -192,7 +206,7 @@ impl<'a> View<'a> { ) .unwrap(); } - ContrastStyle::Surrounded(opening, closing) => { + HintStyle::Surrounded(opening, closing) => { write!( stdout, "{goto}{bg_color}{fg_color}{bra}{hint}{bra_close}{fg_reset}{bg_reset}", @@ -218,7 +232,7 @@ impl<'a> View<'a> { /// - 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.rendering_edge`, the hint can be rendered + /// 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. /// @@ -255,9 +269,9 @@ impl<'a> View<'a> { // 3. Render the hint (e.g. "eo") as an overlay on top of the rendered matched text, // aligned at its leading or the trailing edge. if let Some(ref hint) = mat.hint { - let extra_offset = match self.rendering_edge { - RenderingEdge::Leading => 0, - RenderingEdge::Trailing => text.len() - hint.len(), + let extra_offset = match self.hint_alignment { + HintAlignment::Leading => 0, + HintAlignment::Trailing => text.len() - hint.len(), }; View::render_matched_hint( @@ -265,7 +279,7 @@ impl<'a> View<'a> { hint, (offset_x + extra_offset, offset_y), &self.rendering_colors, - &self.contrast_style, + &self.hint_style, ); } } @@ -446,13 +460,13 @@ path: /usr/local/bin/cargo"; let text = "https://en.wikipedia.org/wiki/Barcelona"; let focused = true; let offset: (usize, usize) = (3, 1); - let colors = colors::RenderingColors { - focus_fg_color: Box::new(&(color::Red)), - focus_bg_color: Box::new(&(color::Blue)), - normal_fg_color: Box::new(&color::Green), - normal_bg_color: Box::new(&color::Magenta), - hint_fg_color: Box::new(&color::Yellow), - hint_bg_color: Box::new(&color::Cyan), + let colors = ViewColors { + focus_fg: Box::new(&(color::Red)), + focus_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), }; View::render_matched_text(&mut writer, text, focused, offset, &colors); @@ -462,8 +476,8 @@ path: /usr/local/bin/cargo"; format!( "{goto}{bg}{fg}{text}{fg_reset}{bg_reset}", goto = cursor::Goto(4, 2), - fg = color::Fg(*colors.focus_fg_color), - bg = color::Bg(*colors.focus_bg_color), + fg = color::Fg(*colors.focus_fg), + bg = color::Bg(*colors.focus_bg), fg_reset = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset), text = &text, @@ -478,13 +492,13 @@ path: /usr/local/bin/cargo"; let text = "https://en.wikipedia.org/wiki/Barcelona"; let focused = false; let offset: (usize, usize) = (3, 1); - let colors = colors::RenderingColors { - focus_fg_color: Box::new(&(color::Red)), - focus_bg_color: Box::new(&(color::Blue)), - normal_fg_color: Box::new(&color::Green), - normal_bg_color: Box::new(&color::Magenta), - hint_fg_color: Box::new(&color::Yellow), - hint_bg_color: Box::new(&color::Cyan), + let colors = ViewColors { + focus_fg: Box::new(&(color::Red)), + focus_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), }; View::render_matched_text(&mut writer, text, focused, offset, &colors); @@ -494,8 +508,8 @@ path: /usr/local/bin/cargo"; format!( "{goto}{bg}{fg}{text}{fg_reset}{bg_reset}", goto = cursor::Goto(4, 2), - fg = color::Fg(*colors.normal_fg_color), - bg = color::Bg(*colors.normal_bg_color), + fg = color::Fg(*colors.match_fg), + bg = color::Bg(*colors.match_bg), fg_reset = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset), text = &text, @@ -509,24 +523,24 @@ path: /usr/local/bin/cargo"; let mut writer = vec![]; let hint_text = "eo"; let offset: (usize, usize) = (3, 1); - let colors = colors::RenderingColors { - focus_fg_color: Box::new(&(color::Red)), - focus_bg_color: Box::new(&(color::Blue)), - normal_fg_color: Box::new(&color::Green), - normal_bg_color: Box::new(&color::Magenta), - hint_fg_color: Box::new(&color::Yellow), - hint_bg_color: Box::new(&color::Cyan), + let colors = ViewColors { + focus_fg: Box::new(&(color::Red)), + focus_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 contrast_style = None; + let hint_style = None; View::render_matched_hint( &mut writer, hint_text, (offset.0 + extra_offset, offset.1), &colors, - &contrast_style, + &hint_style, ); assert_eq!( @@ -534,8 +548,8 @@ path: /usr/local/bin/cargo"; format!( "{goto}{bg}{fg}{text}{fg_reset}{bg_reset}", goto = cursor::Goto(4, 2), - fg = color::Fg(*colors.hint_fg_color), - bg = color::Bg(*colors.hint_bg_color), + fg = color::Fg(*colors.hint_fg), + bg = color::Bg(*colors.hint_bg), fg_reset = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset), text = "eo", @@ -549,24 +563,24 @@ path: /usr/local/bin/cargo"; let mut writer = vec![]; let hint_text = "eo"; let offset: (usize, usize) = (3, 1); - let colors = colors::RenderingColors { - focus_fg_color: Box::new(&(color::Red)), - focus_bg_color: Box::new(&(color::Blue)), - normal_fg_color: Box::new(&color::Green), - normal_bg_color: Box::new(&color::Magenta), - hint_fg_color: Box::new(&color::Yellow), - hint_bg_color: Box::new(&color::Cyan), + let colors = ViewColors { + focus_fg: Box::new(&(color::Red)), + focus_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 contrast_style = Some(ContrastStyle::Underlined); + let hint_style = Some(HintStyle::Underlined); View::render_matched_hint( &mut writer, hint_text, (offset.0 + extra_offset, offset.1), &colors, - &contrast_style, + &hint_style, ); assert_eq!( @@ -574,8 +588,8 @@ path: /usr/local/bin/cargo"; format!( "{goto}{bg}{fg}{sty}{text}{sty_reset}{fg_reset}{bg_reset}", goto = cursor::Goto(4, 2), - fg = color::Fg(*colors.hint_fg_color), - bg = color::Bg(*colors.hint_bg_color), + fg = color::Fg(*colors.hint_fg), + bg = color::Bg(*colors.hint_bg), fg_reset = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset), sty = style::Underline, @@ -591,24 +605,24 @@ path: /usr/local/bin/cargo"; let mut writer = vec![]; let hint_text = "eo"; let offset: (usize, usize) = (3, 1); - let colors = colors::RenderingColors { - focus_fg_color: Box::new(&(color::Red)), - focus_bg_color: Box::new(&(color::Blue)), - normal_fg_color: Box::new(&color::Green), - normal_bg_color: Box::new(&color::Magenta), - hint_fg_color: Box::new(&color::Yellow), - hint_bg_color: Box::new(&color::Cyan), + let colors = ViewColors { + focus_fg: Box::new(&(color::Red)), + focus_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 contrast_style = Some(ContrastStyle::Surrounded('{', '}')); + let hint_style = Some(HintStyle::Surrounded('{', '}')); View::render_matched_hint( &mut writer, hint_text, (offset.0 + extra_offset, offset.1), &colors, - &contrast_style, + &hint_style, ); assert_eq!( @@ -616,8 +630,8 @@ path: /usr/local/bin/cargo"; format!( "{goto}{bg}{fg}{bra}{text}{bra_close}{fg_reset}{bg_reset}", goto = cursor::Goto(4, 2), - fg = color::Fg(*colors.hint_fg_color), - bg = color::Bg(*colors.hint_bg_color), + fg = color::Fg(*colors.hint_fg), + bg = color::Bg(*colors.hint_bg), fg_reset = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset), bra = '{', @@ -640,15 +654,15 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let custom_regexes = [].to_vec(); let alphabet = "abcd"; let mut state = state::State::new(&lines, alphabet, &custom_regexes); - let rendering_colors = colors::RenderingColors { - focus_fg_color: Box::new(&(color::Red)), - focus_bg_color: Box::new(&(color::Blue)), - normal_fg_color: Box::new(&color::Green), - normal_bg_color: Box::new(&color::Magenta), - hint_fg_color: Box::new(&color::Yellow), - hint_bg_color: Box::new(&color::Cyan), + let rendering_colors = ViewColors { + focus_fg: Box::new(&(color::Red)), + focus_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 rendering_edge = RenderingEdge::Leading; + let hint_alignment = HintAlignment::Leading; // create a view without any match let view = View { @@ -656,9 +670,9 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; matches: vec![], // no matches focus_index: 0, multi: false, - rendering_edge, + hint_alignment, rendering_colors: &rendering_colors, - contrast_style: None, + hint_style: None, }; let mut writer = vec![]; @@ -701,25 +715,25 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let reversed = true; let unique = false; - let rendering_colors = colors::RenderingColors { - focus_fg_color: Box::new(&(color::Red)), - focus_bg_color: Box::new(&(color::Blue)), - normal_fg_color: Box::new(&color::Green), - normal_bg_color: Box::new(&color::Magenta), - hint_fg_color: Box::new(&color::Yellow), - hint_bg_color: Box::new(&color::Cyan), + let rendering_colors = ViewColors { + focus_fg: Box::new(&(color::Red)), + focus_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 rendering_edge = RenderingEdge::Leading; - let contrast_style = None; + let hint_alignment = HintAlignment::Leading; + let hint_style = None; let view = View::new( &mut state, multi, reversed, unique, - rendering_edge, + hint_alignment, &rendering_colors, - contrast_style, + hint_style, ); let mut writer = vec![]; @@ -742,10 +756,10 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let expected_match1_text = { let goto7_1 = cursor::Goto(7, 1); format!( - "{goto7_1}{normal_bg_color}{normal_fg_color}127.0.0.1{fg_reset}{bg_reset}", + "{goto7_1}{match_bg}{match_fg}127.0.0.1{fg_reset}{bg_reset}", goto7_1 = goto7_1, - normal_fg_color = color::Fg(*rendering_colors.normal_fg_color), - normal_bg_color = color::Bg(*rendering_colors.normal_bg_color), + match_fg = color::Fg(*rendering_colors.match_fg), + match_bg = color::Bg(*rendering_colors.match_bg), fg_reset = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset) ) @@ -755,10 +769,10 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let goto7_1 = cursor::Goto(7, 1); format!( - "{goto7_1}{hint_bg_color}{hint_fg_color}b{fg_reset}{bg_reset}", + "{goto7_1}{hint_bg}{hint_fg}b{fg_reset}{bg_reset}", goto7_1 = goto7_1, - hint_fg_color = color::Fg(*rendering_colors.hint_fg_color), - hint_bg_color = color::Bg(*rendering_colors.hint_bg_color), + hint_fg = color::Fg(*rendering_colors.hint_fg), + hint_bg = color::Bg(*rendering_colors.hint_bg), fg_reset = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset) ) @@ -767,10 +781,10 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let expected_match2_text = { let goto11_3 = cursor::Goto(11, 3); format!( - "{goto11_3}{focus_bg_color}{focus_fg_color}https://en.wikipedia.org/wiki/Barcelona{fg_reset}{bg_reset}", + "{goto11_3}{focus_bg}{focus_fg}https://en.wikipedia.org/wiki/Barcelona{fg_reset}{bg_reset}", goto11_3 = goto11_3, - focus_fg_color = color::Fg(*rendering_colors.focus_fg_color), - focus_bg_color = color::Bg(*rendering_colors.focus_bg_color), + focus_fg = color::Fg(*rendering_colors.focus_fg), + focus_bg = color::Bg(*rendering_colors.focus_bg), fg_reset = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset) ) @@ -780,10 +794,10 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let goto11_3 = cursor::Goto(11, 3); format!( - "{goto11_3}{hint_bg_color}{hint_fg_color}a{fg_reset}{bg_reset}", + "{goto11_3}{hint_bg}{hint_fg}a{fg_reset}{bg_reset}", goto11_3 = goto11_3, - hint_fg_color = color::Fg(*rendering_colors.hint_fg_color), - hint_bg_color = color::Bg(*rendering_colors.hint_bg_color), + hint_fg = color::Fg(*rendering_colors.hint_fg), + hint_bg = color::Bg(*rendering_colors.hint_bg), fg_reset = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset) ) From 37f22b67af5e0a928ed020d7a217121ab332804c Mon Sep 17 00:00:00 2001 From: graelo Date: Sun, 24 May 2020 21:02:11 +0200 Subject: [PATCH 04/40] feat: copyrat --- .rustfmt.toml | 15 +- Cargo.lock | 175 +++++++++---- Cargo.toml | 17 +- src/alphabets.rs | 46 ++-- src/colors.rs | 54 ++-- src/error.rs | 18 ++ src/main.rs | 354 ++++++++++---------------- src/state.rs | 75 ++++-- src/swapper.rs | 631 +++++++++++++++++++++++++---------------------- src/view.rs | 181 ++++++++------ tmux-thumbs.sh | 2 +- 11 files changed, 840 insertions(+), 728 deletions(-) create mode 100644 src/error.rs diff --git a/.rustfmt.toml b/.rustfmt.toml index 44f8e3a..39587b2 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,2 +1,13 @@ -tab_spaces = 2 -max_width = 120 +format_code_in_doc_comments = true +match_block_trailing_comma = true +condense_wildcard_suffixes = true +use_field_init_shorthand = true +overflow_delimited_expr = true +# width_heuristics = "Max" +normalize_comments = true +reorder_impl_items = true +use_try_shorthand = true +newline_style = "Unix" +format_strings = true +wrap_comments = true +# comment_width = 100 diff --git a/Cargo.lock b/Cargo.lock index d17caff..e972d7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,14 +8,6 @@ dependencies = [ "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "ansi_term" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "atty" version = "0.2.14" @@ -26,6 +18,11 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bitflags" version = "1.2.1" @@ -33,18 +30,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "clap" -version = "2.33.0" +version = "3.0.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "clap_derive 3.0.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "os_str_bytes 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "term_size 1.0.0-beta1 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "clap_derive" +version = "3.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-error 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "copyrat" +version = "0.1.0" +dependencies = [ + "clap 3.0.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "heck" version = "0.3.1" @@ -61,6 +84,23 @@ dependencies = [ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "indexmap" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -81,12 +121,17 @@ name = "numtoa" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "os_str_bytes" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "proc-macro-error" -version = "1.0.2" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro-error-attr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-error-attr 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", @@ -95,7 +140,7 @@ dependencies = [ [[package]] name = "proc-macro-error-attr" -version = "1.0.2" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", @@ -152,31 +197,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "structopt" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "structopt-derive 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "structopt-derive" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro-error 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "syn" version = "1.0.23" @@ -197,6 +220,33 @@ dependencies = [ "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "term_size" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "term_size" +version = "1.0.0-beta1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "termion" version = "1.5.5" @@ -213,6 +263,7 @@ name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "term_size 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -224,16 +275,6 @@ dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "thumbs" -version = "0.4.1" -dependencies = [ - "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "structopt 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", - "termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "unicode-segmentation" version = "1.6.0" @@ -259,6 +300,11 @@ name = "version_check" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "winapi" version = "0.3.8" @@ -268,11 +314,24 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -280,29 +339,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] "checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" -"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +"checksum clap 3.0.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)" = "860643c53f980f0d38a5e25dfab6c3c93b2cb3aa1fe192643d17a293c6c41936" +"checksum clap_derive 3.0.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fb51c9e75b94452505acd21d929323f5a5c6c4735a852adbd39ef5fb1b014f30" "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum hermit-abi 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "61565ff7aaace3525556587bd2dc31d4a07071957be715e63ce7b1eccf51a8f4" +"checksum indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)" = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" "checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" "checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" -"checksum proc-macro-error 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" -"checksum proc-macro-error-attr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" +"checksum os_str_bytes 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "06de47b848347d8c4c94219ad8ecd35eb90231704b067e67e6ae2e36ee023510" +"checksum proc-macro-error 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" +"checksum proc-macro-error-attr 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" "checksum proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "de40dd4ff82d9c9bab6dae29dbab1167e515f8df9ed17d2987cb6012db206933" "checksum quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" "checksum regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" "checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" -"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" -"checksum structopt 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "863246aaf5ddd0d6928dfeb1a9ca65f505599e4e1b399935ef7e75107516b4ef" -"checksum structopt-derive 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "d239ca4b13aee7a2142e6795cbd69e457665ff8037aed33b3effdc430d2f927a" +"checksum strsim 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" "checksum syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)" = "95b5f192649e48a5302a13f2feb224df883b98933222369e4b3b0fe2a5447269" "checksum syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +"checksum term_size 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +"checksum term_size 1.0.0-beta1 (registry+https://github.com/rust-lang/crates.io-index)" = "a8a17d8699e154863becdf18e4fd28bd0be27ca72856f54daf75c00f2566898f" +"checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" "checksum termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" @@ -311,6 +375,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 96e2c6e..b3ed495 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,23 +1,22 @@ [package] -name = "thumbs" -version = "0.4.1" -authors = ["Ferran Basora "] +name = "copyrat" +version = "0.1.0" +authors = ["Ferran Basora ", "u0xy "] edition = "2018" -description = "A lightning fast version copy/pasting like vimium/vimperator" +description = "This is tmux-copycat on Rust steroids." repository = "https://github.com/fcsonline/tmux-thumbs" -keywords = ["rust", "tmux", "tmux-plugin", "vimium", "vimperator"] +keywords = ["rust", "tmux", "tmux-plugin", "tmux-copycat"] license = "MIT" [dependencies] termion = "1.5" regex = "1.3.1" -clap = "2.33.0" -structopt = { version = "0.3", default-features = false } +clap = { version = "3.0.0-beta.1", features = ["suggestions", "color", "wrap_help", "term_size"]} [[bin]] -name = "thumbs" +name = "copyrat" path = "src/main.rs" [[bin]] -name = "tmux-thumbs" +name = "tmux-copyrat" path = "src/swapper.rs" diff --git a/src/alphabets.rs b/src/alphabets.rs index 25c1040..7d9897c 100644 --- a/src/alphabets.rs +++ b/src/alphabets.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use crate::error; const ALPHABETS: [(&'static str, &'static str); 22] = [ ("numeric", "1234567890"), @@ -25,17 +25,17 @@ const ALPHABETS: [(&'static str, &'static str); 22] = [ ("colemak-right-hand", "neioluymjhk"), ]; -pub struct Alphabet<'a> { - letters: &'a str, -} +// pub struct Alphabet<'a> { +// letters: &'a str, +// } -impl<'a> Alphabet<'a> { - fn new(letters: &'a str) -> Alphabet { - Alphabet { letters } - } +/// Type-safe string alphabet (newtype). +#[derive(Debug)] +pub struct Alphabet(pub String); +impl Alphabet { pub fn hints(&self, matches: usize) -> Vec { - let letters: Vec = self.letters.chars().map(|s| s.to_string()).collect(); + let letters: Vec = self.0.chars().map(|s| s.to_string()).collect(); let mut expansion = letters.clone(); let mut expanded: Vec = Vec::new(); @@ -64,14 +64,22 @@ impl<'a> Alphabet<'a> { } } -pub fn get_alphabet(alphabet_name: &str) -> Alphabet { - let alphabets: HashMap<&str, &str> = ALPHABETS.iter().cloned().collect(); +// pub fn get_alphabet(alphabet_name: &str) -> Alphabet { +// let alphabets: HashMap<&str, &str> = ALPHABETS.iter().cloned().collect(); - alphabets - .get(alphabet_name) - .expect(format!("Unknown alphabet: {}", alphabet_name).as_str()); // FIXME +// alphabets +// .get(alphabet_name) +// .expect(format!("Unknown alphabet: {}", alphabet_name).as_str()); - Alphabet::new(alphabets[alphabet_name]) +// Alphabet::new(alphabets[alphabet_name]) +// } + +pub fn parse_alphabet(src: &str) -> Result { + let alphabet = ALPHABETS.iter().find(|&(name, _letters)| name == &src); + match alphabet { + Some((_name, letters)) => Ok(Alphabet(letters.to_string())), + None => Err(error::ParseError::UnknownAlphabet), + } } #[cfg(test)] @@ -80,28 +88,28 @@ mod tests { #[test] fn simple_matches() { - let alphabet = Alphabet::new("abcd"); + let alphabet = Alphabet("abcd".to_string()); let hints = alphabet.hints(3); assert_eq!(hints, ["a", "b", "c"]); } #[test] fn composed_matches() { - let alphabet = Alphabet::new("abcd"); + let alphabet = Alphabet("abcd".to_string()); let hints = alphabet.hints(6); assert_eq!(hints, ["a", "b", "c", "da", "db", "dc"]); } #[test] fn composed_matches_multiple() { - let alphabet = Alphabet::new("abcd"); + let alphabet = Alphabet("abcd".to_string()); let hints = alphabet.hints(8); assert_eq!(hints, ["a", "b", "ca", "cb", "da", "db", "dc", "dd"]); } #[test] fn composed_matches_max() { - let alphabet = Alphabet::new("ab"); + let alphabet = Alphabet("ab".to_string()); let hints = alphabet.hints(8); assert_eq!(hints, ["aa", "ab", "ba", "bb"]); } diff --git a/src/colors.rs b/src/colors.rs index be5f92d..b72b9e9 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -1,35 +1,39 @@ +use crate::error; use termion::color; -pub fn get_color(color_name: &str) -> Box<&dyn color::Color> { - match color_name { - "black" => Box::new(&color::Black), - "red" => Box::new(&color::Red), - "green" => Box::new(&color::Green), - "yellow" => Box::new(&color::Yellow), - "blue" => Box::new(&color::Blue), - "magenta" => Box::new(&color::Magenta), - "cyan" => Box::new(&color::Cyan), - "white" => Box::new(&color::White), - "default" => Box::new(&color::Reset), - _ => panic!("Unknown color: {}", color_name), - } +pub fn parse_color(src: &str) -> Result, error::ParseError> { + match src { + "black" => Ok(Box::new(color::Black)), + "red" => Ok(Box::new(color::Red)), + "green" => Ok(Box::new(color::Green)), + "yellow" => Ok(Box::new(color::Yellow)), + "blue" => Ok(Box::new(color::Blue)), + "magenta" => Ok(Box::new(color::Magenta)), + "cyan" => Ok(Box::new(color::Cyan)), + "white" => Ok(Box::new(color::White)), + // "default" => Ok(Box::new(color::Reset)), + _ => Err(error::ParseError::UnknownColor), + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn match_color() { - let text1 = println!("{}{}", color::Fg(*get_color("green")), "foo"); - let text2 = println!("{}{}", color::Fg(color::Green), "foo"); + #[test] + fn match_color() { + let text1 = format!( + "{}{}", + color::Fg(parse_color("green").unwrap().as_ref()), + "foo" + ); + let text2 = format!("{}{}", color::Fg(color::Green), "foo"); - assert_eq!(text1, text2); - } + assert_eq!(text1, text2); + } - #[test] - #[should_panic] - fn no_match_color() { - println!("{}{}", color::Fg(*get_color("wat")), "foo"); - } + #[test] + fn no_match_color() { + assert!(parse_color("wat").is_err(), "this color should not exist"); + } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..8ec3818 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,18 @@ +use std::fmt; + +#[derive(Debug)] +pub enum ParseError { + ExpectedSurroundingPair, + UnknownAlphabet, + UnknownColor, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ParseError::ExpectedSurroundingPair => write!(f, "Expected 2 chars"), + ParseError::UnknownAlphabet => write!(f, "Expected a known alphabet"), + ParseError::UnknownColor => write!(f, "Expected ANSI color name (magenta, cyan, black, ...)"), + } + } +} diff --git a/src/main.rs b/src/main.rs index 0983e29..86d02dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,239 +1,159 @@ -extern crate clap; -extern crate termion; - -mod alphabets; -mod colors; -mod state; -mod view; - -use self::clap::{App, Arg}; -use clap::crate_version; +use clap::Clap; use std::fs::OpenOptions; use std::io::prelude::*; use std::io::{self, Read}; -use structopt::StructOpt; +use std::path; -// TODO: position as an enum ::Leading ::Trailing +mod alphabets; +mod colors; +mod error; +mod state; +mod view; -/// A lightning fast version copy/pasting like vimium/vimperator. -#[derive(StructOpt, Debug)] -#[structopt(name = "thumbs")] +/// Main configuration, parsed from command line. +#[derive(Clap, Debug)] +#[clap(author, about, version)] struct Opt { - /// Sets the alphabet. - #[structopt(short, long, default_value = "qwerty")] - alphabet: String, + /// Alphabet to draw hints from. + /// + /// Possible values are "{A}", "{A}-homerow", "{A}-left-hand", + /// "{A}-right-hand", where "{A}" is one of "qwerty", "azerty", "qwertz", + /// "dvorak", "colemak". Examples: "qwerty", "dvorak-homerow". + #[clap(short = "k", long, default_value = "qwerty", + parse(try_from_str = alphabets::parse_alphabet))] + alphabet: alphabets::Alphabet, - /// Sets the foreground color for matches. - #[structopt(long, default_value = "green")] - fg_color: String, + /// Enable multi-selection. + #[clap(short, long)] + multi_selection: bool, - /// Sets the background color for matches. - #[structopt(long, default_value = "black")] - bg_color: String, + #[clap(flatten)] + colors: view::ViewColors, + + /// Reverse the order for assigned hints. + #[clap(short, long)] + reverse: bool, + + /// Keep the same hint for identical matches. + #[clap(short, long)] + unique: bool, + + /// Align hint with its match. + #[clap(short = "a", long, arg_enum, default_value = "Leading")] + hint_alignment: view::HintAlignment, + + /// Additional regex patterns. + #[clap(short = "c", long)] + custom_regex: Vec, + + /// Optional hint styling. + /// + /// Underline or surround the hint for increased visibility. + /// If not provided, only the hint colors will be used. + #[clap(short = "s", long, arg_enum)] + hint_style: Option, + + /// Chars surrounding each hint, used with `Surrounded` style. + #[clap(long, default_value = "{}", + parse(try_from_str = parse_chars))] + hint_surroundings: (char, char), + + /// Target path where to store the selected matches. + #[clap(short = "o", long = "output", parse(from_os_str))] + target_path: Option, + + /// Only output if key was uppercased. + #[clap(long)] + uppercased: bool, } -fn app_args<'a>() -> clap::ArgMatches<'a> { - App::new("thumbs") - .version(crate_version!()) - .about("A lightning fast version copy/pasting like vimium/vimperator") - .arg( - Arg::with_name("alphabet") - .help("Sets the alphabet") - .long("alphabet") - .short("a") - .default_value("qwerty"), - ) - .arg( - Arg::with_name("format") - .help("Specifies the out format for the picked hint. (%U: Upcase, %H: Hint)") - .long("format") - .short("f") - .default_value("%H"), - ) - .arg( - Arg::with_name("foreground_color") - .help("Sets the foregroud color for matches") - .long("fg-color") - .default_value("green"), - ) - .arg( - Arg::with_name("background_color") - .help("Sets the background color for matches") - .long("bg-color") - .default_value("black"), - ) - .arg( - Arg::with_name("hint_foreground_color") - .help("Sets the foregroud color for hints") - .long("hint-fg-color") - .default_value("yellow"), - ) - .arg( - Arg::with_name("hint_background_color") - .help("Sets the background color for hints") - .long("hint-bg-color") - .default_value("black"), - ) - .arg( - Arg::with_name("select_foreground_color") - .help("Sets the foreground color for selection") - .long("select-fg-color") - .default_value("blue"), - ) - .arg( - Arg::with_name("select_background_color") - .help("Sets the background color for selection") - .long("select-bg-color") - .default_value("black"), - ) - .arg( - Arg::with_name("multi") - .help("Enable multi-selection") - .long("multi") - .short("m"), - ) - .arg( - Arg::with_name("reverse") - .help("Reverse the order for assigned hints") - .long("reverse") - .short("r"), - ) - .arg( - Arg::with_name("unique") - .help("Don't show duplicated hints for the same match") - .long("unique") - .short("u"), - ) - .arg( - Arg::with_name("position") - .help("Hint position") - .long("position") - .default_value("left") - .short("p"), - ) - .arg( - Arg::with_name("regexp") - .help("Use this regexp as extra pattern to match") - .long("regexp") - .short("x") - .takes_value(true) - .multiple(true), - ) - .arg( - Arg::with_name("contrast") - .help("Put square brackets around hint for visibility") - .long("contrast") - .short("c"), - ) - .arg( - Arg::with_name("target") - .help("Stores the hint in the specified path") - .long("target") - .short("t") - .takes_value(true), - ) - .get_matches() +/// Type introduced due to parsing limitation, +/// as we cannot directly parse into view::HintStyle. +#[derive(Debug, Clap)] +enum HintStyleCli { + Underlined, + Surrounded, +} + +fn parse_chars(src: &str) -> Result<(char, char), error::ParseError> { + if src.len() != 2 { + return Err(error::ParseError::ExpectedSurroundingPair); + } + + let chars: Vec = src.chars().collect(); + Ok((chars[0], chars[1])) } fn main() { - let args = app_args(); - let format = args.value_of("format").unwrap(); - let alphabet = args.value_of("alphabet").unwrap(); - let position = args.value_of("position").unwrap(); - let target = args.value_of("target"); - let multi = args.is_present("multi"); - let reverse = args.is_present("reverse"); - let unique = args.is_present("unique"); - let contrast = args.is_present("contrast"); - let regexp = if let Some(items) = args.values_of("regexp") { - items.collect::>() - } else { - [].to_vec() - }; + let opt = Opt::parse(); - let foreground_color = colors::get_color(args.value_of("foreground_color").unwrap()); - let background_color = colors::get_color(args.value_of("background_color").unwrap()); - let hint_foreground_color = colors::get_color(args.value_of("hint_foreground_color").unwrap()); - let hint_background_color = colors::get_color(args.value_of("hint_background_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()); + // Copy the pane contents (piped in via stdin) into a buffer, and split lines. + let stdin = io::stdin(); + let mut handle = stdin.lock(); - // Copy the pane contents (piped in via stdin) into a buffer, and split lines. - let mut buffer = String::new(); - let stdin = io::stdin(); - let mut handle = stdin.lock(); + let mut buffer = String::new(); + handle.read_to_string(&mut buffer).unwrap(); + let lines: Vec<&str> = buffer.split('\n').collect(); - handle.read_to_string(&mut buffer).unwrap(); + let mut state = state::State::new(&lines, &opt.alphabet, &opt.custom_regex); - let lines: Vec<&str> = buffer.split('\n').collect(); + let hint_style = match opt.hint_style { + None => None, + Some(style) => match style { + HintStyleCli::Underlined => Some(view::HintStyle::Underlined), + HintStyleCli::Surrounded => { + let (open, close) = opt.hint_surroundings; + Some(view::HintStyle::Surrounded(open, close)) + } + }, + }; + let uppercase_flag = opt.uppercased; - let mut state = state::State::new(&lines, alphabet, ®exp); + let selections = { + let mut viewbox = view::View::new( + &mut state, + opt.multi_selection, + opt.reverse, + opt.unique, + opt.hint_alignment, + &opt.colors, + hint_style, + ); - let hint_alignment = if position == "left" { - view::HintAlignment::Leading - } else { - view::HintAlignment::Trailing - }; + viewbox.present() + }; - let rendering_colors = view::ViewColors { - focus_fg: select_foreground_color, - focus_bg: select_background_color, - match_fg: foreground_color, - match_bg: background_color, - hint_fg: hint_foreground_color, - hint_bg: hint_background_color, - }; - - let hint_style = if contrast { - Some(view::HintStyle::Surrounded('[', ']')) - } else { - None - }; - - let selections = { - let mut viewbox = view::View::new( - &mut state, - multi, - reverse, - unique, - hint_alignment, - &rendering_colors, - hint_style, - ); - - viewbox.present() - }; - - // Early exit, signaling tmux we had no selections. - if selections.is_empty() { - ::std::process::exit(1); - } - - let output = selections - .iter() - .map(|(text, upcase)| { - let upcase_value = if *upcase { "true" } else { "false" }; - - let mut output = format.to_string(); - - output = str::replace(&output, "%U", upcase_value); - output = str::replace(&output, "%H", text.as_str()); - output - }) - .collect::>() - .join("\n"); - - match target { - None => println!("{}", output), - Some(target) => { - let mut file = OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(target) - .expect("Unable to open the target file"); - - file.write(output.as_bytes()).unwrap(); + // Early exit, signaling tmux we had no selections. + if selections.is_empty() { + ::std::process::exit(1); + } + + let output = selections + .iter() + .map(|(text, uppercased)| { + let upcase_value = if *uppercased { "true" } else { "false" }; + + let output = if uppercase_flag { upcase_value } else { text }; + // let mut output = &opt.format; + + // output = str::replace(&output, "%U", upcase_value); + // output = str::replace(&output, "%H", text.as_str()); + output + }) + .collect::>() + .join("\n"); + + match opt.target_path { + None => println!("{}", output), + Some(target) => { + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(target) + .expect("Unable to open the target file"); + + file.write(output.as_bytes()).unwrap(); + } } - } } diff --git a/src/state.rs b/src/state.rs index 022c61f..69db1bc 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,3 +1,4 @@ +use super::alphabets::Alphabet; use regex::Regex; use std::collections::HashMap; use std::fmt; @@ -52,12 +53,12 @@ impl<'a> PartialEq for Match<'a> { pub struct State<'a> { pub lines: &'a Vec<&'a str>, - alphabet: &'a str, - regexp: &'a Vec<&'a str>, + alphabet: &'a Alphabet, + regexp: &'a Vec, } impl<'a> State<'a> { - pub fn new(lines: &'a Vec<&'a str>, alphabet: &'a str, regexp: &'a Vec<&'a str>) -> State<'a> { + pub fn new(lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, regexp: &'a Vec) -> State<'a> { State { lines, alphabet, @@ -133,8 +134,8 @@ impl<'a> State<'a> { } } - let alphabet = super::alphabets::get_alphabet(self.alphabet); - let mut hints = alphabet.hints(matches.len()); + // let alphabet = super::alphabets::get_alphabet(self.alphabet); + let mut hints = self.alphabet.hints(matches.len()); // This looks wrong but we do a pop after if !reverse { @@ -174,6 +175,7 @@ impl<'a> State<'a> { #[cfg(test)] mod tests { use super::*; + use crate::alphabets::Alphabet; fn split(output: &str) -> Vec<&str> { output.split("\n").collect::>() @@ -183,7 +185,8 @@ mod tests { fn match_reverse() { let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 3); assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a"); @@ -194,7 +197,8 @@ mod tests { fn match_unique() { let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, true); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, true); assert_eq!(results.len(), 3); assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a"); @@ -205,7 +209,8 @@ mod tests { fn match_docker() { let lines = split("latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 1); assert_eq!( @@ -218,7 +223,8 @@ mod tests { fn match_bash() { let lines = split("path: /var/log/nginx.log\npath: test/log/nginx-2.log:32folder/.nginx@4df2.log"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text, "/var/log/nginx.log"); @@ -230,7 +236,8 @@ mod tests { fn match_paths() { let lines = split("Lorem /tmp/foo/bar_lol, lorem\n Lorem /var/log/boot-strap.log lorem ../log/kern.log lorem"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text.clone(), "/tmp/foo/bar_lol"); @@ -242,7 +249,8 @@ mod tests { fn match_home() { let lines = split("Lorem ~/.gnu/.config.txt, lorem"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 1); assert_eq!(results.get(0).unwrap().text.clone(), "~/.gnu/.config.txt"); @@ -252,7 +260,8 @@ mod tests { fn match_uids() { let lines = split("Lorem ipsum 123e4567-e89b-12d3-a456-426655440000 lorem\n Lorem lorem lorem"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 1); } @@ -261,7 +270,8 @@ mod tests { fn match_shas() { let lines = split("Lorem fd70b5695 5246ddf f924213 lorem\n Lorem 973113963b491874ab2e372ee60d4b4cb75f717c lorem"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 4); assert_eq!(results.get(0).unwrap().text.clone(), "fd70b5695"); @@ -277,7 +287,8 @@ mod tests { fn match_ips() { let lines = split("Lorem ipsum 127.0.0.1 lorem\n Lorem 255.255.10.255 lorem 127.0.0.1 lorem"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text.clone(), "127.0.0.1"); @@ -289,7 +300,8 @@ mod tests { fn match_ipv6s() { let lines = split("Lorem ipsum fe80::2:202:fe4 lorem\n Lorem 2001:67c:670:202:7ba8:5e41:1591:d723 lorem fe80::2:1 lorem ipsum fe80:22:312:fe::1%eth0"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 4); assert_eq!(results.get(0).unwrap().text.clone(), "fe80::2:202:fe4"); @@ -305,7 +317,8 @@ mod tests { fn match_markdown_urls() { let lines = split("Lorem ipsum [link](https://github.io?foo=bar) ![](http://cdn.com/img.jpg) lorem"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 2); assert_eq!(results.get(0).unwrap().pattern.clone(), "markdown_url"); @@ -318,7 +331,8 @@ mod tests { fn match_urls() { let lines = split("Lorem ipsum https://www.rust-lang.org/tools lorem\n Lorem ipsumhttps://crates.io lorem https://github.io?foo=bar lorem ssh://github.io"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 4); assert_eq!(results.get(0).unwrap().text.clone(), "https://www.rust-lang.org/tools"); @@ -335,7 +349,8 @@ mod tests { fn match_addresses() { let lines = split("Lorem 0xfd70b5695 0x5246ddf lorem\n Lorem 0x973113tlorem"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text.clone(), "0xfd70b5695"); @@ -347,7 +362,8 @@ mod tests { fn match_hex_colors() { let lines = split("Lorem #fd7b56 lorem #FF00FF\n Lorem #00fF05 lorem #abcd00 lorem #afRR00"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 4); assert_eq!(results.get(0).unwrap().text.clone(), "#fd7b56"); @@ -360,7 +376,8 @@ mod tests { fn match_ipfs() { let lines = split("Lorem QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ lorem Qmfoobar"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 1); assert_eq!( @@ -374,7 +391,8 @@ mod tests { let lines = split("Lorem 5695 52463 lorem\n Lorem 973113 lorem 99999 lorem 8888 lorem\n 23456 lorem 5432 lorem 23444"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 8); } @@ -383,7 +401,8 @@ mod tests { fn match_diff_a() { let lines = split("Lorem lorem\n--- a/src/main.rs"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 1); assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs"); @@ -393,7 +412,8 @@ mod tests { fn match_diff_b() { let lines = split("Lorem lorem\n+++ b/src/main.rs"); let custom = [].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 1); assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs"); @@ -402,8 +422,13 @@ mod tests { #[test] fn priority() { let lines = split("Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"); - let custom = ["CUSTOM-[0-9]{4,}", "ISSUE-[0-9]{3}"].to_vec(); - let results = State::new(&lines, "abcd", &custom).matches(false, false); + + let custom: Vec = ["CUSTOM-[0-9]{4,}", "ISSUE-[0-9]{3}"] + .iter() + .map(|&s| s.to_string()) + .collect(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); assert_eq!(results.len(), 9); assert_eq!(results.get(0).unwrap().text.clone(), "http://foo.bar"); diff --git a/src/swapper.rs b/src/swapper.rs index f5994fa..a1f221e 100644 --- a/src/swapper.rs +++ b/src/swapper.rs @@ -1,384 +1,417 @@ -extern crate clap; - -use self::clap::{App, Arg}; -use clap::crate_version; +use clap::Clap; use regex::Regex; +use std::path; use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; trait Executor { - fn execute(&mut self, args: Vec) -> String; - fn last_executed(&self) -> Option>; + fn execute(&mut self, args: Vec) -> String; + fn last_executed(&self) -> Option>; } struct RealShell { - executed: Option>, + executed: Option>, } impl RealShell { - fn new() -> RealShell { - RealShell { executed: None } - } + fn new() -> RealShell { + RealShell { executed: None } + } } impl Executor for RealShell { - fn execute(&mut self, args: Vec) -> String { - let execution = Command::new(args[0].as_str()) - .args(&args[1..]) - .output() - .expect("Couldn't run it"); + fn execute(&mut self, args: Vec) -> String { + let execution = Command::new(args[0].as_str()) + .args(&args[1..]) + .output() + .expect("Execution failed"); - self.executed = Some(args); + self.executed = Some(args); - let output: String = String::from_utf8_lossy(&execution.stdout).into(); + let output: String = String::from_utf8_lossy(&execution.stdout).into(); - output.trim_end().to_string() - } + output.trim_end().to_string() + } - fn last_executed(&self) -> Option> { - self.executed.clone() - } + fn last_executed(&self) -> Option> { + self.executed.clone() + } } const TMP_FILE: &str = "/tmp/thumbs-last"; pub struct Swapper<'a> { - executor: Box<&'a mut dyn Executor>, - dir: String, - command: String, - upcase_command: String, - active_pane_id: Option, - active_pane_height: Option, - active_pane_scroll_position: Option, - active_pane_in_copy_mode: Option, - thumbs_pane_id: Option, - content: Option, - signal: String, + executor: Box<&'a mut dyn Executor>, + directory: &'a path::Path, + command: &'a str, + alt_command: &'a str, + active_pane_id: Option, + active_pane_height: Option, + active_pane_scroll_position: Option, + active_pane_in_copy_mode: Option, + thumbs_pane_id: Option, + content: Option, + signal: String, } impl<'a> Swapper<'a> { - fn new(executor: Box<&'a mut dyn Executor>, dir: String, command: String, upcase_command: String) -> Swapper { - let since_the_epoch = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards"); - let signal = format!("thumbs-finished-{}", since_the_epoch.as_secs()); + fn new( + executor: Box<&'a mut dyn Executor>, + directory: &'a path::Path, + command: &'a str, + alt_command: &'a str, + ) -> Swapper<'a> { + let since_the_epoch = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards"); + let signal = format!("thumbs-finished-{}", since_the_epoch.as_secs()); - Swapper { - executor, - dir, - command, - upcase_command, - active_pane_id: None, - active_pane_height: None, - active_pane_scroll_position: None, - active_pane_in_copy_mode: None, - thumbs_pane_id: None, - content: None, - signal, + Swapper { + executor, + directory, + command, + alt_command, + active_pane_id: None, + active_pane_height: None, + active_pane_scroll_position: None, + active_pane_in_copy_mode: None, + thumbs_pane_id: None, + content: None, + signal, + } } - } - pub fn capture_active_pane(&mut self) { - let active_command = vec![ + pub fn capture_active_pane(&mut self) { + let active_command = vec![ "tmux", "list-panes", "-F", "#{pane_id}:#{?pane_in_mode,1,0}:#{pane_height}:#{scroll_position}:#{?pane_active,active,nope}", ]; - let output = self - .executor - .execute(active_command.iter().map(|arg| arg.to_string()).collect()); + let output = self + .executor + .execute(active_command.iter().map(|arg| arg.to_string()).collect()); - let lines: Vec<&str> = output.split('\n').collect(); - let chunks: Vec> = lines.into_iter().map(|line| line.split(':').collect()).collect(); + let lines: Vec<&str> = output.split('\n').collect(); + let chunks: Vec> = lines + .into_iter() + .map(|line| line.split(':').collect()) + .collect(); - let active_pane = chunks - .iter() - .find(|&chunks| *chunks.get(4).unwrap() == "active") - .expect("Unable to find active pane"); + let active_pane = chunks + .iter() + .find(|&chunks| *chunks.get(4).unwrap() == "active") + .expect("Unable to find active pane"); - let pane_id = active_pane.get(0).unwrap(); - let pane_in_copy_mode = active_pane.get(1).unwrap().to_string(); + let pane_id = active_pane.get(0).unwrap(); + let pane_in_copy_mode = active_pane.get(1).unwrap().to_string(); - self.active_pane_id = Some(pane_id.to_string()); - self.active_pane_in_copy_mode = Some(pane_in_copy_mode); + self.active_pane_id = Some(pane_id.to_string()); + self.active_pane_in_copy_mode = Some(pane_in_copy_mode); - if self.active_pane_in_copy_mode.clone().unwrap() == "1" { - let pane_height = active_pane - .get(2) - .unwrap() - .parse() - .expect("Unable to retrieve pane height"); - let pane_scroll_position = active_pane - .get(3) - .unwrap() - .parse() - .expect("Unable to retrieve pane scroll"); + if self.active_pane_in_copy_mode.clone().unwrap() == "1" { + let pane_height = active_pane + .get(2) + .unwrap() + .parse() + .expect("Unable to retrieve pane height"); + let pane_scroll_position = active_pane + .get(3) + .unwrap() + .parse() + .expect("Unable to retrieve pane scroll"); - self.active_pane_height = Some(pane_height); - self.active_pane_scroll_position = Some(pane_scroll_position); - } - } - - pub fn execute_thumbs(&mut self) { - let options_command = vec!["tmux", "show", "-g"]; - let params: Vec = options_command.iter().map(|arg| arg.to_string()).collect(); - let options = self.executor.execute(params); - let lines: Vec<&str> = options.split('\n').collect(); - - let pattern = Regex::new(r#"@thumbs-([\w\-0-9]+) "?(\w+)"?"#).unwrap(); - - let args = lines - .iter() - .flat_map(|line| { - if let Some(captures) = pattern.captures(line) { - let name = captures.get(1).unwrap().as_str(); - let value = captures.get(2).unwrap().as_str(); - - let boolean_params = vec!["reverse", "unique", "contrast"]; - - if boolean_params.iter().any(|&x| x == name) { - return vec![format!("--{}", name)]; - } - - let string_params = vec![ - "position", - "fg-color", - "bg-color", - "hint-bg-color", - "hint-fg-color", - "select-fg-color", - "select-bg-color", - ]; - - if string_params.iter().any(|&x| x == name) { - return vec![format!("--{}", name), format!("'{}'", value)]; - } - - if name.starts_with("regexp") { - return vec!["--regexp".to_string(), format!("'{}'", value)]; - } - - vec![] - } else { - vec![] + self.active_pane_height = Some(pane_height); + self.active_pane_scroll_position = Some(pane_scroll_position); } - }) - .collect::>(); + } - let active_pane_id = self.active_pane_id.as_mut().unwrap().clone(); + pub fn execute_thumbs(&mut self) { + let options_command = vec!["tmux", "show", "-g"]; + let params: Vec = options_command.iter().map(|arg| arg.to_string()).collect(); + let options = self.executor.execute(params); + let lines: Vec<&str> = options.split('\n').collect(); - let scroll_params = if self.active_pane_in_copy_mode.is_some() { - if let (Some(pane_height), Some(scroll_position)) = - (self.active_pane_scroll_position, self.active_pane_scroll_position) - { - format!(" -S {} -E {}", -scroll_position, pane_height - scroll_position - 1) - } else { - "".to_string() - } - } else { - "".to_string() - }; + let pattern = Regex::new(r#"@thumbs-([\w\-0-9]+) "?(\w+)"?"#).unwrap(); - // NOTE: For debugging add echo $PWD && sleep 5 after tee - let pane_command = format!( + let args = lines + .iter() + .flat_map(|line| { + if let Some(captures) = pattern.captures(line) { + let name = captures.get(1).unwrap().as_str(); + let value = captures.get(2).unwrap().as_str(); + + let boolean_params = vec!["reverse", "unique", "contrast"]; + + if boolean_params.iter().any(|&x| x == name) { + return vec![format!("--{}", name)]; + } + + let string_params = vec![ + "position", + "fg-color", + "bg-color", + "hint-bg-color", + "hint-fg-color", + "select-fg-color", + "select-bg-color", + ]; + + if string_params.iter().any(|&x| x == name) { + return vec![format!("--{}", name), format!("'{}'", value)]; + } + + if name.starts_with("regexp") { + return vec!["--regexp".to_string(), format!("'{}'", value)]; + } + + vec![] + } else { + vec![] + } + }) + .collect::>(); + + let active_pane_id = self.active_pane_id.as_mut().unwrap().clone(); + + let scroll_params = if self.active_pane_in_copy_mode.is_some() { + if let (Some(pane_height), Some(scroll_position)) = ( + self.active_pane_scroll_position, + self.active_pane_scroll_position, + ) { + format!( + " -S {} -E {}", + -scroll_position, + pane_height - scroll_position - 1 + ) + } else { + "".to_string() + } + } else { + "".to_string() + }; + + // NOTE: For debugging add echo $PWD && sleep 5 after tee + let pane_command = format!( "tmux capture-pane -t {} -p{} | {}/target/release/thumbs -f '%U:%H' -t {} {}; tmux swap-pane -t {}; tmux wait-for -S {}", active_pane_id, scroll_params, - self.dir, + self.directory.to_str().unwrap(), TMP_FILE, args.join(" "), active_pane_id, self.signal ); - let thumbs_command = vec![ - "tmux", - "new-window", - "-P", - "-d", - "-n", - "[thumbs]", - pane_command.as_str(), - ]; + let thumbs_command = vec![ + "tmux", + "new-window", + "-P", + "-d", + "-n", + "[thumbs]", + pane_command.as_str(), + ]; - let params: Vec = thumbs_command.iter().map(|arg| arg.to_string()).collect(); + let params: Vec = thumbs_command.iter().map(|arg| arg.to_string()).collect(); - self.thumbs_pane_id = Some(self.executor.execute(params)); - } + self.thumbs_pane_id = Some(self.executor.execute(params)); + } - pub fn swap_panes(&mut self) { - let active_pane_id = self.active_pane_id.as_mut().unwrap().clone(); - let thumbs_pane_id = self.thumbs_pane_id.as_mut().unwrap().clone(); + pub fn swap_panes(&mut self) { + let active_pane_id = self.active_pane_id.as_mut().unwrap().clone(); + let thumbs_pane_id = self.thumbs_pane_id.as_mut().unwrap().clone(); - let swap_command = vec![ - "tmux", - "swap-pane", - "-d", - "-s", - active_pane_id.as_str(), - "-t", - thumbs_pane_id.as_str(), - ]; - let params = swap_command.iter().map(|arg| arg.to_string()).collect(); + let swap_command = vec![ + "tmux", + "swap-pane", + "-d", + "-s", + active_pane_id.as_str(), + "-t", + thumbs_pane_id.as_str(), + ]; + let params = swap_command.iter().map(|arg| arg.to_string()).collect(); - self.executor.execute(params); - } + self.executor.execute(params); + } - pub fn wait_thumbs(&mut self) { - let wait_command = vec!["tmux", "wait-for", self.signal.as_str()]; - let params = wait_command.iter().map(|arg| arg.to_string()).collect(); + pub fn wait_thumbs(&mut self) { + let wait_command = vec!["tmux", "wait-for", self.signal.as_str()]; + let params = wait_command.iter().map(|arg| arg.to_string()).collect(); - self.executor.execute(params); - } + self.executor.execute(params); + } - pub fn retrieve_content(&mut self) { - let retrieve_command = vec!["cat", TMP_FILE]; - let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); + pub fn retrieve_content(&mut self) { + let retrieve_command = vec!["cat", TMP_FILE]; + let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); - self.content = Some(self.executor.execute(params)); - } + self.content = Some(self.executor.execute(params)); + } - pub fn destroy_content(&mut self) { - let retrieve_command = vec!["rm", TMP_FILE]; - let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); - - self.executor.execute(params); - } - - pub fn execute_command(&mut self) { - let content = self.content.clone().unwrap(); - let mut splitter = content.splitn(2, ':'); - - if let Some(upcase) = splitter.next() { - if let Some(text) = splitter.next() { - let execute_command = if upcase.trim_end() == "true" { - self.upcase_command.clone() - } else { - self.command.clone() - }; - - let final_command = str::replace(execute_command.as_str(), "{}", text.trim_end()); - let retrieve_command = vec!["bash", "-c", final_command.as_str()]; + pub fn destroy_content(&mut self) { + let retrieve_command = vec!["rm", TMP_FILE]; let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); self.executor.execute(params); - } } - } + + pub fn execute_command(&mut self) { + let content = self.content.clone().unwrap(); + let mut splitter = content.splitn(2, ':'); + + if let Some(upcase) = splitter.next() { + if let Some(text) = splitter.next() { + let execute_command = if upcase.trim_end() == "true" { + self.alt_command.clone() + } else { + self.command.clone() + }; + + let final_command = str::replace(execute_command, "{}", text.trim_end()); + let retrieve_command = vec!["bash", "-c", final_command.as_str()]; + let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); + + self.executor.execute(params); + } + } + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - struct TestShell { - outputs: Vec, - executed: Option>, - } - - impl TestShell { - fn new(outputs: Vec) -> TestShell { - TestShell { - executed: None, - outputs, - } - } - } - - impl Executor for TestShell { - fn execute(&mut self, args: Vec) -> String { - self.executed = Some(args); - self.outputs.pop().unwrap() + struct TestShell { + outputs: Vec, + executed: Option>, } - fn last_executed(&self) -> Option> { - self.executed.clone() + impl TestShell { + fn new(outputs: Vec) -> TestShell { + TestShell { + executed: None, + outputs, + } + } } - } - #[test] - fn retrieve_active_pane() { - let last_command_outputs = vec!["%97:100:24:1:active\n%106:100:24:1:nope\n%107:100:24:1:nope\n".to_string()]; - let mut executor = TestShell::new(last_command_outputs); - let mut swapper = Swapper::new(Box::new(&mut executor), "".to_string(), "".to_string(), "".to_string()); + impl Executor for TestShell { + fn execute(&mut self, args: Vec) -> String { + self.executed = Some(args); + self.outputs.pop().unwrap() + } - swapper.capture_active_pane(); + fn last_executed(&self) -> Option> { + self.executed.clone() + } + } - assert_eq!(swapper.active_pane_id.unwrap(), "%97"); - } + #[test] + fn retrieve_active_pane() { + let last_command_outputs = + vec!["%97:100:24:1:active\n%106:100:24:1:nope\n%107:100:24:1:nope\n".to_string()]; + let mut executor = TestShell::new(last_command_outputs); + let mut swapper = Swapper::new(Box::new(&mut executor), &path::Path::new(""), "", ""); - #[test] - fn swap_panes() { - let last_command_outputs = vec![ - "".to_string(), - "%100".to_string(), - "".to_string(), - "%106:100:24:1:nope\n%98:100:24:1:active\n%107:100:24:1:nope\n".to_string(), - ]; - let mut executor = TestShell::new(last_command_outputs); - let mut swapper = Swapper::new(Box::new(&mut executor), "".to_string(), "".to_string(), "".to_string()); + swapper.capture_active_pane(); + + assert_eq!(swapper.active_pane_id.unwrap(), "%97"); + } + + #[test] + fn swap_panes() { + let last_command_outputs = vec![ + "".to_string(), + "%100".to_string(), + "".to_string(), + "%106:100:24:1:nope\n%98:100:24:1:active\n%107:100:24:1:nope\n".to_string(), + ]; + let mut executor = TestShell::new(last_command_outputs); + let mut swapper = Swapper::new(Box::new(&mut executor), &path::Path::new(""), "", ""); + + swapper.capture_active_pane(); + swapper.execute_thumbs(); + swapper.swap_panes(); + + let expectation = vec!["tmux", "swap-pane", "-d", "-s", "%98", "-t", "%100"]; + + assert_eq!(executor.last_executed().unwrap(), expectation); + } +} + +/// Main configuration, parsed from command line. +#[derive(Clap, Debug)] +#[clap(author, about, version)] +struct Opt { + /// Directory where to execute copyrat. + #[clap(long, required = true)] + directory: path::PathBuf, + + /// Command to execute on selection. + #[clap(short, long, default_value = "'tmux set-buffer {}'")] + command: String, + + /// Command to execute on uppercased selection. + #[clap( + short, + long, + default_value = "'tmux set-bufffer {} && tmux-paste-buffer'" + )] + alt_command: String, +} + +// fn app_args<'a>() -> clap::ArgMatches<'a> { +// App::new("tmux-thumbs") +// .version(crate_version!()) +// .about("A lightning fast version of tmux-fingers, copy/pasting tmux like vimium/vimperator") +// .arg( +// Arg::with_name("dir") +// .help("Directory where to execute thumbs") +// .long("dir") +// .default_value(""), +// ) +// .arg( +// Arg::with_name("command") +// .help("Pick command") +// .long("command") +// .default_value("tmux set-buffer {}"), +// ) +// .arg( +// Arg::with_name("upcase_command") +// .help("Upcase command") +// .long("upcase-command") +// .default_value("tmux set-buffer {} && tmux paste-buffer"), +// ) +// .get_matches() +// } + +fn main() -> std::io::Result<()> { + let opt = Opt::parse(); + // let dir = args.value_of("dir").unwrap(); + // let command = args.value_of("command").unwrap(); + // let upcase_command = args.value_of("upcase_command").unwrap(); + + // if dir.is_empty() { + // panic!("Invalid tmux-thumbs execution. Are you trying to execute tmux-thumbs directly?") + // } + + let mut executor = RealShell::new(); + let mut swapper = Swapper::new( + Box::new(&mut executor), + opt.directory.as_path(), + &opt.command, + &opt.alt_command, + ); swapper.capture_active_pane(); swapper.execute_thumbs(); swapper.swap_panes(); - - let expectation = vec!["tmux", "swap-pane", "-d", "-s", "%98", "-t", "%100"]; - - assert_eq!(executor.last_executed().unwrap(), expectation); - } -} - -fn app_args<'a>() -> clap::ArgMatches<'a> { - App::new("tmux-thumbs") - .version(crate_version!()) - .about("A lightning fast version of tmux-fingers, copy/pasting tmux like vimium/vimperator") - .arg( - Arg::with_name("dir") - .help("Directory where to execute thumbs") - .long("dir") - .default_value(""), - ) - .arg( - Arg::with_name("command") - .help("Pick command") - .long("command") - .default_value("tmux set-buffer {}"), - ) - .arg( - Arg::with_name("upcase_command") - .help("Upcase command") - .long("upcase-command") - .default_value("tmux set-buffer {} && tmux paste-buffer"), - ) - .get_matches() -} - -fn main() -> std::io::Result<()> { - let args = app_args(); - let dir = args.value_of("dir").unwrap(); - let command = args.value_of("command").unwrap(); - let upcase_command = args.value_of("upcase_command").unwrap(); - - if dir.is_empty() { - panic!("Invalid tmux-thumbs execution. Are you trying to execute tmux-thumbs directly?") - } - - let mut executor = RealShell::new(); - let mut swapper = Swapper::new( - Box::new(&mut executor), - dir.to_string(), - command.to_string(), - upcase_command.to_string(), - ); - - swapper.capture_active_pane(); - swapper.execute_thumbs(); - swapper.swap_panes(); - swapper.wait_thumbs(); - swapper.retrieve_content(); - swapper.destroy_content(); - swapper.execute_command(); - Ok(()) + swapper.wait_thumbs(); + swapper.retrieve_content(); + swapper.destroy_content(); + swapper.execute_command(); + Ok(()) } diff --git a/src/view.rs b/src/view.rs index 6d18c38..72e7c83 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,4 +1,5 @@ use super::{colors, state}; +use clap::Clap; use std::char; use std::io::{stdout, Read, Write}; use termion::async_stdin; @@ -14,7 +15,7 @@ pub struct View<'a> { focus_index: usize, multi: bool, hint_alignment: HintAlignment, - rendering_colors: &'a ViewColors<'a>, + rendering_colors: &'a ViewColors, hint_style: Option, } @@ -23,17 +24,42 @@ pub struct View<'a> { /// - `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. -pub struct ViewColors<'a> { - pub focus_fg: Box<&'a dyn color::Color>, - pub focus_bg: Box<&'a dyn color::Color>, - pub match_fg: Box<&'a dyn color::Color>, - pub match_bg: Box<&'a dyn color::Color>, - pub hint_fg: Box<&'a dyn color::Color>, - pub hint_bg: Box<&'a dyn color::Color>, +#[derive(Clap, Debug)] +pub struct ViewColors { + /// Foreground color for matches. + #[clap(long, default_value = "green", + parse(try_from_str = colors::parse_color))] + match_fg: Box, + + /// Background color for matches. + #[clap(long, default_value = "black", + parse(try_from_str = colors::parse_color))] + match_bg: Box, + + /// Foreground color for the focused match. + #[clap(long, default_value = "blue", + parse(try_from_str = colors::parse_color))] + focused_fg: Box, + + /// Background color for the focused match. + #[clap(long, default_value = "black", + parse(try_from_str = colors::parse_color))] + focused_bg: Box, + + /// Foreground color for hints. + #[clap(long, default_value = "white", + parse(try_from_str = colors::parse_color))] + hint_fg: Box, + + /// Background color for hints. + #[clap(long, default_value = "black", + parse(try_from_str = colors::parse_color))] + hint_bg: Box, } /// 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, @@ -135,7 +161,7 @@ impl<'a> View<'a> { ) { // To help identify it, the match thas has focus is rendered with a dedicated color. let (text_fg_color, text_bg_color) = if focused { - (&colors.focus_fg, &colors.focus_bg) + (&colors.focused_fg, &colors.focused_bg) } else { (&colors.match_fg, &colors.match_bg) }; @@ -145,8 +171,8 @@ impl<'a> View<'a> { stdout, "{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}", goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), - fg_color = color::Fg(**text_fg_color), - bg_color = color::Bg(**text_bg_color), + fg_color = color::Fg(text_fg_color.as_ref()), + bg_color = color::Bg(text_bg_color.as_ref()), fg_reset = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset), text = &text, @@ -171,8 +197,8 @@ impl<'a> View<'a> { colors: &ViewColors, hint_style: &Option, ) { - let fg_color = color::Fg(*colors.hint_fg); - let bg_color = color::Bg(*colors.hint_bg); + 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); @@ -426,6 +452,7 @@ impl<'a> View<'a> { #[cfg(test)] mod tests { use super::*; + use crate::alphabets; #[test] fn test_render_all_lines() { @@ -461,12 +488,12 @@ path: /usr/local/bin/cargo"; let focused = true; let offset: (usize, usize) = (3, 1); let colors = ViewColors { - focus_fg: Box::new(&(color::Red)), - focus_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), + 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), }; View::render_matched_text(&mut writer, text, focused, offset, &colors); @@ -476,8 +503,8 @@ path: /usr/local/bin/cargo"; format!( "{goto}{bg}{fg}{text}{fg_reset}{bg_reset}", goto = cursor::Goto(4, 2), - fg = color::Fg(*colors.focus_fg), - bg = color::Bg(*colors.focus_bg), + 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, @@ -493,12 +520,12 @@ path: /usr/local/bin/cargo"; let focused = false; let offset: (usize, usize) = (3, 1); let colors = ViewColors { - focus_fg: Box::new(&(color::Red)), - focus_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), + 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), }; View::render_matched_text(&mut writer, text, focused, offset, &colors); @@ -508,8 +535,8 @@ path: /usr/local/bin/cargo"; format!( "{goto}{bg}{fg}{text}{fg_reset}{bg_reset}", goto = cursor::Goto(4, 2), - fg = color::Fg(*colors.match_fg), - bg = color::Bg(*colors.match_bg), + 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, @@ -524,12 +551,12 @@ path: /usr/local/bin/cargo"; let hint_text = "eo"; let offset: (usize, usize) = (3, 1); let colors = ViewColors { - focus_fg: Box::new(&(color::Red)), - focus_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), + 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; @@ -548,8 +575,8 @@ path: /usr/local/bin/cargo"; format!( "{goto}{bg}{fg}{text}{fg_reset}{bg_reset}", goto = cursor::Goto(4, 2), - fg = color::Fg(*colors.hint_fg), - bg = color::Bg(*colors.hint_bg), + 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", @@ -564,12 +591,12 @@ path: /usr/local/bin/cargo"; let hint_text = "eo"; let offset: (usize, usize) = (3, 1); let colors = ViewColors { - focus_fg: Box::new(&(color::Red)), - focus_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), + 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; @@ -588,8 +615,8 @@ path: /usr/local/bin/cargo"; format!( "{goto}{bg}{fg}{sty}{text}{sty_reset}{fg_reset}{bg_reset}", goto = cursor::Goto(4, 2), - fg = color::Fg(*colors.hint_fg), - bg = color::Bg(*colors.hint_bg), + 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, @@ -606,12 +633,12 @@ path: /usr/local/bin/cargo"; let hint_text = "eo"; let offset: (usize, usize) = (3, 1); let colors = ViewColors { - focus_fg: Box::new(&(color::Red)), - focus_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), + 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; @@ -630,8 +657,8 @@ path: /usr/local/bin/cargo"; format!( "{goto}{bg}{fg}{bra}{text}{bra_close}{fg_reset}{bg_reset}", goto = cursor::Goto(4, 2), - fg = color::Fg(*colors.hint_fg), - bg = color::Bg(*colors.hint_bg), + 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 = '{', @@ -652,15 +679,15 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let lines = content.split('\n').collect(); let custom_regexes = [].to_vec(); - let alphabet = "abcd"; - let mut state = state::State::new(&lines, alphabet, &custom_regexes); + let alphabet = alphabets::Alphabet("abcd".to_string()); + let mut state = state::State::new(&lines, &alphabet, &custom_regexes); let rendering_colors = ViewColors { - focus_fg: Box::new(&(color::Red)), - focus_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), + 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; @@ -709,19 +736,19 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let lines = content.split('\n').collect(); let custom_regexes = [].to_vec(); - let alphabet = "abcd"; - let mut state = state::State::new(&lines, alphabet, &custom_regexes); + let alphabet = alphabets::Alphabet("abcd".to_string()); + let mut state = state::State::new(&lines, &alphabet, &custom_regexes); let multi = false; let reversed = true; let unique = false; let rendering_colors = ViewColors { - focus_fg: Box::new(&(color::Red)), - focus_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), + 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; @@ -758,8 +785,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; 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), - match_bg = color::Bg(*rendering_colors.match_bg), + 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) ) @@ -771,8 +798,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; format!( "{goto7_1}{hint_bg}{hint_fg}b{fg_reset}{bg_reset}", goto7_1 = goto7_1, - hint_fg = color::Fg(*rendering_colors.hint_fg), - hint_bg = color::Bg(*rendering_colors.hint_bg), + 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) ) @@ -783,8 +810,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; format!( "{goto11_3}{focus_bg}{focus_fg}https://en.wikipedia.org/wiki/Barcelona{fg_reset}{bg_reset}", goto11_3 = goto11_3, - focus_fg = color::Fg(*rendering_colors.focus_fg), - focus_bg = color::Bg(*rendering_colors.focus_bg), + focus_fg = color::Fg(rendering_colors.focused_fg.as_ref()), + focus_bg = color::Bg(rendering_colors.focused_bg.as_ref()), fg_reset = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset) ) @@ -796,8 +823,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; format!( "{goto11_3}{hint_bg}{hint_fg}a{fg_reset}{bg_reset}", goto11_3 = goto11_3, - hint_fg = color::Fg(*rendering_colors.hint_fg), - hint_bg = color::Bg(*rendering_colors.hint_bg), + 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) ) diff --git a/tmux-thumbs.sh b/tmux-thumbs.sh index ec6b736..fce71f1 100755 --- a/tmux-thumbs.sh +++ b/tmux-thumbs.sh @@ -5,7 +5,7 @@ PARAMS=() function add-option-param { - VALUE=$(tmux show -vg @thumbs-$1 2> /dev/null) + VALUE=$(tmux show-options -vg @thumbs-$1 2> /dev/null) if [[ ${VALUE} ]]; then PARAMS+=("--$1=${VALUE}") From 905bd2862cde73629ce32128141fede79d675966 Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 25 May 2020 23:06:00 +0200 Subject: [PATCH 05/40] refactor: refactor --- .rustfmt.toml | 19 +- src/error.rs | 46 +- src/lib.rs | 156 ++++++ src/main.rs | 146 +----- src/swapper.rs | 79 ++- src/tmux.rs | 272 ++++++++++ src/view.rs | 1366 ++++++++++++++++++++++++------------------------ 7 files changed, 1200 insertions(+), 884 deletions(-) create mode 100644 src/lib.rs create mode 100644 src/tmux.rs diff --git a/.rustfmt.toml b/.rustfmt.toml index 39587b2..4fbc017 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,13 +1,14 @@ -format_code_in_doc_comments = true -match_block_trailing_comma = true -condense_wildcard_suffixes = true use_field_init_shorthand = true -overflow_delimited_expr = true # width_heuristics = "Max" -normalize_comments = true -reorder_impl_items = true use_try_shorthand = true newline_style = "Unix" -format_strings = true -wrap_comments = true -# comment_width = 100 + +# - nightly features +# wrap_comments = true +# format_code_in_doc_comments = true +# normalize_comments = true +# reorder_impl_items = true +# overflow_delimited_expr = true +# match_block_trailing_comma = true +# condense_wildcard_suffixes = true +# format_strings = true diff --git a/src/error.rs b/src/error.rs index 8ec3818..d38f483 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,17 +2,45 @@ use std::fmt; #[derive(Debug)] pub enum ParseError { - ExpectedSurroundingPair, - UnknownAlphabet, - UnknownColor, + ExpectedSurroundingPair, + UnknownAlphabet, + UnknownColor, + ExpectedPaneIdMarker, + ExpectedInt(std::num::ParseIntError), + ExpectedBool(std::str::ParseBoolError), + ProcessFailure(String), } impl fmt::Display for ParseError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ParseError::ExpectedSurroundingPair => write!(f, "Expected 2 chars"), - ParseError::UnknownAlphabet => write!(f, "Expected a known alphabet"), - ParseError::UnknownColor => write!(f, "Expected ANSI color name (magenta, cyan, black, ...)"), + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ParseError::ExpectedSurroundingPair => write!(f, "Expected 2 chars"), + ParseError::UnknownAlphabet => write!(f, "Expected a known alphabet"), + ParseError::UnknownColor => { + write!(f, "Expected ANSI color name (magenta, cyan, black, ...)") + } + ParseError::ExpectedPaneIdMarker => write!(f, "Expected pane id marker"), + ParseError::ExpectedInt(msg) => write!(f, "Expected an int: {}", msg), + ParseError::ExpectedBool(msg) => write!(f, "Expected a bool: {}", msg), + ParseError::ProcessFailure(msg) => write!(f, "{}", msg), + } + } +} + +impl From for ParseError { + fn from(error: std::num::ParseIntError) -> Self { + ParseError::ExpectedInt(error) + } +} + +impl From for ParseError { + fn from(error: std::str::ParseBoolError) -> Self { + ParseError::ExpectedBool(error) + } +} + +impl From for ParseError { + fn from(error: std::io::Error) -> Self { + ParseError::ProcessFailure(error.to_string()) } - } } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c3f9900 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,156 @@ +use clap::Clap; +use std::fs::OpenOptions; +use std::io::prelude::*; +use std::path; + +pub mod alphabets; +pub mod colors; +pub mod error; +pub mod state; +pub mod view; + +/// Run copyrat on an input string `buffer`, configured by `Opt`. +/// +/// # Note +/// +/// Maybe the decision to move ownership is a bit bold. +pub fn run(buffer: String, opt: Opt) { + let lines: Vec<&str> = buffer.split('\n').collect(); + + let mut state = state::State::new(&lines, &opt.alphabet, &opt.custom_regex); + + let hint_style = match opt.hint_style { + None => None, + Some(style) => match style { + HintStyleCli::Underline => Some(view::HintStyle::Underline), + HintStyleCli::Surround => { + let (open, close) = opt.hint_surroundings; + Some(view::HintStyle::Surround(open, close)) + } + }, + }; + let uppercased_marker = opt.uppercased_marker; + + let selections: Vec<(String, bool)> = { + let mut viewbox = view::View::new( + &mut state, + opt.multi_selection, + opt.reverse, + opt.unique, + opt.hint_alignment, + &opt.colors, + hint_style, + ); + + viewbox.present() + }; + + // Early exit, signaling tmux we had no selections. + if selections.is_empty() { + std::process::exit(1); + } + + let output: String = if uppercased_marker { + selections + .iter() + .map(|(text, uppercased)| format!("{}:{}", *uppercased, text)) + .collect::>() + .join("\n") + } else { + selections + .iter() + .map(|(text, _)| text.as_str()) + .collect::>() + .join("\n") + }; + + match opt.target_path { + None => println!("{}", output), + Some(target) => { + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(target) + .expect("Unable to open the target file"); + + file.write(output.as_bytes()).unwrap(); + } + } +} + +/// Main configuration, parsed from command line. +#[derive(Clap, Debug)] +#[clap(author, about, version)] +pub struct Opt { + /// Alphabet to draw hints from. + /// + /// Possible values are "{A}", "{A}-homerow", "{A}-left-hand", + /// "{A}-right-hand", where "{A}" is one of "qwerty", "azerty", "qwertz", + /// "dvorak", "colemak". Examples: "qwerty", "dvorak-homerow". + #[clap(short = "k", long, default_value = "qwerty", + parse(try_from_str = alphabets::parse_alphabet))] + alphabet: alphabets::Alphabet, + + /// Enable multi-selection. + #[clap(short, long)] + multi_selection: bool, + + #[clap(flatten)] + colors: view::ViewColors, + + /// Reverse the order for assigned hints. + #[clap(short, long)] + reverse: bool, + + /// Keep the same hint for identical matches. + #[clap(short, long)] + unique: bool, + + /// Align hint with its match. + #[clap(short = "a", long, arg_enum, default_value = "Leading")] + hint_alignment: view::HintAlignment, + + /// Additional regex patterns. + #[clap(short = "c", long)] + custom_regex: Vec, + + /// Optional hint styling. + /// + /// Underline or surround the hint for increased visibility. + /// If not provided, only the hint colors will be used. + #[clap(short = "s", long, arg_enum)] + hint_style: Option, + + /// Chars surrounding each hint, used with `Surround` style. + #[clap(long, default_value = "{}", + parse(try_from_str = parse_chars))] + hint_surroundings: (char, char), + + /// Target path where to store the selected matches. + #[clap(short = "o", long = "output", parse(from_os_str))] + target_path: Option, + + /// Describes if the uppercased marker should be added to the output, + /// indicating if hint key was uppercased. This is only used by + /// tmux-copyrat, so it is skipped from clap configuration. + #[clap(skip)] + uppercased_marker: bool, +} + +/// Type introduced due to parsing limitation, +/// as we cannot directly parse into view::HintStyle. +#[derive(Debug, Clap)] +enum HintStyleCli { + Underline, + Surround, +} + +fn parse_chars(src: &str) -> Result<(char, char), error::ParseError> { + if src.len() != 2 { + return Err(error::ParseError::ExpectedSurroundingPair); + } + + let chars: Vec = src.chars().collect(); + Ok((chars[0], chars[1])) +} diff --git a/src/main.rs b/src/main.rs index 86d02dc..244a514 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,88 +1,7 @@ use clap::Clap; -use std::fs::OpenOptions; -use std::io::prelude::*; use std::io::{self, Read}; -use std::path; -mod alphabets; -mod colors; -mod error; -mod state; -mod view; - -/// Main configuration, parsed from command line. -#[derive(Clap, Debug)] -#[clap(author, about, version)] -struct Opt { - /// Alphabet to draw hints from. - /// - /// Possible values are "{A}", "{A}-homerow", "{A}-left-hand", - /// "{A}-right-hand", where "{A}" is one of "qwerty", "azerty", "qwertz", - /// "dvorak", "colemak". Examples: "qwerty", "dvorak-homerow". - #[clap(short = "k", long, default_value = "qwerty", - parse(try_from_str = alphabets::parse_alphabet))] - alphabet: alphabets::Alphabet, - - /// Enable multi-selection. - #[clap(short, long)] - multi_selection: bool, - - #[clap(flatten)] - colors: view::ViewColors, - - /// Reverse the order for assigned hints. - #[clap(short, long)] - reverse: bool, - - /// Keep the same hint for identical matches. - #[clap(short, long)] - unique: bool, - - /// Align hint with its match. - #[clap(short = "a", long, arg_enum, default_value = "Leading")] - hint_alignment: view::HintAlignment, - - /// Additional regex patterns. - #[clap(short = "c", long)] - custom_regex: Vec, - - /// Optional hint styling. - /// - /// Underline or surround the hint for increased visibility. - /// If not provided, only the hint colors will be used. - #[clap(short = "s", long, arg_enum)] - hint_style: Option, - - /// Chars surrounding each hint, used with `Surrounded` style. - #[clap(long, default_value = "{}", - parse(try_from_str = parse_chars))] - hint_surroundings: (char, char), - - /// Target path where to store the selected matches. - #[clap(short = "o", long = "output", parse(from_os_str))] - target_path: Option, - - /// Only output if key was uppercased. - #[clap(long)] - uppercased: bool, -} - -/// Type introduced due to parsing limitation, -/// as we cannot directly parse into view::HintStyle. -#[derive(Debug, Clap)] -enum HintStyleCli { - Underlined, - Surrounded, -} - -fn parse_chars(src: &str) -> Result<(char, char), error::ParseError> { - if src.len() != 2 { - return Err(error::ParseError::ExpectedSurroundingPair); - } - - let chars: Vec = src.chars().collect(); - Ok((chars[0], chars[1])) -} +use copyrat::{run, Opt}; fn main() { let opt = Opt::parse(); @@ -93,67 +12,6 @@ fn main() { let mut buffer = String::new(); handle.read_to_string(&mut buffer).unwrap(); - let lines: Vec<&str> = buffer.split('\n').collect(); - let mut state = state::State::new(&lines, &opt.alphabet, &opt.custom_regex); - - let hint_style = match opt.hint_style { - None => None, - Some(style) => match style { - HintStyleCli::Underlined => Some(view::HintStyle::Underlined), - HintStyleCli::Surrounded => { - let (open, close) = opt.hint_surroundings; - Some(view::HintStyle::Surrounded(open, close)) - } - }, - }; - let uppercase_flag = opt.uppercased; - - let selections = { - let mut viewbox = view::View::new( - &mut state, - opt.multi_selection, - opt.reverse, - opt.unique, - opt.hint_alignment, - &opt.colors, - hint_style, - ); - - viewbox.present() - }; - - // Early exit, signaling tmux we had no selections. - if selections.is_empty() { - ::std::process::exit(1); - } - - let output = selections - .iter() - .map(|(text, uppercased)| { - let upcase_value = if *uppercased { "true" } else { "false" }; - - let output = if uppercase_flag { upcase_value } else { text }; - // let mut output = &opt.format; - - // output = str::replace(&output, "%U", upcase_value); - // output = str::replace(&output, "%H", text.as_str()); - output - }) - .collect::>() - .join("\n"); - - match opt.target_path { - None => println!("{}", output), - Some(target) => { - let mut file = OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(target) - .expect("Unable to open the target file"); - - file.write(output.as_bytes()).unwrap(); - } - } + run(buffer, opt); } diff --git a/src/swapper.rs b/src/swapper.rs index a1f221e..9bf3e0d 100644 --- a/src/swapper.rs +++ b/src/swapper.rs @@ -4,6 +4,10 @@ use std::path; use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; +use copyrat::error; + +mod tmux; + trait Executor { fn execute(&mut self, args: Vec) -> String; fn last_executed(&self) -> Option>; @@ -38,7 +42,7 @@ impl Executor for RealShell { } } -const TMP_FILE: &str = "/tmp/thumbs-last"; +const TMP_FILE: &str = "/tmp/copyrat-last"; pub struct Swapper<'a> { executor: Box<&'a mut dyn Executor>, @@ -83,11 +87,11 @@ impl<'a> Swapper<'a> { pub fn capture_active_pane(&mut self) { let active_command = vec![ - "tmux", - "list-panes", - "-F", - "#{pane_id}:#{?pane_in_mode,1,0}:#{pane_height}:#{scroll_position}:#{?pane_active,active,nope}", - ]; + "tmux", + "list-panes", + "-F", + "#{pane_id}:#{?pane_in_mode,1,0}:#{pane_height}:#{scroll_position}:#{?pane_active,active,nope}", + ]; let output = self .executor @@ -194,15 +198,15 @@ impl<'a> Swapper<'a> { // NOTE: For debugging add echo $PWD && sleep 5 after tee let pane_command = format!( - "tmux capture-pane -t {} -p{} | {}/target/release/thumbs -f '%U:%H' -t {} {}; tmux swap-pane -t {}; tmux wait-for -S {}", - active_pane_id, - scroll_params, - self.directory.to_str().unwrap(), - TMP_FILE, - args.join(" "), - active_pane_id, - self.signal - ); + "tmux capture-pane -t {} -p{} | {}/target/release/thumbs -f '%U:%H' -t {} {}; tmux swap-pane -t {}; tmux wait-for -S {}", + active_pane_id, + scroll_params, + self.directory.to_str().unwrap(), + TMP_FILE, + args.join(" "), + active_pane_id, + self.signal + ); let thumbs_command = vec![ "tmux", @@ -358,37 +362,21 @@ struct Opt { #[clap( short, long, - default_value = "'tmux set-bufffer {} && tmux-paste-buffer'" + default_value = "'tmux set-buffer {} && tmux-paste-buffer'" )] alt_command: String, + + /// Retrieve options from tmux. + /// + /// If active, options formatted like `copyrat-*` are read from tmux. + /// You should prefer reading them from the config file (the default + /// option) as this saves both a command call (about 10ms) and a Regex + /// compilation. + #[clap(long)] + options_from_tmux: bool, } -// fn app_args<'a>() -> clap::ArgMatches<'a> { -// App::new("tmux-thumbs") -// .version(crate_version!()) -// .about("A lightning fast version of tmux-fingers, copy/pasting tmux like vimium/vimperator") -// .arg( -// Arg::with_name("dir") -// .help("Directory where to execute thumbs") -// .long("dir") -// .default_value(""), -// ) -// .arg( -// Arg::with_name("command") -// .help("Pick command") -// .long("command") -// .default_value("tmux set-buffer {}"), -// ) -// .arg( -// Arg::with_name("upcase_command") -// .help("Upcase command") -// .long("upcase-command") -// .default_value("tmux set-buffer {} && tmux paste-buffer"), -// ) -// .get_matches() -// } - -fn main() -> std::io::Result<()> { +fn main() -> Result<(), error::ParseError> { let opt = Opt::parse(); // let dir = args.value_of("dir").unwrap(); // let command = args.value_of("command").unwrap(); @@ -398,7 +386,14 @@ fn main() -> std::io::Result<()> { // panic!("Invalid tmux-thumbs execution. Are you trying to execute tmux-thumbs directly?") // } + let panes: Vec = tmux::list_panes()?; + let active_pane = panes + .iter() + .find(|p| p.is_active) + .expect("One tmux pane should be active"); + let mut executor = RealShell::new(); + let mut swapper = Swapper::new( Box::new(&mut executor), opt.directory.as_path(), diff --git a/src/tmux.rs b/src/tmux.rs new file mode 100644 index 0000000..1a902ce --- /dev/null +++ b/src/tmux.rs @@ -0,0 +1,272 @@ +use copyrat::error::ParseError; +use regex::Regex; +use std::collections::HashMap; +use std::process::Command; + +/// Execute an arbitrary Unix command and return the stdout as a `String` if +/// successful. +pub fn execute(command: &str, args: &Vec<&str>) -> Result { + let output = Command::new(command).args(args).output()?; + + if !output.status.success() { + let msg = String::from_utf8_lossy(&output.stderr); + return Err(ParseError::ProcessFailure(format!( + "Process failure: {} {}, error {}", + command, + args.join(" "), + msg + ))); + } + + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +#[derive(Debug, PartialEq)] +pub struct Pane { + /// Pane identifier. + pub id: u32, + /// Describes if the pane is in some mode. + pub in_mode: bool, + /// Number of lines in the pane. + pub height: u32, + /// Optional offset from the bottom if the pane is in some mode. + /// + /// When a pane is in copy mode, scrolling up changes the + /// `scroll_position`. If the pane is in normal mode, or unscrolled, + /// then `0` is returned. + pub scroll_position: u32, + /// Describes if the pane is currently active (focused). + pub is_active: bool, +} + +impl Pane { + /// Parse a string containing tmux panes status into a new `Pane`. + /// + /// This returns a `Result` as this call can obviously + /// fail if provided an invalid format. + /// + /// The expected format of the tmux status is "%52:false:62:3:false", + /// or "%53:false:23::true". + /// + /// This status line is obtained with `tmux list-panes -F '#{pane_id}:#{?pane_in_mode,true,false}:#{pane_height}:#{scroll_position}:#{?pane_active,true,false}'`. + /// + /// For definitions, look at `Pane` type, + /// and at the tmux man page for definitions. + pub fn parse(src: &str) -> Result { + let items: Vec<&str> = src.split(':').collect(); + assert_eq!(items.len(), 5, "tmux should have returned 5 items per line"); + + let mut iter = items.iter(); + + let id_str = iter.next().unwrap(); + if !id_str.starts_with('%') { + return Err(ParseError::ExpectedPaneIdMarker); + } + let id = id_str[1..].parse::()?; + + let in_mode = iter.next().unwrap().parse::()?; + + let height = iter.next().unwrap().parse::()?; + + let scroll_position = iter.next().unwrap(); + let scroll_position = if scroll_position.is_empty() { + "0" + } else { + scroll_position + }; + let scroll_position = scroll_position.parse::()?; + + let is_active = iter.next().unwrap().parse::()?; + + Ok(Pane { + id, + in_mode, + height, + scroll_position, + is_active, + }) + } +} + +/// Returns a list of `Pane` from the current tmux session. +pub fn list_panes() -> Result, ParseError> { + let args = vec![ + "list-panes", + "-F", + "#{pane_id}:#{?pane_in_mode,1,0}:#{pane_height}:#{scroll_position}:#{?pane_active,active,nope}", + ]; + + let output = execute("tmux", &args)?; + + // Each call to `Pane::parse` returns a `Result`. All results + // are collected into a Result, _>, thanks to `collect()`. + let result: Result, ParseError> = + output.split('\n').map(|line| Pane::parse(line)).collect(); + + result +} + +/// Returns tmux global options as a `HashMap`. The prefix argument is for +/// convenience, in order to target only some of our options. For instance, +/// `get_options("@copyrat-")` will return a `HashMap` which keys are tmux options names like `@copyrat-command`, and associated values. +/// +/// # Example +/// ```get_options("@copyrat-")``` +pub fn get_options(prefix: &str) -> Result, ParseError> { + let args = vec!["show", "-g"]; + + let output = execute("tmux", &args)?; + let lines: Vec<&str> = output.split('\n').collect(); + + let pattern = format!(r#"{prefix}([\w\-0-9]+) "?(\w+)"?"#, prefix = prefix); + let re = Regex::new(&pattern).unwrap(); + + let args: HashMap = lines + .iter() + .flat_map(|line| match re.captures(line) { + None => None, + Some(captures) => { + let key = captures[1].to_string(); + let value = captures[2].to_string(); + Some((key, value)) + } + }) + .collect(); + + Ok(args) +} + +// pub fn toto() { +// let options_command = vec!["tmux", "show", "-g"]; +// let params: Vec = options_command.iter().map(|arg| arg.to_string()).collect(); +// let options = self.executor.execute(params); +// let lines: Vec<&str> = options.split('\n').collect(); + +// let pattern = Regex::new(r#"@thumbs-([\w\-0-9]+) "?(\w+)"?"#).unwrap(); + +// let args = lines +// .iter() +// .flat_map(|line| { +// if let Some(captures) = pattern.captures(line) { +// let name = captures.get(1).unwrap().as_str(); +// let value = captures.get(2).unwrap().as_str(); + +// let boolean_params = vec!["reverse", "unique", "contrast"]; + +// if boolean_params.iter().any(|&x| x == name) { +// return vec![format!("--{}", name)]; +// } + +// let string_params = vec![ +// "position", +// "fg-color", +// "bg-color", +// "hint-bg-color", +// "hint-fg-color", +// "select-fg-color", +// "select-bg-color", +// ]; + +// if string_params.iter().any(|&x| x == name) { +// return vec![format!("--{}", name), format!("'{}'", value)]; +// } + +// if name.starts_with("regexp") { +// return vec!["--regexp".to_string(), format!("'{}'", value)]; +// } +// } +// };} + +#[derive(Debug)] +pub enum CaptureRegion { + /// The entire history. + /// + /// This will end up sending `-S - -E -` to `tmux capture-pane`. + EntireHistory, + /// Region from start line to end line + /// + /// This works as defined in tmux's docs (order does not matter). + Region(i32, i32), +} + +/// Returns the Pane's content as a `String`. The `CaptureRegion` if `None` +/// will result in the Pane's visible area to be capture (mimics the default +/// behavior of tmux `capture-pane`). If a `CaptureRegion` is provided, +/// depending on its value, either the entire history will be captured, or a +/// user-provided region. For the user-provided region, the order of `start` +/// and `end` does not matter. They have the same meaning as described in Tmux +/// documentation. +/// +/// # Note +/// +/// If the pane is in normal mode, capturing the visible area can be done +/// without extra arguments (default behavior of `capture-pane`), but if the +/// pane is in copy mode, we need to take into account the current scroll +/// position. To support both cases, the implementation always provides those +/// parameters to tmux. +pub fn capture_pane(pane: &Pane, region: &Option) -> Result { + let mut args = format!("capture-pane -t {id} -p", id = pane.id); + + let region_str = match region { + None => { + // Will capture the visible area. + // Providing start/end helps support both copy and normal modes. + format!( + " -S {start} -E {end}", + start = pane.scroll_position, + end = pane.height - pane.scroll_position - 1 + ) + } + Some(region) => match region { + CaptureRegion::EntireHistory => String::from(" -S - -E -"), + CaptureRegion::Region(start, end) => { + format!(" -S {start} -E {end}", start = start, end = end) + } + }, + }; + + args.push_str(®ion_str); + + let args: Vec<&str> = args.split(' ').collect(); + + let output = execute("tmux", &args)?; + Ok(output) + + // format!( + // "tmux capture-pane -t {} -p{} | {}/target/release/thumbs -f '%U:%H' -t {} {}; tmux swap-pane -t {}; tmux wait-for -S {}", + // active_pane_id, + // scroll_params, +} + +#[cfg(test)] +mod tests { + use super::Pane; + use copyrat::error; + + #[test] + fn test_parse_pass() { + let output = vec!["%52:false:62:3:false", "%53:false:23::true"]; + let panes: Result, error::ParseError> = + output.iter().map(|&line| Pane::parse(line)).collect(); + let panes = panes.expect("Could not parse tmux panes"); + + let expected = vec![ + Pane { + id: 52, + in_mode: false, + height: 62, + scroll_position: 3, + is_active: false, + }, + Pane { + id: 53, + in_mode: false, + height: 23, + scroll_position: 0, + is_active: true, + }, + ]; + + assert_eq!(panes, expected); + } +} diff --git a/src/view.rs b/src/view.rs index 72e7c83..429e3be 100644 --- a/src/view.rs +++ b/src/view.rs @@ -10,13 +10,13 @@ use termion::screen::AlternateScreen; use termion::{color, cursor, style}; pub struct View<'a> { - state: &'a mut state::State<'a>, - matches: Vec>, - focus_index: usize, - multi: bool, - hint_alignment: HintAlignment, - rendering_colors: &'a ViewColors, - hint_style: Option, + state: &'a mut state::State<'a>, + matches: Vec>, + focus_index: usize, + multi: bool, + hint_alignment: HintAlignment, + rendering_colors: &'a ViewColors, + hint_style: Option, } /// Holds color-related data, for clarity. @@ -26,43 +26,43 @@ pub struct View<'a> { /// - `hint_*` colors are used to render the hints. #[derive(Clap, Debug)] pub struct ViewColors { - /// Foreground color for matches. - #[clap(long, default_value = "green", + /// Foreground color for matches. + #[clap(long, default_value = "green", parse(try_from_str = colors::parse_color))] - match_fg: Box, + match_fg: Box, - /// Background color for matches. - #[clap(long, default_value = "black", + /// Background color for matches. + #[clap(long, default_value = "black", parse(try_from_str = colors::parse_color))] - match_bg: Box, + match_bg: Box, - /// Foreground color for the focused match. - #[clap(long, default_value = "blue", + /// Foreground color for the focused match. + #[clap(long, default_value = "blue", parse(try_from_str = colors::parse_color))] - focused_fg: Box, + focused_fg: Box, - /// Background color for the focused match. - #[clap(long, default_value = "black", + /// Background color for the focused match. + #[clap(long, default_value = "black", parse(try_from_str = colors::parse_color))] - focused_bg: Box, + focused_bg: Box, - /// Foreground color for hints. - #[clap(long, default_value = "white", + /// Foreground color for hints. + #[clap(long, default_value = "white", parse(try_from_str = colors::parse_color))] - hint_fg: Box, + hint_fg: Box, - /// Background color for hints. - #[clap(long, default_value = "black", + /// Background color for hints. + #[clap(long, default_value = "black", parse(try_from_str = colors::parse_color))] - hint_bg: Box, + hint_bg: Box, } /// 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, + Leading, + Trailing, } /// Describes the style of contrast to be used during rendering of the hint's @@ -71,407 +71,413 @@ pub enum HintAlignment { /// # 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 underlined (leveraging `termion::style::Underline`). - Underlined, - /// The hint's text will be surrounded by these chars. - Surrounded(char, char), + /// 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), } /// Returned value after the `View` has finished listening to events. enum CaptureEvent { - /// Exit with no selected matches, - Exit, - /// A vector of matched text and whether it was selected with uppercase. - Hint(Vec<(String, bool)>), + /// Exit with no selected matches, + Exit, + /// A vector of matched text and whether it was selected with uppercase. + Hint(Vec<(String, bool)>), } impl<'a> View<'a> { - pub fn new( - state: &'a mut state::State<'a>, - multi: bool, - reversed: bool, - unique: bool, - hint_alignment: HintAlignment, - rendering_colors: &'a ViewColors, - hint_style: Option, - ) -> View<'a> { - let matches = state.matches(reversed, unique); - let focus_index = if reversed { matches.len() - 1 } else { 0 }; + pub fn new( + state: &'a mut state::State<'a>, + multi: bool, + reversed: bool, + unique: bool, + hint_alignment: HintAlignment, + rendering_colors: &'a ViewColors, + hint_style: Option, + ) -> View<'a> { + let matches = state.matches(reversed, unique); + let focus_index = if reversed { matches.len() - 1 } else { 0 }; - View { - state, - matches, - focus_index, - multi, - hint_alignment, - rendering_colors, - hint_style, - } - } - - /// Move focus onto the previous hint. - pub fn prev(&mut self) { - if self.focus_index > 0 { - self.focus_index -= 1; - } - } - - /// Move focus onto the next hint. - pub fn next(&mut self) { - if self.focus_index < self.matches.len() - 1 { - self.focus_index += 1; - } - } - - /// Render entire state lines on provided writer. - /// - /// 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. - fn render_lines(stdout: &mut dyn Write, lines: &Vec<&str>) -> () { - for (index, line) in lines.iter().enumerate() { - let trimmed_line = line.trim_end(); - - if !trimmed_line.is_empty() { - write!( - stdout, - "{goto}{text}", - goto = cursor::Goto(1, index as u16 + 1), - text = &trimmed_line, - ) - .unwrap(); - } - } - } - - /// Render the Match's `text` field on provided writer. - /// - /// If a Mach is "focused", then it is rendered with a specific color. - /// - /// # Note - /// This writes directly on the writer, avoiding extra allocation. - fn render_matched_text( - stdout: &mut dyn Write, - text: &str, - focused: bool, - offset: (usize, usize), - colors: &ViewColors, - ) { - // To help identify it, the match thas has focus is rendered with a dedicated color. - let (text_fg_color, text_bg_color) = if focused { - (&colors.focused_fg, &colors.focused_bg) - } else { - (&colors.match_fg, &colors.match_bg) - }; - - // Render just the Match's text on top of existing content. - write!( - stdout, - "{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}", - goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), - fg_color = color::Fg(text_fg_color.as_ref()), - bg_color = color::Bg(text_bg_color.as_ref()), - fg_reset = color::Fg(color::Reset), - bg_reset = color::Bg(color::Reset), - text = &text, - ) - .unwrap(); - } - - /// 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( - stdout: &mut dyn Write, - hint_text: &str, - offset: (usize, usize), - colors: &ViewColors, - hint_style: &Option, - ) { - 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); - - match hint_style { - None => { - write!( - stdout, - "{goto}{bg_color}{fg_color}{hint}{fg_reset}{bg_reset}", - goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), - 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 { - HintStyle::Underlined => { - write!( - stdout, - "{goto}{bg_color}{fg_color}{sty}{hint}{sty_reset}{fg_reset}{bg_reset}", - goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), - 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(); + View { + state, + matches, + focus_index, + multi, + hint_alignment, + rendering_colors, + hint_style, } - HintStyle::Surrounded(opening, closing) => { - write!( - stdout, - "{goto}{bg_color}{fg_color}{bra}{hint}{bra_close}{fg_reset}{bg_reset}", - goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), - 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(); - } - }, } - } - /// Render the view on the provided writer. - /// - /// 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. - fn render(&self, stdout: &mut dyn Write) -> () { - write!(stdout, "{}", cursor::Hide).unwrap(); + /// Move focus onto the previous hint. + pub fn prev(&mut self) { + if self.focus_index > 0 { + self.focus_index -= 1; + } + } - // 1. Trim all lines and render non-empty ones. - View::render_lines(stdout, self.state.lines); + /// Move focus onto the next hint. + pub fn next(&mut self) { + if self.focus_index < self.matches.len() - 1 { + self.focus_index += 1; + } + } - for (index, mat) in self.matches.iter().enumerate() { - // 2. Render the match's text. + /// Render entire state lines on provided writer. + /// + /// 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. + fn render_lines(stdout: &mut dyn Write, lines: &Vec<&str>) -> () { + for (index, line) in lines.iter().enumerate() { + let trimmed_line = line.trim_end(); - // 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 offset_x = { - let line = &self.state.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; + if !trimmed_line.is_empty() { + write!( + stdout, + "{goto}{text}", + goto = cursor::Goto(1, index as u16 + 1), + text = &trimmed_line, + ) + .unwrap(); + } + } + } - let text = &mat.text; - - let focused = index == self.focus_index; - - View::render_matched_text(stdout, text, focused, (offset_x, offset_y), &self.rendering_colors); - - // 3. Render the hint (e.g. "eo") as an overlay on top of the rendered matched text, - // aligned at its leading or the trailing edge. - if let Some(ref hint) = mat.hint { - let extra_offset = match self.hint_alignment { - HintAlignment::Leading => 0, - HintAlignment::Trailing => text.len() - hint.len(), + /// Render the Match's `text` field on provided writer. + /// + /// If a Mach is "focused", then it is rendered with a specific color. + /// + /// # Note + /// This writes directly on the writer, avoiding extra allocation. + fn render_matched_text( + stdout: &mut dyn Write, + text: &str, + focused: bool, + offset: (usize, usize), + colors: &ViewColors, + ) { + // To help identify it, the match thas has focus is rendered with a dedicated color. + let (text_fg_color, text_bg_color) = if focused { + (&colors.focused_fg, &colors.focused_bg) + } else { + (&colors.match_fg, &colors.match_bg) }; - View::render_matched_hint( - stdout, - hint, - (offset_x + extra_offset, offset_y), - &self.rendering_colors, - &self.hint_style, - ); - } + // Render just the Match's text on top of existing content. + write!( + stdout, + "{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}", + goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + fg_color = color::Fg(text_fg_color.as_ref()), + bg_color = color::Bg(text_bg_color.as_ref()), + fg_reset = color::Fg(color::Reset), + bg_reset = color::Bg(color::Reset), + text = &text, + ) + .unwrap(); } - stdout.flush().unwrap(); - } + /// 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( + stdout: &mut dyn Write, + hint_text: &str, + offset: (usize, usize), + colors: &ViewColors, + hint_style: &Option, + ) { + 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); - /// Listen to keys entered on stdin, moving focus accordingly, and selecting - /// one or multiple matches. - /// - /// # 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 { - if self.matches.is_empty() { - return CaptureEvent::Exit; + match hint_style { + None => { + write!( + stdout, + "{goto}{bg_color}{fg_color}{hint}{fg_reset}{bg_reset}", + goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + 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 { + HintStyle::Underline => { + write!( + stdout, + "{goto}{bg_color}{fg_color}{sty}{hint}{sty_reset}{fg_reset}{bg_reset}", + goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + 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}", + goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + 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(); + } + }, + } } - 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(); + /// Render the view on the provided writer. + /// + /// 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. + fn render(&self, stdout: &mut dyn Write) -> () { + write!(stdout, "{}", cursor::Hide).unwrap(); - self.render(stdout); + // 1. Trim all lines and render non-empty ones. + View::render_lines(stdout, self.state.lines); - loop { - // This is an option of a result of a key... Let's pop error cases first. - let next_key = stdin.keys().next(); + for (index, mat) in self.matches.iter().enumerate() { + // 2. Render the match's text. - if next_key.is_none() { - // Nothing in the buffer. Wait for a bit... - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } + // 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 offset_x = { + let line = &self.state.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; - 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); - } + let text = &mat.text; - match key_res.unwrap() { - // Clears an ongoing multi-hint selection, or exit. - Key::Esc => { - if self.multi && !typed_hint.is_empty() { - typed_hint.clear(); - } else { - break; - } + let focused = index == self.focus_index; + + View::render_matched_text( + stdout, + text, + focused, + (offset_x, offset_y), + &self.rendering_colors, + ); + + // 3. Render the hint (e.g. "eo") as an overlay on top of the rendered matched text, + // aligned at its leading or the trailing edge. + if let Some(ref hint) = mat.hint { + let extra_offset = match self.hint_alignment { + HintAlignment::Leading => 0, + HintAlignment::Trailing => text.len() - hint.len(), + }; + + View::render_matched_hint( + stdout, + hint, + (offset_x + extra_offset, offset_y), + &self.rendering_colors, + &self.hint_style, + ); + } } - // In multi-selection mode, this appends the selected hint to the - // vector of selections. In normal mode, this returns with the hint - // selected. - Key::Insert => match self.matches.get(self.focus_index) { - Some(mat) => { - chosen.push((mat.text.to_string(), false)); + stdout.flush().unwrap(); + } - if !self.multi { - return CaptureEvent::Hint(chosen); + /// Listen to keys entered on stdin, moving focus accordingly, and selecting + /// one or multiple matches. + /// + /// # 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 { + 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 { + // This is an option of a result of a key... Let's pop error cases first. + let next_key = stdin.keys().next(); + + if next_key.is_none() { + // Nothing in the buffer. Wait for a bit... + std::thread::sleep(std::time::Duration::from_millis(100)); + continue; } - } - None => panic!("Match not found?"), - }, - // Move focus to next/prev match. - Key::Up => self.prev(), - Key::Down => self.next(), - Key::Left => self.prev(), - Key::Right => self.next(), + 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); + } - // Pressing space finalizes an ongoing multi-hint selection (without - // selecting the focused match). Pressing other characters attempts at - // finding a match with a corresponding hint. - Key::Char(ch) => { - if ch == ' ' && self.multi { - return CaptureEvent::Hint(chosen); - } + match key_res.unwrap() { + // Clears an ongoing multi-hint selection, or exit. + Key::Esc => { + if self.multi && !typed_hint.is_empty() { + typed_hint.clear(); + } else { + break; + } + } - let key = ch.to_string(); - let lower_key = key.to_lowercase(); + // In multi-selection mode, this appends the selected hint to the + // vector of selections. In normal mode, this returns with the hint + // selected. + Key::Insert => match self.matches.get(self.focus_index) { + Some(mat) => { + chosen.push((mat.text.to_string(), false)); - typed_hint.push_str(&lower_key); + if !self.multi { + return CaptureEvent::Hint(chosen); + } + } + None => panic!("Match not found?"), + }, - // Find the match that corresponds to the entered key. - let selection = self + // Move focus to next/prev match. + Key::Up => self.prev(), + Key::Down => self.next(), + Key::Left => self.prev(), + Key::Right => self.next(), + + // Pressing space finalizes an ongoing multi-hint selection (without + // selecting the focused match). Pressing other characters attempts at + // finding a match with a 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); + + // Find the match that corresponds to the entered key. + let selection = self .matches .iter() // Avoid cloning typed_hint for comparison. .find(|&mat| mat.hint.as_deref().unwrap_or_default() == &typed_hint); - match selection { - Some(mat) => { - chosen.push((mat.text.to_string(), key != lower_key)); + match selection { + Some(mat) => { + chosen.push((mat.text.to_string(), key != lower_key)); - if self.multi { - typed_hint.clear(); - } else { - return CaptureEvent::Hint(chosen); - } + if self.multi { + typed_hint.clear(); + } else { + return CaptureEvent::Hint(chosen); + } + } + None => { + // TODO: use a Trie or another data structure to determine + // if the entered key belongs to a longer hint. + if !self.multi && typed_hint.len() >= longest_hint.len() { + break; + } + } + } + } + + // Unknown keys are ignored. + _ => (), } - None => { - // TODO: use a Trie or another data structure to determine - // if the entered key belongs to a longer hint. - if !self.multi && typed_hint.len() >= longest_hint.len() { - break; - } - } - } + + // Render on stdout if we did not exit earlier (move focus, + // multi-selection). + self.render(stdout); } - // Unknown keys are ignored. - _ => (), - } - - // Render on stdout if we did not exit earlier (move focus, - // multi-selection). - self.render(stdout); + CaptureEvent::Exit } - 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()); - 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, + }; - let hints = match self.listen(&mut stdin, &mut stdout) { - CaptureEvent::Exit => vec![], - CaptureEvent::Hint(chosen) => chosen, - }; + write!(stdout, "{}", cursor::Show).unwrap(); - write!(stdout, "{}", cursor::Show).unwrap(); - - hints - } + hints + } } #[cfg(test)] mod tests { - use super::*; - use crate::alphabets; + use super::*; + use crate::alphabets; - #[test] - fn test_render_all_lines() { - let content = "some text + #[test] + fn test_render_all_lines() { + let content = "some text * e006b06 - (12 days ago) swapper: Make quotes path: /usr/local/bin/git path: /usr/local/bin/cargo"; - let lines: Vec<&str> = content.split('\n').collect(); + let lines: Vec<&str> = content.split('\n').collect(); - let mut writer = vec![]; - View::render_lines(&mut writer, &lines); + let mut writer = vec![]; + View::render_lines(&mut writer, &lines); - 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!( + 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!( writer, format!( "{}some text{}* e006b06 - (12 days ago) swapper: Make quotes{}path: /usr/local/bin/git{}path: /usr/local/bin/cargo", @@ -479,335 +485,335 @@ path: /usr/local/bin/cargo"; ) .as_bytes() ); - } + } - #[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); - let colors = ViewColors { - 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), - }; + #[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); + let colors = ViewColors { + 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), + }; - View::render_matched_text(&mut writer, text, focused, offset, &colors); + View::render_matched_text(&mut writer, text, focused, offset, &colors); - 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() - ); - } + 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() + ); + } - #[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); - let colors = ViewColors { - 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), - }; + #[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); + let colors = ViewColors { + 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), + }; - View::render_matched_text(&mut writer, text, focused, offset, &colors); + View::render_matched_text(&mut writer, text, focused, offset, &colors); - 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() - ); - } + 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); - let colors = ViewColors { - 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), - }; + #[test] + fn test_render_unstyled_matched_hint() { + let mut writer = vec![]; + let hint_text = "eo"; + let offset: (usize, usize) = (3, 1); + let colors = ViewColors { + 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; + let extra_offset = 0; + let hint_style = None; - View::render_matched_hint( - &mut writer, - hint_text, - (offset.0 + extra_offset, offset.1), - &colors, - &hint_style, - ); + View::render_matched_hint( + &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() - ); - } + 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); - let colors = ViewColors { - 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), - }; + #[test] + fn test_render_underlined_matched_hint() { + let mut writer = vec![]; + let hint_text = "eo"; + let offset: (usize, usize) = (3, 1); + let colors = ViewColors { + 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::Underlined); + let extra_offset = 0; + let hint_style = Some(HintStyle::Underline); - View::render_matched_hint( - &mut writer, - hint_text, - (offset.0 + extra_offset, offset.1), - &colors, - &hint_style, - ); + View::render_matched_hint( + &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() - ); - } + 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); - let colors = ViewColors { - 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), - }; + #[test] + fn test_render_bracketed_matched_hint() { + let mut writer = vec![]; + let hint_text = "eo"; + let offset: (usize, usize) = (3, 1); + let colors = ViewColors { + 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::Surrounded('{', '}')); + let extra_offset = 0; + let hint_style = Some(HintStyle::Surround('{', '}')); - View::render_matched_hint( - &mut writer, - hint_text, - (offset.0 + extra_offset, offset.1), - &colors, - &hint_style, - ); + View::render_matched_hint( + &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() - ); - } + 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() + ); + } - #[test] - /// Simulates rendering without any match. - fn test_render_full_without_matches() { - let content = "lorem 127.0.0.1 lorem + #[test] + /// Simulates rendering without any match. + fn test_render_full_without_matches() { + let content = "lorem 127.0.0.1 lorem Barcelona https://en.wikipedia.org/wiki/Barcelona - "; - let lines = content.split('\n').collect(); + let lines = content.split('\n').collect(); - let custom_regexes = [].to_vec(); - let alphabet = alphabets::Alphabet("abcd".to_string()); - let mut state = state::State::new(&lines, &alphabet, &custom_regexes); - let rendering_colors = ViewColors { - 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 custom_regexes = [].to_vec(); + let alphabet = alphabets::Alphabet("abcd".to_string()); + let mut state = state::State::new(&lines, &alphabet, &custom_regexes); + let rendering_colors = ViewColors { + 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; - // create a view without any match - let view = View { - state: &mut state, - matches: vec![], // no matches - focus_index: 0, - multi: false, - hint_alignment, - rendering_colors: &rendering_colors, - hint_style: None, - }; + // create a view without any match + let view = View { + state: &mut state, + matches: vec![], // no matches + focus_index: 0, + multi: false, + hint_alignment, + rendering_colors: &rendering_colors, + hint_style: None, + }; - let mut writer = vec![]; - view.render(&mut writer); + let mut writer = vec![]; + view.render(&mut writer); - let hide = cursor::Hide; - let goto1 = cursor::Goto(1, 1); - let goto3 = cursor::Goto(1, 3); + let hide = cursor::Hide; + let goto1 = cursor::Goto(1, 1); + let goto3 = cursor::Goto(1, 3); - let expected = format!( - "{hide}{goto1}lorem 127.0.0.1 lorem\ + let expected = format!( + "{hide}{goto1}lorem 127.0.0.1 lorem\ {goto3}Barcelona https://en.wikipedia.org/wiki/Barcelona -", - hide = hide, - goto1 = goto1, - goto3 = goto3, - ); + hide = hide, + goto1 = goto1, + goto3 = goto3, + ); - // println!("{:?}", writer); - // println!("{:?}", expected.as_bytes()); + // println!("{:?}", writer); + // println!("{:?}", expected.as_bytes()); - // println!("matches: {}", view.matches.len()); - // println!("lines: {}", lines.len()); + // println!("matches: {}", view.matches.len()); + // println!("lines: {}", lines.len()); - assert_eq!(writer, expected.as_bytes()); - } + assert_eq!(writer, expected.as_bytes()); + } - #[test] - /// Simulates rendering with matches. - fn test_render_full_with_matches() { - let content = "lorem 127.0.0.1 lorem + #[test] + /// Simulates rendering with matches. + fn test_render_full_with_matches() { + let content = "lorem 127.0.0.1 lorem Barcelona https://en.wikipedia.org/wiki/Barcelona - "; - let lines = content.split('\n').collect(); + let lines = content.split('\n').collect(); - let custom_regexes = [].to_vec(); - let alphabet = alphabets::Alphabet("abcd".to_string()); - let mut state = state::State::new(&lines, &alphabet, &custom_regexes); - let multi = false; - let reversed = true; - let unique = false; + let custom_regexes = [].to_vec(); + let alphabet = alphabets::Alphabet("abcd".to_string()); + let mut state = state::State::new(&lines, &alphabet, &custom_regexes); + let multi = false; + let reversed = true; + let unique = false; - let rendering_colors = ViewColors { - 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; + let rendering_colors = ViewColors { + 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; - let view = View::new( - &mut state, - multi, - reversed, - unique, - hint_alignment, - &rendering_colors, - hint_style, - ); + let view = View::new( + &mut state, + multi, + reversed, + unique, + hint_alignment, + &rendering_colors, + hint_style, + ); - let mut writer = vec![]; - view.render(&mut writer); + let mut writer = vec![]; + view.render(&mut writer); - let expected_content = { - let hide = cursor::Hide; - let goto1 = cursor::Goto(1, 1); - let goto3 = cursor::Goto(1, 3); + let expected_content = { + let hide = cursor::Hide; + let goto1 = cursor::Goto(1, 1); + let goto3 = cursor::Goto(1, 3); - format!( - "{hide}{goto1}lorem 127.0.0.1 lorem\ + format!( + "{hide}{goto1}lorem 127.0.0.1 lorem\ {goto3}Barcelona https://en.wikipedia.org/wiki/Barcelona -", - hide = hide, - goto1 = goto1, - goto3 = goto3, - ) - }; + hide = hide, + goto1 = goto1, + goto3 = goto3, + ) + }; - 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) - ) - }; + 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) + ) + }; - let expected_match1_hint = { - let goto7_1 = cursor::Goto(7, 1); + 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) - ) - }; + 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) + ) + }; - let expected_match2_text = { - let goto11_3 = cursor::Goto(11, 3); - format!( + let expected_match2_text = { + let goto11_3 = cursor::Goto(11, 3); + format!( "{goto11_3}{focus_bg}{focus_fg}https://en.wikipedia.org/wiki/Barcelona{fg_reset}{bg_reset}", goto11_3 = goto11_3, focus_fg = color::Fg(rendering_colors.focused_fg.as_ref()), @@ -815,42 +821,42 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; fg_reset = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset) ) - }; + }; - let expected_match2_hint = { - let goto11_3 = cursor::Goto(11, 3); + 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) - ) - }; + 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) + ) + }; - let expected = [ - expected_content, - expected_match1_text, - expected_match1_hint, - expected_match2_text, - expected_match2_hint, - ] - .concat(); + let expected = [ + expected_content, + expected_match1_text, + expected_match1_hint, + expected_match2_text, + expected_match2_hint, + ] + .concat(); - // println!("{:?}", writer); - // println!("{:?}", expected.as_bytes()); + // 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); + // let diff_point = writer + // .iter() + // .zip(expected.as_bytes().iter()) + // .enumerate() + // .find(|(_idx, (&l, &r))| l != r); + // println!("{:?}", diff_point); - assert_eq!(2, view.matches.len()); + assert_eq!(2, view.matches.len()); - assert_eq!(writer, expected.as_bytes()); - } + assert_eq!(writer, expected.as_bytes()); + } } From b3099b42c9a351b6ada6245f472d74df11d115e5 Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 25 May 2020 23:32:37 +0200 Subject: [PATCH 06/40] refactor: refactor --- src/lib.rs | 28 +++++++--------------------- src/main.rs | 21 ++++++++++++++++++++- src/view.rs | 8 ++++---- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c3f9900..37e52c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,4 @@ use clap::Clap; -use std::fs::OpenOptions; -use std::io::prelude::*; use std::path; pub mod alphabets; @@ -14,12 +12,12 @@ pub mod view; /// # Note /// /// Maybe the decision to move ownership is a bit bold. -pub fn run(buffer: String, opt: Opt) { +pub fn run(buffer: String, opt: &Opt) -> String { let lines: Vec<&str> = buffer.split('\n').collect(); let mut state = state::State::new(&lines, &opt.alphabet, &opt.custom_regex); - let hint_style = match opt.hint_style { + let hint_style = match &opt.hint_style { None => None, Some(style) => match style { HintStyleCli::Underline => Some(view::HintStyle::Underline), @@ -37,7 +35,7 @@ pub fn run(buffer: String, opt: Opt) { opt.multi_selection, opt.reverse, opt.unique, - opt.hint_alignment, + &opt.hint_alignment, &opt.colors, hint_style, ); @@ -64,19 +62,7 @@ pub fn run(buffer: String, opt: Opt) { .join("\n") }; - match opt.target_path { - None => println!("{}", output), - Some(target) => { - let mut file = OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(target) - .expect("Unable to open the target file"); - - file.write(output.as_bytes()).unwrap(); - } - } + output } /// Main configuration, parsed from command line. @@ -108,7 +94,7 @@ pub struct Opt { unique: bool, /// Align hint with its match. - #[clap(short = "a", long, arg_enum, default_value = "Leading")] + #[clap(short = "a", long, arg_enum, default_value = "leading")] hint_alignment: view::HintAlignment, /// Additional regex patterns. @@ -129,11 +115,11 @@ pub struct Opt { /// Target path where to store the selected matches. #[clap(short = "o", long = "output", parse(from_os_str))] - target_path: Option, + pub target_path: Option, /// Describes if the uppercased marker should be added to the output, /// indicating if hint key was uppercased. This is only used by - /// tmux-copyrat, so it is skipped from clap configuration. + /// tmux-copyrat, so it is hidden (skipped) from the CLI. #[clap(skip)] uppercased_marker: bool, } diff --git a/src/main.rs b/src/main.rs index 244a514..cec8f83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,6 @@ use clap::Clap; +use std::fs::OpenOptions; +use std::io::prelude::*; use std::io::{self, Read}; use copyrat::{run, Opt}; @@ -13,5 +15,22 @@ fn main() { let mut buffer = String::new(); handle.read_to_string(&mut buffer).unwrap(); - run(buffer, opt); + // Execute copyrat over the buffer (will take control over stdout). + // This returns the selected matches. + let output: String = run(buffer, &opt); + + // Write output to a target_path if provided, else print to original stdout. + match opt.target_path { + None => println!("{}", output), + Some(target) => { + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(target) + .expect("Unable to open the target file"); + + file.write(output.as_bytes()).unwrap(); + } + } } diff --git a/src/view.rs b/src/view.rs index 429e3be..383fb34 100644 --- a/src/view.rs +++ b/src/view.rs @@ -14,7 +14,7 @@ pub struct View<'a> { matches: Vec>, focus_index: usize, multi: bool, - hint_alignment: HintAlignment, + hint_alignment: &'a HintAlignment, rendering_colors: &'a ViewColors, hint_style: Option, } @@ -91,7 +91,7 @@ impl<'a> View<'a> { multi: bool, reversed: bool, unique: bool, - hint_alignment: HintAlignment, + hint_alignment: &'a HintAlignment, rendering_colors: &'a ViewColors, hint_style: Option, ) -> View<'a> { @@ -703,7 +703,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; matches: vec![], // no matches focus_index: 0, multi: false, - hint_alignment, + hint_alignment: &hint_alignment, rendering_colors: &rendering_colors, hint_style: None, }; @@ -764,7 +764,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; multi, reversed, unique, - hint_alignment, + &hint_alignment, &rendering_colors, hint_style, ); From 623c66cbba508e6498248313ea0408e47bd4c2b1 Mon Sep 17 00:00:00 2001 From: graelo Date: Tue, 26 May 2020 08:11:45 +0200 Subject: [PATCH 07/40] refactor: refactor --- src/lib.rs | 6 +++--- src/swapper.rs | 25 +++++++++++++++---------- src/tmux.rs | 37 ++++++++++++++++--------------------- src/view.rs | 9 +++++---- 4 files changed, 39 insertions(+), 38 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 37e52c1..2fc1f80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,7 @@ pub fn run(buffer: String, opt: &Opt) -> String { &mut state, opt.multi_selection, opt.reverse, - opt.unique, + opt.unique_hint, &opt.hint_alignment, &opt.colors, hint_style, @@ -91,7 +91,7 @@ pub struct Opt { /// Keep the same hint for identical matches. #[clap(short, long)] - unique: bool, + unique_hint: bool, /// Align hint with its match. #[clap(short = "a", long, arg_enum, default_value = "leading")] @@ -113,7 +113,7 @@ pub struct Opt { parse(try_from_str = parse_chars))] hint_surroundings: (char, char), - /// Target path where to store the selected matches. + /// Optional target path where to store the selected matches. #[clap(short = "o", long = "output", parse(from_os_str))] pub target_path: Option, diff --git a/src/swapper.rs b/src/swapper.rs index 9bf3e0d..a01966a 100644 --- a/src/swapper.rs +++ b/src/swapper.rs @@ -198,14 +198,13 @@ impl<'a> Swapper<'a> { // NOTE: For debugging add echo $PWD && sleep 5 after tee let pane_command = format!( - "tmux capture-pane -t {} -p{} | {}/target/release/thumbs -f '%U:%H' -t {} {}; tmux swap-pane -t {}; tmux wait-for -S {}", - active_pane_id, - scroll_params, - self.directory.to_str().unwrap(), - TMP_FILE, - args.join(" "), - active_pane_id, - self.signal + "tmux capture-pane -t {active_id} -p{scroll_params} | {dir}/target/release/thumbs -f '%U:%H' -t {tmpfile} {args}; tmux swap-pane -t {active_id}; tmux wait-for -S {signal}", + active_id = active_pane_id, + scroll_params = scroll_params, + dir = self.directory.to_str().unwrap(), + tmpfile = TMP_FILE, + args = args.join(" "), + signal = self.signal ); let thumbs_command = vec![ @@ -358,7 +357,7 @@ struct Opt { #[clap(short, long, default_value = "'tmux set-buffer {}'")] command: String, - /// Command to execute on uppercased selection. + /// Command to execute on alt selection. #[clap( short, long, @@ -369,11 +368,15 @@ struct Opt { /// Retrieve options from tmux. /// /// If active, options formatted like `copyrat-*` are read from tmux. - /// You should prefer reading them from the config file (the default + /// You should consider reading them from the config file (the default /// option) as this saves both a command call (about 10ms) and a Regex /// compilation. #[clap(long)] options_from_tmux: bool, + + /// Optionally capture entire pane history. + #[clap(long, arg_enum, default_value = "entire-history")] + capture: tmux::CaptureRegion, } fn main() -> Result<(), error::ParseError> { @@ -392,6 +395,8 @@ fn main() -> Result<(), error::ParseError> { .find(|p| p.is_active) .expect("One tmux pane should be active"); + let content = tmux::capture_pane(&active_pane, &opt.capture)?; + let mut executor = RealShell::new(); let mut swapper = Swapper::new( diff --git a/src/tmux.rs b/src/tmux.rs index 1a902ce..48f4447 100644 --- a/src/tmux.rs +++ b/src/tmux.rs @@ -1,3 +1,4 @@ +use clap::Clap; use copyrat::error::ParseError; use regex::Regex; use std::collections::HashMap; @@ -5,7 +6,7 @@ use std::process::Command; /// Execute an arbitrary Unix command and return the stdout as a `String` if /// successful. -pub fn execute(command: &str, args: &Vec<&str>) -> Result { +fn execute(command: &str, args: &Vec<&str>) -> Result { let output = Command::new(command).args(args).output()?; if !output.status.success() { @@ -177,25 +178,24 @@ pub fn get_options(prefix: &str) -> Result, ParseError> // } // };} -#[derive(Debug)] +#[derive(Clap, Debug)] pub enum CaptureRegion { /// The entire history. /// /// This will end up sending `-S - -E -` to `tmux capture-pane`. EntireHistory, - /// Region from start line to end line - /// - /// This works as defined in tmux's docs (order does not matter). - Region(i32, i32), + /// The visible area. + VisibleArea, + ///// Region from start line to end line + ///// + ///// This works as defined in tmux's docs (order does not matter). + //Region(i32, i32), } -/// Returns the Pane's content as a `String`. The `CaptureRegion` if `None` -/// will result in the Pane's visible area to be capture (mimics the default -/// behavior of tmux `capture-pane`). If a `CaptureRegion` is provided, -/// depending on its value, either the entire history will be captured, or a -/// user-provided region. For the user-provided region, the order of `start` -/// and `end` does not matter. They have the same meaning as described in Tmux -/// documentation. +/// Returns the entire Pane content as a `String`. +/// +/// `CaptureRegion` specifies if the visible area is captured, or the entire +/// history. /// /// # Note /// @@ -204,11 +204,11 @@ pub enum CaptureRegion { /// pane is in copy mode, we need to take into account the current scroll /// position. To support both cases, the implementation always provides those /// parameters to tmux. -pub fn capture_pane(pane: &Pane, region: &Option) -> Result { +pub fn capture_pane(pane: &Pane, region: &CaptureRegion) -> Result { let mut args = format!("capture-pane -t {id} -p", id = pane.id); let region_str = match region { - None => { + CaptureRegion::VisibleArea => { // Will capture the visible area. // Providing start/end helps support both copy and normal modes. format!( @@ -217,12 +217,7 @@ pub fn capture_pane(pane: &Pane, region: &Option) -> Result match region { - CaptureRegion::EntireHistory => String::from(" -S - -E -"), - CaptureRegion::Region(start, end) => { - format!(" -S {start} -E {end}", start = start, end = end) - } - }, + CaptureRegion::EntireHistory => String::from(" -S - -E -"), }; args.push_str(®ion_str); diff --git a/src/view.rs b/src/view.rs index 383fb34..f0ac6b9 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,4 +1,5 @@ use super::{colors, state}; + use clap::Clap; use std::char; use std::io::{stdout, Read, Write}; @@ -90,12 +91,12 @@ impl<'a> View<'a> { state: &'a mut state::State<'a>, multi: bool, reversed: bool, - unique: bool, + unique_hint: bool, hint_alignment: &'a HintAlignment, rendering_colors: &'a ViewColors, hint_style: Option, ) -> View<'a> { - let matches = state.matches(reversed, unique); + let matches = state.matches(reversed, unique_hint); let focus_index = if reversed { matches.len() - 1 } else { 0 }; View { @@ -746,7 +747,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let mut state = state::State::new(&lines, &alphabet, &custom_regexes); let multi = false; let reversed = true; - let unique = false; + let unique_hint = false; let rendering_colors = ViewColors { focused_fg: Box::new(color::Red), @@ -763,7 +764,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; &mut state, multi, reversed, - unique, + unique_hint, &hint_alignment, &rendering_colors, hint_style, From 791aaadd49dc3222b8939073bbb21bd9e646e961 Mon Sep 17 00:00:00 2001 From: graelo Date: Wed, 27 May 2020 10:04:42 +0200 Subject: [PATCH 08/40] refactor: refactor --- Cargo.toml | 2 +- src/{swapper.rs => bridge.rs} | 136 +++++-- src/error.rs | 2 + src/lib.rs | 112 ++++-- src/main.rs | 17 +- src/process.rs | 21 + src/regexes.rs | 25 ++ src/state.rs | 738 +++++++++++++++++----------------- src/tmux.rs | 42 +- src/view.rs | 28 +- 10 files changed, 655 insertions(+), 468 deletions(-) rename src/{swapper.rs => bridge.rs} (79%) create mode 100644 src/process.rs create mode 100644 src/regexes.rs diff --git a/Cargo.toml b/Cargo.toml index b3ed495..23e7a2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,4 @@ path = "src/main.rs" [[bin]] name = "tmux-copyrat" -path = "src/swapper.rs" +path = "src/bridge.rs" diff --git a/src/swapper.rs b/src/bridge.rs similarity index 79% rename from src/swapper.rs rename to src/bridge.rs index a01966a..b33f95e 100644 --- a/src/swapper.rs +++ b/src/bridge.rs @@ -1,10 +1,11 @@ use clap::Clap; use regex::Regex; -use std::path; +use std::collections::HashMap; use std::process::Command; +use std::str::FromStr; use std::time::{SystemTime, UNIX_EPOCH}; -use copyrat::error; +use copyrat::{error, process, CliOpt}; mod tmux; @@ -46,7 +47,7 @@ const TMP_FILE: &str = "/tmp/copyrat-last"; pub struct Swapper<'a> { executor: Box<&'a mut dyn Executor>, - directory: &'a path::Path, + // directory: &'a path::Path, command: &'a str, alt_command: &'a str, active_pane_id: Option, @@ -61,7 +62,7 @@ pub struct Swapper<'a> { impl<'a> Swapper<'a> { fn new( executor: Box<&'a mut dyn Executor>, - directory: &'a path::Path, + // directory: &'a path::Path, command: &'a str, alt_command: &'a str, ) -> Swapper<'a> { @@ -72,7 +73,7 @@ impl<'a> Swapper<'a> { Swapper { executor, - directory, + // directory, command, alt_command, active_pane_id: None, @@ -198,10 +199,10 @@ impl<'a> Swapper<'a> { // NOTE: For debugging add echo $PWD && sleep 5 after tee let pane_command = format!( - "tmux capture-pane -t {active_id} -p{scroll_params} | {dir}/target/release/thumbs -f '%U:%H' -t {tmpfile} {args}; tmux swap-pane -t {active_id}; tmux wait-for -S {signal}", + "tmux capture-pane -t {active_id} -p{scroll_params} | target/release/thumbs -f '%U:%H' -t {tmpfile} {args}; tmux swap-pane -t {active_id}; tmux wait-for -S {signal}", active_id = active_pane_id, scroll_params = scroll_params, - dir = self.directory.to_str().unwrap(), + // dir = self.directory.to_str().unwrap(), tmpfile = TMP_FILE, args = args.join(" "), signal = self.signal @@ -317,7 +318,7 @@ mod tests { let last_command_outputs = vec!["%97:100:24:1:active\n%106:100:24:1:nope\n%107:100:24:1:nope\n".to_string()]; let mut executor = TestShell::new(last_command_outputs); - let mut swapper = Swapper::new(Box::new(&mut executor), &path::Path::new(""), "", ""); + let mut swapper = Swapper::new(Box::new(&mut executor), "", ""); swapper.capture_active_pane(); @@ -333,7 +334,7 @@ mod tests { "%106:100:24:1:nope\n%98:100:24:1:active\n%107:100:24:1:nope\n".to_string(), ]; let mut executor = TestShell::new(last_command_outputs); - let mut swapper = Swapper::new(Box::new(&mut executor), &path::Path::new(""), "", ""); + let mut swapper = Swapper::new(Box::new(&mut executor), "", ""); swapper.capture_active_pane(); swapper.execute_thumbs(); @@ -348,21 +349,13 @@ mod tests { /// Main configuration, parsed from command line. #[derive(Clap, Debug)] #[clap(author, about, version)] -struct Opt { - /// Directory where to execute copyrat. - #[clap(long, required = true)] - directory: path::PathBuf, - +struct BridgeOpt { /// Command to execute on selection. - #[clap(short, long, default_value = "'tmux set-buffer {}'")] + #[clap(short, long, default_value = "tmux set-buffer {}")] command: String, - /// Command to execute on alt selection. - #[clap( - short, - long, - default_value = "'tmux set-buffer {} && tmux-paste-buffer'" - )] + /// Command to execute on uppercased selection. + #[clap(short, long, default_value = "tmux set-buffer {} && tmux-paste-buffer")] alt_command: String, /// Retrieve options from tmux. @@ -372,22 +365,54 @@ struct Opt { /// option) as this saves both a command call (about 10ms) and a Regex /// compilation. #[clap(long)] - options_from_tmux: bool, + get_options_from_tmux: bool, /// Optionally capture entire pane history. #[clap(long, arg_enum, default_value = "entire-history")] capture: tmux::CaptureRegion, + + // Include CLI Options + #[clap(flatten)] + cli_options: CliOpt, +} + +impl BridgeOpt { + /// Try parsing provided options, and update self with the valid values. + pub fn merge_map( + &mut self, + options: &HashMap, + ) -> Result<(), error::ParseError> { + for (name, value) in options { + match name.as_ref() { + "@copyrat-command" => { + self.command = String::from(value); + } + "@copyrat-alt-command" => { + self.alt_command = String::from(value); + } + "@copyrat-capture" => { + self.capture = tmux::CaptureRegion::from_str(&value)?; + } + + // Ignore unknown options. + _ => (), + } + } + + // Pass the call to cli_options. + self.cli_options.merge_map(options)?; + + Ok(()) + } } fn main() -> Result<(), error::ParseError> { - let opt = Opt::parse(); - // let dir = args.value_of("dir").unwrap(); - // let command = args.value_of("command").unwrap(); - // let upcase_command = args.value_of("upcase_command").unwrap(); + let mut opt = BridgeOpt::parse(); - // if dir.is_empty() { - // panic!("Invalid tmux-thumbs execution. Are you trying to execute tmux-thumbs directly?") - // } + if opt.get_options_from_tmux { + let tmux_options = tmux::get_options("@copyrat-")?; + opt.merge_map(&tmux_options)?; + } let panes: Vec = tmux::list_panes()?; let active_pane = panes @@ -395,23 +420,46 @@ fn main() -> Result<(), error::ParseError> { .find(|p| p.is_active) .expect("One tmux pane should be active"); - let content = tmux::capture_pane(&active_pane, &opt.capture)?; + let buffer = tmux::capture_pane(&active_pane, &opt.capture)?; - let mut executor = RealShell::new(); + let selections: Vec<(String, bool)> = if active_pane.in_mode { + // TODO: fancy stuff + vec![(String::new(), false)] + } else { + copyrat::run(buffer, &opt.cli_options) + }; - let mut swapper = Swapper::new( - Box::new(&mut executor), - opt.directory.as_path(), - &opt.command, - &opt.alt_command, - ); + selections.iter().for_each(|(text, b)| { + let command = if *b { + opt.alt_command.replace("{}", text) + } else { + opt.command.replace("{}", text) + }; + let args: Vec<&str> = vec![]; + + // Simply execute the command as is and don't mind about potential + // errors. + process::execute(&command, &args).unwrap(); + }); + + if false { + let mut executor = RealShell::new(); + + let mut swapper = Swapper::new( + Box::new(&mut executor), + // opt.directory.as_path(), + &opt.command, + &opt.alt_command, + ); + + swapper.capture_active_pane(); + swapper.execute_thumbs(); + swapper.swap_panes(); + swapper.wait_thumbs(); + swapper.retrieve_content(); + swapper.destroy_content(); + swapper.execute_command(); + } - swapper.capture_active_pane(); - swapper.execute_thumbs(); - swapper.swap_panes(); - swapper.wait_thumbs(); - swapper.retrieve_content(); - swapper.destroy_content(); - swapper.execute_command(); Ok(()) } diff --git a/src/error.rs b/src/error.rs index d38f483..aa233cf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,6 +8,7 @@ pub enum ParseError { ExpectedPaneIdMarker, ExpectedInt(std::num::ParseIntError), ExpectedBool(std::str::ParseBoolError), + ExpectedString(String), ProcessFailure(String), } @@ -22,6 +23,7 @@ impl fmt::Display for ParseError { ParseError::ExpectedPaneIdMarker => write!(f, "Expected pane id marker"), ParseError::ExpectedInt(msg) => write!(f, "Expected an int: {}", msg), ParseError::ExpectedBool(msg) => write!(f, "Expected a bool: {}", msg), + ParseError::ExpectedString(msg) => write!(f, "Expected {}", msg), ParseError::ProcessFailure(msg) => write!(f, "{}", msg), } } diff --git a/src/lib.rs b/src/lib.rs index 2fc1f80..d951492 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,13 @@ use clap::Clap; +use std::collections::HashMap; use std::path; +use std::str::FromStr; pub mod alphabets; pub mod colors; pub mod error; +pub mod process; +pub mod regexes; pub mod state; pub mod view; @@ -11,8 +15,8 @@ pub mod view; /// /// # Note /// -/// Maybe the decision to move ownership is a bit bold. -pub fn run(buffer: String, opt: &Opt) -> String { +/// Maybe the decision to take ownership of the buffer is a bit bold. +pub fn run(buffer: String, opt: &CliOpt) -> Vec<(String, bool)> { let lines: Vec<&str> = buffer.split('\n').collect(); let mut state = state::State::new(&lines, &opt.alphabet, &opt.custom_regex); @@ -27,7 +31,6 @@ pub fn run(buffer: String, opt: &Opt) -> String { } }, }; - let uppercased_marker = opt.uppercased_marker; let selections: Vec<(String, bool)> = { let mut viewbox = view::View::new( @@ -43,32 +46,13 @@ pub fn run(buffer: String, opt: &Opt) -> String { viewbox.present() }; - // Early exit, signaling tmux we had no selections. - if selections.is_empty() { - std::process::exit(1); - } - - let output: String = if uppercased_marker { - selections - .iter() - .map(|(text, uppercased)| format!("{}:{}", *uppercased, text)) - .collect::>() - .join("\n") - } else { - selections - .iter() - .map(|(text, _)| text.as_str()) - .collect::>() - .join("\n") - }; - - output + selections } /// Main configuration, parsed from command line. #[derive(Clap, Debug)] #[clap(author, about, version)] -pub struct Opt { +pub struct CliOpt { /// Alphabet to draw hints from. /// /// Possible values are "{A}", "{A}-homerow", "{A}-left-hand", @@ -78,13 +62,20 @@ pub struct Opt { parse(try_from_str = alphabets::parse_alphabet))] alphabet: alphabets::Alphabet, + // /// Which existing regexes to use. + // #[clap(short = "x", long, arg_enum)] + // regex_id: Vec, + + // TODO: choose if pre-baked regexes is a good idea + // TODO: check if compiled regexes are possible + /// Additional regex patterns. + #[clap(short = "X", long)] + custom_regex: Vec, + /// Enable multi-selection. #[clap(short, long)] multi_selection: bool, - #[clap(flatten)] - colors: view::ViewColors, - /// Reverse the order for assigned hints. #[clap(short, long)] reverse: bool, @@ -93,14 +84,13 @@ pub struct Opt { #[clap(short, long)] unique_hint: bool, + #[clap(flatten)] + colors: view::ViewColors, + /// Align hint with its match. #[clap(short = "a", long, arg_enum, default_value = "leading")] hint_alignment: view::HintAlignment, - /// Additional regex patterns. - #[clap(short = "c", long)] - custom_regex: Vec, - /// Optional hint styling. /// /// Underline or surround the hint for increased visibility. @@ -121,7 +111,7 @@ pub struct Opt { /// indicating if hint key was uppercased. This is only used by /// tmux-copyrat, so it is hidden (skipped) from the CLI. #[clap(skip)] - uppercased_marker: bool, + pub uppercased_marker: bool, } /// Type introduced due to parsing limitation, @@ -132,6 +122,20 @@ enum HintStyleCli { Surround, } +impl FromStr for HintStyleCli { + type Err = error::ParseError; + + fn from_str(s: &str) -> Result { + match s { + "leading" => Ok(HintStyleCli::Underline), + "trailing" => Ok(HintStyleCli::Surround), + _ => Err(error::ParseError::ExpectedString(String::from( + "underline or surround", + ))), + } + } +} + fn parse_chars(src: &str) -> Result<(char, char), error::ParseError> { if src.len() != 2 { return Err(error::ParseError::ExpectedSurroundingPair); @@ -140,3 +144,47 @@ fn parse_chars(src: &str) -> Result<(char, char), error::ParseError> { let chars: Vec = src.chars().collect(); Ok((chars[0], chars[1])) } + +impl CliOpt { + /// Try parsing provided options, and update self with the valid values. + pub fn merge_map( + &mut self, + options: &HashMap, + ) -> Result<(), error::ParseError> { + for (name, value) in options { + match name.as_ref() { + "@copyrat-alphabet" => { + self.alphabet = alphabets::parse_alphabet(value)?; + } + "@copyrat-regex-id" => (), // TODO + "@copyrat-custom-regex" => self.custom_regex = vec![String::from(value)], + "@copyrat-multi-selection" => { + self.multi_selection = value.parse::()?; + } + "@copyrat-reverse" => { + self.reverse = value.parse::()?; + } + "@copyrat-unique-hint" => { + self.unique_hint = value.parse::()?; + } + + "@copyrat-match-fg" => self.colors.match_fg = colors::parse_color(value)?, + "@copyrat-match-bg" => self.colors.match_bg = colors::parse_color(value)?, + "@copyrat-focused-fg" => self.colors.focused_fg = colors::parse_color(value)?, + "@copyrat-focused-bg" => self.colors.focused_bg = colors::parse_color(value)?, + "@copyrat-hint-fg" => self.colors.hint_fg = colors::parse_color(value)?, + "@copyrat-hint-bg" => self.colors.hint_bg = colors::parse_color(value)?, + + "@copyrat-hint-alignment" => { + self.hint_alignment = view::HintAlignment::from_str(&value)? + } + "@copyrat-hint-style" => self.hint_style = Some(HintStyleCli::from_str(&value)?), + + // Ignore unknown options. + _ => (), + } + } + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index cec8f83..5c7f128 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,10 @@ use std::fs::OpenOptions; use std::io::prelude::*; use std::io::{self, Read}; -use copyrat::{run, Opt}; +use copyrat::{run, CliOpt}; fn main() { - let opt = Opt::parse(); + let opt = CliOpt::parse(); // Copy the pane contents (piped in via stdin) into a buffer, and split lines. let stdin = io::stdin(); @@ -17,7 +17,18 @@ fn main() { // Execute copyrat over the buffer (will take control over stdout). // This returns the selected matches. - let output: String = run(buffer, &opt); + let selections: Vec<(String, bool)> = run(buffer, &opt); + + // Early exit, signaling no selections were found. + if selections.is_empty() { + std::process::exit(1); + } + + let output = selections + .iter() + .map(|(text, _)| text.as_str()) + .collect::>() + .join("\n"); // Write output to a target_path if provided, else print to original stdout. match opt.target_path { diff --git a/src/process.rs b/src/process.rs new file mode 100644 index 0000000..af6a5f1 --- /dev/null +++ b/src/process.rs @@ -0,0 +1,21 @@ +use std::process::Command; + +use crate::error::ParseError; + +/// Execute an arbitrary Unix command and return the stdout as a `String` if +/// successful. +pub fn execute(command: &str, args: &Vec<&str>) -> Result { + let output = Command::new(command).args(args).output()?; + + if !output.status.success() { + let msg = String::from_utf8_lossy(&output.stderr); + return Err(ParseError::ProcessFailure(format!( + "Process failure: {} {}, error {}", + command, + args.join(" "), + msg + ))); + } + + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} diff --git a/src/regexes.rs b/src/regexes.rs new file mode 100644 index 0000000..a9bbd79 --- /dev/null +++ b/src/regexes.rs @@ -0,0 +1,25 @@ +pub const EXCLUDE_PATTERNS: [(&'static str, &'static str); 1] = + [("bash", r"[[:cntrl:]]\[([0-9]{1,2};)?([0-9]{1,2})?m")]; + +pub const PATTERNS: [(&'static str, &'static str); 14] = [ + ("markdown_url", r"\[[^]]*\]\(([^)]+)\)"), + ( + "url", + r"((https?://|git@|git://|ssh://|ftp://|file:///)[^ ]+)", + ), + ("diff_a", r"--- a/([^ ]+)"), + ("diff_b", r"\+\+\+ b/([^ ]+)"), + ("docker", r"sha256:([0-9a-f]{64})"), + ("path", r"(([.\w\-@~]+)?(/[.\w\-@]+)+)"), + ("color", r"#[0-9a-fA-F]{6}"), + ( + "uid", + r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + ), + ("ipfs", r"Qm[0-9a-zA-Z]{44}"), + ("sha", r"[0-9a-f]{7,40}"), + ("ip", r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"), + ("ipv6", r"[A-f0-9:]+:+[A-f0-9:]+[%\w\d]+"), + ("address", r"0x[0-9a-fA-F]+"), + ("number", r"[0-9]{4,}"), +]; diff --git a/src/state.rs b/src/state.rs index 69db1bc..f896186 100644 --- a/src/state.rs +++ b/src/state.rs @@ -3,445 +3,463 @@ use regex::Regex; use std::collections::HashMap; use std::fmt; -const EXCLUDE_PATTERNS: [(&'static str, &'static str); 1] = [("bash", r"[[:cntrl:]]\[([0-9]{1,2};)?([0-9]{1,2})?m")]; - -const PATTERNS: [(&'static str, &'static str); 14] = [ - ("markdown_url", r"\[[^]]*\]\(([^)]+)\)"), - ("url", r"((https?://|git@|git://|ssh://|ftp://|file:///)[^ ]+)"), - ("diff_a", r"--- a/([^ ]+)"), - ("diff_b", r"\+\+\+ b/([^ ]+)"), - ("docker", r"sha256:([0-9a-f]{64})"), - ("path", r"(([.\w\-@~]+)?(/[.\w\-@]+)+)"), - ("color", r"#[0-9a-fA-F]{6}"), - ("uid", r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"), - ("ipfs", r"Qm[0-9a-zA-Z]{44}"), - ("sha", r"[0-9a-f]{7,40}"), - ("ip", r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"), - ("ipv6", r"[A-f0-9:]+:+[A-f0-9:]+[%\w\d]+"), - ("address", r"0x[0-9a-fA-F]+"), - ("number", r"[0-9]{4,}"), -]; +use crate::regexes::{EXCLUDE_PATTERNS, PATTERNS}; #[derive(Clone)] pub struct Match<'a> { - pub x: i32, - pub y: i32, - pub pattern: &'a str, - pub text: &'a str, - pub hint: Option, + pub x: i32, + pub y: i32, + pub pattern: &'a str, + pub text: &'a str, + pub hint: Option, } impl<'a> fmt::Debug for Match<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "Match {{ x: {}, y: {}, pattern: {}, text: {}, hint: <{}> }}", - self.x, - self.y, - self.pattern, - self.text, - self.hint.clone().unwrap_or("".to_string()) - ) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Match {{ x: {}, y: {}, pattern: {}, text: {}, hint: <{}> }}", + self.x, + self.y, + self.pattern, + self.text, + self.hint.clone().unwrap_or("".to_string()) + ) + } } impl<'a> PartialEq for Match<'a> { - fn eq(&self, other: &Match) -> bool { - self.x == other.x && self.y == other.y - } + fn eq(&self, other: &Match) -> bool { + self.x == other.x && self.y == other.y + } } pub struct State<'a> { - pub lines: &'a Vec<&'a str>, - alphabet: &'a Alphabet, - regexp: &'a Vec, + pub lines: &'a Vec<&'a str>, + alphabet: &'a Alphabet, + regexp: &'a Vec, } impl<'a> State<'a> { - pub fn new(lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, regexp: &'a Vec) -> State<'a> { - State { - lines, - alphabet, - regexp, + pub fn new( + lines: &'a Vec<&'a str>, + alphabet: &'a Alphabet, + regexp: &'a Vec, + ) -> State<'a> { + State { + lines, + alphabet, + regexp, + } } - } - pub fn matches(&self, reverse: bool, unique: bool) -> Vec> { - let mut matches = Vec::new(); + pub fn matches(&self, reverse: bool, unique: bool) -> Vec> { + let mut matches = Vec::new(); - let exclude_patterns = EXCLUDE_PATTERNS - .iter() - .map(|tuple| (tuple.0, Regex::new(tuple.1).unwrap())) - .collect::>(); + let exclude_patterns = EXCLUDE_PATTERNS + .iter() + .map(|tuple| (tuple.0, Regex::new(tuple.1).unwrap())) + .collect::>(); - let custom_patterns = self - .regexp - .iter() - .map(|regexp| ("custom", Regex::new(regexp).expect("Invalid custom regexp"))) - .collect::>(); + let custom_patterns = self + .regexp + .iter() + .map(|regexp| ("custom", Regex::new(regexp).expect("Invalid custom regexp"))) + .collect::>(); - let patterns = PATTERNS - .iter() - .map(|tuple| (tuple.0, Regex::new(tuple.1).unwrap())) - .collect::>(); + let patterns = PATTERNS + .iter() + .map(|tuple| (tuple.0, Regex::new(tuple.1).unwrap())) + .collect::>(); - let all_patterns = [exclude_patterns, custom_patterns, patterns].concat(); + let all_patterns = [exclude_patterns, custom_patterns, patterns].concat(); - for (index, line) in self.lines.iter().enumerate() { - let mut chunk: &str = line; - let mut offset: i32 = 0; + for (index, line) in self.lines.iter().enumerate() { + let mut chunk: &str = line; + let mut offset: i32 = 0; - loop { - let submatches = all_patterns - .iter() - .filter_map(|tuple| match tuple.1.find_iter(chunk).nth(0) { - Some(m) => Some((tuple.0, tuple.1.clone(), m)), - None => None, - }) - .collect::>(); - let first_match_option = submatches.iter().min_by(|x, y| x.2.start().cmp(&y.2.start())); + loop { + let submatches = all_patterns + .iter() + .filter_map(|tuple| match tuple.1.find_iter(chunk).nth(0) { + Some(m) => Some((tuple.0, tuple.1.clone(), m)), + None => None, + }) + .collect::>(); + let first_match_option = submatches + .iter() + .min_by(|x, y| x.2.start().cmp(&y.2.start())); - if let Some(first_match) = first_match_option { - let (name, pattern, matching) = first_match; - let text = matching.as_str(); + if let Some(first_match) = first_match_option { + let (name, pattern, matching) = first_match; + let text = matching.as_str(); - if let Some(captures) = pattern.captures(text) { - let (subtext, substart) = if let Some(capture) = captures.get(1) { - (capture.as_str(), capture.start()) - } else { - (matching.as_str(), 0) - }; + if let Some(captures) = pattern.captures(text) { + let (subtext, substart) = if let Some(capture) = captures.get(1) { + (capture.as_str(), capture.start()) + } else { + (matching.as_str(), 0) + }; - // Never hint or broke bash color sequences - if *name != "bash" { - matches.push(Match { - x: offset + matching.start() as i32 + substart as i32, - y: index as i32, - pattern: name, - text: subtext, - hint: None, - }); + // Never hint or broke bash color sequences + if *name != "bash" { + matches.push(Match { + x: offset + matching.start() as i32 + substart as i32, + y: index as i32, + pattern: name, + text: subtext, + hint: None, + }); + } + + chunk = chunk.get(matching.end()..).expect("Unknown chunk"); + offset += matching.end() as i32; + } else { + panic!("No matching?"); + } + } else { + break; + } } + } - chunk = chunk.get(matching.end()..).expect("Unknown chunk"); - offset += matching.end() as i32; - } else { - panic!("No matching?"); - } + // let alphabet = super::alphabets::get_alphabet(self.alphabet); + let mut hints = self.alphabet.hints(matches.len()); + + // This looks wrong but we do a pop after + if !reverse { + hints.reverse(); } else { - break; + matches.reverse(); + hints.reverse(); } - } - } - // let alphabet = super::alphabets::get_alphabet(self.alphabet); - let mut hints = self.alphabet.hints(matches.len()); + if unique { + let mut previous: HashMap<&str, String> = HashMap::new(); - // This looks wrong but we do a pop after - if !reverse { - hints.reverse(); - } else { - matches.reverse(); - hints.reverse(); - } - - if unique { - let mut previous: HashMap<&str, String> = HashMap::new(); - - for mat in &mut matches { - if let Some(previous_hint) = previous.get(mat.text) { - mat.hint = Some(previous_hint.clone()); - } else if let Some(hint) = hints.pop() { - mat.hint = Some(hint.to_string().clone()); - previous.insert(mat.text, hint.to_string().clone()); + for mat in &mut matches { + if let Some(previous_hint) = previous.get(mat.text) { + mat.hint = Some(previous_hint.clone()); + } else if let Some(hint) = hints.pop() { + mat.hint = Some(hint.to_string().clone()); + previous.insert(mat.text, hint.to_string().clone()); + } + } + } else { + for mat in &mut matches { + if let Some(hint) = hints.pop() { + mat.hint = Some(hint.to_string().clone()); + } + } } - } - } else { - for mat in &mut matches { - if let Some(hint) = hints.pop() { - mat.hint = Some(hint.to_string().clone()); + + if reverse { + matches.reverse(); } - } - } - if reverse { - matches.reverse(); + matches } - - matches - } } #[cfg(test)] mod tests { - use super::*; - use crate::alphabets::Alphabet; + use super::*; + use crate::alphabets::Alphabet; - fn split(output: &str) -> Vec<&str> { - output.split("\n").collect::>() - } + fn split(output: &str) -> Vec<&str> { + output.split("\n").collect::>() + } - #[test] - fn match_reverse() { - let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_reverse() { + let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 3); - assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a"); - assert_eq!(results.last().unwrap().hint.clone().unwrap(), "c"); - } + assert_eq!(results.len(), 3); + assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a"); + assert_eq!(results.last().unwrap().hint.clone().unwrap(), "c"); + } - #[test] - fn match_unique() { - let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, true); + #[test] + fn match_unique() { + let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, true); - assert_eq!(results.len(), 3); - assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a"); - assert_eq!(results.last().unwrap().hint.clone().unwrap(), "a"); - } + assert_eq!(results.len(), 3); + assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a"); + assert_eq!(results.last().unwrap().hint.clone().unwrap(), "a"); + } - #[test] - fn match_docker() { - let lines = split("latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_docker() { + let lines = split("latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 1); - assert_eq!( - results.get(0).unwrap().text, - "30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4" - ); - } + assert_eq!(results.len(), 1); + assert_eq!( + results.get(0).unwrap().text, + "30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4" + ); + } - #[test] - fn match_bash() { - let lines = split("path: /var/log/nginx.log\npath: test/log/nginx-2.log:32folder/.nginx@4df2.log"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_bash() { + let lines = split("path: /var/log/nginx.log\npath: test/log/nginx-2.log:32folder/.nginx@4df2.log"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 3); - assert_eq!(results.get(0).unwrap().text, "/var/log/nginx.log"); - assert_eq!(results.get(1).unwrap().text, "test/log/nginx-2.log"); - assert_eq!(results.get(2).unwrap().text, "folder/.nginx@4df2.log"); - } + assert_eq!(results.len(), 3); + assert_eq!(results.get(0).unwrap().text, "/var/log/nginx.log"); + assert_eq!(results.get(1).unwrap().text, "test/log/nginx-2.log"); + assert_eq!(results.get(2).unwrap().text, "folder/.nginx@4df2.log"); + } - #[test] - fn match_paths() { - let lines = split("Lorem /tmp/foo/bar_lol, lorem\n Lorem /var/log/boot-strap.log lorem ../log/kern.log lorem"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_paths() { + let lines = split("Lorem /tmp/foo/bar_lol, lorem\n Lorem /var/log/boot-strap.log lorem ../log/kern.log lorem"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 3); - assert_eq!(results.get(0).unwrap().text.clone(), "/tmp/foo/bar_lol"); - assert_eq!(results.get(1).unwrap().text.clone(), "/var/log/boot-strap.log"); - assert_eq!(results.get(2).unwrap().text.clone(), "../log/kern.log"); - } + assert_eq!(results.len(), 3); + assert_eq!(results.get(0).unwrap().text.clone(), "/tmp/foo/bar_lol"); + assert_eq!( + results.get(1).unwrap().text.clone(), + "/var/log/boot-strap.log" + ); + assert_eq!(results.get(2).unwrap().text.clone(), "../log/kern.log"); + } - #[test] - fn match_home() { - let lines = split("Lorem ~/.gnu/.config.txt, lorem"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_home() { + let lines = split("Lorem ~/.gnu/.config.txt, lorem"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 1); - assert_eq!(results.get(0).unwrap().text.clone(), "~/.gnu/.config.txt"); - } + assert_eq!(results.len(), 1); + assert_eq!(results.get(0).unwrap().text.clone(), "~/.gnu/.config.txt"); + } - #[test] - fn match_uids() { - let lines = split("Lorem ipsum 123e4567-e89b-12d3-a456-426655440000 lorem\n Lorem lorem lorem"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_uids() { + let lines = + split("Lorem ipsum 123e4567-e89b-12d3-a456-426655440000 lorem\n Lorem lorem lorem"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 1); - } + assert_eq!(results.len(), 1); + } - #[test] - fn match_shas() { - let lines = split("Lorem fd70b5695 5246ddf f924213 lorem\n Lorem 973113963b491874ab2e372ee60d4b4cb75f717c lorem"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_shas() { + let lines = split("Lorem fd70b5695 5246ddf f924213 lorem\n Lorem 973113963b491874ab2e372ee60d4b4cb75f717c lorem"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 4); - assert_eq!(results.get(0).unwrap().text.clone(), "fd70b5695"); - assert_eq!(results.get(1).unwrap().text.clone(), "5246ddf"); - assert_eq!(results.get(2).unwrap().text.clone(), "f924213"); - assert_eq!( - results.get(3).unwrap().text.clone(), - "973113963b491874ab2e372ee60d4b4cb75f717c" - ); - } + assert_eq!(results.len(), 4); + assert_eq!(results.get(0).unwrap().text.clone(), "fd70b5695"); + assert_eq!(results.get(1).unwrap().text.clone(), "5246ddf"); + assert_eq!(results.get(2).unwrap().text.clone(), "f924213"); + assert_eq!( + results.get(3).unwrap().text.clone(), + "973113963b491874ab2e372ee60d4b4cb75f717c" + ); + } - #[test] - fn match_ips() { - let lines = split("Lorem ipsum 127.0.0.1 lorem\n Lorem 255.255.10.255 lorem 127.0.0.1 lorem"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_ips() { + let lines = + split("Lorem ipsum 127.0.0.1 lorem\n Lorem 255.255.10.255 lorem 127.0.0.1 lorem"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 3); - assert_eq!(results.get(0).unwrap().text.clone(), "127.0.0.1"); - assert_eq!(results.get(1).unwrap().text.clone(), "255.255.10.255"); - assert_eq!(results.get(2).unwrap().text.clone(), "127.0.0.1"); - } + assert_eq!(results.len(), 3); + assert_eq!(results.get(0).unwrap().text.clone(), "127.0.0.1"); + assert_eq!(results.get(1).unwrap().text.clone(), "255.255.10.255"); + assert_eq!(results.get(2).unwrap().text.clone(), "127.0.0.1"); + } - #[test] - fn match_ipv6s() { - let lines = split("Lorem ipsum fe80::2:202:fe4 lorem\n Lorem 2001:67c:670:202:7ba8:5e41:1591:d723 lorem fe80::2:1 lorem ipsum fe80:22:312:fe::1%eth0"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_ipv6s() { + let lines = split("Lorem ipsum fe80::2:202:fe4 lorem\n Lorem 2001:67c:670:202:7ba8:5e41:1591:d723 lorem fe80::2:1 lorem ipsum fe80:22:312:fe::1%eth0"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 4); - assert_eq!(results.get(0).unwrap().text.clone(), "fe80::2:202:fe4"); - assert_eq!( - results.get(1).unwrap().text.clone(), - "2001:67c:670:202:7ba8:5e41:1591:d723" - ); - assert_eq!(results.get(2).unwrap().text.clone(), "fe80::2:1"); - assert_eq!(results.get(3).unwrap().text.clone(), "fe80:22:312:fe::1%eth0"); - } + assert_eq!(results.len(), 4); + assert_eq!(results.get(0).unwrap().text.clone(), "fe80::2:202:fe4"); + assert_eq!( + results.get(1).unwrap().text.clone(), + "2001:67c:670:202:7ba8:5e41:1591:d723" + ); + assert_eq!(results.get(2).unwrap().text.clone(), "fe80::2:1"); + assert_eq!( + results.get(3).unwrap().text.clone(), + "fe80:22:312:fe::1%eth0" + ); + } - #[test] - fn match_markdown_urls() { - let lines = split("Lorem ipsum [link](https://github.io?foo=bar) ![](http://cdn.com/img.jpg) lorem"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_markdown_urls() { + let lines = split( + "Lorem ipsum [link](https://github.io?foo=bar) ![](http://cdn.com/img.jpg) lorem", + ); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 2); - assert_eq!(results.get(0).unwrap().pattern.clone(), "markdown_url"); - assert_eq!(results.get(0).unwrap().text.clone(), "https://github.io?foo=bar"); - assert_eq!(results.get(1).unwrap().pattern.clone(), "markdown_url"); - assert_eq!(results.get(1).unwrap().text.clone(), "http://cdn.com/img.jpg"); - } + assert_eq!(results.len(), 2); + assert_eq!(results.get(0).unwrap().pattern.clone(), "markdown_url"); + assert_eq!( + results.get(0).unwrap().text.clone(), + "https://github.io?foo=bar" + ); + assert_eq!(results.get(1).unwrap().pattern.clone(), "markdown_url"); + assert_eq!( + results.get(1).unwrap().text.clone(), + "http://cdn.com/img.jpg" + ); + } - #[test] - fn match_urls() { - let lines = split("Lorem ipsum https://www.rust-lang.org/tools lorem\n Lorem ipsumhttps://crates.io lorem https://github.io?foo=bar lorem ssh://github.io"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_urls() { + let lines = split("Lorem ipsum https://www.rust-lang.org/tools lorem\n Lorem ipsumhttps://crates.io lorem https://github.io?foo=bar lorem ssh://github.io"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 4); - assert_eq!(results.get(0).unwrap().text.clone(), "https://www.rust-lang.org/tools"); - assert_eq!(results.get(0).unwrap().pattern.clone(), "url"); - assert_eq!(results.get(1).unwrap().text.clone(), "https://crates.io"); - assert_eq!(results.get(1).unwrap().pattern.clone(), "url"); - assert_eq!(results.get(2).unwrap().text.clone(), "https://github.io?foo=bar"); - assert_eq!(results.get(2).unwrap().pattern.clone(), "url"); - assert_eq!(results.get(3).unwrap().text.clone(), "ssh://github.io"); - assert_eq!(results.get(3).unwrap().pattern.clone(), "url"); - } + assert_eq!(results.len(), 4); + assert_eq!( + results.get(0).unwrap().text.clone(), + "https://www.rust-lang.org/tools" + ); + assert_eq!(results.get(0).unwrap().pattern.clone(), "url"); + assert_eq!(results.get(1).unwrap().text.clone(), "https://crates.io"); + assert_eq!(results.get(1).unwrap().pattern.clone(), "url"); + assert_eq!( + results.get(2).unwrap().text.clone(), + "https://github.io?foo=bar" + ); + assert_eq!(results.get(2).unwrap().pattern.clone(), "url"); + assert_eq!(results.get(3).unwrap().text.clone(), "ssh://github.io"); + assert_eq!(results.get(3).unwrap().pattern.clone(), "url"); + } - #[test] - fn match_addresses() { - let lines = split("Lorem 0xfd70b5695 0x5246ddf lorem\n Lorem 0x973113tlorem"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_addresses() { + let lines = split("Lorem 0xfd70b5695 0x5246ddf lorem\n Lorem 0x973113tlorem"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 3); - assert_eq!(results.get(0).unwrap().text.clone(), "0xfd70b5695"); - assert_eq!(results.get(1).unwrap().text.clone(), "0x5246ddf"); - assert_eq!(results.get(2).unwrap().text.clone(), "0x973113"); - } + assert_eq!(results.len(), 3); + assert_eq!(results.get(0).unwrap().text.clone(), "0xfd70b5695"); + assert_eq!(results.get(1).unwrap().text.clone(), "0x5246ddf"); + assert_eq!(results.get(2).unwrap().text.clone(), "0x973113"); + } - #[test] - fn match_hex_colors() { - let lines = split("Lorem #fd7b56 lorem #FF00FF\n Lorem #00fF05 lorem #abcd00 lorem #afRR00"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_hex_colors() { + let lines = + split("Lorem #fd7b56 lorem #FF00FF\n Lorem #00fF05 lorem #abcd00 lorem #afRR00"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 4); - assert_eq!(results.get(0).unwrap().text.clone(), "#fd7b56"); - assert_eq!(results.get(1).unwrap().text.clone(), "#FF00FF"); - assert_eq!(results.get(2).unwrap().text.clone(), "#00fF05"); - assert_eq!(results.get(3).unwrap().text.clone(), "#abcd00"); - } + assert_eq!(results.len(), 4); + assert_eq!(results.get(0).unwrap().text.clone(), "#fd7b56"); + assert_eq!(results.get(1).unwrap().text.clone(), "#FF00FF"); + assert_eq!(results.get(2).unwrap().text.clone(), "#00fF05"); + assert_eq!(results.get(3).unwrap().text.clone(), "#abcd00"); + } - #[test] - fn match_ipfs() { - let lines = split("Lorem QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ lorem Qmfoobar"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_ipfs() { + let lines = split("Lorem QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ lorem Qmfoobar"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 1); - assert_eq!( - results.get(0).unwrap().text.clone(), - "QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ" - ); - } + assert_eq!(results.len(), 1); + assert_eq!( + results.get(0).unwrap().text.clone(), + "QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ" + ); + } - #[test] - fn match_process_port() { - let lines = + #[test] + fn match_process_port() { + let lines = split("Lorem 5695 52463 lorem\n Lorem 973113 lorem 99999 lorem 8888 lorem\n 23456 lorem 5432 lorem 23444"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 8); - } + assert_eq!(results.len(), 8); + } - #[test] - fn match_diff_a() { - let lines = split("Lorem lorem\n--- a/src/main.rs"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_diff_a() { + let lines = split("Lorem lorem\n--- a/src/main.rs"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 1); - assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs"); - } + assert_eq!(results.len(), 1); + assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs"); + } - #[test] - fn match_diff_b() { - let lines = split("Lorem lorem\n+++ b/src/main.rs"); - let custom = [].to_vec(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + #[test] + fn match_diff_b() { + let lines = split("Lorem lorem\n+++ b/src/main.rs"); + let custom = [].to_vec(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 1); - assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs"); - } + assert_eq!(results.len(), 1); + assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs"); + } - #[test] - fn priority() { - let lines = split("Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"); + #[test] + fn priority() { + let lines = split("Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"); - let custom: Vec = ["CUSTOM-[0-9]{4,}", "ISSUE-[0-9]{3}"] - .iter() - .map(|&s| s.to_string()) - .collect(); - let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let custom: Vec = ["CUSTOM-[0-9]{4,}", "ISSUE-[0-9]{3}"] + .iter() + .map(|&s| s.to_string()) + .collect(); + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &custom).matches(false, false); - assert_eq!(results.len(), 9); - assert_eq!(results.get(0).unwrap().text.clone(), "http://foo.bar"); - assert_eq!(results.get(1).unwrap().text.clone(), "CUSTOM-52463"); - assert_eq!(results.get(2).unwrap().text.clone(), "ISSUE-123"); - assert_eq!(results.get(3).unwrap().text.clone(), "/var/fd70b569/9999.log"); - assert_eq!(results.get(4).unwrap().text.clone(), "52463"); - assert_eq!(results.get(5).unwrap().text.clone(), "973113"); - assert_eq!( - results.get(6).unwrap().text.clone(), - "123e4567-e89b-12d3-a456-426655440000" - ); - assert_eq!(results.get(7).unwrap().text.clone(), "8888"); - assert_eq!(results.get(8).unwrap().text.clone(), "https://crates.io/23456/fd70b569"); - } + assert_eq!(results.len(), 9); + assert_eq!(results.get(0).unwrap().text.clone(), "http://foo.bar"); + assert_eq!(results.get(1).unwrap().text.clone(), "CUSTOM-52463"); + assert_eq!(results.get(2).unwrap().text.clone(), "ISSUE-123"); + assert_eq!( + results.get(3).unwrap().text.clone(), + "/var/fd70b569/9999.log" + ); + assert_eq!(results.get(4).unwrap().text.clone(), "52463"); + assert_eq!(results.get(5).unwrap().text.clone(), "973113"); + assert_eq!( + results.get(6).unwrap().text.clone(), + "123e4567-e89b-12d3-a456-426655440000" + ); + assert_eq!(results.get(7).unwrap().text.clone(), "8888"); + assert_eq!( + results.get(8).unwrap().text.clone(), + "https://crates.io/23456/fd70b569" + ); + } } diff --git a/src/tmux.rs b/src/tmux.rs index 48f4447..dece861 100644 --- a/src/tmux.rs +++ b/src/tmux.rs @@ -1,26 +1,10 @@ use clap::Clap; -use copyrat::error::ParseError; use regex::Regex; use std::collections::HashMap; -use std::process::Command; +use std::str::FromStr; -/// Execute an arbitrary Unix command and return the stdout as a `String` if -/// successful. -fn execute(command: &str, args: &Vec<&str>) -> Result { - let output = Command::new(command).args(args).output()?; - - if !output.status.success() { - let msg = String::from_utf8_lossy(&output.stderr); - return Err(ParseError::ProcessFailure(format!( - "Process failure: {} {}, error {}", - command, - args.join(" "), - msg - ))); - } - - Ok(String::from_utf8_lossy(&output.stdout).to_string()) -} +use copyrat::error::ParseError; +use copyrat::process; #[derive(Debug, PartialEq)] pub struct Pane { @@ -97,7 +81,7 @@ pub fn list_panes() -> Result, ParseError> { "#{pane_id}:#{?pane_in_mode,1,0}:#{pane_height}:#{scroll_position}:#{?pane_active,active,nope}", ]; - let output = execute("tmux", &args)?; + let output = process::execute("tmux", &args)?; // Each call to `Pane::parse` returns a `Result`. All results // are collected into a Result, _>, thanks to `collect()`. @@ -116,7 +100,7 @@ pub fn list_panes() -> Result, ParseError> { pub fn get_options(prefix: &str) -> Result, ParseError> { let args = vec!["show", "-g"]; - let output = execute("tmux", &args)?; + let output = process::execute("tmux", &args)?; let lines: Vec<&str> = output.split('\n').collect(); let pattern = format!(r#"{prefix}([\w\-0-9]+) "?(\w+)"?"#, prefix = prefix); @@ -192,6 +176,20 @@ pub enum CaptureRegion { //Region(i32, i32), } +impl FromStr for CaptureRegion { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + match s { + "leading" => Ok(CaptureRegion::EntireHistory), + "trailing" => Ok(CaptureRegion::VisibleArea), + _ => Err(ParseError::ExpectedString(String::from( + "entire-history or visible-area", + ))), + } + } +} + /// Returns the entire Pane content as a `String`. /// /// `CaptureRegion` specifies if the visible area is captured, or the entire @@ -224,7 +222,7 @@ pub fn capture_pane(pane: &Pane, region: &CaptureRegion) -> Result = args.split(' ').collect(); - let output = execute("tmux", &args)?; + let output = process::execute("tmux", &args)?; Ok(output) // format!( diff --git a/src/view.rs b/src/view.rs index f0ac6b9..c3eb5e6 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,8 +1,10 @@ use super::{colors, state}; +use crate::error::ParseError; use clap::Clap; use std::char; use std::io::{stdout, Read, Write}; +use std::str::FromStr; use termion::async_stdin; use termion::event::Key; use termion::input::TermRead; @@ -30,32 +32,32 @@ pub struct ViewColors { /// Foreground color for matches. #[clap(long, default_value = "green", parse(try_from_str = colors::parse_color))] - match_fg: Box, + pub match_fg: Box, /// Background color for matches. #[clap(long, default_value = "black", parse(try_from_str = colors::parse_color))] - match_bg: Box, + pub match_bg: Box, /// Foreground color for the focused match. #[clap(long, default_value = "blue", parse(try_from_str = colors::parse_color))] - focused_fg: Box, + pub focused_fg: Box, /// Background color for the focused match. #[clap(long, default_value = "black", parse(try_from_str = colors::parse_color))] - focused_bg: Box, + pub focused_bg: Box, /// Foreground color for hints. #[clap(long, default_value = "white", parse(try_from_str = colors::parse_color))] - hint_fg: Box, + pub hint_fg: Box, /// Background color for hints. #[clap(long, default_value = "black", parse(try_from_str = colors::parse_color))] - hint_bg: Box, + pub hint_bg: Box, } /// Describes if, during rendering, a hint should aligned to the leading edge of @@ -66,6 +68,20 @@ pub enum HintAlignment { Trailing, } +impl FromStr for HintAlignment { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + 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. /// From 50391320ee08c5a110ff3de8df691b216b0fcd78 Mon Sep 17 00:00:00 2001 From: graelo Date: Thu, 28 May 2020 07:07:51 +0200 Subject: [PATCH 09/40] refactor: refactor --- src/bridge.rs | 47 ++++++++++++++++++++++++++++++++--------- src/lib.rs | 2 ++ src/tmux.rs | 53 +++++++++++++++++++++++++++++++++++++++++++---- src/view.rs | 17 +++++++++++++++ tmux-copyrat.tmux | 15 ++++++++++++++ tmux-thumbs.tmux | 15 -------------- 6 files changed, 120 insertions(+), 29 deletions(-) create mode 100755 tmux-copyrat.tmux delete mode 100755 tmux-thumbs.tmux diff --git a/src/bridge.rs b/src/bridge.rs index b33f95e..8546dd2 100644 --- a/src/bridge.rs +++ b/src/bridge.rs @@ -351,11 +351,11 @@ mod tests { #[clap(author, about, version)] struct BridgeOpt { /// Command to execute on selection. - #[clap(short, long, default_value = "tmux set-buffer {}")] + #[clap(long, default_value = "tmux set-buffer {}")] command: String, /// Command to execute on uppercased selection. - #[clap(short, long, default_value = "tmux set-buffer {} && tmux-paste-buffer")] + #[clap(long, default_value = "tmux set-buffer {} && tmux-paste-buffer")] alt_command: String, /// Retrieve options from tmux. @@ -364,7 +364,7 @@ struct BridgeOpt { /// You should consider reading them from the config file (the default /// option) as this saves both a command call (about 10ms) and a Regex /// compilation. - #[clap(long)] + #[clap(short = "T", long)] get_options_from_tmux: bool, /// Optionally capture entire pane history. @@ -415,6 +415,7 @@ fn main() -> Result<(), error::ParseError> { } let panes: Vec = tmux::list_panes()?; + let active_pane = panes .iter() .find(|p| p.is_active) @@ -423,22 +424,48 @@ fn main() -> Result<(), error::ParseError> { let buffer = tmux::capture_pane(&active_pane, &opt.capture)?; let selections: Vec<(String, bool)> = if active_pane.in_mode { - // TODO: fancy stuff - vec![(String::new(), false)] + // If the current pane is in copy mode, we have to dance a little with + // Panes, because the current pane has already locked the Alternate + // Screen, preventing copyrat::run to execute. + let initial_pane = active_pane; + + // Create a new window without switching to it. + let temp_pane: tmux::Pane = tmux::create_new_window("[copyrat]")?; + + // Swap the two panes, changing the active pane to be the temp_pane. + // After swap, temp_pane has the same height than the initial_pane + // had before being swapped. + tmux::swap_panes(&initial_pane, &temp_pane)?; + + // Running copyrat now will render in the newly created temp_pane + // (locking stdin, writing to its stdout), but this is almost + // transparent to the user. + let selections = copyrat::run(buffer, &opt.cli_options); + + // Swap back the two panes, making initial_pane the active one again. + tmux::swap_panes(&temp_pane, &initial_pane)?; + + tmux::kill_pane(&temp_pane)?; + + selections } else { copyrat::run(buffer, &opt.cli_options) }; - selections.iter().for_each(|(text, b)| { - let command = if *b { + // Execute a command on each selection. + // TODO: consider getting rid of multi-selection mode. + selections.iter().for_each(|(text, uppercased)| { + let raw_command = if *uppercased { opt.alt_command.replace("{}", text) } else { opt.command.replace("{}", text) }; - let args: Vec<&str> = vec![]; + let mut it = raw_command.split(' ').into_iter(); + let command = it.next().unwrap(); + let args: Vec<&str> = it.collect(); - // Simply execute the command as is and don't mind about potential - // errors. + // Simply execute the command as is, and let the program crash on + // potential errors because it is not our responsibility. process::execute(&command, &args).unwrap(); }); diff --git a/src/lib.rs b/src/lib.rs index d951492..be776fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ pub fn run(buffer: String, opt: &CliOpt) -> Vec<(String, bool)> { let hint_style = match &opt.hint_style { None => None, Some(style) => match style { + HintStyleCli::Italic => Some(view::HintStyle::Italic), HintStyleCli::Underline => Some(view::HintStyle::Underline), HintStyleCli::Surround => { let (open, close) = opt.hint_surroundings; @@ -118,6 +119,7 @@ pub struct CliOpt { /// as we cannot directly parse into view::HintStyle. #[derive(Debug, Clap)] enum HintStyleCli { + Italic, Underline, Surround, } diff --git a/src/tmux.rs b/src/tmux.rs index dece861..313124c 100644 --- a/src/tmux.rs +++ b/src/tmux.rs @@ -78,15 +78,18 @@ pub fn list_panes() -> Result, ParseError> { let args = vec![ "list-panes", "-F", - "#{pane_id}:#{?pane_in_mode,1,0}:#{pane_height}:#{scroll_position}:#{?pane_active,active,nope}", + "#{pane_id}:#{?pane_in_mode,true,false}:#{pane_height}:#{scroll_position}:#{?pane_active,true,false}", ]; let output = process::execute("tmux", &args)?; // Each call to `Pane::parse` returns a `Result`. All results // are collected into a Result, _>, thanks to `collect()`. - let result: Result, ParseError> = - output.split('\n').map(|line| Pane::parse(line)).collect(); + let result: Result, ParseError> = output + .trim_end() // trim last '\n' as it would create an empty line + .split('\n') + .map(|line| Pane::parse(line)) + .collect(); result } @@ -203,7 +206,7 @@ impl FromStr for CaptureRegion { /// position. To support both cases, the implementation always provides those /// parameters to tmux. pub fn capture_pane(pane: &Pane, region: &CaptureRegion) -> Result { - let mut args = format!("capture-pane -t {id} -p", id = pane.id); + let mut args = format!("capture-pane -t %{id} -p", id = pane.id); let region_str = match region { CaptureRegion::VisibleArea => { @@ -231,6 +234,48 @@ pub fn capture_pane(pane: &Pane, region: &CaptureRegion) -> Result Result { + let args = vec!["new-window", "-P", "-d", "-n", name, "-F", + "#{pane_id}:#{?pane_in_mode,true,false}:#{pane_height}:#{scroll_position}:#{?pane_active,true,false}"]; + + let output = process::execute("tmux", &args)?; + + let pane = Pane::parse(output.trim_end())?; // trim last '\n' as it would create an empty line + + Ok(pane) +} + +/// Ask tmux to swap two `Pane`s and change the active pane to be the target +/// `Pane`. +pub fn swap_panes(pane_a: &Pane, pane_b: &Pane) -> Result<(), ParseError> { + let pa_id = format!("%{}", pane_a.id); + let pb_id = format!("%{}", pane_b.id); + + let args = vec!["swap-pane", "-s", &pa_id, "-t", &pb_id]; + + process::execute("tmux", &args)?; + + Ok(()) +} + +/// Ask tmux to kill the provided `Pane`. +pub fn kill_pane(pane: &Pane) -> Result<(), ParseError> { + let p_id = format!("%{}", pane.id); + + let args = vec!["kill-pane", "-t", &p_id]; + + process::execute("tmux", &args)?; + + Ok(()) +} + #[cfg(test)] mod tests { use super::Pane; diff --git a/src/view.rs b/src/view.rs index c3eb5e6..ce27a8a 100644 --- a/src/view.rs +++ b/src/view.rs @@ -88,6 +88,8 @@ impl FromStr for HintAlignment { /// # 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 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. @@ -234,6 +236,21 @@ impl<'a> View<'a> { .unwrap(); } Some(hint_style) => match hint_style { + HintStyle::Italic => { + write!( + stdout, + "{goto}{bg_color}{fg_color}{sty}{hint}{sty_reset}{fg_reset}{bg_reset}", + goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + 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(); + } HintStyle::Underline => { write!( stdout, diff --git a/tmux-copyrat.tmux b/tmux-copyrat.tmux new file mode 100755 index 0000000..9409dc9 --- /dev/null +++ b/tmux-copyrat.tmux @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +DEFAULT_COPYRAT_KEY="space" +COPYRAT_KEY=$(tmux show-option -gqv @copyrat-key) +COPYRAT_KEY=${COPYRAT_KEY:-$DEFAULT_COPYRAT_KEY} + +BINARY="${CURRENT_DIR}/target/release/tmux-copyrat" + +tmux bind-key $COPYRAT_KEY run-shell -b "${BINARY} -T" + +if [ ! -f "$BINARY" ]; then + cd "${CURRENT_DIR}" && cargo build --release +fi diff --git a/tmux-thumbs.tmux b/tmux-thumbs.tmux deleted file mode 100755 index d879bdc..0000000 --- a/tmux-thumbs.tmux +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -DEFAULT_THUMBS_KEY="space" -THUMBS_KEY=$(tmux show-option -gqv @thumbs-key) -THUMBS_KEY=${THUMBS_KEY:-$DEFAULT_THUMBS_KEY} - -tmux bind-key $THUMBS_KEY run-shell -b "${CURRENT_DIR}/tmux-thumbs.sh" - -BINARY="${CURRENT_DIR}/target/release/thumbs" - -if [ ! -f "$BINARY" ]; then - cd "${CURRENT_DIR}" && cargo build --release -fi From 58ceb190b6863a45afb615bb366bc034bceb647a Mon Sep 17 00:00:00 2001 From: graelo Date: Thu, 28 May 2020 09:24:33 +0200 Subject: [PATCH 10/40] refactor: refactor --- src/bridge.rs | 9 +++++---- src/lib.rs | 4 +--- src/tmux.rs | 55 ++++++++++----------------------------------------- src/view.rs | 53 +++++++++++++++++++++++++++---------------------- 4 files changed, 45 insertions(+), 76 deletions(-) diff --git a/src/bridge.rs b/src/bridge.rs index 8546dd2..dd9be5c 100644 --- a/src/bridge.rs +++ b/src/bridge.rs @@ -429,13 +429,14 @@ fn main() -> Result<(), error::ParseError> { // Screen, preventing copyrat::run to execute. let initial_pane = active_pane; - // Create a new window without switching to it. - let temp_pane: tmux::Pane = tmux::create_new_window("[copyrat]")?; + // Create a new window without switching to it, with a `sh` command + // for faster startup. + let temp_pane: tmux::Pane = tmux::create_new_window("[copyrat]", "sh")?; // Swap the two panes, changing the active pane to be the temp_pane. // After swap, temp_pane has the same height than the initial_pane // had before being swapped. - tmux::swap_panes(&initial_pane, &temp_pane)?; + tmux::swap_panes(initial_pane, &temp_pane)?; // Running copyrat now will render in the newly created temp_pane // (locking stdin, writing to its stdout), but this is almost @@ -443,7 +444,7 @@ fn main() -> Result<(), error::ParseError> { let selections = copyrat::run(buffer, &opt.cli_options); // Swap back the two panes, making initial_pane the active one again. - tmux::swap_panes(&temp_pane, &initial_pane)?; + tmux::swap_panes(&temp_pane, initial_pane)?; tmux::kill_pane(&temp_pane)?; diff --git a/src/lib.rs b/src/lib.rs index be776fe..54096f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,9 +66,7 @@ pub struct CliOpt { // /// Which existing regexes to use. // #[clap(short = "x", long, arg_enum)] // regex_id: Vec, - - // TODO: choose if pre-baked regexes is a good idea - // TODO: check if compiled regexes are possible + // /// Additional regex patterns. #[clap(short = "X", long)] custom_regex: Vec, diff --git a/src/tmux.rs b/src/tmux.rs index 313124c..2c009f6 100644 --- a/src/tmux.rs +++ b/src/tmux.rs @@ -124,47 +124,6 @@ pub fn get_options(prefix: &str) -> Result, ParseError> Ok(args) } -// pub fn toto() { -// let options_command = vec!["tmux", "show", "-g"]; -// let params: Vec = options_command.iter().map(|arg| arg.to_string()).collect(); -// let options = self.executor.execute(params); -// let lines: Vec<&str> = options.split('\n').collect(); - -// let pattern = Regex::new(r#"@thumbs-([\w\-0-9]+) "?(\w+)"?"#).unwrap(); - -// let args = lines -// .iter() -// .flat_map(|line| { -// if let Some(captures) = pattern.captures(line) { -// let name = captures.get(1).unwrap().as_str(); -// let value = captures.get(2).unwrap().as_str(); - -// let boolean_params = vec!["reverse", "unique", "contrast"]; - -// if boolean_params.iter().any(|&x| x == name) { -// return vec![format!("--{}", name)]; -// } - -// let string_params = vec![ -// "position", -// "fg-color", -// "bg-color", -// "hint-bg-color", -// "hint-fg-color", -// "select-fg-color", -// "select-bg-color", -// ]; - -// if string_params.iter().any(|&x| x == name) { -// return vec![format!("--{}", name), format!("'{}'", value)]; -// } - -// if name.starts_with("regexp") { -// return vec!["--regexp".to_string(), format!("'{}'", value)]; -// } -// } -// };} - #[derive(Clap, Debug)] pub enum CaptureRegion { /// The entire history. @@ -198,6 +157,10 @@ impl FromStr for CaptureRegion { /// `CaptureRegion` specifies if the visible area is captured, or the entire /// history. /// +/// # TODO +/// +/// Capture with `capture-pane -J` joins wrapped lines. +/// /// # Note /// /// If the pane is in normal mode, capturing the visible area can be done @@ -234,16 +197,18 @@ pub fn capture_pane(pane: &Pane, region: &CaptureRegion) -> Result Result { +pub fn create_new_window(name: &str, command: &str) -> Result { let args = vec!["new-window", "-P", "-d", "-n", name, "-F", - "#{pane_id}:#{?pane_in_mode,true,false}:#{pane_height}:#{scroll_position}:#{?pane_active,true,false}"]; + "#{pane_id}:#{?pane_in_mode,true,false}:#{pane_height}:#{scroll_position}:#{?pane_active,true,false}", + command]; let output = process::execute("tmux", &args)?; diff --git a/src/view.rs b/src/view.rs index ce27a8a..bf5bb25 100644 --- a/src/view.rs +++ b/src/view.rs @@ -3,14 +3,9 @@ use super::{colors, state}; use crate::error::ParseError; use clap::Clap; use std::char; -use std::io::{stdout, Read, Write}; +use std::io; use std::str::FromStr; -use termion::async_stdin; -use termion::event::Key; -use termion::input::TermRead; -use termion::raw::IntoRawMode; -use termion::screen::AlternateScreen; -use termion::{color, cursor, style}; +use termion::{self, color, cursor, event, style}; pub struct View<'a> { state: &'a mut state::State<'a>, @@ -149,7 +144,7 @@ impl<'a> View<'a> { /// # Notes /// - All trailing whitespaces are trimmed, empty lines are skipped. /// - This writes directly on the writer, avoiding extra allocation. - fn render_lines(stdout: &mut dyn Write, lines: &Vec<&str>) -> () { + fn render_lines(stdout: &mut dyn io::Write, lines: &Vec<&str>) -> () { for (index, line) in lines.iter().enumerate() { let trimmed_line = line.trim_end(); @@ -172,7 +167,7 @@ impl<'a> View<'a> { /// # Note /// This writes directly on the writer, avoiding extra allocation. fn render_matched_text( - stdout: &mut dyn Write, + stdout: &mut dyn io::Write, text: &str, focused: bool, offset: (usize, usize), @@ -210,7 +205,7 @@ impl<'a> View<'a> { /// # Note /// This writes directly on the writer, avoiding extra allocation. fn render_matched_hint( - stdout: &mut dyn Write, + stdout: &mut dyn io::Write, hint_text: &str, offset: (usize, usize), colors: &ViewColors, @@ -299,7 +294,7 @@ impl<'a> View<'a> { /// # Note /// Multibyte characters are taken into account, so that the Match's `text` /// and `hint` are rendered in their proper position. - fn render(&self, stdout: &mut dyn Write) -> () { + fn render(&self, stdout: &mut dyn io::Write) -> () { write!(stdout, "{}", cursor::Hide).unwrap(); // 1. Trim all lines and render non-empty ones. @@ -359,7 +354,9 @@ impl<'a> View<'a> { /// # 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, reader: &mut dyn io::Read, writer: &mut dyn io::Write) -> CaptureEvent { + use termion::input::TermRead; // Trait for `reader.keys().next()`. + if self.matches.is_empty() { return CaptureEvent::Exit; } @@ -374,11 +371,11 @@ impl<'a> View<'a> { .unwrap() .clone(); - self.render(stdout); + self.render(writer); loop { // This is an option of a result of a key... Let's pop error cases first. - let next_key = stdin.keys().next(); + let next_key = reader.keys().next(); if next_key.is_none() { // Nothing in the buffer. Wait for a bit... @@ -394,7 +391,7 @@ impl<'a> View<'a> { match key_res.unwrap() { // Clears an ongoing multi-hint selection, or exit. - Key::Esc => { + event::Key::Esc => { if self.multi && !typed_hint.is_empty() { typed_hint.clear(); } else { @@ -405,7 +402,7 @@ impl<'a> View<'a> { // In multi-selection mode, this appends the selected hint to the // vector of selections. In normal mode, this returns with the hint // selected. - Key::Insert => match self.matches.get(self.focus_index) { + event::Key::Insert => match self.matches.get(self.focus_index) { Some(mat) => { chosen.push((mat.text.to_string(), false)); @@ -417,15 +414,15 @@ impl<'a> View<'a> { }, // Move focus to next/prev match. - Key::Up => self.prev(), - Key::Down => self.next(), - Key::Left => self.prev(), - Key::Right => self.next(), + event::Key::Up => self.prev(), + event::Key::Down => self.next(), + event::Key::Left => self.prev(), + event::Key::Right => self.next(), // Pressing space finalizes an ongoing multi-hint selection (without // selecting the focused match). Pressing other characters attempts at // finding a match with a corresponding hint. - Key::Char(ch) => { + event::Key::Char(ch) => { if ch == ' ' && self.multi { return CaptureEvent::Hint(chosen); } @@ -468,15 +465,23 @@ impl<'a> View<'a> { // Render on stdout if we did not exit earlier (move focus, // multi-selection). - self.render(stdout); + self.render(writer); } 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()); + 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."), + ); let hints = match self.listen(&mut stdin, &mut stdout) { CaptureEvent::Exit => vec![], From 01d6e62689c5eea6be320757c2bcaa7abf8c3db3 Mon Sep 17 00:00:00 2001 From: graelo Date: Fri, 29 May 2020 11:50:19 +0200 Subject: [PATCH 11/40] feat: first complete workflow --- src/bridge.rs | 437 ++++------------------------------------------ src/lib.rs | 1 + src/tmux.rs | 163 +++++++++-------- tmux-copyrat.tmux | 6 +- tmux-thumbs.sh | 28 --- 5 files changed, 119 insertions(+), 516 deletions(-) delete mode 100755 tmux-thumbs.sh diff --git a/src/bridge.rs b/src/bridge.rs index dd9be5c..a4831e3 100644 --- a/src/bridge.rs +++ b/src/bridge.rs @@ -1,351 +1,11 @@ use clap::Clap; -use regex::Regex; use std::collections::HashMap; -use std::process::Command; use std::str::FromStr; -use std::time::{SystemTime, UNIX_EPOCH}; use copyrat::{error, process, CliOpt}; mod tmux; -trait Executor { - fn execute(&mut self, args: Vec) -> String; - fn last_executed(&self) -> Option>; -} - -struct RealShell { - executed: Option>, -} - -impl RealShell { - fn new() -> RealShell { - RealShell { executed: None } - } -} - -impl Executor for RealShell { - fn execute(&mut self, args: Vec) -> String { - let execution = Command::new(args[0].as_str()) - .args(&args[1..]) - .output() - .expect("Execution failed"); - - self.executed = Some(args); - - let output: String = String::from_utf8_lossy(&execution.stdout).into(); - - output.trim_end().to_string() - } - - fn last_executed(&self) -> Option> { - self.executed.clone() - } -} - -const TMP_FILE: &str = "/tmp/copyrat-last"; - -pub struct Swapper<'a> { - executor: Box<&'a mut dyn Executor>, - // directory: &'a path::Path, - command: &'a str, - alt_command: &'a str, - active_pane_id: Option, - active_pane_height: Option, - active_pane_scroll_position: Option, - active_pane_in_copy_mode: Option, - thumbs_pane_id: Option, - content: Option, - signal: String, -} - -impl<'a> Swapper<'a> { - fn new( - executor: Box<&'a mut dyn Executor>, - // directory: &'a path::Path, - command: &'a str, - alt_command: &'a str, - ) -> Swapper<'a> { - let since_the_epoch = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards"); - let signal = format!("thumbs-finished-{}", since_the_epoch.as_secs()); - - Swapper { - executor, - // directory, - command, - alt_command, - active_pane_id: None, - active_pane_height: None, - active_pane_scroll_position: None, - active_pane_in_copy_mode: None, - thumbs_pane_id: None, - content: None, - signal, - } - } - - pub fn capture_active_pane(&mut self) { - let active_command = vec![ - "tmux", - "list-panes", - "-F", - "#{pane_id}:#{?pane_in_mode,1,0}:#{pane_height}:#{scroll_position}:#{?pane_active,active,nope}", - ]; - - let output = self - .executor - .execute(active_command.iter().map(|arg| arg.to_string()).collect()); - - let lines: Vec<&str> = output.split('\n').collect(); - let chunks: Vec> = lines - .into_iter() - .map(|line| line.split(':').collect()) - .collect(); - - let active_pane = chunks - .iter() - .find(|&chunks| *chunks.get(4).unwrap() == "active") - .expect("Unable to find active pane"); - - let pane_id = active_pane.get(0).unwrap(); - let pane_in_copy_mode = active_pane.get(1).unwrap().to_string(); - - self.active_pane_id = Some(pane_id.to_string()); - self.active_pane_in_copy_mode = Some(pane_in_copy_mode); - - if self.active_pane_in_copy_mode.clone().unwrap() == "1" { - let pane_height = active_pane - .get(2) - .unwrap() - .parse() - .expect("Unable to retrieve pane height"); - let pane_scroll_position = active_pane - .get(3) - .unwrap() - .parse() - .expect("Unable to retrieve pane scroll"); - - self.active_pane_height = Some(pane_height); - self.active_pane_scroll_position = Some(pane_scroll_position); - } - } - - pub fn execute_thumbs(&mut self) { - let options_command = vec!["tmux", "show", "-g"]; - let params: Vec = options_command.iter().map(|arg| arg.to_string()).collect(); - let options = self.executor.execute(params); - let lines: Vec<&str> = options.split('\n').collect(); - - let pattern = Regex::new(r#"@thumbs-([\w\-0-9]+) "?(\w+)"?"#).unwrap(); - - let args = lines - .iter() - .flat_map(|line| { - if let Some(captures) = pattern.captures(line) { - let name = captures.get(1).unwrap().as_str(); - let value = captures.get(2).unwrap().as_str(); - - let boolean_params = vec!["reverse", "unique", "contrast"]; - - if boolean_params.iter().any(|&x| x == name) { - return vec![format!("--{}", name)]; - } - - let string_params = vec![ - "position", - "fg-color", - "bg-color", - "hint-bg-color", - "hint-fg-color", - "select-fg-color", - "select-bg-color", - ]; - - if string_params.iter().any(|&x| x == name) { - return vec![format!("--{}", name), format!("'{}'", value)]; - } - - if name.starts_with("regexp") { - return vec!["--regexp".to_string(), format!("'{}'", value)]; - } - - vec![] - } else { - vec![] - } - }) - .collect::>(); - - let active_pane_id = self.active_pane_id.as_mut().unwrap().clone(); - - let scroll_params = if self.active_pane_in_copy_mode.is_some() { - if let (Some(pane_height), Some(scroll_position)) = ( - self.active_pane_scroll_position, - self.active_pane_scroll_position, - ) { - format!( - " -S {} -E {}", - -scroll_position, - pane_height - scroll_position - 1 - ) - } else { - "".to_string() - } - } else { - "".to_string() - }; - - // NOTE: For debugging add echo $PWD && sleep 5 after tee - let pane_command = format!( - "tmux capture-pane -t {active_id} -p{scroll_params} | target/release/thumbs -f '%U:%H' -t {tmpfile} {args}; tmux swap-pane -t {active_id}; tmux wait-for -S {signal}", - active_id = active_pane_id, - scroll_params = scroll_params, - // dir = self.directory.to_str().unwrap(), - tmpfile = TMP_FILE, - args = args.join(" "), - signal = self.signal - ); - - let thumbs_command = vec![ - "tmux", - "new-window", - "-P", - "-d", - "-n", - "[thumbs]", - pane_command.as_str(), - ]; - - let params: Vec = thumbs_command.iter().map(|arg| arg.to_string()).collect(); - - self.thumbs_pane_id = Some(self.executor.execute(params)); - } - - pub fn swap_panes(&mut self) { - let active_pane_id = self.active_pane_id.as_mut().unwrap().clone(); - let thumbs_pane_id = self.thumbs_pane_id.as_mut().unwrap().clone(); - - let swap_command = vec![ - "tmux", - "swap-pane", - "-d", - "-s", - active_pane_id.as_str(), - "-t", - thumbs_pane_id.as_str(), - ]; - let params = swap_command.iter().map(|arg| arg.to_string()).collect(); - - self.executor.execute(params); - } - - pub fn wait_thumbs(&mut self) { - let wait_command = vec!["tmux", "wait-for", self.signal.as_str()]; - let params = wait_command.iter().map(|arg| arg.to_string()).collect(); - - self.executor.execute(params); - } - - pub fn retrieve_content(&mut self) { - let retrieve_command = vec!["cat", TMP_FILE]; - let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); - - self.content = Some(self.executor.execute(params)); - } - - pub fn destroy_content(&mut self) { - let retrieve_command = vec!["rm", TMP_FILE]; - let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); - - self.executor.execute(params); - } - - pub fn execute_command(&mut self) { - let content = self.content.clone().unwrap(); - let mut splitter = content.splitn(2, ':'); - - if let Some(upcase) = splitter.next() { - if let Some(text) = splitter.next() { - let execute_command = if upcase.trim_end() == "true" { - self.alt_command.clone() - } else { - self.command.clone() - }; - - let final_command = str::replace(execute_command, "{}", text.trim_end()); - let retrieve_command = vec!["bash", "-c", final_command.as_str()]; - let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); - - self.executor.execute(params); - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - struct TestShell { - outputs: Vec, - executed: Option>, - } - - impl TestShell { - fn new(outputs: Vec) -> TestShell { - TestShell { - executed: None, - outputs, - } - } - } - - impl Executor for TestShell { - fn execute(&mut self, args: Vec) -> String { - self.executed = Some(args); - self.outputs.pop().unwrap() - } - - fn last_executed(&self) -> Option> { - self.executed.clone() - } - } - - #[test] - fn retrieve_active_pane() { - let last_command_outputs = - vec!["%97:100:24:1:active\n%106:100:24:1:nope\n%107:100:24:1:nope\n".to_string()]; - let mut executor = TestShell::new(last_command_outputs); - let mut swapper = Swapper::new(Box::new(&mut executor), "", ""); - - swapper.capture_active_pane(); - - assert_eq!(swapper.active_pane_id.unwrap(), "%97"); - } - - #[test] - fn swap_panes() { - let last_command_outputs = vec![ - "".to_string(), - "%100".to_string(), - "".to_string(), - "%106:100:24:1:nope\n%98:100:24:1:active\n%107:100:24:1:nope\n".to_string(), - ]; - let mut executor = TestShell::new(last_command_outputs); - let mut swapper = Swapper::new(Box::new(&mut executor), "", ""); - - swapper.capture_active_pane(); - swapper.execute_thumbs(); - swapper.swap_panes(); - - let expectation = vec!["tmux", "swap-pane", "-d", "-s", "%98", "-t", "%100"]; - - assert_eq!(executor.last_executed().unwrap(), expectation); - } -} - /// Main configuration, parsed from command line. #[derive(Clap, Debug)] #[clap(author, about, version)] @@ -358,18 +18,26 @@ struct BridgeOpt { #[clap(long, default_value = "tmux set-buffer {} && tmux-paste-buffer")] alt_command: String, - /// Retrieve options from tmux. + /// Don't read options from Tmux. /// - /// If active, options formatted like `copyrat-*` are read from tmux. - /// You should consider reading them from the config file (the default - /// option) as this saves both a command call (about 10ms) and a Regex - /// compilation. - #[clap(short = "T", long)] - get_options_from_tmux: bool, + /// By default, options formatted like `copyrat-*` are read from tmux. + /// However, you should consider reading them from the config file (the + /// default option) as this saves both a command call (about 10ms) and a + /// Regex compilation. + #[clap(long)] + ignore_options_from_tmux: bool, - /// Optionally capture entire pane history. - #[clap(long, arg_enum, default_value = "entire-history")] - capture: tmux::CaptureRegion, + /// Name of the copyrat temporary window. + /// + /// Copyrat is launched in a temporary window of that name. The only pane + /// in this temp window gets swapped with the current active one for + /// in-place searching, then swapped back and killed after we exit. + #[clap(long, default_value = "[copyrat]")] + window_name: String, + + /// Capture visible area or entire pane history. + #[clap(long, arg_enum, default_value = "visible-area")] + capture_region: tmux::CaptureRegion, // Include CLI Options #[clap(flatten)] @@ -391,7 +59,7 @@ impl BridgeOpt { self.alt_command = String::from(value); } "@copyrat-capture" => { - self.capture = tmux::CaptureRegion::from_str(&value)?; + self.capture_region = tmux::CaptureRegion::from_str(&value)?; } // Ignore unknown options. @@ -406,52 +74,35 @@ impl BridgeOpt { } } +/// fn main() -> Result<(), error::ParseError> { let mut opt = BridgeOpt::parse(); - if opt.get_options_from_tmux { - let tmux_options = tmux::get_options("@copyrat-")?; + if !opt.ignore_options_from_tmux { + let tmux_options: HashMap = tmux::get_options("@copyrat-")?; + + // Override default values with those coming from tmux. opt.merge_map(&tmux_options)?; } let panes: Vec = tmux::list_panes()?; let active_pane = panes - .iter() + .into_iter() .find(|p| p.is_active) - .expect("One tmux pane should be active"); + .expect("Exactly one tmux pane should be active in the current window."); - let buffer = tmux::capture_pane(&active_pane, &opt.capture)?; + let buffer = tmux::capture_pane(&active_pane, &opt.capture_region)?; - let selections: Vec<(String, bool)> = if active_pane.in_mode { - // If the current pane is in copy mode, we have to dance a little with - // Panes, because the current pane has already locked the Alternate - // Screen, preventing copyrat::run to execute. - let initial_pane = active_pane; + // We have to dance a little with Panes, because this process i/o streams + // are connected to the pane in the window newly created for us, instead + // of the active current pane. + let temp_pane_spec = format!("{}.0", opt.window_name); + tmux::swap_pane_with(&temp_pane_spec)?; - // Create a new window without switching to it, with a `sh` command - // for faster startup. - let temp_pane: tmux::Pane = tmux::create_new_window("[copyrat]", "sh")?; + let selections = copyrat::run(buffer, &opt.cli_options); - // Swap the two panes, changing the active pane to be the temp_pane. - // After swap, temp_pane has the same height than the initial_pane - // had before being swapped. - tmux::swap_panes(initial_pane, &temp_pane)?; - - // Running copyrat now will render in the newly created temp_pane - // (locking stdin, writing to its stdout), but this is almost - // transparent to the user. - let selections = copyrat::run(buffer, &opt.cli_options); - - // Swap back the two panes, making initial_pane the active one again. - tmux::swap_panes(&temp_pane, initial_pane)?; - - tmux::kill_pane(&temp_pane)?; - - selections - } else { - copyrat::run(buffer, &opt.cli_options) - }; + tmux::swap_pane_with(&temp_pane_spec)?; // Execute a command on each selection. // TODO: consider getting rid of multi-selection mode. @@ -461,6 +112,7 @@ fn main() -> Result<(), error::ParseError> { } else { opt.command.replace("{}", text) }; + let mut it = raw_command.split(' ').into_iter(); let command = it.next().unwrap(); let args: Vec<&str> = it.collect(); @@ -470,24 +122,5 @@ fn main() -> Result<(), error::ParseError> { process::execute(&command, &args).unwrap(); }); - if false { - let mut executor = RealShell::new(); - - let mut swapper = Swapper::new( - Box::new(&mut executor), - // opt.directory.as_path(), - &opt.command, - &opt.alt_command, - ); - - swapper.capture_active_pane(); - swapper.execute_thumbs(); - swapper.swap_panes(); - swapper.wait_thumbs(); - swapper.retrieve_content(); - swapper.destroy_content(); - swapper.execute_command(); - } - Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 54096f5..f2e2ed7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -136,6 +136,7 @@ impl FromStr for HintStyleCli { } } +/// Try to parse a `&str` into a tuple of `char`s. fn parse_chars(src: &str) -> Result<(char, char), error::ParseError> { if src.len() != 2 { return Err(error::ParseError::ExpectedSurroundingPair); diff --git a/src/tmux.rs b/src/tmux.rs index 2c009f6..6d9b83c 100644 --- a/src/tmux.rs +++ b/src/tmux.rs @@ -1,6 +1,7 @@ use clap::Clap; use regex::Regex; use std::collections::HashMap; +use std::fmt; use std::str::FromStr; use copyrat::error::ParseError; @@ -8,8 +9,8 @@ use copyrat::process; #[derive(Debug, PartialEq)] pub struct Pane { - /// Pane identifier. - pub id: u32, + /// Pane identifier, e.g. `%37`. + pub id: PaneId, /// Describes if the pane is in some mode. pub in_mode: bool, /// Number of lines in the pane. @@ -24,7 +25,9 @@ pub struct Pane { pub is_active: bool, } -impl Pane { +impl FromStr for Pane { + type Err = ParseError; + /// Parse a string containing tmux panes status into a new `Pane`. /// /// This returns a `Result` as this call can obviously @@ -37,17 +40,20 @@ impl Pane { /// /// For definitions, look at `Pane` type, /// and at the tmux man page for definitions. - pub fn parse(src: &str) -> Result { + fn from_str(src: &str) -> Result { let items: Vec<&str> = src.split(':').collect(); assert_eq!(items.len(), 5, "tmux should have returned 5 items per line"); let mut iter = items.iter(); + // Pane id must be start with '%' followed by a `u32` let id_str = iter.next().unwrap(); - if !id_str.starts_with('%') { - return Err(ParseError::ExpectedPaneIdMarker); - } - let id = id_str[1..].parse::()?; + let id = PaneId::from_str(id_str)?; + // if !id_str.starts_with('%') { + // return Err(ParseError::ExpectedPaneIdMarker); + // } + // let id = id_str[1..].parse::()?; + // let id = format!("%{}", id); let in_mode = iter.next().unwrap().parse::()?; @@ -73,6 +79,58 @@ impl Pane { } } +#[derive(Debug, PartialEq)] +pub struct PaneId(String); + +impl FromStr for PaneId { + type Err = ParseError; + + /// Parse into PaneId. The `&str` must be start with '%' + /// followed by a `u32`. + fn from_str(src: &str) -> Result { + if !src.starts_with('%') { + return Err(ParseError::ExpectedPaneIdMarker); + } + let id = src[1..].parse::()?; + let id = format!("%{}", id); + Ok(PaneId(id)) + } +} + +impl fmt::Display for PaneId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Clap, Debug)] +pub enum CaptureRegion { + /// The entire history. + /// + /// This will end up sending `-S - -E -` to `tmux capture-pane`. + EntireHistory, + /// The visible area. + VisibleArea, + ///// Region from start line to end line + ///// + ///// This works as defined in tmux's docs (order does not matter). + //Region(i32, i32), +} + +impl FromStr for CaptureRegion { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + match s { + "leading" => Ok(CaptureRegion::EntireHistory), + "trailing" => Ok(CaptureRegion::VisibleArea), + _ => Err(ParseError::ExpectedString(String::from( + "entire-history or visible-area", + ))), + } + } +} + /// Returns a list of `Pane` from the current tmux session. pub fn list_panes() -> Result, ParseError> { let args = vec![ @@ -88,7 +146,7 @@ pub fn list_panes() -> Result, ParseError> { let result: Result, ParseError> = output .trim_end() // trim last '\n' as it would create an empty line .split('\n') - .map(|line| Pane::parse(line)) + .map(|line| Pane::from_str(line)) .collect(); result @@ -124,34 +182,6 @@ pub fn get_options(prefix: &str) -> Result, ParseError> Ok(args) } -#[derive(Clap, Debug)] -pub enum CaptureRegion { - /// The entire history. - /// - /// This will end up sending `-S - -E -` to `tmux capture-pane`. - EntireHistory, - /// The visible area. - VisibleArea, - ///// Region from start line to end line - ///// - ///// This works as defined in tmux's docs (order does not matter). - //Region(i32, i32), -} - -impl FromStr for CaptureRegion { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match s { - "leading" => Ok(CaptureRegion::EntireHistory), - "trailing" => Ok(CaptureRegion::VisibleArea), - _ => Err(ParseError::ExpectedString(String::from( - "entire-history or visible-area", - ))), - } - } -} - /// Returns the entire Pane content as a `String`. /// /// `CaptureRegion` specifies if the visible area is captured, or the entire @@ -169,11 +199,10 @@ impl FromStr for CaptureRegion { /// position. To support both cases, the implementation always provides those /// parameters to tmux. pub fn capture_pane(pane: &Pane, region: &CaptureRegion) -> Result { - let mut args = format!("capture-pane -t %{id} -p", id = pane.id); + let mut args = format!("capture-pane -t {pane_id} -p", pane_id = pane.id); let region_str = match region { CaptureRegion::VisibleArea => { - // Will capture the visible area. // Providing start/end helps support both copy and normal modes. format!( " -S {start} -E {end}", @@ -190,51 +219,12 @@ pub fn capture_pane(pane: &Pane, region: &CaptureRegion) -> Result Result { - let args = vec!["new-window", "-P", "-d", "-n", name, "-F", - "#{pane_id}:#{?pane_in_mode,true,false}:#{pane_height}:#{scroll_position}:#{?pane_active,true,false}", - command]; - - let output = process::execute("tmux", &args)?; - - let pane = Pane::parse(output.trim_end())?; // trim last '\n' as it would create an empty line - - Ok(pane) -} - -/// Ask tmux to swap two `Pane`s and change the active pane to be the target -/// `Pane`. -pub fn swap_panes(pane_a: &Pane, pane_b: &Pane) -> Result<(), ParseError> { - let pa_id = format!("%{}", pane_a.id); - let pb_id = format!("%{}", pane_b.id); - - let args = vec!["swap-pane", "-s", &pa_id, "-t", &pb_id]; - - process::execute("tmux", &args)?; - - Ok(()) -} - -/// Ask tmux to kill the provided `Pane`. -pub fn kill_pane(pane: &Pane) -> Result<(), ParseError> { - let p_id = format!("%{}", pane.id); - - let args = vec!["kill-pane", "-t", &p_id]; +/// Ask tmux to swap the current Pane with the target_pane (uses Tmux format). +pub fn swap_pane_with(target_pane: &str) -> Result<(), ParseError> { + // -Z: keep the window zoomed if it was zoomed. + let args = vec!["swap-pane", "-Z", "-s", target_pane]; process::execute("tmux", &args)?; @@ -244,25 +234,28 @@ pub fn kill_pane(pane: &Pane) -> Result<(), ParseError> { #[cfg(test)] mod tests { use super::Pane; + use super::PaneId; use copyrat::error; + use std::str::FromStr; #[test] fn test_parse_pass() { let output = vec!["%52:false:62:3:false", "%53:false:23::true"]; let panes: Result, error::ParseError> = - output.iter().map(|&line| Pane::parse(line)).collect(); + output.iter().map(|&line| Pane::from_str(line)).collect(); let panes = panes.expect("Could not parse tmux panes"); let expected = vec![ Pane { - id: 52, + id: PaneId::from_str("%52").unwrap(), in_mode: false, height: 62, scroll_position: 3, is_active: false, }, Pane { - id: 53, + // id: PaneId::from_str("%53").unwrap(), + id: PaneId(String::from("%53")), in_mode: false, height: 23, scroll_position: 0, diff --git a/tmux-copyrat.tmux b/tmux-copyrat.tmux index 9409dc9..a7b7c1f 100755 --- a/tmux-copyrat.tmux +++ b/tmux-copyrat.tmux @@ -6,9 +6,13 @@ DEFAULT_COPYRAT_KEY="space" COPYRAT_KEY=$(tmux show-option -gqv @copyrat-key) COPYRAT_KEY=${COPYRAT_KEY:-$DEFAULT_COPYRAT_KEY} +DEFAULT_COPYRAT_WINDOW_NAME="[copyrat]" +COPYRAT_WINDOW_NAME=$(tmux show-option -gqv @copyrat-window-name) +COPYRAT_WINDOW_NAME=${COPYRAT_WINDOW_NAME:-$DEFAULT_COPYRAT_WINDOW_NAME} + BINARY="${CURRENT_DIR}/target/release/tmux-copyrat" -tmux bind-key $COPYRAT_KEY run-shell -b "${BINARY} -T" +tmux bind-key ${COPYRAT_KEY} new-window -d -n ${COPYRAT_WINDOW_NAME} "${BINARY} --window-name ${COPYRAT_WINDOW_NAME} --reverse --unique" if [ ! -f "$BINARY" ]; then cd "${CURRENT_DIR}" && cargo build --release diff --git a/tmux-thumbs.sh b/tmux-thumbs.sh deleted file mode 100755 index fce71f1..0000000 --- a/tmux-thumbs.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash - -[ -f ~/.bash_profile ] && source ~/.bash_profile - -PARAMS=() - -function add-option-param { - VALUE=$(tmux show-options -vg @thumbs-$1 2> /dev/null) - - if [[ ${VALUE} ]]; then - PARAMS+=("--$1=${VALUE}") - fi -} - -add-option-param "command" -add-option-param "upcase-command" - -# Remove empty arguments from PARAMS. -# Otherwise, they would choke up tmux-thumbs when passed to it. -for i in "${!PARAMS[@]}"; do - [ -n "${PARAMS[$i]}" ] || unset "PARAMS[$i]" -done - -CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -${CURRENT_DIR}/target/release/tmux-thumbs --dir "${CURRENT_DIR}" "${PARAMS[@]}" - -true From 1ea1af7bdd21b92b0051355bd1efa168b3a11636 Mon Sep 17 00:00:00 2001 From: graelo Date: Fri, 29 May 2020 16:23:46 +0200 Subject: [PATCH 12/40] refactor: refactor --- src/bridge.rs | 75 ++++++++++++++++++++++++++++++++++----------------- src/lib.rs | 2 ++ src/tmux.rs | 6 +++++ src/view.rs | 26 ++++++++++++++---- 4 files changed, 79 insertions(+), 30 deletions(-) diff --git a/src/bridge.rs b/src/bridge.rs index a4831e3..4cd92df 100644 --- a/src/bridge.rs +++ b/src/bridge.rs @@ -10,14 +10,18 @@ mod tmux; #[derive(Clap, Debug)] #[clap(author, about, version)] struct BridgeOpt { - /// Command to execute on selection. - #[clap(long, default_value = "tmux set-buffer {}")] - command: String, - - /// Command to execute on uppercased selection. - #[clap(long, default_value = "tmux set-buffer {} && tmux-paste-buffer")] - alt_command: String, + ///// Command to execute on selection. + //#[clap(long, default_value = "tmux set-buffer {}")] + //command: String, + ///// Command to execute on uppercased selection. + ///// + ///// This defaults to pasting in the original pane. + //#[clap( + // long, + // default_value = "tmux set-buffer {} && tmux paste-buffer -t '#{active_pane}" + //)] + //alt_command: String, /// Don't read options from Tmux. /// /// By default, options formatted like `copyrat-*` are read from tmux. @@ -52,12 +56,12 @@ impl BridgeOpt { ) -> Result<(), error::ParseError> { for (name, value) in options { match name.as_ref() { - "@copyrat-command" => { - self.command = String::from(value); - } - "@copyrat-alt-command" => { - self.alt_command = String::from(value); - } + // "@copyrat-command" => { + // self.command = String::from(value); + // } + // "@copyrat-alt-command" => { + // self.alt_command = String::from(value); + // } "@copyrat-capture" => { self.capture_region = tmux::CaptureRegion::from_str(&value)?; } @@ -104,23 +108,44 @@ fn main() -> Result<(), error::ParseError> { tmux::swap_pane_with(&temp_pane_spec)?; - // Execute a command on each selection. // TODO: consider getting rid of multi-selection mode. - selections.iter().for_each(|(text, uppercased)| { - let raw_command = if *uppercased { - opt.alt_command.replace("{}", text) - } else { - opt.command.replace("{}", text) - }; - let mut it = raw_command.split(' ').into_iter(); - let command = it.next().unwrap(); - let args: Vec<&str> = it.collect(); + // Execute a command on each group of selections (normal and uppercased). + let (normal_selections, uppercased_selections): (Vec<(String, bool)>, Vec<(String, bool)>) = + selections + .into_iter() + .partition(|(_text, uppercased)| !*uppercased); + let buffer_selections: String = normal_selections + .into_iter() + .map(|(text, _)| text) + .collect::>() + .join("\n"); + + if buffer_selections.len() > 0 { + let args = vec!["set-buffer", &buffer_selections]; // Simply execute the command as is, and let the program crash on // potential errors because it is not our responsibility. - process::execute(&command, &args).unwrap(); - }); + process::execute("tmux", &args).unwrap(); + } + + let buffer_selections: String = uppercased_selections + .into_iter() + .map(|(text, _)| text) + .collect::>() + .join("\n"); + + if buffer_selections.len() > 0 { + let args = vec!["set-buffer", &buffer_selections]; + // Simply execute the command as is, and let the program crash on + // potential errors because it is not our responsibility. + process::execute("tmux", &args).unwrap(); + + let args = vec!["paste-buffer", "-t", active_pane.id.as_str()]; + // Simply execute the command as is, and let the program crash on + // potential errors because it is not our responsibility. + process::execute("tmux", &args).unwrap(); + } Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index f2e2ed7..8454e33 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ pub fn run(buffer: String, opt: &CliOpt) -> Vec<(String, bool)> { let hint_style = match &opt.hint_style { None => None, Some(style) => match style { + HintStyleCli::Bold => Some(view::HintStyle::Bold), HintStyleCli::Italic => Some(view::HintStyle::Italic), HintStyleCli::Underline => Some(view::HintStyle::Underline), HintStyleCli::Surround => { @@ -117,6 +118,7 @@ pub struct CliOpt { /// as we cannot directly parse into view::HintStyle. #[derive(Debug, Clap)] enum HintStyleCli { + Bold, Italic, Underline, Surround, diff --git a/src/tmux.rs b/src/tmux.rs index 6d9b83c..7f5d5a2 100644 --- a/src/tmux.rs +++ b/src/tmux.rs @@ -97,6 +97,12 @@ impl FromStr for PaneId { } } +impl PaneId { + pub fn as_str(&self) -> &str { + &self.0 + } +} + impl fmt::Display for PaneId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) diff --git a/src/view.rs b/src/view.rs index bf5bb25..181045d 100644 --- a/src/view.rs +++ b/src/view.rs @@ -83,6 +83,8 @@ impl FromStr for HintAlignment { /// # 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`). @@ -231,6 +233,21 @@ impl<'a> View<'a> { .unwrap(); } Some(hint_style) => match hint_style { + HintStyle::Bold => { + write!( + stdout, + "{goto}{bg_color}{fg_color}{sty}{hint}{sty_reset}{fg_reset}{bg_reset}", + goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + 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(); + } HintStyle::Italic => { write!( stdout, @@ -433,11 +450,10 @@ impl<'a> View<'a> { typed_hint.push_str(&lower_key); // Find the match that corresponds to the entered key. - let selection = self - .matches - .iter() - // Avoid cloning typed_hint for comparison. - .find(|&mat| mat.hint.as_deref().unwrap_or_default() == &typed_hint); + let selection = self.matches + .iter() + // Avoid cloning typed_hint for comparison. + .find(|&mat| mat.hint.as_deref().unwrap_or_default() == &typed_hint); match selection { Some(mat) => { From d8386615a34a14625e4ac1674544dbfc5649d946 Mon Sep 17 00:00:00 2001 From: graelo Date: Fri, 29 May 2020 16:40:44 +0200 Subject: [PATCH 13/40] refactor: remove command & alt_command --- src/bridge.rs | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/bridge.rs b/src/bridge.rs index 4cd92df..45e4304 100644 --- a/src/bridge.rs +++ b/src/bridge.rs @@ -10,18 +10,6 @@ mod tmux; #[derive(Clap, Debug)] #[clap(author, about, version)] struct BridgeOpt { - ///// Command to execute on selection. - //#[clap(long, default_value = "tmux set-buffer {}")] - //command: String, - - ///// Command to execute on uppercased selection. - ///// - ///// This defaults to pasting in the original pane. - //#[clap( - // long, - // default_value = "tmux set-buffer {} && tmux paste-buffer -t '#{active_pane}" - //)] - //alt_command: String, /// Don't read options from Tmux. /// /// By default, options formatted like `copyrat-*` are read from tmux. @@ -50,23 +38,16 @@ struct BridgeOpt { impl BridgeOpt { /// Try parsing provided options, and update self with the valid values. + /// Unknown options are simply ignored. pub fn merge_map( &mut self, options: &HashMap, ) -> Result<(), error::ParseError> { for (name, value) in options { match name.as_ref() { - // "@copyrat-command" => { - // self.command = String::from(value); - // } - // "@copyrat-alt-command" => { - // self.alt_command = String::from(value); - // } "@copyrat-capture" => { self.capture_region = tmux::CaptureRegion::from_str(&value)?; } - - // Ignore unknown options. _ => (), } } @@ -89,6 +70,7 @@ fn main() -> Result<(), error::ParseError> { opt.merge_map(&tmux_options)?; } + // Identify active pane and capture its content. let panes: Vec = tmux::list_panes()?; let active_pane = panes @@ -108,6 +90,8 @@ fn main() -> Result<(), error::ParseError> { tmux::swap_pane_with(&temp_pane_spec)?; + // Finally copy selection to a tmux buffer, and paste it to the active + // buffer if it was uppercased. // TODO: consider getting rid of multi-selection mode. // Execute a command on each group of selections (normal and uppercased). From cab6ff3418f0eed4dd240ed2a089d767338ac828 Mon Sep 17 00:00:00 2001 From: graelo Date: Fri, 29 May 2020 18:43:17 +0200 Subject: [PATCH 14/40] refactor(clean): better names --- src/lib.rs | 14 ++------ src/main.rs | 14 +++----- src/view.rs | 100 ++++++++++++++++++---------------------------------- 3 files changed, 43 insertions(+), 85 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8454e33..05fd230 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ pub mod view; /// # Note /// /// Maybe the decision to take ownership of the buffer is a bit bold. -pub fn run(buffer: String, opt: &CliOpt) -> Vec<(String, bool)> { +pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { let lines: Vec<&str> = buffer.split('\n').collect(); let mut state = state::State::new(&lines, &opt.alphabet, &opt.custom_regex); @@ -34,10 +34,9 @@ pub fn run(buffer: String, opt: &CliOpt) -> Vec<(String, bool)> { }, }; - let selections: Vec<(String, bool)> = { + let selection: Option<(String, bool)> = { let mut viewbox = view::View::new( &mut state, - opt.multi_selection, opt.reverse, opt.unique_hint, &opt.hint_alignment, @@ -48,7 +47,7 @@ pub fn run(buffer: String, opt: &CliOpt) -> Vec<(String, bool)> { viewbox.present() }; - selections + selection } /// Main configuration, parsed from command line. @@ -72,10 +71,6 @@ pub struct CliOpt { #[clap(short = "X", long)] custom_regex: Vec, - /// Enable multi-selection. - #[clap(short, long)] - multi_selection: bool, - /// Reverse the order for assigned hints. #[clap(short, long)] reverse: bool, @@ -161,9 +156,6 @@ impl CliOpt { } "@copyrat-regex-id" => (), // TODO "@copyrat-custom-regex" => self.custom_regex = vec![String::from(value)], - "@copyrat-multi-selection" => { - self.multi_selection = value.parse::()?; - } "@copyrat-reverse" => { self.reverse = value.parse::()?; } diff --git a/src/main.rs b/src/main.rs index 5c7f128..2cee8da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,22 +17,18 @@ fn main() { // Execute copyrat over the buffer (will take control over stdout). // This returns the selected matches. - let selections: Vec<(String, bool)> = run(buffer, &opt); + let selection: Option<(String, bool)> = run(buffer, &opt); // Early exit, signaling no selections were found. - if selections.is_empty() { + if selection.is_none() { std::process::exit(1); } - let output = selections - .iter() - .map(|(text, _)| text.as_str()) - .collect::>() - .join("\n"); + let (text, _) = selection.unwrap(); // Write output to a target_path if provided, else print to original stdout. match opt.target_path { - None => println!("{}", output), + None => println!("{}", text), Some(target) => { let mut file = OpenOptions::new() .create(true) @@ -41,7 +37,7 @@ fn main() { .open(target) .expect("Unable to open the target file"); - file.write(output.as_bytes()).unwrap(); + file.write(text.as_bytes()).unwrap(); } } } diff --git a/src/view.rs b/src/view.rs index 181045d..6edc9a1 100644 --- a/src/view.rs +++ b/src/view.rs @@ -11,7 +11,6 @@ pub struct View<'a> { state: &'a mut state::State<'a>, matches: Vec>, focus_index: usize, - multi: bool, hint_alignment: &'a HintAlignment, rendering_colors: &'a ViewColors, hint_style: Option, @@ -94,17 +93,16 @@ pub enum HintStyle { } /// Returned value after the `View` has finished listening to events. -enum CaptureEvent { +enum Event { /// Exit with no selected matches, Exit, /// A vector of matched text and whether it was selected with uppercase. - Hint(Vec<(String, bool)>), + Match((String, bool)), } impl<'a> View<'a> { pub fn new( state: &'a mut state::State<'a>, - multi: bool, reversed: bool, unique_hint: bool, hint_alignment: &'a HintAlignment, @@ -118,7 +116,6 @@ impl<'a> View<'a> { state, matches, focus_index, - multi, hint_alignment, rendering_colors, hint_style, @@ -312,8 +309,6 @@ impl<'a> View<'a> { /// Multibyte characters are taken into account, so that the Match's `text` /// and `hint` are rendered in their proper position. fn render(&self, stdout: &mut dyn io::Write) -> () { - write!(stdout, "{}", cursor::Hide).unwrap(); - // 1. Trim all lines and render non-empty ones. View::render_lines(stdout, self.state.lines); @@ -365,21 +360,23 @@ impl<'a> View<'a> { stdout.flush().unwrap(); } - /// Listen to keys entered on stdin, moving focus accordingly, and selecting - /// one or multiple matches. + /// Listen to keys entered on stdin, moving focus accordingly, or + /// selecting one match. /// /// # 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, reader: &mut dyn io::Read, writer: &mut dyn io::Write) -> CaptureEvent { + /// + /// - 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, reader: &mut dyn io::Read, writer: &mut dyn io::Write) -> Event { use termion::input::TermRead; // Trait for `reader.keys().next()`. if self.matches.is_empty() { - return CaptureEvent::Exit; + return Event::Exit; } - let mut chosen = vec![]; - let mut typed_hint: String = "".to_owned(); + let mut typed_hint = String::new(); + + // TODO: simplify let longest_hint = self .matches .iter() @@ -407,68 +404,39 @@ impl<'a> View<'a> { } match key_res.unwrap() { - // Clears an ongoing multi-hint selection, or exit. event::Key::Esc => { - if self.multi && !typed_hint.is_empty() { - typed_hint.clear(); - } else { - break; - } + break; } - // In multi-selection mode, this appends the selected hint to the - // vector of selections. In normal mode, this returns with the hint - // selected. - event::Key::Insert => match self.matches.get(self.focus_index) { - Some(mat) => { - chosen.push((mat.text.to_string(), false)); - - if !self.multi { - return CaptureEvent::Hint(chosen); - } - } - None => panic!("Match not found?"), - }, - // Move focus to next/prev match. event::Key::Up => self.prev(), event::Key::Down => self.next(), event::Key::Left => self.prev(), event::Key::Right => self.next(), - // Pressing space finalizes an ongoing multi-hint selection (without - // selecting the focused match). Pressing other characters attempts at - // finding a match with a corresponding hint. + // 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. event::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); // Find the match that corresponds to the entered key. - let selection = self.matches + let found = self.matches .iter() // Avoid cloning typed_hint for comparison. .find(|&mat| mat.hint.as_deref().unwrap_or_default() == &typed_hint); - match selection { + match found { Some(mat) => { - chosen.push((mat.text.to_string(), key != lower_key)); - - if self.multi { - typed_hint.clear(); - } else { - return CaptureEvent::Hint(chosen); - } + let text = mat.text.to_string(); + let uppercased = key != lower_key; + return Event::Match((text, uppercased)); } None => { - // TODO: use a Trie or another data structure to determine - // if the entered key belongs to a longer hint. - if !self.multi && typed_hint.len() >= longest_hint.len() { + if typed_hint.len() >= longest_hint.len() { break; } } @@ -479,15 +447,18 @@ impl<'a> View<'a> { _ => (), } - // Render on stdout if we did not exit earlier (move focus, - // multi-selection). + // Render on stdout if we did not exit earlier. self.render(writer); } - CaptureEvent::Exit + Event::Exit } - pub fn present(&mut self) -> Vec<(String, bool)> { + /// Configure the terminal and display the `View`. + /// + /// - 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)> { use std::io::Write; use termion::raw::IntoRawMode; use termion::screen::AlternateScreen; @@ -499,14 +470,16 @@ impl<'a> View<'a> { .expect("Cannot access alternate screen."), ); - let hints = match self.listen(&mut stdin, &mut stdout) { - CaptureEvent::Exit => vec![], - CaptureEvent::Hint(chosen) => chosen, + write!(stdout, "{}", cursor::Hide).unwrap(); + + let selection = match self.listen(&mut stdin, &mut stdout) { + Event::Exit => None, + Event::Match((text, uppercased)) => Some((text, uppercased)), }; write!(stdout, "{}", cursor::Show).unwrap(); - hints + selection } } @@ -757,7 +730,6 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; state: &mut state, matches: vec![], // no matches focus_index: 0, - multi: false, hint_alignment: &hint_alignment, rendering_colors: &rendering_colors, hint_style: None, @@ -799,7 +771,6 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let custom_regexes = [].to_vec(); let alphabet = alphabets::Alphabet("abcd".to_string()); let mut state = state::State::new(&lines, &alphabet, &custom_regexes); - let multi = false; let reversed = true; let unique_hint = false; @@ -816,7 +787,6 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let view = View::new( &mut state, - multi, reversed, unique_hint, &hint_alignment, From 4e8f4fb90b5f4cd616230bbffe8f0ab4c59ba422 Mon Sep 17 00:00:00 2001 From: graelo Date: Fri, 29 May 2020 20:36:27 +0200 Subject: [PATCH 15/40] feat: configurable base text color --- src/view.rs | 102 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 73 insertions(+), 29 deletions(-) diff --git a/src/view.rs b/src/view.rs index 6edc9a1..36d29a7 100644 --- a/src/view.rs +++ b/src/view.rs @@ -23,6 +23,14 @@ pub struct View<'a> { /// - `hint_*` colors are used to render the hints. #[derive(Clap, Debug)] pub struct ViewColors { + /// Foreground color for base text. + #[clap(long, default_value = "blue", parse(try_from_str = colors::parse_color))] + pub text_fg: Box, + + /// Background color for base text. + #[clap(long, default_value = "white", parse(try_from_str = colors::parse_color))] + pub text_bg: Box, + /// Foreground color for matches. #[clap(long, default_value = "green", parse(try_from_str = colors::parse_color))] @@ -143,15 +151,19 @@ impl<'a> View<'a> { /// # Notes /// - All trailing whitespaces are trimmed, empty lines are skipped. /// - This writes directly on the writer, avoiding extra allocation. - fn render_lines(stdout: &mut dyn io::Write, lines: &Vec<&str>) -> () { + fn render_base_text(stdout: &mut dyn io::Write, lines: &Vec<&str>, colors: &ViewColors) -> () { for (index, line) in lines.iter().enumerate() { let trimmed_line = line.trim_end(); if !trimmed_line.is_empty() { write!( stdout, - "{goto}{text}", + "{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}", goto = cursor::Goto(1, index as u16 + 1), + fg_color = color::Fg(colors.text_fg.as_ref()), + bg_color = color::Bg(colors.text_bg.as_ref()), + fg_reset = color::Fg(color::Reset), + bg_reset = color::Bg(color::Reset), text = &trimmed_line, ) .unwrap(); @@ -159,9 +171,9 @@ impl<'a> View<'a> { } } - /// Render the Match's `text` field on provided writer. + /// Render the Match's `text` field on provided writer using the `match_*g` color. /// - /// If a Mach is "focused", then it is rendered with a specific color. + /// If a Mach is "focused", it is then rendered with the `focused_*g` colors. /// /// # Note /// This writes directly on the writer, avoiding extra allocation. @@ -173,7 +185,7 @@ impl<'a> View<'a> { colors: &ViewColors, ) { // To help identify it, the match thas has focus is rendered with a dedicated color. - let (text_fg_color, text_bg_color) = if focused { + let (fg_color, bg_color) = if focused { (&colors.focused_fg, &colors.focused_bg) } else { (&colors.match_fg, &colors.match_bg) @@ -184,8 +196,8 @@ impl<'a> View<'a> { stdout, "{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}", goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), - fg_color = color::Fg(text_fg_color.as_ref()), - bg_color = color::Bg(text_bg_color.as_ref()), + fg_color = color::Fg(fg_color.as_ref()), + bg_color = color::Bg(bg_color.as_ref()), fg_reset = color::Fg(color::Reset), bg_reset = color::Bg(color::Reset), text = &text, @@ -308,9 +320,9 @@ impl<'a> View<'a> { /// # Note /// Multibyte characters are taken into account, so that the Match's `text` /// and `hint` are rendered in their proper position. - fn render(&self, stdout: &mut dyn io::Write) -> () { + fn full_render(&self, stdout: &mut dyn io::Write) -> () { // 1. Trim all lines and render non-empty ones. - View::render_lines(stdout, self.state.lines); + View::render_base_text(stdout, self.state.lines, &self.rendering_colors); for (index, mat) in self.matches.iter().enumerate() { // 2. Render the match's text. @@ -385,7 +397,7 @@ impl<'a> View<'a> { .unwrap() .clone(); - self.render(writer); + self.full_render(writer); loop { // This is an option of a result of a key... Let's pop error cases first. @@ -448,7 +460,7 @@ impl<'a> View<'a> { } // Render on stdout if we did not exit earlier. - self.render(writer); + self.full_render(writer); } Event::Exit @@ -497,22 +509,36 @@ path: /usr/local/bin/git path: /usr/local/bin/cargo"; let lines: Vec<&str> = content.split('\n').collect(); + let colors = ViewColors { + 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), + }; let mut writer = vec![]; - View::render_lines(&mut writer, &lines); + View::render_base_text(&mut writer, &lines, &colors); 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!( - writer, - format!( - "{}some text{}* e006b06 - (12 days ago) swapper: Make quotes{}path: /usr/local/bin/git{}path: /usr/local/bin/cargo", - goto1, goto2, goto3, goto6, - ) - .as_bytes() - ); + writer, + format!( + "{g1}{bg}{fg}some text{fg_reset}{bg_reset}{g2}{bg}{fg}* e006b06 - (12 days ago) swapper: Make quotes{fg_reset}{bg_reset}{g3}{bg}{fg}path: /usr/local/bin/git{fg_reset}{bg_reset}{g6}{bg}{fg}path: /usr/local/bin/cargo{fg_reset}{bg_reset}", + 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() + ); } #[test] @@ -522,6 +548,8 @@ path: /usr/local/bin/cargo"; let focused = true; let offset: (usize, usize) = (3, 1); let colors = ViewColors { + 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), @@ -554,6 +582,8 @@ path: /usr/local/bin/cargo"; let focused = false; let offset: (usize, usize) = (3, 1); let colors = ViewColors { + 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), @@ -585,6 +615,8 @@ path: /usr/local/bin/cargo"; let hint_text = "eo"; let offset: (usize, usize) = (3, 1); let colors = ViewColors { + 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), @@ -625,6 +657,8 @@ path: /usr/local/bin/cargo"; let hint_text = "eo"; let offset: (usize, usize) = (3, 1); let colors = ViewColors { + 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), @@ -667,6 +701,8 @@ path: /usr/local/bin/cargo"; let hint_text = "eo"; let offset: (usize, usize) = (3, 1); let colors = ViewColors { + 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), @@ -716,6 +752,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let alphabet = alphabets::Alphabet("abcd".to_string()); let mut state = state::State::new(&lines, &alphabet, &custom_regexes); let rendering_colors = ViewColors { + 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), @@ -736,18 +774,20 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; }; let mut writer = vec![]; - view.render(&mut writer); + view.full_render(&mut writer); - let hide = cursor::Hide; let goto1 = cursor::Goto(1, 1); let goto3 = cursor::Goto(1, 3); let expected = format!( - "{hide}{goto1}lorem 127.0.0.1 lorem\ - {goto3}Barcelona https://en.wikipedia.org/wiki/Barcelona -", - hide = hide, + "{goto1}{bg}{fg}lorem 127.0.0.1 lorem{fg_reset}{bg_reset}\ + {goto3}{bg}{fg}Barcelona https://en.wikipedia.org/wiki/Barcelona -{fg_reset}{bg_reset}", goto1 = goto1, goto3 = goto3, + 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), ); // println!("{:?}", writer); @@ -775,6 +815,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let unique_hint = false; let rendering_colors = ViewColors { + 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), @@ -795,19 +837,21 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; ); let mut writer = vec![]; - view.render(&mut writer); + view.full_render(&mut writer); let expected_content = { - let hide = cursor::Hide; let goto1 = cursor::Goto(1, 1); let goto3 = cursor::Goto(1, 3); format!( - "{hide}{goto1}lorem 127.0.0.1 lorem\ - {goto3}Barcelona https://en.wikipedia.org/wiki/Barcelona -", - hide = hide, + "{goto1}{bg}{fg}lorem 127.0.0.1 lorem{fg_reset}{bg_reset}\ + {goto3}{bg}{fg}Barcelona https://en.wikipedia.org/wiki/Barcelona -{fg_reset}{bg_reset}", goto1 = goto1, goto3 = goto3, + 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) ) }; From 836d5bdc97af6dc17515af10a2dfca570cc37f9e Mon Sep 17 00:00:00 2001 From: graelo Date: Fri, 29 May 2020 20:48:54 +0200 Subject: [PATCH 16/40] feat: add bright-* colors & set defaults --- src/colors.rs | 8 ++++++++ src/view.rs | 14 +++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/colors.rs b/src/colors.rs index b72b9e9..c3614db 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -11,6 +11,14 @@ pub fn parse_color(src: &str) -> Result, error::ParseError "magenta" => Ok(Box::new(color::Magenta)), "cyan" => Ok(Box::new(color::Cyan)), "white" => Ok(Box::new(color::White)), + "bright-black" => Ok(Box::new(color::LightBlack)), + "bright-red" => Ok(Box::new(color::LightRed)), + "bright-green" => Ok(Box::new(color::LightGreen)), + "bright-yellow" => Ok(Box::new(color::LightYellow)), + "bright-blue" => Ok(Box::new(color::LightBlue)), + "bright-magenta" => Ok(Box::new(color::LightMagenta)), + "bright-cyan" => Ok(Box::new(color::LightCyan)), + "bright-white" => Ok(Box::new(color::LightWhite)), // "default" => Ok(Box::new(color::Reset)), _ => Err(error::ParseError::UnknownColor), } diff --git a/src/view.rs b/src/view.rs index 36d29a7..577b8af 100644 --- a/src/view.rs +++ b/src/view.rs @@ -24,30 +24,30 @@ pub struct View<'a> { #[derive(Clap, Debug)] pub struct ViewColors { /// Foreground color for base text. - #[clap(long, default_value = "blue", parse(try_from_str = colors::parse_color))] + #[clap(long, default_value = "bright-cyan", parse(try_from_str = colors::parse_color))] pub text_fg: Box, /// Background color for base text. - #[clap(long, default_value = "white", parse(try_from_str = colors::parse_color))] + #[clap(long, default_value = "bright-white", parse(try_from_str = colors::parse_color))] pub text_bg: Box, /// Foreground color for matches. - #[clap(long, default_value = "green", + #[clap(long, default_value = "yellow", parse(try_from_str = colors::parse_color))] pub match_fg: Box, /// Background color for matches. - #[clap(long, default_value = "black", + #[clap(long, default_value = "bright-white", parse(try_from_str = colors::parse_color))] pub match_bg: Box, /// Foreground color for the focused match. - #[clap(long, default_value = "blue", + #[clap(long, default_value = "magenta", parse(try_from_str = colors::parse_color))] pub focused_fg: Box, /// Background color for the focused match. - #[clap(long, default_value = "black", + #[clap(long, default_value = "bright-white", parse(try_from_str = colors::parse_color))] pub focused_bg: Box, @@ -57,7 +57,7 @@ pub struct ViewColors { pub hint_fg: Box, /// Background color for hints. - #[clap(long, default_value = "black", + #[clap(long, default_value = "magenta", parse(try_from_str = colors::parse_color))] pub hint_bg: Box, } From 53f7084303554d78c4ae023bd62d9642153932ab Mon Sep 17 00:00:00 2001 From: graelo Date: Sat, 30 May 2020 19:28:54 +0200 Subject: [PATCH 17/40] refactor: wip alphabet & regexes --- src/alphabets.rs | 203 ++++++++++++++++++++++++++--------------------- src/lib.rs | 2 +- src/state.rs | 13 ++- src/view.rs | 3 + 4 files changed, 124 insertions(+), 97 deletions(-) diff --git a/src/alphabets.rs b/src/alphabets.rs index 7d9897c..0afe1bb 100644 --- a/src/alphabets.rs +++ b/src/alphabets.rs @@ -1,116 +1,141 @@ use crate::error; const ALPHABETS: [(&'static str, &'static str); 22] = [ - ("numeric", "1234567890"), - ("abcd", "abcd"), - ("qwerty", "asdfqwerzxcvjklmiuopghtybn"), - ("qwerty-homerow", "asdfjklgh"), - ("qwerty-left-hand", "asdfqwerzcxv"), - ("qwerty-right-hand", "jkluiopmyhn"), - ("azerty", "qsdfazerwxcvjklmuiopghtybn"), - ("azerty-homerow", "qsdfjkmgh"), - ("azerty-left-hand", "qsdfazerwxcv"), - ("azerty-right-hand", "jklmuiophyn"), - ("qwertz", "asdfqweryxcvjkluiopmghtzbn"), - ("qwertz-homerow", "asdfghjkl"), - ("qwertz-left-hand", "asdfqweryxcv"), - ("qwertz-right-hand", "jkluiopmhzn"), - ("dvorak", "aoeuqjkxpyhtnsgcrlmwvzfidb"), - ("dvorak-homerow", "aoeuhtnsid"), - ("dvorak-left-hand", "aoeupqjkyix"), - ("dvorak-right-hand", "htnsgcrlmwvz"), - ("colemak", "arstqwfpzxcvneioluymdhgjbk"), - ("colemak-homerow", "arstneiodh"), - ("colemak-left-hand", "arstqwfpzxcv"), - ("colemak-right-hand", "neioluymjhk"), + ("numeric", "1234567890"), + ("abcd", "abcd"), + ("qwerty", "asdfqwerzxcvjklmiuopghtybn"), + ("qwerty-homerow", "asdfjklgh"), + ("qwerty-left-hand", "asdfqwerzcxv"), + ("qwerty-right-hand", "jkluiopmyhn"), + ("azerty", "qsdfazerwxcvjklmuiopghtybn"), + ("azerty-homerow", "qsdfjkmgh"), + ("azerty-left-hand", "qsdfazerwxcv"), + ("azerty-right-hand", "jklmuiophyn"), + ("qwertz", "asdfqweryxcvjkluiopmghtzbn"), + ("qwertz-homerow", "asdfghjkl"), + ("qwertz-left-hand", "asdfqweryxcv"), + ("qwertz-right-hand", "jkluiopmhzn"), + ("dvorak", "aoeuqjkxpyhtnsgcrlmwvzfidb"), + ("dvorak-homerow", "aoeuhtnsid"), + ("dvorak-left-hand", "aoeupqjkyix"), + ("dvorak-right-hand", "htnsgcrlmwvz"), + ("colemak", "arstqwfpzxcvneioluymdhgjbk"), + ("colemak-homerow", "arstneiodh"), + ("colemak-left-hand", "arstqwfpzxcv"), + ("colemak-right-hand", "neioluymjhk"), ]; -// pub struct Alphabet<'a> { -// letters: &'a str, -// } - /// Type-safe string alphabet (newtype). #[derive(Debug)] pub struct Alphabet(pub String); impl Alphabet { - pub fn hints(&self, matches: usize) -> Vec { - let letters: Vec = self.0.chars().map(|s| s.to_string()).collect(); + /// Create `n` hints. + /// + /// ``` + /// // The algorithm works as follows: + /// // --- lead ---- + /// // initial state | a b c d + /// + /// // along as we need more hints, and still have capacity, do the following + /// + /// // --- lead ---- --- gen --- -------------- prev --------------- + /// // pick d, generate da db dc dd | a b c (d) da db dc dd + /// // pick c, generate ca cb cc cd | a b (c) (d) ca cb cc cd da db dc dd + /// // pick b, generate ba bb bc bd | a (b) (c) (d) ba bb bc bd ca cb cc cd da db dc dd + /// // pick a, generate aa ab ac ad | (a) (b) (c) (d) aa ab ac ad ba bb bc bd ca cb cc cd da db dc dd + /// ``` + pub fn make_hints(&self, n: usize) -> Vec { + let letters: Vec = self.0.chars().map(|s| s.to_string()).collect(); - let mut expansion = letters.clone(); - let mut expanded: Vec = Vec::new(); + let mut lead = letters.clone(); + let mut prev: Vec = Vec::new(); - loop { - if expansion.len() + expanded.len() >= matches { - break; - } - if expansion.is_empty() { - break; - } + loop { + if lead.len() + prev.len() >= n { + break; + } - let prefix = expansion.pop().expect("Ouch!"); - let sub_expansion: Vec = letters - .iter() - .take(matches - expansion.len() - expanded.len()) - .map(|s| prefix.clone() + s) - .collect(); + if lead.is_empty() { + break; + } + let prefix = lead.pop().unwrap(); - expanded.splice(0..0, sub_expansion); + // generate characters pairs + let gen: Vec = letters + .iter() + .take(n - lead.len() - prev.len()) + .map(|s| prefix.clone() + s) + .collect(); + + // Insert gen in front of prev + prev.splice(0..0, gen); + } + + lead = lead.iter().take(n - prev.len()).cloned().collect(); + lead.append(&mut prev); + lead } - - expansion = expansion.iter().take(matches - expanded.len()).cloned().collect(); - expansion.append(&mut expanded); - expansion - } } -// pub fn get_alphabet(alphabet_name: &str) -> Alphabet { -// let alphabets: HashMap<&str, &str> = ALPHABETS.iter().cloned().collect(); - -// alphabets -// .get(alphabet_name) -// .expect(format!("Unknown alphabet: {}", alphabet_name).as_str()); - -// Alphabet::new(alphabets[alphabet_name]) -// } - +/// Parse a name string into `Alphabet`, supporting the CLI. +/// +/// # Note +/// +/// Letters 'n' and 'N' are systematically removed to prevent conflict with +/// navigation keys (arrows and 'n' 'N'). pub fn parse_alphabet(src: &str) -> Result { - let alphabet = ALPHABETS.iter().find(|&(name, _letters)| name == &src); - match alphabet { - Some((_name, letters)) => Ok(Alphabet(letters.to_string())), - None => Err(error::ParseError::UnknownAlphabet), - } + let alphabet_pair = ALPHABETS.iter().find(|&(name, _letters)| name == &src); + + match alphabet_pair { + Some((_name, letters)) => { + let letters = letters.replace(&['n', 'N'][..], ""); + Ok(Alphabet(letters.to_string())) + } + None => Err(error::ParseError::UnknownAlphabet), + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn simple_matches() { - let alphabet = Alphabet("abcd".to_string()); - let hints = alphabet.hints(3); - assert_eq!(hints, ["a", "b", "c"]); - } + #[test] + fn simple_matches() { + let alphabet = Alphabet("abcd".to_string()); + let hints = alphabet.make_hints(3); + assert_eq!(hints, ["a", "b", "c"]); + } - #[test] - fn composed_matches() { - let alphabet = Alphabet("abcd".to_string()); - let hints = alphabet.hints(6); - assert_eq!(hints, ["a", "b", "c", "da", "db", "dc"]); - } + #[test] + fn composed_matches() { + let alphabet = Alphabet("abcd".to_string()); + let hints = alphabet.make_hints(6); + assert_eq!(hints, ["a", "b", "c", "da", "db", "dc"]); + } - #[test] - fn composed_matches_multiple() { - let alphabet = Alphabet("abcd".to_string()); - let hints = alphabet.hints(8); - assert_eq!(hints, ["a", "b", "ca", "cb", "da", "db", "dc", "dd"]); - } + #[test] + fn composed_matches_multiple() { + let alphabet = Alphabet("abcd".to_string()); + let hints = alphabet.make_hints(8); + assert_eq!(hints, ["a", "b", "ca", "cb", "da", "db", "dc", "dd"]); + } - #[test] - fn composed_matches_max() { - let alphabet = Alphabet("ab".to_string()); - let hints = alphabet.hints(8); - assert_eq!(hints, ["aa", "ab", "ba", "bb"]); - } + #[test] + fn composed_matches_max_2() { + let alphabet = Alphabet("ab".to_string()); + let hints = alphabet.make_hints(8); + assert_eq!(hints, ["aa", "ab", "ba", "bb"]); + } + + #[test] + fn composed_matches_max_4() { + let alphabet = Alphabet("abcd".to_string()); + let hints = alphabet.make_hints(13); + assert_eq!( + hints, + ["a", "ba", "bb", "bc", "bd", "ca", "cb", "cc", "cd", "da", "db", "dc", "dd"] + ); + // a (b) (c) (d) a ba bc bd ca cb cc cd da db dc dd + } } diff --git a/src/lib.rs b/src/lib.rs index 05fd230..f716926 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,7 +59,7 @@ pub struct CliOpt { /// Possible values are "{A}", "{A}-homerow", "{A}-left-hand", /// "{A}-right-hand", where "{A}" is one of "qwerty", "azerty", "qwertz", /// "dvorak", "colemak". Examples: "qwerty", "dvorak-homerow". - #[clap(short = "k", long, default_value = "qwerty", + #[clap(short = "k", long, default_value = "dvorak", parse(try_from_str = alphabets::parse_alphabet))] alphabet: alphabets::Alphabet, diff --git a/src/state.rs b/src/state.rs index f896186..8a94816 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,8 +1,8 @@ -use super::alphabets::Alphabet; use regex::Regex; use std::collections::HashMap; use std::fmt; +use crate::alphabets::Alphabet; use crate::regexes::{EXCLUDE_PATTERNS, PATTERNS}; #[derive(Clone)] @@ -37,19 +37,19 @@ impl<'a> PartialEq for Match<'a> { pub struct State<'a> { pub lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, - regexp: &'a Vec, + custom_regexes: &'a Vec, } impl<'a> State<'a> { pub fn new( lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, - regexp: &'a Vec, + custom_regexes: &'a Vec, ) -> State<'a> { State { lines, alphabet, - regexp, + custom_regexes, } } @@ -62,7 +62,7 @@ impl<'a> State<'a> { .collect::>(); let custom_patterns = self - .regexp + .custom_regexes .iter() .map(|regexp| ("custom", Regex::new(regexp).expect("Invalid custom regexp"))) .collect::>(); @@ -123,8 +123,7 @@ impl<'a> State<'a> { } } - // let alphabet = super::alphabets::get_alphabet(self.alphabet); - let mut hints = self.alphabet.hints(matches.len()); + let mut hints = self.alphabet.make_hints(matches.len()); // This looks wrong but we do a pop after if !reverse { diff --git a/src/view.rs b/src/view.rs index 577b8af..14fb6ef 100644 --- a/src/view.rs +++ b/src/view.rs @@ -426,6 +426,9 @@ impl<'a> View<'a> { event::Key::Left => self.prev(), event::Key::Right => self.next(), + event::Key::Char(_ch @ 'n') => self.next(), + event::Key::Char(_ch @ 'N') => self.prev(), + // 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. From 56106f4e3854c4a39d6cfe50067c215ecb5a5551 Mon Sep 17 00:00:00 2001 From: graelo Date: Sat, 30 May 2020 20:27:32 +0200 Subject: [PATCH 18/40] refactor: better alphabets --- src/alphabets.rs | 105 ++++++++++++++++++++++++++++++++++------------- src/lib.rs | 8 +++- 2 files changed, 83 insertions(+), 30 deletions(-) diff --git a/src/alphabets.rs b/src/alphabets.rs index 0afe1bb..e34e156 100644 --- a/src/alphabets.rs +++ b/src/alphabets.rs @@ -1,8 +1,7 @@ use crate::error; -const ALPHABETS: [(&'static str, &'static str); 22] = [ - ("numeric", "1234567890"), - ("abcd", "abcd"), +const ALPHABETS: [(&'static str, &'static str); 21] = [ + // ("abcd", "abcd"), ("qwerty", "asdfqwerzxcvjklmiuopghtybn"), ("qwerty-homerow", "asdfjklgh"), ("qwerty-left-hand", "asdfqwerzcxv"), @@ -23,14 +22,45 @@ const ALPHABETS: [(&'static str, &'static str); 22] = [ ("colemak-homerow", "arstneiodh"), ("colemak-left-hand", "arstqwfpzxcv"), ("colemak-right-hand", "neioluymjhk"), + ( + "longest", + "aoeuqjkxpyhtnsgcrlmwvzfidb;,~<>'@!#$%^&*~1234567890", + ), ]; +/// Parse a name string into `Alphabet`, used during CLI parsing. +/// +/// # Note +/// +/// Letters 'n' and 'N' are systematically removed to prevent conflict with +/// navigation keys (arrows and 'n' 'N'). +pub fn parse_alphabet(src: &str) -> Result { + let alphabet_pair = ALPHABETS.iter().find(|&(name, _letters)| name == &src); + + match alphabet_pair { + Some((_name, letters)) => { + let letters = letters.replace(&['n', 'N'][..], ""); + Ok(Alphabet(letters.to_string())) + } + None => Err(error::ParseError::UnknownAlphabet), + } +} + /// Type-safe string alphabet (newtype). #[derive(Debug)] pub struct Alphabet(pub String); impl Alphabet { - /// Create `n` hints. + /// Create `n` hints from the Alphabet. + /// + /// An Alphabet of `m` letters can produce at most `m^2` hints. In case + /// this limit is exceeded, this function will generate the `n` hints from + /// an Alphabet which has more letters (50). This will ensure 2500 hints + /// can be generated, which should cover all use cases (I think even + /// easymotion has less). + /// + /// If more hints are needed, unfortunately, this will keep producing + /// empty (`""`) hints. /// /// ``` /// // The algorithm works as follows: @@ -46,7 +76,19 @@ impl Alphabet { /// // pick a, generate aa ab ac ad | (a) (b) (c) (d) aa ab ac ad ba bb bc bd ca cb cc cd da db dc dd /// ``` pub fn make_hints(&self, n: usize) -> Vec { - let letters: Vec = self.0.chars().map(|s| s.to_string()).collect(); + // Shortcut if we have enough letters in the Alphabet. + if self.0.len() >= n { + return self.0.chars().take(n).map(|c| c.to_string()).collect(); + } + + // Use the "longest" alphabet if the current alphabet cannot produce as + // many hints as asked. + let letters: Vec = if self.0.len().pow(2) >= n { + self.0.chars().collect() + } else { + let alt_alphabet = parse_alphabet("longest").unwrap(); + alt_alphabet.0.chars().collect() + }; let mut lead = letters.clone(); let mut prev: Vec = Vec::new(); @@ -65,34 +107,23 @@ impl Alphabet { let gen: Vec = letters .iter() .take(n - lead.len() - prev.len()) - .map(|s| prefix.clone() + s) + .map(|c| format!("{}{}", prefix, c)) .collect(); // Insert gen in front of prev - prev.splice(0..0, gen); + prev.splice(..0, gen); } - lead = lead.iter().take(n - prev.len()).cloned().collect(); - lead.append(&mut prev); - lead - } -} + // Finalize by concatenating the lead and prev components, filling + // with "" as necessary. + let lead: Vec = lead.iter().map(|c| c.to_string()).collect(); -/// Parse a name string into `Alphabet`, supporting the CLI. -/// -/// # Note -/// -/// Letters 'n' and 'N' are systematically removed to prevent conflict with -/// navigation keys (arrows and 'n' 'N'). -pub fn parse_alphabet(src: &str) -> Result { - let alphabet_pair = ALPHABETS.iter().find(|&(name, _letters)| name == &src); + let filler: Vec = std::iter::repeat("") + .take(n - lead.len() - prev.len()) + .map(|s| s.to_string()) + .collect(); - match alphabet_pair { - Some((_name, letters)) => { - let letters = letters.replace(&['n', 'N'][..], ""); - Ok(Alphabet(letters.to_string())) - } - None => Err(error::ParseError::UnknownAlphabet), + [lead, prev, filler].concat() } } @@ -124,7 +155,7 @@ mod tests { #[test] fn composed_matches_max_2() { let alphabet = Alphabet("ab".to_string()); - let hints = alphabet.make_hints(8); + let hints = alphabet.make_hints(4); assert_eq!(hints, ["aa", "ab", "ba", "bb"]); } @@ -136,6 +167,24 @@ mod tests { hints, ["a", "ba", "bb", "bc", "bd", "ca", "cb", "cc", "cd", "da", "db", "dc", "dd"] ); - // a (b) (c) (d) a ba bc bd ca cb cc cd da db dc dd + } + + #[test] + fn matches_with_longest_alphabet() { + let alphabet = Alphabet("ab".to_string()); + let hints = alphabet.make_hints(2500); + assert_eq!(hints.len(), 2500); + assert_eq!(&hints[..3], ["aa", "ao", "ae"]); + assert_eq!(&hints[2497..], ["08", "09", "00"]); + } + + #[test] + fn matches_exceed_longest_alphabet() { + let alphabet = Alphabet("ab".to_string()); + let hints = alphabet.make_hints(10000); + // 2500 unique hints are produced from the longest alphabet + // The 7500 last ones come from the filler ("" empty hints). + assert_eq!(hints.len(), 10000); + assert!(&hints[2500..].iter().all(|s| s == "")); } } diff --git a/src/lib.rs b/src/lib.rs index f716926..f7cc786 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,8 +57,12 @@ pub struct CliOpt { /// Alphabet to draw hints from. /// /// Possible values are "{A}", "{A}-homerow", "{A}-left-hand", - /// "{A}-right-hand", where "{A}" is one of "qwerty", "azerty", "qwertz", - /// "dvorak", "colemak". Examples: "qwerty", "dvorak-homerow". + /// "{A}-right-hand", where "{A}" is one of "qwerty", "azerty", "qwertz" + /// "dvorak", "colemak". + /// + /// # Examples + /// + /// "qwerty", "dvorak-homerow", "azerty-right-hand". #[clap(short = "k", long, default_value = "dvorak", parse(try_from_str = alphabets::parse_alphabet))] alphabet: alphabets::Alphabet, From 7e96bef522bd3202c21dc14339734bed778cf237 Mon Sep 17 00:00:00 2001 From: graelo Date: Sat, 30 May 2020 22:23:33 +0200 Subject: [PATCH 19/40] refactor: move reverse into the State --- src/lib.rs | 3 +-- src/state.rs | 47 +++++++++++++++++++++++++---------------------- src/view.rs | 28 +++++++++++++++++++--------- 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f7cc786..5913c07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,7 @@ pub mod view; pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { let lines: Vec<&str> = buffer.split('\n').collect(); - let mut state = state::State::new(&lines, &opt.alphabet, &opt.custom_regex); + let mut state = state::State::new(&lines, &opt.alphabet, &opt.custom_regex, opt.reverse); let hint_style = match &opt.hint_style { None => None, @@ -37,7 +37,6 @@ pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { let selection: Option<(String, bool)> = { let mut viewbox = view::View::new( &mut state, - opt.reverse, opt.unique_hint, &opt.hint_alignment, &opt.colors, diff --git a/src/state.rs b/src/state.rs index 8a94816..a1fa525 100644 --- a/src/state.rs +++ b/src/state.rs @@ -38,6 +38,7 @@ pub struct State<'a> { pub lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, custom_regexes: &'a Vec, + pub reverse: bool, } impl<'a> State<'a> { @@ -45,15 +46,17 @@ impl<'a> State<'a> { lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, custom_regexes: &'a Vec, + reverse: bool, ) -> State<'a> { State { lines, alphabet, custom_regexes, + reverse, } } - pub fn matches(&self, reverse: bool, unique: bool) -> Vec> { + pub fn matches(&self, unique: bool) -> Vec> { let mut matches = Vec::new(); let exclude_patterns = EXCLUDE_PATTERNS @@ -126,7 +129,7 @@ impl<'a> State<'a> { let mut hints = self.alphabet.make_hints(matches.len()); // This looks wrong but we do a pop after - if !reverse { + if !self.reverse { hints.reverse(); } else { matches.reverse(); @@ -152,7 +155,7 @@ impl<'a> State<'a> { } } - if reverse { + if self.reverse { matches.reverse(); } @@ -174,7 +177,7 @@ mod tests { let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a"); @@ -186,7 +189,7 @@ mod tests { let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, true); + let results = State::new(&lines, &alphabet, &custom, false).matches(true); assert_eq!(results.len(), 3); assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a"); @@ -198,7 +201,7 @@ mod tests { let lines = split("latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!( @@ -212,7 +215,7 @@ mod tests { let lines = split("path: /var/log/nginx.log\npath: test/log/nginx-2.log:32folder/.nginx@4df2.log"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text, "/var/log/nginx.log"); @@ -225,7 +228,7 @@ mod tests { let lines = split("Lorem /tmp/foo/bar_lol, lorem\n Lorem /var/log/boot-strap.log lorem ../log/kern.log lorem"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text.clone(), "/tmp/foo/bar_lol"); @@ -241,7 +244,7 @@ mod tests { let lines = split("Lorem ~/.gnu/.config.txt, lorem"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!(results.get(0).unwrap().text.clone(), "~/.gnu/.config.txt"); @@ -253,7 +256,7 @@ mod tests { split("Lorem ipsum 123e4567-e89b-12d3-a456-426655440000 lorem\n Lorem lorem lorem"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 1); } @@ -263,7 +266,7 @@ mod tests { let lines = split("Lorem fd70b5695 5246ddf f924213 lorem\n Lorem 973113963b491874ab2e372ee60d4b4cb75f717c lorem"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 4); assert_eq!(results.get(0).unwrap().text.clone(), "fd70b5695"); @@ -281,7 +284,7 @@ mod tests { split("Lorem ipsum 127.0.0.1 lorem\n Lorem 255.255.10.255 lorem 127.0.0.1 lorem"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text.clone(), "127.0.0.1"); @@ -294,7 +297,7 @@ mod tests { let lines = split("Lorem ipsum fe80::2:202:fe4 lorem\n Lorem 2001:67c:670:202:7ba8:5e41:1591:d723 lorem fe80::2:1 lorem ipsum fe80:22:312:fe::1%eth0"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 4); assert_eq!(results.get(0).unwrap().text.clone(), "fe80::2:202:fe4"); @@ -316,7 +319,7 @@ mod tests { ); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 2); assert_eq!(results.get(0).unwrap().pattern.clone(), "markdown_url"); @@ -336,7 +339,7 @@ mod tests { let lines = split("Lorem ipsum https://www.rust-lang.org/tools lorem\n Lorem ipsumhttps://crates.io lorem https://github.io?foo=bar lorem ssh://github.io"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 4); assert_eq!( @@ -360,7 +363,7 @@ mod tests { let lines = split("Lorem 0xfd70b5695 0x5246ddf lorem\n Lorem 0x973113tlorem"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text.clone(), "0xfd70b5695"); @@ -374,7 +377,7 @@ mod tests { split("Lorem #fd7b56 lorem #FF00FF\n Lorem #00fF05 lorem #abcd00 lorem #afRR00"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 4); assert_eq!(results.get(0).unwrap().text.clone(), "#fd7b56"); @@ -388,7 +391,7 @@ mod tests { let lines = split("Lorem QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ lorem Qmfoobar"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!( @@ -403,7 +406,7 @@ mod tests { split("Lorem 5695 52463 lorem\n Lorem 973113 lorem 99999 lorem 8888 lorem\n 23456 lorem 5432 lorem 23444"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 8); } @@ -413,7 +416,7 @@ mod tests { let lines = split("Lorem lorem\n--- a/src/main.rs"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs"); @@ -424,7 +427,7 @@ mod tests { let lines = split("Lorem lorem\n+++ b/src/main.rs"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs"); @@ -439,7 +442,7 @@ mod tests { .map(|&s| s.to_string()) .collect(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom).matches(false, false); + let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 9); assert_eq!(results.get(0).unwrap().text.clone(), "http://foo.bar"); diff --git a/src/view.rs b/src/view.rs index 14fb6ef..cd313bf 100644 --- a/src/view.rs +++ b/src/view.rs @@ -111,14 +111,13 @@ enum Event { impl<'a> View<'a> { pub fn new( state: &'a mut state::State<'a>, - reversed: bool, unique_hint: bool, hint_alignment: &'a HintAlignment, rendering_colors: &'a ViewColors, hint_style: Option, ) -> View<'a> { - let matches = state.matches(reversed, unique_hint); - let focus_index = if reversed { matches.len() - 1 } else { 0 }; + let matches = state.matches(unique_hint); + let focus_index = if state.reverse { matches.len() - 1 } else { 0 }; View { state, @@ -426,8 +425,20 @@ impl<'a> View<'a> { event::Key::Left => self.prev(), event::Key::Right => self.next(), - event::Key::Char(_ch @ 'n') => self.next(), - event::Key::Char(_ch @ 'N') => self.prev(), + event::Key::Char(_ch @ 'n') => { + if self.state.reverse { + self.prev() + } else { + self.next() + } + } + event::Key::Char(_ch @ 'N') => { + if self.state.reverse { + self.next() + } else { + self.prev() + } + } // TODO: use a Trie or another data structure to determine // if the entered key belongs to a longer hint. @@ -753,7 +764,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let custom_regexes = [].to_vec(); let alphabet = alphabets::Alphabet("abcd".to_string()); - let mut state = state::State::new(&lines, &alphabet, &custom_regexes); + let mut state = state::State::new(&lines, &alphabet, &custom_regexes, false); let rendering_colors = ViewColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), @@ -813,8 +824,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let custom_regexes = [].to_vec(); let alphabet = alphabets::Alphabet("abcd".to_string()); - let mut state = state::State::new(&lines, &alphabet, &custom_regexes); - let reversed = true; + let reverse = true; + let mut state = state::State::new(&lines, &alphabet, &custom_regexes, reverse); let unique_hint = false; let rendering_colors = ViewColors { @@ -832,7 +843,6 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let view = View::new( &mut state, - reversed, unique_hint, &hint_alignment, &rendering_colors, From 2e9b5fb7bef222823c6f224fff518dc445949345 Mon Sep 17 00:00:00 2001 From: graelo Date: Sun, 31 May 2020 00:17:51 +0200 Subject: [PATCH 20/40] refactor: better state --- src/regexes.rs | 2 +- src/state.rs | 268 ++++++++++++++++++++++++++++++------------------- src/view.rs | 28 +++--- 3 files changed, 177 insertions(+), 121 deletions(-) diff --git a/src/regexes.rs b/src/regexes.rs index a9bbd79..d8e10c3 100644 --- a/src/regexes.rs +++ b/src/regexes.rs @@ -1,5 +1,5 @@ pub const EXCLUDE_PATTERNS: [(&'static str, &'static str); 1] = - [("bash", r"[[:cntrl:]]\[([0-9]{1,2};)?([0-9]{1,2})?m")]; + [("ansi_colors", r"[[:cntrl:]]\[([0-9]{1,2};)?([0-9]{1,2})?m")]; pub const PATTERNS: [(&'static str, &'static str); 14] = [ ("markdown_url", r"\[[^]]*\]\(([^)]+)\)"), diff --git a/src/state.rs b/src/state.rs index a1fa525..3808aff 100644 --- a/src/state.rs +++ b/src/state.rs @@ -11,7 +11,7 @@ pub struct Match<'a> { pub y: i32, pub pattern: &'a str, pub text: &'a str, - pub hint: Option, + pub hint: String, } impl<'a> fmt::Debug for Match<'a> { @@ -19,11 +19,7 @@ impl<'a> fmt::Debug for Match<'a> { write!( f, "Match {{ x: {}, y: {}, pattern: {}, text: {}, hint: <{}> }}", - self.x, - self.y, - self.pattern, - self.text, - self.hint.clone().unwrap_or("".to_string()) + self.x, self.y, self.pattern, self.text, self.hint, ) } } @@ -34,6 +30,24 @@ impl<'a> PartialEq for Match<'a> { } } +/// Internal surrogate for `Match`, before a Hint has been associated. +struct RawMatch<'a> { + pub x: i32, + pub y: i32, + pub pattern: &'a str, + pub text: &'a str, +} + +impl<'a> fmt::Debug for RawMatch<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "RawMatch {{ x: {}, y: {}, pattern: {}, text: {} }}", + self.x, self.y, self.pattern, self.text, + ) + } +} + pub struct State<'a> { pub lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, @@ -57,103 +71,13 @@ impl<'a> State<'a> { } pub fn matches(&self, unique: bool) -> Vec> { - let mut matches = Vec::new(); + let mut raw_matches = self.raw_matches(); - let exclude_patterns = EXCLUDE_PATTERNS - .iter() - .map(|tuple| (tuple.0, Regex::new(tuple.1).unwrap())) - .collect::>(); - - let custom_patterns = self - .custom_regexes - .iter() - .map(|regexp| ("custom", Regex::new(regexp).expect("Invalid custom regexp"))) - .collect::>(); - - let patterns = PATTERNS - .iter() - .map(|tuple| (tuple.0, Regex::new(tuple.1).unwrap())) - .collect::>(); - - let all_patterns = [exclude_patterns, custom_patterns, patterns].concat(); - - for (index, line) in self.lines.iter().enumerate() { - let mut chunk: &str = line; - let mut offset: i32 = 0; - - loop { - let submatches = all_patterns - .iter() - .filter_map(|tuple| match tuple.1.find_iter(chunk).nth(0) { - Some(m) => Some((tuple.0, tuple.1.clone(), m)), - None => None, - }) - .collect::>(); - let first_match_option = submatches - .iter() - .min_by(|x, y| x.2.start().cmp(&y.2.start())); - - if let Some(first_match) = first_match_option { - let (name, pattern, matching) = first_match; - let text = matching.as_str(); - - if let Some(captures) = pattern.captures(text) { - let (subtext, substart) = if let Some(capture) = captures.get(1) { - (capture.as_str(), capture.start()) - } else { - (matching.as_str(), 0) - }; - - // Never hint or broke bash color sequences - if *name != "bash" { - matches.push(Match { - x: offset + matching.start() as i32 + substart as i32, - y: index as i32, - pattern: name, - text: subtext, - hint: None, - }); - } - - chunk = chunk.get(matching.end()..).expect("Unknown chunk"); - offset += matching.end() as i32; - } else { - panic!("No matching?"); - } - } else { - break; - } - } + if self.reverse { + raw_matches.reverse(); } - let mut hints = self.alphabet.make_hints(matches.len()); - - // This looks wrong but we do a pop after - if !self.reverse { - hints.reverse(); - } else { - matches.reverse(); - hints.reverse(); - } - - if unique { - let mut previous: HashMap<&str, String> = HashMap::new(); - - for mat in &mut matches { - if let Some(previous_hint) = previous.get(mat.text) { - mat.hint = Some(previous_hint.clone()); - } else if let Some(hint) = hints.pop() { - mat.hint = Some(hint.to_string().clone()); - previous.insert(mat.text, hint.to_string().clone()); - } - } - } else { - for mat in &mut matches { - if let Some(hint) = hints.pop() { - mat.hint = Some(hint.to_string().clone()); - } - } - } + let mut matches = self.associate_hints(&raw_matches, unique); if self.reverse { matches.reverse(); @@ -161,6 +85,140 @@ impl<'a> State<'a> { matches } + + fn raw_matches(&self) -> Vec> { + let mut matches = Vec::new(); + + let exclude_regexes = EXCLUDE_PATTERNS + .iter() + .map(|&(name, pattern)| (name, Regex::new(pattern).unwrap())) + .collect::>(); + + let custom_regexes = self + .custom_regexes + .iter() + .map(|pattern| { + ( + "custom", + Regex::new(pattern).expect("Invalid custom regexp"), + ) + }) + .collect::>(); + + let regexes = PATTERNS + .iter() + .map(|&(name, pattern)| (name, Regex::new(pattern).unwrap())) + .collect::>(); + + let all_regexes = [exclude_regexes, custom_regexes, regexes].concat(); + + for (index, line) in self.lines.iter().enumerate() { + // Remainder of the line to be searched for matches. + // This advances iteratively, until no matches can be found. + let mut chunk: &str = line; + let mut offset: i32 = 0; + + // Use all avail regexes to match the chunk and select the match + // occuring the earliest on the chunk. Save its matched text and + // position in a `Match` struct. + loop { + let chunk_matches = all_regexes + .iter() + .filter_map(|(&ref name, regex)| match regex.find_iter(chunk).nth(0) { + Some(m) => Some((name, regex.clone(), m)), + None => None, + }) + .collect::>(); + + if chunk_matches.is_empty() { + break; + } + + let first_match = chunk_matches + .iter() + .min_by(|x, y| x.2.start().cmp(&y.2.start())) + .unwrap(); + + let (name, pattern, matching) = first_match; + let text = matching.as_str(); + + let captures = pattern + .captures(text) + .expect("At this stage the regex must have matched."); + + // Handle both capturing and non-capturing patterns. + let (subtext, substart) = if let Some(capture) = captures.get(1) { + (capture.as_str(), capture.start()) + } else { + (text, 0) + }; + + // Never hint or break ansi color sequences. + if *name != "ansi_colors" { + matches.push(RawMatch { + x: offset + matching.start() as i32 + substart as i32, + y: index as i32, + pattern: name, + text: subtext, + }); + } + + chunk = chunk.get(matching.end()..).expect("Unknown chunk"); + offset += matching.end() as i32; + } + } + + matches + } + + /// Associate a hint to each `RawMatch`, returning a vector of `Match`es. + /// + /// If `unique` is `true`, all duplicate matches will have the same hint. + /// For copying matched text, this seems easier and more natural. + /// If `unique` is `false`, duplicate matches will have their own hint. + fn associate_hints(&self, raw_matches: &Vec>, unique: bool) -> Vec> { + let hints = self.alphabet.make_hints(raw_matches.len()); + let mut hints_iter = hints.iter(); + + let mut result: Vec> = vec![]; + + if unique { + // Map (text, hint) + let mut known: HashMap<&str, &str> = HashMap::new(); + + for raw_mat in raw_matches { + let hint: &str = known.entry(raw_mat.text).or_insert( + hints_iter + .next() + .expect("We should have as many hints as necessary, even invisible ones."), + ); + + result.push(Match { + x: raw_mat.x, + y: raw_mat.y, + pattern: raw_mat.pattern, + text: raw_mat.text, + hint: hint.to_string(), + }); + } + } else { + for raw_mat in raw_matches { + let hint = hints_iter + .next() + .expect("We should have as many hints as necessary, even invisible ones."); + + result.push(Match { + x: raw_mat.x, + y: raw_mat.y, + pattern: raw_mat.pattern, + text: raw_mat.text, + hint: hint.to_string(), + }); + } + } + + result + } } #[cfg(test)] @@ -180,8 +238,8 @@ mod tests { let results = State::new(&lines, &alphabet, &custom, false).matches(false); assert_eq!(results.len(), 3); - assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a"); - assert_eq!(results.last().unwrap().hint.clone().unwrap(), "c"); + assert_eq!(results.first().unwrap().hint, "a"); + assert_eq!(results.last().unwrap().hint, "c"); } #[test] @@ -192,8 +250,8 @@ mod tests { let results = State::new(&lines, &alphabet, &custom, false).matches(true); assert_eq!(results.len(), 3); - assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a"); - assert_eq!(results.last().unwrap().hint.clone().unwrap(), "a"); + assert_eq!(results.first().unwrap().hint, "a"); + assert_eq!(results.last().unwrap().hint, "a"); } #[test] @@ -211,7 +269,7 @@ mod tests { } #[test] - fn match_bash() { + fn match_ansi_colors() { let lines = split("path: /var/log/nginx.log\npath: test/log/nginx-2.log:32folder/.nginx@4df2.log"); let custom = [].to_vec(); let alphabet = Alphabet("abcd".to_string()); diff --git a/src/view.rs b/src/view.rs index cd313bf..2348856 100644 --- a/src/view.rs +++ b/src/view.rs @@ -352,20 +352,18 @@ impl<'a> View<'a> { // 3. Render the hint (e.g. "eo") as an overlay on top of the rendered matched text, // aligned at its leading or the trailing edge. - if let Some(ref hint) = mat.hint { - let extra_offset = match self.hint_alignment { - HintAlignment::Leading => 0, - HintAlignment::Trailing => text.len() - hint.len(), - }; + let extra_offset = match self.hint_alignment { + HintAlignment::Leading => 0, + HintAlignment::Trailing => text.len() - mat.hint.len(), + }; - View::render_matched_hint( - stdout, - hint, - (offset_x + extra_offset, offset_y), - &self.rendering_colors, - &self.hint_style, - ); - } + View::render_matched_hint( + stdout, + &mat.hint, + (offset_x + extra_offset, offset_y), + &self.rendering_colors, + &self.hint_style, + ); } stdout.flush().unwrap(); @@ -391,7 +389,7 @@ impl<'a> View<'a> { let longest_hint = self .matches .iter() - .filter_map(|m| m.hint.clone()) + .map(|m| &m.hint) .max_by(|x, y| x.len().cmp(&y.len())) .unwrap() .clone(); @@ -453,7 +451,7 @@ impl<'a> View<'a> { let found = self.matches .iter() // Avoid cloning typed_hint for comparison. - .find(|&mat| mat.hint.as_deref().unwrap_or_default() == &typed_hint); + .find(|&mat| &mat.hint == &typed_hint); match found { Some(mat) => { From be698ab74170915b59428089258bc2b32958f167 Mon Sep 17 00:00:00 2001 From: graelo Date: Sun, 31 May 2020 22:45:36 +0200 Subject: [PATCH 21/40] feat: use a Trie for hint check --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/state.rs | 24 ++++++++++++++++++++++++ src/view.rs | 53 ++++++++++++++++++++++++++-------------------------- 4 files changed, 59 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e972d7b..51c7756 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,7 @@ version = "0.1.0" dependencies = [ "clap 3.0.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "sequence_trie 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -195,6 +196,11 @@ name = "regex-syntax" version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "sequence_trie" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "strsim" version = "0.10.0" @@ -361,6 +367,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" "checksum regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" "checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" +"checksum sequence_trie 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1ee22067b7ccd072eeb64454b9c6e1b33b61cd0d49e895fd48676a184580e0c3" "checksum strsim 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" "checksum syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)" = "95b5f192649e48a5302a13f2feb224df883b98933222369e4b3b0fe2a5447269" "checksum syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" diff --git a/Cargo.toml b/Cargo.toml index 23e7a2d..ab11bcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ license = "MIT" termion = "1.5" regex = "1.3.1" clap = { version = "3.0.0-beta.1", features = ["suggestions", "color", "wrap_help", "term_size"]} +sequence_trie = "0.3.6" [[bin]] name = "copyrat" diff --git a/src/state.rs b/src/state.rs index 3808aff..a4a8e18 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,4 +1,5 @@ use regex::Regex; +use sequence_trie::SequenceTrie; use std::collections::HashMap; use std::fmt; @@ -70,6 +71,8 @@ impl<'a> State<'a> { } } + /// Returns a vector of `Match`es, each corresponding to a pattern match + /// in the lines, its location (x, y), and associated hint. pub fn matches(&self, unique: bool) -> Vec> { let mut raw_matches = self.raw_matches(); @@ -86,6 +89,9 @@ impl<'a> State<'a> { matches } + /// Internal function that searches the state lines for pattern matches. + /// Returns a vector of `RawMatch`es (text, location, pattern id) without + /// an associated hint. The hint is attached to `Match`, not to `RawMatch`. fn raw_matches(&self) -> Vec> { let mut matches = Vec::new(); @@ -219,6 +225,24 @@ impl<'a> State<'a> { result } + + /// Builds a `SequenceTrie` that helps determine if a sequence of keys + /// entered by the user corresponds to a match. This kind of lookup + /// directly returns a reference to the corresponding `Match` if any. + pub fn build_lookup_trie(matches: &'a Vec>) -> SequenceTrie { + let mut trie = SequenceTrie::new(); + + for (index, mat) in matches.iter().enumerate() { + let hint_chars = mat.hint.chars().collect::>(); + + // no need to insert twice the same hint + if trie.get(&hint_chars).is_none() { + trie.insert_owned(hint_chars, index); + } + } + + trie + } } #[cfg(test)] diff --git a/src/view.rs b/src/view.rs index 2348856..e6dd3c3 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,15 +1,17 @@ -use super::{colors, state}; - -use crate::error::ParseError; use clap::Clap; +use sequence_trie::SequenceTrie; use std::char; use std::io; use std::str::FromStr; use termion::{self, color, cursor, event, style}; +use crate::error::ParseError; +use crate::{colors, state}; + pub struct View<'a> { state: &'a mut state::State<'a>, matches: Vec>, + lookup_trie: SequenceTrie, focus_index: usize, hint_alignment: &'a HintAlignment, rendering_colors: &'a ViewColors, @@ -117,11 +119,13 @@ impl<'a> View<'a> { hint_style: Option, ) -> View<'a> { let matches = state.matches(unique_hint); + let lookup_trie = state::State::build_lookup_trie(&matches); let focus_index = if state.reverse { matches.len() - 1 } else { 0 }; View { state, matches, + lookup_trie, focus_index, hint_alignment, rendering_colors, @@ -385,15 +389,6 @@ impl<'a> View<'a> { let mut typed_hint = String::new(); - // TODO: simplify - let longest_hint = self - .matches - .iter() - .map(|m| &m.hint) - .max_by(|x, y| x.len().cmp(&y.len())) - .unwrap() - .clone(); - self.full_render(writer); loop { @@ -447,21 +442,26 @@ impl<'a> View<'a> { typed_hint.push_str(&lower_key); - // Find the match that corresponds to the entered key. - let found = self.matches - .iter() - // Avoid cloning typed_hint for comparison. - .find(|&mat| &mat.hint == &typed_hint); - - match found { - Some(mat) => { - let text = mat.text.to_string(); - let uppercased = key != lower_key; - return Event::Match((text, uppercased)); - } + match self + .lookup_trie + .get_node(&typed_hint.chars().collect::>()) + { None => { - if typed_hint.len() >= longest_hint.len() { - break; + // An unknown key was entered. + return Event::Exit; + } + Some(node) => { + 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; } } } @@ -779,6 +779,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let view = View { state: &mut state, matches: vec![], // no matches + lookup_trie: SequenceTrie::new(), focus_index: 0, hint_alignment: &hint_alignment, rendering_colors: &rendering_colors, From fd76ea1491e0766de4385dfbe7083b9b842fc7de Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 07:30:00 +0200 Subject: [PATCH 22/40] feat: search for specific pattern names --- src/error.rs | 2 + src/lib.rs | 18 ++-- src/regexes.rs | 20 +++- src/state.rs | 244 +++++++++++++++++++++++++++---------------------- src/view.rs | 10 +- 5 files changed, 174 insertions(+), 120 deletions(-) diff --git a/src/error.rs b/src/error.rs index aa233cf..89b7398 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,6 +5,7 @@ pub enum ParseError { ExpectedSurroundingPair, UnknownAlphabet, UnknownColor, + UnknownPatternName, ExpectedPaneIdMarker, ExpectedInt(std::num::ParseIntError), ExpectedBool(std::str::ParseBoolError), @@ -20,6 +21,7 @@ impl fmt::Display for ParseError { ParseError::UnknownColor => { write!(f, "Expected ANSI color name (magenta, cyan, black, ...)") } + ParseError::UnknownPatternName => write!(f, "Expected a known pattern name"), ParseError::ExpectedPaneIdMarker => write!(f, "Expected pane id marker"), ParseError::ExpectedInt(msg) => write!(f, "Expected an int: {}", msg), ParseError::ExpectedBool(msg) => write!(f, "Expected a bool: {}", msg), diff --git a/src/lib.rs b/src/lib.rs index 5913c07..296cf7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,13 @@ pub mod view; pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { let lines: Vec<&str> = buffer.split('\n').collect(); - let mut state = state::State::new(&lines, &opt.alphabet, &opt.custom_regex, opt.reverse); + let mut state = state::State::new( + &lines, + &opt.alphabet, + &opt.named_pattern, + &opt.custom_regex, + opt.reverse, + ); let hint_style = match &opt.hint_style { None => None, @@ -66,15 +72,15 @@ pub struct CliOpt { parse(try_from_str = alphabets::parse_alphabet))] alphabet: alphabets::Alphabet, - // /// Which existing regexes to use. - // #[clap(short = "x", long, arg_enum)] - // regex_id: Vec, - // + /// Pattern names to use (all if not specified). + #[clap(short = "x", long = "--pattern-name", parse(try_from_str = regexes::parse_pattern_name))] + named_pattern: Vec, + /// Additional regex patterns. #[clap(short = "X", long)] custom_regex: Vec, - /// Reverse the order for assigned hints. + /// Assign hints starting from the bottom of the screen. #[clap(short, long)] reverse: bool, diff --git a/src/regexes.rs b/src/regexes.rs index d8e10c3..cca6679 100644 --- a/src/regexes.rs +++ b/src/regexes.rs @@ -1,3 +1,5 @@ +use crate::error; + pub const EXCLUDE_PATTERNS: [(&'static str, &'static str); 1] = [("ansi_colors", r"[[:cntrl:]]\[([0-9]{1,2};)?([0-9]{1,2})?m")]; @@ -5,15 +7,15 @@ pub const PATTERNS: [(&'static str, &'static str); 14] = [ ("markdown_url", r"\[[^]]*\]\(([^)]+)\)"), ( "url", - r"((https?://|git@|git://|ssh://|ftp://|file:///)[^ ]+)", + r"((https?://|git@|git://|ssh://|ftp://|file:///)[^ \(\)\[\]\{\}]+)", ), ("diff_a", r"--- a/([^ ]+)"), ("diff_b", r"\+\+\+ b/([^ ]+)"), ("docker", r"sha256:([0-9a-f]{64})"), ("path", r"(([.\w\-@~]+)?(/[.\w\-@]+)+)"), - ("color", r"#[0-9a-fA-F]{6}"), + ("hexcolor", r"#[0-9a-fA-F]{6}"), ( - "uid", + "uuid", r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", ), ("ipfs", r"Qm[0-9a-zA-Z]{44}"), @@ -23,3 +25,15 @@ pub const PATTERNS: [(&'static str, &'static str); 14] = [ ("address", r"0x[0-9a-fA-F]+"), ("number", r"[0-9]{4,}"), ]; + +/// Type-safe string Pattern Name (newtype). +#[derive(Debug)] +pub struct NamedPattern(pub String, pub String); + +/// Parse a name string into `NamedPattern`, used during CLI parsing. +pub fn parse_pattern_name(src: &str) -> Result { + match PATTERNS.iter().find(|&(name, _pattern)| name == &src) { + Some((name, pattern)) => Ok(NamedPattern(name.to_string(), pattern.to_string())), + None => Err(error::ParseError::UnknownPatternName), + } +} diff --git a/src/state.rs b/src/state.rs index a4a8e18..66b327f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use std::fmt; use crate::alphabets::Alphabet; -use crate::regexes::{EXCLUDE_PATTERNS, PATTERNS}; +use crate::regexes::{NamedPattern, EXCLUDE_PATTERNS, PATTERNS}; #[derive(Clone)] pub struct Match<'a> { @@ -52,6 +52,7 @@ impl<'a> fmt::Debug for RawMatch<'a> { pub struct State<'a> { pub lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, + named_patterns: &'a Vec, custom_regexes: &'a Vec, pub reverse: bool, } @@ -60,12 +61,14 @@ impl<'a> State<'a> { pub fn new( lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, + named_patterns: &'a Vec, custom_regexes: &'a Vec, reverse: bool, ) -> State<'a> { State { lines, alphabet, + named_patterns, custom_regexes, reverse, } @@ -111,10 +114,17 @@ impl<'a> State<'a> { }) .collect::>(); - let regexes = PATTERNS - .iter() - .map(|&(name, pattern)| (name, Regex::new(pattern).unwrap())) - .collect::>(); + let regexes = if self.named_patterns.is_empty() { + PATTERNS + .iter() + .map(|&(name, pattern)| (name, Regex::new(pattern).unwrap())) + .collect::>() + } else { + self.named_patterns + .iter() + .map(|NamedPattern(name, pattern)| (name.as_str(), Regex::new(pattern).unwrap())) + .collect::>() + }; let all_regexes = [exclude_regexes, custom_regexes, regexes].concat(); @@ -257,9 +267,10 @@ mod tests { #[test] fn match_reverse() { let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.first().unwrap().hint, "a"); @@ -269,9 +280,10 @@ mod tests { #[test] fn match_unique() { let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(true); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(true); assert_eq!(results.len(), 3); assert_eq!(results.first().unwrap().hint, "a"); @@ -281,9 +293,10 @@ mod tests { #[test] fn match_docker() { let lines = split("latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!( @@ -295,9 +308,10 @@ mod tests { #[test] fn match_ansi_colors() { let lines = split("path: /var/log/nginx.log\npath: test/log/nginx-2.log:32folder/.nginx@4df2.log"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text, "/var/log/nginx.log"); @@ -308,37 +322,37 @@ mod tests { #[test] fn match_paths() { let lines = split("Lorem /tmp/foo/bar_lol, lorem\n Lorem /var/log/boot-strap.log lorem ../log/kern.log lorem"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); - assert_eq!(results.get(0).unwrap().text.clone(), "/tmp/foo/bar_lol"); - assert_eq!( - results.get(1).unwrap().text.clone(), - "/var/log/boot-strap.log" - ); - assert_eq!(results.get(2).unwrap().text.clone(), "../log/kern.log"); + assert_eq!(results.get(0).unwrap().text, "/tmp/foo/bar_lol"); + assert_eq!(results.get(1).unwrap().text, "/var/log/boot-strap.log"); + assert_eq!(results.get(2).unwrap().text, "../log/kern.log"); } #[test] fn match_home() { let lines = split("Lorem ~/.gnu/.config.txt, lorem"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); - assert_eq!(results.get(0).unwrap().text.clone(), "~/.gnu/.config.txt"); + assert_eq!(results.get(0).unwrap().text, "~/.gnu/.config.txt"); } #[test] - fn match_uids() { + fn match_uuids() { let lines = split("Lorem ipsum 123e4567-e89b-12d3-a456-426655440000 lorem\n Lorem lorem lorem"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); } @@ -346,16 +360,17 @@ mod tests { #[test] fn match_shas() { let lines = split("Lorem fd70b5695 5246ddf f924213 lorem\n Lorem 973113963b491874ab2e372ee60d4b4cb75f717c lorem"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 4); - assert_eq!(results.get(0).unwrap().text.clone(), "fd70b5695"); - assert_eq!(results.get(1).unwrap().text.clone(), "5246ddf"); - assert_eq!(results.get(2).unwrap().text.clone(), "f924213"); + assert_eq!(results.get(0).unwrap().text, "fd70b5695"); + assert_eq!(results.get(1).unwrap().text, "5246ddf"); + assert_eq!(results.get(2).unwrap().text, "f924213"); assert_eq!( - results.get(3).unwrap().text.clone(), + results.get(3).unwrap().text, "973113963b491874ab2e372ee60d4b4cb75f717c" ); } @@ -364,34 +379,33 @@ mod tests { fn match_ips() { let lines = split("Lorem ipsum 127.0.0.1 lorem\n Lorem 255.255.10.255 lorem 127.0.0.1 lorem"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); - assert_eq!(results.get(0).unwrap().text.clone(), "127.0.0.1"); - assert_eq!(results.get(1).unwrap().text.clone(), "255.255.10.255"); - assert_eq!(results.get(2).unwrap().text.clone(), "127.0.0.1"); + assert_eq!(results.get(0).unwrap().text, "127.0.0.1"); + assert_eq!(results.get(1).unwrap().text, "255.255.10.255"); + assert_eq!(results.get(2).unwrap().text, "127.0.0.1"); } #[test] fn match_ipv6s() { let lines = split("Lorem ipsum fe80::2:202:fe4 lorem\n Lorem 2001:67c:670:202:7ba8:5e41:1591:d723 lorem fe80::2:1 lorem ipsum fe80:22:312:fe::1%eth0"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 4); - assert_eq!(results.get(0).unwrap().text.clone(), "fe80::2:202:fe4"); + assert_eq!(results.get(0).unwrap().text, "fe80::2:202:fe4"); assert_eq!( - results.get(1).unwrap().text.clone(), + results.get(1).unwrap().text, "2001:67c:670:202:7ba8:5e41:1591:d723" ); - assert_eq!(results.get(2).unwrap().text.clone(), "fe80::2:1"); - assert_eq!( - results.get(3).unwrap().text.clone(), - "fe80:22:312:fe::1%eth0" - ); + assert_eq!(results.get(2).unwrap().text, "fe80::2:1"); + assert_eq!(results.get(3).unwrap().text, "fe80:22:312:fe::1%eth0"); } #[test] @@ -399,85 +413,81 @@ mod tests { let lines = split( "Lorem ipsum [link](https://github.io?foo=bar) ![](http://cdn.com/img.jpg) lorem", ); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 2); - assert_eq!(results.get(0).unwrap().pattern.clone(), "markdown_url"); - assert_eq!( - results.get(0).unwrap().text.clone(), - "https://github.io?foo=bar" - ); - assert_eq!(results.get(1).unwrap().pattern.clone(), "markdown_url"); - assert_eq!( - results.get(1).unwrap().text.clone(), - "http://cdn.com/img.jpg" - ); + assert_eq!(results.get(0).unwrap().pattern, "markdown_url"); + assert_eq!(results.get(0).unwrap().text, "https://github.io?foo=bar"); + assert_eq!(results.get(1).unwrap().pattern, "markdown_url"); + assert_eq!(results.get(1).unwrap().text, "http://cdn.com/img.jpg"); } #[test] fn match_urls() { let lines = split("Lorem ipsum https://www.rust-lang.org/tools lorem\n Lorem ipsumhttps://crates.io lorem https://github.io?foo=bar lorem ssh://github.io"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 4); assert_eq!( - results.get(0).unwrap().text.clone(), + results.get(0).unwrap().text, "https://www.rust-lang.org/tools" ); - assert_eq!(results.get(0).unwrap().pattern.clone(), "url"); - assert_eq!(results.get(1).unwrap().text.clone(), "https://crates.io"); - assert_eq!(results.get(1).unwrap().pattern.clone(), "url"); - assert_eq!( - results.get(2).unwrap().text.clone(), - "https://github.io?foo=bar" - ); - assert_eq!(results.get(2).unwrap().pattern.clone(), "url"); - assert_eq!(results.get(3).unwrap().text.clone(), "ssh://github.io"); - assert_eq!(results.get(3).unwrap().pattern.clone(), "url"); + assert_eq!(results.get(0).unwrap().pattern, "url"); + assert_eq!(results.get(1).unwrap().text, "https://crates.io"); + assert_eq!(results.get(1).unwrap().pattern, "url"); + assert_eq!(results.get(2).unwrap().text, "https://github.io?foo=bar"); + assert_eq!(results.get(2).unwrap().pattern, "url"); + assert_eq!(results.get(3).unwrap().text, "ssh://github.io"); + assert_eq!(results.get(3).unwrap().pattern, "url"); } #[test] fn match_addresses() { let lines = split("Lorem 0xfd70b5695 0x5246ddf lorem\n Lorem 0x973113tlorem"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); - assert_eq!(results.get(0).unwrap().text.clone(), "0xfd70b5695"); - assert_eq!(results.get(1).unwrap().text.clone(), "0x5246ddf"); - assert_eq!(results.get(2).unwrap().text.clone(), "0x973113"); + assert_eq!(results.get(0).unwrap().text, "0xfd70b5695"); + assert_eq!(results.get(1).unwrap().text, "0x5246ddf"); + assert_eq!(results.get(2).unwrap().text, "0x973113"); } #[test] fn match_hex_colors() { let lines = split("Lorem #fd7b56 lorem #FF00FF\n Lorem #00fF05 lorem #abcd00 lorem #afRR00"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 4); - assert_eq!(results.get(0).unwrap().text.clone(), "#fd7b56"); - assert_eq!(results.get(1).unwrap().text.clone(), "#FF00FF"); - assert_eq!(results.get(2).unwrap().text.clone(), "#00fF05"); - assert_eq!(results.get(3).unwrap().text.clone(), "#abcd00"); + assert_eq!(results.get(0).unwrap().text, "#fd7b56"); + assert_eq!(results.get(1).unwrap().text, "#FF00FF"); + assert_eq!(results.get(2).unwrap().text, "#00fF05"); + assert_eq!(results.get(3).unwrap().text, "#abcd00"); } #[test] fn match_ipfs() { let lines = split("Lorem QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ lorem Qmfoobar"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!( - results.get(0).unwrap().text.clone(), + results.get(0).unwrap().text, "QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ" ); } @@ -486,9 +496,10 @@ mod tests { fn match_process_port() { let lines = split("Lorem 5695 52463 lorem\n Lorem 973113 lorem 99999 lorem 8888 lorem\n 23456 lorem 5432 lorem 23444"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 8); } @@ -496,53 +507,72 @@ mod tests { #[test] fn match_diff_a() { let lines = split("Lorem lorem\n--- a/src/main.rs"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); - assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs"); + assert_eq!(results.get(0).unwrap().text, "src/main.rs"); } #[test] fn match_diff_b() { let lines = split("Lorem lorem\n+++ b/src/main.rs"); - let custom = [].to_vec(); + let named_pat = vec![]; + let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); - assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs"); + assert_eq!(results.get(0).unwrap().text, "src/main.rs"); } #[test] fn priority() { let lines = split("Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"); + let named_pat = vec![]; let custom: Vec = ["CUSTOM-[0-9]{4,}", "ISSUE-[0-9]{3}"] .iter() .map(|&s| s.to_string()) .collect(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &custom, false).matches(false); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 9); - assert_eq!(results.get(0).unwrap().text.clone(), "http://foo.bar"); - assert_eq!(results.get(1).unwrap().text.clone(), "CUSTOM-52463"); - assert_eq!(results.get(2).unwrap().text.clone(), "ISSUE-123"); + assert_eq!(results.get(0).unwrap().text, "http://foo.bar"); + assert_eq!(results.get(1).unwrap().text, "CUSTOM-52463"); + assert_eq!(results.get(2).unwrap().text, "ISSUE-123"); + assert_eq!(results.get(3).unwrap().text, "/var/fd70b569/9999.log"); + assert_eq!(results.get(4).unwrap().text, "52463"); + assert_eq!(results.get(5).unwrap().text, "973113"); assert_eq!( - results.get(3).unwrap().text.clone(), - "/var/fd70b569/9999.log" - ); - assert_eq!(results.get(4).unwrap().text.clone(), "52463"); - assert_eq!(results.get(5).unwrap().text.clone(), "973113"); - assert_eq!( - results.get(6).unwrap().text.clone(), + results.get(6).unwrap().text, "123e4567-e89b-12d3-a456-426655440000" ); - assert_eq!(results.get(7).unwrap().text.clone(), "8888"); + assert_eq!(results.get(7).unwrap().text, "8888"); assert_eq!( - results.get(8).unwrap().text.clone(), + results.get(8).unwrap().text, + "https://crates.io/23456/fd70b569" + ); + } + + #[test] + fn named_patterns() { + let lines = split("Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"); + + use crate::regexes::parse_pattern_name; + let named_pat = vec![parse_pattern_name("url").unwrap()]; + + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + + assert_eq!(results.len(), 2); + assert_eq!(results.get(0).unwrap().text, "http://foo.bar"); + assert_eq!( + results.get(1).unwrap().text, "https://crates.io/23456/fd70b569" ); } diff --git a/src/view.rs b/src/view.rs index e6dd3c3..8e5818d 100644 --- a/src/view.rs +++ b/src/view.rs @@ -760,9 +760,10 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let lines = content.split('\n').collect(); - let custom_regexes = [].to_vec(); + let named_pat = vec![]; + let custom_regexes = vec![]; let alphabet = alphabets::Alphabet("abcd".to_string()); - let mut state = state::State::new(&lines, &alphabet, &custom_regexes, false); + let mut state = state::State::new(&lines, &alphabet, &named_pat, &custom_regexes, false); let rendering_colors = ViewColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), @@ -821,10 +822,11 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let lines = content.split('\n').collect(); - let custom_regexes = [].to_vec(); + let named_pat = vec![]; + let custom_regexes = vec![]; let alphabet = alphabets::Alphabet("abcd".to_string()); let reverse = true; - let mut state = state::State::new(&lines, &alphabet, &custom_regexes, reverse); + let mut state = state::State::new(&lines, &alphabet, &named_pat, &custom_regexes, reverse); let unique_hint = false; let rendering_colors = ViewColors { From 809cdb21f4f2a3c486e6be93947a783c042f46bf Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 09:16:56 +0200 Subject: [PATCH 23/40] chore(docs): state & some cleanup --- src/state.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/state.rs b/src/state.rs index 66b327f..5cd2205 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,12 +1,14 @@ +use std::collections; +use std::fmt; + use regex::Regex; use sequence_trie::SequenceTrie; -use std::collections::HashMap; -use std::fmt; use crate::alphabets::Alphabet; use crate::regexes::{NamedPattern, EXCLUDE_PATTERNS, PATTERNS}; -#[derive(Clone)] +/// Represents matched text, its location on screen, the pattern that created +/// it, and the associated hint. pub struct Match<'a> { pub x: i32, pub y: i32, @@ -25,12 +27,6 @@ impl<'a> fmt::Debug for Match<'a> { } } -impl<'a> PartialEq for Match<'a> { - fn eq(&self, other: &Match) -> bool { - self.x == other.x && self.y == other.y - } -} - /// Internal surrogate for `Match`, before a Hint has been associated. struct RawMatch<'a> { pub x: i32, @@ -49,6 +45,7 @@ impl<'a> fmt::Debug for RawMatch<'a> { } } +/// Holds data for the `View`. pub struct State<'a> { pub lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, @@ -95,6 +92,13 @@ impl<'a> State<'a> { /// Internal function that searches the state lines for pattern matches. /// Returns a vector of `RawMatch`es (text, location, pattern id) without /// an associated hint. The hint is attached to `Match`, not to `RawMatch`. + /// + /// # Notes + /// + /// Custom regexes have priority over other regexes. + /// + /// If no named patterns were specified, it will search for all available + /// patterns from the `PATTERNS` catalog. fn raw_matches(&self) -> Vec> { let mut matches = Vec::new(); @@ -141,7 +145,7 @@ impl<'a> State<'a> { let chunk_matches = all_regexes .iter() .filter_map(|(&ref name, regex)| match regex.find_iter(chunk).nth(0) { - Some(m) => Some((name, regex.clone(), m)), + Some(m) => Some((name, regex, m)), None => None, }) .collect::>(); @@ -150,12 +154,12 @@ impl<'a> State<'a> { break; } - let first_match = chunk_matches + // First match on the chunk. + let (name, pattern, matching) = chunk_matches .iter() .min_by(|x, y| x.2.start().cmp(&y.2.start())) .unwrap(); - let (name, pattern, matching) = first_match; let text = matching.as_str(); let captures = pattern @@ -200,7 +204,7 @@ impl<'a> State<'a> { if unique { // Map (text, hint) - let mut known: HashMap<&str, &str> = HashMap::new(); + let mut known: collections::HashMap<&str, &str> = collections::HashMap::new(); for raw_mat in raw_matches { let hint: &str = known.entry(raw_mat.text).or_insert( From 8464c451e3acb44c90f36c8166374d0a2d1f3a60 Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 09:33:40 +0200 Subject: [PATCH 24/40] refactor: rename state -> model --- src/lib.rs | 6 ++--- src/{state.rs => model.rs} | 50 +++++++++++++++++++------------------- src/view.rs | 39 ++++++++++++++--------------- 3 files changed, 48 insertions(+), 47 deletions(-) rename src/{state.rs => model.rs} (93%) diff --git a/src/lib.rs b/src/lib.rs index 296cf7c..97b6f30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,9 +6,9 @@ use std::str::FromStr; pub mod alphabets; pub mod colors; pub mod error; +pub mod model; pub mod process; pub mod regexes; -pub mod state; pub mod view; /// Run copyrat on an input string `buffer`, configured by `Opt`. @@ -19,7 +19,7 @@ pub mod view; pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { let lines: Vec<&str> = buffer.split('\n').collect(); - let mut state = state::State::new( + let mut model = model::Model::new( &lines, &opt.alphabet, &opt.named_pattern, @@ -42,7 +42,7 @@ pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { let selection: Option<(String, bool)> = { let mut viewbox = view::View::new( - &mut state, + &mut model, opt.unique_hint, &opt.hint_alignment, &opt.colors, diff --git a/src/state.rs b/src/model.rs similarity index 93% rename from src/state.rs rename to src/model.rs index 5cd2205..fc372dc 100644 --- a/src/state.rs +++ b/src/model.rs @@ -46,7 +46,7 @@ impl<'a> fmt::Debug for RawMatch<'a> { } /// Holds data for the `View`. -pub struct State<'a> { +pub struct Model<'a> { pub lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, named_patterns: &'a Vec, @@ -54,15 +54,15 @@ pub struct State<'a> { pub reverse: bool, } -impl<'a> State<'a> { +impl<'a> Model<'a> { pub fn new( lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, named_patterns: &'a Vec, custom_regexes: &'a Vec, reverse: bool, - ) -> State<'a> { - State { + ) -> Model<'a> { + Model { lines, alphabet, named_patterns, @@ -89,7 +89,7 @@ impl<'a> State<'a> { matches } - /// Internal function that searches the state lines for pattern matches. + /// Internal function that searches the model's lines for pattern matches. /// Returns a vector of `RawMatch`es (text, location, pattern id) without /// an associated hint. The hint is attached to `Match`, not to `RawMatch`. /// @@ -274,7 +274,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.first().unwrap().hint, "a"); @@ -287,7 +287,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(true); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(true); assert_eq!(results.len(), 3); assert_eq!(results.first().unwrap().hint, "a"); @@ -300,7 +300,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!( @@ -315,7 +315,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text, "/var/log/nginx.log"); @@ -329,7 +329,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text, "/tmp/foo/bar_lol"); @@ -343,7 +343,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!(results.get(0).unwrap().text, "~/.gnu/.config.txt"); @@ -356,7 +356,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); } @@ -367,7 +367,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 4); assert_eq!(results.get(0).unwrap().text, "fd70b5695"); @@ -386,7 +386,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text, "127.0.0.1"); @@ -400,7 +400,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 4); assert_eq!(results.get(0).unwrap().text, "fe80::2:202:fe4"); @@ -420,7 +420,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 2); assert_eq!(results.get(0).unwrap().pattern, "markdown_url"); @@ -435,7 +435,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 4); assert_eq!( @@ -457,7 +457,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text, "0xfd70b5695"); @@ -472,7 +472,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 4); assert_eq!(results.get(0).unwrap().text, "#fd7b56"); @@ -487,7 +487,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!( @@ -503,7 +503,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 8); } @@ -514,7 +514,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!(results.get(0).unwrap().text, "src/main.rs"); @@ -526,7 +526,7 @@ mod tests { let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!(results.get(0).unwrap().text, "src/main.rs"); @@ -542,7 +542,7 @@ mod tests { .map(|&s| s.to_string()) .collect(); let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 9); assert_eq!(results.get(0).unwrap().text, "http://foo.bar"); @@ -571,7 +571,7 @@ mod tests { let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = State::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 2); assert_eq!(results.get(0).unwrap().text, "http://foo.bar"); diff --git a/src/view.rs b/src/view.rs index 8e5818d..eee90de 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,16 +1,17 @@ -use clap::Clap; -use sequence_trie::SequenceTrie; use std::char; use std::io; use std::str::FromStr; + +use clap::Clap; +use sequence_trie::SequenceTrie; use termion::{self, color, cursor, event, style}; use crate::error::ParseError; -use crate::{colors, state}; +use crate::{colors, model}; pub struct View<'a> { - state: &'a mut state::State<'a>, - matches: Vec>, + model: &'a mut model::Model<'a>, + matches: Vec>, lookup_trie: SequenceTrie, focus_index: usize, hint_alignment: &'a HintAlignment, @@ -112,18 +113,18 @@ enum Event { impl<'a> View<'a> { pub fn new( - state: &'a mut state::State<'a>, + model: &'a mut model::Model<'a>, unique_hint: bool, hint_alignment: &'a HintAlignment, rendering_colors: &'a ViewColors, hint_style: Option, ) -> View<'a> { - let matches = state.matches(unique_hint); - let lookup_trie = state::State::build_lookup_trie(&matches); - let focus_index = if state.reverse { matches.len() - 1 } else { 0 }; + 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 }; View { - state, + model, matches, lookup_trie, focus_index, @@ -147,7 +148,7 @@ impl<'a> View<'a> { } } - /// Render entire state lines on provided writer. + /// Render entire model lines on provided writer. /// /// This renders the basic content on which matches and hints can be rendered. /// @@ -325,7 +326,7 @@ impl<'a> View<'a> { /// and `hint` are rendered in their proper position. fn full_render(&self, stdout: &mut dyn io::Write) -> () { // 1. Trim all lines and render non-empty ones. - View::render_base_text(stdout, self.state.lines, &self.rendering_colors); + View::render_base_text(stdout, self.model.lines, &self.rendering_colors); for (index, mat) in self.matches.iter().enumerate() { // 2. Render the match's text. @@ -335,7 +336,7 @@ impl<'a> View<'a> { // instance ´ + e = é. Consequently the hint offset has to be adjusted // to the left. let offset_x = { - let line = &self.state.lines[mat.y as usize]; + 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) @@ -419,14 +420,14 @@ impl<'a> View<'a> { event::Key::Right => self.next(), event::Key::Char(_ch @ 'n') => { - if self.state.reverse { + if self.model.reverse { self.prev() } else { self.next() } } event::Key::Char(_ch @ 'N') => { - if self.state.reverse { + if self.model.reverse { self.next() } else { self.prev() @@ -763,7 +764,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let named_pat = vec![]; let custom_regexes = vec![]; let alphabet = alphabets::Alphabet("abcd".to_string()); - let mut state = state::State::new(&lines, &alphabet, &named_pat, &custom_regexes, false); + let mut model = model::Model::new(&lines, &alphabet, &named_pat, &custom_regexes, false); let rendering_colors = ViewColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), @@ -778,7 +779,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; // create a view without any match let view = View { - state: &mut state, + model: &mut model, matches: vec![], // no matches lookup_trie: SequenceTrie::new(), focus_index: 0, @@ -826,7 +827,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let custom_regexes = vec![]; let alphabet = alphabets::Alphabet("abcd".to_string()); let reverse = true; - let mut state = state::State::new(&lines, &alphabet, &named_pat, &custom_regexes, reverse); + let mut model = model::Model::new(&lines, &alphabet, &named_pat, &custom_regexes, reverse); let unique_hint = false; let rendering_colors = ViewColors { @@ -843,7 +844,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let hint_style = None; let view = View::new( - &mut state, + &mut model, unique_hint, &hint_alignment, &rendering_colors, From b223e280f1a970a76604c3b4be4339958cf036a2 Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 10:06:50 +0200 Subject: [PATCH 25/40] feat: add focus wrap around --- src/lib.rs | 5 +++++ src/view.rs | 31 ++++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 97b6f30..c264586 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,7 @@ pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { let mut viewbox = view::View::new( &mut model, opt.unique_hint, + opt.focus_wrap_around, &opt.hint_alignment, &opt.colors, hint_style, @@ -95,6 +96,10 @@ pub struct CliOpt { #[clap(short = "a", long, arg_enum, default_value = "leading")] hint_alignment: view::HintAlignment, + /// Move focus back to first/last match. + #[clap(long)] + focus_wrap_around: bool, + /// Optional hint styling. /// /// Underline or surround the hint for increased visibility. diff --git a/src/view.rs b/src/view.rs index eee90de..4568312 100644 --- a/src/view.rs +++ b/src/view.rs @@ -14,6 +14,7 @@ pub struct View<'a> { matches: Vec>, lookup_trie: SequenceTrie, focus_index: usize, + focus_wrap_around: bool, hint_alignment: &'a HintAlignment, rendering_colors: &'a ViewColors, hint_style: Option, @@ -115,6 +116,7 @@ impl<'a> View<'a> { pub fn new( model: &'a mut model::Model<'a>, unique_hint: bool, + focus_wrap_around: bool, hint_alignment: &'a HintAlignment, rendering_colors: &'a ViewColors, hint_style: Option, @@ -128,6 +130,7 @@ impl<'a> View<'a> { matches, lookup_trie, focus_index, + focus_wrap_around, hint_alignment, rendering_colors, hint_style, @@ -136,15 +139,31 @@ impl<'a> View<'a> { /// Move focus onto the previous hint. pub fn prev(&mut self) { - if self.focus_index > 0 { - self.focus_index -= 1; + 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; + } } } /// Move focus onto the next hint. pub fn next(&mut self) { - if self.focus_index < self.matches.len() - 1 { - self.focus_index += 1; + 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; + } } } @@ -380,7 +399,6 @@ impl<'a> View<'a> { /// # 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, reader: &mut dyn io::Read, writer: &mut dyn io::Write) -> Event { use termion::input::TermRead; // Trait for `reader.keys().next()`. @@ -783,6 +801,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; matches: vec![], // no matches lookup_trie: SequenceTrie::new(), focus_index: 0, + focus_wrap_around: false, hint_alignment: &hint_alignment, rendering_colors: &rendering_colors, hint_style: None, @@ -829,6 +848,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let reverse = true; let mut model = model::Model::new(&lines, &alphabet, &named_pat, &custom_regexes, reverse); let unique_hint = false; + let wrap_around = false; let rendering_colors = ViewColors { text_fg: Box::new(color::Black), @@ -846,6 +866,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let view = View::new( &mut model, unique_hint, + wrap_around, &hint_alignment, &rendering_colors, hint_style, From ea512a8dfc72be5c38120120fef0327003256dff Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 10:14:16 +0200 Subject: [PATCH 26/40] refactor: reorder elements in view.rs --- src/view.rs | 184 ++++++++++++++++++++++++++-------------------------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/src/view.rs b/src/view.rs index 4568312..de2f577 100644 --- a/src/view.rs +++ b/src/view.rs @@ -20,98 +20,6 @@ pub struct View<'a> { hint_style: Option, } -/// 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)] -pub struct ViewColors { - /// Foreground color for base text. - #[clap(long, default_value = "bright-cyan", parse(try_from_str = colors::parse_color))] - pub text_fg: Box, - - /// Background color for base text. - #[clap(long, default_value = "bright-white", parse(try_from_str = colors::parse_color))] - pub text_bg: Box, - - /// Foreground color for matches. - #[clap(long, default_value = "yellow", - parse(try_from_str = colors::parse_color))] - pub match_fg: Box, - - /// Background color for matches. - #[clap(long, default_value = "bright-white", - parse(try_from_str = colors::parse_color))] - pub match_bg: Box, - - /// Foreground color for the focused match. - #[clap(long, default_value = "magenta", - parse(try_from_str = colors::parse_color))] - pub focused_fg: Box, - - /// Background color for the focused match. - #[clap(long, default_value = "bright-white", - parse(try_from_str = colors::parse_color))] - pub focused_bg: Box, - - /// Foreground color for hints. - #[clap(long, default_value = "white", - parse(try_from_str = colors::parse_color))] - pub hint_fg: Box, - - /// Background color for hints. - #[clap(long, default_value = "magenta", - parse(try_from_str = colors::parse_color))] - pub hint_bg: Box, -} - -/// 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 { - 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), -} - -/// Returned value after the `View` has finished listening to events. -enum Event { - /// Exit with no selected matches, - Exit, - /// A vector of matched text and whether it was selected with uppercase. - Match((String, bool)), -} - impl<'a> View<'a> { pub fn new( model: &'a mut model::Model<'a>, @@ -526,6 +434,52 @@ impl<'a> View<'a> { } } +/// 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 { + 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), +} + +/// Returned value after the `View` has finished listening to events. +enum Event { + /// Exit with no selected matches, + Exit, + /// A vector of matched text and whether it was selected with uppercase. + Match((String, bool)), +} + #[cfg(test)] mod tests { use super::*; @@ -965,3 +919,49 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; assert_eq!(writer, expected.as_bytes()); } } + +/// 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)] +pub struct ViewColors { + /// Foreground color for base text. + #[clap(long, default_value = "bright-cyan", parse(try_from_str = colors::parse_color))] + pub text_fg: Box, + + /// Background color for base text. + #[clap(long, default_value = "bright-white", parse(try_from_str = colors::parse_color))] + pub text_bg: Box, + + /// Foreground color for matches. + #[clap(long, default_value = "yellow", + parse(try_from_str = colors::parse_color))] + pub match_fg: Box, + + /// Background color for matches. + #[clap(long, default_value = "bright-white", + parse(try_from_str = colors::parse_color))] + pub match_bg: Box, + + /// Foreground color for the focused match. + #[clap(long, default_value = "magenta", + parse(try_from_str = colors::parse_color))] + pub focused_fg: Box, + + /// Background color for the focused match. + #[clap(long, default_value = "bright-white", + parse(try_from_str = colors::parse_color))] + pub focused_bg: Box, + + /// Foreground color for hints. + #[clap(long, default_value = "white", + parse(try_from_str = colors::parse_color))] + pub hint_fg: Box, + + /// Background color for hints. + #[clap(long, default_value = "magenta", + parse(try_from_str = colors::parse_color))] + pub hint_bg: Box, +} From 372781e231e48e7bfc0ea4c9c384cc92e61bc972 Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 10:32:14 +0200 Subject: [PATCH 27/40] feat: remove y from alphabet at runtime --- src/alphabets.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/alphabets.rs b/src/alphabets.rs index e34e156..e1e7aaa 100644 --- a/src/alphabets.rs +++ b/src/alphabets.rs @@ -1,5 +1,11 @@ use crate::error; +/// Catalog of available alphabets. +/// +/// # Note +/// +/// Keep in mind letters 'n' and 'y' are systematically removed at runtime to +/// prevent conflict with navigation and yank/copy keys. const ALPHABETS: [(&'static str, &'static str); 21] = [ // ("abcd", "abcd"), ("qwerty", "asdfqwerzxcvjklmiuopghtybn"), @@ -24,7 +30,7 @@ const ALPHABETS: [(&'static str, &'static str); 21] = [ ("colemak-right-hand", "neioluymjhk"), ( "longest", - "aoeuqjkxpyhtnsgcrlmwvzfidb;,~<>'@!#$%^&*~1234567890", + "aoeuqjkxpyhtnsgcrlmwvzfidb-;,~<>'@!#$%^&*~1234567890", ), ]; @@ -33,13 +39,14 @@ const ALPHABETS: [(&'static str, &'static str); 21] = [ /// # Note /// /// Letters 'n' and 'N' are systematically removed to prevent conflict with -/// navigation keys (arrows and 'n' 'N'). +/// navigation keys (arrows and 'n' 'N'). Letters 'y' and 'Y' are also removed +/// to prevent conflict with yank/copy. pub fn parse_alphabet(src: &str) -> Result { let alphabet_pair = ALPHABETS.iter().find(|&(name, _letters)| name == &src); match alphabet_pair { Some((_name, letters)) => { - let letters = letters.replace(&['n', 'N'][..], ""); + let letters = letters.replace(&['n', 'N', 'y', 'Y'][..], ""); Ok(Alphabet(letters.to_string())) } None => Err(error::ParseError::UnknownAlphabet), From 31e0d5cc9ea3b7aa649c1ef5634dd2ef304a82e2 Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 10:33:48 +0200 Subject: [PATCH 28/40] feat: implement y/Y yank --- src/view.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/view.rs b/src/view.rs index de2f577..9151f68 100644 --- a/src/view.rs +++ b/src/view.rs @@ -360,6 +360,15 @@ impl<'a> View<'a> { } } + 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)); + } + // 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. From 92ced5d3bdf47ad6b7e60d18df470f0d38a2e514 Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 10:34:20 +0200 Subject: [PATCH 29/40] feat: do not render hint on focused match --- src/view.rs | 56 +++++++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/view.rs b/src/view.rs index 9151f68..c5698b1 100644 --- a/src/view.rs +++ b/src/view.rs @@ -282,20 +282,23 @@ impl<'a> View<'a> { &self.rendering_colors, ); - // 3. 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(), - }; + if !focused { + // 3. 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(), + }; - View::render_matched_hint( - stdout, - &mat.hint, - (offset_x + extra_offset, offset_y), - &self.rendering_colors, - &self.hint_style, - ); + View::render_matched_hint( + stdout, + &mat.hint, + (offset_x + extra_offset, offset_y), + &self.rendering_colors, + &self.hint_style, + ); + } } stdout.flush().unwrap(); @@ -891,25 +894,28 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; ) }; - let expected_match2_hint = { - let goto11_3 = cursor::Goto(11, 3); + // Because reverse is true, this second match is focused, + // then the hint should not be rendered. - 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) - ) - }; + // 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) + // ) + // }; let expected = [ expected_content, expected_match1_text, expected_match1_hint, expected_match2_text, - expected_match2_hint, + // expected_match2_hint, ] .concat(); From fb07f64c97f2a165c216c7dd499e62b67bae12bc Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 11:57:23 +0200 Subject: [PATCH 30/40] feat(config): only render diffs --- src/view.rs | 180 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 117 insertions(+), 63 deletions(-) diff --git a/src/view.rs b/src/view.rs index c5698b1..0b3cafc 100644 --- a/src/view.rs +++ b/src/view.rs @@ -45,8 +45,10 @@ impl<'a> View<'a> { } } - /// Move focus onto the previous hint. - pub fn prev(&mut self) { + /// 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; if self.focus_wrap_around { if self.focus_index == 0 { self.focus_index = self.matches.len() - 1; @@ -58,10 +60,14 @@ impl<'a> View<'a> { self.focus_index -= 1; } } + let new_index = self.focus_index; + (old_index, new_index) } - /// Move focus onto the next hint. - pub fn next(&mut self) { + /// 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; if self.focus_wrap_around { if self.focus_index == self.matches.len() - 1 { self.focus_index = 0; @@ -73,6 +79,26 @@ impl<'a> View<'a> { self.focus_index += 1; } } + 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) } /// Render entire model lines on provided writer. @@ -107,6 +133,7 @@ impl<'a> View<'a> { /// If a Mach is "focused", it is then rendered with the `focused_*g` colors. /// /// # Note + /// /// This writes directly on the writer, avoiding extra allocation. fn render_matched_text( stdout: &mut dyn io::Write, @@ -237,7 +264,41 @@ impl<'a> View<'a> { } } - /// Render the view on the provided writer. + /// 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); + + View::render_matched_text( + 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(), + }; + + View::render_matched_hint( + stdout, + &mat.hint, + (offset_x + extra_offset, offset_y), + &self.rendering_colors, + &self.hint_style, + ); + } + } + + /// Full nender the view on the provided writer. /// /// This renders in 3 phases: /// - all lines are rendered verbatim @@ -256,54 +317,34 @@ impl<'a> View<'a> { View::render_base_text(stdout, self.model.lines, &self.rendering_colors); for (index, mat) in self.matches.iter().enumerate() { - // 2. Render the match's text. - - // 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 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; - - let text = &mat.text; - let focused = index == self.focus_index; - - View::render_matched_text( - stdout, - text, - focused, - (offset_x, offset_y), - &self.rendering_colors, - ); - - if !focused { - // 3. 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(), - }; - - View::render_matched_hint( - stdout, - &mat.hint, - (offset_x + extra_offset, offset_y), - &self.rendering_colors, - &self.hint_style, - ); - } + self.render_match(stdout, mat, focused); } stdout.flush().unwrap(); } + /// 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); + + // 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); + + stdout.flush().unwrap(); + } + /// Listen to keys entered on stdin, moving focus accordingly, or /// selecting one match. /// @@ -327,7 +368,7 @@ impl<'a> View<'a> { if next_key.is_none() { // Nothing in the buffer. Wait for a bit... - std::thread::sleep(std::time::Duration::from_millis(100)); + std::thread::sleep(std::time::Duration::from_millis(50)); continue; } @@ -343,26 +384,40 @@ impl<'a> View<'a> { } // Move focus to next/prev match. - event::Key::Up => self.prev(), - event::Key::Down => self.next(), - event::Key::Left => self.prev(), - event::Key::Right => self.next(), - + 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); + } event::Key::Char(_ch @ 'n') => { - if self.model.reverse { - self.prev() + let (old_index, focused_index) = if self.model.reverse { + self.prev_focus_index() } else { - self.next() - } + self.next_focus_index() + }; + self.diff_render(writer, old_index, focused_index); } event::Key::Char(_ch @ 'N') => { - if self.model.reverse { - self.next() + let (old_index, focused_index) = if self.model.reverse { + self.next_focus_index() } else { - self.prev() - } + self.prev_focus_index() + }; + self.diff_render(writer, old_index, focused_index); } + // Yank/copy event::Key::Char(_ch @ 'y') => { let text = self.matches.get(self.focus_index).unwrap().text; return Event::Match((text.to_string(), false)); @@ -410,8 +465,7 @@ impl<'a> View<'a> { _ => (), } - // Render on stdout if we did not exit earlier. - self.full_render(writer); + // End of event processing loop. } Event::Exit From 57f032af7cd87b2469c57730f630f50d8916a673 Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 12:01:53 +0200 Subject: [PATCH 31/40] refactor: view: un-nest trie processing --- src/view.rs | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/view.rs b/src/view.rs index 0b3cafc..fbc1f7e 100644 --- a/src/view.rs +++ b/src/view.rs @@ -436,28 +436,29 @@ impl<'a> View<'a> { typed_hint.push_str(&lower_key); - match self + let node = self .lookup_trie - .get_node(&typed_hint.chars().collect::>()) - { - None => { - // An unknown key was entered. - return Event::Exit; - } - Some(node) => { - 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; - } - } + .get_node(&typed_hint.chars().collect::>()); + + 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; } } From 0accc9bc6b5f3d8d892b36674a1748d25904b26e Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 15:25:54 +0200 Subject: [PATCH 32/40] refactor: view -> ui --- src/lib.rs | 22 ++++++------- src/model.rs | 2 +- src/{view.rs => ui.rs} | 74 +++++++++++++++++++++--------------------- 3 files changed, 49 insertions(+), 49 deletions(-) rename src/{view.rs => ui.rs} (95%) diff --git a/src/lib.rs b/src/lib.rs index c264586..04ac105 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ pub mod error; pub mod model; pub mod process; pub mod regexes; -pub mod view; +pub mod ui; /// Run copyrat on an input string `buffer`, configured by `Opt`. /// @@ -30,18 +30,18 @@ pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { let hint_style = match &opt.hint_style { None => None, Some(style) => match style { - HintStyleCli::Bold => Some(view::HintStyle::Bold), - HintStyleCli::Italic => Some(view::HintStyle::Italic), - HintStyleCli::Underline => Some(view::HintStyle::Underline), + HintStyleCli::Bold => Some(ui::HintStyle::Bold), + HintStyleCli::Italic => Some(ui::HintStyle::Italic), + HintStyleCli::Underline => Some(ui::HintStyle::Underline), HintStyleCli::Surround => { let (open, close) = opt.hint_surroundings; - Some(view::HintStyle::Surround(open, close)) + Some(ui::HintStyle::Surround(open, close)) } }, }; let selection: Option<(String, bool)> = { - let mut viewbox = view::View::new( + let mut ui = ui::Ui::new( &mut model, opt.unique_hint, opt.focus_wrap_around, @@ -50,7 +50,7 @@ pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { hint_style, ); - viewbox.present() + ui.present() }; selection @@ -90,11 +90,11 @@ pub struct CliOpt { unique_hint: bool, #[clap(flatten)] - colors: view::ViewColors, + colors: ui::UiColors, /// Align hint with its match. #[clap(short = "a", long, arg_enum, default_value = "leading")] - hint_alignment: view::HintAlignment, + hint_alignment: ui::HintAlignment, /// Move focus back to first/last match. #[clap(long)] @@ -124,7 +124,7 @@ pub struct CliOpt { } /// Type introduced due to parsing limitation, -/// as we cannot directly parse into view::HintStyle. +/// as we cannot directly parse into ui::HintStyle. #[derive(Debug, Clap)] enum HintStyleCli { Bold, @@ -185,7 +185,7 @@ impl CliOpt { "@copyrat-hint-bg" => self.colors.hint_bg = colors::parse_color(value)?, "@copyrat-hint-alignment" => { - self.hint_alignment = view::HintAlignment::from_str(&value)? + self.hint_alignment = ui::HintAlignment::from_str(&value)? } "@copyrat-hint-style" => self.hint_style = Some(HintStyleCli::from_str(&value)?), diff --git a/src/model.rs b/src/model.rs index fc372dc..8fc36ec 100644 --- a/src/model.rs +++ b/src/model.rs @@ -45,7 +45,7 @@ impl<'a> fmt::Debug for RawMatch<'a> { } } -/// Holds data for the `View`. +/// Holds data for the `Ui`. pub struct Model<'a> { pub lines: &'a Vec<&'a str>, alphabet: &'a Alphabet, diff --git a/src/view.rs b/src/ui.rs similarity index 95% rename from src/view.rs rename to src/ui.rs index fbc1f7e..c39ab18 100644 --- a/src/view.rs +++ b/src/ui.rs @@ -9,31 +9,31 @@ use termion::{self, color, cursor, event, style}; use crate::error::ParseError; use crate::{colors, model}; -pub struct View<'a> { +pub struct Ui<'a> { model: &'a mut model::Model<'a>, matches: Vec>, lookup_trie: SequenceTrie, focus_index: usize, focus_wrap_around: bool, hint_alignment: &'a HintAlignment, - rendering_colors: &'a ViewColors, + rendering_colors: &'a UiColors, hint_style: Option, } -impl<'a> View<'a> { +impl<'a> Ui<'a> { pub fn new( model: &'a mut model::Model<'a>, unique_hint: bool, focus_wrap_around: bool, hint_alignment: &'a HintAlignment, - rendering_colors: &'a ViewColors, + rendering_colors: &'a UiColors, hint_style: Option, - ) -> View<'a> { + ) -> Ui<'a> { 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 }; - View { + Ui { model, matches, lookup_trie, @@ -108,7 +108,7 @@ impl<'a> View<'a> { /// # Notes /// - All trailing whitespaces are trimmed, empty lines are skipped. /// - This writes directly on the writer, avoiding extra allocation. - fn render_base_text(stdout: &mut dyn io::Write, lines: &Vec<&str>, colors: &ViewColors) -> () { + fn render_base_text(stdout: &mut dyn io::Write, lines: &Vec<&str>, colors: &UiColors) -> () { for (index, line) in lines.iter().enumerate() { let trimmed_line = line.trim_end(); @@ -140,7 +140,7 @@ impl<'a> View<'a> { text: &str, focused: bool, offset: (usize, usize), - colors: &ViewColors, + colors: &UiColors, ) { // To help identify it, the match thas has focus is rendered with a dedicated color. let (fg_color, bg_color) = if focused { @@ -177,7 +177,7 @@ impl<'a> View<'a> { stdout: &mut dyn io::Write, hint_text: &str, offset: (usize, usize), - colors: &ViewColors, + colors: &UiColors, hint_style: &Option, ) { let fg_color = color::Fg(colors.hint_fg.as_ref()); @@ -271,7 +271,7 @@ impl<'a> View<'a> { let (offset_x, offset_y) = self.match_offsets(mat); - View::render_matched_text( + Ui::render_matched_text( stdout, text, focused, @@ -288,7 +288,7 @@ impl<'a> View<'a> { HintAlignment::Trailing => text.len() - mat.hint.len(), }; - View::render_matched_hint( + Ui::render_matched_hint( stdout, &mat.hint, (offset_x + extra_offset, offset_y), @@ -298,7 +298,7 @@ impl<'a> View<'a> { } } - /// Full nender the view on the provided writer. + /// Full nender the Ui on the provided writer. /// /// This renders in 3 phases: /// - all lines are rendered verbatim @@ -314,7 +314,7 @@ impl<'a> View<'a> { /// and `hint` are rendered in their proper position. fn full_render(&self, stdout: &mut dyn io::Write) -> () { // 1. Trim all lines and render non-empty ones. - View::render_base_text(stdout, self.model.lines, &self.rendering_colors); + Ui::render_base_text(stdout, self.model.lines, &self.rendering_colors); for (index, mat) in self.matches.iter().enumerate() { let focused = index == self.focus_index; @@ -472,7 +472,7 @@ impl<'a> View<'a> { Event::Exit } - /// Configure the terminal and display the `View`. + /// Configure the terminal and display the `Ui`. /// /// - Setup steps: switch to alternate screen, switch to raw mode, hide the cursor. /// - Teardown steps: show cursor, back to main screen. @@ -539,7 +539,7 @@ pub enum HintStyle { Surround(char, char), } -/// Returned value after the `View` has finished listening to events. +/// Returned value after the `Ui` has finished listening to events. enum Event { /// Exit with no selected matches, Exit, @@ -561,7 +561,7 @@ path: /usr/local/bin/git path: /usr/local/bin/cargo"; let lines: Vec<&str> = content.split('\n').collect(); - let colors = ViewColors { + let colors = UiColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), focused_fg: Box::new(color::Red), @@ -573,7 +573,7 @@ path: /usr/local/bin/cargo"; }; let mut writer = vec![]; - View::render_base_text(&mut writer, &lines, &colors); + Ui::render_base_text(&mut writer, &lines, &colors); let goto1 = cursor::Goto(1, 1); let goto2 = cursor::Goto(1, 2); @@ -599,7 +599,7 @@ path: /usr/local/bin/cargo"; let text = "https://en.wikipedia.org/wiki/Barcelona"; let focused = true; let offset: (usize, usize) = (3, 1); - let colors = ViewColors { + let colors = UiColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), focused_fg: Box::new(color::Red), @@ -610,7 +610,7 @@ path: /usr/local/bin/cargo"; hint_bg: Box::new(color::Cyan), }; - View::render_matched_text(&mut writer, text, focused, offset, &colors); + Ui::render_matched_text(&mut writer, text, focused, offset, &colors); assert_eq!( writer, @@ -633,7 +633,7 @@ path: /usr/local/bin/cargo"; let text = "https://en.wikipedia.org/wiki/Barcelona"; let focused = false; let offset: (usize, usize) = (3, 1); - let colors = ViewColors { + let colors = UiColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), focused_fg: Box::new(color::Red), @@ -644,7 +644,7 @@ path: /usr/local/bin/cargo"; hint_bg: Box::new(color::Cyan), }; - View::render_matched_text(&mut writer, text, focused, offset, &colors); + Ui::render_matched_text(&mut writer, text, focused, offset, &colors); assert_eq!( writer, @@ -666,7 +666,7 @@ path: /usr/local/bin/cargo"; let mut writer = vec![]; let hint_text = "eo"; let offset: (usize, usize) = (3, 1); - let colors = ViewColors { + let colors = UiColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), focused_fg: Box::new(color::Red), @@ -680,7 +680,7 @@ path: /usr/local/bin/cargo"; let extra_offset = 0; let hint_style = None; - View::render_matched_hint( + Ui::render_matched_hint( &mut writer, hint_text, (offset.0 + extra_offset, offset.1), @@ -708,7 +708,7 @@ path: /usr/local/bin/cargo"; let mut writer = vec![]; let hint_text = "eo"; let offset: (usize, usize) = (3, 1); - let colors = ViewColors { + let colors = UiColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), focused_fg: Box::new(color::Red), @@ -722,7 +722,7 @@ path: /usr/local/bin/cargo"; let extra_offset = 0; let hint_style = Some(HintStyle::Underline); - View::render_matched_hint( + Ui::render_matched_hint( &mut writer, hint_text, (offset.0 + extra_offset, offset.1), @@ -752,7 +752,7 @@ path: /usr/local/bin/cargo"; let mut writer = vec![]; let hint_text = "eo"; let offset: (usize, usize) = (3, 1); - let colors = ViewColors { + let colors = UiColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), focused_fg: Box::new(color::Red), @@ -766,7 +766,7 @@ path: /usr/local/bin/cargo"; let extra_offset = 0; let hint_style = Some(HintStyle::Surround('{', '}')); - View::render_matched_hint( + Ui::render_matched_hint( &mut writer, hint_text, (offset.0 + extra_offset, offset.1), @@ -804,7 +804,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let custom_regexes = vec![]; let alphabet = alphabets::Alphabet("abcd".to_string()); let mut model = model::Model::new(&lines, &alphabet, &named_pat, &custom_regexes, false); - let rendering_colors = ViewColors { + let rendering_colors = UiColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), focused_fg: Box::new(color::Red), @@ -816,8 +816,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; }; let hint_alignment = HintAlignment::Leading; - // create a view without any match - let view = View { + // create a Ui without any match + let ui = Ui { model: &mut model, matches: vec![], // no matches lookup_trie: SequenceTrie::new(), @@ -829,7 +829,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; }; let mut writer = vec![]; - view.full_render(&mut writer); + ui.full_render(&mut writer); let goto1 = cursor::Goto(1, 1); let goto3 = cursor::Goto(1, 3); @@ -848,7 +848,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; // println!("{:?}", writer); // println!("{:?}", expected.as_bytes()); - // println!("matches: {}", view.matches.len()); + // println!("matches: {}", ui.matches.len()); // println!("lines: {}", lines.len()); assert_eq!(writer, expected.as_bytes()); @@ -871,7 +871,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let unique_hint = false; let wrap_around = false; - let rendering_colors = ViewColors { + let rendering_colors = UiColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), focused_fg: Box::new(color::Red), @@ -884,7 +884,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let hint_alignment = HintAlignment::Leading; let hint_style = None; - let view = View::new( + let ui = Ui::new( &mut model, unique_hint, wrap_around, @@ -894,7 +894,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; ); let mut writer = vec![]; - view.full_render(&mut writer); + ui.full_render(&mut writer); let expected_content = { let goto1 = cursor::Goto(1, 1); @@ -984,7 +984,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; // .find(|(_idx, (&l, &r))| l != r); // println!("{:?}", diff_point); - assert_eq!(2, view.matches.len()); + assert_eq!(2, ui.matches.len()); assert_eq!(writer, expected.as_bytes()); } @@ -996,7 +996,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; /// - `normal_*` colors are used to render other matched text. /// - `hint_*` colors are used to render the hints. #[derive(Clap, Debug)] -pub struct ViewColors { +pub struct UiColors { /// Foreground color for base text. #[clap(long, default_value = "bright-cyan", parse(try_from_str = colors::parse_color))] pub text_fg: Box, From 0ac40614a0c1438c913d4170a6aa389e96a2fc7d Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 15:34:54 +0200 Subject: [PATCH 33/40] refactor: ui::new() args --- src/lib.rs | 2 +- src/ui.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 04ac105..8fa6f58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,8 +45,8 @@ pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { &mut model, opt.unique_hint, opt.focus_wrap_around, - &opt.hint_alignment, &opt.colors, + &opt.hint_alignment, hint_style, ); diff --git a/src/ui.rs b/src/ui.rs index c39ab18..7a7b609 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -15,8 +15,8 @@ pub struct Ui<'a> { lookup_trie: SequenceTrie, focus_index: usize, focus_wrap_around: bool, - hint_alignment: &'a HintAlignment, rendering_colors: &'a UiColors, + hint_alignment: &'a HintAlignment, hint_style: Option, } @@ -25,8 +25,8 @@ impl<'a> Ui<'a> { model: &'a mut model::Model<'a>, unique_hint: bool, focus_wrap_around: bool, - hint_alignment: &'a HintAlignment, rendering_colors: &'a UiColors, + hint_alignment: &'a HintAlignment, hint_style: Option, ) -> Ui<'a> { let matches = model.matches(unique_hint); @@ -39,8 +39,8 @@ impl<'a> Ui<'a> { lookup_trie, focus_index, focus_wrap_around, - hint_alignment, rendering_colors, + hint_alignment, hint_style, } } @@ -823,8 +823,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; lookup_trie: SequenceTrie::new(), focus_index: 0, focus_wrap_around: false, - hint_alignment: &hint_alignment, rendering_colors: &rendering_colors, + hint_alignment: &hint_alignment, hint_style: None, }; @@ -888,8 +888,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; &mut model, unique_hint, wrap_around, - &hint_alignment, &rendering_colors, + &hint_alignment, hint_style, ); From 45d84dc646afea4f2c6c065bea44736e0ebb256d Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 16:01:20 +0200 Subject: [PATCH 34/40] feat: increase refresh rate 50ms -> 25ms --- src/ui.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui.rs b/src/ui.rs index 7a7b609..7ece37e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -368,7 +368,7 @@ impl<'a> Ui<'a> { if next_key.is_none() { // Nothing in the buffer. Wait for a bit... - std::thread::sleep(std::time::Duration::from_millis(50)); + std::thread::sleep(std::time::Duration::from_millis(25)); continue; } From 005fa7890f5678eed1e7fdb38f2fb806f1cb8b14 Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 17:43:51 +0200 Subject: [PATCH 35/40] refactor: model better structure --- src/model.rs | 76 ++++++++++++++++++++++++++-------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/model.rs b/src/model.rs index 8fc36ec..3034202 100644 --- a/src/model.rs +++ b/src/model.rs @@ -7,44 +7,6 @@ use sequence_trie::SequenceTrie; use crate::alphabets::Alphabet; use crate::regexes::{NamedPattern, EXCLUDE_PATTERNS, PATTERNS}; -/// Represents matched text, its location on screen, the pattern that created -/// it, and the associated hint. -pub struct Match<'a> { - pub x: i32, - pub y: i32, - pub pattern: &'a str, - pub text: &'a str, - pub hint: String, -} - -impl<'a> fmt::Debug for Match<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "Match {{ x: {}, y: {}, pattern: {}, text: {}, hint: <{}> }}", - self.x, self.y, self.pattern, self.text, self.hint, - ) - } -} - -/// Internal surrogate for `Match`, before a Hint has been associated. -struct RawMatch<'a> { - pub x: i32, - pub y: i32, - pub pattern: &'a str, - pub text: &'a str, -} - -impl<'a> fmt::Debug for RawMatch<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "RawMatch {{ x: {}, y: {}, pattern: {}, text: {} }}", - self.x, self.y, self.pattern, self.text, - ) - } -} - /// Holds data for the `Ui`. pub struct Model<'a> { pub lines: &'a Vec<&'a str>, @@ -259,6 +221,44 @@ impl<'a> Model<'a> { } } +/// Represents matched text, its location on screen, the pattern that created +/// it, and the associated hint. +pub struct Match<'a> { + pub x: i32, + pub y: i32, + pub pattern: &'a str, + pub text: &'a str, + pub hint: String, +} + +impl<'a> fmt::Debug for Match<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Match {{ x: {}, y: {}, pattern: {}, text: {}, hint: <{}> }}", + self.x, self.y, self.pattern, self.text, self.hint, + ) + } +} + +/// Internal surrogate for `Match`, before a Hint has been associated. +struct RawMatch<'a> { + pub x: i32, + pub y: i32, + pub pattern: &'a str, + pub text: &'a str, +} + +impl<'a> fmt::Debug for RawMatch<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "RawMatch {{ x: {}, y: {}, pattern: {}, text: {} }}", + self.x, self.y, self.pattern, self.text, + ) + } +} + #[cfg(test)] mod tests { use super::*; From aeca7223a613a504264f58c044ba0738630cd48e Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 1 Jun 2020 20:12:02 +0200 Subject: [PATCH 36/40] refactor: move line creation inside model --- src/lib.rs | 4 +-- src/model.rs | 99 +++++++++++++++++++++++++--------------------------- src/ui.rs | 10 ++---- 3 files changed, 51 insertions(+), 62 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8fa6f58..742ba30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,10 +17,8 @@ pub mod ui; /// /// Maybe the decision to take ownership of the buffer is a bit bold. pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { - let lines: Vec<&str> = buffer.split('\n').collect(); - let mut model = model::Model::new( - &lines, + &buffer, &opt.alphabet, &opt.named_pattern, &opt.custom_regex, diff --git a/src/model.rs b/src/model.rs index 3034202..557c4ff 100644 --- a/src/model.rs +++ b/src/model.rs @@ -9,7 +9,8 @@ use crate::regexes::{NamedPattern, EXCLUDE_PATTERNS, PATTERNS}; /// Holds data for the `Ui`. pub struct Model<'a> { - pub lines: &'a Vec<&'a str>, + // buffer: &'a str, + pub lines: Vec<&'a str>, alphabet: &'a Alphabet, named_patterns: &'a Vec, custom_regexes: &'a Vec, @@ -18,13 +19,16 @@ pub struct Model<'a> { impl<'a> Model<'a> { pub fn new( - lines: &'a Vec<&'a str>, + buffer: &'a str, alphabet: &'a Alphabet, named_patterns: &'a Vec, custom_regexes: &'a Vec, reverse: bool, ) -> Model<'a> { + let lines = buffer.split('\n').collect(); + Model { + // buffer, lines, alphabet, named_patterns, @@ -264,17 +268,13 @@ mod tests { use super::*; use crate::alphabets::Alphabet; - fn split(output: &str) -> Vec<&str> { - output.split("\n").collect::>() - } - #[test] fn match_reverse() { - let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); + let buffer = "lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.first().unwrap().hint, "a"); @@ -283,11 +283,11 @@ mod tests { #[test] fn match_unique() { - let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); + let buffer = "lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(true); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(true); assert_eq!(results.len(), 3); assert_eq!(results.first().unwrap().hint, "a"); @@ -296,11 +296,11 @@ mod tests { #[test] fn match_docker() { - let lines = split("latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago"); + let buffer = "latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!( @@ -311,11 +311,11 @@ mod tests { #[test] fn match_ansi_colors() { - let lines = split("path: /var/log/nginx.log\npath: test/log/nginx-2.log:32folder/.nginx@4df2.log"); + let buffer = "path: /var/log/nginx.log\npath: test/log/nginx-2.log:32folder/.nginx@4df2.log"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text, "/var/log/nginx.log"); @@ -325,11 +325,11 @@ mod tests { #[test] fn match_paths() { - let lines = split("Lorem /tmp/foo/bar_lol, lorem\n Lorem /var/log/boot-strap.log lorem ../log/kern.log lorem"); + let buffer = "Lorem /tmp/foo/bar_lol, lorem\n Lorem /var/log/boot-strap.log lorem ../log/kern.log lorem"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text, "/tmp/foo/bar_lol"); @@ -339,11 +339,11 @@ mod tests { #[test] fn match_home() { - let lines = split("Lorem ~/.gnu/.config.txt, lorem"); + let buffer = "Lorem ~/.gnu/.config.txt, lorem"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!(results.get(0).unwrap().text, "~/.gnu/.config.txt"); @@ -351,23 +351,22 @@ mod tests { #[test] fn match_uuids() { - let lines = - split("Lorem ipsum 123e4567-e89b-12d3-a456-426655440000 lorem\n Lorem lorem lorem"); + let buffer = "Lorem ipsum 123e4567-e89b-12d3-a456-426655440000 lorem\n Lorem lorem lorem"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); } #[test] fn match_shas() { - let lines = split("Lorem fd70b5695 5246ddf f924213 lorem\n Lorem 973113963b491874ab2e372ee60d4b4cb75f717c lorem"); + let buffer = "Lorem fd70b5695 5246ddf f924213 lorem\n Lorem 973113963b491874ab2e372ee60d4b4cb75f717c lorem"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 4); assert_eq!(results.get(0).unwrap().text, "fd70b5695"); @@ -381,12 +380,11 @@ mod tests { #[test] fn match_ips() { - let lines = - split("Lorem ipsum 127.0.0.1 lorem\n Lorem 255.255.10.255 lorem 127.0.0.1 lorem"); + let buffer = "Lorem ipsum 127.0.0.1 lorem\n Lorem 255.255.10.255 lorem 127.0.0.1 lorem"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text, "127.0.0.1"); @@ -396,11 +394,11 @@ mod tests { #[test] fn match_ipv6s() { - let lines = split("Lorem ipsum fe80::2:202:fe4 lorem\n Lorem 2001:67c:670:202:7ba8:5e41:1591:d723 lorem fe80::2:1 lorem ipsum fe80:22:312:fe::1%eth0"); + let buffer = "Lorem ipsum fe80::2:202:fe4 lorem\n Lorem 2001:67c:670:202:7ba8:5e41:1591:d723 lorem fe80::2:1 lorem ipsum fe80:22:312:fe::1%eth0"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 4); assert_eq!(results.get(0).unwrap().text, "fe80::2:202:fe4"); @@ -414,13 +412,12 @@ mod tests { #[test] fn match_markdown_urls() { - let lines = split( - "Lorem ipsum [link](https://github.io?foo=bar) ![](http://cdn.com/img.jpg) lorem", - ); + let buffer = + "Lorem ipsum [link](https://github.io?foo=bar) ![](http://cdn.com/img.jpg) lorem"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 2); assert_eq!(results.get(0).unwrap().pattern, "markdown_url"); @@ -431,11 +428,11 @@ mod tests { #[test] fn match_urls() { - let lines = split("Lorem ipsum https://www.rust-lang.org/tools lorem\n Lorem ipsumhttps://crates.io lorem https://github.io?foo=bar lorem ssh://github.io"); + let buffer = "Lorem ipsum https://www.rust-lang.org/tools lorem\n Lorem ipsumhttps://crates.io lorem https://github.io?foo=bar lorem ssh://github.io"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 4); assert_eq!( @@ -453,11 +450,11 @@ mod tests { #[test] fn match_addresses() { - let lines = split("Lorem 0xfd70b5695 0x5246ddf lorem\n Lorem 0x973113tlorem"); + let buffer = "Lorem 0xfd70b5695 0x5246ddf lorem\n Lorem 0x973113tlorem"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 3); assert_eq!(results.get(0).unwrap().text, "0xfd70b5695"); @@ -467,12 +464,11 @@ mod tests { #[test] fn match_hex_colors() { - let lines = - split("Lorem #fd7b56 lorem #FF00FF\n Lorem #00fF05 lorem #abcd00 lorem #afRR00"); + let buffer = "Lorem #fd7b56 lorem #FF00FF\n Lorem #00fF05 lorem #abcd00 lorem #afRR00"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 4); assert_eq!(results.get(0).unwrap().text, "#fd7b56"); @@ -483,11 +479,11 @@ mod tests { #[test] fn match_ipfs() { - let lines = split("Lorem QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ lorem Qmfoobar"); + let buffer = "Lorem QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ lorem Qmfoobar"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!( @@ -498,23 +494,22 @@ mod tests { #[test] fn match_process_port() { - let lines = - split("Lorem 5695 52463 lorem\n Lorem 973113 lorem 99999 lorem 8888 lorem\n 23456 lorem 5432 lorem 23444"); + let buffer = "Lorem 5695 52463 lorem\n Lorem 973113 lorem 99999 lorem 8888 lorem\n 23456 lorem 5432 lorem 23444"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 8); } #[test] fn match_diff_a() { - let lines = split("Lorem lorem\n--- a/src/main.rs"); + let buffer = "Lorem lorem\n--- a/src/main.rs"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!(results.get(0).unwrap().text, "src/main.rs"); @@ -522,11 +517,11 @@ mod tests { #[test] fn match_diff_b() { - let lines = split("Lorem lorem\n+++ b/src/main.rs"); + let buffer = "Lorem lorem\n+++ b/src/main.rs"; let named_pat = vec![]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 1); assert_eq!(results.get(0).unwrap().text, "src/main.rs"); @@ -534,7 +529,7 @@ mod tests { #[test] fn priority() { - let lines = split("Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"); + let buffer = "Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"; let named_pat = vec![]; let custom: Vec = ["CUSTOM-[0-9]{4,}", "ISSUE-[0-9]{3}"] @@ -542,7 +537,7 @@ mod tests { .map(|&s| s.to_string()) .collect(); let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 9); assert_eq!(results.get(0).unwrap().text, "http://foo.bar"); @@ -564,14 +559,14 @@ mod tests { #[test] fn named_patterns() { - let lines = split("Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"); + let buffer = "Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"; use crate::regexes::parse_pattern_name; let named_pat = vec![parse_pattern_name("url").unwrap()]; let custom = vec![]; let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(&lines, &alphabet, &named_pat, &custom, false).matches(false); + let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); assert_eq!(results.len(), 2); assert_eq!(results.get(0).unwrap().text, "http://foo.bar"); diff --git a/src/ui.rs b/src/ui.rs index 7ece37e..58b68f7 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -314,7 +314,7 @@ impl<'a> Ui<'a> { /// and `hint` are rendered in their proper position. fn full_render(&self, stdout: &mut dyn io::Write) -> () { // 1. Trim all lines and render non-empty ones. - Ui::render_base_text(stdout, self.model.lines, &self.rendering_colors); + Ui::render_base_text(stdout, &self.model.lines, &self.rendering_colors); for (index, mat) in self.matches.iter().enumerate() { let focused = index == self.focus_index; @@ -798,12 +798,10 @@ path: /usr/local/bin/cargo"; Barcelona https://en.wikipedia.org/wiki/Barcelona - "; - let lines = content.split('\n').collect(); - let named_pat = vec![]; let custom_regexes = vec![]; let alphabet = alphabets::Alphabet("abcd".to_string()); - let mut model = model::Model::new(&lines, &alphabet, &named_pat, &custom_regexes, false); + let mut model = model::Model::new(content, &alphabet, &named_pat, &custom_regexes, false); let rendering_colors = UiColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), @@ -861,13 +859,11 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; Barcelona https://en.wikipedia.org/wiki/Barcelona - "; - let lines = content.split('\n').collect(); - let named_pat = vec![]; let custom_regexes = vec![]; let alphabet = alphabets::Alphabet("abcd".to_string()); let reverse = true; - let mut model = model::Model::new(&lines, &alphabet, &named_pat, &custom_regexes, reverse); + let mut model = model::Model::new(content, &alphabet, &named_pat, &custom_regexes, reverse); let unique_hint = false; let wrap_around = false; From a98c4598ba94df129f18c777a2cf82c3b2509284 Mon Sep 17 00:00:00 2001 From: graelo Date: Tue, 2 Jun 2020 09:38:42 +0200 Subject: [PATCH 37/40] feat(ui): more efficient base text render --- src/ui.rs | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 58b68f7..75e85db 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -109,23 +109,35 @@ impl<'a> Ui<'a> { /// - All trailing whitespaces are trimmed, empty lines are skipped. /// - This writes directly on the writer, avoiding extra allocation. fn render_base_text(stdout: &mut dyn io::Write, lines: &Vec<&str>, 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(); + for (index, line) in lines.iter().enumerate() { let trimmed_line = line.trim_end(); if !trimmed_line.is_empty() { write!( stdout, - "{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}", + "{goto}{text}", goto = cursor::Goto(1, index as u16 + 1), - fg_color = color::Fg(colors.text_fg.as_ref()), - bg_color = color::Bg(colors.text_bg.as_ref()), - fg_reset = color::Fg(color::Reset), - bg_reset = color::Bg(color::Reset), text = &trimmed_line, ) .unwrap(); } } + + write!( + stdout, + "{fg_reset}{bg_reset}", + fg_reset = color::Fg(color::Reset), + bg_reset = color::Bg(color::Reset), + ) + .unwrap(); } /// Render the Match's `text` field on provided writer using the `match_*g` color. @@ -582,7 +594,7 @@ path: /usr/local/bin/cargo"; assert_eq!( writer, format!( - "{g1}{bg}{fg}some text{fg_reset}{bg_reset}{g2}{bg}{fg}* e006b06 - (12 days ago) swapper: Make quotes{fg_reset}{bg_reset}{g3}{bg}{fg}path: /usr/local/bin/git{fg_reset}{bg_reset}{g6}{bg}{fg}path: /usr/local/bin/cargo{fg_reset}{bg_reset}", + "{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}", g1 = goto1, g2 = goto2, g3 = goto3, g6 = goto6, fg = color::Fg(colors.text_fg.as_ref()), bg = color::Bg(colors.text_bg.as_ref()), @@ -833,8 +845,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let goto3 = cursor::Goto(1, 3); let expected = format!( - "{goto1}{bg}{fg}lorem 127.0.0.1 lorem{fg_reset}{bg_reset}\ - {goto3}{bg}{fg}Barcelona https://en.wikipedia.org/wiki/Barcelona -{fg_reset}{bg_reset}", + "{bg}{fg}{goto1}lorem 127.0.0.1 lorem\ + {goto3}Barcelona https://en.wikipedia.org/wiki/Barcelona -{fg_reset}{bg_reset}", goto1 = goto1, goto3 = goto3, fg = color::Fg(rendering_colors.text_fg.as_ref()), @@ -897,8 +909,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let goto3 = cursor::Goto(1, 3); format!( - "{goto1}{bg}{fg}lorem 127.0.0.1 lorem{fg_reset}{bg_reset}\ - {goto3}{bg}{fg}Barcelona https://en.wikipedia.org/wiki/Barcelona -{fg_reset}{bg_reset}", + "{bg}{fg}{goto1}lorem 127.0.0.1 lorem\ + {goto3}Barcelona https://en.wikipedia.org/wiki/Barcelona -{fg_reset}{bg_reset}", goto1 = goto1, goto3 = goto3, fg = color::Fg(rendering_colors.text_fg.as_ref()), From 45a408354705ae93a6efcee7b825452c30b6f01d Mon Sep 17 00:00:00 2001 From: graelo Date: Tue, 2 Jun 2020 11:17:35 +0200 Subject: [PATCH 38/40] refactor: ui factor out goto cursor --- src/ui.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 75e85db..b75bb01 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -196,13 +196,14 @@ impl<'a> Ui<'a> { let bg_color = color::Bg(colors.hint_bg.as_ref()); let fg_reset = color::Fg(color::Reset); let bg_reset = color::Bg(color::Reset); + let goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1); match hint_style { None => { write!( stdout, "{goto}{bg_color}{fg_color}{hint}{fg_reset}{bg_reset}", - goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + goto = goto, fg_color = fg_color, bg_color = bg_color, fg_reset = fg_reset, @@ -216,7 +217,7 @@ impl<'a> Ui<'a> { write!( stdout, "{goto}{bg_color}{fg_color}{sty}{hint}{sty_reset}{fg_reset}{bg_reset}", - goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + goto = goto, fg_color = fg_color, bg_color = bg_color, fg_reset = fg_reset, @@ -231,7 +232,7 @@ impl<'a> Ui<'a> { write!( stdout, "{goto}{bg_color}{fg_color}{sty}{hint}{sty_reset}{fg_reset}{bg_reset}", - goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + goto = goto, fg_color = fg_color, bg_color = bg_color, fg_reset = fg_reset, @@ -246,7 +247,7 @@ impl<'a> Ui<'a> { write!( stdout, "{goto}{bg_color}{fg_color}{sty}{hint}{sty_reset}{fg_reset}{bg_reset}", - goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + goto = goto, fg_color = fg_color, bg_color = bg_color, fg_reset = fg_reset, @@ -261,7 +262,7 @@ impl<'a> Ui<'a> { write!( stdout, "{goto}{bg_color}{fg_color}{bra}{hint}{bra_close}{fg_reset}{bg_reset}", - goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), + goto = goto, fg_color = fg_color, bg_color = bg_color, fg_reset = fg_reset, From 193dca67e3e5f64fcff83645fc70529dc32563cf Mon Sep 17 00:00:00 2001 From: graelo Date: Tue, 2 Jun 2020 13:10:48 +0200 Subject: [PATCH 39/40] feat: capture wrapped lines --- src/model.rs | 23 ++--------------- src/tmux.rs | 2 +- src/ui.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 71 insertions(+), 27 deletions(-) diff --git a/src/model.rs b/src/model.rs index 557c4ff..4334fdc 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,5 +1,4 @@ use std::collections; -use std::fmt; use regex::Regex; use sequence_trie::SequenceTrie; @@ -227,6 +226,7 @@ impl<'a> Model<'a> { /// Represents matched text, its location on screen, the pattern that created /// it, and the associated hint. +#[derive(Debug)] pub struct Match<'a> { pub x: i32, pub y: i32, @@ -235,17 +235,8 @@ pub struct Match<'a> { pub hint: String, } -impl<'a> fmt::Debug for Match<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "Match {{ x: {}, y: {}, pattern: {}, text: {}, hint: <{}> }}", - self.x, self.y, self.pattern, self.text, self.hint, - ) - } -} - /// Internal surrogate for `Match`, before a Hint has been associated. +#[derive(Debug)] struct RawMatch<'a> { pub x: i32, pub y: i32, @@ -253,16 +244,6 @@ struct RawMatch<'a> { pub text: &'a str, } -impl<'a> fmt::Debug for RawMatch<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "RawMatch {{ x: {}, y: {}, pattern: {}, text: {} }}", - self.x, self.y, self.pattern, self.text, - ) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/tmux.rs b/src/tmux.rs index 7f5d5a2..acff280 100644 --- a/src/tmux.rs +++ b/src/tmux.rs @@ -205,7 +205,7 @@ pub fn get_options(prefix: &str) -> Result, ParseError> /// position. To support both cases, the implementation always provides those /// parameters to tmux. pub fn capture_pane(pane: &Pane, region: &CaptureRegion) -> Result { - let mut args = format!("capture-pane -t {pane_id} -p", pane_id = pane.id); + let mut args = format!("capture-pane -t {pane_id} -J -p", pane_id = pane.id); let region_str = match region { CaptureRegion::VisibleArea => { diff --git a/src/ui.rs b/src/ui.rs index b75bb01..f2e6574 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -11,6 +11,8 @@ use crate::{colors, model}; pub struct Ui<'a> { model: &'a mut model::Model<'a>, + term_width: u16, + line_offsets: Vec, matches: Vec>, lookup_trie: SequenceTrie, focus_index: usize, @@ -33,8 +35,13 @@ impl<'a> Ui<'a> { let lookup_trie = model::Model::build_lookup_trie(&matches); let focus_index = if model.reverse { matches.len() - 1 } else { 0 }; + let (term_width, _) = termion::terminal_size().expect("Cannot read the terminal size."); + let line_offsets = get_line_offsets(&model.lines, term_width); + Ui { model, + term_width, + line_offsets, matches, lookup_trie, focus_index, @@ -45,6 +52,25 @@ impl<'a> Ui<'a> { } } + /// 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) + } + /// 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) { @@ -108,7 +134,12 @@ impl<'a> Ui<'a> { /// # Notes /// - All trailing whitespaces are trimmed, empty lines are skipped. /// - This writes directly on the writer, avoiding extra allocation. - fn render_base_text(stdout: &mut dyn io::Write, lines: &Vec<&str>, colors: &UiColors) -> () { + fn render_base_text( + stdout: &mut dyn io::Write, + lines: &Vec<&str>, + line_offsets: &Vec, + colors: &UiColors, + ) -> () { write!( stdout, "{bg_color}{fg_color}", @@ -117,14 +148,17 @@ impl<'a> Ui<'a> { ) .unwrap(); - for (index, line) in lines.iter().enumerate() { + for (line_index, line) in lines.iter().enumerate() { let trimmed_line = line.trim_end(); if !trimmed_line.is_empty() { + let offset_y: usize = + *(line_offsets.get(line_index)).expect("Cannot get offset_per_line."); + write!( stdout, "{goto}{text}", - goto = cursor::Goto(1, index as u16 + 1), + goto = cursor::Goto(1, offset_y as u16 + 1), text = &trimmed_line, ) .unwrap(); @@ -283,6 +317,7 @@ impl<'a> Ui<'a> { let text = mat.text; let (offset_x, offset_y) = self.match_offsets(mat); + let (offset_x, offset_y) = self.map_coords_to_wrapped_space(offset_x, offset_y); Ui::render_matched_text( stdout, @@ -327,7 +362,12 @@ impl<'a> Ui<'a> { /// and `hint` are rendered in their proper position. fn full_render(&self, stdout: &mut dyn io::Write) -> () { // 1. Trim all lines and render non-empty ones. - Ui::render_base_text(stdout, &self.model.lines, &self.rendering_colors); + Ui::render_base_text( + stdout, + &self.model.lines, + &self.line_offsets, + &self.rendering_colors, + ); for (index, mat) in self.matches.iter().enumerate() { let focused = index == self.focus_index; @@ -514,6 +554,23 @@ impl<'a> Ui<'a> { } } +/// 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 { + 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() +} + /// Describes if, during rendering, a hint should aligned to the leading edge of /// the matched text, or to its trailing edge. #[derive(Debug, Clap)] @@ -574,6 +631,8 @@ path: /usr/local/bin/git path: /usr/local/bin/cargo"; let lines: Vec<&str> = content.split('\n').collect(); + let line_offsets: Vec = (0..lines.len()).collect(); + let colors = UiColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), @@ -586,7 +645,7 @@ path: /usr/local/bin/cargo"; }; let mut writer = vec![]; - Ui::render_base_text(&mut writer, &lines, &colors); + Ui::render_base_text(&mut writer, &lines, &line_offsets, &colors); let goto1 = cursor::Goto(1, 1); let goto2 = cursor::Goto(1, 2); @@ -815,6 +874,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let custom_regexes = vec![]; let alphabet = alphabets::Alphabet("abcd".to_string()); let mut model = model::Model::new(content, &alphabet, &named_pat, &custom_regexes, false); + let term_width: u16 = 80; + let line_offsets = get_line_offsets(&model.lines, term_width); let rendering_colors = UiColors { text_fg: Box::new(color::Black), text_bg: Box::new(color::White), @@ -830,6 +891,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; // create a Ui without any match let ui = Ui { model: &mut model, + term_width, + line_offsets, matches: vec![], // no matches lookup_trie: SequenceTrie::new(), focus_index: 0, From 7ffe2fbf7a86136991f4bdaf5332fe7d6c1adea0 Mon Sep 17 00:00:00 2001 From: graelo Date: Tue, 2 Jun 2020 20:29:13 +0200 Subject: [PATCH 40/40] build(github-actions): update workflow --- .github/workflows/rust.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7eb1343..586e578 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -12,10 +12,10 @@ jobs: - name: Build run: cargo build --release - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v2 with: - name: thumbs-macos.zip - path: ./target/release/thumbs + name: copyrat-macos.zip + path: ./target/release/*copyrat build-linux: runs-on: ubuntu-latest @@ -31,7 +31,7 @@ jobs: - name: Build run: cargo build --release - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v2 with: - name: thumbs-linux.zip - path: ./target/release/thumbs + name: copyrat-linux.zip + path: ./target/release/*copyrat