//! Module containing skim's entry point
use std::io::{BufWriter, Stderr};
use std::sync::Arc;
use std::time::Duration;

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use eyre::{self, OptionExt, Result};
#[cfg(feature = "image")]
use ratatui_image::picker::Picker;
use tokio::runtime::Handle;
use tokio::select;
use tokio::task::block_in_place;

use crate::binds::SkimEvent;
use crate::reader::{Reader, ReaderControl};
use crate::tui::actions::Action;
#[cfg(feature = "image")]
use crate::tui::util::detect_image_picker;
use crate::tui::{App, Event, Size, TICK_RATE, Tui};
use crate::{SkimItem, SkimItemReceiver, SkimOptions, SkimOutput};

/// Stream type yielded by the IPC listener. With the `listen` feature disabled the
/// listener branch can never fire, so its payload type is uninhabited.
#[cfg(feature = "listen")]
type RemoteStream = interprocess::local_socket::tokio::Stream;
#[cfg(not(feature = "listen"))]
type RemoteStream = std::convert::Infallible;

/// Main entry point for running skim
pub struct Skim<Backend = ratatui::backend::CrosstermBackend<BufWriter<Stderr>>>
where
    Backend: ratatui::backend::Backend,
    Backend::Error: Send + Sync + 'static,
{
    app: App,
    tui: Option<Tui<Backend>>,
    height: Size,
    reader: Reader,
    reader_done: bool,
    initial_cmd: String,
    reader_control: Option<ReaderControl>,
    matcher_interval: Option<tokio::time::Interval>,
    #[cfg(feature = "listen")]
    listener: Option<interprocess::local_socket::tokio::Listener>,
    final_event: Event,
    final_key: KeyEvent,
    /// Whether the `start` event has already been fired (fired exactly once).
    start_fired: bool,
}

impl Skim {
    /// Run skim, collecting items from the source and using options
    ///
    /// # Params
    ///
    /// - options: the "complex" options that control how skim behaves
    /// - source: a stream of items to be passed to skim for filtering.
    ///   If None is given, skim will invoke the command given to fetch the items.
    ///
    /// # Returns
    ///
    /// - None: on internal errors.
    /// - `SkimOutput`: the collected key, event, query, selected items, etc.
    ///
    /// # Errors
    ///
    /// Returns an error if skim initialization or the TUI loop fails.
    ///
    /// # Panics
    ///
    /// Panics if the tui fails to initialize
    pub fn run_with(options: SkimOptions, source: Option<SkimItemReceiver>) -> Result<SkimOutput> {
        trace!("running skim");
        let mut skim = Self::init(options, source)?;

        skim.start();

        if skim.should_enter() {
            skim.init_tui()?;
            let task = async {
                skim.enter().await?;
                skim.run().await?;
                eyre::Ok(())
            };

            if let Ok(handle) = Handle::try_current() {
                block_in_place(|| handle.block_on(task))?;
            } else {
                let rt = tokio::runtime::Runtime::new()?;
                rt.block_on(task)?;
            }
        } else {
            // We didn't enter
            skim.final_event = Event::Action(Action::Accept(None));
        }
        let output = skim.output();
        debug!("output: {output:?}");

        Ok(output)
    }

    /// Run skim with a Vec of items
    ///
    /// # Errors
    ///
    /// Returns an error if sending items to the channel or running skim fails.
    ///
    /// ```no_run
    /// use skim::prelude::*;
    ///
    /// let opts = SkimOptionsBuilder::default().multi(true).build().unwrap();
    /// Skim::run_items(opts, ["foo", "bar"]);
    /// ```
    pub fn run_items<I, T>(options: SkimOptions, items: I) -> Result<SkimOutput>
    where
        I: IntoIterator<Item = T>,
        T: SkimItem,
    {
        const BATCH_SIZE: usize = 1024;
        let (tx, rx) = crate::prelude::unbounded();
        let mut batch: Vec<Arc<dyn SkimItem>> = Vec::with_capacity(BATCH_SIZE);
        for item in items {
            if batch.len() == 1024 {
                tx.send(batch)?;
                batch = Vec::with_capacity(BATCH_SIZE);
            }
            batch.push(Arc::new(item) as Arc<dyn SkimItem>);
        }
        tx.send(batch)?;
        Self::run_with(options, Some(rx))
    }

