kanban-tui 0.7.2

Terminal user interface for the kanban project management tool
Documentation
use crate::app::App;
use crate::components::centered_rect;
use crate::components::ListItemConfig;
use crate::theme::*;
use ratatui::{
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Clear, Paragraph},
    Frame,
};

pub fn help_popup_viewport_height(frame_area: Rect) -> usize {
    let popup = centered_rect(80, 80, frame_area);
    let block = Block::default().borders(Borders::ALL);
    let inner = block.inner(popup);
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .horizontal_margin(2)
        .constraints([
            Constraint::Length(2),
            Constraint::Min(0),
            Constraint::Length(2),
        ])
        .split(inner);
    chunks[1].height as usize
}

pub fn render_help_popup(app: &App, frame: &mut Frame) {
    use crate::keybindings::KeybindingRegistry;

    let area = centered_rect(80, 80, frame.area());
    frame.render_widget(Clear, area);

    let block = Block::default()
        .title("Help - Keybindings for Current Context")
        .borders(Borders::ALL)
        .border_style(focused_border());

    let inner = block.inner(area);
    frame.render_widget(block, area);

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .horizontal_margin(2)
        .constraints([
            Constraint::Length(2),
            Constraint::Min(0),
            Constraint::Length(2),
        ])
        .split(inner);

    let provider = KeybindingRegistry::get_provider(app);
    let context = provider.get_context();

    frame.render_widget(
        Paragraph::new(vec![
            Line::from(Span::styled(
                context.name.clone(),
                Style::default()
                    .fg(Color::Cyan)
                    .add_modifier(Modifier::BOLD),
            )),
            Line::from(""),
        ]),
        chunks[0],
    );

    let raw_height = help_popup_viewport_height(frame.area());

    let selected_idx = app.ui_state.help_list.get_selected_index();

    let adjusted_height = app
        .ui_state
        .help_list
        .get_adjusted_viewport_height(raw_height);
    let page_info = app.ui_state.help_list.get_render_info(adjusted_height);

    let mut rendered_lines: Vec<Line> = crate::scroll_indicators::render_above_indicator(
        page_info.show_above_indicator,
        page_info.items_above,
        "item",
    )
    .into_iter()
    .collect();

    let visible_lines: Vec<Line> = page_info
        .visible_indices
        .iter()
        .filter_map(|&i| {
            let binding = context.bindings.get(i)?;
            let is_selected = selected_idx == Some(i);
            let config = ListItemConfig::new().selected(is_selected).focused(true);
            let prefix = config.item_prefix();
            let style = config.item_style();
            Some(Line::from(vec![
                Span::styled(prefix.to_string(), style),
                Span::styled(binding.key.to_string(), Style::default().fg(Color::Yellow)),
                Span::raw(" "),
                Span::styled(binding.description.clone(), style),
            ]))
        })
        .collect();
    rendered_lines.extend(visible_lines);
    rendered_lines.extend(crate::scroll_indicators::render_below_indicator(
        page_info.show_below_indicator,
        page_info.items_below,
        "item",
    ));

    frame.render_widget(Paragraph::new(rendered_lines), chunks[1]);

    let footer = Paragraph::new(vec![
        Line::from(""),
        Line::from(Span::styled(
            "j/k or ↑↓: navigate | Enter: activate | ESC or ?: close",
            Style::default()
                .fg(Color::DarkGray)
                .add_modifier(Modifier::ITALIC),
        )),
    ]);
    frame.render_widget(footer, chunks[2]);
}