aven 0.1.6

Local-first task manager CLI and sync server
Documentation
use ratatui::Frame;
use ratatui::style::Style;
use ratatui::text::{Line, Text};
use ratatui::widgets::Paragraph;

use super::super::dialog::{Dialog, dialog_hint_line};
use super::super::timestamps::optional_local_timestamp_display;
use crate::tui::config_overlay::CONFIG_STATUS_TITLE;
use crate::tui::store::{SyncStatusCheck, TuiSyncStatus};
use crate::tui::theme::{BG_ALT, FG, FG_MUTED, GREEN, RED};

pub(in crate::tui::ui) fn render_sync_status(frame: &mut Frame, status: &TuiSyncStatus) {
    let width = frame.area().width.saturating_sub(8).clamp(64, 88);
    let lines = sync_status_lines(status);
    let height = (lines.len() as u16)
        .saturating_add(2)
        .min(frame.area().height.saturating_sub(2))
        .max(3);
    let content = Dialog::new(CONFIG_STATUS_TITLE, width, height).render_block(frame);

    frame.render_widget(
        Paragraph::new(Text::from(lines)).style(Style::new().fg(FG).bg(BG_ALT)),
        content,
    );
}

fn sync_status_lines(status: &TuiSyncStatus) -> Vec<Line<'static>> {
    let mut lines = vec![
        section_line("connection"),
        status_row(
            "server reach",
            reachability(status),
            if status.last_error_value().is_some() {
                Style::new().fg(RED)
            } else if status.last_success.is_some() {
                Style::new().fg(GREEN)
            } else {
                Style::new().fg(FG_MUTED)
            },
        ),
        check_row(
            "configured server",
            status.configured_server.as_ref(),
            "not configured",
        ),
        value_row(
            "pinned server",
            status.pinned_server.as_deref().unwrap_or("none"),
            Style::new().fg(FG_MUTED),
        ),
    ];
    if let Some(server_match) = &status.server_match {
        lines.push(status_row(
            "server match",
            if server_match.ok {
                "yes".to_string()
            } else {
                server_match.value.clone()
            },
            check_style(server_match),
        ));
    }
    lines.extend([
        check_row(
            "daemon server",
            status.daemon_server.as_ref(),
            "not configured",
        ),
        status_row(
            "auth token",
            if status.auth_token_configured {
                "configured".to_string()
            } else {
                "not configured".to_string()
            },
            if status.auth_token_configured {
                Style::new().fg(GREEN)
            } else {
                Style::new().fg(FG_MUTED)
            },
        ),
        value_row(
            "interval",
            format!("{} seconds", status.interval_seconds),
            Style::new().fg(FG_MUTED),
        ),
        check_row("daemon wake", Some(&status.daemon_wake), "not checked"),
        Line::from(""),
        section_line("state"),
        count_row("pending changes", status.pending_changes),
        count_row("conflicts", status.conflicts),
        value_row(
            "sync cursor",
            status.sync_cursor.as_deref().unwrap_or("missing"),
            Style::new().fg(FG_MUTED),
        ),
        value_row(
            "local sequence",
            status.local_sequence.as_deref().unwrap_or("missing"),
            Style::new().fg(FG_MUTED),
        ),
        Line::from(""),
        section_line("last sync"),
        value_row(
            "last attempt",
            optional_local_timestamp_display(status.last_attempt.as_deref(), "never"),
            Style::new().fg(FG_MUTED),
        ),
        value_row(
            "last synced",
            optional_local_timestamp_display(status.last_success.as_deref(), "never"),
            if status.last_success.is_some() {
                Style::new().fg(GREEN)
            } else {
                Style::new().fg(FG_MUTED)
            },
        ),
        value_row(
            "last error",
            status.last_error_value().unwrap_or("none"),
            if status.last_error_value().is_some() {
                Style::new().fg(RED)
            } else {
                Style::new().fg(FG_MUTED)
            },
        ),
        value_row(
            "last pushed",
            status.last_pushed.as_deref().unwrap_or("unknown"),
            Style::new().fg(FG_MUTED),
        ),
        value_row(
            "last pulled",
            status.last_pulled.as_deref().unwrap_or("unknown"),
            Style::new().fg(FG_MUTED),
        ),
        value_row(
            "last cursor",
            status.last_cursor.as_deref().unwrap_or("unknown"),
            Style::new().fg(FG_MUTED),
        ),
        Line::from(""),
        dialog_hint_line(&[("Enter/Esc", "close")]),
    ]);
    if let Some(error) = &status.config_error {
        lines.insert(1, status_row("config", error.clone(), Style::new().fg(RED)));
    }
    lines
}

const LABEL_WIDTH: usize = 18;

fn section_line(label: &str) -> Line<'static> {
    super::shared::section_line(label)
}

fn count_row(label: &str, value: i64) -> Line<'static> {
    super::shared::count_row(LABEL_WIDTH, label, value)
}

fn value_row(label: &str, value: impl Into<String>, value_style: Style) -> Line<'static> {
    super::shared::value_row(LABEL_WIDTH, label, value, value_style)
}

fn check_row(label: &str, check: Option<&SyncStatusCheck>, fallback: &str) -> Line<'static> {
    match check {
        Some(check) => status_row(label, check.value.clone(), check_style(check)),
        None => value_row(label, fallback, Style::new().fg(FG_MUTED)),
    }
}

fn status_row(label: &str, value: String, value_style: Style) -> Line<'static> {
    value_row(label, value, value_style)
}

fn check_style(check: &SyncStatusCheck) -> Style {
    if check.ok {
        Style::new().fg(GREEN)
    } else {
        Style::new().fg(RED)
    }
}

fn reachability(status: &TuiSyncStatus) -> String {
    if !status.enabled {
        return "sync disabled".to_string();
    }
    if status.last_error_value().is_some() {
        return "last attempt failed".to_string();
    }
    if status.last_success.is_some() {
        return "last sync reached server".to_string();
    }
    "unknown, no sync recorded".to_string()
}

#[cfg(test)]
pub(in crate::tui::ui) fn sync_status_lines_for_test(status: &TuiSyncStatus) -> Vec<Line<'static>> {
    sync_status_lines(status)
}