rootle 0.4.1

Modal TUI for browsing remote source-control systems (GitHub in-tree, any forge via stdio providers) from the terminal
Documentation
//! Bottom modeline: mode chip · context · key hints (PLAN.md §2, §5).

use crate::keymap;
use crate::mode::Mode;
use crate::theme::{Semantic, Theme};
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;

/// Chip color per mode — modeline, keybinds popup, anywhere a mode
/// chip is drawn.
pub(crate) fn mode_color(mode: Mode, sem: &Semantic) -> Color {
    match mode {
        Mode::Browse => sem.mode_browse,
        Mode::Search | Mode::Find => sem.mode_search,
        Mode::Insert => sem.mode_insert,
        Mode::Normal => sem.mode_normal,
        Mode::Leader => sem.mode_leader,
        Mode::Visual => sem.mode_visual,
    }
}

pub struct Modeline {
    pub context: String,
    /// Transient one-line status ("searching GitHub…", errors) shown
    /// right after the mode chip, in warning color.
    pub status: Option<String>,
}

impl Default for Modeline {
    fn default() -> Self {
        Self::new()
    }
}

impl Modeline {
    pub fn new() -> Modeline {
        Modeline {
            context: String::new(),
            status: None,
        }
    }

    pub fn render(&self, frame: &mut Frame, area: Rect, mode: Mode, theme: &Theme) {
        let sem = &theme.semantic;
        let chip_bg = mode_color(mode, sem);

        let mut spans = vec![
            Span::styled(
                format!(" {} ", mode.chip()),
                Style::default()
                    .fg(sem.crust)
                    .bg(chip_bg)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled(" ", Style::default().bg(sem.mantle)),
        ];
        if let Some(status) = &self.status {
            spans.push(Span::styled(
                format!(" {status} "),
                Style::default().fg(sem.warning).bg(sem.mantle),
            ));
        }
        spans.push(Span::styled(
            format!(" {} ", self.context),
            Style::default().fg(sem.subtext0).bg(sem.mantle),
        ));

        let hint_text: Vec<Span> = keymap::hints(mode)
            .iter()
            .flat_map(|(k, desc)| {
                [
                    Span::styled(
                        format!(" {k}"),
                        Style::default()
                            .fg(sem.text)
                            .bg(sem.mantle)
                            .add_modifier(Modifier::BOLD),
                    ),
                    Span::styled(
                        format!(" {desc} ·"),
                        Style::default().fg(sem.hint).bg(sem.mantle),
                    ),
                ]
                .into_iter()
            })
            .collect();

        let used: usize = spans.iter().map(|s| s.content.width()).sum::<usize>()
            + hint_text.iter().map(|s| s.content.width()).sum::<usize>();
        let pad = (area.width as usize).saturating_sub(used);
        spans.push(Span::styled(
            " ".repeat(pad),
            Style::default().bg(sem.mantle),
        ));
        spans.extend(hint_text);

        frame.render_widget(Paragraph::new(Line::from(spans)), area);
    }
}

use unicode_width::UnicodeWidthStr;