co-author 0.1.3

Co-Author your git commits from the command line
use super::err::UiError;
use super::input_reader::InputReader;
use crate::authors::author::Author;
use crate::common::runner::Runner;
use crate::Result;
use colored::Colorize;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::io::Write;

const FZF_SEPARATOR: &str = " - ";

pub struct Cli {
	reader: Box<dyn InputReader>,
	runner: Box<dyn Runner>,
}

impl Cli {
	pub fn new(reader: Box<dyn InputReader>, runner: Box<dyn Runner>) -> Self {
		Self { reader, runner }
	}

	pub fn message_prompt(&mut self) -> Result<String> {
		let prompt_msg = "Enter commit message:";
		let input = self.reader.readline(&format!("{prompt_msg}\n"))?;
		Ok(input.trim().to_string())
	}

	pub fn aliases_prompt(&mut self, authors: &[Author]) -> Result<Vec<String>> {
		let pretty_authors = Self::prettify_authors(authors);
		let prompt_msg = "Enter co-authors aliases separated by spaces:";
		let input = self.reader.readline(&format!("\n{pretty_authors}\n\n{prompt_msg}\n"))?;
		Ok(input.split_whitespace().map(ToString::to_string).collect())
	}

	pub fn pre_populated_message_prompt(&mut self, prev_commit_msg: &str) -> Result<String> {
		let prompt_msg = "Update commit message:";
		let input = self
			.reader
			.readline_with_prompt(&format!("{prompt_msg}\n"), (prev_commit_msg, ""))?;
		Ok(input.trim().to_string())
	}

	pub fn fzf_prompt(&self, authors: &[Author]) -> Result<Vec<u64>> {
		let mut fzf_proc = self
			.runner
			.attach("fzf", &["--multi".to_string(), "--ansi".to_string()])?;
		let stdin = fzf_proc
			.stdin
			.as_mut()
			.ok_or(UiError::Fzf("Could not attach stdin".to_string()))?;

		for author in authors.iter().map(Self::fzf_format) {
			writeln!(stdin, "{author}").map_err(|_| UiError::Fzf("Could not pipe to stdin".to_string()))?;
		}

		let output = fzf_proc
			.wait_with_output()
			.map_err(|_| UiError::Fzf("Could not read output".to_string()))?;
		let selected_aliases: Vec<u64> = String::from_utf8_lossy(&output.stdout)
			.lines()
			.map(|line| Self::hash_of(&line.replace(FZF_SEPARATOR, "")))
			.collect();

		Ok(selected_aliases)
	}

	fn hash_of(str: &str) -> u64 {
		let mut hasher = DefaultHasher::new();
		str.hash(&mut hasher);
		hasher.finish()
	}

	fn prettify_authors(authors: &[Author]) -> String {
		authors.iter().map(Self::prettify).collect::<Vec<String>>().join("\n")
	}

	fn prettify(author: &Author) -> String {
		format!(
			"{} {} {} {}",
			"".yellow(),
			author.alias().blue(),
			"->".green(),
			author.name()
		)
	}

	fn fzf_format(author: &Author) -> String {
		format!("{}{}{}", author.alias().blue(), FZF_SEPARATOR, author.name())
	}
}