use crate::config::{
ConfigStatus, HistoryForgetConfig, Indicators, ModeIndicatorPosition, PromptDurationConfig,
RSourceStatus, ReprexMode, SpinnerConfig, StatusColorConfig, StatusConfig, ViColorConfig,
ViConfig,
};
use crate::editor::prompt::PromptFormatter;
use nu_ansi_term::Color;
use reedline::{HistoryItemId, HistorySessionId, Reedline};
use std::path::PathBuf;
use std::time::{Duration, Instant};
mod sponge;
#[cfg(test)]
mod sponge_tests;
pub use sponge::SpongeQueue;
use super::prompt::RPrompt;
use super::reprex::ReprexRuntime;
#[derive(Default)]
pub enum PendingHistoryContext {
Command {
store: Option<crate::history::HistoryStore>,
history_id: Option<HistoryItemId>,
},
#[default]
None,
}
fn color_to_ansi_code(color: Color) -> String {
use nu_ansi_term::Style;
match color {
Color::Default => String::new(),
c => {
let style = Style::new().fg(c);
style.prefix().to_string()
}
}
}
pub struct ReplState {
pub line_editor: Reedline,
pub shell_line_editor: Reedline,
pub prompt_config: PromptRuntimeConfig,
pub reprex: ReprexRuntime,
pub should_exit: bool,
pub config_path: Option<PathBuf>,
pub config_status: ConfigStatus,
pub r_source_status: RSourceStatus,
pub r_home: Option<PathBuf>,
pub forget_config: HistoryForgetConfig,
pub sponge_queue: SpongeQueue,
pub dir_stack: Vec<PathBuf>,
pub history_session_id: Option<HistorySessionId>,
pub r_history: crate::history::HistoryRuntime,
pub shell_history: crate::history::HistoryRuntime,
pub pending_history_context: PendingHistoryContext,
}
pub struct PromptRuntimeConfig {
prompt_formatter: PromptFormatter,
main_template: String,
continuation_template: String,
shell_template: String,
mode_indicator_position: ModeIndicatorPosition,
indicators: Indicators,
shell_enabled: bool,
main_color: Color,
continuation_color: Color,
shell_color: Color,
mode_indicator_color: Color,
status_config: StatusConfig,
status_colors: StatusColorConfig,
last_command_failed: bool,
duration_config: PromptDurationConfig,
duration_color: Color,
last_command_start: Option<Instant>,
last_command_duration: Option<Duration>,
spinner_config: SpinnerConfig,
vi_config: ViConfig,
vi_colors: ViColorConfig,
}
impl PromptRuntimeConfig {
pub fn builder(
prompt_formatter: PromptFormatter,
main_template: impl Into<String>,
continuation_template: impl Into<String>,
shell_template: impl Into<String>,
) -> PromptRuntimeConfigBuilder {
PromptRuntimeConfigBuilder::new(
prompt_formatter,
main_template,
continuation_template,
shell_template,
)
}
pub fn build_main_prompt(&self, reprex_mode: ReprexMode) -> RPrompt {
if self.shell_enabled {
let shell_format = self.prompt_formatter.format(&self.shell_template);
let cont_format = self.prompt_formatter.format(&self.continuation_template);
RPrompt::new(shell_format, cont_format)
.with_colors(
self.shell_color,
self.continuation_color,
self.mode_indicator_color,
)
.with_vi_symbol(self.vi_config.symbol.clone())
.with_vi_colors(self.vi_colors.clone())
} else {
let main_format = self.prompt_formatter.format(&self.main_template);
let cont_format = self.prompt_formatter.format(&self.continuation_template);
let mode_indicator = self.current_mode_indicator(reprex_mode);
let prompt_color = self.get_status_prompt_color();
let main_format = self.expand_status_placeholder(&main_format, prompt_color);
let main_format = self.expand_duration_placeholder(&main_format, prompt_color);
RPrompt::new(main_format, cont_format)
.with_mode_indicator(mode_indicator, self.mode_indicator_position)
.with_colors(
prompt_color,
self.continuation_color,
self.mode_indicator_color,
)
.with_vi_symbol(self.vi_config.symbol.clone())
.with_vi_colors(self.vi_colors.clone())
}
}
fn expand_status_placeholder(&self, template: &str, prompt_color: Color) -> String {
use nu_ansi_term::Style;
if !template.contains("{status}") {
return template.to_string();
}
let symbol = if self.last_command_failed {
&self.status_config.symbol.error
} else {
&self.status_config.symbol.success
};
let colored_symbol = if symbol.is_empty() {
String::new()
} else {
let status_color = if self.last_command_failed {
self.status_colors.error
} else {
self.status_colors.success
};
let status_style = match status_color {
Color::Default => Style::new(),
c => Style::new().fg(c),
};
let prompt_style = match prompt_color {
Color::Default => Style::new(),
c => Style::new().fg(c),
};
format!("{}{}", status_style.paint(symbol), prompt_style.prefix())
};
template.replace("{status}", &colored_symbol)
}
fn get_status_prompt_color(&self) -> Color {
if self.status_config.override_prompt_color {
if self.last_command_failed {
self.status_colors.error
} else {
self.status_colors.success
}
} else {
self.main_color
}
}
pub fn set_last_command_failed(&mut self, failed: bool) {
self.last_command_failed = failed;
}
pub fn set_command_start(&mut self) {
self.last_command_start = Some(Instant::now());
}
pub fn set_command_duration(&mut self) {
self.last_command_duration = self.last_command_start.take().map(|start| start.elapsed());
}
pub fn clear_command_duration(&mut self) {
self.last_command_duration = None;
}
fn expand_duration_placeholder(&self, template: &str, prompt_color: Color) -> String {
use nu_ansi_term::Style;
if !template.contains("{duration}") {
return template.to_string();
}
let duration_str = match self.last_command_duration {
Some(duration)
if duration.as_millis() >= u128::from(self.duration_config.threshold_ms) =>
{
let time_str = render_time(duration);
let formatted = self.duration_config.format.replace("{value}", &time_str);
let duration_style = match self.duration_color {
Color::Default => Style::new(),
c => Style::new().fg(c),
};
let prompt_style = match prompt_color {
Color::Default => Style::new(),
c => Style::new().fg(c),
};
format!(
"{}{}",
duration_style.paint(&formatted),
prompt_style.prefix()
)
}
_ => String::new(),
};
template.replace("{duration}", &duration_str)
}
pub fn build_cont_prompt(&self, reprex_mode: ReprexMode) -> RPrompt {
let cont_format = self.prompt_formatter.format(&self.continuation_template);
let mode_indicator = self.current_mode_indicator(reprex_mode);
RPrompt::new(cont_format.clone(), cont_format)
.with_mode_indicator(mode_indicator, self.mode_indicator_position)
.with_colors(
self.continuation_color,
self.continuation_color,
self.mode_indicator_color,
)
}
fn current_mode_indicator(&self, reprex_mode: ReprexMode) -> Option<String> {
if self.mode_indicator_position == ModeIndicatorPosition::None {
return None;
}
match reprex_mode {
ReprexMode::Off => None,
ReprexMode::On => Some(self.indicators.reprex.clone()),
ReprexMode::Format => Some(self.indicators.reprex_format.clone()),
}
}
pub fn is_shell_enabled(&self) -> bool {
self.shell_enabled
}
pub fn set_shell(&mut self, enabled: bool) {
self.shell_enabled = enabled;
crate::ipc::set_in_alternate_mode(enabled);
}
pub fn start_spinner(&self) {
if self.shell_enabled {
return;
}
if self.spinner_config.frames.is_empty() {
return;
}
arf_libr::start_spinner();
}
}
fn render_time(duration: Duration) -> String {
let total_secs = duration.as_secs();
if total_secs == 0 {
return format!("{}ms", duration.as_millis());
}
let days = total_secs / 86400;
let hours = (total_secs % 86400) / 3600;
let minutes = (total_secs % 3600) / 60;
let seconds = total_secs % 60;
let mut result = String::new();
let mut started = false;
if days > 0 {
result.push_str(&format!("{}d", days));
started = true;
}
if started || hours > 0 {
result.push_str(&format!("{}h", hours));
started = true;
}
if started || minutes > 0 {
result.push_str(&format!("{}m", minutes));
}
result.push_str(&format!("{}s", seconds));
result
}
pub struct PromptRuntimeConfigBuilder {
prompt_formatter: PromptFormatter,
main_template: String,
continuation_template: String,
shell_template: String,
mode_indicator_position: ModeIndicatorPosition,
indicators: Indicators,
main_color: Color,
continuation_color: Color,
shell_color: Color,
mode_indicator_color: Color,
status_config: StatusConfig,
status_colors: StatusColorConfig,
duration_config: PromptDurationConfig,
duration_color: Color,
spinner_config: SpinnerConfig,
vi_config: ViConfig,
vi_colors: ViColorConfig,
}
impl PromptRuntimeConfigBuilder {
fn new(
prompt_formatter: PromptFormatter,
main_template: impl Into<String>,
continuation_template: impl Into<String>,
shell_template: impl Into<String>,
) -> Self {
Self {
prompt_formatter,
main_template: main_template.into(),
continuation_template: continuation_template.into(),
shell_template: shell_template.into(),
mode_indicator_position: ModeIndicatorPosition::default(),
indicators: Indicators::default(),
main_color: Color::Default,
continuation_color: Color::Default,
shell_color: Color::Default,
mode_indicator_color: Color::Default,
status_config: StatusConfig::default(),
status_colors: StatusColorConfig::default(),
duration_config: PromptDurationConfig::default(),
duration_color: Color::Default,
spinner_config: SpinnerConfig::default(),
vi_config: ViConfig::default(),
vi_colors: ViColorConfig::default(),
}
}
pub fn mode_indicator_position(mut self, position: ModeIndicatorPosition) -> Self {
self.mode_indicator_position = position;
self
}
pub fn indicators(mut self, indicators: Indicators) -> Self {
self.indicators = indicators;
self
}
pub fn main_color(mut self, color: Color) -> Self {
self.main_color = color;
self
}
pub fn continuation_color(mut self, color: Color) -> Self {
self.continuation_color = color;
self
}
pub fn shell_color(mut self, color: Color) -> Self {
self.shell_color = color;
self
}
pub fn mode_indicator_color(mut self, color: Color) -> Self {
self.mode_indicator_color = color;
self
}
pub fn status(mut self, config: StatusConfig, colors: StatusColorConfig) -> Self {
self.status_config = config;
self.status_colors = colors;
self
}
pub fn duration(mut self, config: PromptDurationConfig, color: Color) -> Self {
self.duration_config = config;
self.duration_color = color;
self
}
pub fn spinner(mut self, config: SpinnerConfig) -> Self {
self.spinner_config = config;
self
}
pub fn vi(mut self, config: ViConfig, colors: ViColorConfig) -> Self {
self.vi_config = config;
self.vi_colors = colors;
self
}
pub fn build(self) -> PromptRuntimeConfig {
arf_libr::set_spinner_frames(&self.spinner_config.frames);
let color_code = color_to_ansi_code(self.spinner_config.color);
arf_libr::set_spinner_color(&color_code);
PromptRuntimeConfig {
prompt_formatter: self.prompt_formatter,
main_template: self.main_template,
continuation_template: self.continuation_template,
shell_template: self.shell_template,
mode_indicator_position: self.mode_indicator_position,
indicators: self.indicators,
shell_enabled: false,
main_color: self.main_color,
continuation_color: self.continuation_color,
shell_color: self.shell_color,
mode_indicator_color: self.mode_indicator_color,
status_config: self.status_config,
status_colors: self.status_colors,
last_command_failed: false,
duration_config: self.duration_config,
duration_color: self.duration_color,
last_command_start: None,
last_command_duration: None,
spinner_config: self.spinner_config,
vi_config: self.vi_config,
vi_colors: self.vi_colors,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::StatusSymbol;
use reedline::Prompt;
fn create_test_config(mode: ReprexMode) -> PromptRuntimeConfig {
create_test_config_with_indicators(mode, Indicators::default())
}
fn create_test_config_with_indicators(
_mode: ReprexMode,
indicators: Indicators,
) -> PromptRuntimeConfig {
PromptRuntimeConfig::builder(PromptFormatter::default(), "r> ", "+ ", "[bash] $ ")
.indicators(indicators)
.build()
}
#[test]
fn test_prompt_runtime_config_build_main_prompt() {
let _guard = crate::test_utils::lock_env();
let config = create_test_config(ReprexMode::Off);
let prompt = config.build_main_prompt(ReprexMode::Off);
assert_eq!(prompt.render_prompt_left(), "r> ");
}
#[test]
fn test_prompt_runtime_config_reprex_mode_indicator() {
let _guard = crate::test_utils::lock_env();
let config = create_test_config(ReprexMode::On);
let prompt = config.build_main_prompt(ReprexMode::On);
assert_eq!(prompt.render_prompt_left(), "[reprex] r> ");
}
#[test]
fn test_prompt_runtime_config_set_reprex_mode() {
let _guard = crate::test_utils::lock_env();
let mut runtime =
ReprexRuntime::new(ReprexMode::Off, "#> ", crate::config::FormatterBackend::Air);
assert!(!runtime.is_enabled());
runtime.set_mode(ReprexMode::On);
assert!(runtime.is_enabled());
runtime.set_mode(ReprexMode::Off);
assert!(!runtime.is_enabled());
}
#[test]
fn test_prompt_runtime_config_set_reprex_with_comment() {
let _guard = crate::test_utils::lock_env();
let mut runtime =
ReprexRuntime::new(ReprexMode::Off, "#> ", crate::config::FormatterBackend::Air);
runtime.set_mode(ReprexMode::On);
assert!(runtime.is_enabled());
}
#[test]
fn test_prompt_runtime_config_shell_mode_prompt() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_config(ReprexMode::Off);
let mut runtime =
ReprexRuntime::new(ReprexMode::Off, "#> ", crate::config::FormatterBackend::Air);
let prompt = config.build_main_prompt(runtime.mode);
assert_eq!(prompt.render_prompt_left(), "r> ");
config.set_shell(true);
let prompt = config.build_main_prompt(runtime.mode);
assert_eq!(prompt.render_prompt_left(), "[bash] $ ");
runtime.set_mode(ReprexMode::On);
let prompt = config.build_main_prompt(runtime.mode);
assert_eq!(prompt.render_prompt_left(), "[bash] $ ");
config.set_shell(false);
let prompt = config.build_main_prompt(runtime.mode);
assert_eq!(prompt.render_prompt_left(), "[reprex] r> ");
}
#[test]
fn test_prompt_runtime_config_format_mode_indicator() {
let _guard = crate::test_utils::lock_env();
let config = create_test_config(ReprexMode::Off);
let mut runtime =
ReprexRuntime::new(ReprexMode::Off, "#> ", crate::config::FormatterBackend::Air);
let prompt = config.build_main_prompt(runtime.mode);
assert_eq!(prompt.render_prompt_left(), "r> ");
runtime.set_mode(ReprexMode::On);
let prompt = config.build_main_prompt(runtime.mode);
assert_eq!(prompt.render_prompt_left(), "[reprex] r> ");
runtime.set_mode(ReprexMode::Format);
let prompt = config.build_main_prompt(runtime.mode);
assert_eq!(prompt.render_prompt_left(), "[format] r> ");
runtime.set_mode(ReprexMode::Off);
let prompt = config.build_main_prompt(runtime.mode);
assert_eq!(prompt.render_prompt_left(), "r> ");
runtime.set_mode(ReprexMode::Format);
let prompt = config.build_main_prompt(runtime.mode);
assert_eq!(prompt.render_prompt_left(), "[format] r> ");
runtime.set_mode(ReprexMode::On);
let prompt = config.build_main_prompt(runtime.mode);
assert_eq!(prompt.render_prompt_left(), "[reprex] r> ");
}
#[test]
fn test_prompt_runtime_config_custom_format_indicator() {
let _guard = crate::test_utils::lock_env();
let indicators = Indicators {
reprex_format: "[AIR] ".to_string(),
..Indicators::default()
};
let config = create_test_config_with_indicators(ReprexMode::Format, indicators);
let prompt = config.build_main_prompt(ReprexMode::Format);
assert_eq!(prompt.render_prompt_left(), "[AIR] r> ");
}
#[test]
fn test_prompt_runtime_config_cwd_placeholder_expansion() {
let _guard = crate::test_utils::lock_env();
let config = PromptRuntimeConfig::builder(
PromptFormatter::default(),
"{cwd_short}> ",
"+ ",
"[{shell}] $ ",
)
.build();
let prompt = config.build_main_prompt(ReprexMode::Off);
let rendered = prompt.render_prompt_left().to_string();
assert!(
!rendered.contains("{cwd_short}"),
"Placeholder should be expanded, got: {}",
rendered
);
assert!(
rendered.ends_with("> "),
"Prompt should end with '> ', got: {}",
rendered
);
}
#[test]
fn test_prompt_runtime_config_dynamic_cwd_update() {
let _guard = crate::test_utils::lock_env_and_cwd();
let config =
PromptRuntimeConfig::builder(PromptFormatter::default(), "{cwd}> ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.build();
let original_cwd = std::env::current_dir().unwrap();
let prompt1 = config.build_main_prompt(ReprexMode::Off);
let rendered1 = prompt1.render_prompt_left().to_string();
let temp_dir = std::env::temp_dir();
std::env::set_current_dir(&temp_dir).unwrap();
let prompt2 = config.build_main_prompt(ReprexMode::Off);
let rendered2 = prompt2.render_prompt_left().to_string();
if original_cwd != temp_dir {
assert_ne!(
rendered1, rendered2,
"Prompt should update when cwd changes.\nBefore: {}\nAfter: {}",
rendered1, rendered2
);
}
#[cfg(unix)]
let is_absolute_path = rendered2.starts_with("/");
#[cfg(windows)]
let is_absolute_path = rendered2.len() >= 3 && rendered2.chars().nth(1) == Some(':');
assert!(
rendered2.contains(&temp_dir.to_string_lossy().to_string()) || is_absolute_path,
"Prompt should contain temp dir path, got: {}",
rendered2
);
}
#[test]
fn test_status_indicator_with_error_symbol() {
let _guard = crate::test_utils::lock_env();
let status_config = StatusConfig {
symbol: StatusSymbol {
success: "".to_string(),
error: "✗ ".to_string(),
},
override_prompt_color: false,
};
let mut config =
PromptRuntimeConfig::builder(PromptFormatter::default(), "{status}r> ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.status(status_config, StatusColorConfig::default())
.build();
let prompt = config.build_main_prompt(ReprexMode::Off);
assert_eq!(prompt.render_prompt_left(), "r> ");
config.set_last_command_failed(true);
let prompt = config.build_main_prompt(ReprexMode::Off);
let rendered = prompt.render_prompt_left();
assert!(
rendered.contains("✗"),
"Should contain error symbol, got: {}",
rendered
);
assert!(
rendered.ends_with("r> "),
"Should end with prompt, got: {}",
rendered
);
config.set_last_command_failed(false);
let prompt = config.build_main_prompt(ReprexMode::Off);
assert_eq!(prompt.render_prompt_left(), "r> ");
}
#[test]
fn test_status_indicator_with_empty_symbols() {
let _guard = crate::test_utils::lock_env();
let status_config = StatusConfig {
symbol: StatusSymbol {
success: "".to_string(),
error: "".to_string(),
},
override_prompt_color: false,
};
let mut config =
PromptRuntimeConfig::builder(PromptFormatter::default(), "{status}r> ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.status(status_config, StatusColorConfig::default())
.build();
let prompt = config.build_main_prompt(ReprexMode::Off);
assert_eq!(prompt.render_prompt_left(), "r> ");
config.set_last_command_failed(true);
let prompt = config.build_main_prompt(ReprexMode::Off);
assert_eq!(prompt.render_prompt_left(), "r> ");
}
#[test]
fn test_status_without_placeholder() {
let _guard = crate::test_utils::lock_env();
let status_config = StatusConfig {
symbol: StatusSymbol {
success: "✓ ".to_string(),
error: "✗ ".to_string(),
},
override_prompt_color: false,
};
let mut config = PromptRuntimeConfig::builder(
PromptFormatter::default(),
"r> ", "+ ",
"$ ",
)
.mode_indicator_position(ModeIndicatorPosition::None)
.status(status_config, StatusColorConfig::default())
.build();
let prompt = config.build_main_prompt(ReprexMode::Off);
assert_eq!(prompt.render_prompt_left(), "r> ");
config.set_last_command_failed(true);
let prompt = config.build_main_prompt(ReprexMode::Off);
assert_eq!(prompt.render_prompt_left(), "r> ");
}
#[test]
fn test_status_override_prompt_color() {
let _guard = crate::test_utils::lock_env();
let status_config = StatusConfig {
symbol: StatusSymbol {
success: "".to_string(),
error: "✗ ".to_string(),
},
override_prompt_color: true, };
let status_colors = StatusColorConfig {
success: Color::Green,
error: Color::Red,
};
let mut config =
PromptRuntimeConfig::builder(PromptFormatter::default(), "{status}r> ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.main_color(Color::LightGreen) .status(status_config, status_colors)
.build();
let prompt = config.build_main_prompt(ReprexMode::Off);
let rendered = prompt.render_prompt_left();
assert!(
rendered.contains("r> "),
"Should contain prompt text, got: {}",
rendered
);
config.set_last_command_failed(true);
let prompt = config.build_main_prompt(ReprexMode::Off);
let rendered = prompt.render_prompt_left();
assert!(
rendered.contains("✗") && rendered.contains("r>"),
"Should contain error symbol and prompt, got: {}",
rendered
);
}
#[test]
fn test_spinner_not_started_in_shell_mode() {
let _guard = crate::test_utils::lock_env();
let mut config = create_test_config(ReprexMode::Off);
config.set_shell(true);
config.start_spinner();
assert!(config.is_shell_enabled());
}
#[test]
fn test_spinner_not_started_with_empty_frames() {
let _guard = crate::test_utils::lock_env();
let config = create_test_config(ReprexMode::Off);
config.start_spinner();
}
#[test]
fn test_render_time_seconds_only() {
assert_eq!(render_time(Duration::from_secs(5)), "5s");
assert_eq!(render_time(Duration::from_secs(59)), "59s");
}
#[test]
fn test_render_time_minutes_and_seconds() {
assert_eq!(render_time(Duration::from_secs(60)), "1m0s");
assert_eq!(render_time(Duration::from_secs(90)), "1m30s");
assert_eq!(render_time(Duration::from_secs(3599)), "59m59s");
}
#[test]
fn test_render_time_hours() {
assert_eq!(render_time(Duration::from_secs(3600)), "1h0m0s");
assert_eq!(render_time(Duration::from_secs(3661)), "1h1m1s");
assert_eq!(render_time(Duration::from_secs(7200)), "2h0m0s");
}
#[test]
fn test_render_time_days() {
assert_eq!(render_time(Duration::from_secs(86400)), "1d0h0m0s");
assert_eq!(render_time(Duration::from_secs(90061)), "1d1h1m1s");
}
#[test]
fn test_render_time_subsecond_shows_milliseconds() {
assert_eq!(render_time(Duration::from_millis(0)), "0ms");
assert_eq!(render_time(Duration::from_millis(500)), "500ms");
assert_eq!(render_time(Duration::from_millis(800)), "800ms");
assert_eq!(render_time(Duration::from_millis(999)), "999ms");
assert_eq!(render_time(Duration::from_millis(2500)), "2s");
}
#[test]
fn test_duration_placeholder_below_threshold() {
let _guard = crate::test_utils::lock_env();
let mut config =
PromptRuntimeConfig::builder(PromptFormatter::default(), "{duration}r> ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.build();
config.last_command_duration = Some(Duration::from_millis(500));
let prompt = config.build_main_prompt(ReprexMode::Off);
assert_eq!(prompt.render_prompt_left(), "r> ");
}
#[test]
fn test_duration_placeholder_above_threshold() {
let _guard = crate::test_utils::lock_env();
let mut config =
PromptRuntimeConfig::builder(PromptFormatter::default(), "{duration}r> ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.build();
config.last_command_duration = Some(Duration::from_secs(5));
let prompt = config.build_main_prompt(ReprexMode::Off);
let rendered = prompt.render_prompt_left();
assert!(
rendered.contains("5s"),
"Should contain duration time, got: {}",
rendered
);
assert!(
rendered.ends_with("r> "),
"Should end with prompt, got: {}",
rendered
);
}
#[test]
fn test_duration_placeholder_no_data() {
let _guard = crate::test_utils::lock_env();
let config =
PromptRuntimeConfig::builder(PromptFormatter::default(), "{duration}r> ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.build();
let prompt = config.build_main_prompt(ReprexMode::Off);
assert_eq!(prompt.render_prompt_left(), "r> ");
}
#[test]
fn test_clear_command_duration() {
let _guard = crate::test_utils::lock_env();
let mut config =
PromptRuntimeConfig::builder(PromptFormatter::default(), "{duration}r> ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.build();
config.last_command_duration = Some(Duration::from_secs(5));
let prompt = config.build_main_prompt(ReprexMode::Off);
let rendered = prompt.render_prompt_left();
assert!(
rendered.contains("5s"),
"Should contain duration before clearing, got: {}",
rendered
);
config.clear_command_duration();
let prompt = config.build_main_prompt(ReprexMode::Off);
assert_eq!(prompt.render_prompt_left(), "r> ");
}
#[test]
fn test_duration_placeholder_not_present() {
let _guard = crate::test_utils::lock_env();
let mut config =
PromptRuntimeConfig::builder(PromptFormatter::default(), "r> ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.build();
config.last_command_duration = Some(Duration::from_secs(5));
let prompt = config.build_main_prompt(ReprexMode::Off);
assert_eq!(prompt.render_prompt_left(), "r> ");
}
#[test]
fn test_duration_custom_threshold() {
let _guard = crate::test_utils::lock_env();
let duration_config = PromptDurationConfig {
threshold_ms: 500,
..PromptDurationConfig::default()
};
let mut config =
PromptRuntimeConfig::builder(PromptFormatter::default(), "{duration}r> ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.duration(duration_config, Color::Default)
.build();
config.last_command_duration = Some(Duration::from_millis(600));
let prompt = config.build_main_prompt(ReprexMode::Off);
let rendered = prompt.render_prompt_left();
assert!(
rendered.contains("600ms"),
"Should contain duration time in milliseconds, got: {}",
rendered
);
}
#[test]
fn test_duration_custom_format() {
let _guard = crate::test_utils::lock_env();
let duration_config = PromptDurationConfig {
format: "took {value} ".to_string(),
threshold_ms: 2000,
};
let mut config =
PromptRuntimeConfig::builder(PromptFormatter::default(), "{duration}r> ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.duration(duration_config, Color::Default)
.build();
config.last_command_duration = Some(Duration::from_secs(5));
let prompt = config.build_main_prompt(ReprexMode::Off);
let rendered = prompt.render_prompt_left();
assert!(
rendered.contains("took 5s"),
"Should contain formatted duration, got: {}",
rendered
);
assert!(
rendered.ends_with("r> "),
"Should end with prompt, got: {}",
rendered
);
}
#[test]
fn test_duration_format_with_brackets() {
let _guard = crate::test_utils::lock_env();
let duration_config = PromptDurationConfig {
format: "({value}) ".to_string(),
threshold_ms: 2000,
};
let mut config =
PromptRuntimeConfig::builder(PromptFormatter::default(), "{duration}r> ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.duration(duration_config, Color::Default)
.build();
config.last_command_duration = Some(Duration::from_secs(90));
let prompt = config.build_main_prompt(ReprexMode::Off);
let rendered = prompt.render_prompt_left();
assert!(
rendered.contains("(1m30s)"),
"Should contain bracketed duration, got: {}",
rendered
);
}
#[test]
fn test_duration_format_without_value_placeholder() {
let _guard = crate::test_utils::lock_env();
let duration_config = PromptDurationConfig {
format: "slow! ".to_string(),
threshold_ms: 2000,
};
let mut config =
PromptRuntimeConfig::builder(PromptFormatter::default(), "{duration}r> ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.duration(duration_config, Color::Default)
.build();
config.last_command_duration = Some(Duration::from_secs(5));
let prompt = config.build_main_prompt(ReprexMode::Off);
let rendered = prompt.render_prompt_left();
assert!(
rendered.contains("slow!"),
"Should contain static text from format, got: {}",
rendered
);
assert!(
!rendered.contains("5s"),
"Should not contain time value when {{value}} is absent, got: {}",
rendered
);
}
}