ui-cli 0.3.0

A CLI to add components to your app.
Documentation
use std::error::Error;
use std::io;
use std::time::{Duration, Instant};

use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, KeyCode, MouseEventKind};
use crossterm::execute;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode};
use ratatui::Terminal;
use ratatui::backend::{Backend, CrosstermBackend};

use super::app::App;
use super::header::Tab;
use super::tabs::_render;
use super::tabs::{tab1_components, tab2_hooks};

pub fn run(tick_rate: Duration, components: Vec<String>) -> Result<Vec<String>, Box<dyn Error>> {
    // Setup terminal
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    // Create app and run it
    let app = App::new("Rust/UI CLI", components);
    let app_result = run_app(&mut terminal, app, tick_rate);

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

    app_result
}

/* ========================================================== */
/*                     ✨ FUNCTIONS ✨                        */
/* ========================================================== */

fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App, tick_rate: Duration) -> Result<Vec<String>, Box<dyn Error>> {
    let mut last_tick = Instant::now();
    loop {
        terminal.draw(|frame| _render::render(frame, &mut app))?;

        let timeout = tick_rate.saturating_sub(last_tick.elapsed());
        if !event::poll(timeout)? {
            app.on_tick();
            last_tick = Instant::now();
            continue;
        }
        match event::read()? {
            event::Event::Key(key) if key.kind == event::KeyEventKind::Press => {
                // Handle search mode in Components tab
                if app.components_search_active && matches!(app.header.tabs.current, Tab::Components) {
                    match key.code {
                        KeyCode::Char('j') | KeyCode::Down => app.on_down(),
                        KeyCode::Char('k') | KeyCode::Up => app.on_up(),
                        KeyCode::Char(c) => app.components_search_input(c),
                        KeyCode::Backspace => app.components_search_backspace(),
                        KeyCode::Esc => app.toggle_components_search(),
                        _ => {}
                    }
                // Handle search mode in Hooks tab
                } else if app.hooks_search_active && matches!(app.header.tabs.current, Tab::Hooks) {
                    match key.code {
                        KeyCode::Char('j') | KeyCode::Down => app.on_down(),
                        KeyCode::Char('k') | KeyCode::Up => app.on_up(),
                        KeyCode::Char(c) => app.hooks_search_input(c),
                        KeyCode::Backspace => app.hooks_search_backspace(),
                        KeyCode::Esc => app.toggle_hooks_search(),
                        _ => {}
                    }
                } else {
                    match key.code {
                        KeyCode::Char('?') => {
                            app.toggle_help_popup();
                        }
                        KeyCode::Char('/') if matches!(app.header.tabs.current, Tab::Components) => {
                            app.toggle_components_search();
                        }
                        KeyCode::Char('/') if matches!(app.header.tabs.current, Tab::Hooks) => {
                            app.toggle_hooks_search();
                        }
                        KeyCode::Char(' ') if matches!(app.header.tabs.current, Tab::Components) => {
                            if let Some(component) = tab1_components::get_selected_component(&app) {
                                app.toggle_component_checkbox(&component);
                            }
                        }
                        KeyCode::Char(' ') if matches!(app.header.tabs.current, Tab::Hooks) => {
                            if let Some(hook) = tab2_hooks::get_selected_hook(&app) {
                                app.toggle_hook_checkbox(hook);
                            }
                        }
                        KeyCode::Enter
                            if matches!(app.header.tabs.current, Tab::Components)
                                && !app.show_popup
                                && !app.components_checked.is_empty() =>
                        {
                            app.toggle_popup();
                        }
                        // Confirm selection when Enter is pressed in popup
                        KeyCode::Enter
                            if matches!(app.header.tabs.current, Tab::Components)
                                && app.show_popup
                                && !app.components_checked.is_empty() =>
                        {
                            // Return selected components
                            let selected: Vec<String> = app.components_checked.into_iter().collect();
                            return Ok(selected);
                        }
                        KeyCode::Enter
                            if matches!(app.header.tabs.current, Tab::Hooks)
                                && !app.show_popup
                                && !app.hooks_checked.is_empty() =>
                        {
                            app.toggle_popup();
                        }
                        KeyCode::Esc if app.show_help_popup => {
                            app.toggle_help_popup();
                        }
                        KeyCode::Esc
                            if matches!(app.header.tabs.current, Tab::Components) && !app.show_popup =>
                        {
                            // Handle double-tap Escape to deselect all components
                            let now = Instant::now();
                            let is_double_tap = if let Some(last_time) = app.last_escape_time {
                                now.duration_since(last_time).as_millis() < 500
                            } else {
                                false
                            };

                            if is_double_tap && !app.components_checked.is_empty() {
                                app.deselect_all_components();
                                app.last_escape_time = None;
                            } else {
                                app.last_escape_time = Some(now);
                            }
                        }
                        KeyCode::Esc
                            if matches!(app.header.tabs.current, Tab::Components) && app.show_popup =>
                        {
                            app.toggle_popup();
                        }
                        KeyCode::Esc
                            if matches!(app.header.tabs.current, Tab::Hooks) && !app.show_popup =>
                        {
                            // Handle double-tap Escape to deselect all hooks
                            let now = Instant::now();
                            let is_double_tap = if let Some(last_time) = app.last_escape_time {
                                now.duration_since(last_time).as_millis() < 500
                            } else {
                                false
                            };

                            if is_double_tap && !app.hooks_checked.is_empty() {
                                app.deselect_all_hooks();
                                app.last_escape_time = None;
                            } else {
                                app.last_escape_time = Some(now);
                            }
                        }
                        KeyCode::Esc
                            if matches!(app.header.tabs.current, Tab::Hooks) && app.show_popup =>
                        {
                            app.toggle_popup();
                        }
                        KeyCode::Char('h') | KeyCode::Left => {
                            if !app.show_popup && !app.show_help_popup {
                                app.on_left();
                            }
                        }
                        KeyCode::Char('j') | KeyCode::Down => {
                            if !app.show_popup && !app.show_help_popup {
                                app.on_down();
                            }
                        }
                        KeyCode::Char('k') | KeyCode::Up => {
                            if !app.show_popup && !app.show_help_popup {
                                app.on_up();
                            }
                        }
                        KeyCode::Char('l') | KeyCode::Right => {
                            if !app.show_popup && !app.show_help_popup {
                                app.on_right();
                            }
                        }
                        KeyCode::Char(c) => app.on_key(c),
                        _ => {}
                    }
                }
            }
            event::Event::Mouse(mouse) => {
                match mouse.kind {
                    MouseEventKind::Down(_) => {
                        let terminal_width = terminal.size()?.width;
                        let now = std::time::Instant::now();
                        let current_pos = (mouse.column, mouse.row);

                        // Check for double-click (within 500ms and same position)
                        let is_double_click =
                            if let (Some(last_time), Some(last_pos)) = (app.last_click_time, app.last_click_pos) {
                                now.duration_since(last_time).as_millis() < 500 && last_pos == current_pos
                            } else {
                                false
                            };

                        if is_double_click {
                            // Handle double-click on component list items
                            if let Some(visual_index) =
                                app.get_components_double_click_info(mouse.column, mouse.row, terminal_width)
                            {
                                if let Some(component) =
                                    tab1_components::get_component_at_visual_index(&app, visual_index)
                                {
                                    app.toggle_component_checkbox(&component);
                                }
                            }
                            // Handle double-click on hook list items
                            if let Some(visual_index) =
                                app.get_hooks_double_click_info(mouse.column, mouse.row, terminal_width)
                            {
                                if let Some(hook) = tab2_hooks::get_hook_at_visual_index(&app, visual_index) {
                                    app.toggle_hook_checkbox(hook);
                                }
                            }
                            // Reset click tracking after double-click
                            app.last_click_time = None;
                            app.last_click_pos = None;
                        } else {
                            // Single click - update tracking
                            app.on_mouse_click(mouse.column, mouse.row, terminal_width);
                            app.last_click_time = Some(now);
                            app.last_click_pos = Some(current_pos);
                        }
                    }
                    MouseEventKind::ScrollUp => {
                        if !app.show_popup && !app.show_help_popup {
                            app.on_up();
                        }
                    }
                    MouseEventKind::ScrollDown => {
                        if !app.show_popup && !app.show_help_popup {
                            app.on_down();
                        }
                    }
                    _ => {}
                }
            }
            _ => {}
        }
        if app.should_quit {
            return Ok(Vec::new());
        }
    }
}