From 2a82e3bc66ddb73a91ccaa53ba91133ef97dad2d Mon Sep 17 00:00:00 2001 From: cool-mist Date: Sun, 3 Aug 2025 20:18:12 +0530 Subject: [PATCH] add/remove channel, toggle read status --- src/commands.rs | 25 ++++++---- src/main.rs | 4 +- src/persistence.rs | 49 +++++++++++++------ src/ui.rs | 67 +++++++++++++++++++++++--- src/ui/actions.rs | 116 +++++++++++++++++++++++++++++++++++++++++---- src/ui/articles.rs | 20 ++++---- src/ui/channels.rs | 47 ++++++++++++++++-- src/ui/controls.rs | 34 ++++++++++++- src/ui/debug.rs | 30 +++++++----- src/ui/executor.rs | 49 +++++++++++++++++++ 10 files changed, 371 insertions(+), 70 deletions(-) create mode 100644 src/ui/executor.rs diff --git a/src/commands.rs b/src/commands.rs index a180ba4..bb929c6 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,10 +6,18 @@ use crate::{ }; pub struct TrsEnv { + name: String, db: Db, http_client: reqwest::blocking::Client, } +impl Clone for TrsEnv { + fn clone(&self) -> Self { + let name = self.name.clone(); + TrsEnv::new(&name).expect("Failed to clone TrsEnv") + } +} + impl TrsEnv { pub fn new(instance_name: &str) -> Result { let db = Db::create(instance_name)?; @@ -17,11 +25,15 @@ impl TrsEnv { .user_agent("cool-mist/trs") .build() .map_err(|e| TrsError::ReqwestError(e, "Failed to create HTTP client".to_string()))?; - Ok(TrsEnv { db, http_client }) + Ok(TrsEnv { + name: instance_name.to_string(), + db, + http_client, + }) } } -pub fn add_channel(ctx: &mut TrsEnv, args: &AddChannelArgs) -> Result { +pub fn add_channel(ctx: &TrsEnv, args: &AddChannelArgs) -> Result { let rss = ctx.http_client.get(&args.link).send().map_err(|e| { TrsError::ReqwestError( e, @@ -41,18 +53,15 @@ pub fn add_channel(ctx: &mut TrsEnv, args: &AddChannelArgs) -> Result Result, TrsError> { +pub fn list_channels(ctx: &TrsEnv, args: &ListChannelArgs) -> Result, TrsError> { ctx.db.list_channels(args.limit.unwrap_or(u32::MAX)) } -pub fn remove_channel(ctx: &mut TrsEnv, args: &RemoveChannelArgs) -> Result<(), TrsError> { +pub fn remove_channel(ctx: &TrsEnv, args: &RemoveChannelArgs) -> Result<(), TrsError> { ctx.db.remove_channel(args.id).map(|_| ()) } -pub fn mark_read(ctx: &mut TrsEnv, args: &args::MarkReadArgs) -> Result<(), TrsError> { +pub fn mark_read(ctx: &TrsEnv, args: &args::MarkReadArgs) -> Result<(), TrsError> { match args.unread { true => ctx.db.mark_article_unread(args.id as i64)?, false => ctx.db.mark_article_read(args.id as i64)?, diff --git a/src/main.rs b/src/main.rs index 8a4b5df..4d7dbb4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ pub mod ui; fn main() -> Result<()> { let args = argh::from_env::(); - let mut ctx = TrsEnv::new("test")?; + let mut ctx = TrsEnv::new("test3")?; match args.sub_command { TrsSubCommand::AddChannel(args) => { commands::add_channel(&mut ctx, &args)?; @@ -50,6 +50,6 @@ fn main() -> Result<()> { Ok(()) } TrsSubCommand::MarkRead(args) => commands::mark_read(&mut ctx, &args), - TrsSubCommand::Ui(args) => ui::ui(&mut ctx, &args), + TrsSubCommand::Ui(args) => ui::ui(ctx, &args), } } diff --git a/src/persistence.rs b/src/persistence.rs index 7732ab7..7e897bc 100644 --- a/src/persistence.rs +++ b/src/persistence.rs @@ -13,7 +13,7 @@ const SCHEMA_CHANNELS: &'static str = "CREATE TABLE IF NOT EXISTS Channels ( \ name TEXT NOT NULL, \ link TEXT NOT NULL UNIQUE, \ description TEXT, \ - last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP \ + last_update INTEGER\ )"; const SCHEMA_ARTICLES: &'static str = "CREATE TABLE IF NOT EXISTS Articles ( \ @@ -22,15 +22,15 @@ const SCHEMA_ARTICLES: &'static str = "CREATE TABLE IF NOT EXISTS Articles ( \ title TEXT NOT NULL, \ description TEXT, \ link TEXT NOT NULL UNIQUE, \ - pub_date TIMESTAMP, \ - last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP, \ - unread BOOLEAN DEFAULT FALSE, \ + pub_date INTEGER, \ + last_update INTEGER , \ + unread BOOLEAN DEFAULT TRUE, \ FOREIGN KEY(channel_id) REFERENCES Channels(id) ON DELETE CASCADE \ )"; const ADD_CHANNEL: &'static str = "INSERT INTO Channels (name, link, description, last_update) \ - VALUES (?1, ?2, ?3, CURRENT_TIMESTAMP)\ - ON CONFLICT(link) DO UPDATE SET name=?1, description=?3, last_update=CURRENT_TIMESTAMP"; + VALUES (?1, ?2, ?3, ?4)\ + ON CONFLICT(link) DO UPDATE SET name=?1, description=?3, last_update=?4"; const REMOVE_CHANNEL: &'static str = "DELETE FROM Channels WHERE id = ?1"; const LIST_CHANNELS: &'static str = "SELECT id, name, link, description, last_update FROM Channels LIMIT ?1"; @@ -38,9 +38,9 @@ const GET_CHANNEL: &'static str = "SELECT id, name, link, description, last_update FROM Channels WHERE link = ?1"; const ADD_ARTICLE: &'static str = - "INSERT INTO Articles (channel_id, title, description, link, pub_date, last_update) \ - VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP) \ - ON CONFLICT(link) DO UPDATE SET pub_date=?5, last_update=CURRENT_TIMESTAMP"; + "INSERT INTO Articles (channel_id, title, description, link, pub_date, last_update, unread) \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6, true) \ + ON CONFLICT(link) DO UPDATE SET last_update=?6"; const GET_ARTICLES_BY_CHANNEL: &'static str = "SELECT id, channel_id, title, description, link, pub_date, last_update, unread FROM Articles WHERE channel_id = ?1"; @@ -130,7 +130,12 @@ impl Db { self.connection .execute( ADD_CHANNEL, - (&channel.title, &channel.link, &channel.description), + ( + &channel.title, + &channel.link, + &channel.description, + OffsetDateTime::now_utc().unix_timestamp(), + ), ) .map_err(|e| TrsError::SqlError(e, "Failed to add channel".to_string()))?; @@ -201,7 +206,8 @@ impl Db { &article.title, &article.description, &article.link, - article.date.map(|d| d.to_string()), + article.date.map(|d| d.unix_timestamp()), + OffsetDateTime::now_utc().unix_timestamp(), ), ) .map_err(|e| TrsError::SqlError(e, "Failed to add article".to_string()))?; @@ -250,7 +256,7 @@ impl Db { row.get(1)?, row.get(2)?, row.get(3)?, - row.get(4)?, + Db::read_datetime(4, &row)?, Vec::new(), )) } @@ -262,11 +268,26 @@ impl Db { title: row.get(2)?, description: row.get(3)?, link: row.get(4)?, - pub_date: row.get(5).ok(), - last_update: row.get(6).ok(), + pub_date: Db::read_datetime(5, row).ok(), + last_update: Db::read_datetime(6, row).ok(), unread: row.get(7)?, }) } + + fn read_datetime( + idx: usize, + row: &rusqlite::Row, + ) -> std::result::Result { + row.get::(idx).map(|ts| { + OffsetDateTime::from_unix_timestamp(ts).map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + idx, + rusqlite::types::Type::Integer, + Box::new(e), + ) + }) + })? + } } impl RssChannelD { diff --git a/src/ui.rs b/src/ui.rs index 760af30..99399ff 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -3,12 +3,17 @@ pub mod articles; pub mod channels; pub mod controls; pub mod debug; +pub mod executor; pub mod title; -use std::io::Stdout; +use std::{ + io::Stdout, + sync::mpsc::{channel, Receiver, Sender}, + thread, +}; use crate::{ - args::{ListChannelArgs, UiArgs}, + args::{self, ListChannelArgs, UiArgs}, commands::{self, TrsEnv}, error::{Result, TrsError}, persistence::RssChannelD, @@ -18,6 +23,7 @@ use channels::ChannelsWidget; use controls::ControlsWidget; use crossterm::event; use debug::DebugWidget; +use executor::UiCommandExecutor; use ratatui::{ prelude::*, widgets::{Block, Borders}, @@ -33,6 +39,10 @@ pub struct AppState { highlighted_channel: Option, highlighted_article: Option, last_action: Option, + show_add_channel_ui: bool, + add_channel: String, + dispatcher: Sender, + receiver: Receiver, } #[derive(Clone, Copy, PartialEq)] @@ -52,10 +62,31 @@ pub enum UiAction { FocusEntryDown, ToggleDebug, OpenArticle, + ShowAddChannelUi, + RemoveChannel, + ToggleReadStatus, Exit, } -pub fn ui(ctx: &mut TrsEnv, args: &UiArgs) -> Result<()> { +#[derive(Debug, Clone, PartialEq)] +pub enum PopupUiAction { + None, + Submit, + AddChar(char), + Backspace, + Close, +} + +#[derive(Debug)] +pub enum UiCommandDispatchActions { + AddChannel(args::AddChannelArgs), + RemoveChannel(args::RemoveChannelArgs), + MarkArticleRead(args::MarkReadArgs), +} + +pub fn ui(ctx: TrsEnv, args: &UiArgs) -> Result<()> { + let (tdispatch, rdispatch) = channel(); + let (tupdate, rupdate) = channel(); let mut terminal = ratatui::init(); let mut app_state = AppState { channels: Vec::new(), @@ -66,19 +97,31 @@ pub fn ui(ctx: &mut TrsEnv, args: &UiArgs) -> Result<()> { highlighted_article: None, highlighted_channel: None, last_action: None, + show_add_channel_ui: false, + add_channel: String::new(), + dispatcher: tdispatch, + receiver: rupdate, }; - let channels = commands::list_channels(ctx, &ListChannelArgs { limit: None })?; + let ctx_cloned = ctx.clone(); + let executor = UiCommandExecutor::new(rdispatch, tupdate); + let executor_handle = thread::spawn(move || { + executor.run(ctx_cloned); + }); + + let channels = commands::list_channels(&ctx, &ListChannelArgs { limit: None })?; app_state.channels = channels; loop { draw(&app_state, &mut terminal)?; - handle_events(&mut app_state)?; + handle_events(&mut app_state, &ctx)?; if app_state.exit { break; } } + drop(app_state); + executor_handle.join().unwrap(); ratatui::restore(); Ok(()) } @@ -93,8 +136,20 @@ fn draw(app_state: &AppState, terminal: &mut Terminal>) Ok(()) } -fn handle_events(state: &mut AppState) -> Result<()> { +fn handle_events(state: &mut AppState, ctx: &TrsEnv) -> Result<()> { + let recv_action = state.receiver.try_recv(); + + if let Ok(_) = recv_action { + let channels = commands::list_channels(&ctx, &ListChannelArgs { limit: None })?; + state.channels = channels; + } + let raw_event = event::read().map_err(|e| TrsError::TuiError(e))?; + if state.show_add_channel_ui { + let event = controls::parse_popup_ui_action(raw_event); + return actions::handle_popup_action(state, event); + } + let event = controls::parse_ui_action(raw_event); state.last_action = Some(event.clone()); actions::handle_action(state, event) diff --git a/src/ui/actions.rs b/src/ui/actions.rs index a6a3473..93be2a6 100644 --- a/src/ui/actions.rs +++ b/src/ui/actions.rs @@ -1,6 +1,10 @@ -use crate::{error::TrsError, persistence::RssChannelD}; +use crate::{ + args, + error::TrsError, + persistence::{RssArticleD, RssChannelD}, +}; -use super::{AppState, FocussedPane, UiAction}; +use super::{AppState, FocussedPane, PopupUiAction, UiAction, UiCommandDispatchActions}; pub fn handle_action( app_state: &mut AppState, @@ -19,21 +23,100 @@ pub fn handle_action( UiAction::OpenArticle => { if let Some(channel_idx) = app_state.highlighted_channel { if let Some(article_idx) = app_state.highlighted_article { - if let Some(channel) = app_state.channels.get(channel_idx) { - if let Some(article) = channel.articles.get(article_idx) { - let open_res = open::that(&article.link); - if let Err(e) = open_res { - eprintln!("Failed to open article: {}", e); - } + if let Some(channel) = app_state.channels.get_mut(channel_idx) { + if let Some(article) = channel.articles.get_mut(article_idx) { + article.unread = false; + app_state + .dispatcher + .send(UiCommandDispatchActions::MarkArticleRead( + args::MarkReadArgs { + id: article.id as u32, + unread: false, + }, + )) + .unwrap(); + _ = open::that(&article.link); } } } } } + UiAction::ShowAddChannelUi => { + app_state.show_add_channel_ui = true; + } + UiAction::RemoveChannel => { + let hi_channel = get_highlighted_channel(app_state); + let Some(channel) = hi_channel else { + return Ok(()); + }; + + let remove_channel_args = args::RemoveChannelArgs { + id: channel.id as u32, + }; + + app_state + .dispatcher + .send(UiCommandDispatchActions::RemoveChannel(remove_channel_args)) + .unwrap(); + } + UiAction::ToggleReadStatus => { + let article = get_highlighted_article_mut(app_state); + let mut article_id = None; + let mut unread = None; + if let Some(article) = article { + article.unread = !article.unread; + article_id = Some(article.id); + unread = Some(article.unread); + } + + if let Some(article_id) = article_id { + if let Some(unread) = unread { + app_state + .dispatcher + .send(UiCommandDispatchActions::MarkArticleRead( + args::MarkReadArgs { + id: article_id as u32, + unread, + }, + )) + .unwrap(); + } + } + } }; Ok(()) } +pub fn handle_popup_action( + state: &mut AppState, + event: PopupUiAction, +) -> std::result::Result<(), TrsError> { + match event { + PopupUiAction::None => {} + PopupUiAction::Submit => { + let add_channel_args = args::AddChannelArgs { + link: state.add_channel.clone(), + }; + state + .dispatcher + .send(UiCommandDispatchActions::AddChannel(add_channel_args)) + .unwrap(); + state.show_add_channel_ui = false; + } + PopupUiAction::AddChar(c) => { + state.add_channel.push(c); + } + PopupUiAction::Backspace => { + state.add_channel.pop(); + } + PopupUiAction::Close => { + state.show_add_channel_ui = false; + } + }; + + Ok(()) +} + fn saturating_add(num: usize, to_add: usize, max: usize) -> usize { if num + to_add > max { max @@ -60,6 +143,18 @@ fn get_highlighted_channel<'a>(app_state: &'a AppState) -> Option<&'a RssChannel .and_then(|idx| app_state.channels.get(idx).or_else(|| None)) } +fn get_highlighted_channel_mut<'a>(app_state: &'a mut AppState) -> Option<&'a mut RssChannelD> { + app_state + .highlighted_channel + .and_then(|idx| app_state.channels.get_mut(idx).or_else(|| None)) +} + +fn get_highlighted_article_mut<'a>(app_state: &'a mut AppState) -> Option<&'a mut RssArticleD> { + let hi_article = app_state.highlighted_article?; + let channel = get_highlighted_channel_mut(app_state)?; + channel.articles.get_mut(hi_article) +} + fn focus_entry_up(app_state: &mut AppState) { match app_state.focussed { FocussedPane::Channels => decrement_highlighted_channel_idx(app_state), @@ -77,12 +172,13 @@ fn focus_entry_down(app_state: &mut AppState) { } fn increment_highlighted_channel_idx(app_state: &mut AppState) -> Option { - let max_channel_idx = app_state.channels.len().saturating_sub(1); - if max_channel_idx == 0 { + let channels_len = app_state.channels.len(); + if channels_len == 0 { app_state.highlighted_channel = None; return Some(false); } + let max_channel_idx = channels_len.saturating_sub(1); app_state.highlighted_channel = app_state .highlighted_channel .map(|idx| saturating_add(idx, 1, max_channel_idx)) diff --git a/src/ui/articles.rs b/src/ui/articles.rs index 0372706..4ec7d57 100644 --- a/src/ui/articles.rs +++ b/src/ui/articles.rs @@ -58,22 +58,20 @@ impl<'a> Widget for ArticlesWidget<'a> { .filter(|h| *h == idx) .is_some(); let mut lines = Vec::new(); - let id = Span::styled( - format!("{:>3}. ", idx + 1), - get_article_id_style(current_highlighted), - ); + let id = if article.unread { + Span::styled(" * ", Style::default().fg(Color::Red)) + } else { + Span::styled( + format!("{:>3}. ", idx + 1), + get_article_id_style(current_highlighted), + ) + }; let title = Span::styled( article.title.clone(), get_article_title_style(current_highlighted), ); - let unread = if article.unread { - Span::styled(" (unread)", Style::default().fg(Color::Red)) - } else { - Span::raw("") - }; - - lines.push(Line::from(vec![id, title, unread])); + lines.push(Line::from(vec![id, title])); let para = Paragraph::new(lines) .block(Block::default()) .style(get_channel_list_item_block_style(current_highlighted)) diff --git a/src/ui/channels.rs b/src/ui/channels.rs index 5933b74..c7de066 100644 --- a/src/ui/channels.rs +++ b/src/ui/channels.rs @@ -5,6 +5,7 @@ use ratatui::{ text::{Line, Span}, widgets::{Block, Paragraph, Widget}, }; +use time::format_description; use super::AppState; @@ -12,6 +13,10 @@ pub struct ChannelsWidget<'a> { state: &'a AppState, } +pub struct AddChannelWidget<'a> { + state: &'a str, +} + impl<'a> ChannelsWidget<'a> { pub fn new(state: &'a AppState) -> Self { Self { state } @@ -44,36 +49,50 @@ impl<'a> Widget for ChannelsWidget<'a> { .highlighted_channel .filter(|h| *h == idx) .is_some(); - let mut lines = Vec::new(); + let mut spans = Vec::new(); let id = Span::styled( format!("{:>3}. ", idx + 1), get_channel_id_style(current_highlighted), ); + spans.push(id); let title = Span::styled( channel.title.clone(), get_channel_title_style(current_highlighted), ); + spans.push(title); - lines.push(Line::from(vec![id, title])); + let format = format_description::parse("[year]-[month]-[day]").unwrap(); if let Some(article) = channel.articles.first() { let pub_date_text = match article.pub_date { - Some(date) => format!("Last update: {}", date), + Some(date) => format!(" {}", date.format(&format).unwrap()), None => "".to_string(), }; let pub_date = Span::styled( pub_date_text, get_channel_pub_date_style(current_highlighted), ); - lines.push(Line::from(vec![pub_date])); + spans.push(pub_date); } - let para = Paragraph::new(lines) + let para = Paragraph::new(Line::from(spans)) .block(Block::default()) .style(get_channel_list_item_block_style(current_highlighted)) .alignment(Alignment::Left); para.render(row, buf); } + + if self.state.show_add_channel_ui { + let add_channel_area = Layout::default() + .direction(Direction::Vertical) + .constraints(Constraint::from_percentages(vec![90, 10])) + .split(area) + .to_vec(); + AddChannelWidget { + state: &self.state.add_channel, + } + .render(add_channel_area[1], buf); + } } } @@ -116,3 +135,21 @@ fn get_channel_title_style(highlighted: bool) -> Style { Style::default() } } + +impl<'a> Widget for AddChannelWidget<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let block = Block::default() + .title_top(Line::from("Add Channel").centered()) + .title_style( + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD | Modifier::ITALIC | Modifier::UNDERLINED), + ) + .borders(ratatui::widgets::Borders::ALL) + .border_style(Style::default().fg(Color::DarkGray)); + + let para = Paragraph::new(Line::from(self.state)).block(block); + + para.render(area, buf); + } +} diff --git a/src/ui/controls.rs b/src/ui/controls.rs index 21ade21..340cbd7 100644 --- a/src/ui/controls.rs +++ b/src/ui/controls.rs @@ -7,10 +7,33 @@ use ratatui::{ widgets::{Block, Borders, Paragraph, Widget}, }; -use super::UiAction; +use super::{PopupUiAction, UiAction}; pub struct ControlsWidget; +pub fn parse_popup_ui_action(raw_event: Event) -> PopupUiAction { + match raw_event { + Event::Key(key_event) => { + if key_event.kind != KeyEventKind::Press { + return PopupUiAction::None; + } + + if key_event.modifiers != KeyModifiers::NONE { + return PopupUiAction::None; + } + + return match key_event.code { + KeyCode::Backspace => PopupUiAction::Backspace, + KeyCode::Char(c) => PopupUiAction::AddChar(c), + KeyCode::Enter => PopupUiAction::Submit, + KeyCode::Esc => PopupUiAction::Close, + _ => PopupUiAction::None, + }; + } + _ => PopupUiAction::None, + } +} + pub fn parse_ui_action(raw_event: Event) -> UiAction { match raw_event { Event::Key(key_event) => { @@ -36,6 +59,9 @@ pub fn parse_ui_action(raw_event: Event) -> UiAction { KeyCode::Char('j') => UiAction::FocusEntryDown, KeyCode::Char('d') => UiAction::ToggleDebug, KeyCode::Char('q') => UiAction::Exit, + KeyCode::Char('a') => UiAction::ShowAddChannelUi, + KeyCode::Char('r') => UiAction::RemoveChannel, + KeyCode::Char('u') => UiAction::ToggleReadStatus, KeyCode::Enter => UiAction::OpenArticle, _ => UiAction::None, }; @@ -69,6 +95,12 @@ impl Widget for ControlsWidget { description!(" to navigate up/down, "), control!("h/l"), description!(" to switch between channels and articles, "), + control!("a"), + description!(" add a new RSS channel, "), + control!("r"), + description!(" remove an RSS channel, "), + control!("u"), + description!(" toggle read state of article, "), control!("q"), description!(" to exit"), ]) diff --git a/src/ui/debug.rs b/src/ui/debug.rs index 7cd8ba1..ae5ea9e 100644 --- a/src/ui/debug.rs +++ b/src/ui/debug.rs @@ -23,27 +23,31 @@ impl<'a> Widget for DebugWidget<'a> { lines.push(format!("channels: {}", self.state.channels.len())); lines.push(format!("highlighted: {:?}", self.state.highlighted_channel)); if let Some(h) = self.state.highlighted_channel { - for channel in &self.state.channels { - if channel.id as usize == h { - lines.push(format!( - "highlighted channel: ({},{},{},{:?})", - channel.id, channel.title, channel.link, channel.last_update - )); + let hi_channel = self.state.channels.get(h); + if let Some(channel) = hi_channel { + lines.push(format!( + "highlighted channel: ({}, {:?}, {})", + channel.id, channel.last_update, channel.title, + )); - if let Some(article) = channel.articles.first() { + if let Some(article) = channel.articles.first() { + lines.push(format!( + "first article: ({} ,{:?}, {})", + article.id, article.last_update, article.title, + )); + } + if let Some(h) = self.state.highlighted_article { + let hi_article = channel.articles.get(h); + if let Some(article) = hi_article { lines.push(format!( - "first article: ({},{},{},{:?})", - article.id, article.title, article.link, article.last_update + "highlighted article: ({}, {:?}, {}, unread={})", + article.id, article.last_update, article.title, article.unread, )); } } } } - if let Some(h) = self.state.highlighted_article { - lines.push(format!("highlighted article: {}", h)); - } - let line_areas = Layout::default() .direction(Direction::Vertical) .constraints( diff --git a/src/ui/executor.rs b/src/ui/executor.rs new file mode 100644 index 0000000..80cd11c --- /dev/null +++ b/src/ui/executor.rs @@ -0,0 +1,49 @@ +use std::sync::mpsc::{Receiver, Sender}; + +use super::UiCommandDispatchActions; + +pub struct UiCommandExecutor { + pub command_receiver: Receiver, + pub status_sender: Sender, +} + +impl UiCommandExecutor { + pub fn new( + command_receiver: Receiver, + status_sender: Sender, + ) -> Self { + UiCommandExecutor { + command_receiver, + status_sender, + } + } + + pub fn run(&self, ctx: crate::commands::TrsEnv) -> () { + loop { + let action = self.command_receiver.recv(); + let Ok(action) = action else { + break; + }; + + match action { + UiCommandDispatchActions::AddChannel(args) => { + if let Ok(_) = crate::commands::add_channel(&ctx, &args) { + self.status_sender.send(1).unwrap_or_default(); + }; + } + + UiCommandDispatchActions::RemoveChannel(args) => { + if let Ok(_) = crate::commands::remove_channel(&ctx, &args) { + self.status_sender.send(1).unwrap_or_default(); + } + } + + UiCommandDispatchActions::MarkArticleRead(args) => { + if let Ok(_) = crate::commands::mark_read(&ctx, &args) { + self.status_sender.send(1).unwrap_or_default(); + } + } + } + } + } +}