bath 0.4.0

A TUI tool to manage and export environment variable profiles
use crate::tui::input;
use crate::tui::state::AppState;
use crate::tui::ui;
use crate::tui::util;
use anyhow::Result;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::execute;
use crossterm::terminal::{
    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::io::{stdout, Stdout};

type Tui = Terminal<CrosstermBackend<Stdout>>;

/// Installs a panic hook that restores the terminal before the previous hook
/// runs, so a panic anywhere (AppState::new, nested dialog loops, drawing)
/// leaves a usable shell instead of a raw-mode alternate screen.
fn install_panic_hook() {
    let previous = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        let _ = restore_terminal_raw();
        previous(info);
    }));
}

/// Best-effort terminal restore usable from the panic hook (no Terminal
/// handle). Every step is attempted even when an earlier one fails — skipping
/// LeaveAlternateScreen just because disable_raw_mode failed would leave the
/// shell unusable — and the first error is reported afterwards.
fn restore_terminal_raw() -> Result<()> {
    let raw = disable_raw_mode();
    let screen = execute!(
        stdout(),
        LeaveAlternateScreen,
        DisableMouseCapture,
        crossterm::cursor::Show
    );
    raw?;
    screen?;
    Ok(())
}

/// Best-effort restore through the ratatui handle: like
/// [`restore_terminal_raw`], every step is attempted and the first error is
/// reported afterwards.
fn restore_terminal(terminal: &mut Tui) -> Result<()> {
    let raw = disable_raw_mode();
    let screen = execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,
        DisableMouseCapture
    );
    let cursor = terminal.show_cursor();
    raw?;
    screen?;
    cursor?;
    Ok(())
}

pub fn run() -> Result<()> {
    install_panic_hook();
    enable_raw_mode()?;
    let mut stdout = stdout();
    // Between enable_raw_mode succeeding and the guarded event loop below, an
    // Err propagating via `?` would leave raw mode enabled (these are Errs,
    // not panics, so the panic hook never fires). Restore explicitly instead.
    if let Err(e) = execute!(stdout, EnterAlternateScreen, EnableMouseCapture) {
        // The alternate screen may already have been entered when only the
        // mouse-capture part failed; restoring everything is best-effort.
        let _ = restore_terminal_raw();
        return Err(e.into());
    }
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = match Terminal::new(backend) {
        Ok(terminal) => terminal,
        Err(e) => {
            let _ = restore_terminal_raw();
            return Err(e.into());
        }
    };

    // Run the event loop and always restore the terminal afterwards, even
    // when the loop (or AppState::new) returned an error.
    let result = run_event_loop(&mut terminal);
    let restored = restore_terminal(&mut terminal);
    result.and(restored)
}

fn run_event_loop(terminal: &mut Tui) -> Result<()> {
    let mut app = AppState::new()?;

    loop {
        terminal.draw(|f| ui::draw_main_ui(f, &mut app))?;

        if let Some(key) = util::next_key_press(std::time::Duration::from_millis(200))? {
            if input::handle_key_event(terminal, &mut app, key)? {
                return Ok(());
            }
        }
    }
}