huddle 0.1.0

Decentralized, terminal-native chat rooms — LAN mDNS or direct dial, Megolm-encrypted.
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Wrap};

use crate::app::{DialPeerState, JoinRoomState, StartField, StartRoomState};
use crate::ui::centered_rect;

pub fn render_start_room(f: &mut Frame, s: &StartRoomState) {
    let area = centered_rect(50, 13, f.area());
    f.render_widget(Clear, area);

    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::default().fg(Color::Cyan))
        .title(Span::styled(
            " start a new room ",
            Style::default()
                .fg(Color::Cyan)
                .add_modifier(Modifier::BOLD),
        ))
        .padding(Padding::uniform(1));

    let lines = vec![
        Line::from(""),
        field_line("name", &s.name, matches!(s.focus, StartField::Name), false),
        Line::from(""),
        encrypted_line(s),
        Line::from(""),
        passphrase_line(s),
        Line::from(""),
        Line::from(""),
        Line::from(vec![
            Span::styled(" Tab", Style::default().fg(Color::Yellow)),
            Span::styled(" next  ", Style::default().fg(Color::DarkGray)),
            Span::styled("Enter", Style::default().fg(Color::Yellow)),
            Span::styled(" start  ", Style::default().fg(Color::DarkGray)),
            Span::styled("Esc", Style::default().fg(Color::Yellow)),
            Span::styled(" cancel", Style::default().fg(Color::DarkGray)),
        ]),
    ];

    let para = Paragraph::new(lines).block(block);
    f.render_widget(para, area);
}

fn field_line(label: &str, value: &str, focused: bool, mask: bool) -> Line<'static> {
    let display = if mask {
        "*".repeat(value.chars().count())
    } else {
        value.to_string()
    };
    let cursor = if focused { "_" } else { "" };
    let value_str = format!("[ {}{} ]", display, cursor);
    let value_style = if focused {
        Style::default().fg(Color::Yellow)
    } else {
        Style::default().fg(Color::White)
    };
    Line::from(vec![
        Span::styled(format!(" {:<11}", label), Style::default().fg(Color::DarkGray)),
        Span::styled(value_str, value_style),
    ])
}

fn encrypted_line(s: &StartRoomState) -> Line<'static> {
    let check = if s.encrypted { "[x]" } else { "[ ]" };
    let label = if s.encrypted { "yes" } else { "no" };
    let focused = matches!(s.focus, StartField::Encrypted);
    let style = if focused {
        Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
    } else {
        Style::default().fg(Color::White)
    };
    Line::from(vec![
        Span::styled(" encrypted   ", Style::default().fg(Color::DarkGray)),
        Span::styled(format!("{} {}", check, label), style),
        Span::styled(
            if focused { "   (Enter/Space toggle)" } else { "" },
            Style::default().fg(Color::DarkGray),
        ),
    ])
}

fn passphrase_line(s: &StartRoomState) -> Line<'static> {
    if !s.encrypted {
        return Line::from(Span::styled(
            " passphrase  (encryption disabled)",
            Style::default().fg(Color::DarkGray),
        ));
    }
    field_line(
        "passphrase",
        &s.passphrase,
        matches!(s.focus, StartField::Passphrase),
        true,
    )
}

pub fn render_join_room(f: &mut Frame, j: &JoinRoomState) {
    let area = centered_rect(54, 9, f.area());
    f.render_widget(Clear, area);

    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::default().fg(Color::Magenta))
        .title(Span::styled(
            format!(" join #{} (encrypted) ", j.room_name),
            Style::default()
                .fg(Color::Magenta)
                .add_modifier(Modifier::BOLD),
        ))
        .padding(Padding::uniform(1));

    let masked = "*".repeat(j.passphrase.chars().count());
    let lines = vec![
        Line::from(""),
        Line::from(vec![
            Span::styled(" passphrase  ", Style::default().fg(Color::DarkGray)),
            Span::styled(
                format!("[ {}_ ]", masked),
                Style::default().fg(Color::Yellow),
            ),
        ]),
        Line::from(""),
        Line::from(""),
        Line::from(vec![
            Span::styled(" Enter", Style::default().fg(Color::Yellow)),
            Span::styled(" join  ", Style::default().fg(Color::DarkGray)),
            Span::styled("Esc", Style::default().fg(Color::Yellow)),
            Span::styled(" cancel", Style::default().fg(Color::DarkGray)),
        ]),
    ];

    let para = Paragraph::new(lines).block(block);
    f.render_widget(para, area);
}

pub fn render_quit_confirm(f: &mut Frame) {
    let area = centered_rect(40, 6, f.area());
    f.render_widget(Clear, area);
    let para = Paragraph::new(vec![
        Line::from(""),
        Line::from(Span::styled(
            " quit huddle?",
            Style::default().fg(Color::White),
        )),
        Line::from(""),
        Line::from(vec![
            Span::styled(" [y]", Style::default().fg(Color::Yellow)),
            Span::styled(" yes  ", Style::default().fg(Color::DarkGray)),
            Span::styled("[Esc/n]", Style::default().fg(Color::Yellow)),
            Span::styled(" cancel", Style::default().fg(Color::DarkGray)),
        ]),
    ])
    .block(
        Block::default()
            .borders(Borders::ALL)
            .border_style(Style::default().fg(Color::Red))
            .padding(Padding::uniform(1)),
    );
    f.render_widget(para, area);
}

