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)
.create_reader(&bytes[..]);
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> {

View File

@ -11,7 +11,7 @@ pub mod ui;
#[tokio::main]
async fn main() -> Result<()> {
let args = argh::from_env::<TrsArgs>();
let db_name = "test3";
let db_name = "test4";
match args.sub_command {
TrsSubCommand::AddChannel(args) => {
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, \
name TEXT NOT NULL, \
link TEXT NOT NULL UNIQUE, \
feed_link TEXT NOT NULL UNIQUE, \
description TEXT, \
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 \
)";
const ADD_CHANNEL: &'static str = "INSERT INTO Channels (name, link, description, last_update) \
VALUES (?1, ?2, ?3, ?4)\
ON CONFLICT(link) DO UPDATE SET name=?1, description=?3, last_update=?4";
const ADD_CHANNEL: &'static str = "INSERT INTO Channels (name, link, feed_link, description, last_update) \
VALUES (?1, ?2, ?3, ?4, ?5)\
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 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 =
"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 =
"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";
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";
@ -63,6 +64,7 @@ pub struct RssChannelD {
pub id: i64,
pub title: String,
pub link: String,
pub feed_link: String,
pub description: String,
pub last_update: OffsetDateTime,
pub articles: Vec<RssArticleD>,
@ -126,13 +128,14 @@ impl Db {
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
.execute(
ADD_CHANNEL,
(
&channel.title,
&channel.link,
feed_link.as_ref(),
&channel.description,
OffsetDateTime::now_utc().unix_timestamp(),
),
@ -256,7 +259,8 @@ impl Db {
row.get(1)?,
row.get(2)?,
row.get(3)?,
Db::read_datetime(4, &row)?,
row.get(4)?,
Db::read_datetime(5, &row)?,
Vec::new(),
))
}
@ -295,6 +299,7 @@ impl RssChannelD {
id: i64,
title: String,
link: String,
feed_link: String,
description: String,
last_update: OffsetDateTime,
articles: Vec<RssArticleD>,
@ -303,6 +308,7 @@ impl RssChannelD {
id,
title,
link,
feed_link,
description,
last_update,
articles,

View File

@ -1,9 +1,9 @@
pub mod actions;
pub mod articles;
pub mod backend;
pub mod channels;
pub mod controls;
pub mod debug;
pub mod executor;
pub mod title;
use std::{
@ -22,7 +22,6 @@ use channels::ChannelsWidget;
use controls::ControlsWidget;
use crossterm::event::{self, KeyEventKind};
use debug::DebugWidget;
use executor::UiCommandExecutor;
use futures::{FutureExt, StreamExt};
use ratatui::{
prelude::*,
@ -66,6 +65,7 @@ pub enum UiAction {
ShowAddChannelUi,
RemoveChannel,
ToggleReadStatus,
SyncChannel,
Exit,
}
@ -86,11 +86,24 @@ pub enum UiCommandDispatchActions {
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<()> {
let (app_dispatch, app_recv) = channel();
let (executor_dispatch, executor_recv) = tokio::sync::mpsc::unbounded_channel();
let event_recv = start_event_loop(executor_recv);
let mut terminal = ratatui::init();
let (ui_action_publisher, ui_action_receiver) = channel();
let (backend_event_publisher, backend_event_receiver) = tokio::sync::mpsc::unbounded_channel();
let event_receiver = start_event_loop(backend_event_receiver);
let mut app_state = AppState {
channels: Vec::new(),
exit: false,
@ -102,15 +115,11 @@ pub async fn ui(args: &UiArgs, db_name: &str) -> Result<()> {
last_action: None,
show_add_channel_ui: false,
add_channel: String::new(),
dispatcher: app_dispatch,
receiver: event_recv,
dispatcher: ui_action_publisher,
receiver: event_receiver,
};
let db_name = db_name.to_string();
std::thread::spawn(move || {
let mut executor = UiCommandExecutor::new(app_recv, executor_dispatch);
executor.run(db_name);
});
start_backend(db_name, ui_action_receiver, backend_event_publisher);
app_state
.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)))?;
let mut terminal = ratatui::init();
loop {
draw(&app_state, &mut terminal)?;
handle_events(&mut app_state).await?;
@ -132,6 +142,13 @@ pub async fn ui(args: &UiArgs, db_name: &str) -> Result<()> {
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(
mut executor_recv: UnboundedReceiver<BackendEvent>,
) -> 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(())
}

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