mirror of
https://github.com/TECHNOFAB11/tmux-copyrat.git
synced 2025-12-13 00:20:08 +01:00
refactor: add comments & change some names
This commit is contained in:
parent
34d0bb5a35
commit
777a460ec9
2 changed files with 177 additions and 143 deletions
47
src/main.rs
47
src/main.rs
|
|
@ -12,6 +12,8 @@ use std::fs::OpenOptions;
|
|||
use std::io::prelude::*;
|
||||
use std::io::{self, Read};
|
||||
|
||||
// TODO: position as an enum ::Leading ::Trailing
|
||||
|
||||
fn app_args<'a>() -> clap::ArgMatches<'a> {
|
||||
App::new("thumbs")
|
||||
.version(crate_version!())
|
||||
|
|
@ -138,17 +140,18 @@ fn main() {
|
|||
let select_foreground_color = colors::get_color(args.value_of("select_foreground_color").unwrap());
|
||||
let select_background_color = colors::get_color(args.value_of("select_background_color").unwrap());
|
||||
|
||||
// Copy the pane contents (piped in via stdin) into a buffer, and split lines.
|
||||
let mut buffer = String::new();
|
||||
let stdin = io::stdin();
|
||||
let mut handle = stdin.lock();
|
||||
let mut output = String::new();
|
||||
|
||||
handle.read_to_string(&mut output).unwrap();
|
||||
handle.read_to_string(&mut buffer).unwrap();
|
||||
|
||||
let lines = output.split('\n').collect::<Vec<&str>>();
|
||||
let lines: Vec<&str> = buffer.split('\n').collect();
|
||||
|
||||
let mut state = state::State::new(&lines, alphabet, ®exp);
|
||||
|
||||
let selected = {
|
||||
let selections = {
|
||||
let mut viewbox = view::View::new(
|
||||
&mut state,
|
||||
multi,
|
||||
|
|
@ -167,22 +170,28 @@ fn main() {
|
|||
viewbox.present()
|
||||
};
|
||||
|
||||
if !selected.is_empty() {
|
||||
let output = selected
|
||||
.iter()
|
||||
.map(|(text, upcase)| {
|
||||
let upcase_value = if *upcase { "true" } else { "false" };
|
||||
// Early exit, signaling tmux we had no selections.
|
||||
if selections.is_empty() {
|
||||
::std::process::exit(1);
|
||||
}
|
||||
|
||||
let mut output = format.to_string();
|
||||
let output = selections
|
||||
.iter()
|
||||
.map(|(text, upcase)| {
|
||||
let upcase_value = if *upcase { "true" } else { "false" };
|
||||
|
||||
output = str::replace(&output, "%U", upcase_value);
|
||||
output = str::replace(&output, "%H", text.as_str());
|
||||
output
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let mut output = format.to_string();
|
||||
|
||||
if let Some(target) = target {
|
||||
output = str::replace(&output, "%U", upcase_value);
|
||||
output = str::replace(&output, "%H", text.as_str());
|
||||
output
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
match target {
|
||||
None => println!("{}", output),
|
||||
Some(target) => {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
|
|
@ -191,10 +200,6 @@ fn main() {
|
|||
.expect("Unable to open the target file");
|
||||
|
||||
file.write(output.as_bytes()).unwrap();
|
||||
} else {
|
||||
print!("{}", output);
|
||||
}
|
||||
} else {
|
||||
::std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
273
src/view.rs
273
src/view.rs
|
|
@ -62,97 +62,124 @@ impl<'a> View<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Move focus onto the previous hint.
|
||||
pub fn prev(&mut self) {
|
||||
if self.skip > 0 {
|
||||
self.skip -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Move focus onto the next hint.
|
||||
pub fn next(&mut self) {
|
||||
if self.skip < self.matches.len() - 1 {
|
||||
self.skip += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn make_hint_text(&self, hint: &str) -> String {
|
||||
if self.contrast {
|
||||
format!("[{}]", hint)
|
||||
} else {
|
||||
hint.to_string()
|
||||
}
|
||||
}
|
||||
// /// TODO remove
|
||||
// fn make_hint_text(&self, hint: &str) -> String {
|
||||
// if self.contrast {
|
||||
// format!("[{}]", hint)
|
||||
// } else {
|
||||
// hint.to_string()
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Render the view on stdout.
|
||||
fn render(&self, stdout: &mut dyn Write) -> () {
|
||||
write!(stdout, "{}", cursor::Hide).unwrap();
|
||||
|
||||
// Trim all lines and render non-empty ones.
|
||||
for (index, line) in self.state.lines.iter().enumerate() {
|
||||
let clean = line.trim_end_matches(|c: char| c.is_whitespace());
|
||||
// remove trailing whitespaces
|
||||
let cleaned_line = line.trim_end_matches(|c: char| c.is_whitespace());
|
||||
|
||||
if !clean.is_empty() {
|
||||
let text = self.make_hint_text(line);
|
||||
|
||||
print!("{goto}{text}", goto = cursor::Goto(1, index as u16 + 1), text = &text);
|
||||
if cleaned_line.is_empty() {
|
||||
continue; // Don't render empty lines.
|
||||
}
|
||||
|
||||
// let text = self.make_hint_text(line);
|
||||
// print!(
|
||||
write!(
|
||||
stdout,
|
||||
"{goto}{text}",
|
||||
goto = cursor::Goto(1, index as u16 + 1),
|
||||
text = &cleaned_line,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let selected = self.matches.get(self.skip);
|
||||
// let focused = self.matches.get(self.skip);
|
||||
|
||||
for mat in self.matches.iter() {
|
||||
let selected_color = if selected == Some(mat) {
|
||||
&self.select_foreground_color
|
||||
for (index, mat) in self.matches.iter().enumerate() {
|
||||
// 1. Render the match's text.
|
||||
//
|
||||
|
||||
// To help identify it, the match thas has focus is rendered with a dedicated color.
|
||||
// let (text_fg_color, text_bg_color) = if focused == Some(mat) {
|
||||
let (text_fg_color, text_bg_color) = if index == self.skip {
|
||||
(&self.select_foreground_color, &self.select_background_color)
|
||||
} else {
|
||||
&self.foreground_color
|
||||
};
|
||||
let selected_background_color = if selected == Some(mat) {
|
||||
&self.select_background_color
|
||||
} else {
|
||||
&self.background_color
|
||||
(&self.foreground_color, &self.background_color)
|
||||
};
|
||||
|
||||
// Find long utf sequences and extract it from mat.x
|
||||
// If multibyte characters occur before the hint (in the "prefix"), then
|
||||
// their compouding takes less space on screen when printed: for
|
||||
// instance ´ + e = é. Consequently the hint offset has to be adjusted
|
||||
// to the left.
|
||||
let line = &self.state.lines[mat.y as usize];
|
||||
let prefix = &line[0..mat.x as usize];
|
||||
let extra = prefix.len() - prefix.chars().count();
|
||||
let offset = (mat.x as u16) - (extra as u16);
|
||||
let text = self.make_hint_text(mat.text);
|
||||
let adjust = prefix.len() - prefix.chars().count();
|
||||
let offset = (mat.x as u16) - (adjust as u16);
|
||||
let text = &mat.text; //self.make_hint_text(mat.text);
|
||||
|
||||
print!(
|
||||
"{goto}{background}{foregroud}{text}{resetf}{resetb}",
|
||||
// Render just the match's text on top of existing content.
|
||||
write!(
|
||||
stdout,
|
||||
"{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}",
|
||||
goto = cursor::Goto(offset + 1, mat.y as u16 + 1),
|
||||
foregroud = color::Fg(**selected_color),
|
||||
background = color::Bg(**selected_background_color),
|
||||
resetf = color::Fg(color::Reset),
|
||||
resetb = color::Bg(color::Reset),
|
||||
text = &text
|
||||
);
|
||||
fg_color = color::Fg(**text_fg_color),
|
||||
bg_color = color::Bg(**text_bg_color),
|
||||
fg_reset = color::Fg(color::Reset),
|
||||
bg_reset = color::Bg(color::Reset),
|
||||
text = &text,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// 2. Render the hint (e.g. ";k") on top of the text at the beginning or the end.
|
||||
//
|
||||
if let Some(ref hint) = mat.hint {
|
||||
let extra_position = if self.position == "left" {
|
||||
let extra_offset = if self.position == "left" {
|
||||
0
|
||||
} else {
|
||||
text.len() - mat.hint.clone().unwrap().len()
|
||||
text.len() - hint.len()
|
||||
};
|
||||
|
||||
let text = self.make_hint_text(hint.as_str());
|
||||
|
||||
print!(
|
||||
"{goto}{background}{foregroud}{text}{resetf}{resetb}",
|
||||
goto = cursor::Goto(offset + extra_position as u16 + 1, mat.y as u16 + 1),
|
||||
foregroud = color::Fg(*self.hint_foreground_color),
|
||||
background = color::Bg(*self.hint_background_color),
|
||||
resetf = color::Fg(color::Reset),
|
||||
resetb = color::Bg(color::Reset),
|
||||
text = &text
|
||||
);
|
||||
write!(
|
||||
stdout,
|
||||
"{goto}{bg_color}{fg_color}{hint}{fg_reset}{bg_reset}",
|
||||
goto = cursor::Goto(offset + extra_offset as u16 + 1, mat.y as u16 + 1),
|
||||
fg_color = color::Fg(*self.hint_foreground_color),
|
||||
bg_color = color::Bg(*self.hint_background_color),
|
||||
fg_reset = color::Fg(color::Reset),
|
||||
bg_reset = color::Bg(color::Reset),
|
||||
hint = hint,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
stdout.flush().unwrap();
|
||||
}
|
||||
|
||||
/// Listen to keys entered on stdin
|
||||
///
|
||||
/// # Panics
|
||||
/// This function panics if termion cannot read the entered keys on stdin.
|
||||
/// This function also panics if the user types Insert on a line without hints.
|
||||
fn listen(&mut self, stdin: &mut dyn Read, stdout: &mut dyn Write) -> CaptureEvent {
|
||||
if self.matches.is_empty() {
|
||||
return CaptureEvent::Exit
|
||||
return CaptureEvent::Exit;
|
||||
}
|
||||
|
||||
let mut chosen = vec![];
|
||||
|
|
@ -168,81 +195,83 @@ impl<'a> View<'a> {
|
|||
self.render(stdout);
|
||||
|
||||
loop {
|
||||
match stdin.keys().next() {
|
||||
Some(key) => {
|
||||
match key {
|
||||
Ok(key) => {
|
||||
match key {
|
||||
Key::Esc => {
|
||||
if self.multi && !typed_hint.is_empty() {
|
||||
typed_hint.clear();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Key::Insert => match self.matches.iter().enumerate().find(|&h| h.0 == self.skip) {
|
||||
Some(hm) => {
|
||||
chosen.push((hm.1.text.to_string(), false));
|
||||
// This is an option of a result of a key... Let's pop error cases first.
|
||||
let next_key = stdin.keys().next();
|
||||
|
||||
if !self.multi {
|
||||
return CaptureEvent::Hint(chosen);
|
||||
}
|
||||
}
|
||||
_ => panic!("Match not found?"),
|
||||
},
|
||||
Key::Up => {
|
||||
self.prev();
|
||||
}
|
||||
Key::Down => {
|
||||
self.next();
|
||||
}
|
||||
Key::Left => {
|
||||
self.prev();
|
||||
}
|
||||
Key::Right => {
|
||||
self.next();
|
||||
}
|
||||
Key::Char(ch) => {
|
||||
if ch == ' ' && self.multi {
|
||||
return CaptureEvent::Hint(chosen);
|
||||
}
|
||||
if next_key.is_none() {
|
||||
// Nothing in the buffer. Wait for a bit...
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = ch.to_string();
|
||||
let lower_key = key.to_lowercase();
|
||||
let key_res = next_key.unwrap();
|
||||
if let Err(err) = key_res {
|
||||
// Termion not being able to read from stdin is an unrecoverable error.
|
||||
panic!(err);
|
||||
}
|
||||
|
||||
typed_hint.push_str(lower_key.as_str());
|
||||
|
||||
let selection = self.matches.iter().find(|mat| mat.hint == Some(typed_hint.clone()));
|
||||
|
||||
match selection {
|
||||
Some(mat) => {
|
||||
chosen.push((mat.text.to_string(), key != lower_key));
|
||||
|
||||
if self.multi {
|
||||
typed_hint.clear();
|
||||
} else {
|
||||
return CaptureEvent::Hint(chosen);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !self.multi && typed_hint.len() >= longest_hint.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Unknown key
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => panic!(err),
|
||||
match key_res.unwrap() {
|
||||
// Clears an ongoing multi-hint selection, or exit.
|
||||
Key::Esc => {
|
||||
if self.multi && !typed_hint.is_empty() {
|
||||
typed_hint.clear();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Nothing in the buffer. Wait for a bit...
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
|
||||
// TODO: What does this do?
|
||||
Key::Insert => match self.matches.iter().enumerate().find(|&(idx, _)| idx == self.skip) {
|
||||
Some((_idx, mtch)) => {
|
||||
chosen.push((mtch.text.to_string(), false));
|
||||
|
||||
if !self.multi {
|
||||
return CaptureEvent::Hint(chosen);
|
||||
}
|
||||
}
|
||||
None => panic!("Match not found?"),
|
||||
},
|
||||
|
||||
// Move focus to next/prev hint.
|
||||
Key::Up => self.prev(),
|
||||
Key::Down => self.next(),
|
||||
Key::Left => self.prev(),
|
||||
Key::Right => self.next(),
|
||||
|
||||
// Pressing space finalizes an ongoing multi-hint selection.
|
||||
// Others characters attempt the corresponding hint.
|
||||
Key::Char(ch) => {
|
||||
if ch == ' ' && self.multi {
|
||||
return CaptureEvent::Hint(chosen);
|
||||
}
|
||||
|
||||
let key = ch.to_string();
|
||||
let lower_key = key.to_lowercase();
|
||||
|
||||
typed_hint.push_str(&lower_key);
|
||||
|
||||
let selection = self.matches.iter().find(|&mtch| mtch.hint == Some(typed_hint.clone()));
|
||||
|
||||
match selection {
|
||||
Some(mtch) => {
|
||||
chosen.push((mtch.text.to_string(), key != lower_key));
|
||||
|
||||
if self.multi {
|
||||
typed_hint.clear();
|
||||
} else {
|
||||
return CaptureEvent::Hint(chosen);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !self.multi && typed_hint.len() >= longest_hint.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown keys are ignored.
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.render(stdout);
|
||||
|
|
@ -294,11 +323,11 @@ mod tests {
|
|||
hint_foreground_color: colors::get_color("default"),
|
||||
};
|
||||
|
||||
let result = view.make_hint_text("a");
|
||||
assert_eq!(result, "a".to_string());
|
||||
// let result = view.make_hint_text("a");
|
||||
// assert_eq!(result, "a".to_string());
|
||||
|
||||
view.contrast = true;
|
||||
let result = view.make_hint_text("a");
|
||||
assert_eq!(result, "[a]".to_string());
|
||||
// view.contrast = true;
|
||||
// let result = view.make_hint_text("a");
|
||||
// assert_eq!(result, "[a]".to_string());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue