tuicr 0.5.0

Review AI-generated diffs like a GitHub pull request, right from your terminal.
use ratatui::{
    Frame,
    layout::{Constraint, Flex, Layout, Rect},
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Clear, Paragraph, Wrap},
};

use crate::app::App;
use crate::model::{CommentType, LineRange};
use crate::theme::Theme;
use crate::ui::styles;

pub fn render_comment_input(frame: &mut Frame, app: &App) {
    let theme = &app.theme;
    let area = centered_rect(60, 40, frame.area());

    frame.render_widget(Clear, area);

    let action = if app.editing_comment_id.is_some() {
        "Edit"
    } else {
        "Add"
    };

    let comment_kind = if app.comment_is_file_level {
        "File Comment".to_string()
    } else if let Some((range, _)) = app.comment_line_range {
        if range.is_single() {
            format!("Line {} Comment", range.start)
        } else {
            format!("Lines {}-{} Comment", range.start, range.end)
        }
    } else {
        match app.comment_line {
            Some((line, _)) => format!("Line {line} Comment"),
            None => "Line Comment".to_string(),
        }
    };

    let newline_hint = if app.supports_keyboard_enhancement {
        "Shift-Enter"
    } else {
        "Ctrl-J"
    };
    let block = Block::default()
        .title(format!(
            " {} {} [{}] (Enter to save, {} for newline) ",
            action,
            comment_kind,
            app.comment_type.as_str(),
            newline_hint
        ))
        .borders(Borders::ALL)
        .border_style(styles::border_style(theme, true));

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

    // Build content with type selector hint and input area
    let type_style = styles::comment_type_style(theme, app.comment_type);
    let type_hint = Line::from(vec![
        Span::styled("Type: ", styles::dim_style(theme)),
        Span::styled(app.comment_type.as_str(), type_style),
        Span::styled(" (Tab to cycle)", styles::dim_style(theme)),
    ]);

    let separator = Line::from(Span::styled(
        "".repeat(inner.width as usize),
        styles::dim_style(theme),
    ));

    // Build content lines with cursor
    let mut lines = vec![type_hint, separator, Line::from("")];

    let cursor_style = Style::default()
        .fg(theme.cursor_color)
        .add_modifier(Modifier::UNDERLINED);

    if app.comment_buffer.is_empty() {
        // Show placeholder with cursor at start
        lines.push(Line::from(vec![
            Span::styled(" ", cursor_style),
            Span::styled("Type your comment...", styles::dim_style(theme)),
        ]));
    } else {
        // Split buffer into lines and render with cursor
        let buffer_lines: Vec<&str> = app.comment_buffer.split('\n').collect();
        let mut char_offset = 0;

        for (line_idx, text) in buffer_lines.iter().enumerate() {
            let line_start = char_offset;
            let line_end = char_offset + text.len();

            // Check if cursor is on this line
            let cursor_on_this_line = app.comment_cursor >= line_start
                && (app.comment_cursor <= line_end
                    || (line_idx == buffer_lines.len() - 1
                        && app.comment_cursor == app.comment_buffer.len()));

            if cursor_on_this_line {
                let cursor_pos_in_line = app.comment_cursor - line_start;
                let cursor_pos_in_line = cursor_pos_in_line.min(text.len());
                let (before_cursor, after_cursor) = text.split_at(cursor_pos_in_line);
                if after_cursor.is_empty() {
                    lines.push(Line::from(vec![
                        Span::raw(before_cursor.to_string()),
                        Span::styled(" ", cursor_style),
                    ]));
                } else {
                    let mut chars = after_cursor.chars();
                    let cursor_char = chars.next().unwrap();
                    let remaining = chars.as_str();
                    lines.push(Line::from(vec![
                        Span::raw(before_cursor.to_string()),
                        Span::styled(cursor_char.to_string(), cursor_style),
                        Span::raw(remaining.to_string()),
                    ]));
                }
            } else {
                lines.push(Line::from(Span::raw(text.to_string())));
            }

            // Account for newline character (except for last line)
            char_offset = line_end + 1;
        }
    }

    let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });

    frame.render_widget(paragraph, inner);
}

/// Format a comment as multiple lines with a box border (themed version)
pub fn format_comment_lines(
    theme: &Theme,
    comment_type: CommentType,
    content: &str,
    line_range: Option<LineRange>,
) -> Vec<Line<'static>> {
    let type_style = styles::comment_type_style(theme, comment_type);
    let border_style = styles::comment_border_style(theme, comment_type);

    let line_info = match line_range {
        Some(range) if range.is_single() => format!("L{} ", range.start),
        Some(range) => format!("L{}-L{} ", range.start, range.end),
        None => String::new(),
    };
    let content_lines: Vec<&str> = content.split('\n').collect();

    let mut result = Vec::new();

    // Top border with type label
    result.push(Line::from(vec![
        Span::styled("     ╭─ ", border_style),
        Span::styled(format!("[{}] ", comment_type.as_str()), type_style),
        Span::styled(line_info, styles::dim_style(theme)),
        Span::styled("".repeat(30), border_style),
    ]));

    // Content lines
    for line in &content_lines {
        result.push(Line::from(vec![
            Span::styled("", border_style),
            Span::raw(line.to_string()),
        ]));
    }

    // Bottom border
    result.push(Line::from(vec![Span::styled(
        "".to_string() + &"".repeat(38),
        border_style,
    )]));

    result
}

pub fn render_confirm_dialog(frame: &mut Frame, app: &App, message: &str) {
    let theme = &app.theme;
    let area = centered_rect(50, 20, frame.area());

    frame.render_widget(Clear, area);

    let block = Block::default()
        .title(" Confirm ")
        .borders(Borders::ALL)
        .border_style(styles::border_style(theme, true));

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

    let lines = vec![
        Line::from(""),
        Line::from(Span::raw(message)),
        Line::from(""),
        Line::from(vec![
            Span::styled("  [Y]", Style::default().add_modifier(Modifier::BOLD)),
            Span::raw("es    "),
            Span::styled("[N]", Style::default().add_modifier(Modifier::BOLD)),
            Span::raw("o"),
        ]),
    ];

    let paragraph = Paragraph::new(lines).alignment(ratatui::layout::Alignment::Center);
    frame.render_widget(paragraph, inner);
}

fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
    let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
    let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
    let [area] = vertical.areas(area);
    let [area] = horizontal.areas(area);
    area
}