azuredevopstui 0.1.1

Terminal UI for your Azure DevOps pull requests, work items and sprint board
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::Style;
use ratatui::text::{Line, Span};

use crate::app::App;

pub fn render(frame: &mut Frame, app: &App, area: Rect) {
    let theme = &app.theme;
    let mut spans: Vec<Span> = app
        .actions()
        .iter()
        .flat_map(|action| {
            [
                Span::styled(
                    format!(" [{}] ", action.key_label()),
                    Style::new().fg(theme.toolbar_key),
                ),
                Span::raw(action.label()),
            ]
        })
        .collect();
    if let Some(error) = &app.error {
        spans.push(Span::styled(
            format!("  {error}"),
            Style::new().fg(theme.error),
        ));
    }
    let updated = app
        .last_updated
        .map(|at| format!("updated {} ยท ", ago(at.elapsed().as_secs())))
        .unwrap_or_default();
    let user = app
        .user
        .as_ref()
        .map(|user| format!("{updated}{} ", user.provider_display_name))
        .unwrap_or_default();
    let [actions_area, user_area] = Layout::horizontal([
        Constraint::Min(0),
        Constraint::Length(user.chars().count() as u16),
    ])
    .areas(area);

    let style = Style::new().bg(theme.toolbar_bg);
    frame.render_widget(Line::from(spans).style(style), actions_area);
    frame.render_widget(Line::from(user).style(style.fg(theme.muted)), user_area);
}

fn ago(secs: u64) -> String {
    match secs {
        0..60 => format!("{secs}s ago"),
        60..3600 => format!("{}m ago", secs / 60),
        _ => format!("{}h ago", secs / 3600),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ago_uses_largest_whole_unit() {
        assert_eq!(ago(0), "0s ago");
        assert_eq!(ago(59), "59s ago");
        assert_eq!(ago(60), "1m ago");
        assert_eq!(ago(3599), "59m ago");
        assert_eq!(ago(7200), "2h ago");
    }
}