1extern crate regex;
27
28mod cache;
29mod modules;
30
31use super::ShellProps;
32use crate::config::PromptConfig;
33use crate::translator::ioprocessor::IOProcessor;
34use cache::PromptCache;
35use modules::*;
36
37use regex::Regex;
38use std::time::Duration;
39
40const PROMPT_KEY_REGEX: &str = r"\$\{(.*?)\}";
41const PROMPT_USER: &str = "${USER}";
43const PROMPT_HOSTNAME: &str = "${HOSTNAME}";
44const PROMPT_WRKDIR: &str = "${WRKDIR}";
45const PROMPT_CMDTIME: &str = "${CMD_TIME}";
46const PROMPT_RC: &str = "${RC}";
47
48pub struct ShellPrompt {
52 prompt_line: String,
53 translate: bool,
54 break_opt: Option<BreakOptions>,
55 duration_opt: Option<DurationOptions>,
56 rc_opt: Option<RcOptions>,
57 git_opt: Option<GitOptions>,
58 cache: PromptCache,
59}
60
61struct BreakOptions {
65 pub break_with: String,
66}
67
68struct DurationOptions {
72 pub minimum: Duration,
73}
74
75struct RcOptions {
79 pub ok: String,
80 pub err: String,
81}
82
83struct GitOptions {
87 pub branch: String,
88 pub commit_ref_len: usize,
89 pub commit_ref_prepend: Option<String>,
90 pub commit_ref_append: Option<String>
91}
92
93impl ShellPrompt {
94 pub(super) fn new(prompt_opt: &PromptConfig) -> ShellPrompt {
98 let break_opt: Option<BreakOptions> = match prompt_opt.break_enabled {
99 true => Some(BreakOptions::new(&prompt_opt.break_str)),
100 false => None,
101 };
102 let duration_opt: Option<DurationOptions> =
103 match DurationOptions::should_enable(&prompt_opt.prompt_line) {
104 true => Some(DurationOptions::new(prompt_opt.min_duration)),
105 false => None,
106 };
107 let rc_opt: Option<RcOptions> = match RcOptions::should_enable(&prompt_opt.prompt_line) {
108 true => Some(RcOptions::new(&prompt_opt.rc_ok, &prompt_opt.rc_err)),
109 false => None,
110 };
111 let git_opt: Option<GitOptions> = match GitOptions::should_enable(&prompt_opt.prompt_line) {
112 true => Some(GitOptions::new(
113 &prompt_opt.git_branch,
114 prompt_opt.git_commit_ref,
115 &prompt_opt.git_commit_prepend,
116 &prompt_opt.git_commit_append
117 )),
118 false => None,
119 };
120 ShellPrompt {
121 prompt_line: prompt_opt.prompt_line.clone(),
122 translate: prompt_opt.translate,
123 break_opt: break_opt,
124 duration_opt: duration_opt,
125 rc_opt: rc_opt,
126 git_opt: git_opt,
127 cache: PromptCache::new(),
128 }
129 }
130
131 pub(super) fn get_line(&mut self, shell_props: &ShellProps, processor: &IOProcessor) -> String {
135 let mut prompt_line: String = self.process_prompt(shell_props, processor);
136 if self.translate {
138 prompt_line = processor.text_to_cyrillic(&prompt_line);
139 }
140 prompt_line
142 }
143
144 fn process_prompt(&mut self, shell_props: &ShellProps, processor: &IOProcessor) -> String {
150 let mut prompt_line: String = self.prompt_line.clone();
151 lazy_static! {
153 static ref RE: Regex = Regex::new(PROMPT_KEY_REGEX).unwrap();
154 }
155 for regex_match in RE.captures_iter(prompt_line.clone().as_str()) {
156 let mtch: String = String::from(®ex_match[0]);
157 let replace_with: String = self.resolve_key(shell_props, processor, &mtch);
158 prompt_line = prompt_line.replace(mtch.as_str(), replace_with.as_str());
159 }
160 prompt_line = String::from(prompt_line.trim());
162 if let Some(brkopt) = &self.break_opt {
164 prompt_line += "\n";
165 prompt_line += brkopt.break_with.trim();
166 }
167 self.cache.invalidate();
169 prompt_line
171 }
172
173 fn resolve_key(
177 &mut self,
178 shell_props: &ShellProps,
179 processor: &IOProcessor,
180 key: &String,
181 ) -> String {
182 match key.as_str() {
183 PROMPT_CMDTIME => {
184 match &self.duration_opt {
185 Some(opt) => {
186 if shell_props.elapsed_time.as_millis() >= opt.minimum.as_millis() {
187 let millis: u128 = shell_props.elapsed_time.as_millis();
188 let secs: f64 = (millis as f64 / 1000 as f64) as f64;
189 String::from(format!("took {:.1}s", secs))
190 } else {
191 String::from("")
192 }
193 }
194 None => String::from(""),
195 }
196 }
197 modules::git::PROMPT_GIT_BRANCH => {
198 if self.git_opt.is_none() {
199 return String::from("");
200 }
201 if self.cache.get_cached_git().is_none() {
203 let repo_opt = git::find_repository(&shell_props.wrkdir);
204 match repo_opt {
205 Some(repo) => self.cache.cache_git(repo),
206 None => return String::from(""),
207 };
208 }
209 let branch: String = match git::get_branch(self.cache.get_cached_git().unwrap()) {
211 Some(branch) => branch,
212 None => return String::from(""),
213 };
214 String::from(format!(
216 "{}{}",
217 self.git_opt.as_ref().unwrap().branch.clone(),
218 branch
219 ))
220 }
221 modules::git::PROMPT_GIT_COMMIT => {
222 if self.git_opt.is_none() {
223 return String::from("");
224 }
225 if self.cache.get_cached_git().is_none() {
227 let repo_opt = git::find_repository(&shell_props.wrkdir);
228 match repo_opt {
229 Some(repo) => self.cache.cache_git(repo),
230 None => return String::from(""),
231 };
232 }
233 match git::get_commit(
235 self.cache.get_cached_git().unwrap(),
236 self.git_opt.as_ref().unwrap().commit_ref_len,
237 ) {
238 Some(commit) => {
239 let commit_prepend: String = match &self.git_opt.as_ref().unwrap().commit_ref_prepend {
241 Some(s) => s.clone(),
242 None => String::from("")
243 };
244 let commit_append: String = match &self.git_opt.as_ref().unwrap().commit_ref_append {
245 Some(s) => s.clone(),
246 None => String::from("")
247 };
248 format!("{}{}{}", commit_prepend, commit, commit_append)
249 },
250 None => String::from(""),
251 }
252 }
253 PROMPT_HOSTNAME => shell_props.hostname.clone(),
254 modules::colors::PROMPT_KBLINK | modules::colors::PROMPT_KBLK | modules::colors::PROMPT_KBLU | modules::colors::PROMPT_KBOLD | 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_KSELECT | modules::colors::PROMPT_KWHT | modules::colors::PROMPT_KYEL => colors::PromptColor::from_key(key.as_str()).to_string(),
255 modules::language::PROMPT_LANG => language::language_to_str(processor.language),
256 PROMPT_RC => match &self.rc_opt {
257 Some(opt) => match shell_props.exit_status {
258 0 => opt.ok.clone(),
259 _ => opt.err.clone(),
260 },
261 None => String::from(""),
262 },
263 PROMPT_USER => shell_props.username.clone(),
264 PROMPT_WRKDIR => shell_props.wrkdir.as_path().display().to_string(),
265 _ => key.clone(), }
267 }
268}
269
270impl BreakOptions {
271 pub fn new(break_with: &String) -> BreakOptions {
275 BreakOptions {
276 break_with: break_with.clone(),
277 }
278 }
279}
280
281impl DurationOptions {
282 pub fn should_enable(prompt_line: &String) -> bool {
286 prompt_line.contains(PROMPT_CMDTIME)
287 }
288
289 pub fn new(min_duration: usize) -> DurationOptions {
293 DurationOptions {
294 minimum: Duration::from_millis(min_duration as u64),
295 }
296 }
297}
298
299impl RcOptions {
300 pub fn should_enable(prompt_line: &String) -> bool {
304 prompt_line.contains(PROMPT_RC)
305 }
306
307 pub fn new(ok_str: &String, err_str: &String) -> RcOptions {
311 RcOptions {
312 ok: ok_str.clone(),
313 err: err_str.clone(),
314 }
315 }
316}
317
318impl GitOptions {
319 pub fn should_enable(prompt_line: &String) -> bool {
323 prompt_line.contains(modules::git::PROMPT_GIT_BRANCH) || prompt_line.contains(modules::git::PROMPT_GIT_COMMIT)
324 }
325
326 pub fn new(branch: &String, commit: usize, commit_prepend: &Option<String>, commit_append: &Option<String>) -> GitOptions {
330 GitOptions {
331 branch: branch.clone(),
332 commit_ref_len: commit,
333 commit_ref_prepend: commit_prepend.clone(),
334 commit_ref_append: commit_append.clone()
335 }
336 }
337}
338
339#[cfg(test)]
340mod tests {
341
342 use super::*;
343 use crate::config::PromptConfig;
344 use crate::translator::ioprocessor::IOProcessor;
345 use crate::translator::new_translator;
346 use crate::translator::lang::Language;
347 use colors::PromptColor;
348
349 use git2::Repository;
350 use std::path::PathBuf;
351 use std::time::Duration;
352
353 #[test]
354 fn test_prompt_simple() {
355 let prompt_config_default = PromptConfig::default();
356 let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
357 let iop: IOProcessor = get_ioprocessor();
358 let shellenv: ShellProps = get_shellenv();
359 let _ = prompt.get_line(&shellenv, &iop);
361 prompt.translate = true;
362 let _ = prompt.get_line(&shellenv, &iop);
364 let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
366 let expected_prompt_line = String::from(format!(
367 "{}@{}:{}$",
368 shellenv.username.clone(),
369 shellenv.hostname.clone(),
370 shellenv.wrkdir.display()
371 ));
372 assert_eq!(prompt_line, expected_prompt_line);
373 println!("\n");
376 }
377
378 #[test]
379 fn test_prompt_colors() {
380 let mut prompt_config_default = PromptConfig::default();
381 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${KBOLD}BOLD${KBLINK}BLINK${KSELECT}SELECTED${KRST}");
383 let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
384 let iop: IOProcessor = get_ioprocessor();
385 let shellenv: ShellProps = get_shellenv();
386 let _ = prompt.get_line(&shellenv, &iop);
388 prompt.translate = true;
389 let _ = prompt.get_line(&shellenv, &iop);
391 let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
393 let expected_prompt_line = String::from(format!(
394 "{}RED{}YEL{}BLU{}GRN{}WHT{}GRY{}BLK{}MAG{}CYN{}BOLD{}BLINK{}SELECTED{}",
395 PromptColor::Red.to_string(),
396 PromptColor::Yellow.to_string(),
397 PromptColor::Blue.to_string(),
398 PromptColor::Green.to_string(),
399 PromptColor::White.to_string(),
400 PromptColor::Gray.to_string(),
401 PromptColor::Black.to_string(),
402 PromptColor::Magenta.to_string(),
403 PromptColor::Cyan.to_string(),
404 PromptColor::Bold.to_string(),
405 PromptColor::Blink.to_string(),
406 PromptColor::Select.to_string(),
407 PromptColor::Reset.to_string()
408 ));
409 assert_eq!(prompt_line, expected_prompt_line);
410 println!("\n");
413 }
414
415 #[test]
416 fn test_prompt_lang_time_with_break() {
417 let mut prompt_config_default = PromptConfig::default();
418 prompt_config_default.prompt_line = String::from("${LANG} ~ ${KYEL}${USER}${KRST} on ${KGRN}${HOSTNAME}${KRST} in ${KCYN}${WRKDIR}${KRST} ${KYEL}${CMD_TIME}${KRST}");
420 prompt_config_default.break_enabled = true;
421 let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
422 let iop: IOProcessor = get_ioprocessor();
423 let mut shellenv: ShellProps = get_shellenv();
424 shellenv.elapsed_time = Duration::from_millis(5100);
425 shellenv.wrkdir = PathBuf::from("/tmp/");
426 let _ = prompt.get_line(&shellenv, &iop);
428 prompt.translate = true;
429 let _ = prompt.get_line(&shellenv, &iop);
431 let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
433 let expected_prompt_line = String::from(format!(
434 "{} ~ {}{}{} on {}{}{} in {}{}{} {}took 5.1s{}\n❯",
435 language::language_to_str(Language::Russian),
436 PromptColor::Yellow.to_string(),
437 shellenv.username.clone(),
438 PromptColor::Reset.to_string(),
439 PromptColor::Green.to_string(),
440 shellenv.hostname.clone(),
441 PromptColor::Reset.to_string(),
442 PromptColor::Cyan.to_string(),
443 shellenv.wrkdir.display(),
444 PromptColor::Reset.to_string(),
445 PromptColor::Yellow.to_string(),
446 PromptColor::Reset.to_string()
447 ));
448 assert_eq!(prompt_line, expected_prompt_line);
449 println!("\n");
452 }
453
454 #[test]
455 fn test_prompt_git() {
456 let repo: Repository = git::find_repository(&PathBuf::from("./")).unwrap();
459 let branch: String = git::get_branch(&repo).unwrap();
461 let commit: String = git::get_commit(&repo, 8).unwrap();
462 let mut prompt_config = PromptConfig::default();
463 prompt_config.prompt_line =
465 String::from("${USER}@${HOSTNAME}:${WRKDIR} ${GIT_BRANCH} ${GIT_COMMIT}");
466 let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config);
467 let iop: IOProcessor = get_ioprocessor();
468 let mut shellenv: ShellProps = get_shellenv();
469 shellenv.elapsed_time = Duration::from_millis(5100);
470 shellenv.wrkdir = PathBuf::from("./");
471 let _ = prompt.get_line(&shellenv, &iop);
473 prompt.translate = true;
474 let _ = prompt.get_line(&shellenv, &iop);
476 let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
478 let expected_prompt_line = String::from(format!(
479 "{}@{}:{} on {} {}",
480 shellenv.username.clone(),
481 shellenv.hostname.clone(),
482 shellenv.wrkdir.display(),
483 branch,
484 commit
485 ));
486 assert_eq!(prompt_line, expected_prompt_line);
487 println!("\n");
490 prompt_config.git_commit_append = Some(String::from(")"));
492 prompt_config.git_commit_prepend = Some(String::from("("));
493 let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config);
494 let iop: IOProcessor = get_ioprocessor();
495 let mut shellenv: ShellProps = get_shellenv();
496 shellenv.elapsed_time = Duration::from_millis(5100);
497 shellenv.wrkdir = PathBuf::from("./");
498 let _ = prompt.get_line(&shellenv, &iop);
500 prompt.translate = true;
501 let _ = prompt.get_line(&shellenv, &iop);
503 let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
505 let expected_prompt_line = String::from(format!(
506 "{}@{}:{} on {} ({})",
507 shellenv.username.clone(),
508 shellenv.hostname.clone(),
509 shellenv.wrkdir.display(),
510 branch,
511 commit
512 ));
513 assert_eq!(prompt_line, expected_prompt_line);
514 println!("\n");
517 }
518
519 #[test]
520 fn test_prompt_git_not_in_repo() {
521 let mut prompt_config_default = PromptConfig::default();
522 prompt_config_default.prompt_line =
524 String::from("${USER}@${HOSTNAME}:${WRKDIR} ${GIT_BRANCH} ${GIT_COMMIT}");
525 let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
526 let iop: IOProcessor = get_ioprocessor();
527 let mut shellenv: ShellProps = get_shellenv();
528 shellenv.elapsed_time = Duration::from_millis(5100);
529 shellenv.wrkdir = PathBuf::from("/");
530 let _ = prompt.get_line(&shellenv, &iop);
532 prompt.translate = true;
533 let _ = prompt.get_line(&shellenv, &iop);
535 let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
537 let expected_prompt_line = String::from(format!(
538 "{}@{}:{}",
539 shellenv.username.clone(),
540 shellenv.hostname.clone(),
541 shellenv.wrkdir.display()
542 ));
543 assert_eq!(prompt_line, expected_prompt_line);
544 println!("\n");
547 }
548
549 #[test]
550 fn test_prompt_rc_ok() {
551 let mut prompt_config_default = PromptConfig::default();
552 prompt_config_default.prompt_line = String::from("${RC} ${USER}@${HOSTNAME}:${WRKDIR}");
554 let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
555 let iop: IOProcessor = get_ioprocessor();
556 let mut shellenv: ShellProps = get_shellenv();
557 shellenv.elapsed_time = Duration::from_millis(5100);
558 shellenv.wrkdir = PathBuf::from("/");
559 let _ = prompt.get_line(&shellenv, &iop);
561 prompt.translate = true;
562 let _ = prompt.get_line(&shellenv, &iop);
564 let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
566 let expected_prompt_line = String::from(format!(
567 "✔ {}@{}:{}",
568 shellenv.username.clone(),
569 shellenv.hostname.clone(),
570 shellenv.wrkdir.display()
571 ));
572 assert_eq!(prompt_line, expected_prompt_line);
573 println!("\n");
576 }
577
578 #[test]
579 fn test_prompt_rc_error() {
580 let mut prompt_config_default = PromptConfig::default();
581 prompt_config_default.prompt_line = String::from("${RC} ${USER}@${HOSTNAME}:${WRKDIR}");
583 let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
584 let iop: IOProcessor = get_ioprocessor();
585 let mut shellenv: ShellProps = get_shellenv();
586 shellenv.elapsed_time = Duration::from_millis(5100);
587 shellenv.wrkdir = PathBuf::from("/");
588 shellenv.exit_status = 255;
589 let _ = prompt.get_line(&shellenv, &iop);
591 prompt.translate = true;
592 let _ = prompt.get_line(&shellenv, &iop);
594 let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
596 let expected_prompt_line = String::from(format!(
597 "✖ {}@{}:{}",
598 shellenv.username.clone(),
599 shellenv.hostname.clone(),
600 shellenv.wrkdir.display()
601 ));
602 assert_eq!(prompt_line, expected_prompt_line);
603 println!("\n");
606 }
607
608 #[test]
609 fn test_prompt_unresolved() {
610 let mut prompt_config_default = PromptConfig::default();
611 prompt_config_default.prompt_line = String::from("${USER}@${HOSTNAME}:${WRKDIR} ${FOOBAR}");
613 let mut prompt: ShellPrompt = ShellPrompt::new(&prompt_config_default);
614 let iop: IOProcessor = get_ioprocessor();
615 let mut shellenv: ShellProps = get_shellenv();
616 shellenv.elapsed_time = Duration::from_millis(5100);
617 shellenv.wrkdir = PathBuf::from("/");
618 shellenv.exit_status = 255;
619 let _ = prompt.get_line(&shellenv, &iop);
621 prompt.translate = true;
622 let _ = prompt.get_line(&shellenv, &iop);
624 let prompt_line: String = prompt.process_prompt(&shellenv, &iop);
626 let expected_prompt_line = String::from(format!(
627 "{}@{}:{} {}",
628 shellenv.username.clone(),
629 shellenv.hostname.clone(),
630 shellenv.wrkdir.display(),
631 "${FOOBAR}"
632 ));
633 assert_eq!(prompt_line, expected_prompt_line);
634 println!("\n");
637 }
638
639 fn get_ioprocessor() -> IOProcessor {
640 IOProcessor::new(Language::Russian, new_translator(Language::Russian))
641 }
642
643 fn get_shellenv() -> ShellProps {
644 ShellProps {
645 hostname: String::from("default"),
646 username: String::from("user"),
647 elapsed_time: Duration::from_secs(0),
648 exit_status: 0,
649 wrkdir: PathBuf::from("/home/user/")
650 }
651 }
652}