use std::borrow::Cow;
use std::env;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::{Arc, RwLock};
use chrono::Local;
use reedline::{Color, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus};
use super::git::{current_git_branch_at, format_branch_label, format_git_status_at};
use crate::cli::color::{cyan, green, red, white, yellow};
use crate::config::PromptConfig;
pub const EXIT_CODE_NONE: i32 = i32::MIN;
pub fn shorten_path(path: &Path) -> String {
if let Some(home) = dirs_home() {
if path == home {
return "~".to_string();
}
if let Ok(rel) = path.strip_prefix(&home) {
return format!("~/{}", rel.display());
}
}
path.display().to_string()
}
pub(super) fn dirs_home() -> Option<PathBuf> {
env::var_os("HOME").map(PathBuf::from)
}
enum AsyncGitState {
Outdated,
Loading { branch: String },
Ready { formatted: String, cwd: PathBuf },
Revalidating { stale: String },
}
pub struct JarvisPrompt {
last_exit_code: Arc<AtomicI32>,
config: PromptConfig,
git_state: Arc<RwLock<AsyncGitState>>,
}
impl JarvisPrompt {
pub fn new(last_exit_code: Arc<AtomicI32>, config: PromptConfig) -> Self {
Self {
last_exit_code,
config,
git_state: Arc::new(RwLock::new(AsyncGitState::Outdated)),
}
}
pub fn refresh_git_status(&self) {
let cwd = env::current_dir().unwrap_or_default();
let nerd_font = self.config.nerd_font;
let Ok(mut state) = self.git_state.write() else {
return;
};
if matches!(
&*state,
AsyncGitState::Loading { .. } | AsyncGitState::Revalidating { .. }
) {
return;
}
let prev = std::mem::replace(&mut *state, AsyncGitState::Outdated);
match prev {
AsyncGitState::Ready {
formatted,
cwd: cached_cwd,
} if cached_cwd == cwd => {
*state = AsyncGitState::Revalidating { stale: formatted };
}
AsyncGitState::Ready { .. } | AsyncGitState::Outdated => {
match current_git_branch_at(&cwd) {
Some(branch_name) => {
*state = AsyncGitState::Loading {
branch: branch_name,
};
}
None => {
*state = AsyncGitState::Ready {
formatted: String::new(),
cwd,
};
return;
}
}
}
_ => unreachable!(),
}
let git_state = Arc::clone(&self.git_state);
let cwd_for_thread = cwd.clone();
drop(state);
std::thread::spawn(move || {
let formatted = match current_git_branch_at(&cwd_for_thread) {
Some(branch_name) => {
let status = format_git_status_at(&cwd_for_thread, nerd_font);
let branch_label = format_branch_label(&branch_name, nerd_font);
format!("on {branch_label} {status}")
}
None => String::new(),
};
if let Ok(mut s) = git_state.write() {
*s = AsyncGitState::Ready {
formatted,
cwd: cwd_for_thread,
};
}
});
}
fn resolve_git_part(&self) -> String {
let nerd_font = self.config.nerd_font;
let Ok(state) = self.git_state.try_read() else {
return String::new();
};
match &*state {
AsyncGitState::Outdated => String::new(),
AsyncGitState::Loading { branch } => {
let branch_label = format_branch_label(branch, nerd_font);
format!("on {branch_label}")
}
AsyncGitState::Ready { formatted, .. } => formatted.clone(),
AsyncGitState::Revalidating { stale } => stale.clone(),
}
}
}
impl Prompt for JarvisPrompt {
fn render_prompt_left(&self) -> Cow<'_, str> {
let cwd = env::current_dir().unwrap_or_default();
let cwd_display = shorten_path(&cwd);
let git_part = self.resolve_git_part();
let code = self.last_exit_code.load(Ordering::Relaxed);
let label = if code != 0 && code != EXIT_CODE_NONE {
red("\u{2717} jarvis")
} else if code == 0 {
cyan("\u{2714}\u{fe0e} jarvis")
} else {
cyan("jarvis")
};
let cwd_label = if self.config.nerd_font {
yellow(&format!("\u{f4d3} {cwd_display}"))
} else {
yellow(&cwd_display)
};
Cow::Owned(format!("{label} in {cwd_label} {git_part}\n"))
}
fn get_prompt_color(&self) -> Color {
Color::White
}
fn render_prompt_right(&self) -> Cow<'_, str> {
let now = Local::now().format("%H:%M:%S").to_string();
Cow::Owned(white(&now))
}
fn render_prompt_indicator(&self, _edit_mode: PromptEditMode) -> Cow<'_, str> {
Cow::Owned(green("\u{276f} "))
}
fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> {
Cow::Borrowed(" :: ")
}
fn render_prompt_history_search_indicator(
&self,
history_search: PromptHistorySearch,
) -> Cow<'_, str> {
let prefix = match history_search.status {
PromptHistorySearchStatus::Passing => "",
PromptHistorySearchStatus::Failing => "(failed) ",
};
Cow::Owned(format!("{prefix}(search: '{}') ", history_search.term))
}
}