sara-tasks 2.6.0

Sara — folder-aware task manager
use ratatui::{
    style::{Color, Modifier, Style},
    text::{Line, Span},
};

use crate::infrastructure::tui::theme::{Ink, ink};

pub(super) fn nav_line<'a>(text: &str, color: Color, italic: bool, selected: bool) -> Line<'a> {
    let mut style = Style::default().fg(color);
    if italic {
        style = style.add_modifier(Modifier::ITALIC);
    }
    if selected {
        style = style
            .bg(ink(Ink::Select))
            .fg(ink(Ink::Text))
            .add_modifier(Modifier::BOLD);
    }
    let prefix = if selected { " ▶ " } else { "   " };
    Line::from(vec![
        Span::styled(prefix.to_string(), style),
        Span::styled(text.to_string(), style),
    ])
}

pub(super) fn collapsed_text(s: &str, verbose: bool) -> String {
    const COLLAPSED_CHARS: usize = 160;
    if verbose || s.chars().count() <= COLLAPSED_CHARS {
        return s.to_string();
    }
    let t: String = s.chars().take(COLLAPSED_CHARS).collect();
    format!("{t}…  (v to expand)")
}

pub(super) fn label_line(
    glyph: &str,
    title: &str,
    count: Option<usize>,
    width: usize,
    selected: bool,
) -> Line<'static> {
    let mut head = Style::default()
        .fg(ink(Ink::Accent))
        .add_modifier(Modifier::BOLD);
    let mut count_style = Style::default().fg(ink(Ink::Soft));
    if selected {
        head = head.fg(ink(Ink::Text)).bg(ink(Ink::Select));
        count_style = count_style.fg(ink(Ink::Text)).bg(ink(Ink::Select));
    }
    let mut spans = vec![Span::styled(
        format!(" {glyph} {}", title.to_uppercase()),
        head,
    )];
    if let Some(n) = count {
        spans.push(Span::styled(format!("  {n}"), count_style));
    }
    let used: usize = spans.iter().map(|s| s.width()).sum();
    let rule = width.saturating_sub(used + 1);
    if rule > 0 {
        spans.push(Span::raw(" "));
        spans.push(Span::styled(
            "─".repeat(rule),
            Style::default().fg(ink(Ink::Muted)),
        ));
    }
    Line::from(spans)
}
pub(super) use crate::infrastructure::tui::screen::bar;

pub(super) fn feedback_mark(open: usize, revise: bool, bg: Color) -> Vec<Span<'static>> {
    let mut spans = vec![];
    if open > 0 {
        spans.push(Span::styled(
            format!("  ! {open}"),
            Style::default()
                .fg(ink(Ink::Accent))
                .bg(bg)
                .add_modifier(Modifier::BOLD),
        ));
    }
    if revise {
        spans.push(Span::styled(
            " ⟳",
            Style::default().fg(ink(Ink::Warn)).bg(bg),
        ));
    }
    spans
}

pub(super) fn wrap_hanging(
    lines: Vec<Line<'static>>,
    width: usize,
    sel: Option<(usize, usize)>,
) -> (Vec<Line<'static>>, Option<(usize, usize)>) {
    let width = width.max(8);
    let mut out = Vec::with_capacity(lines.len());
    let mut starts = Vec::with_capacity(lines.len());
    let mut ends = Vec::with_capacity(lines.len());
    for line in lines {
        starts.push(out.len());
        out.extend(wrap_line(line, width));
        ends.push(out.len() - 1);
    }
    let sel = sel.and_then(|(a, b)| Some((*starts.get(a)?, *ends.get(b)?)));
    (out, sel)
}

fn hanging_indent(text: &str) -> usize {
    let mut col = 0;
    for ch in text.chars() {
        if ch.is_alphanumeric() || matches!(ch, '"' | '\'' | '(' | '[' | '/' | '.' | '~') {
            return col;
        }
        col += Span::raw(ch.to_string()).width();
    }
    0
}

fn pieces(s: &str) -> Vec<&str> {
    let mut out = vec![];
    let mut start = 0;
    let mut in_space = None;
    for (i, ch) in s.char_indices() {
        let sp = ch == ' ';
        if in_space.is_some_and(|prev| prev != sp) {
            out.push(&s[start..i]);
            start = i;
        }
        in_space = Some(sp);
    }
    if start < s.len() {
        out.push(&s[start..]);
    }
    out
}

fn wrap_line(line: Line<'static>, width: usize) -> Vec<Line<'static>> {
    if line.width() <= width {
        return vec![line];
    }
    let text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
    let indent = hanging_indent(&text).min(width / 2);
    let mut out: Vec<Line<'static>> = vec![];
    let mut cur: Vec<Span<'static>> = vec![];
    let mut used = 0usize;
    let mut fresh = false;
    let flush = |cur: &mut Vec<Span<'static>>, out: &mut Vec<Line<'static>>| {
        while cur
            .last()
            .is_some_and(|s| !s.content.is_empty() && s.content.trim().is_empty())
        {
            cur.pop();
        }
        out.push(Line::from(std::mem::take(cur)).style(line.style));
    };
    for span in &line.spans {
        for piece in pieces(&span.content) {
            let is_space = piece.starts_with(' ');
            if fresh && is_space {
                continue;
            }
            let w = Span::raw(piece).width();
            if used + w > width && used > indent && !fresh {
                flush(&mut cur, &mut out);
                cur.push(Span::raw(" ".repeat(indent)));
                used = indent;
                fresh = true;
                if is_space {
                    continue;
                }
            }
            if used + w <= width {
                cur.push(Span::styled(piece.to_string(), span.style));
                used += w;
                fresh = false;
                continue;
            }
            let mut chunk = String::new();
            for ch in piece.chars() {
                let cw = Span::raw(ch.to_string()).width();
                if used + cw > width {
                    if !chunk.is_empty() {
                        cur.push(Span::styled(std::mem::take(&mut chunk), span.style));
                    }
                    flush(&mut cur, &mut out);
                    cur.push(Span::raw(" ".repeat(indent)));
                    used = indent;
                }
                chunk.push(ch);
                used += cw;
            }
            if !chunk.is_empty() {
                cur.push(Span::styled(chunk, span.style));
            }
            fresh = false;
        }
    }
    if !cur.is_empty() {
        flush(&mut cur, &mut out);
    }
    out
}