purple-ssh 3.15.16

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_key(app: &mut App, key: KeyEvent) {
    let Screen::Welcome {
        known_hosts_count, ..
    } = &app.screen
    else {
        return;
    };
    let known_hosts_count = *known_hosts_count;

    // Closing Welcome seeds last_seen_version so future launches do not re-trigger the upgraded flow.
    let version = env!("CARGO_PKG_VERSION");
    if let Err(e) = crate::preferences::save_last_seen_version(version) {
        log::warn!("[purple] failed to seed last_seen_version on welcome close: {e}");
    }
    if key.code == KeyCode::Char('?') {
        app.set_screen(Screen::Help {
            return_screen: Box::new(Screen::HostList),
        });
    } else if key.code == KeyCode::Char('I') && known_hosts_count > 0 {
        app.set_screen(Screen::HostList);
        super::confirm::execute_known_hosts_import(app);
    } else {
        app.set_screen(Screen::HostList);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ssh_config::model::SshConfigFile;
    use crossterm::event::KeyModifiers;

    fn make_app_on_welcome(known_hosts_count: usize) -> App {
        let scratch = tempfile::tempdir().expect("tempdir").keep();
        crate::preferences::set_path_override(scratch.join("preferences"));
        let config = SshConfigFile {
            elements: SshConfigFile::parse_content(""),
            path: scratch.join("test_config"),
            crlf: false,
            bom: false,
        };
        let mut app = App::new(config);
        app.screen = Screen::Welcome {
            has_backup: false,
            host_count: 0,
            known_hosts_count,
        };
        app
    }

    fn k(code: KeyCode) -> KeyEvent {
        KeyEvent::new(code, KeyModifiers::NONE)
    }

    #[test]
    fn question_key_opens_help() {
        let _lock = crate::demo_flag::GLOBAL_TEST_LOCK.lock().unwrap();
        let mut app = make_app_on_welcome(0);
        handle_key(&mut app, k(KeyCode::Char('?')));
        assert!(matches!(app.screen, Screen::Help { .. }));
    }

    #[test]
    fn any_other_key_returns_to_host_list() {
        let _lock = crate::demo_flag::GLOBAL_TEST_LOCK.lock().unwrap();
        let mut app = make_app_on_welcome(0);
        handle_key(&mut app, k(KeyCode::Esc));
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn capital_i_with_zero_known_hosts_just_dismisses() {
        let _lock = crate::demo_flag::GLOBAL_TEST_LOCK.lock().unwrap();
        let mut app = make_app_on_welcome(0);
        handle_key(&mut app, k(KeyCode::Char('I')));
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn capital_i_with_known_hosts_triggers_import_path() {
        // ENV_LOCK serialises HOME/PATH env mutation across the test binary.
        let _path_guard = crate::vault_ssh::tests::ENV_LOCK
            .lock()
            .unwrap_or_else(|p| p.into_inner());
        let old_home = std::env::var("HOME").ok();
        let home = tempfile::tempdir().expect("tempdir").keep();
        // SAFETY: ENV_LOCK held above serializes env mutation in this test
        // binary; HOME is restored below before the lock drops.
        unsafe {
            std::env::set_var("HOME", &home);
        }
        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            let mut app = make_app_on_welcome(1);
            handle_key(&mut app, k(KeyCode::Char('I')));
            assert!(matches!(app.screen, Screen::HostList));
            assert!(
                app.status_center.toast().is_some() || app.status_center.status().is_some(),
                "I-key with known_hosts_count > 0 must attempt import and emit a status"
            );
        }));
        // SAFETY: ENV_LOCK still held; restore prior HOME before lock drops.
        unsafe {
            match old_home {
                Some(h) => std::env::set_var("HOME", h),
                None => std::env::remove_var("HOME"),
            }
        }
        if let Err(e) = result {
            std::panic::resume_unwind(e);
        }
    }
}