purple-ssh 3.26.1

Free, open-source terminal SSH manager and SSH config editor in Rust for macOS and Linux that keeps ~/.ssh/config in sync with 16 cloud providers, monitors live SSH tunnels and manages Docker and Podman containers fleet-wide. Spin up a VM, it shows up. Destroy it, it dims. Plus scp, Vault SSH certs and an MCP server for AI agents. MIT licensed.
Documentation
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Clear, Paragraph};
use unicode_width::UnicodeWidthStr;

use super::design;
use super::theme;
use crate::app::{App, Screen, SnippetFormField};

pub fn render(frame: &mut Frame, app: &mut App) {
    let title = match (&app.screen, app.snippets.form_editing()) {
        (Screen::SnippetForm, Some(_)) => "Snippets > Edit",
        _ => "Snippets > Add",
    };

    let fields = SnippetFormField::ALL;

    // Block: top(1) + fields * 2 (divider + content) + bottom(1)
    let block_height = 2 + fields.len() as u16 * 2;
    let total_height = block_height + 1; // + footer

    let form_area = design::overlay_area(frame, design::OVERLAY_W, design::OVERLAY_H, total_height);
    frame.render_widget(Clear, form_area);

    let block_area = Rect::new(form_area.x, form_area.y, form_area.width, block_height);

    let block = design::overlay_block(title);
    let inner = block.inner(block_area);
    frame.render_widget(block, block_area);

    for (i, &field) in fields.iter().enumerate() {
        let divider_y = design::form_divider_y(inner, i);
        let content_y = divider_y + 1;

        let is_focused = app.snippets.form().focused_field == field;
        let label_style = if is_focused {
            theme::accent_bold()
        } else {
            theme::muted()
        };
        let required = matches!(field, SnippetFormField::Name | SnippetFormField::Command);
        let label = if required {
            format!(" {}* ", field.label())
        } else {
            format!(" {} ", field.label())
        };
        super::render_divider(
            frame,
            block_area,
            divider_y,
            &label,
            label_style,
            theme::border_dim(),
        );

        let content_area = Rect::new(inner.x + 1, content_y, inner.width.saturating_sub(1), 1);
        render_field_content(frame, content_area, field, app.snippets.form());
    }

    // Footer below the block. The Default hosts field is a picker (Space opens
    // the host picker), so its footer carries the Space hint; the text fields
    // map to FieldKind::Text.
    let footer_area = design::render_overlay_footer(frame, block_area);
    if app.forms.is_discard_pending() {
        design::render_discard_prompt(frame, footer_area, app);
    } else {
        let kind = app.snippets.form().focused_field.kind();
        design::form_save_footer(design::FormFooterMode::Expanded(kind)).render_with_status(
            frame,
            footer_area,
            app,
        );
    }
}

fn render_field_content(
    frame: &mut Frame,
    area: Rect,
    field: SnippetFormField,
    form: &crate::app::SnippetForm,
) {
    let is_focused = form.focused_field == field;

    let placeholder = match field {
        SnippetFormField::Name => crate::messages::hints::SNIPPET_NAME,
        SnippetFormField::Command => crate::messages::hints::SNIPPET_COMMAND,
        SnippetFormField::Description => "",
        SnippetFormField::DefaultHosts => crate::messages::hints::SNIPPET_DEFAULT_HOSTS,
    };

    let field_value: String = match field {
        SnippetFormField::Name => form.name.clone(),
        SnippetFormField::Command => form.command.clone(),
        SnippetFormField::Description => form.description.clone(),
        SnippetFormField::DefaultHosts => {
            crate::messages::snippet::snippet_default_hosts_summary(&form.default_hosts)
        }
    };

    let is_picker = field.is_picker();
    let content = if is_picker && is_focused {
        // Picker field: value (or placeholder) plus a right-aligned arrow,
        // mirroring how host_form renders its picker fields.
        let arrow_pos = (area.width as usize).saturating_sub(1);
        let (display, display_style) = if field_value.is_empty() {
            (placeholder.to_string(), theme::muted())
        } else {
            (field_value.clone(), theme::bold())
        };
        let gap = arrow_pos.saturating_sub(display.width());
        Line::from(vec![
            Span::styled(display, display_style),
            Span::raw(" ".repeat(gap)),
            Span::styled(design::PICKER_ARROW, theme::muted()),
        ])
    } else if field_value.is_empty() && is_focused {
        if placeholder.is_empty() {
            Line::from(Span::styled(
                crate::messages::hints::SNIPPET_OPTIONAL,
                theme::muted(),
            ))
        } else {
            Line::from(Span::styled(placeholder, theme::muted()))
        }
    } else if field_value.is_empty() {
        Line::from(Span::raw(""))
    } else {
        Line::from(Span::styled(field_value.clone(), theme::bold()))
    };

    frame.render_widget(Paragraph::new(content), area);

    // The Default hosts field is picker-driven, not text, so it shows no cursor.
    if is_focused && !is_picker {
        let prefix: String = field_value.chars().take(form.cursor_pos).collect();
        let cursor_x = area
            .x
            .saturating_add(prefix.width().min(u16::MAX as usize) as u16);
        let cursor_y = area.y;
        if area.width > 0 && cursor_x < area.x.saturating_add(area.width) {
            frame.set_cursor_position((cursor_x, cursor_y));
        }
    }
}