sac-cli 0.1.0

Terminal-based AI coding agent — fork of NAC with extended backend support and context management
Documentation
use super::*;

pub(super) fn classify_tool_status(is_error: bool, preview: &str) -> ToolStatus {
    if is_error {
        return ToolStatus::Error;
    }
    if preview.starts_with("Command timed out after") {
        return ToolStatus::TimedOut;
    }
    if preview.starts_with("Exit code:") {
        return ToolStatus::Failed;
    }
    ToolStatus::Ok
}

pub(super) fn panel_is_selectable(panel: PanelId) -> bool {
    matches!(
        panel,
        PanelId::Prompt
            | PanelId::Response
            | PanelId::PreviousResponse
            | PanelId::Workspace
            | PanelId::FileChanges
            | PanelId::CompactStream
            | PanelId::ThreadEpisodes
            | PanelId::Events
            | PanelId::Threads
            | PanelId::ThreadList
            | PanelId::Tools
            | PanelId::Terminals
            | PanelId::Worksets
            | PanelId::Stream
    )
}

pub(super) fn line_to_plain_text(line: &Line<'_>) -> String {
    line.spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<Vec<_>>()
        .join("")
}

pub(super) fn status_span(label: &str, tone: Tone) -> Span<'static> {
    Span::styled(
        label.to_string(),
        Style::default()
            .fg(tone.color())
            .add_modifier(Modifier::BOLD),
    )
}

pub(super) fn tool_label(thread_name: Option<&str>, tool_name: &str) -> String {
    match thread_name {
        Some(thread_name) => format!("{thread_name}/{tool_name}"),
        None => tool_name.to_string(),
    }
}

pub(super) fn format_duration(duration: Duration) -> String {
    if duration.as_secs() >= 60 {
        let minutes = duration.as_secs() / 60;
        let seconds = duration.as_secs() % 60;
        format!("{minutes}m{seconds:02}s")
    } else if duration.as_secs() > 0 {
        format!("{:.1}s", duration.as_secs_f64())
    } else {
        format!("{}ms", duration.as_millis())
    }
}

pub(super) fn format_runtime(duration: Duration) -> String {
    let total_seconds = duration.as_secs();
    let hours = total_seconds / 3_600;
    let minutes = (total_seconds % 3_600) / 60;
    let seconds = total_seconds % 60;
    format!("T+{hours:02}:{minutes:02}:{seconds:02}")
}

pub(super) fn format_optional_runtime(duration: Option<Duration>) -> String {
    duration
        .map(format_runtime)
        .unwrap_or_else(|| "T+--:--:--".to_string())
}

pub(super) fn duration_to_millis_u64(duration: Duration) -> u64 {
    duration.as_millis().min(u128::from(u64::MAX)) as u64
}

pub(super) fn compact_path(path: &str, max_width: usize) -> String {
    if path.chars().count() <= max_width {
        return path.to_string();
    }
    if max_width <= 1 {
        return "".to_string();
    }
    let suffix_len = max_width.saturating_sub(1);
    let suffix: String = path
        .chars()
        .rev()
        .take(suffix_len)
        .collect::<Vec<_>>()
        .into_iter()
        .rev()
        .collect();
    format!("{suffix}")
}

pub(super) fn fit_text(text: &str, max_width: usize) -> String {
    if text.chars().count() <= max_width {
        return text.to_string();
    }
    if max_width == 0 {
        return String::new();
    }
    if max_width == 1 {
        return "".to_string();
    }
    let mut out = take_chars(text, max_width - 1);
    out.push('');
    out
}

pub(super) fn one_line(text: &str) -> String {
    text.split_whitespace().collect::<Vec<_>>().join(" ")
}

pub(super) fn pad_cell(text: &str, width: usize) -> String {
    format!("{:<width$}", fit_text(text, width), width = width)
}

pub(super) fn pad_to(text: &str, width: usize) -> String {
    if width == 0 {
        return String::new();
    }
    format!("{text:<width$}")
}

pub(super) fn inner_width(area: Rect) -> usize {
    area.width.saturating_sub(2) as usize
}

pub(super) fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
    let overlay_width = width.min(area.width);
    let overlay_height = height.min(area.height);
    let x = area.x + area.width.saturating_sub(overlay_width) / 2;
    let y = area.y + area.height.saturating_sub(overlay_height) / 2;
    Rect::new(x, y, overlay_width, overlay_height)
}

pub(super) fn compact_hint_cell(binding: PaneFocusBinding, width: usize) -> String {
    let text = format!("{} {}", binding.short_binding(), binding.label);
    fit_text(&text, width)
}