whoseportisitanyway 1.1.2

Cross-platform TUI for discovering which ports are in use, who owns them, and what they're for
Documentation
use crossterm::event::KeyCode;
use nix::sys::signal::{self, Signal};
use nix::unistd::Pid;

use super::{App, View};

pub fn handle_table_key(app: &mut App, key: KeyCode) {
    match key {
        KeyCode::Char('q') | KeyCode::Esc => app.should_quit = true,
        KeyCode::Char('j') | KeyCode::Down if !app.entries.is_empty() => {
            app.selected = (app.selected + 1).min(app.entries.len() - 1);
        }
        KeyCode::Char('k') | KeyCode::Up => {
            app.selected = app.selected.saturating_sub(1);
        }
        KeyCode::Char('G') if !app.entries.is_empty() => {
            app.selected = app.entries.len() - 1;
        }
        KeyCode::Char('g') => {
            app.selected = 0;
        }
        KeyCode::Enter | KeyCode::Char('d') if app.selected_entry().is_some() => {
            app.view = View::Detail;
        }
        KeyCode::Char('x') if app.selected_entry().is_some() => {
            app.view = View::Confirm;
        }
        KeyCode::Char('s') => app.cycle_sort(),
        KeyCode::Char('f') => app.cycle_filter(),
        KeyCode::Char('h') => app.toggle_hide_system(),
        KeyCode::Tab => app.cycle_group(),
        KeyCode::Char('r') => {
            let _ = app.refresh();
        }
        _ => {}
    }
}

pub fn handle_detail_key(app: &mut App, key: KeyCode) {
    match key {
        KeyCode::Char('q') | KeyCode::Esc => app.view = View::Table,
        KeyCode::Char('x') => app.view = View::Confirm,
        KeyCode::Char('o') => {
            if let Some(entry) = app.selected_entry() {
                let url = super::detail::build_search_url(entry);
                super::detail::open_in_browser(&url);
            }
        }
        _ => {}
    }
}