    /// Initialize the TUI with the default crossterm backend, but do not enter it yet
    ///
    /// # Errors
    ///
    /// Returns an error if the TUI backend cannot be initialized.
    pub fn init_tui(&mut self) -> Result<()> {
        let mut tui = Tui::new_with_height(self.height)?;
        if self.app.options.no_mouse {
            tui.disable_mouse();
        }
        self.tui = Some(tui);
        Ok(())
    }
}

impl<Backend: ratatui::backend::Backend + 'static> Skim<Backend>
where
    Backend::Error: Send + Sync + 'static,
{
    /// Initialize skim, without starting anything yet
    ///
    /// # Errors
    ///
    /// Returns an error if parsing the height or other options fails.
    pub fn init(options: SkimOptions, source: Option<SkimItemReceiver>) -> Result<Self> {
        let height = Size::try_from(options.height.as_str())?;

        // application state
        // Initialize theme from options
        let theme = Arc::new(crate::theme::ColorTheme::init_from_options(&options));
        let mut reader = Reader::from_options(&options).source(source);
        let cmd = options.cmd.clone().unwrap_or_default();

        let app = App::from_options(options, theme.clone(), cmd.clone());

        // Give the reader its own dedicated pool (⌈N/3⌉ threads) so it never
        // competes with the matcher's pool (⌊2N/3⌋ threads) for the same
        // worker threads.
        reader.set_thread_pool(Arc::clone(&app.reader_pool));

        //------------------------------------------------------------------------------
        // reader
        // In interactive mode, expand all placeholders ({}, {q}, etc) with initial query (empty or from --query)
        let initial_cmd = if app.options.interactive && app.options.cmd.is_some() {
            let expanded = app.expand_cmd(&cmd, true);
            log::debug!("Interactive mode: initial_cmd = {expanded:?} (from template {cmd:?})");
            expanded
        } else {
            cmd.clone()
        };
        Ok(Self {
            app,
            height,
            reader,
            reader_done: false,
            initial_cmd,
            tui: None,
            reader_control: None,
            matcher_interval: None,
            #[cfg(feature = "listen")]
            listener: None,
            final_event: Event::Quit,
            final_key: KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
            start_fired: false,
        })
    }

    /// Start the reader and matcher, but do not enter the TUI yet
    pub fn start(&mut self) {
        debug!("Starting reader with initial_cmd: {:?}", self.initial_cmd);
        self.reader_control = Some(self.reader.collect(self.app.item_pool.clone(), &self.initial_cmd));
        self.app.restart_matcher(true);
        // If the TUI is already available (e.g. test harnesses that build the
        // TUI before starting), fire the `start` event now. In the normal
        // binary flow the TUI is created after `start()`, so `enter()` fires it.
        self.fire_start_event();
    }

    /// Fire the `start` event exactly once, as soon as the TUI event channel is
    /// available. The event is routed through the keymap like any other key, so
    /// a `--bind start:<action>` binding runs when skim comes up. In sync mode,
    /// render the completed matcher output first so the action sees every item.
    fn fire_start_event(&mut self) {
        if self.start_fired {
            return;
        }
        if let Some(tui) = self.tui.as_ref() {
            if self.app.options.sync && tui.event_tx.try_send(Event::Render).is_err() {
                return;
            }
            if tui.event_tx.try_send(Event::Key(SkimEvent::Start.into())).is_ok() {
                self.start_fired = true;
            }
        }
    }

    /// Handle a reload event by killing the current reader, clearing items, and starting a new reader.
    ///
    /// This encapsulates the reload logic from the main event loop so it can
    /// be reused by test harnesses without reimplementing it.
    pub fn handle_reload(&mut self, new_cmd: &str) {
        debug!("reloading with cmd {new_cmd}");
        // Kill the current reader
        if let Some(rc) = self.reader_control.as_mut() {
            rc.kill();
        }
        // Clear items
        self.app.item_pool.clear();
        // Clear displayed items unless no_clear_if_empty is set
        if !self.app.options.no_clear_if_empty {
            self.app.item_list.clear();
        }
        self.app.restart_matcher(true);
        // Start a new reader with the new command
        self.reader_control = Some(self.reader.collect(self.app.item_pool.clone(), new_cmd));
        self.reader_done = false;
        // A new read is in flight: arm the `load` event to fire again once the
        // new item set has been read and rendered.
        self.app.reader_done = false;
        self.app.load_event_fired = false;
    }

    /// Check if the reader has finished and restart the matcher if needed.
    ///
    /// This encapsulates the reader-status check from the main event loop
    /// so it can be reused by test harnesses.
    ///
    /// Returns `true` if the reader has completed.
    pub fn check_reader(&mut self) -> bool {
        if self
            .reader_control
            .as_ref()
            .is_some_and(super::reader::ReaderControl::is_done)
            && !self.reader_done
        {
            self.reader_done = true;
            // Signal that reading is complete. The `load` event is fired later
            // from `App::poll_completion_events` (the heartbeat handler) once the
            // reader is done, the matcher has stopped, and every item has been
            // consumed, so a `load` binding sees a fully-populated, stable list.
            self.app.reader_done = true;
            self.app.restart_matcher(false);
            // If the matcher already consumed everything, stop the periodic
            // interval immediately rather than waiting for the next tick.
            if self.app.matcher_control.stopped() && self.app.item_pool.num_not_taken() == 0 {
                trace!("matcher interval stopped in check_reader: all items consumed");
                self.matcher_interval = None;
            }
            true
        } else {
            false
        }
    }

    /// Returns `true` if the reader is done (has finished producing items).
    pub fn reader_done(&self) -> bool {
        self.reader_done
            && self
                .reader_control
                .as_ref()
                .is_none_or(super::reader::ReaderControl::is_done)
    }

    /// Returns `true` if the matcher is stopped
    pub fn matcher_stopped(&self) -> bool {
        self.app.matcher_control.stopped()
    }

    /// Initialize the TUI with a caller-provided instance.
    ///
    /// Use this instead of [`init_tui()`](Skim::init_tui) when you need a
    /// non-default backend (e.g. `TestBackend` for snapshot tests).
    pub fn init_tui_with(&mut self, tui: Tui<Backend>) {
        self.tui = Some(tui);
    }

    /// Returns a shared reference to the application state.
    pub fn app(&self) -> &App {
        &self.app
    }

    /// Returns a mutable reference to the application state.
    pub fn app_mut(&mut self) -> &mut App {
        &mut self.app
    }

    /// Returns a shared reference to the TUI.
    ///
    /// # Panics
    ///
    /// Panics if the TUI has not been initialized yet.
    pub fn tui_ref(&self) -> &Tui<Backend> {
        self.tui.as_ref().expect("TUI needs to be initialized before access")
    }

    /// Returns a mutable reference to the TUI.
    ///
    /// # Panics
    ///
    /// Panics if the TUI has not been initialized yet.
    pub fn tui_mut(&mut self) -> &mut Tui<Backend> {
        self.tui.as_mut().expect("TUI needs to be initialized before access")
    }

    /// Returns mutable references to both the app and the TUI simultaneously.
    ///
    /// This is useful when you need to call `app.handle_event(tui, ...)` or
    /// `tui.draw(|frame| frame.render_widget(app, ...))`, which require
    /// disjoint mutable borrows of both fields.
    ///
    /// # Panics
    ///
    /// Panics if the TUI has not been initialized yet.
    pub fn app_and_tui(&mut self) -> (&mut App, &mut Tui<Backend>) {
        (
            &mut self.app,
            self.tui.as_mut().expect("TUI needs to be initialized before access"),
        )
    }

    /// Returns a shared reference to the final event that caused skim to quit.
    pub fn final_event(&self) -> &Event {
        &self.final_event
    }

    /// Returns a clone of the TUI event sender.
    ///
    /// Use this to send events (e.g. [`Event::Render`], [`Event::Action`])
    /// to the running skim instance from outside the event loop. The sender
    /// is cheap to clone and can be moved into async blocks or other tasks.
    ///
    /// Must be called after [`init_tui()`](Skim::init_tui).
    ///
    /// # Panics
    ///
    /// Panics if `init_tui` has not been called before this method.
    pub fn event_sender(&self) -> tokio::sync::mpsc::Sender<Event> {
        self.tui
            .as_ref()
            .expect("TUI needs to be initialized using Skim::init_tui before getting the event sender")
            .event_tx
            .clone()
    }

    /// Enter the TUI
    ///
    /// # Errors
    ///
    /// Returns an error if the TUI cannot be entered or the input listener fails.
    ///
    /// # Panics
    ///
    /// Panics if `init_tui` has not been called before this method.
    ///
    /// # Async
    ///
    /// This will call `init_listener`, which requires a tokio runtime
    /// Though it is not technically async code, this is a good hint.
    #[allow(clippy::unused_async, unknown_lints, clippy::unused_async_trait_impl)]
    pub async fn enter(&mut self) -> Result<()> {
        debug!("Entering TUI");
        let tui = self
            .tui
            .as_mut()
            .expect("TUI needs to be initialized using Skim::init_tui before entering");

        tui.enter_terminal()?;
        #[cfg(feature = "image")]
        if self.app.options.image == Some(crate::options::ImageProtocol::Detect) {
            if !tui.is_fullscreen {
                crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?;
            }
            let picker = detect_image_picker().unwrap_or_else(|err| {
                warn!("failed to query terminal image protocol: {err:?}");
                Picker::halfblocks()
            });
            if !tui.is_fullscreen {
                crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)?;
            }
            self.app.options.image_picker = Some(picker.clone());
            self.app.preview.set_image_picker(Some(picker));
        } else if self.app.options.image == Some(crate::options::ImageProtocol::Halfblocks) {
            let picker = Picker::halfblocks();
            self.app.options.image_picker = Some(picker.clone());
            self.app.preview.set_image_picker(Some(picker));
        }

        self.init_listener()?;
        self.tui
            .as_mut()
            .expect("TUI needs to be initialized using Skim::init_tui before starting")
            .start();
        // In the normal binary flow the TUI is created after `start()`, so this
        // is the first point at which the `start` event can be queued.
        self.fire_start_event();
        Ok(())
    }

    /// Checks read-0 select-1, filter, and sync to wait and returns whether or not we should enter
    ///
    /// # Panics
    ///
    /// Panics if `start` has not been called before this method.
    pub fn should_enter(&mut self) -> bool {
        let reader_control = self
            .reader_control
            .as_ref()
            .expect("reader_control needs to be initialized using Skim::start");
        let app = &mut self.app;

        // Filter mode: wait for all items to be read and matched, then return without entering TUI
        if app.options.filter.is_some() {
            trace!("filter mode: waiting for all items to be processed");
            loop {
                // `--min-query-length` short-circuits `restart_matcher`, so the pool would
                // never be drained and this loop would spin forever. There is nothing to
                // match in that case: stop as soon as the reader is done.
                if app.query_below_min_length() {
                    if reader_control.is_done() {
                        debug!("filter mode: query shorter than --min-query-length, no results");
                        app.item_list.items.clear();
                        return false;
                    }
                    std::thread::sleep(Duration::from_millis(1));
                    continue;
                }
                let matcher_stopped = app.matcher_control.stopped();
                let reader_done = reader_control.is_done();
                if matcher_stopped && reader_done && app.item_pool.num_not_taken() == 0 {
                    break;
                }
                std::thread::sleep(Duration::from_millis(1));
                app.restart_matcher(false);
            }
            app.item_list.items = app
                .item_list
                .processed_items
                .lock()
                .take()
                .unwrap_or_default()
                .items
                .into_iter()
                .filter(|i| !i.item.disabled())
                .collect();
            debug!("filter mode: matched {} items", app.item_list.items.len());
            return false;
        }

        // Deal with read-0 / select-1
        let min_items_before_enter = if app.options.exit_0 {
            1
        } else if app.options.select_1 {
            2
        } else if app.options.sync {
            usize::MAX
        } else {
            0
        };
        if min_items_before_enter > 0 || app.options.sync {
            trace!(
                "checking matcher, stopped: {}, processed: {}, matched: {}/{}, pool: {}, query: {}, reader_control_done: {}",
                app.matcher_control.stopped(),
                app.matcher_control.get_num_processed(),
                app.matcher_control.get_num_matched(),
                min_items_before_enter,
                app.item_pool.num_not_taken(),
                app.input.value,
                reader_control.is_done()
            );
            while app.matcher_control.get_num_matched() < min_items_before_enter
                && (!app.matcher_control.stopped() || !reader_control.is_done())
            {
                trace!("still waiting");
                std::thread::sleep(Duration::from_millis(1));
                app.restart_matcher(false);
            }
            trace!(
                "checked matcher, stopped: {}, processed: {}, pool: {}, query: {}, reader_control_done: {}",
                app.matcher_control.stopped(),
                app.matcher_control.get_num_processed(),
                app.item_pool.num_not_taken(),
                app.input.value,
                reader_control.is_done()
            );
            trace!(
                "checking for matched item count before entering: {}/{min_items_before_enter}",
                app.matcher_control.get_num_matched()
            );
            if app.matcher_control.get_num_matched() == min_items_before_enter - 1 {
                app.item_list.items = app.item_list.processed_items.lock().take().unwrap_or_default().items;
                debug!("early exit, result: {:?}", app.results());
                return false;
            }
        }
        true
    }

    /// Initialize the IPC socket listener
    /// This needs to be called from an async context despite being sync
    #[cfg_attr(not(feature = "listen"), allow(clippy::unnecessary_wraps, clippy::unused_self))]
    fn init_listener(&mut self) -> Result<()> {
        #[cfg(feature = "listen")]
        if let Some(socket_name) = &self.app.options.listen {
            self.listener = Some(
                interprocess::local_socket::ListenerOptions::new()
                    .name(interprocess::local_socket::ToNsName::to_ns_name::<
                        interprocess::local_socket::GenericNamespaced,
                    >(socket_name.to_owned())?)
                    .create_tokio()?,
            );
        }
        Ok(())
    }

    /// Capture `self` and extract the output
    /// This will perform cleanup
    ///
    /// # Panics
    ///
    /// Panics if the selected items or current item cannot be retrieved.
    pub fn output(mut self) -> SkimOutput {
        if let Some(mut rc) = self.reader_control.take() {
            rc.kill();
        }

        // Extract final_key and is_abort from final_event
        let is_abort = !matches!(&self.final_event, Event::Action(Action::Accept(_)));

        let selected_items = self.app.results();

        let cmd = if self.app.options.interactive {
            self.app.input.to_string()
        } else if self.app.options.cmd_query.is_some() {
            self.app.options.cmd_query.clone().unwrap()
        } else {
            self.initial_cmd.clone()
        };
        let query = self.app.input.to_string();
        let current = self.app.item_list.selected();
        let header = self.app.header.header.clone();
        let final_event = self.final_event.clone();
        let final_key = self.final_key;

        drop(self);

        SkimOutput {
            final_event,
            is_abort,
            final_key,
            query,
            cmd,
            selected_items,
            current,
            header,
        }
    }

    /// Returns true if skim has finished (the user accepted or aborted)
    pub fn should_quit(&self) -> bool {
        self.app.should_quit
    }

    /// If `needs_render` has been set (e.g. by the matcher thread), immediately
    /// send a `Render` event to the TUI so the screen updates without waiting
    /// for the next heartbeat tick.  Respects the 30 FPS frame-rate cap.
    fn try_flush_render(&mut self) {
        use std::sync::atomic::Ordering;
        if self.app.needs_render.load(Ordering::Relaxed)
            && self.app.last_render_timer.elapsed().as_millis() > 1000 / u128::from(TICK_RATE)
        {
            self.app.needs_render.store(false, Ordering::Relaxed);
            self.app.last_render_timer = std::time::Instant::now();
            if let Some(tui) = self.tui.as_ref() {
                let _ = tui.event_tx.try_send(Event::Render);
            }
        }
    }

    /// Process a single event loop iteration.
    ///
    /// This awaits the next event from the TUI, matcher, or IPC listener,
    /// processes it, and returns. Use this in your own event loop when you
    /// need fine-grained control over the application lifecycle.
    ///
    /// Returns `Ok(true)` if skim should quit, `Ok(false)` to continue.
    ///
    /// # Errors
    ///
    /// Returns an error if the TUI cannot produce the next event or if event handling fails.
    ///
    /// # Panics
    ///
    /// Panics if `init_tui` has not been called before this method.
    ///
    /// # Example
    ///
    /// ```ignore
    /// while !skim.tick().await? {
    ///     // do your own work between ticks
    /// }
    /// ```
    pub async fn tick(&mut self) -> Result<bool> {
        // Retry the one-shot `start` event until the (bounded) event channel
        // accepts it. `start()`/`enter()` fire it eagerly, but if the channel was
        // momentarily full there, this guarantees it is not lost. Idempotent: the
        // `start_fired` guard makes every call after the first a no-op.
        self.fire_start_event();
        let matcher_interval = &mut self.matcher_interval;
        let items_available = self.app.item_pool.items_available.clone();
        select! {
            event = self.tui.as_mut().expect("TUI should be initialized before the event loop can start").next() => {
                let evt = event.ok_or_eyre("Could not acquire next event")?;

                if let Event::Key(k) = &evt {
                  self.final_key.clone_from(k);
                } else {
                  self.final_event = evt.clone();
                }


                // Handle reload event separately
                if let Event::Reload(new_cmd) = &evt {
                    self.handle_reload(&new_cmd.clone());
                } else {
                    self.app.handle_event(self.tui.as_mut().expect("TUI should be initialized before handling events"), &evt)?;
                }

                if let Some(action) = self.app.final_action.take() {
                    self.final_event = Event::Action(action);
                }

                // Check reader status and update
                self.check_reader();
            }
            () = async {
                match matcher_interval {
                    Some(interval) => { interval.tick().await; },
                    None => std::future::pending::<()>().await,
                }
            } => {
              // Check for a pending debounced restart (e.g. the user typed
              // while a match-all was running).  Checking here (every 10ms)
              // rather than only on the heartbeat (83ms) eliminates most of
              // the latency between query change and matcher restart.
              if self.app.pending_matcher_restart {
                  self.app.restart_matcher(true);
              } else {
                  self.app.restart_matcher(false);
              }
              // Check if the matcher (or reader) has set the render flag and
              // flush a render event immediately instead of waiting for the
              // next heartbeat tick.  This can shave up to ~80ms of latency
              // when the heartbeat runs at 12 Hz.
              self.try_flush_render();
              // Once the reader has finished and the matcher has consumed all
              // items, stop the periodic interval — it can only produce empty
              // ticks from this point.  The `items_available` Notify branch
              // still handles the (rare) case of late-arriving items.
              if self.reader_done
                  && self.app.matcher_control.stopped()
                  && self.app.item_pool.num_not_taken() == 0
              {
                  trace!("matcher interval stopped: reader done, matcher idle, no pending items");
                  self.matcher_interval = None;
              }
            }
            // Wake immediately when new items arrive in the pool so the matcher
            // can pick them up without waiting for the next periodic interval.
            () = items_available.notified() => {
                self.app.restart_matcher(false);
                self.try_flush_render();
            }
            // IPC listener branch. `tokio::select!` does not allow `#[cfg]` on a
            // branch, so the branch stays but its body is gated: with the `listen`
            // feature off the future is a never-resolving `pending()` and the handler
            // is unreachable (`RemoteStream` is uninhabited).
            Ok(stream) = async {
                #[cfg(feature = "listen")]
                if let Some(l) = self.listener.as_ref() {
                    return interprocess::local_socket::traits::tokio::Listener::accept(l).await;
                }
                std::future::pending::<std::io::Result<RemoteStream>>().await
            } => {
                #[cfg(feature = "listen")]
                {
                    debug!("Listener accepted a connection");
                    let event_tx_clone_ipc = self.tui.as_ref().expect("TUI should be initialized before listening").event_tx.clone();
                    tokio::spawn(async move {
                        use tokio::io::AsyncBufReadExt;
                        let reader = tokio::io::BufReader::new(stream);
                        let mut lines = reader.lines();
                        while let Ok(Some(line)) = lines.next_line().await {
                            debug!("listener: got {line}");
                            if let Ok(act) = ron::from_str::<Action>(&line) {
                                debug!("listener: parsed into action {act:?}");
                                if let Err(e) = event_tx_clone_ipc.try_send(Event::Action(act)) {
                                    warn!("listener: failed to send action to backend: {e:?}");
                                }
                            }
                        }
                    });
                }
                #[cfg(not(feature = "listen"))]
                match stream {}
            }
        }

        Ok(self.app.should_quit)
    }

    /// Run the event loop on the current task until skim quits.
    ///
    /// This is a convenience wrapper around [`tick()`](Self::tick) that loops
    /// until the user accepts or aborts. Use `tick()` directly if you need
    /// to interleave your own logic between iterations.
    ///
    /// # Errors
    ///
    /// Returns an error if any tick in the event loop fails.
    pub async fn run(&mut self) -> Result<()> {
        self.matcher_interval = Some(tokio::time::interval(Duration::from_millis(10)));
        trace!("Starting event loop");
        loop {
            if self.tick().await? {
                break Ok(());
            }
        }
    }

    /// Spawn the event loop and run a user-provided future concurrently.
    ///
    /// This consumes `self`, spawns the event loop as a local task, and runs
    /// `user_task` alongside it. When the user accepts or aborts in the TUI,
    /// the event loop completes and the [`SkimOutput`] is returned — regardless
    /// of whether `user_task` has finished.
    ///
    /// Use this when you need to send items or do other work concurrently
    /// while the TUI is running.
    ///
    /// # Errors
    ///
    /// Returns an error if the event loop or the task join fails.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let output = skim.run_until(async {
    ///     for i in 1..=10 {
    ///         tx.send(vec![Arc::new(format!("item {i}"))]);
    ///         tokio::time::sleep(Duration::from_millis(100)).await;
    ///     }
    /// }).await?;
    /// ```
    pub async fn run_until<F: Future + 'static>(mut self, user_task: F) -> Result<SkimOutput> {
        let local = tokio::task::LocalSet::new();
        local
            .run_until(async {
                let handle = tokio::task::spawn_local(async move {
                    self.run().await?;
                    Ok(self.output())
                });
                tokio::task::spawn_local(user_task);
                handle.await?
            })
            .await
    }
}

#[cfg(test)]
#[path = "skim_tests.rs"]
mod tests;
