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 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 handlers::poll_bg_messages(app, &mut rx);
103
104 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}