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 last_click: Option<(std::time::Instant, usize)> = None;
95
96    loop {
97        terminal.draw(|frame| ui::render(frame, app))?;
98
99        let has_event = event::poll(Duration::from_millis(50))?;
100
101        // Check background messages
102        handlers::poll_bg_messages(app, &mut rx);
103
104        // Auto-refresh check
105        if app.should_auto_refresh() {
106            app.loading = true;
107            app.status_message = Some(" Auto-refreshing...".to_string());
108            app.reset_refresh_timer();
109            spawn_background_fetch(app, tx.clone());
110        }
111
112        if !has_event {
113            continue;
114        }
115
116        let event = event::read()?;
117
118        if let Event::Key(key) = &event {
119            let size = terminal.size()?;
120            let width = size.width as usize;
121            let height = size.height as usize;
122            if handlers::handle_key_event(app, key, width, height, &tx) {
123                break;
124            }
125        }
126
127        if let Event::Mouse(mouse) = &event {
128            let size = terminal.size()?;
129            let width = size.width as usize;
130            let height = size.height as usize;
131            handlers::handle_mouse_event(app, mouse, width, height, &mut last_click, &tx);
132        }
133    }
134    Ok(())
135}