Skip to main content

feed/tui/
mod.rs

1pub mod action;
2pub mod app;
3mod handlers;
4pub mod keybindings;
5pub mod ui;
6
7use std::io;
8use std::time::Duration;
9
10use anyhow::Result;
11use crossterm::{
12    event::{
13        self, DisableMouseCapture, EnableMouseCapture, Event, KeyboardEnhancementFlags,
14        PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
15    },
16    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
17    ExecutableCommand, QueueableCommand,
18};
19use ratatui::prelude::*;
20use tokio::sync::mpsc;
21
22use crate::article_store::{ArticleStore, FilterParams};
23use app::App;
24
25enum BgMessage {
26    FetchComplete(Vec<crate::article::Article>),
27    ArticleContent {
28        url: String,
29        title: String,
30        content: String,
31    },
32}
33
34pub async fn run(store: ArticleStore, filter_params: FilterParams) -> Result<()> {
35    enable_raw_mode()?;
36    let mut stdout = io::stdout();
37    stdout.execute(EnterAlternateScreen)?;
38    stdout.execute(EnableMouseCapture)?;
39
40    let supports_enhancement = matches!(
41        crossterm::terminal::supports_keyboard_enhancement(),
42        Ok(true)
43    );
44    if supports_enhancement {
45        stdout.queue(PushKeyboardEnhancementFlags(
46            KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
47                | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES,
48        ))?;
49    }
50
51    let mut terminal = Terminal::new(CrosstermBackend::new(stdout))?;
52
53    let mut app = App::new(store, filter_params);
54
55    let (tx, rx) = mpsc::unbounded_channel();
56
57    // Background refresh on startup
58    spawn_background_fetch(&app, tx.clone());
59    app.status_message = Some(" Updating...".to_string());
60    app.reset_refresh_timer();
61
62    let result = event_loop(&mut terminal, &mut app, tx, rx).await;
63
64    if supports_enhancement {
65        io::stdout().execute(PopKeyboardEnhancementFlags)?;
66    }
67    io::stdout().execute(DisableMouseCapture)?;
68    disable_raw_mode()?;
69    io::stdout().execute(LeaveAlternateScreen)?;
70
71    result
72}
73
74fn spawn_background_fetch(app: &App, tx: mpsc::UnboundedSender<BgMessage>) {
75    let client = app.store.client().clone();
76    let feeds = app.store.feeds().to_vec();
77    let config = app.store.config().clone();
78    let data_dir = app.store.data_dir().to_path_buf();
79
80    tokio::spawn(async move {
81        let mut temp_store = ArticleStore::with_client(feeds, config, data_dir, client);
82        temp_store.fetch(false).await;
83        let articles = temp_store.take_articles();
84        let _ = tx.send(BgMessage::FetchComplete(articles));
85    });
86}
87
88async fn event_loop(
89    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
90    app: &mut App,
91    tx: mpsc::UnboundedSender<BgMessage>,
92    mut rx: mpsc::UnboundedReceiver<BgMessage>,
93) -> Result<()> {
94    let mut pending_articles: Option<Vec<crate::article::Article>> = None;
95    let mut last_click: Option<(std::time::Instant, usize)> = None;
96
97    loop {
98        terminal.draw(|frame| ui::render(frame, app))?;
99
100        let has_event = event::poll(Duration::from_millis(50))?;
101
102        // Check background messages
103        handlers::poll_bg_messages(app, &mut rx, &mut pending_articles);
104
105        // Auto-refresh check
106        if app.should_auto_refresh() {
107            app.loading = true;
108            app.status_message = Some(" Auto-refreshing...".to_string());
109            app.reset_refresh_timer();
110            spawn_background_fetch(app, tx.clone());
111        }
112
113        if !has_event {
114            continue;
115        }
116
117        let event = event::read()?;
118
119        if let Event::Key(key) = &event {
120            let size = terminal.size()?;
121            let width = size.width as usize;
122            let height = size.height as usize;
123            if handlers::handle_key_event(app, key, width, height, &mut pending_articles, &tx) {
124                break;
125            }
126        }
127
128        if let Event::Mouse(mouse) = &event {
129            let size = terminal.size()?;
130            let width = size.width as usize;
131            let height = size.height as usize;
132            handlers::handle_mouse_event(
133                app,
134                mouse,
135                width,
136                height,
137                &mut pending_articles,
138                &mut last_click,
139                &tx,
140            );
141        }
142    }
143    Ok(())
144}