fuckport 0.1.0

A CLI for killing processes by PID, name, or port.
Documentation
use std::collections::BTreeSet;

use dialoguer::{MultiSelect, theme::ColorfulTheme};
use sysinfo::Pid;

use crate::error::AppResult;
use crate::process::{ProcessCatalog, ProcessRecord};

pub fn pick_interactive(catalog: &ProcessCatalog, verbose: bool) -> AppResult<BTreeSet<Pid>> {
    let records = catalog.process_records();
    if records.is_empty() {
        return Ok(BTreeSet::new());
    }

    let items = records
        .iter()
        .map(|record| format_record(record, verbose))
        .collect::<Vec<_>>();

    let selections = MultiSelect::with_theme(&ColorfulTheme::default())
        .with_prompt("Select processes to kill")
        .items(&items)
        .interact()
        .map_err(|error| format!("interactive selection failed: {error}"))?;

    Ok(selections
        .into_iter()
        .map(|index| records[index].pid)
        .collect())
}

fn format_record(record: &ProcessRecord, verbose: bool) -> String {
    let ports = record
        .ports
        .iter()
        .map(|port| format!(":{port}"))
        .collect::<Vec<_>>()
        .join(",");
    let ports = if ports.is_empty() {
        "-".to_string()
    } else {
        ports
    };

    if verbose && !record.cmd.is_empty() {
        return format!(
            "{:<7} {:<24} {:<14} {}",
            record.pid.as_u32(),
            truncate(&record.name, 24),
            ports,
            record.cmd
        );
    }

    format!(
        "{:<7} {:<24} {}",
        record.pid.as_u32(),
        truncate(&record.name, 24),
        ports
    )
}

fn truncate(value: &str, width: usize) -> String {
    let mut result = value.chars().take(width).collect::<String>();
    if value.chars().count() > width && width > 1 {
        result.pop();
        result.push('~');
    }
    result
}