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::prelude::*;
|
||||||
use std::io::{self, Read};
|
use std::io::{self, Read};
|
||||||
|
|
||||||
|
// TODO: position as an enum ::Leading ::Trailing
|
||||||
|
|
||||||
fn app_args<'a>() -> clap::ArgMatches<'a> {
|
fn app_args<'a>() -> clap::ArgMatches<'a> {
|
||||||
App::new("thumbs")
|
App::new("thumbs")
|
||||||
.version(crate_version!())
|
.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_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 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 stdin = io::stdin();
|
||||||
let mut handle = stdin.lock();
|
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 mut state = state::State::new(&lines, alphabet, ®exp);
|
||||||
|
|
||||||
let selected = {
|
let selections = {
|
||||||
let mut viewbox = view::View::new(
|
let mut viewbox = view::View::new(
|
||||||
&mut state,
|
&mut state,
|
||||||
multi,
|
multi,
|
||||||
|
|
@ -167,22 +170,28 @@ fn main() {
|
||||||
viewbox.present()
|
viewbox.present()
|
||||||
};
|
};
|
||||||
|
|
||||||
if !selected.is_empty() {
|
// Early exit, signaling tmux we had no selections.
|
||||||
let output = selected
|
if selections.is_empty() {
|
||||||
.iter()
|
::std::process::exit(1);
|
||||||
.map(|(text, upcase)| {
|
}
|
||||||
let upcase_value = if *upcase { "true" } else { "false" };
|
|
||||||
|
|
||||||
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);
|
let mut output = format.to_string();
|
||||||
output = str::replace(&output, "%H", text.as_str());
|
|
||||||
output
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
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()
|
let mut file = OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
|
|
@ -191,10 +200,6 @@ fn main() {
|
||||||
.expect("Unable to open the target file");
|
.expect("Unable to open the target file");
|
||||||
|
|
||||||
file.write(output.as_bytes()).unwrap();
|
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) {
|
pub fn prev(&mut self) {
|
||||||
if self.skip > 0 {
|
if self.skip > 0 {
|
||||||
self.skip -= 1;
|
self.skip -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move focus onto the next hint.
|
||||||
pub fn next(&mut self) {
|
pub fn next(&mut self) {
|
||||||
if self.skip < self.matches.len() - 1 {
|
if self.skip < self.matches.len() - 1 {
|
||||||
self.skip += 1;
|
self.skip += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_hint_text(&self, hint: &str) -> String {
|
// /// TODO remove
|
||||||
if self.contrast {
|
// fn make_hint_text(&self, hint: &str) -> String {
|
||||||
format!("[{}]", hint)
|
// if self.contrast {
|
||||||
} else {
|
// format!("[{}]", hint)
|
||||||
hint.to_string()
|
// } else {
|
||||||
}
|
// hint.to_string()
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/// Render the view on stdout.
|
||||||
fn render(&self, stdout: &mut dyn Write) -> () {
|
fn render(&self, stdout: &mut dyn Write) -> () {
|
||||||
write!(stdout, "{}", cursor::Hide).unwrap();
|
write!(stdout, "{}", cursor::Hide).unwrap();
|
||||||
|
|
||||||
|
// Trim all lines and render non-empty ones.
|
||||||
for (index, line) in self.state.lines.iter().enumerate() {
|
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() {
|
if cleaned_line.is_empty() {
|
||||||
let text = self.make_hint_text(line);
|
continue; // Don't render empty lines.
|
||||||
|
|
||||||
print!("{goto}{text}", goto = cursor::Goto(1, index as u16 + 1), text = &text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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() {
|
for (index, mat) in self.matches.iter().enumerate() {
|
||||||
let selected_color = if selected == Some(mat) {
|
// 1. Render the match's text.
|
||||||
&self.select_foreground_color
|
//
|
||||||
|
|
||||||
|
// 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 {
|
} else {
|
||||||
&self.foreground_color
|
(&self.foreground_color, &self.background_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
|
// 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 line = &self.state.lines[mat.y as usize];
|
||||||
let prefix = &line[0..mat.x as usize];
|
let prefix = &line[0..mat.x as usize];
|
||||||
let extra = prefix.len() - prefix.chars().count();
|
let adjust = prefix.len() - prefix.chars().count();
|
||||||
let offset = (mat.x as u16) - (extra as u16);
|
let offset = (mat.x as u16) - (adjust as u16);
|
||||||
let text = self.make_hint_text(mat.text);
|
let text = &mat.text; //self.make_hint_text(mat.text);
|
||||||
|
|
||||||
print!(
|
// Render just the match's text on top of existing content.
|
||||||
"{goto}{background}{foregroud}{text}{resetf}{resetb}",
|
write!(
|
||||||
|
stdout,
|
||||||
|
"{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}",
|
||||||
goto = cursor::Goto(offset + 1, mat.y as u16 + 1),
|
goto = cursor::Goto(offset + 1, mat.y as u16 + 1),
|
||||||
foregroud = color::Fg(**selected_color),
|
fg_color = color::Fg(**text_fg_color),
|
||||||
background = color::Bg(**selected_background_color),
|
bg_color = color::Bg(**text_bg_color),
|
||||||
resetf = color::Fg(color::Reset),
|
fg_reset = color::Fg(color::Reset),
|
||||||
resetb = color::Bg(color::Reset),
|
bg_reset = color::Bg(color::Reset),
|
||||||
text = &text
|
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 {
|
if let Some(ref hint) = mat.hint {
|
||||||
let extra_position = if self.position == "left" {
|
let extra_offset = if self.position == "left" {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
text.len() - mat.hint.clone().unwrap().len()
|
text.len() - hint.len()
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = self.make_hint_text(hint.as_str());
|
write!(
|
||||||
|
stdout,
|
||||||
print!(
|
"{goto}{bg_color}{fg_color}{hint}{fg_reset}{bg_reset}",
|
||||||
"{goto}{background}{foregroud}{text}{resetf}{resetb}",
|
goto = cursor::Goto(offset + extra_offset as u16 + 1, mat.y as u16 + 1),
|
||||||
goto = cursor::Goto(offset + extra_position as u16 + 1, mat.y as u16 + 1),
|
fg_color = color::Fg(*self.hint_foreground_color),
|
||||||
foregroud = color::Fg(*self.hint_foreground_color),
|
bg_color = color::Bg(*self.hint_background_color),
|
||||||
background = color::Bg(*self.hint_background_color),
|
fg_reset = color::Fg(color::Reset),
|
||||||
resetf = color::Fg(color::Reset),
|
bg_reset = color::Bg(color::Reset),
|
||||||
resetb = color::Bg(color::Reset),
|
hint = hint,
|
||||||
text = &text
|
)
|
||||||
);
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout.flush().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 {
|
fn listen(&mut self, stdin: &mut dyn Read, stdout: &mut dyn Write) -> CaptureEvent {
|
||||||
if self.matches.is_empty() {
|
if self.matches.is_empty() {
|
||||||
return CaptureEvent::Exit
|
return CaptureEvent::Exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut chosen = vec![];
|
let mut chosen = vec![];
|
||||||
|
|
@ -168,81 +195,83 @@ impl<'a> View<'a> {
|
||||||
self.render(stdout);
|
self.render(stdout);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match stdin.keys().next() {
|
// This is an option of a result of a key... Let's pop error cases first.
|
||||||
Some(key) => {
|
let next_key = stdin.keys().next();
|
||||||
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 {
|
if next_key.is_none() {
|
||||||
return CaptureEvent::Hint(chosen);
|
// Nothing in the buffer. Wait for a bit...
|
||||||
}
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
}
|
continue;
|
||||||
_ => 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 key_res = next_key.unwrap();
|
||||||
let lower_key = key.to_lowercase();
|
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());
|
match key_res.unwrap() {
|
||||||
|
// Clears an ongoing multi-hint selection, or exit.
|
||||||
let selection = self.matches.iter().find(|mat| mat.hint == Some(typed_hint.clone()));
|
Key::Esc => {
|
||||||
|
if self.multi && !typed_hint.is_empty() {
|
||||||
match selection {
|
typed_hint.clear();
|
||||||
Some(mat) => {
|
} else {
|
||||||
chosen.push((mat.text.to_string(), key != lower_key));
|
break;
|
||||||
|
|
||||||
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...
|
// TODO: What does this do?
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
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);
|
self.render(stdout);
|
||||||
|
|
@ -294,11 +323,11 @@ mod tests {
|
||||||
hint_foreground_color: colors::get_color("default"),
|
hint_foreground_color: colors::get_color("default"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = view.make_hint_text("a");
|
// let result = view.make_hint_text("a");
|
||||||
assert_eq!(result, "a".to_string());
|
// assert_eq!(result, "a".to_string());
|
||||||
|
|
||||||
view.contrast = true;
|
// view.contrast = true;
|
||||||
let result = view.make_hint_text("a");
|
// let result = view.make_hint_text("a");
|
||||||
assert_eq!(result, "[a]".to_string());
|
// assert_eq!(result, "[a]".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue