From 905bd2862cde73629ce32128141fede79d675966 Mon Sep 17 00:00:00 2001 From: graelo Date: Mon, 25 May 2020 23:06:00 +0200 Subject: [PATCH] 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()); + } }