feat: copyrat

This commit is contained in:
graelo 2020-05-24 21:02:11 +02:00
parent 0d45a2872a
commit 37f22b67af
11 changed files with 840 additions and 728 deletions

View file

@ -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<String> {
let letters: Vec<String> = self.letters.chars().map(|s| s.to_string()).collect();
let letters: Vec<String> = self.0.chars().map(|s| s.to_string()).collect();
let mut expansion = letters.clone();
let mut expanded: Vec<String> = 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<Alphabet, error::ParseError> {
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"]);
}

View file

@ -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<Box<dyn color::Color>, 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");
}
}

18
src/error.rs Normal file
View file

@ -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, ...)"),
}
}
}

View file

@ -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<String>,
/// 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<HintStyleCli>,
/// 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<path::PathBuf>,
/// 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<char> = 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::<Vec<_>>()
} 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, &regexp);
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::<Vec<_>>()
.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::<Vec<&str>>()
.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();
}
}
}
}

View file

@ -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<String>,
}
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<String>) -> 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::<Vec<&str>>()
@ -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<String> = ["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");

View file

@ -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>) -> String;
fn last_executed(&self) -> Option<Vec<String>>;
fn execute(&mut self, args: Vec<String>) -> String;
fn last_executed(&self) -> Option<Vec<String>>;
}
struct RealShell {
executed: Option<Vec<String>>,
executed: Option<Vec<String>>,
}
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>) -> 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>) -> 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<Vec<String>> {
self.executed.clone()
}
fn last_executed(&self) -> Option<Vec<String>> {
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<String>,
active_pane_height: Option<i32>,
active_pane_scroll_position: Option<i32>,
active_pane_in_copy_mode: Option<String>,
thumbs_pane_id: Option<String>,
content: Option<String>,
signal: String,
executor: Box<&'a mut dyn Executor>,
directory: &'a path::Path,
command: &'a str,
alt_command: &'a str,
active_pane_id: Option<String>,
active_pane_height: Option<i32>,
active_pane_scroll_position: Option<i32>,
active_pane_in_copy_mode: Option<String>,
thumbs_pane_id: Option<String>,
content: Option<String>,
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<Vec<&str>> = lines.into_iter().map(|line| line.split(':').collect()).collect();
let lines: Vec<&str> = output.split('\n').collect();
let chunks: Vec<Vec<&str>> = 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<String> = 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::<Vec<String>>();
}
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<String> = 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::<Vec<String>>();
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<String> = thumbs_command.iter().map(|arg| arg.to_string()).collect();
let params: Vec<String> = 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<String>,
executed: Option<Vec<String>>,
}
impl TestShell {
fn new(outputs: Vec<String>) -> TestShell {
TestShell {
executed: None,
outputs,
}
}
}
impl Executor for TestShell {
fn execute(&mut self, args: Vec<String>) -> String {
self.executed = Some(args);
self.outputs.pop().unwrap()
struct TestShell {
outputs: Vec<String>,
executed: Option<Vec<String>>,
}
fn last_executed(&self) -> Option<Vec<String>> {
self.executed.clone()
impl TestShell {
fn new(outputs: Vec<String>) -> 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>) -> String {
self.executed = Some(args);
self.outputs.pop().unwrap()
}
swapper.capture_active_pane();
fn last_executed(&self) -> Option<Vec<String>> {
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(())
}

View file

@ -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<HintStyle>,
}
@ -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<dyn color::Color>,
/// Background color for matches.
#[clap(long, default_value = "black",
parse(try_from_str = colors::parse_color))]
match_bg: Box<dyn color::Color>,
/// Foreground color for the focused match.
#[clap(long, default_value = "blue",
parse(try_from_str = colors::parse_color))]
focused_fg: Box<dyn color::Color>,
/// Background color for the focused match.
#[clap(long, default_value = "black",
parse(try_from_str = colors::parse_color))]
focused_bg: Box<dyn color::Color>,
/// Foreground color for hints.
#[clap(long, default_value = "white",
parse(try_from_str = colors::parse_color))]
hint_fg: Box<dyn color::Color>,
/// Background color for hints.
#[clap(long, default_value = "black",
parse(try_from_str = colors::parse_color))]
hint_bg: Box<dyn color::Color>,
}
/// 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<HintStyle>,
) {
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)
)