add sync action

This commit is contained in:
cool-mist 2025-09-09 00:08:24 +05:30
parent 31501d65d8
commit 23f912d845
8 changed files with 132 additions and 100 deletions

View File

@ -50,7 +50,7 @@ pub fn add_channel(ctx: &TrsEnv, args: &AddChannelArgs) -> Result<RssChannelD, T
.ignore_invalid_encoding_declarations(true) .ignore_invalid_encoding_declarations(true)
.create_reader(&bytes[..]); .create_reader(&bytes[..]);
let channel = parser::parse_rss_channel(xml_source_stream)?; let channel = parser::parse_rss_channel(xml_source_stream)?;
ctx.db.add_channel(&channel) ctx.db.add_channel(&args.link, &channel)
} }
pub fn list_channels(ctx: &TrsEnv, args: &ListChannelArgs) -> Result<Vec<RssChannelD>, TrsError> { pub fn list_channels(ctx: &TrsEnv, args: &ListChannelArgs) -> Result<Vec<RssChannelD>, TrsError> {

View File

@ -11,7 +11,7 @@ pub mod ui;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let args = argh::from_env::<TrsArgs>(); let args = argh::from_env::<TrsArgs>();
let db_name = "test3"; let db_name = "test4";
match args.sub_command { match args.sub_command {
TrsSubCommand::AddChannel(args) => { TrsSubCommand::AddChannel(args) => {
let mut ctx = TrsEnv::new(db_name)?; let mut ctx = TrsEnv::new(db_name)?;

View File

@ -12,6 +12,7 @@ const SCHEMA_CHANNELS: &'static str = "CREATE TABLE IF NOT EXISTS Channels ( \
id INTEGER PRIMARY KEY, \ id INTEGER PRIMARY KEY, \
name TEXT NOT NULL, \ name TEXT NOT NULL, \
link TEXT NOT NULL UNIQUE, \ link TEXT NOT NULL UNIQUE, \
feed_link TEXT NOT NULL UNIQUE, \
description TEXT, \ description TEXT, \
last_update INTEGER\ last_update INTEGER\
)"; )";
@ -28,14 +29,14 @@ const SCHEMA_ARTICLES: &'static str = "CREATE TABLE IF NOT EXISTS Articles ( \
FOREIGN KEY(channel_id) REFERENCES Channels(id) ON DELETE CASCADE \ FOREIGN KEY(channel_id) REFERENCES Channels(id) ON DELETE CASCADE \
)"; )";
const ADD_CHANNEL: &'static str = "INSERT INTO Channels (name, link, description, last_update) \ const ADD_CHANNEL: &'static str = "INSERT INTO Channels (name, link, feed_link, description, last_update) \
VALUES (?1, ?2, ?3, ?4)\ VALUES (?1, ?2, ?3, ?4, ?5)\
ON CONFLICT(link) DO UPDATE SET name=?1, description=?3, last_update=?4"; ON CONFLICT(link) DO UPDATE SET name=?1, description=?4, last_update=?5";
const REMOVE_CHANNEL: &'static str = "DELETE FROM Channels WHERE id = ?1"; const REMOVE_CHANNEL: &'static str = "DELETE FROM Channels WHERE id = ?1";
const LIST_CHANNELS: &'static str = const LIST_CHANNELS: &'static str =
"SELECT id, name, link, description, last_update FROM Channels LIMIT ?1"; "SELECT id, name, link, feed_link, description, last_update FROM Channels order by last_update DESC LIMIT ?1";
const GET_CHANNEL: &'static str = const GET_CHANNEL: &'static str =
"SELECT id, name, link, description, last_update FROM Channels WHERE link = ?1"; "SELECT id, name, link, feed_link, description, last_update FROM Channels WHERE link = ?1";
const ADD_ARTICLE: &'static str = const ADD_ARTICLE: &'static str =
"INSERT INTO Articles (channel_id, title, description, link, pub_date, last_update, unread) \ "INSERT INTO Articles (channel_id, title, description, link, pub_date, last_update, unread) \
@ -49,7 +50,7 @@ const GET_ARTICLE: &'static str =
"SELECT id, channel_id, title, description, link, pub_date, last_update, unread FROM Articles WHERE link = ?1"; "SELECT id, channel_id, title, description, link, pub_date, last_update, unread FROM Articles WHERE link = ?1";
const LIST_ARTICLES: &'static str = const LIST_ARTICLES: &'static str =
"SELECT id, channel_id, title, description, link, pub_date, last_update, unread FROM Articles"; "SELECT id, channel_id, title, description, link, pub_date, last_update, unread FROM Articles order by last_update DESC";
const MARK_ARTICLE_READ: &'static str = "UPDATE Articles SET unread = FALSE WHERE id = ?1"; const MARK_ARTICLE_READ: &'static str = "UPDATE Articles SET unread = FALSE WHERE id = ?1";
@ -63,6 +64,7 @@ pub struct RssChannelD {
pub id: i64, pub id: i64,
pub title: String, pub title: String,
pub link: String, pub link: String,
pub feed_link: String,
pub description: String, pub description: String,
pub last_update: OffsetDateTime, pub last_update: OffsetDateTime,
pub articles: Vec<RssArticleD>, pub articles: Vec<RssArticleD>,
@ -126,13 +128,14 @@ impl Db {
Ok(channel) Ok(channel)
} }
pub fn add_channel(&self, channel: &RssChannel) -> Result<RssChannelD> { pub fn add_channel(&self, feed_link: impl AsRef<str>, channel: &RssChannel) -> Result<RssChannelD> {
self.connection self.connection
.execute( .execute(
ADD_CHANNEL, ADD_CHANNEL,
( (
&channel.title, &channel.title,
&channel.link, &channel.link,
feed_link.as_ref(),
&channel.description, &channel.description,
OffsetDateTime::now_utc().unix_timestamp(), OffsetDateTime::now_utc().unix_timestamp(),
), ),
@ -256,7 +259,8 @@ impl Db {
row.get(1)?, row.get(1)?,
row.get(2)?, row.get(2)?,
row.get(3)?, row.get(3)?,
Db::read_datetime(4, &row)?, row.get(4)?,
Db::read_datetime(5, &row)?,
Vec::new(), Vec::new(),
)) ))
} }
@ -295,6 +299,7 @@ impl RssChannelD {
id: i64, id: i64,
title: String, title: String,
link: String, link: String,
feed_link: String,
description: String, description: String,
last_update: OffsetDateTime, last_update: OffsetDateTime,
articles: Vec<RssArticleD>, articles: Vec<RssArticleD>,
@ -303,6 +308,7 @@ impl RssChannelD {
id, id,
title, title,
link, link,
feed_link,
description, description,
last_update, last_update,
articles, articles,

View File

@ -1,9 +1,9 @@
pub mod actions; pub mod actions;
pub mod articles; pub mod articles;
pub mod backend;
pub mod channels; pub mod channels;
pub mod controls; pub mod controls;
pub mod debug; pub mod debug;
pub mod executor;
pub mod title; pub mod title;
use std::{ use std::{
@ -22,7 +22,6 @@ use channels::ChannelsWidget;
use controls::ControlsWidget; use controls::ControlsWidget;
use crossterm::event::{self, KeyEventKind}; use crossterm::event::{self, KeyEventKind};
use debug::DebugWidget; use debug::DebugWidget;
use executor::UiCommandExecutor;
use futures::{FutureExt, StreamExt}; use futures::{FutureExt, StreamExt};
use ratatui::{ use ratatui::{
prelude::*, prelude::*,
@ -66,6 +65,7 @@ pub enum UiAction {
ShowAddChannelUi, ShowAddChannelUi,
RemoveChannel, RemoveChannel,
ToggleReadStatus, ToggleReadStatus,
SyncChannel,
Exit, Exit,
} }
@ -86,11 +86,24 @@ pub enum UiCommandDispatchActions {
ListChannels(args::ListChannelArgs), ListChannels(args::ListChannelArgs),
} }
/// APP
/// - Listen Event
/// - Publish UiCommandDispatchActions
///
/// BACKEND
/// - Listen UiCommandDispatchActions
/// - Publish BackendEvent
///
/// EVENT LOOP
/// - Listen BackendEvent
/// - Listen crossterm::event::Event
/// - Publish Event
pub async fn ui(args: &UiArgs, db_name: &str) -> Result<()> { pub async fn ui(args: &UiArgs, db_name: &str) -> Result<()> {
let (app_dispatch, app_recv) = channel(); let (ui_action_publisher, ui_action_receiver) = channel();
let (executor_dispatch, executor_recv) = tokio::sync::mpsc::unbounded_channel(); let (backend_event_publisher, backend_event_receiver) = tokio::sync::mpsc::unbounded_channel();
let event_recv = start_event_loop(executor_recv); let event_receiver = start_event_loop(backend_event_receiver);
let mut terminal = ratatui::init();
let mut app_state = AppState { let mut app_state = AppState {
channels: Vec::new(), channels: Vec::new(),
exit: false, exit: false,
@ -102,15 +115,11 @@ pub async fn ui(args: &UiArgs, db_name: &str) -> Result<()> {
last_action: None, last_action: None,
show_add_channel_ui: false, show_add_channel_ui: false,
add_channel: String::new(), add_channel: String::new(),
dispatcher: app_dispatch, dispatcher: ui_action_publisher,
receiver: event_recv, receiver: event_receiver,
}; };
let db_name = db_name.to_string(); start_backend(db_name, ui_action_receiver, backend_event_publisher);
std::thread::spawn(move || {
let mut executor = UiCommandExecutor::new(app_recv, executor_dispatch);
executor.run(db_name);
});
app_state app_state
.dispatcher .dispatcher
@ -119,6 +128,7 @@ pub async fn ui(args: &UiArgs, db_name: &str) -> Result<()> {
)) ))
.map_err(|e| TrsError::Error(format!("Unable to send initial app: {}", e)))?; .map_err(|e| TrsError::Error(format!("Unable to send initial app: {}", e)))?;
let mut terminal = ratatui::init();
loop { loop {
draw(&app_state, &mut terminal)?; draw(&app_state, &mut terminal)?;
handle_events(&mut app_state).await?; handle_events(&mut app_state).await?;
@ -132,6 +142,13 @@ pub async fn ui(args: &UiArgs, db_name: &str) -> Result<()> {
Ok(()) Ok(())
} }
fn start_backend(db_name: &str, app_recv: std::sync::mpsc::Receiver<UiCommandDispatchActions>, executor_dispatch: tokio::sync::mpsc::UnboundedSender<BackendEvent>) {
let db_name = db_name.to_string();
std::thread::spawn(move || {
backend::start(db_name, app_recv, executor_dispatch);
});
}
fn start_event_loop( fn start_event_loop(
mut executor_recv: UnboundedReceiver<BackendEvent>, mut executor_recv: UnboundedReceiver<BackendEvent>,
) -> UnboundedReceiver<Event> { ) -> UnboundedReceiver<Event> {

View File

@ -82,6 +82,18 @@ pub fn handle_action(
} }
} }
} }
UiAction::SyncChannel => {
let channel = get_highlighted_channel(app_state);
if let Some(channel) = channel {
let sync_channel_args = args::AddChannelArgs {
link: channel.feed_link.clone(),
};
app_state
.dispatcher
.send(UiCommandDispatchActions::AddChannel(sync_channel_args))
.unwrap();
}
}
}; };
Ok(()) Ok(())
} }

66
src/ui/backend.rs Normal file
View File

@ -0,0 +1,66 @@
use std::sync::mpsc::Receiver;
use tokio::sync::mpsc::UnboundedSender;
use crate::{commands::TrsEnv, ui::BackendEvent};
use super::UiCommandDispatchActions;
// This one will have to run on the same thread as this manages the sqlite connection
pub fn start(
db_name: String,
cmd_recv: Receiver<UiCommandDispatchActions>,
backend_dispatch: UnboundedSender<BackendEvent>,
) -> () {
let ctx = TrsEnv::new(db_name.as_str()).unwrap();
loop {
let action = cmd_recv.recv();
let Ok(action) = action else {
break;
};
match action {
UiCommandDispatchActions::AddChannel(args) => {
if let Ok(_) = crate::commands::add_channel(&ctx, &args) {
send_new_state_default(&ctx, &backend_dispatch);
};
}
UiCommandDispatchActions::RemoveChannel(args) => {
if let Ok(_) = crate::commands::remove_channel(&ctx, &args) {
send_new_state_default(&ctx, &backend_dispatch);
}
}
UiCommandDispatchActions::MarkArticleRead(args) => {
if let Ok(_) = crate::commands::mark_read(&ctx, &args) {
send_new_state_default(&ctx, &backend_dispatch);
}
}
UiCommandDispatchActions::ListChannels(args) => {
send_new_state(&ctx, args, &backend_dispatch);
}
}
}
}
fn send_new_state_default(
ctx: &crate::commands::TrsEnv,
dispatcher: &UnboundedSender<BackendEvent>,
) {
send_new_state(
ctx,
crate::args::ListChannelArgs { limit: None },
dispatcher,
);
}
fn send_new_state(
ctx: &crate::commands::TrsEnv,
args: crate::args::ListChannelArgs,
dispatcher: &UnboundedSender<BackendEvent>,
) {
if let Ok(channels) = crate::commands::list_channels(ctx, &args) {
dispatcher
.send(BackendEvent::ReloadState(channels))
.unwrap_or_default();
}
}

View File

@ -57,11 +57,12 @@ pub fn parse_ui_action(raw_event: Event) -> UiAction {
KeyCode::Char('h') => UiAction::FocusPaneLeft, KeyCode::Char('h') => UiAction::FocusPaneLeft,
KeyCode::Char('k') => UiAction::FocusEntryUp, KeyCode::Char('k') => UiAction::FocusEntryUp,
KeyCode::Char('j') => UiAction::FocusEntryDown, KeyCode::Char('j') => UiAction::FocusEntryDown,
KeyCode::Char('d') => UiAction::ToggleDebug, KeyCode::Char('n') => UiAction::ToggleDebug,
KeyCode::Char('q') => UiAction::Exit, KeyCode::Char('q') => UiAction::Exit,
KeyCode::Char('a') => UiAction::ShowAddChannelUi, KeyCode::Char('a') => UiAction::ShowAddChannelUi,
KeyCode::Char('r') => UiAction::RemoveChannel, KeyCode::Char('d') => UiAction::RemoveChannel,
KeyCode::Char('u') => UiAction::ToggleReadStatus, KeyCode::Char('r') => UiAction::ToggleReadStatus,
KeyCode::Char('s') => UiAction::SyncChannel,
KeyCode::Enter => UiAction::OpenArticle, KeyCode::Enter => UiAction::OpenArticle,
_ => UiAction::None, _ => UiAction::None,
}; };
@ -97,9 +98,11 @@ impl Widget for ControlsWidget {
description!(" to switch between channels and articles, "), description!(" to switch between channels and articles, "),
control!("a"), control!("a"),
description!(" add a new RSS channel, "), description!(" add a new RSS channel, "),
control!("s"),
description!(" sync channel, "),
control!("d"),
description!(" delete an RSS channel, "),
control!("r"), control!("r"),
description!(" remove an RSS channel, "),
control!("u"),
description!(" toggle read state of article, "), description!(" toggle read state of article, "),
control!("q"), control!("q"),
description!(" to exit"), description!(" to exit"),

View File

@ -1,72 +0,0 @@
use std::sync::mpsc::Receiver;
use tokio::sync::mpsc::UnboundedSender;
use crate::{commands::TrsEnv, ui::BackendEvent};
use super::UiCommandDispatchActions;
pub struct UiCommandExecutor {
pub app_recv: Receiver<UiCommandDispatchActions>,
pub executor_dispatch: UnboundedSender<BackendEvent>,
}
impl UiCommandExecutor {
pub fn new(
app_recv: Receiver<UiCommandDispatchActions>,
executor_dispatch: UnboundedSender<BackendEvent>,
) -> Self {
UiCommandExecutor {
app_recv,
executor_dispatch,
}
}
// This one will have to run on the same thread as this manages the sqlite connection
pub fn run(&mut self, db_name: String) -> () {
let ctx = TrsEnv::new(db_name.as_str()).unwrap();
loop {
let action = self.app_recv.recv();
let Ok(action) = action else {
break;
};
match action {
UiCommandDispatchActions::AddChannel(args) => {
if let Ok(_) = crate::commands::add_channel(&ctx, &args) {
self.send_new_state_default(&ctx);
};
}
UiCommandDispatchActions::RemoveChannel(args) => {
if let Ok(_) = crate::commands::remove_channel(&ctx, &args) {
self.send_new_state_default(&ctx);
}
}
UiCommandDispatchActions::MarkArticleRead(args) => {
if let Ok(_) = crate::commands::mark_read(&ctx, &args) {
self.send_new_state_default(&ctx);
}
}
UiCommandDispatchActions::ListChannels(args) => {
self.send_new_state(&ctx, args);
}
}
}
}
fn send_new_state_default(&mut self, ctx: &crate::commands::TrsEnv) {
self.send_new_state(ctx, crate::args::ListChannelArgs { limit: None });
}
fn send_new_state(
&mut self,
ctx: &crate::commands::TrsEnv,
args: crate::args::ListChannelArgs,
) {
if let Ok(channels) = crate::commands::list_channels(ctx, &args) {
self.executor_dispatch
.send(BackendEvent::ReloadState(channels))
.unwrap_or_default();
}
}
}