mirror of
https://github.com/TECHNOFAB11/tmux-copyrat.git
synced 2025-12-13 00:20:08 +01:00
chore: a new start
This commit is contained in:
commit
34d0bb5a35
21 changed files with 2319 additions and 0 deletions
108
src/alphabets.rs
Normal file
108
src/alphabets.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
const ALPHABETS: [(&'static str, &'static str); 22] = [
|
||||
("numeric", "1234567890"),
|
||||
("abcd", "abcd"),
|
||||
("qwerty", "asdfqwerzxcvjklmiuopghtybn"),
|
||||
("qwerty-homerow", "asdfjklgh"),
|
||||
("qwerty-left-hand", "asdfqwerzcxv"),
|
||||
("qwerty-right-hand", "jkluiopmyhn"),
|
||||
("azerty", "qsdfazerwxcvjklmuiopghtybn"),
|
||||
("azerty-homerow", "qsdfjkmgh"),
|
||||
("azerty-left-hand", "qsdfazerwxcv"),
|
||||
("azerty-right-hand", "jklmuiophyn"),
|
||||
("qwertz", "asdfqweryxcvjkluiopmghtzbn"),
|
||||
("qwertz-homerow", "asdfghjkl"),
|
||||
("qwertz-left-hand", "asdfqweryxcv"),
|
||||
("qwertz-right-hand", "jkluiopmhzn"),
|
||||
("dvorak", "aoeuqjkxpyhtnsgcrlmwvzfidb"),
|
||||
("dvorak-homerow", "aoeuhtnsid"),
|
||||
("dvorak-left-hand", "aoeupqjkyix"),
|
||||
("dvorak-right-hand", "htnsgcrlmwvz"),
|
||||
("colemak", "arstqwfpzxcvneioluymdhgjbk"),
|
||||
("colemak-homerow", "arstneiodh"),
|
||||
("colemak-left-hand", "arstqwfpzxcv"),
|
||||
("colemak-right-hand", "neioluymjhk"),
|
||||
];
|
||||
|
||||
pub struct Alphabet<'a> {
|
||||
letters: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Alphabet<'a> {
|
||||
fn new(letters: &'a str) -> Alphabet {
|
||||
Alphabet { letters }
|
||||
}
|
||||
|
||||
pub fn hints(&self, matches: usize) -> Vec<String> {
|
||||
let letters: Vec<String> = self.letters.chars().map(|s| s.to_string()).collect();
|
||||
|
||||
let mut expansion = letters.clone();
|
||||
let mut expanded: Vec<String> = Vec::new();
|
||||
|
||||
loop {
|
||||
if expansion.len() + expanded.len() >= matches {
|
||||
break;
|
||||
}
|
||||
if expansion.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let prefix = expansion.pop().expect("Ouch!");
|
||||
let sub_expansion: Vec<String> = letters
|
||||
.iter()
|
||||
.take(matches - expansion.len() - expanded.len())
|
||||
.map(|s| prefix.clone() + s)
|
||||
.collect();
|
||||
|
||||
expanded.splice(0..0, sub_expansion);
|
||||
}
|
||||
|
||||
expansion = expansion.iter().take(matches - expanded.len()).cloned().collect();
|
||||
expansion.append(&mut expanded);
|
||||
expansion
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_alphabet(alphabet_name: &str) -> Alphabet {
|
||||
let alphabets: HashMap<&str, &str> = ALPHABETS.iter().cloned().collect();
|
||||
|
||||
alphabets
|
||||
.get(alphabet_name)
|
||||
.expect(format!("Unknown alphabet: {}", alphabet_name).as_str()); // FIXME
|
||||
|
||||
Alphabet::new(alphabets[alphabet_name])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn simple_matches() {
|
||||
let alphabet = Alphabet::new("abcd");
|
||||
let hints = alphabet.hints(3);
|
||||
assert_eq!(hints, ["a", "b", "c"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composed_matches() {
|
||||
let alphabet = Alphabet::new("abcd");
|
||||
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 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 hints = alphabet.hints(8);
|
||||
assert_eq!(hints, ["aa", "ab", "ba", "bb"]);
|
||||
}
|
||||
}
|
||||
35
src/colors.rs
Normal file
35
src/colors.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
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),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn match_color() {
|
||||
let text1 = println!("{}{}", color::Fg(*get_color("green")), "foo");
|
||||
let text2 = println!("{}{}", color::Fg(color::Green), "foo");
|
||||
|
||||
assert_eq!(text1, text2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn no_match_color() {
|
||||
println!("{}{}", color::Fg(*get_color("wat")), "foo");
|
||||
}
|
||||
}
|
||||
200
src/main.rs
Normal file
200
src/main.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
extern crate clap;
|
||||
extern crate termion;
|
||||
|
||||
mod alphabets;
|
||||
mod colors;
|
||||
mod state;
|
||||
mod view;
|
||||
|
||||
use self::clap::{App, Arg};
|
||||
use clap::crate_version;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::prelude::*;
|
||||
use std::io::{self, Read};
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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 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());
|
||||
|
||||
let stdin = io::stdin();
|
||||
let mut handle = stdin.lock();
|
||||
let mut output = String::new();
|
||||
|
||||
handle.read_to_string(&mut output).unwrap();
|
||||
|
||||
let lines = output.split('\n').collect::<Vec<&str>>();
|
||||
|
||||
let mut state = state::State::new(&lines, alphabet, ®exp);
|
||||
|
||||
let selected = {
|
||||
let mut viewbox = view::View::new(
|
||||
&mut state,
|
||||
multi,
|
||||
reverse,
|
||||
unique,
|
||||
contrast,
|
||||
position,
|
||||
select_foreground_color,
|
||||
select_background_color,
|
||||
foreground_color,
|
||||
background_color,
|
||||
hint_foreground_color,
|
||||
hint_background_color,
|
||||
);
|
||||
|
||||
viewbox.present()
|
||||
};
|
||||
|
||||
if !selected.is_empty() {
|
||||
let output = selected
|
||||
.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");
|
||||
|
||||
if let Some(target) = 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();
|
||||
} else {
|
||||
print!("{}", output);
|
||||
}
|
||||
} else {
|
||||
::std::process::exit(1);
|
||||
}
|
||||
}
|
||||
422
src/state.rs
Normal file
422
src/state.rs
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
|
||||
const EXCLUDE_PATTERNS: [(&'static str, &'static str); 1] = [("bash", r"[[:cntrl:]]\[([0-9]{1,2};)?([0-9]{1,2})?m")];
|
||||
|
||||
const PATTERNS: [(&'static str, &'static str); 14] = [
|
||||
("markdown_url", r"\[[^]]*\]\(([^)]+)\)"),
|
||||
("url", r"((https?://|git@|git://|ssh://|ftp://|file:///)[^ ]+)"),
|
||||
("diff_a", r"--- a/([^ ]+)"),
|
||||
("diff_b", r"\+\+\+ b/([^ ]+)"),
|
||||
("docker", r"sha256:([0-9a-f]{64})"),
|
||||
("path", r"(([.\w\-@~]+)?(/[.\w\-@]+)+)"),
|
||||
("color", r"#[0-9a-fA-F]{6}"),
|
||||
("uid", r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"),
|
||||
("ipfs", r"Qm[0-9a-zA-Z]{44}"),
|
||||
("sha", r"[0-9a-f]{7,40}"),
|
||||
("ip", r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"),
|
||||
("ipv6", r"[A-f0-9:]+:+[A-f0-9:]+[%\w\d]+"),
|
||||
("address", r"0x[0-9a-fA-F]+"),
|
||||
("number", r"[0-9]{4,}"),
|
||||
];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Match<'a> {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub pattern: &'a str,
|
||||
pub text: &'a str,
|
||||
pub hint: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> fmt::Debug for Match<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Match {{ x: {}, y: {}, pattern: {}, text: {}, hint: <{}> }}",
|
||||
self.x,
|
||||
self.y,
|
||||
self.pattern,
|
||||
self.text,
|
||||
self.hint.clone().unwrap_or("<undefined>".to_string())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq for Match<'a> {
|
||||
fn eq(&self, other: &Match) -> bool {
|
||||
self.x == other.x && self.y == other.y
|
||||
}
|
||||
}
|
||||
|
||||
pub struct State<'a> {
|
||||
pub lines: &'a Vec<&'a str>,
|
||||
alphabet: &'a str,
|
||||
regexp: &'a Vec<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> State<'a> {
|
||||
pub fn new(lines: &'a Vec<&'a str>, alphabet: &'a str, regexp: &'a Vec<&'a str>) -> State<'a> {
|
||||
State {
|
||||
lines,
|
||||
alphabet,
|
||||
regexp,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(&self, reverse: bool, unique: bool) -> Vec<Match<'a>> {
|
||||
let mut matches = Vec::new();
|
||||
|
||||
let exclude_patterns = EXCLUDE_PATTERNS
|
||||
.iter()
|
||||
.map(|tuple| (tuple.0, Regex::new(tuple.1).unwrap()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let custom_patterns = self
|
||||
.regexp
|
||||
.iter()
|
||||
.map(|regexp| ("custom", Regex::new(regexp).expect("Invalid custom regexp")))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let patterns = PATTERNS
|
||||
.iter()
|
||||
.map(|tuple| (tuple.0, Regex::new(tuple.1).unwrap()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let all_patterns = [exclude_patterns, custom_patterns, patterns].concat();
|
||||
|
||||
for (index, line) in self.lines.iter().enumerate() {
|
||||
let mut chunk: &str = line;
|
||||
let mut offset: i32 = 0;
|
||||
|
||||
loop {
|
||||
let submatches = all_patterns
|
||||
.iter()
|
||||
.filter_map(|tuple| match tuple.1.find_iter(chunk).nth(0) {
|
||||
Some(m) => Some((tuple.0, tuple.1.clone(), m)),
|
||||
None => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let first_match_option = submatches.iter().min_by(|x, y| x.2.start().cmp(&y.2.start()));
|
||||
|
||||
if let Some(first_match) = first_match_option {
|
||||
let (name, pattern, matching) = first_match;
|
||||
let text = matching.as_str();
|
||||
|
||||
if let Some(captures) = pattern.captures(text) {
|
||||
let (subtext, substart) = if let Some(capture) = captures.get(1) {
|
||||
(capture.as_str(), capture.start())
|
||||
} else {
|
||||
(matching.as_str(), 0)
|
||||
};
|
||||
|
||||
// Never hint or broke bash color sequences
|
||||
if *name != "bash" {
|
||||
matches.push(Match {
|
||||
x: offset + matching.start() as i32 + substart as i32,
|
||||
y: index as i32,
|
||||
pattern: name,
|
||||
text: subtext,
|
||||
hint: None,
|
||||
});
|
||||
}
|
||||
|
||||
chunk = chunk.get(matching.end()..).expect("Unknown chunk");
|
||||
offset += matching.end() as i32;
|
||||
} else {
|
||||
panic!("No matching?");
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let alphabet = super::alphabets::get_alphabet(self.alphabet);
|
||||
let mut hints = alphabet.hints(matches.len());
|
||||
|
||||
// This looks wrong but we do a pop after
|
||||
if !reverse {
|
||||
hints.reverse();
|
||||
} else {
|
||||
matches.reverse();
|
||||
hints.reverse();
|
||||
}
|
||||
|
||||
if unique {
|
||||
let mut previous: HashMap<&str, String> = HashMap::new();
|
||||
|
||||
for mat in &mut matches {
|
||||
if let Some(previous_hint) = previous.get(mat.text) {
|
||||
mat.hint = Some(previous_hint.clone());
|
||||
} else if let Some(hint) = hints.pop() {
|
||||
mat.hint = Some(hint.to_string().clone());
|
||||
previous.insert(mat.text, hint.to_string().clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for mat in &mut matches {
|
||||
if let Some(hint) = hints.pop() {
|
||||
mat.hint = Some(hint.to_string().clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if reverse {
|
||||
matches.reverse();
|
||||
}
|
||||
|
||||
matches
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn split(output: &str) -> Vec<&str> {
|
||||
output.split("\n").collect::<Vec<&str>>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_reverse() {
|
||||
let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a");
|
||||
assert_eq!(results.last().unwrap().hint.clone().unwrap(), "c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_unique() {
|
||||
let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, true);
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a");
|
||||
assert_eq!(results.last().unwrap().hint.clone().unwrap(), "a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
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);
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(
|
||||
results.get(0).unwrap().text,
|
||||
"30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_bash() {
|
||||
let lines = split("path: [32m/var/log/nginx.log[m\npath: [32mtest/log/nginx-2.log:32[mfolder/.nginx@4df2.log");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results.get(0).unwrap().text, "/var/log/nginx.log");
|
||||
assert_eq!(results.get(1).unwrap().text, "test/log/nginx-2.log");
|
||||
assert_eq!(results.get(2).unwrap().text, "folder/.nginx@4df2.log");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_paths() {
|
||||
let lines = split("Lorem /tmp/foo/bar_lol, lorem\n Lorem /var/log/boot-strap.log lorem ../log/kern.log lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "/tmp/foo/bar_lol");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "/var/log/boot-strap.log");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "../log/kern.log");
|
||||
}
|
||||
|
||||
#[test]
|
||||
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);
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "~/.gnu/.config.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_uids() {
|
||||
let lines = split("Lorem ipsum 123e4567-e89b-12d3-a456-426655440000 lorem\n Lorem lorem lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_shas() {
|
||||
let lines = split("Lorem fd70b5695 5246ddf f924213 lorem\n Lorem 973113963b491874ab2e372ee60d4b4cb75f717c lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 4);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "fd70b5695");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "5246ddf");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "f924213");
|
||||
assert_eq!(
|
||||
results.get(3).unwrap().text.clone(),
|
||||
"973113963b491874ab2e372ee60d4b4cb75f717c"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_ips() {
|
||||
let lines = split("Lorem ipsum 127.0.0.1 lorem\n Lorem 255.255.10.255 lorem 127.0.0.1 lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "127.0.0.1");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "255.255.10.255");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "127.0.0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_ipv6s() {
|
||||
let lines = split("Lorem ipsum fe80::2:202:fe4 lorem\n Lorem 2001:67c:670:202:7ba8:5e41:1591:d723 lorem fe80::2:1 lorem ipsum fe80:22:312:fe::1%eth0");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 4);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "fe80::2:202:fe4");
|
||||
assert_eq!(
|
||||
results.get(1).unwrap().text.clone(),
|
||||
"2001:67c:670:202:7ba8:5e41:1591:d723"
|
||||
);
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "fe80::2:1");
|
||||
assert_eq!(results.get(3).unwrap().text.clone(), "fe80:22:312:fe::1%eth0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_markdown_urls() {
|
||||
let lines = split("Lorem ipsum [link](https://github.io?foo=bar)  lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
assert_eq!(results.get(0).unwrap().pattern.clone(), "markdown_url");
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "https://github.io?foo=bar");
|
||||
assert_eq!(results.get(1).unwrap().pattern.clone(), "markdown_url");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "http://cdn.com/img.jpg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_urls() {
|
||||
let lines = split("Lorem ipsum https://www.rust-lang.org/tools lorem\n Lorem ipsumhttps://crates.io lorem https://github.io?foo=bar lorem ssh://github.io");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 4);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "https://www.rust-lang.org/tools");
|
||||
assert_eq!(results.get(0).unwrap().pattern.clone(), "url");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "https://crates.io");
|
||||
assert_eq!(results.get(1).unwrap().pattern.clone(), "url");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "https://github.io?foo=bar");
|
||||
assert_eq!(results.get(2).unwrap().pattern.clone(), "url");
|
||||
assert_eq!(results.get(3).unwrap().text.clone(), "ssh://github.io");
|
||||
assert_eq!(results.get(3).unwrap().pattern.clone(), "url");
|
||||
}
|
||||
|
||||
#[test]
|
||||
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);
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "0xfd70b5695");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "0x5246ddf");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "0x973113");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_hex_colors() {
|
||||
let lines = split("Lorem #fd7b56 lorem #FF00FF\n Lorem #00fF05 lorem #abcd00 lorem #afRR00");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 4);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "#fd7b56");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "#FF00FF");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "#00fF05");
|
||||
assert_eq!(results.get(3).unwrap().text.clone(), "#abcd00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_ipfs() {
|
||||
let lines = split("Lorem QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ lorem Qmfoobar");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(
|
||||
results.get(0).unwrap().text.clone(),
|
||||
"QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_process_port() {
|
||||
let lines =
|
||||
split("Lorem 5695 52463 lorem\n Lorem 973113 lorem 99999 lorem 8888 lorem\n 23456 lorem 5432 lorem 23444");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_diff_a() {
|
||||
let lines = split("Lorem lorem\n--- a/src/main.rs");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_diff_b() {
|
||||
let lines = split("Lorem lorem\n+++ b/src/main.rs");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn priority() {
|
||||
let lines = split("Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem");
|
||||
let custom = ["CUSTOM-[0-9]{4,}", "ISSUE-[0-9]{3}"].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 9);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "http://foo.bar");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "CUSTOM-52463");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "ISSUE-123");
|
||||
assert_eq!(results.get(3).unwrap().text.clone(), "/var/fd70b569/9999.log");
|
||||
assert_eq!(results.get(4).unwrap().text.clone(), "52463");
|
||||
assert_eq!(results.get(5).unwrap().text.clone(), "973113");
|
||||
assert_eq!(
|
||||
results.get(6).unwrap().text.clone(),
|
||||
"123e4567-e89b-12d3-a456-426655440000"
|
||||
);
|
||||
assert_eq!(results.get(7).unwrap().text.clone(), "8888");
|
||||
assert_eq!(results.get(8).unwrap().text.clone(), "https://crates.io/23456/fd70b569");
|
||||
}
|
||||
}
|
||||
384
src/swapper.rs
Normal file
384
src/swapper.rs
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
extern crate clap;
|
||||
|
||||
use self::clap::{App, Arg};
|
||||
use clap::crate_version;
|
||||
use regex::Regex;
|
||||
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>>;
|
||||
}
|
||||
|
||||
struct RealShell {
|
||||
executed: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl RealShell {
|
||||
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");
|
||||
|
||||
self.executed = Some(args);
|
||||
|
||||
let output: String = String::from_utf8_lossy(&execution.stdout).into();
|
||||
|
||||
output.trim_end().to_string()
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn capture_active_pane(&mut self) {
|
||||
let active_command = vec![
|
||||
"tmux",
|
||||
"list-panes",
|
||||
"-F",
|
||||
"#{pane_id}:#{?pane_in_mode,1,0}:#{pane_height}:#{scroll_position}:#{?pane_active,active,nope}",
|
||||
];
|
||||
|
||||
let output = self
|
||||
.executor
|
||||
.execute(active_command.iter().map(|arg| arg.to_string()).collect());
|
||||
|
||||
let lines: Vec<&str> = output.split('\n').collect();
|
||||
let chunks: Vec<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 pane_id = active_pane.get(0).unwrap();
|
||||
let pane_in_copy_mode = active_pane.get(1).unwrap().to_string();
|
||||
|
||||
self.active_pane_id = Some(pane_id.to_string());
|
||||
self.active_pane_in_copy_mode = Some(pane_in_copy_mode);
|
||||
|
||||
if self.active_pane_in_copy_mode.clone().unwrap() == "1" {
|
||||
let pane_height = active_pane
|
||||
.get(2)
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("Unable to retrieve pane height");
|
||||
let pane_scroll_position = active_pane
|
||||
.get(3)
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("Unable to retrieve pane scroll");
|
||||
|
||||
self.active_pane_height = Some(pane_height);
|
||||
self.active_pane_scroll_position = Some(pane_scroll_position);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_thumbs(&mut self) {
|
||||
let options_command = vec!["tmux", "show", "-g"];
|
||||
let params: Vec<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![]
|
||||
}
|
||||
})
|
||||
.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,
|
||||
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 params: Vec<String> = thumbs_command.iter().map(|arg| arg.to_string()).collect();
|
||||
|
||||
self.thumbs_pane_id = Some(self.executor.execute(params));
|
||||
}
|
||||
|
||||
pub fn swap_panes(&mut self) {
|
||||
let active_pane_id = self.active_pane_id.as_mut().unwrap().clone();
|
||||
let thumbs_pane_id = self.thumbs_pane_id.as_mut().unwrap().clone();
|
||||
|
||||
let swap_command = vec![
|
||||
"tmux",
|
||||
"swap-pane",
|
||||
"-d",
|
||||
"-s",
|
||||
active_pane_id.as_str(),
|
||||
"-t",
|
||||
thumbs_pane_id.as_str(),
|
||||
];
|
||||
let params = swap_command.iter().map(|arg| arg.to_string()).collect();
|
||||
|
||||
self.executor.execute(params);
|
||||
}
|
||||
|
||||
pub fn wait_thumbs(&mut self) {
|
||||
let wait_command = vec!["tmux", "wait-for", self.signal.as_str()];
|
||||
let params = wait_command.iter().map(|arg| arg.to_string()).collect();
|
||||
|
||||
self.executor.execute(params);
|
||||
}
|
||||
|
||||
pub fn retrieve_content(&mut self) {
|
||||
let retrieve_command = vec!["cat", TMP_FILE];
|
||||
let params = retrieve_command.iter().map(|arg| arg.to_string()).collect();
|
||||
|
||||
self.content = Some(self.executor.execute(params));
|
||||
}
|
||||
|
||||
pub fn destroy_content(&mut self) {
|
||||
let retrieve_command = vec!["rm", TMP_FILE];
|
||||
let params = retrieve_command.iter().map(|arg| arg.to_string()).collect();
|
||||
|
||||
self.executor.execute(params);
|
||||
}
|
||||
|
||||
pub fn execute_command(&mut self) {
|
||||
let content = self.content.clone().unwrap();
|
||||
let mut splitter = content.splitn(2, ':');
|
||||
|
||||
if let Some(upcase) = splitter.next() {
|
||||
if let Some(text) = splitter.next() {
|
||||
let execute_command = if upcase.trim_end() == "true" {
|
||||
self.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()];
|
||||
let params = retrieve_command.iter().map(|arg| arg.to_string()).collect();
|
||||
|
||||
self.executor.execute(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct TestShell {
|
||||
outputs: Vec<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()
|
||||
}
|
||||
|
||||
fn last_executed(&self) -> Option<Vec<String>> {
|
||||
self.executed.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieve_active_pane() {
|
||||
let last_command_outputs = vec!["%97:100:24:1:active\n%106:100:24:1:nope\n%107:100:24:1:nope\n".to_string()];
|
||||
let mut executor = TestShell::new(last_command_outputs);
|
||||
let mut swapper = Swapper::new(Box::new(&mut executor), "".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), "".to_string(), "".to_string(), "".to_string());
|
||||
|
||||
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(())
|
||||
}
|
||||
304
src/view.rs
Normal file
304
src/view.rs
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
use super::*;
|
||||
use std::char;
|
||||
use std::io::{stdout, Read, Write};
|
||||
use termion::async_stdin;
|
||||
use termion::event::Key;
|
||||
use termion::input::TermRead;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use termion::{color, cursor};
|
||||
|
||||
pub struct View<'a> {
|
||||
state: &'a mut state::State<'a>,
|
||||
skip: usize,
|
||||
multi: bool,
|
||||
contrast: bool,
|
||||
position: &'a str,
|
||||
matches: Vec<state::Match<'a>>,
|
||||
select_foreground_color: Box<&'a dyn color::Color>,
|
||||
select_background_color: Box<&'a dyn color::Color>,
|
||||
foreground_color: Box<&'a dyn color::Color>,
|
||||
background_color: Box<&'a dyn color::Color>,
|
||||
hint_background_color: Box<&'a dyn color::Color>,
|
||||
hint_foreground_color: Box<&'a dyn color::Color>,
|
||||
}
|
||||
|
||||
enum CaptureEvent {
|
||||
Exit,
|
||||
Hint(Vec<(String, bool)>),
|
||||
}
|
||||
|
||||
impl<'a> View<'a> {
|
||||
pub fn new(
|
||||
state: &'a mut state::State<'a>,
|
||||
multi: bool,
|
||||
reverse: bool,
|
||||
unique: bool,
|
||||
contrast: bool,
|
||||
position: &'a str,
|
||||
select_foreground_color: Box<&'a dyn color::Color>,
|
||||
select_background_color: Box<&'a dyn color::Color>,
|
||||
foreground_color: Box<&'a dyn color::Color>,
|
||||
background_color: Box<&'a dyn color::Color>,
|
||||
hint_foreground_color: Box<&'a dyn color::Color>,
|
||||
hint_background_color: Box<&'a dyn color::Color>,
|
||||
) -> View<'a> {
|
||||
let matches = state.matches(reverse, unique);
|
||||
let skip = if reverse { matches.len() - 1 } else { 0 };
|
||||
|
||||
View {
|
||||
state,
|
||||
skip,
|
||||
multi,
|
||||
contrast,
|
||||
position,
|
||||
matches,
|
||||
select_foreground_color,
|
||||
select_background_color,
|
||||
foreground_color,
|
||||
background_color,
|
||||
hint_foreground_color,
|
||||
hint_background_color,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev(&mut self) {
|
||||
if self.skip > 0 {
|
||||
self.skip -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
if self.skip < self.matches.len() - 1 {
|
||||
self.skip += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn make_hint_text(&self, hint: &str) -> String {
|
||||
if self.contrast {
|
||||
format!("[{}]", hint)
|
||||
} else {
|
||||
hint.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, stdout: &mut dyn Write) -> () {
|
||||
write!(stdout, "{}", cursor::Hide).unwrap();
|
||||
|
||||
for (index, line) in self.state.lines.iter().enumerate() {
|
||||
let clean = line.trim_end_matches(|c: char| c.is_whitespace());
|
||||
|
||||
if !clean.is_empty() {
|
||||
let text = self.make_hint_text(line);
|
||||
|
||||
print!("{goto}{text}", goto = cursor::Goto(1, index as u16 + 1), text = &text);
|
||||
}
|
||||
}
|
||||
|
||||
let selected = self.matches.get(self.skip);
|
||||
|
||||
for mat in self.matches.iter() {
|
||||
let selected_color = if selected == Some(mat) {
|
||||
&self.select_foreground_color
|
||||
} else {
|
||||
&self.foreground_color
|
||||
};
|
||||
let selected_background_color = if selected == Some(mat) {
|
||||
&self.select_background_color
|
||||
} else {
|
||||
&self.background_color
|
||||
};
|
||||
|
||||
// Find long utf sequences and extract it from mat.x
|
||||
let line = &self.state.lines[mat.y as usize];
|
||||
let prefix = &line[0..mat.x as usize];
|
||||
let extra = prefix.len() - prefix.chars().count();
|
||||
let offset = (mat.x as u16) - (extra as u16);
|
||||
let text = self.make_hint_text(mat.text);
|
||||
|
||||
print!(
|
||||
"{goto}{background}{foregroud}{text}{resetf}{resetb}",
|
||||
goto = cursor::Goto(offset + 1, mat.y as u16 + 1),
|
||||
foregroud = color::Fg(**selected_color),
|
||||
background = color::Bg(**selected_background_color),
|
||||
resetf = color::Fg(color::Reset),
|
||||
resetb = color::Bg(color::Reset),
|
||||
text = &text
|
||||
);
|
||||
|
||||
if let Some(ref hint) = mat.hint {
|
||||
let extra_position = if self.position == "left" {
|
||||
0
|
||||
} else {
|
||||
text.len() - mat.hint.clone().unwrap().len()
|
||||
};
|
||||
|
||||
let text = self.make_hint_text(hint.as_str());
|
||||
|
||||
print!(
|
||||
"{goto}{background}{foregroud}{text}{resetf}{resetb}",
|
||||
goto = cursor::Goto(offset + extra_position as u16 + 1, mat.y as u16 + 1),
|
||||
foregroud = color::Fg(*self.hint_foreground_color),
|
||||
background = color::Bg(*self.hint_background_color),
|
||||
resetf = color::Fg(color::Reset),
|
||||
resetb = color::Bg(color::Reset),
|
||||
text = &text
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
stdout.flush().unwrap();
|
||||
}
|
||||
|
||||
fn listen(&mut self, stdin: &mut dyn Read, stdout: &mut dyn Write) -> CaptureEvent {
|
||||
if self.matches.is_empty() {
|
||||
return CaptureEvent::Exit
|
||||
}
|
||||
|
||||
let mut chosen = vec![];
|
||||
let mut typed_hint: String = "".to_owned();
|
||||
let longest_hint = self
|
||||
.matches
|
||||
.iter()
|
||||
.filter_map(|m| m.hint.clone())
|
||||
.max_by(|x, y| x.len().cmp(&y.len()))
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
self.render(stdout);
|
||||
|
||||
loop {
|
||||
match stdin.keys().next() {
|
||||
Some(key) => {
|
||||
match key {
|
||||
Ok(key) => {
|
||||
match key {
|
||||
Key::Esc => {
|
||||
if self.multi && !typed_hint.is_empty() {
|
||||
typed_hint.clear();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Key::Insert => match self.matches.iter().enumerate().find(|&h| h.0 == self.skip) {
|
||||
Some(hm) => {
|
||||
chosen.push((hm.1.text.to_string(), false));
|
||||
|
||||
if !self.multi {
|
||||
return CaptureEvent::Hint(chosen);
|
||||
}
|
||||
}
|
||||
_ => panic!("Match not found?"),
|
||||
},
|
||||
Key::Up => {
|
||||
self.prev();
|
||||
}
|
||||
Key::Down => {
|
||||
self.next();
|
||||
}
|
||||
Key::Left => {
|
||||
self.prev();
|
||||
}
|
||||
Key::Right => {
|
||||
self.next();
|
||||
}
|
||||
Key::Char(ch) => {
|
||||
if ch == ' ' && self.multi {
|
||||
return CaptureEvent::Hint(chosen);
|
||||
}
|
||||
|
||||
let key = ch.to_string();
|
||||
let lower_key = key.to_lowercase();
|
||||
|
||||
typed_hint.push_str(lower_key.as_str());
|
||||
|
||||
let selection = self.matches.iter().find(|mat| mat.hint == Some(typed_hint.clone()));
|
||||
|
||||
match selection {
|
||||
Some(mat) => {
|
||||
chosen.push((mat.text.to_string(), key != lower_key));
|
||||
|
||||
if self.multi {
|
||||
typed_hint.clear();
|
||||
} else {
|
||||
return CaptureEvent::Hint(chosen);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !self.multi && typed_hint.len() >= longest_hint.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Unknown key
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => panic!(err),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Nothing in the buffer. Wait for a bit...
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
self.render(stdout);
|
||||
}
|
||||
|
||||
CaptureEvent::Exit
|
||||
}
|
||||
|
||||
pub fn present(&mut self) -> Vec<(String, bool)> {
|
||||
let mut stdin = async_stdin();
|
||||
let mut stdout = AlternateScreen::from(stdout().into_raw_mode().unwrap());
|
||||
|
||||
let hints = match self.listen(&mut stdin, &mut stdout) {
|
||||
CaptureEvent::Exit => vec![],
|
||||
CaptureEvent::Hint(chosen) => chosen,
|
||||
};
|
||||
|
||||
write!(stdout, "{}", cursor::Show).unwrap();
|
||||
|
||||
hints
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn split(output: &str) -> Vec<&str> {
|
||||
output.split("\n").collect::<Vec<&str>>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hint_text() {
|
||||
let lines = split("lorem 127.0.0.1 lorem");
|
||||
let custom = [].to_vec();
|
||||
let mut state = state::State::new(&lines, "abcd", &custom);
|
||||
let mut view = View {
|
||||
state: &mut state,
|
||||
skip: 0,
|
||||
multi: false,
|
||||
contrast: false,
|
||||
position: &"",
|
||||
matches: vec![],
|
||||
select_foreground_color: colors::get_color("default"),
|
||||
select_background_color: colors::get_color("default"),
|
||||
foreground_color: colors::get_color("default"),
|
||||
background_color: colors::get_color("default"),
|
||||
hint_background_color: colors::get_color("default"),
|
||||
hint_foreground_color: colors::get_color("default"),
|
||||
};
|
||||
|
||||
let result = view.make_hint_text("a");
|
||||
assert_eq!(result, "a".to_string());
|
||||
|
||||
view.contrast = true;
|
||||
let result = view.make_hint_text("a");
|
||||
assert_eq!(result, "[a]".to_string());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue