portwatch 0.1.8

A cross-platform TUI for monitoring network ports and managing processes
mod alerts;
mod app;
mod backends;
mod daemon;
mod events;
mod models;
mod ui;

use anyhow::Result;
use app::AppState;
use clap::{Parser, Subcommand};
use crossterm::{
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use events::{EventHandler, InputContext};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
use std::time::{Duration, Instant};

#[derive(Parser, Debug)]
#[command(name = "portwatch")]
#[command(author, version, about = "A cross-platform TUI for monitoring ports and managing processes", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,

    #[command(flatten)]
    tui: TuiArgs,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// Run alert monitoring without the TUI (notifications still fire)
    Daemon {
        #[arg(
            short,
            long,
            default_value_t = 2000,
            help = "Polling interval in milliseconds"
        )]
        interval_ms: u64,

        #[arg(
            long,
            help = "Reload alerts.json from disk on every poll (picks up edits without restart)"
        )]
        watch_config: bool,
    },
}

#[derive(Parser, Debug)]
struct TuiArgs {
    #[arg(short, long, default_value_t = 2000, help = "Auto-refresh interval in milliseconds")]
    refresh_interval: u64,

    #[arg(short, long, help = "Initial filter to apply")]
    filter: Option<String>,
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    if let Some(Commands::Daemon {
        interval_ms,
        watch_config,
    }) = cli.command
    {
        return daemon::run_daemon_loop(
            std::time::Duration::from_millis(interval_ms),
            watch_config,
        );
    }

    let args = cli.tui;

    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    let result = run_app(&mut terminal, args);

    disable_raw_mode()?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
    terminal.show_cursor()?;

    if let Err(err) = result {
        eprintln!("Error: {}", err);
        std::process::exit(1);
    }

    Ok(())
}

fn run_app<B: ratatui::backend::Backend>(
    terminal: &mut Terminal<B>,
    args: TuiArgs,
) -> Result<()> {
    let mut state = AppState::new();
    let mut event_handler = EventHandler::new();

    if let Some(filter) = args.filter {
        state.filter = filter;
    }

    state.refresh()?;

    let refresh_interval = Duration::from_millis(args.refresh_interval);
    let mut last_refresh = Instant::now();

    loop {
        terminal.draw(|f| ui::render(f, &state, &event_handler))?;

        let timeout = refresh_interval
            .checked_sub(last_refresh.elapsed())
            .unwrap_or(Duration::from_millis(100));

        let ctx = InputContext {
            show_help: state.show_help,
            alert_rules_open: state.alert_rules.open,
            alert_rule_form: state.alert_rules.form.is_some(),
            alert_rule_form_focus: state.alert_rules.form_focus,
        };
        let action = event_handler.next_action(timeout, ctx)?;

        let should_quit = state.apply_action(action)?;
        if should_quit {
            break;
        }

        if last_refresh.elapsed() >= refresh_interval {
            state.refresh()?;
            last_refresh = Instant::now();
        }
    }

    Ok(())
}