rho-coding-agent 0.28.1

A lightweight agent harness inspired by Pi
Documentation
use std::time::{Duration, Instant};

use ratatui::text::{Line, Span};

use super::{render::display_width, theme::Theme};

const SPINNER_LABEL: &str = "⠋ working";

pub(super) fn spinner_width(available: usize) -> usize {
    if available >= display_width(SPINNER_LABEL) {
        display_width(SPINNER_LABEL)
    } else {
        available.min(display_width(""))
    }
}

#[derive(Clone, Debug, Default)]
pub(super) struct LoadingSpinner {
    started_at: Option<Instant>,
}

impl LoadingSpinner {
    const FRAMES: [&'static str; 10] = ["", "", "", "", "", "", "", "", "", ""];
    pub(super) const FRAME_INTERVAL: Duration = Duration::from_millis(80);

    pub(super) fn start(&mut self) {
        self.started_at = Some(Instant::now());
    }

    pub(super) fn start_if_needed(&mut self) {
        if self.started_at.is_none() {
            self.start();
        }
    }

    pub(super) fn stop(&mut self) {
        self.started_at = None;
    }

    fn frame_at(&self, now: Instant) -> &'static str {
        let Some(started_at) = self.started_at else {
            return Self::FRAMES[0];
        };
        let interval_ms = Self::FRAME_INTERVAL.as_millis().max(1);
        let frame = now
            .saturating_duration_since(started_at)
            .as_millis()
            .checked_div(interval_ms)
            .unwrap_or(0) as usize;
        Self::FRAMES[frame % Self::FRAMES.len()]
    }

    pub(super) fn line(&self, now: Instant, available: usize) -> Line<'static> {
        if available >= display_width(SPINNER_LABEL) {
            Line::from(vec![
                Span::styled(self.frame_at(now), Theme::accent()),
                Span::styled(" working", Theme::dim()),
            ])
        } else if available > 0 {
            Line::from(Span::styled(self.frame_at(now), Theme::accent()))
        } else {
            Line::default()
        }
    }
}

pub(super) fn jump_to_bottom_text(width: usize, binding: &str, alongside_spinner: bool) -> String {
    let full = format!("↓ jump to bottom  {binding}");
    let compact = format!("↓ bottom {binding}");
    let shortcut = format!("{binding}");
    let spinner_width = usize::from(alongside_spinner) * (display_width(SPINNER_LABEL) + 1);
    let available = width.saturating_sub(spinner_width);

    if display_width(&full) <= available {
        full
    } else if display_width(&compact) <= available {
        compact
    } else if display_width(&shortcut) <= width {
        shortcut
    } else {
        super::render::truncate_one_line(&shortcut, width)
    }
}

#[cfg(test)]
#[path = "activity_tests.rs"]
mod tests;