use std::borrow::Cow;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicI32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use reedline::{Color, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus};
use tracing::{debug, warn};
use super::EXIT_CODE_NONE;
pub const CMD_DURATION_NONE: u64 = u64::MAX;
struct PromptCache {
left: String,
right: String,
continuation: String,
}
pub struct StarshipPrompt {
last_exit_code: Arc<AtomicI32>,
cmd_duration_ms: Arc<AtomicU64>,
starship_path: PathBuf,
session_key: String,
cache: Mutex<Option<PromptCache>>,
}
impl StarshipPrompt {
pub fn new(
last_exit_code: Arc<AtomicI32>,
cmd_duration_ms: Arc<AtomicU64>,
starship_path: PathBuf,
) -> Self {
Self {
last_exit_code,
cmd_duration_ms,
starship_path,
session_key: format!("{:x}", rand::random::<u64>()),
cache: Mutex::new(None),
}
}
pub fn mark_dirty(&self) {
if let Ok(mut guard) = self.cache.lock() {
*guard = None;
}
}
fn run_starship(&self, extra_args: &[&str]) -> String {
let code = self.last_exit_code.load(Ordering::Relaxed);
let duration = self.cmd_duration_ms.load(Ordering::Relaxed);
let mut cmd = Command::new(&self.starship_path);
cmd.arg("prompt");
cmd.env("STARSHIP_SHELL", "");
cmd.env("STARSHIP_SESSION_KEY", &self.session_key);
for arg in extra_args {
cmd.arg(arg);
}
if code != EXIT_CODE_NONE {
cmd.arg(format!("--status={code}"));
}
if duration != CMD_DURATION_NONE {
cmd.arg(format!("--cmd-duration={duration}"));
}
if let Some(width) = terminal_width() {
cmd.arg(format!("--terminal-width={width}"));
}
match cmd.output() {
Ok(output) => {
let text = String::from_utf8_lossy(&output.stdout).to_string();
debug!(
extra_args = ?extra_args,
status = code,
duration_ms = duration,
output_len = text.len(),
"starship prompt executed"
);
text
}
Err(e) => {
warn!(error = %e, "Failed to execute starship prompt");
String::from("\u{276f} ")
}
}
}
}
impl Prompt for StarshipPrompt {
fn render_prompt_left(&self) -> Cow<'_, str> {
let mut guard = self.cache.lock().unwrap_or_else(|e| e.into_inner());
if guard.is_none() {
*guard = Some(PromptCache {
left: self.run_starship(&[]),
right: self.run_starship(&["--right"]),
continuation: self.run_starship(&["--continuation"]),
});
}
Cow::Owned(guard.as_ref().unwrap().left.clone())
}
fn get_prompt_color(&self) -> Color {
Color::White
}
fn render_prompt_right(&self) -> Cow<'_, str> {
let guard = self.cache.lock().unwrap_or_else(|e| e.into_inner());
match guard.as_ref() {
Some(c) => Cow::Owned(c.right.clone()),
None => Cow::Owned(self.run_starship(&["--right"])),
}
}
fn render_prompt_indicator(&self, _edit_mode: PromptEditMode) -> Cow<'_, str> {
Cow::Borrowed("")
}
fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> {
let guard = self.cache.lock().unwrap_or_else(|e| e.into_inner());
match guard.as_ref() {
Some(c) => Cow::Owned(c.continuation.clone()),
None => Cow::Owned(self.run_starship(&["--continuation"])),
}
}
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))
}
}
fn terminal_width() -> Option<u16> {
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 {
Some(ws.ws_col)
} else {
None
}
}
}