http-diff 0.0.5

http-diff - CLI tool to verify consistency across web server versions. Ideal for large-scale refactors, sanity tests and maintaining data integrity across versions.
use actions::{event_to_app_action, AppAction};
use anyhow::{anyhow, Result};
use app_state::AppState;
use chrono::Local;
use clap::Parser;
use crossterm::style::Stylize;
use crossterm::{
    event::{self, DisableMouseCapture, EnableMouseCapture},
    execute,
    terminal::{
        disable_raw_mode, enable_raw_mode, EnterAlternateScreen,
        LeaveAlternateScreen,
    },
};
use http_diff::app::App as HttpDiff;
use ratatui::{
    backend::{Backend, CrosstermBackend},
    Terminal,
};
use reducer::update_state;
use std::{fs::File, process, sync::Arc};
use std::{io, time::Duration};
use std::{path::Path, time::Instant};
use tokio::sync::broadcast;
use tracing::error;
use tracing_subscriber::{
    filter::{LevelFilter, Targets},
    fmt,
    prelude::*,
};
use ui::{top::print_logo, ui};
use worker::{
    get_configuration_file_watcher, handle_commands_to_http_diff_loop,
    process_app_action,
};

pub mod actions;
pub mod app_state;
pub mod cli;
pub mod http_diff;
pub mod reducer;
pub mod ui;
pub mod worker;
use cli::Arguments;

pub fn initialize_panic_handler() {
    let original_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |panic_info| {
        crossterm::execute!(
            std::io::stderr(),
            crossterm::terminal::LeaveAlternateScreen
        )
        .unwrap();
        crossterm::terminal::disable_raw_mode().unwrap();
        original_hook(panic_info);
    }));
}

fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
    initialize_panic_handler();
    execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?;
    enable_raw_mode()?;

    let backend = CrosstermBackend::new(io::stdout());

    let mut terminal = Terminal::new(backend)?;
    terminal.hide_cursor()?;

    Ok(terminal)
}

fn reset_terminal(
    terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
) -> Result<()> {
    disable_raw_mode()?;
    execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,
        DisableMouseCapture,
    )?;
    terminal.show_cursor()?;
    Ok(())
}

#[tokio::main]
async fn main() -> Result<()> {
    let args = match Arguments::try_parse() {
        Err(err) => {
            println!("{err}");
            return Ok(());
        }
        Ok(args) => args,
    };

    if args.enable_log {
        let _ = tracing_subscriber::registry()
            .with(
                fmt::layer()
                    .with_writer(Arc::new(File::create("./.log")?))
                    .json()
                    .with_filter(
                        Targets::default()
                            .with_target("http_diff", LevelFilter::DEBUG),
                    ),
            )
            .try_init();
    }

    let mut terminal =
        if !args.headless { Some(init_terminal()?) } else { None };

    let res = run_app(&mut terminal, args).await;

    if let Some(mut terminal) = terminal {
        reset_terminal(&mut terminal)?;
    }

    if let Err(err) = res {
        println!("\n{}", err.to_string().red());

        process::exit(1);
    }

    Ok(())
}

async fn run_app<B: Backend>(
    terminal: &mut Option<Terminal<B>>,
    args: Arguments,
) -> Result<()> {
    let output_directory = Path::new(&args.output_directory);

    let started_at = Local::now();

    let base_output_directory = output_directory
        .join(started_at.format("%Y-%m-%d %H:%M:%S").to_string());

    let mut app = AppState::new(&output_directory, args.headless);

    let (event_loop_actions_sender, mut event_loop_actions_receiver) =
        broadcast::channel::<AppAction>(1000);

    let worker_actions_sender = event_loop_actions_sender.clone();

    let mut http_diff_actions_receiver = event_loop_actions_sender.subscribe();
    let mut worker_actions_receiver = worker_actions_sender.subscribe();

    let mut http_diff = HttpDiff::new(event_loop_actions_sender.clone())?;

    if app.is_headless_mode {
        print_logo();
    };

    tokio::spawn(async move {
        loop {
            match handle_commands_to_http_diff_loop(
                &mut http_diff_actions_receiver,
                &mut http_diff,
            )
            .await
            {
                Ok(()) => break,
                Err(app_error) => {
                    error!("handle_commands_to_http_diff_loop returned error: {app_error}");

                    let _ = http_diff.app_actions_sender.send(
                        AppAction::SetCriticalException(app_error.into()),
                    );
                }
            }
        }
    });

    let app_actions_sender = event_loop_actions_sender.clone();
    let configuration_file_path = args.configuration.clone();

    let _watcher = get_configuration_file_watcher(
        configuration_file_path,
        app_actions_sender,
    )
    .await?;

    tokio::spawn(async move {
        loop {
            let action = match worker_actions_receiver.recv().await {
                Ok(action) => action,
                Err(_) => break,
            };

            let output_dir = base_output_directory.clone();
            let sender = worker_actions_sender.clone();

            tokio::spawn(async move {
                process_app_action(action, sender, output_dir).await;
            });
        }
    });

    let event_timeout = Duration::from_millis(60);

    event_loop_actions_sender
        .send(AppAction::TryLoadConfigurationFile(args.configuration))?;

    loop {
        if app.should_quit {
            if app.critical_exception.is_some() && app.is_headless_mode {
                return Err(anyhow!(app.critical_exception.unwrap()));
            }
            return Ok(());
        }

        if let Some(terminal) = terminal {
            terminal.draw(|f| ui(f, &mut app))?;

            let started_to_poll = Instant::now();

            // receive input and dispatch actions
            while started_to_poll.elapsed() < event_timeout {
                if event::poll(event_timeout - started_to_poll.elapsed())? {
                    if let Some(action) =
                        event_to_app_action(&event::read()?, &app)
                    {
                        event_loop_actions_sender.send(action)?;
                    }
                } else {
                    break;
                }
            }
        };

        // receive and act on actions
        loop {
            match event_loop_actions_receiver.try_recv() {
                Ok(action) => {
                    let mut current_action = Some(action);

                    while let Some(action) = current_action {
                        current_action = update_state(
                            &mut app,
                            action,
                            &event_loop_actions_sender,
                        );
                    }
                }
                Err(_) => {
                    break;
                }
            }
        }

        app.on_tick();
    }
}