purple-ssh 3.18.5

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 super::ctx::Nav;
use crate::app::{App, KeysState, Screen, UiSelection};

/// The slice of App the help overlay and Keys-tab navigation touch: the help
/// scroll / tag-picker selection state (`ui`), the discovered-keys list
/// (`keys`) and the screen. None of these handlers notify or defer whole-App
/// work, so they only implement `Nav`.
struct HelpCtx<'a> {
    ui: &'a mut UiSelection,
    keys: &'a mut KeysState,
    screen: &'a mut Screen,
}

impl Nav for HelpCtx<'_> {
    fn screen_mut(&mut self) -> &mut Screen {
        self.screen
    }
}

fn ctx_from_app(app: &mut App) -> HelpCtx<'_> {
    HelpCtx {
        ui: &mut app.ui,
        keys: &mut app.keys,
        screen: &mut app.screen,
    }
}

pub(super) fn handle_key(app: &mut App, key: KeyEvent) {
    let mut ctx = ctx_from_app(app);
    help_key(&mut ctx, key);
}

fn help_key(ctx: &mut HelpCtx, key: KeyEvent) {
    match key.code {
        KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('?') => {
            ctx.ui.set_help_scroll(0);
            let return_screen = match &*ctx.screen {
                Screen::Help { return_screen } => (**return_screen).clone(),
                _ => Screen::HostList,
            };
            ctx.set_screen(return_screen);
        }
        KeyCode::Char('j') | KeyCode::Down => {
            ctx.ui
                .set_help_scroll(ctx.ui.help_scroll().saturating_add(1));
        }
        KeyCode::Char('k') | KeyCode::Up => {
            ctx.ui
                .set_help_scroll(ctx.ui.help_scroll().saturating_sub(1));
        }
        KeyCode::PageDown => {
            ctx.ui
                .set_help_scroll(ctx.ui.help_scroll().saturating_add(10));
        }
        KeyCode::PageUp => {
            ctx.ui
                .set_help_scroll(ctx.ui.help_scroll().saturating_sub(10));
        }
        _ => {}
    }
}

pub(super) fn handle_key_list_key(app: &mut App, key: KeyEvent) {
    let mut ctx = ctx_from_app(app);
    key_list_key(&mut ctx, key);
}

fn key_list_key(ctx: &mut HelpCtx, key: KeyEvent) {
    match key.code {
        KeyCode::Char('q') | KeyCode::Esc | KeyCode::Char('K') => {
            ctx.set_screen(Screen::HostList);
        }
        KeyCode::Char('?') => {
            let old = (*ctx.screen).clone();
            ctx.set_screen(Screen::Help {
                return_screen: Box::new(old),
            });
        }
        KeyCode::Char('j') | KeyCode::Down => {
            let len = ctx.keys.list().len();
            crate::app::cycle_selection(ctx.keys.list_state_mut(), len, true);
        }
        KeyCode::Char('k') | KeyCode::Up => {
            let len = ctx.keys.list().len();
            crate::app::cycle_selection(ctx.keys.list_state_mut(), len, false);
        }
        KeyCode::PageDown => {
            let len = ctx.keys.list().len();
            crate::app::page_down(ctx.keys.list_state_mut(), len, 10);
        }
        KeyCode::PageUp => {
            let len = ctx.keys.list().len();
            crate::app::page_up(ctx.keys.list_state_mut(), len, 10);
        }
        KeyCode::Enter => {
            if let Some(index) = ctx.keys.list_state().selected() {
                if index < ctx.keys.list().len() {
                    ctx.set_screen(Screen::KeyDetail { index });
                }
            }
        }
        _ => {}
    }
}

pub(super) fn handle_key_detail_key(app: &mut App, key: KeyEvent) {
    let mut ctx = ctx_from_app(app);
    key_detail_key(&mut ctx, key);
}

fn key_detail_key(ctx: &mut HelpCtx, key: KeyEvent) {
    match key.code {
        KeyCode::Esc | KeyCode::Char('q') => {
            ctx.set_screen(Screen::KeyList);
        }
        KeyCode::Char('?') => {
            let old = (*ctx.screen).clone();
            ctx.set_screen(Screen::Help {
                return_screen: Box::new(old),
            });
        }
        _ => {}
    }
}