strop-editor 0.3.5

strop — a modal text editor in Rust: see the cut before you make it
//! `:help` buffer decoration (0.3.1): the help text is generated by
//! editor/help.rs; here its rows get house-style color — the same
//! typed-decoration pattern the git surfaces use (0010 §5), never a
//! special-purpose renderer fork.

use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span;

use super::{ACCENT, MUTED, TEXT};

/// Key-column hue per section: the eye learns "amber = leader, blue =
/// normal" in one page (color is structure).
fn section_color(section: &str) -> Color {
    match section {
        "normal" => Color::Rgb(0x7a, 0xa2, 0xf7),   // blue
        "visual" => Color::Rgb(0xbb, 0x9a, 0xf7),   // purple
        "insert" => Color::Rgb(0x9e, 0xce, 0x6a),   // green
        "leader" => ACCENT,                         // amber
        "git" => Color::Rgb(0x7d, 0xcf, 0xff),      // cyan
        "ex+panes" => Color::Rgb(0xe0, 0xaf, 0x68), // yellow
        _ => ACCENT,
    }
}

/// One help row → styled spans (`section` is the enclosing `[x]` header
/// the render loop last saw):
/// - the header line wears accent bold
/// - `[section]` headers are section-hued bold, trailed by a dim rule
/// - binding rows: keys in the section's hue (bold), description in text
/// - planned `(soon)` rows are muted end to end
pub(crate) fn row_spans(text: &str, section: &str, width: u16) -> Vec<Span<'static>> {
    if text.starts_with("strop help") {
        return vec![Span::styled(
            text.to_string(),
            Style::default().fg(ACCENT).add_modifier(Modifier::BOLD),
        )];
    }
    if text.starts_with('[') && text.ends_with(']') {
        let used = text.chars().count();
        let pad = (width as usize).saturating_sub(used + 1);
        return vec![
            Span::styled(
                text.to_string(),
                Style::default()
                    .fg(section_color(section))
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled(
                format!(" {}", "".repeat(pad)),
                Style::default().fg(Color::Rgb(0x3a, 0x3d, 0x4d)),
            ),
        ];
    }
    let planned = text.ends_with("(soon)");
    let fg = if planned { MUTED } else { TEXT };
    let key_fg = if planned {
        MUTED
    } else {
        section_color(section)
    };
    // rows are `  <keys padded>  <desc>` — the two-space gap after the
    // key column is the split
    match text
        .strip_prefix("  ")
        .and_then(|t| t.find("  ").map(|i| i + 2))
    {
        Some(split) if split < text.len() => {
            let (keys, desc) = text.split_at(split);
            vec![
                Span::styled(
                    keys.to_string(),
                    Style::default().fg(key_fg).add_modifier(Modifier::BOLD),
                ),
                Span::styled(desc.to_string(), Style::default().fg(fg)),
            ]
        }
        _ => vec![Span::styled(text.to_string(), Style::default().fg(fg))],
    }
}

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

    #[test]
    fn sections_and_keys_get_section_hues() {
        let header = row_spans("[leader]", "leader", 40);
        assert_eq!(header[0].style.fg, Some(ACCENT));
        assert!(header[1].content.starts_with(' '), "rule trails");

        let row = row_spans("  space f  file finder", "leader", 40);
        assert_eq!(row[0].content, "  space f");
        assert_eq!(row[0].style.fg, Some(ACCENT));
        assert_eq!(row[1].content, "  file finder");
        assert_eq!(row[1].style.fg, Some(TEXT));

        let normal = row_spans("  h j k l  move", "normal", 40);
        assert_eq!(normal[0].style.fg, Some(Color::Rgb(0x7a, 0xa2, 0xf7)));

        let soon = row_spans("  space j  jumplist picker  (soon)", "leader", 40);
        assert!(soon.iter().all(|s| s.style.fg == Some(MUTED)));
    }
}