purple-ssh 3.5.0

Open-source terminal SSH manager that keeps ~/.ssh/config in sync with your cloud infra. Spin up a VM on AWS, GCP, Azure, Hetzner or 12 other cloud providers and it appears in your host list. Destroy it and the entry dims. Search hundreds of hosts, transfer files, manage Docker and Podman over SSH, sign Vault SSH certs. Rust TUI, MIT licensed.
Documentation
use crossterm::event::{KeyCode, KeyEvent};

use crate::app::{App, Screen};

pub(super) fn handle_tag_input(app: &mut App, key: KeyEvent) {
    match key.code {
        KeyCode::Enter => {
            if let Some(ref input) = app.tags.input {
                let tags: Vec<String> = input
                    .split(',')
                    .map(|t| t.trim().to_string())
                    .filter(|t| !t.is_empty())
                    .collect();
                if let Some(host) = app.selected_host() {
                    let alias = host.alias.clone();
                    let old_tags = host.tags.clone();
                    app.hosts_state.ssh_config.set_host_tags(&alias, &tags);
                    if let Err(e) = app.hosts_state.ssh_config.write() {
                        // Restore old tags on write failure
                        app.hosts_state.ssh_config.set_host_tags(&alias, &old_tags);
                        app.notify_error(crate::messages::failed_to_save(&e));
                    } else {
                        app.update_last_modified();
                        let count = tags.len();
                        app.reload_hosts();
                        app.select_host_by_alias(&alias);
                        app.notify(crate::messages::tagged_host(&alias, count));
                    }
                }
            }
            app.tags.input = None;
            app.tags.cursor = 0;
        }
        KeyCode::Esc => {
            app.tags.input = None;
            app.tags.cursor = 0;
        }
        KeyCode::Left if app.tags.cursor > 0 => {
            app.tags.cursor -= 1;
        }
        KeyCode::Right => {
            if let Some(ref input) = app.tags.input {
                if app.tags.cursor < input.chars().count() {
                    app.tags.cursor += 1;
                }
            }
        }
        KeyCode::Home => {
            app.tags.cursor = 0;
        }
        KeyCode::End => {
            if let Some(ref input) = app.tags.input {
                app.tags.cursor = input.chars().count();
            }
        }
        KeyCode::Char(c) => {
            if let Some(ref mut input) = app.tags.input {
                let byte_pos = crate::app::char_to_byte_pos(input, app.tags.cursor);
                input.insert(byte_pos, c);
                app.tags.cursor += 1;
            }
        }
        KeyCode::Backspace if app.tags.cursor > 0 => {
            if let Some(ref mut input) = app.tags.input {
                let byte_pos = crate::app::char_to_byte_pos(input, app.tags.cursor);
                let prev = crate::app::char_to_byte_pos(input, app.tags.cursor - 1);
                input.drain(prev..byte_pos);
                app.tags.cursor -= 1;
            }
        }
        _ => {}
    }
}

pub(super) fn handle_host_detail(app: &mut App, key: KeyEvent) {
    let index = match app.screen {
        Screen::HostDetail { index } => index,
        _ => return,
    };
    match key.code {
        KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('i') => {
            app.set_screen(Screen::HostList);
        }
        KeyCode::Char('?') => {
            let old = std::mem::replace(&mut app.screen, Screen::HostList);
            app.set_screen(Screen::Help {
                return_screen: Box::new(old),
            });
        }
        KeyCode::Char('e') => {
            if let Some(host) = app.hosts_state.list.get(index).cloned() {
                super::open_edit_form(app, host);
            }
        }
        KeyCode::Char('T') => {
            if let Some(host) = app.hosts_state.list.get(index) {
                let stale_hint = if host.stale.is_some() {
                    Some(super::stale_provider_hint(host))
                } else {
                    None
                };
                let alias = host.alias.clone();
                if let Some(hint) = stale_hint {
                    app.notify_warning(crate::messages::stale_host(&hint));
                }
                app.refresh_tunnel_list(&alias);
                app.ui.tunnel_list_state = ratatui::widgets::ListState::default();
                if !app.tunnels.list.is_empty() {
                    app.ui.tunnel_list_state.select(Some(0));
                }
                app.set_screen(Screen::TunnelList { alias });
            }
        }
        KeyCode::Char('r') => {
            if let Some(host) = app.hosts_state.list.get(index) {
                let stale_hint = if host.stale.is_some() {
                    Some(super::stale_provider_hint(host))
                } else {
                    None
                };
                let alias = host.alias.clone();
                if let Some(hint) = stale_hint {
                    app.notify_warning(crate::messages::stale_host(&hint));
                }
                app.set_screen(Screen::SnippetPicker {
                    target_aliases: vec![alias],
                });
                app.ui.snippet_picker_state = ratatui::widgets::ListState::default();
                let indices = app.filtered_snippet_indices();
                if !indices.is_empty() {
                    app.ui.snippet_picker_state.select(Some(0));
                }
            }
        }
        _ => {}
    }
}