pub fn handle_confirm_key(app: &mut App, key: KeyCode) {
    match key {
        KeyCode::Char('y') => {
            if let Some(entry) = app.selected_entry() {
                if entry.pid != 0 {
                    let _ = signal::kill(Pid::from_raw(entry.pid as i32), Signal::SIGTERM);
                }
            }
            app.view = View::Table;
            let _ = app.refresh();
        }
        KeyCode::Char('n') | KeyCode::Esc => app.view = View::Table,
        _ => {}
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{Classification, Ownership, PortEntry, PortState, Protocol};

    fn test_app(n: usize) -> App {
        let entries: Vec<PortEntry> = (0..n)
            .map(|i| PortEntry {
                port: 3000 + i as u16,
                protocol: Protocol::Tcp,
                pid: 100 + i as u32,
                process_name: format!("proc{i}"),
                command_line: format!("proc{i} --serve"),
                classification: Classification::DevServer,
                ownership: Ownership::Untracked,
                state: PortState::Listen,
                local_addr: format!("0.0.0.0:{}", 3000 + i),
                all_addrs: vec![format!("0.0.0.0:{}", 3000 + i)],
                project: None,
                uid: None,
                user: None,
                remote_addr: None,
            })
            .collect();
        App {
            all_entries: entries.clone(),
            entries,
            selected: 0,
            view: View::Table,
            should_quit: false,
            watched_ports: vec![],
            sort_field: super::super::SortField::Port,
            filter: super::super::Filter::All,
            group_field: super::super::GroupField::None,
            group_labels: vec![],
            konami: super::super::KonamiDetector::new(),
            konami_mode: false,
            shuffle_remaining: 0,
            hide_system: false,
        }
    }

    #[test]
    fn table_q_quits() {
        let mut app = test_app(3);
        handle_table_key(&mut app, KeyCode::Char('q'));
        assert!(app.should_quit);
    }

    #[test]
    fn table_esc_quits() {
        let mut app = test_app(3);
        handle_table_key(&mut app, KeyCode::Esc);
        assert!(app.should_quit);
    }

    #[test]
    fn table_j_moves_down() {
        let mut app = test_app(5);
        handle_table_key(&mut app, KeyCode::Char('j'));
        assert_eq!(app.selected, 1);
    }

    #[test]
    fn table_down_moves_down() {
        let mut app = test_app(5);
        handle_table_key(&mut app, KeyCode::Down);
        assert_eq!(app.selected, 1);
    }

    #[test]
    fn table_k_moves_up() {
        let mut app = test_app(5);
        app.selected = 3;
        handle_table_key(&mut app, KeyCode::Char('k'));
        assert_eq!(app.selected, 2);
    }

    #[test]
    fn table_up_moves_up() {
        let mut app = test_app(5);
        app.selected = 2;
        handle_table_key(&mut app, KeyCode::Up);
        assert_eq!(app.selected, 1);
    }

    #[test]
    fn table_k_at_zero_stays() {
        let mut app = test_app(5);
        handle_table_key(&mut app, KeyCode::Char('k'));
        assert_eq!(app.selected, 0);
    }

    #[test]
    fn table_g_goes_to_top() {
        let mut app = test_app(5);
        app.selected = 4;
        handle_table_key(&mut app, KeyCode::Char('g'));
        assert_eq!(app.selected, 0);
    }

    #[test]
    fn table_big_g_goes_to_bottom() {
        let mut app = test_app(5);
        handle_table_key(&mut app, KeyCode::Char('G'));
        assert_eq!(app.selected, 4);
    }

    #[test]
    fn table_enter_opens_detail() {
        let mut app = test_app(3);
        handle_table_key(&mut app, KeyCode::Enter);
        assert_eq!(app.view, View::Detail);
    }

    #[test]
    fn table_d_opens_detail() {
        let mut app = test_app(3);
        handle_table_key(&mut app, KeyCode::Char('d'));
        assert_eq!(app.view, View::Detail);
    }

    #[test]
    fn table_x_opens_confirm() {
        let mut app = test_app(3);
        handle_table_key(&mut app, KeyCode::Char('x'));
        assert_eq!(app.view, View::Confirm);
    }

    #[test]
    fn table_s_cycles_sort() {
        let mut app = test_app(3);
        assert_eq!(app.sort_field, super::super::SortField::Port);
        handle_table_key(&mut app, KeyCode::Char('s'));
        assert_eq!(app.sort_field, super::super::SortField::Process);
    }

    #[test]
    fn table_f_cycles_filter() {
        let mut app = test_app(3);
        assert_eq!(app.filter, super::super::Filter::All);
        handle_table_key(&mut app, KeyCode::Char('f'));
        assert_eq!(app.filter, super::super::Filter::Listen);
    }

    #[test]
    fn table_j_clamps_at_end() {
        let mut app = test_app(3);
        app.selected = 2;
        handle_table_key(&mut app, KeyCode::Char('j'));
        assert_eq!(app.selected, 2);
    }

    #[test]
    fn table_empty_j_noop() {
        let mut app = test_app(0);
        handle_table_key(&mut app, KeyCode::Char('j'));
        assert_eq!(app.selected, 0);
    }

    #[test]
    fn table_empty_big_g_noop() {
        let mut app = test_app(0);
        handle_table_key(&mut app, KeyCode::Char('G'));
        assert_eq!(app.selected, 0);
    }

    #[test]
    fn table_unknown_key_noop() {
        let mut app = test_app(3);
        handle_table_key(&mut app, KeyCode::Char('z'));
        assert_eq!(app.view, View::Table);
        assert!(!app.should_quit);
    }

    #[test]
    fn table_enter_empty_noop() {
        let mut app = test_app(0);
        handle_table_key(&mut app, KeyCode::Enter);
        assert_eq!(app.view, View::Table);
    }

    #[test]
    fn table_x_empty_noop() {
        let mut app = test_app(0);
        handle_table_key(&mut app, KeyCode::Char('x'));
        assert_eq!(app.view, View::Table);
    }

    #[test]
    fn table_tab_cycles_group() {
        let mut app = test_app(3);
        assert_eq!(app.group_field, super::super::GroupField::None);
        handle_table_key(&mut app, KeyCode::Tab);
        assert_eq!(app.group_field, super::super::GroupField::Type);
        handle_table_key(&mut app, KeyCode::Tab);
        assert_eq!(app.group_field, super::super::GroupField::Project);
    }

    #[test]
    fn detail_q_back_to_table() {
        let mut app = test_app(3);
        app.view = View::Detail;
        handle_detail_key(&mut app, KeyCode::Char('q'));
        assert_eq!(app.view, View::Table);
    }

    #[test]
    fn detail_esc_back_to_table() {
        let mut app = test_app(3);
        app.view = View::Detail;
        handle_detail_key(&mut app, KeyCode::Esc);
        assert_eq!(app.view, View::Table);
    }

    #[test]
    fn detail_x_opens_confirm() {
        let mut app = test_app(3);
        app.view = View::Detail;
        handle_detail_key(&mut app, KeyCode::Char('x'));
        assert_eq!(app.view, View::Confirm);
    }

    #[test]
    fn detail_unknown_noop() {
        let mut app = test_app(3);
        app.view = View::Detail;
        handle_detail_key(&mut app, KeyCode::Char('z'));
        assert_eq!(app.view, View::Detail);
    }

    #[test]
    fn detail_o_does_not_change_view() {
        // 'o' triggers browser open but does not change the view
        let mut app = test_app(3);
        app.view = View::Detail;
        handle_detail_key(&mut app, KeyCode::Char('o'));
        assert_eq!(app.view, View::Detail);
    }

    #[test]
    fn detail_o_on_empty_app_is_noop() {
        let mut app = test_app(0);
        app.view = View::Detail;
        handle_detail_key(&mut app, KeyCode::Char('o'));
        assert_eq!(app.view, View::Detail);
    }

    #[test]
    fn confirm_n_back_to_table() {
        let mut app = test_app(3);
        app.view = View::Confirm;
        handle_confirm_key(&mut app, KeyCode::Char('n'));
        assert_eq!(app.view, View::Table);
    }

    #[test]
    fn confirm_esc_back_to_table() {
        let mut app = test_app(3);
        app.view = View::Confirm;
        handle_confirm_key(&mut app, KeyCode::Esc);
        assert_eq!(app.view, View::Table);
    }

    #[test]
    fn confirm_unknown_noop() {
        let mut app = test_app(3);
        app.view = View::Confirm;
        handle_confirm_key(&mut app, KeyCode::Char('z'));
        assert_eq!(app.view, View::Confirm);
    }

    #[test]
    fn table_h_toggles_hide_system() {
        let mut app = test_app(3);
        assert!(!app.hide_system, "starts false");
        handle_table_key(&mut app, KeyCode::Char('h'));
        assert!(app.hide_system, "toggled to true");
        handle_table_key(&mut app, KeyCode::Char('h'));
        assert!(!app.hide_system, "toggled back to false");
    }
}