fmtview 0.4.3

Fast CLI viewer for highlighting, search, and diffs across JSON, JSONL, markup, Markdown, TOML, text, and Jinja
Documentation
use ratatui::{style::Style, text::Span};

use super::palette::plain_style;

pub(crate) fn format_count(value: usize) -> String {
    let raw = value.to_string();
    let mut formatted = String::with_capacity(raw.len() + raw.len() / 3);
    let first_group = raw.len() % 3;
    for (index, ch) in raw.chars().enumerate() {
        if index > 0
            && (index == first_group || (index > first_group && (index - first_group) % 3 == 0))
        {
            formatted.push(',');
        }
        formatted.push(ch);
    }
    formatted
}

pub(crate) fn slice_spans(spans: &[Span<'static>], start: usize, end: usize) -> Vec<Span<'static>> {
    if end <= start {
        return Vec::new();
    }

    let mut sliced = Vec::new();
    let mut cursor = 0;

    for span in spans {
        let text = span.content.as_ref();
        let len = char_count(text);
        let span_start = cursor;
        let span_end = cursor + len;
        cursor = span_end;

        let overlap_start = start.max(span_start);
        let overlap_end = end.min(span_end);
        if overlap_start >= overlap_end {
            continue;
        }

        let text = slice_chars(text, overlap_start - span_start, overlap_end - span_start);
        push_styled_span(&mut sliced, text, span.style);
    }

    sliced
}

pub(crate) fn push_styled_span(spans: &mut Vec<Span<'static>>, text: String, style: Style) {
    let style = if style == plain_style() {
        Style::default()
    } else {
        style
    };

    if text.is_empty() {
        return;
    }

    if let Some(previous) = spans.last_mut()
        && previous.style == style
    {
        previous.content.to_mut().push_str(&text);
        return;
    }

    spans.push(Span::styled(text, style));
}

pub(crate) fn slice_chars(text: &str, start: usize, end: usize) -> String {
    if end <= start {
        return String::new();
    }

    if text.is_ascii() {
        let start = start.min(text.len());
        let end = end.min(text.len());
        if end <= start {
            return String::new();
        }
        return text[start..end].to_owned();
    }

    text.chars().skip(start).take(end - start).collect()
}

pub(crate) fn char_count(text: &str) -> usize {
    if text.is_ascii() {
        text.len()
    } else {
        text.chars().count()
    }
}

pub(crate) fn byte_index_for_char(line: &str, char_index: usize) -> usize {
    if line.is_ascii() {
        return char_index.min(line.len());
    }

    line.char_indices()
        .nth(char_index)
        .map(|(index, _)| index)
        .unwrap_or(line.len())
}