gosuto 0.11.1

Gōsuto (ゴースト) — a cyberpunk terminal Matrix client with vim motions
use ratatui::{
    Frame,
    layout::Rect,
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Paragraph, Wrap},
};

use crate::app::App;
use crate::input::{FocusPanel, VimMode};
use crate::ui::{panel, theme};

pub fn render(app: &App, frame: &mut Frame, area: Rect) {
    let (content, style) = match app.vim.mode {
        VimMode::Command => {
            let cmd = format!(":{}", app.vim.command_buffer);
            (cmd, theme::MAGENTA)
        }
        VimMode::Insert => {
            let text = if app.vim.input_buffer.is_empty() {
                "type message here...".to_string()
            } else {
                app.vim.input_buffer.clone()
            };
            (text, theme::CYAN)
        }
        VimMode::Normal => {
            if app.vim.searching {
                let search = format!("/{}", app.vim.search_query);
                (search, theme::CYAN)
            } else if app.vim.focus == FocusPanel::Messages {
                (
                    "j/k: navigate, r: reply, e: edit, a: react, d: delete".to_string(),
                    theme::DIM,
                )
            } else if app.vim.focus == FocusPanel::Members {
                ("Enter: dm, c: call, v: verify".to_string(), theme::DIM)
            } else {
                ("press i to type, : for commands".to_string(), theme::DIM)
            }
        }
    };

    let is_placeholder = app.vim.mode == VimMode::Normal && !app.vim.searching
        || (app.vim.mode == VimMode::Insert && app.vim.input_buffer.is_empty());

    let text_style = if is_placeholder {
        theme::dim_style()
    } else {
        Style::default().fg(style)
    };

    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(theme::border_style())
        .style(Style::default().bg(theme::CHAT_BG));

    let prefix = match app.vim.mode {
        VimMode::Insert => "> ",
        VimMode::Command => "",
        VimMode::Normal if app.vim.searching => "",
        VimMode::Normal => "> ",
    };

    let text_lines: Vec<Line> =
        if app.vim.mode == VimMode::Insert && !app.vim.input_buffer.is_empty() {
            content
                .split('\n')
                .enumerate()
                .map(|(i, line_str)| {
                    let line_prefix = if i == 0 { prefix } else { "  " };
                    Line::from(vec![
                        Span::styled(line_prefix, Style::default().fg(style)),
                        Span::styled(line_str.to_string(), text_style),
                    ])
                })
                .collect()
        } else {
            vec![Line::from(vec![
                Span::styled(prefix, Style::default().fg(style)),
                Span::styled(content, text_style),
            ])]
        };

    let mut all_lines = Vec::new();
    if let Some(ref ctx) = app.reply_context {
        all_lines.push(Line::from(vec![
            Span::styled(" Replying to ", theme::reply_indicator_style()),
            Span::styled(
                &ctx.sender,
                Style::default()
                    .fg(theme::sender_color(&ctx.sender))
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled(format!("  {}", ctx.body_preview), theme::dim_style()),
        ]));
    } else if let Some(ref ctx) = app.edit_context {
        let preview = crate::app::truncate_preview(&ctx.original_body, 50);
        all_lines.push(Line::from(vec![
            Span::styled(" Editing ", theme::edit_indicator_style()),
            Span::styled(format!(" {}", preview), theme::dim_style()),
        ]));
    }
    all_lines.extend(text_lines);

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

    frame.render_widget(paragraph, area);

    // Gradient border in Insert/Command mode
    let phase = app.anim_clock.phase;
    match app.vim.mode {
        VimMode::Insert => {
            panel::apply_gradient_border_with_bg(
                frame.buffer_mut(),
                area,
                theme::CYAN,
                theme::INPUT_BORDER_CYAN_DIM,
                phase,
                theme::CHAT_BG,
            );
        }
        VimMode::Command => {
            panel::apply_gradient_border_with_bg(
                frame.buffer_mut(),
                area,
                theme::MAGENTA,
                theme::INPUT_BORDER_MAGENTA_DIM,
                phase,
                theme::CHAT_BG,
            );
        }
        VimMode::Normal if app.vim.searching => {
            panel::apply_gradient_border_with_bg(
                frame.buffer_mut(),
                area,
                theme::CYAN,
                theme::GRADIENT_BORDER_END,
                phase,
                theme::CHAT_BG,
            );
        }
        VimMode::Normal => {}
    }

    // Show cursor in insert/command mode
    if app.vim.mode == VimMode::Insert || app.vim.mode == VimMode::Command || app.vim.searching {
        let reply_offset: u16 = if app.reply_context.is_some() || app.edit_context.is_some() {
            1
        } else {
            0
        };
        let (cursor_x, cursor_y) = match app.vim.mode {
            VimMode::Insert => {
                let text_width = area.width.saturating_sub(4); // 2 borders + 2 prefix
                let (_total, vis_row, vis_col) = app.vim.visual_cursor_info(text_width);
                let x = area.x + 1 + 2 + vis_col; // +2 for prefix "> "
                let y = area.y + 1 + reply_offset + vis_row;
                (x, y)
            }
            VimMode::Command => {
                let x = area.x + 1 + prefix.len() as u16 + app.vim.command_buffer.len() as u16 + 1;
                let y = area.y + 1;
                (x, y)
            }
            VimMode::Normal => {
                let x = area.x + 1 + prefix.len() as u16 + app.vim.search_query.len() as u16 + 1;
                let y = area.y + 1;
                (x, y)
            }
        };
        if app.anim_clock.cursor_visible() {
            frame.set_cursor_position((cursor_x, cursor_y));
        }
    }
}