osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
use std::time::Duration;

use crate::ui::ResolvedRenderSettings;
use crate::ui::style::{StyleToken, apply_style_with_theme_overrides};

#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct TimingSummary {
    pub(crate) total: Duration,
    pub(crate) parse: Option<Duration>,
    pub(crate) execute: Option<Duration>,
    pub(crate) render: Option<Duration>,
}

pub(crate) fn format_timing_badge(
    summary: TimingSummary,
    debug_level: u8,
    resolved: &ResolvedRenderSettings,
) -> String {
    if debug_level == 0 {
        return String::new();
    }

    let mut text = format_duration(summary.total, debug_level);
    if debug_level >= 3 {
        let mut parts = Vec::new();
        if let Some(parse) = summary.parse {
            parts.push(format!("p{}", format_duration(parse, 2)));
        }
        if let Some(execute) = summary.execute {
            parts.push(format!("e{}", format_duration(execute, 2)));
        }
        if let Some(render) = summary.render {
            parts.push(format!("r{}", format_duration(render, 2)));
        }
        if !parts.is_empty() {
            text.push(' ');
            text.push_str(&parts.join(" "));
        }
    }

    apply_style_with_theme_overrides(
        &text,
        timing_style(summary.total),
        resolved.color,
        &resolved.theme,
        &resolved.style_overrides,
    )
}

pub(crate) fn right_align_timing_line(
    summary: TimingSummary,
    debug_level: u8,
    resolved: &ResolvedRenderSettings,
) -> String {
    let badge = format_timing_badge(summary, debug_level, resolved);
    if badge.is_empty() {
        return String::new();
    }

    let width = resolved.width.unwrap_or(80);
    let visible = visible_width(&badge);
    let padding = width.saturating_sub(visible);
    format!("{}{}\n", " ".repeat(padding), badge)
}

fn timing_style(total: Duration) -> StyleToken {
    let total_ms = total.as_millis();
    if total_ms <= 250 {
        StyleToken::MessageSuccess
    } else if total_ms <= 1_000 {
        StyleToken::MessageWarning
    } else {
        StyleToken::MessageError
    }
}

fn format_duration(duration: Duration, debug_level: u8) -> String {
    let secs = duration.as_secs_f64();
    if debug_level <= 1 {
        if duration.as_millis() == 0 && !duration.is_zero() {
            return "<1ms".to_string();
        }
        if secs >= 1.0 {
            return format!("{:.2}s", secs);
        }
        return format!("{}ms", duration.as_millis());
    }

    if secs >= 1.0 {
        return format!("{secs:.2}s");
    }
    let ms = duration.as_secs_f64() * 1_000.0;
    format!("{ms:.1}ms")
}

fn visible_width(text: &str) -> usize {
    let mut width = 0usize;
    let mut chars = text.chars().peekable();

    while let Some(ch) = chars.next() {
        if ch == '\x1b' && matches!(chars.peek(), Some('[')) {
            chars.next();
            for next in chars.by_ref() {
                if ('@'..='~').contains(&next) {
                    break;
                }
            }
            continue;
        }
        width += 1;
    }

    width
}

#[cfg(test)]
mod tests {
    use std::hint::black_box;

    use super::{
        TimingSummary, format_duration, format_timing_badge, right_align_timing_line, timing_style,
        visible_width,
    };
    use crate::ui::RenderSettings;
    use crate::ui::style::StyleToken;
    use std::time::Duration;

    #[test]
    fn level_three_timing_includes_phase_breakdown() {
        let resolved = RenderSettings::test_plain(crate::core::output::OutputFormat::Table)
            .resolve_render_settings();
        let text = format_timing_badge(
            TimingSummary {
                total: Duration::from_millis(187),
                parse: Some(Duration::from_millis(2)),
                execute: Some(Duration::from_millis(180)),
                render: Some(Duration::from_millis(5)),
            },
            3,
            &resolved,
        );

        assert!(text.contains("187.0ms"));
        assert!(text.contains("p2.0ms"));
        assert!(text.contains("e180.0ms"));
        assert!(text.contains("r5.0ms"));
    }

    #[test]
    fn debug_level_zero_and_empty_alignment_yield_no_output() {
        let resolved = RenderSettings::test_plain(crate::core::output::OutputFormat::Table)
            .resolve_render_settings();

        assert!(black_box(format_timing_badge(TimingSummary::default(), 0, &resolved)).is_empty());
        assert!(
            black_box(right_align_timing_line(
                TimingSummary::default(),
                0,
                &resolved
            ))
            .is_empty()
        );
    }

    #[test]
    fn duration_and_style_helpers_cover_threshold_edges() {
        assert_eq!(
            black_box(format_duration(Duration::from_nanos(1), 1)),
            "<1ms"
        );
        assert_eq!(
            black_box(format_duration(Duration::from_millis(9), 1)),
            "9ms"
        );
        assert_eq!(
            black_box(format_duration(Duration::from_millis(9), 2)),
            "9.0ms"
        );
        assert_eq!(
            black_box(format_duration(Duration::from_millis(1_250), 1)),
            "1.25s"
        );
        assert_eq!(
            black_box(format_duration(Duration::from_millis(1_250), 2)),
            "1.25s"
        );

        assert_eq!(
            black_box(timing_style(Duration::from_millis(250))),
            StyleToken::MessageSuccess
        );
        assert_eq!(
            black_box(timing_style(Duration::from_millis(251))),
            StyleToken::MessageWarning
        );
        assert_eq!(
            black_box(timing_style(Duration::from_millis(1_001))),
            StyleToken::MessageError
        );
    }

    #[test]
    fn visible_width_and_right_alignment_ignore_ansi_sequences() {
        assert_eq!(black_box(visible_width("\u{1b}[31mwarn\u{1b}[0m")), 4);

        let mut resolved = RenderSettings::test_plain(crate::core::output::OutputFormat::Table)
            .resolve_render_settings();
        resolved.width = Some(12);
        let line = right_align_timing_line(
            TimingSummary {
                total: Duration::from_millis(9),
                ..TimingSummary::default()
            },
            1,
            &resolved,
        );

        assert_eq!(line, "         9ms\n");
    }
}