fmtview 0.4.2

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

use crate::{
    formats::json::chat::ChatRole, load::ViewFile, transform::FormatKind,
    tui::palette::gutter_style,
};

use super::footer::gutter_digits;

const WRAP_GUTTER_MINOR_TICK_ROWS: usize = 8;
const WRAP_GUTTER_MAJOR_TICK_ROWS: usize = 64;
const CHAT_ROLE_GUTTER_WIDTH: usize = 12;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::viewer) struct GutterLayout {
    line_digits: usize,
    chat_role: bool,
}

impl GutterLayout {
    pub(in crate::viewer) fn new(line_digits: usize, chat_role: bool) -> Self {
        Self {
            line_digits,
            chat_role: chat_role && line_digits > 0,
        }
    }

    pub(in crate::viewer) fn for_view(
        file: &dyn ViewFile,
        selection_mode: bool,
        mode: FormatKind,
    ) -> Self {
        let line_digits = gutter_digits(file, selection_mode);
        Self::new(
            line_digits,
            matches!(mode, FormatKind::Json | FormatKind::Jsonl),
        )
    }

    pub(in crate::viewer) fn width(self) -> usize {
        self.line_number_width()
            .saturating_add(self.chat_role_width())
    }

    pub(in crate::viewer) fn content_start(self) -> usize {
        self.width()
    }

    pub(in crate::viewer) fn line_number(self, line_number: usize) -> Span<'static> {
        if self.line_digits == 0 {
            return Span::raw("");
        }

        Span::styled(
            format!("{line_number:>width$} │ ", width = self.line_digits),
            gutter_style(),
        )
    }

    pub(in crate::viewer) fn continuation(self, row_index: usize) -> Span<'static> {
        if self.line_digits == 0 {
            return Span::raw("");
        }

        let marker = continuation_gutter_marker(row_index);
        Span::styled(
            format!("{:>width$} {marker} ", "", width = self.line_digits),
            gutter_style(),
        )
    }

    pub(in crate::viewer) fn chat_role(self, role: Option<ChatRole>) -> Span<'static> {
        if !self.chat_role {
            return Span::raw("");
        }

        match role {
            Some(role) => Span::styled(format!("{:<9}", role.label()), role.style()),
            None => Span::styled(format!("{:<9}", ""), gutter_style()),
        }
    }

    pub(in crate::viewer) fn chat_role_enabled(self) -> bool {
        self.chat_role
    }

    fn line_number_width(self) -> usize {
        if self.line_digits == 0 {
            0
        } else {
            self.line_digits + 3
        }
    }

    fn chat_role_width(self) -> usize {
        if self.chat_role {
            CHAT_ROLE_GUTTER_WIDTH
        } else {
            0
        }
    }
}

#[cfg(test)]
pub(in crate::viewer) fn line_number_gutter(
    line_number: usize,
    gutter_digits: usize,
) -> Span<'static> {
    GutterLayout::new(gutter_digits, false).line_number(line_number)
}

#[cfg(test)]
pub(in crate::viewer) fn continuation_gutter(
    row_index: usize,
    gutter_digits: usize,
) -> Span<'static> {
    GutterLayout::new(gutter_digits, false).continuation(row_index)
}

pub(in crate::viewer) fn continuation_gutter_marker(row_index: usize) -> char {
    if row_index > 0 && row_index % WRAP_GUTTER_MAJOR_TICK_ROWS == 0 {
        ''
    } else if row_index > 0 && row_index % WRAP_GUTTER_MINOR_TICK_ROWS == 0 {
        ''
    } else {
        ''
    }
}