extern crate regex;
mod cache;
mod modules;
use super::ShellProps;
use crate::config::PromptConfig;
use crate::translator::ioprocessor::IOProcessor;
use cache::PromptCache;
use modules::*;
use regex::Regex;
use std::time::Duration;
const PROMPT_KEY_REGEX: &str = r"\$\{(.*?)\}";
const PROMPT_USER: &str = "${USER}";
const PROMPT_HOSTNAME: &str = "${HOSTNAME}";
const PROMPT_WRKDIR: &str = "${WRKDIR}";
const PROMPT_CMDTIME: &str = "${CMD_TIME}";
const PROMPT_RC: &str = "${RC}";
pub struct ShellPrompt {
prompt_line: String,
translate: bool,
break_opt: Option<BreakOptions>,
duration_opt: Option<DurationOptions>,
rc_opt: Option<RcOptions>,
git_opt: Option<GitOptions>,
cache: PromptCache,
}
struct BreakOptions {
pub break_with: String,
}
struct DurationOptions {
pub minimum: Duration,
}
struct RcOptions {
pub ok: String,
pub err: String,
}
struct GitOptions {
pub branch: String,
pub commit_ref_len: usize,
}
impl ShellPrompt {
pub(super) fn new(prompt_opt: &PromptConfig) -> ShellPrompt {
let break_opt: Option<BreakOptions> = match prompt_opt.break_enabled {
true => Some(BreakOptions::new(&prompt_opt.break_str)),
false => None,
};
let duration_opt: Option<DurationOptions> =
match DurationOptions::should_enable(&prompt_opt.prompt_line) {
true => Some(DurationOptions::new(prompt_opt.min_duration)),
false => None,
};
let rc_opt: Option<RcOptions> = match RcOptions::should_enable(&prompt_opt.prompt_line) {
true => Some(RcOptions::new(&prompt_opt.rc_ok, &prompt_opt.rc_err)),
false => None,
};
let git_opt: Option<GitOptions> = match GitOptions::should_enable(&prompt_opt.prompt_line) {
true => Some(GitOptions::new(
&prompt_opt.git_branch,
prompt_opt.git_commit_ref,
)),
false => None,
};
ShellPrompt {
prompt_line: prompt_opt.prompt_line.clone(),
translate: prompt_opt.translate,
break_opt: break_opt,
duration_opt: duration_opt,
rc_opt: rc_opt,
git_opt: git_opt,
cache: PromptCache::new(),
}
}
pub(super) fn get_line(&mut self, shell_props: &ShellProps, processor: &IOProcessor) -> String {
let mut prompt_line: String = self.process_prompt(shell_props, processor);
if self.translate {
prompt_line = processor.text_to_cyrillic(&prompt_line);
}
prompt_line
}
fn process_prompt(&mut self, shell_props: &ShellProps, processor: &IOProcessor) -> String {
let mut prompt_line: String = self.prompt_line.clone();
lazy_static! {
static ref RE: Regex = Regex::new(PROMPT_KEY_REGEX).unwrap();
}
for regex_match in RE.captures_iter(prompt_line.clone().as_str()) {
let mtch: String = String::from(®ex_match[0]);
let replace_with: String = self.resolve_key(shell_props, processor, &mtch);
prompt_line = prompt_line.replace(mtch.as_str(), replace_with.as_str());
}
prompt_line = String::from(prompt_line.trim());
if let Some(brkopt) = &self.break_opt {
prompt_line += "\n";
prompt_line += brkopt.break_with.trim();
}
self.cache.invalidate();
prompt_line
}
fn resolve_key(
&mut self,
shell_props: &ShellProps,
processor: &IOProcessor,
key: &String,
) -> String {
match key.as_str() {
PROMPT_CMDTIME => {
match &self.duration_opt {
Some(opt) => {
if shell_props.elapsed_time.as_millis() >= opt.minimum.as_millis() {
let millis: u128 = shell_props.elapsed_time.as_millis();
let secs: f64 = (millis as f64 / 1000 as f64) as f64;
String::from(format!("took {:.1}s", secs))
} else {
String::from("")
}
}
None => String::from(""),
}
}
modules::git::PROMPT_GIT_BRANCH => {
if self.git_opt.is_none() {
return String::from("");
}
if self.cache.get_cached_git().is_none() {
let repo_opt = git::find_repository(&shell_props.wrkdir);
match repo_opt {
Some(repo) => self.cache.cache_git(repo),
None => return String::from(""),
};
}
let branch: String = match git::get_branch(self.cache.get_cached_git().unwrap()) {
Some(branch) => branch,
None => return String::from(""),
};
String::from(format!(
"{}{}",
self.git_opt.as_ref().unwrap().branch.clone(),
branch
))
}
modules::git::PROMPT_GIT_COMMIT => {
if self.git_opt.is_none() {
return String::from("");
}
if self.cache.get_cached_git().is_none() {
let repo_opt = git::find_repository(&shell_props.wrkdir);
match repo_opt {
Some(repo) => self.cache.cache_git(repo),
None => return String::from(""),
};
}
match git::get_commit(
self.cache.get_cached_git().unwrap(),
self.git_opt.as_ref().unwrap().commit_ref_len,
) {
Some(commit) => commit,
None => String::from(""),
}
}
PROMPT_HOSTNAME => shell_props.hostname.clone(),
modules::colors::PROMPT_KBLK | modules::colors::PROMPT_KBLU | modules::colors::PROMPT_KCYN | modules::colors::PROMPT_KGRN | modules::colors::PROMPT_KGRY | modules::colors::PROMPT_KMAG | modules::colors::PROMPT_KRED | modules::colors::PROMPT_KRST | modules::colors::PROMPT_KWHT | modules::colors::PROMPT_KYEL => colors::PromptColor::from_key(key.as_str()).to_string(),
modules::language::PROMPT_LANG => language::language_to_str(processor.language),
PROMPT_RC => match &self.rc_opt {
Some(opt) => match shell_props.exit_status {
0 => opt.ok.clone(),
_ => opt.err.clone(),
},
None => String::from(""),
},
PROMPT_USER => shell_props.username.clone(),
PROMPT_WRKDIR => shell_props.wrkdir.as_path().display().to_string(),
_ => key.clone(),
}
}
}
impl BreakOptions {
pub fn new(break_with: &String) -> BreakOptions {
BreakOptions {
break_with: break_with.clone(),
}
}
}
impl DurationOptions {
pub fn should_enable(prompt_line: &String) -> bool {
prompt_line.contains(PROMPT_CMDTIME)
}
pub fn new(min_duration: usize) -> DurationOptions {
DurationOptions {
minimum: Duration::from_millis(min_duration as u64),
}
}
}
impl RcOptions {
pub fn should_enable(prompt_line: &String) -> bool {
prompt_line.contains(PROMPT_RC)
}
pub fn new(ok_str: &String, err_str: &String) -> RcOptions {
RcOptions {
ok: ok_str.clone(),
err: err_str.clone(),
}
}
}
impl GitOptions {
pub fn should_enable(prompt_line: &String) -> bool {
prompt_line.contains(modules::git::PROMPT_GIT_BRANCH) || prompt_line.contains(modules::git::PROMPT_GIT_COMMIT)
}
pub fn new(branch: &String, commit: usize) -> GitOptions {
GitOptions {
branch: branch.clone(),
commit_ref_len: commit,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::PromptConfig;
use crate::translator::ioprocessor::IOProcessor;
use crate::translator::new_translator;
use crate::translator::lang::Language;
use colors::PromptColor;
use git2::Repository;
use std::path::PathBuf;
use std::time::Duration;
#[test]
fn test_prompt_simple() {
let prompt_config_default = PromptConfig::default();
let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
let iop: IOProcessor = get_ioprocessor();
let shellenv: ShellProps = get_shellenv();
let _ = prompt.get_line(&shellenv, &iop);
prompt.translate = true;
let _ = prompt.get_line(&shellenv, &iop);
let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
let expected_prompt_line = String::from(format!(
"{}@{}:{}$",
shellenv.username.clone(),
shellenv.hostname.clone(),
shellenv.wrkdir.display()
));
assert_eq!(prompt_line, expected_prompt_line);
println!("\n");
}
#[test]
fn test_prompt_colors() {
let mut prompt_config_default = PromptConfig::default();
prompt_config_default.prompt_line = String::from("${KRED}RED${KYEL}YEL${KBLU}BLU${KGRN}GRN${KWHT}WHT${KGRY}GRY${KBLK}BLK${KMAG}MAG${KCYN}CYN${KRST}");
let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
let iop: IOProcessor = get_ioprocessor();
let shellenv: ShellProps = get_shellenv();
let _ = prompt.get_line(&shellenv, &iop);
prompt.translate = true;
let _ = prompt.get_line(&shellenv, &iop);
let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
let expected_prompt_line = String::from(format!(
"{}RED{}YEL{}BLU{}GRN{}WHT{}GRY{}BLK{}MAG{}CYN{}",
PromptColor::Red.to_string(),
PromptColor::Yellow.to_string(),
PromptColor::Blue.to_string(),
PromptColor::Green.to_string(),
PromptColor::White.to_string(),
PromptColor::Gray.to_string(),
PromptColor::Black.to_string(),
PromptColor::Magenta.to_string(),
PromptColor::Cyan.to_string(),
PromptColor::Reset.to_string()
));
assert_eq!(prompt_line, expected_prompt_line);
println!("\n");
}
#[test]
fn test_prompt_lang_time_with_break() {
let mut prompt_config_default = PromptConfig::default();
prompt_config_default.prompt_line = String::from("${LANG} ~ ${KYEL}${USER}${KRST} on ${KGRN}${HOSTNAME}${KRST} in ${KCYN}${WRKDIR}${KRST} ${KYEL}${CMD_TIME}${KRST}");
prompt_config_default.break_enabled = true;
let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
let iop: IOProcessor = get_ioprocessor();
let mut shellenv: ShellProps = get_shellenv();
shellenv.elapsed_time = Duration::from_millis(5100);
shellenv.wrkdir = PathBuf::from("/tmp/");
let _ = prompt.get_line(&shellenv, &iop);
prompt.translate = true;
let _ = prompt.get_line(&shellenv, &iop);
let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
let expected_prompt_line = String::from(format!(
"{} ~ {}{}{} on {}{}{} in {}{}{} {}took 5.1s{}\n❯",
language::language_to_str(Language::Russian),
PromptColor::Yellow.to_string(),
shellenv.username.clone(),
PromptColor::Reset.to_string(),
PromptColor::Green.to_string(),
shellenv.hostname.clone(),
PromptColor::Reset.to_string(),
PromptColor::Cyan.to_string(),
shellenv.wrkdir.display(),
PromptColor::Reset.to_string(),
PromptColor::Yellow.to_string(),
PromptColor::Reset.to_string()
));
assert_eq!(prompt_line, expected_prompt_line);
println!("\n");
}
#[test]
fn test_prompt_git() {
let repo: Repository = git::find_repository(&PathBuf::from("./")).unwrap();
let branch: String = git::get_branch(&repo).unwrap();
let commit: String = git::get_commit(&repo, 8).unwrap();
let mut prompt_config_default = PromptConfig::default();
prompt_config_default.prompt_line =
String::from("${USER}@${HOSTNAME}:${WRKDIR} ${GIT_BRANCH}:${GIT_COMMIT}");
let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
let iop: IOProcessor = get_ioprocessor();
let mut shellenv: ShellProps = get_shellenv();
shellenv.elapsed_time = Duration::from_millis(5100);
shellenv.wrkdir = PathBuf::from("./");
let _ = prompt.get_line(&shellenv, &iop);
prompt.translate = true;
let _ = prompt.get_line(&shellenv, &iop);
let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
let expected_prompt_line = String::from(format!(
"{}@{}:{} on {}:{}",
shellenv.username.clone(),
shellenv.hostname.clone(),
shellenv.wrkdir.display(),
branch,
commit
));
assert_eq!(prompt_line, expected_prompt_line);
println!("\n");
}
#[test]
fn test_prompt_git_not_in_repo() {
let mut prompt_config_default = PromptConfig::default();
prompt_config_default.prompt_line =
String::from("${USER}@${HOSTNAME}:${WRKDIR} ${GIT_BRANCH} ${GIT_COMMIT}");
let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
let iop: IOProcessor = get_ioprocessor();
let mut shellenv: ShellProps = get_shellenv();
shellenv.elapsed_time = Duration::from_millis(5100);
shellenv.wrkdir = PathBuf::from("/");
let _ = prompt.get_line(&shellenv, &iop);
prompt.translate = true;
let _ = prompt.get_line(&shellenv, &iop);
let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
let expected_prompt_line = String::from(format!(
"{}@{}:{}",
shellenv.username.clone(),
shellenv.hostname.clone(),
shellenv.wrkdir.display()
));
assert_eq!(prompt_line, expected_prompt_line);
println!("\n");
}
#[test]
fn test_prompt_rc_ok() {
let mut prompt_config_default = PromptConfig::default();
prompt_config_default.prompt_line = String::from("${RC} ${USER}@${HOSTNAME}:${WRKDIR}");
let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
let iop: IOProcessor = get_ioprocessor();
let mut shellenv: ShellProps = get_shellenv();
shellenv.elapsed_time = Duration::from_millis(5100);
shellenv.wrkdir = PathBuf::from("/");
let _ = prompt.get_line(&shellenv, &iop);
prompt.translate = true;
let _ = prompt.get_line(&shellenv, &iop);
let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
let expected_prompt_line = String::from(format!(
"✔ {}@{}:{}",
shellenv.username.clone(),
shellenv.hostname.clone(),
shellenv.wrkdir.display()
));
assert_eq!(prompt_line, expected_prompt_line);
println!("\n");
}
#[test]
fn test_prompt_rc_error() {
let mut prompt_config_default = PromptConfig::default();
prompt_config_default.prompt_line = String::from("${RC} ${USER}@${HOSTNAME}:${WRKDIR}");
let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
let iop: IOProcessor = get_ioprocessor();
let mut shellenv: ShellProps = get_shellenv();
shellenv.elapsed_time = Duration::from_millis(5100);
shellenv.wrkdir = PathBuf::from("/");
shellenv.exit_status = 255;
let _ = prompt.get_line(&shellenv, &iop);
prompt.translate = true;
let _ = prompt.get_line(&shellenv, &iop);
let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
let expected_prompt_line = String::from(format!(
"✖ {}@{}:{}",
shellenv.username.clone(),
shellenv.hostname.clone(),
shellenv.wrkdir.display()
));
assert_eq!(prompt_line, expected_prompt_line);
println!("\n");
}
#[test]
fn test_prompt_unresolved() {
let mut prompt_config_default = PromptConfig::default();
prompt_config_default.prompt_line = String::from("${USER}@${HOSTNAME}:${WRKDIR} ${FOOBAR}");
let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
let iop: IOProcessor = get_ioprocessor();
let mut shellenv: ShellProps = get_shellenv();
shellenv.elapsed_time = Duration::from_millis(5100);
shellenv.wrkdir = PathBuf::from("/");
shellenv.exit_status = 255;
let _ = prompt.get_line(&shellenv, &iop);
prompt.translate = true;
let _ = prompt.get_line(&shellenv, &iop);
let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
let expected_prompt_line = String::from(format!(
"{}@{}:{} {}",
shellenv.username.clone(),
shellenv.hostname.clone(),
shellenv.wrkdir.display(),
"${FOOBAR}"
));
assert_eq!(prompt_line, expected_prompt_line);
println!("\n");
}
fn get_ioprocessor() -> IOProcessor {
IOProcessor::new(Language::Russian, new_translator(Language::Russian))
}
fn get_shellenv() -> ShellProps {
ShellProps {
hostname: String::from("default"),
username: String::from("user"),
elapsed_time: Duration::from_secs(0),
exit_status: 0,
wrkdir: PathBuf::from("/home/user/")
}
}
}