pub fn render_help(f: &mut Frame) {
    let area = centered_rect(64, 22, f.area());
    f.render_widget(Clear, area);
    let lines = vec![
        Line::from(""),
        Line::from(Span::styled(" Lobby", Style::default().fg(Color::Cyan).bold())),
        Line::from(""),
        kv("  s", "start a new room"),
        kv("  d", "dial a peer by IP:port (Direct mode)"),
        kv("  Tab", "switch focus: known peers <-> rooms"),
        kv("  Enter", "join selected room / reconnect peer"),
        kv("  j/k or arrows", "navigate the focused list"),
        kv("  r", "refresh rooms / retry connect"),
        kv("  x", "forget the highlighted peer"),
        kv("  ?", "this help"),
        kv("  q / Ctrl-C", "quit"),
        Line::from(""),
        Line::from(Span::styled(" In a room", Style::default().fg(Color::Cyan).bold())),
        Line::from(""),
        kv("  /", "type a message"),
        kv("  Esc", "blur input / back to lobby"),
        kv("  ^Tab / ^N", "next tab"),
        kv("  ^P", "previous tab"),
        kv("  1..9", "jump to tab N"),
        kv("  ^L", "leave the current room"),
        kv("  ^B", "back to lobby (without leaving)"),
    ];
    let para = Paragraph::new(lines)
        .wrap(Wrap { trim: false })
        .block(
            Block::default()
                .borders(Borders::ALL)
                .border_style(Style::default().fg(Color::Cyan))
                .padding(Padding::uniform(1))
                .title(Span::styled(
                    " help  (press any key to close) ",
                    Style::default().fg(Color::Cyan).bold(),
                )),
        );
    f.render_widget(para, area);
}

fn kv(k: &'static str, v: &'static str) -> Line<'static> {
    Line::from(vec![
        Span::styled(k, Style::default().fg(Color::Yellow)),
        Span::styled(format!("   {}", v), Style::default().fg(Color::White)),
    ])
}

pub fn render_dial_peer(f: &mut Frame, s: &DialPeerState) {
    let area = centered_rect(64, 11, f.area());
    f.render_widget(Clear, area);

    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::default().fg(Color::Cyan))
        .padding(Padding::uniform(1))
        .title(Span::styled(
            " dial a peer ",
            Style::default()
                .fg(Color::Cyan)
                .add_modifier(Modifier::BOLD),
        ));

    let status = s
        .status
        .clone()
        .unwrap_or_else(|| "ip:port  ·  [ipv6]:port  ·  /ip4/.../tcp/...".into());

    let lines = vec![
        Line::from(""),
        Line::from(vec![
            Span::styled(" address  ", Style::default().fg(Color::DarkGray)),
            Span::styled(
                format!("[ {}_ ]", s.address),
                Style::default().fg(Color::Yellow),
            ),
        ]),
        Line::from(""),
        Line::from(Span::styled(
            format!(" {}", status),
            Style::default().fg(Color::DarkGray),
        )),
        Line::from(""),
        Line::from(""),
        Line::from(vec![
            Span::styled(" Enter", Style::default().fg(Color::Yellow)),
            Span::styled(" dial  ", Style::default().fg(Color::DarkGray)),
            Span::styled("Esc", Style::default().fg(Color::Yellow)),
            Span::styled(" cancel", Style::default().fg(Color::DarkGray)),
        ]),
    ];

    let para = Paragraph::new(lines).wrap(Wrap { trim: false }).block(block);
    f.render_widget(para, area);
}

pub fn render_info(f: &mut Frame, msg: &str) {
    let area = centered_rect(56, 8, f.area());
    f.render_widget(Clear, area);
    let para = Paragraph::new(vec![
        Line::from(""),
        Line::from(Span::styled(msg, Style::default().fg(Color::White))),
        Line::from(""),
        Line::from(""),
        Line::from(Span::styled(
            "  press any key to dismiss",
            Style::default().fg(Color::DarkGray),
        )),
    ])
    .wrap(Wrap { trim: false })
    .block(
        Block::default()
            .borders(Borders::ALL)
            .border_style(Style::default().fg(Color::Cyan))
            .padding(Padding::uniform(1)),
    );
    f.render_widget(para, area);
}

pub fn render_error(f: &mut Frame, msg: &str) {
    let area = centered_rect(56, 8, f.area());
    f.render_widget(Clear, area);
    let para = Paragraph::new(vec![
        Line::from(""),
        Line::from(Span::styled(msg, Style::default().fg(Color::White))),
        Line::from(""),
        Line::from(""),
        Line::from(Span::styled(
            "  press any key to dismiss",
            Style::default().fg(Color::DarkGray),
        )),
    ])
    .wrap(Wrap { trim: false })
    .block(
        Block::default()
            .borders(Borders::ALL)
            .border_style(Style::default().fg(Color::Red))
            .padding(Padding::uniform(1))
            .title(Span::styled(
                " error ",
                Style::default().fg(Color::Red).bold(),
            )),
    );
    f.render_widget(para, area);
}