sshfwd 0.4.0

TUI-based SSH port forwarding manager with automatic port discovery
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Clear, Paragraph};
use ratatui::Frame;

use super::hotkey_spans;
use crate::app::{ModalState, Model};
use crate::forward::ForwardKind;

pub fn render(model: &Model, frame: &mut Frame) {
    let ModalState::PortInput {
        kind,
        remote_port,
        local_port,
        buffer,
        error,
        ..
    } = &model.modal
    else {
        return;
    };

    let area = centered_rect(44, 7, frame.area());

    frame.render_widget(Clear, area);

    let (title, label, border_color) = match kind {
        ForwardKind::Local => (
            format!(" Forward port :{} ", remote_port),
            "Local port: ",
            Color::Cyan,
        ),
        ForwardKind::Reverse => (
            format!(" Reverse :{} → remote ", local_port),
            "Remote bind port: ",
            Color::Magenta,
        ),
    };

    let block = Block::bordered()
        .border_type(BorderType::Rounded)
        .border_style(Style::default().fg(border_color))
        .title(title);

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

    let mut lines = Vec::new();
    lines.push(Line::raw(""));

    if let Some(err) = error {
        lines.push(Line::from(vec![
            Span::raw("  "),
            Span::styled(err.as_str(), Style::default().fg(Color::Red)),
        ]));
    } else {
        lines.push(Line::raw(""));
    }

    lines.push(Line::from(vec![
        Span::raw(format!("  {}", label)),
        Span::styled(
            format!("{}\u{2588}", buffer),
            Style::default()
                .fg(Color::White)
                .add_modifier(Modifier::BOLD),
        ),
    ]));

    lines.push(Line::raw(""));

    let mut hint_spans = vec![Span::raw("  ")];
    hint_spans.extend(hotkey_spans("Enter", "Confirm  "));
    hint_spans.extend(hotkey_spans("Esc", "Cancel"));
    lines.push(Line::from(hint_spans));

    let paragraph = Paragraph::new(lines);
    frame.render_widget(paragraph, inner);
}

fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
    let vertical = Layout::vertical([
        Constraint::Fill(1),
        Constraint::Length(height),
        Constraint::Fill(1),
    ])
    .split(area);

    let horizontal = Layout::horizontal([
        Constraint::Fill(1),
        Constraint::Length(width),
        Constraint::Fill(1),
    ])
    .split(vertical[1]);

    horizontal[1]
}