fix architecture

This commit is contained in:
cool-mist 2025-09-01 22:39:27 +05:30
parent 7605dcf3b6
commit 5ea14dc226
5 changed files with 228 additions and 68 deletions

93
Cargo.lock generated
View File

@ -201,6 +201,7 @@ dependencies = [
"crossterm_winapi", "crossterm_winapi",
"derive_more", "derive_more",
"document-features", "document-features",
"futures-core",
"mio", "mio",
"parking_lot", "parking_lot",
"rustix 1.0.7", "rustix 1.0.7",
@ -388,6 +389,21 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.31" version = "0.3.31"
@ -404,12 +420,34 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.31" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.31" version = "0.3.31"
@ -428,8 +466,10 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io", "futures-io",
"futures-macro",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
"memchr", "memchr",
@ -622,7 +662,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.5.10",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
@ -772,6 +812,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "io-uring"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
dependencies = [
"bitflags",
"cfg-if",
"libc",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.11.0" version = "2.11.0"
@ -1446,6 +1497,16 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "socket2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.0" version = "1.2.0"
@ -1600,17 +1661,31 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.45.1" version = "1.47.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
"io-uring",
"libc", "libc",
"mio", "mio",
"pin-project-lite", "pin-project-lite",
"socket2", "slab",
"windows-sys 0.52.0", "socket2 0.6.0",
"tokio-macros",
"windows-sys 0.59.0",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]
@ -1635,13 +1710,14 @@ dependencies = [
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.15" version = "0.7.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"futures-util",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
] ]
@ -1716,11 +1792,14 @@ version = "0.1.0"
dependencies = [ dependencies = [
"argh", "argh",
"crossterm 0.29.0", "crossterm 0.29.0",
"futures",
"open", "open",
"ratatui", "ratatui",
"reqwest", "reqwest",
"rusqlite", "rusqlite",
"time", "time",
"tokio",
"tokio-util",
"xml-rs", "xml-rs",
] ]

View File

@ -5,12 +5,15 @@ edition = "2021"
[dependencies] [dependencies]
argh = "0.1.13" argh = "0.1.13"
crossterm = "0.29.0" crossterm = { version = "0.29.0", features = ["event-stream"] }
futures = "0.3.31"
open = "5.3.2" open = "5.3.2"
ratatui = "0.29.0" ratatui = "0.29.0"
reqwest = { version = "0.12.20", features = ["blocking"] } reqwest = { version = "0.12.20", features = ["blocking"] }
rusqlite = { version = "0.36.0", features = ["bundled", "time"] } rusqlite = { version = "0.36.0", features = ["bundled", "time"] }
time = { version = "0.3.41", features = ["parsing"] } time = { version = "0.3.41", features = ["parsing"] }
tokio = { version = "1.47.1", features = ["macros", "rt", "rt-multi-thread"] }
tokio-util = { version = "0.7.16", features = ["futures-util"] }
xml-rs = "0.8.26" xml-rs = "0.8.26"
[profile.release] [profile.release]

View File

@ -8,15 +8,18 @@ pub mod parser;
pub mod persistence; pub mod persistence;
pub mod ui; pub mod ui;
fn main() -> Result<()> { #[tokio::main]
async fn main() -> Result<()> {
let args = argh::from_env::<TrsArgs>(); let args = argh::from_env::<TrsArgs>();
let mut ctx = TrsEnv::new("test3")?; let db_name = "test3";
match args.sub_command { match args.sub_command {
TrsSubCommand::AddChannel(args) => { TrsSubCommand::AddChannel(args) => {
let mut ctx = TrsEnv::new(db_name)?;
commands::add_channel(&mut ctx, &args)?; commands::add_channel(&mut ctx, &args)?;
Ok(()) Ok(())
} }
TrsSubCommand::ListChannels(args) => { TrsSubCommand::ListChannels(args) => {
let mut ctx = TrsEnv::new(db_name)?;
let channels = commands::list_channels(&mut ctx, &args)?; let channels = commands::list_channels(&mut ctx, &args)?;
for channel in channels { for channel in channels {
println!( println!(
@ -27,8 +30,12 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
TrsSubCommand::RemoveChannel(args) => commands::remove_channel(&mut ctx, &args), TrsSubCommand::RemoveChannel(args) => {
let mut ctx = TrsEnv::new("test3")?;
commands::remove_channel(&mut ctx, &args)
}
TrsSubCommand::GetArticles(args) => { TrsSubCommand::GetArticles(args) => {
let mut ctx = TrsEnv::new(db_name)?;
let channels = commands::get_articles_by_channel(&mut ctx, &args)?; let channels = commands::get_articles_by_channel(&mut ctx, &args)?;
for channel in channels { for channel in channels {
println!( println!(
@ -49,7 +56,10 @@ fn main() -> Result<()> {
} }
Ok(()) Ok(())
} }
TrsSubCommand::MarkRead(args) => commands::mark_read(&mut ctx, &args), TrsSubCommand::MarkRead(args) => {
TrsSubCommand::Ui(args) => ui::ui(ctx, &args), let mut ctx = TrsEnv::new("test3")?;
commands::mark_read(&mut ctx, &args)
}
TrsSubCommand::Ui(args) => ui::ui(&args, db_name).await,
} }
} }

123
src/ui.rs
View File

@ -8,27 +8,28 @@ pub mod title;
use std::{ use std::{
io::Stdout, io::Stdout,
sync::mpsc::{channel, Receiver, Sender}, sync::mpsc::{channel, Sender},
thread, time::Duration, time::Duration,
}; };
use crate::{ use crate::{
args::{self, ListChannelArgs, UiArgs}, args::{self, UiArgs},
commands::{self, TrsEnv},
error::{Result, TrsError}, error::{Result, TrsError},
persistence::RssChannelD, persistence::RssChannelD,
}; };
use articles::ArticlesWidget; use articles::ArticlesWidget;
use channels::ChannelsWidget; use channels::ChannelsWidget;
use controls::ControlsWidget; use controls::ControlsWidget;
use crossterm::event; use crossterm::event::{self, KeyEventKind};
use debug::DebugWidget; use debug::DebugWidget;
use executor::UiCommandExecutor; use executor::UiCommandExecutor;
use futures::{FutureExt, StreamExt};
use ratatui::{ use ratatui::{
prelude::*, prelude::*,
widgets::{Block, Borders}, widgets::{Block, Borders},
}; };
use title::TitleWidget; use title::TitleWidget;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
pub struct AppState { pub struct AppState {
exit: bool, exit: bool,
@ -42,7 +43,7 @@ pub struct AppState {
show_add_channel_ui: bool, show_add_channel_ui: bool,
add_channel: String, add_channel: String,
dispatcher: Sender<UiCommandDispatchActions>, dispatcher: Sender<UiCommandDispatchActions>,
receiver: Receiver<u64>, receiver: UnboundedReceiver<Event>,
} }
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
@ -82,11 +83,13 @@ pub enum UiCommandDispatchActions {
AddChannel(args::AddChannelArgs), AddChannel(args::AddChannelArgs),
RemoveChannel(args::RemoveChannelArgs), RemoveChannel(args::RemoveChannelArgs),
MarkArticleRead(args::MarkReadArgs), MarkArticleRead(args::MarkReadArgs),
ListChannels(args::ListChannelArgs),
} }
pub fn ui(ctx: TrsEnv, args: &UiArgs) -> Result<()> { pub async fn ui(args: &UiArgs, db_name: &str) -> Result<()> {
let (tdispatch, rdispatch) = channel(); let (app_dispatch, app_recv) = channel();
let (tupdate, rupdate) = 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 mut terminal = ratatui::init();
let mut app_state = AppState { let mut app_state = AppState {
channels: Vec::new(), channels: Vec::new(),
@ -99,33 +102,81 @@ pub fn ui(ctx: TrsEnv, args: &UiArgs) -> 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: tdispatch, dispatcher: app_dispatch,
receiver: rupdate, receiver: event_recv,
}; };
let ctx_cloned = ctx.clone(); let db_name = db_name.to_string();
let executor = UiCommandExecutor::new(rdispatch, tupdate); std::thread::spawn(move || {
let executor_handle = thread::spawn(move || { let mut executor = UiCommandExecutor::new(app_recv, executor_dispatch);
executor.run(ctx_cloned); executor.run(db_name);
}); });
let channels = commands::list_channels(&ctx, &ListChannelArgs { limit: None })?; app_state
app_state.channels = channels; .dispatcher
.send(UiCommandDispatchActions::ListChannels(
args::ListChannelArgs { limit: None },
))
.map_err(|e| TrsError::Error(format!("Unable to send initial app: {}", e)))?;
loop { loop {
draw(&app_state, &mut terminal)?; draw(&app_state, &mut terminal)?;
handle_events(&mut app_state, &ctx)?; handle_events(&mut app_state).await?;
if app_state.exit { if app_state.exit {
break; break;
} }
} }
drop(app_state); drop(app_state);
executor_handle.join().unwrap();
ratatui::restore(); ratatui::restore();
Ok(()) Ok(())
} }
fn start_event_loop(
mut executor_recv: UnboundedReceiver<BackendEvent>,
) -> UnboundedReceiver<Event> {
let (evt_dispatch, evt_recv) = tokio::sync::mpsc::unbounded_channel();
let _event_tx = evt_dispatch.clone();
let _task = tokio::spawn(async move {
let mut reader = crossterm::event::EventStream::new();
let mut tick_interval = tokio::time::interval(Duration::from_millis(250));
loop {
let tick_delay = tick_interval.tick();
let crossterm_event = reader.next().fuse();
tokio::select! {
user_input = crossterm_event => {
match user_input {
Some(Ok(evt)) => {
match evt {
crossterm::event::Event::Key(key) => {
if key.kind == KeyEventKind::Press {
_event_tx.send(Event::UserInput(crossterm::event::Event::Key(key))).unwrap();
}
},
_ => {}
}
},
_ => {}
}
},
executor_event = executor_recv.recv() => {
match executor_event {
Some(backend_event) => {
_event_tx.send(Event::BackendEvent(backend_event)).unwrap();
},
None => {}
}
},
_ = tick_delay => {
_event_tx.send(Event::Tick).unwrap();
},
}
}
});
evt_recv
}
fn draw(app_state: &AppState, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> { fn draw(app_state: &AppState, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
terminal terminal
.draw(|f| { .draw(|f| {
@ -138,20 +189,29 @@ fn draw(app_state: &AppState, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
pub enum Event { pub enum Event {
UserInput(crossterm::event::Event), UserInput(crossterm::event::Event),
ReloadState, BackendEvent(BackendEvent),
Tick, Tick,
} }
fn handle_events(state: &mut AppState, ctx: &TrsEnv) -> Result<()> { pub enum BackendEvent {
let event = get_event(state)?; ReloadState(Vec<RssChannelD>),
}
async fn handle_events(state: &mut AppState) -> Result<()> {
let event = state.receiver.recv().await;
let Some(event) = event else {
return Ok(());
};
match event { match event {
Event::UserInput(event) => { Event::UserInput(event) => {
handle_user_input(state, event)?; handle_user_input(state, event)?;
} }
Event::ReloadState => { Event::BackendEvent(backend_event) => match backend_event {
let channels = commands::list_channels(&ctx, &ListChannelArgs { limit: None })?; BackendEvent::ReloadState(channels) => {
state.channels = channels; state.channels = channels;
} }
},
Event::Tick => {} Event::Tick => {}
}; };
@ -171,21 +231,6 @@ fn handle_user_input(state: &mut AppState, event: event::Event) -> Result<()> {
return Ok(()); return Ok(());
} }
fn get_event(state: &mut AppState) -> Result<Event> {
let recv_action = state.receiver.try_recv();
if let Ok(_) = recv_action {
return Ok(Event::ReloadState);
}
let raw_event = event::poll(Duration::from_millis(250)).map_err(|e| TrsError::TuiError(e))?;
if raw_event == false {
return Ok(Event::Tick);
}
// It's guaranteed that an event is available now
Ok(Event::UserInput(event::read().unwrap()))
}
struct AppStateWidget<'a> { struct AppStateWidget<'a> {
app_state: &'a AppState, app_state: &'a AppState,
} }

View File

@ -1,26 +1,32 @@
use std::sync::mpsc::{Receiver, Sender}; use std::sync::mpsc::Receiver;
use tokio::sync::mpsc::UnboundedSender;
use crate::{commands::TrsEnv, ui::BackendEvent};
use super::UiCommandDispatchActions; use super::UiCommandDispatchActions;
pub struct UiCommandExecutor { pub struct UiCommandExecutor {
pub command_receiver: Receiver<UiCommandDispatchActions>, pub app_recv: Receiver<UiCommandDispatchActions>,
pub status_sender: Sender<u64>, pub executor_dispatch: UnboundedSender<BackendEvent>,
} }
impl UiCommandExecutor { impl UiCommandExecutor {
pub fn new( pub fn new(
command_receiver: Receiver<UiCommandDispatchActions>, app_recv: Receiver<UiCommandDispatchActions>,
status_sender: Sender<u64>, executor_dispatch: UnboundedSender<BackendEvent>,
) -> Self { ) -> Self {
UiCommandExecutor { UiCommandExecutor {
command_receiver, app_recv,
status_sender, executor_dispatch,
} }
} }
pub fn run(&self, ctx: crate::commands::TrsEnv) -> () { // 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 { loop {
let action = self.command_receiver.recv(); let action = self.app_recv.recv();
let Ok(action) = action else { let Ok(action) = action else {
break; break;
}; };
@ -28,22 +34,39 @@ impl UiCommandExecutor {
match action { match action {
UiCommandDispatchActions::AddChannel(args) => { UiCommandDispatchActions::AddChannel(args) => {
if let Ok(_) = crate::commands::add_channel(&ctx, &args) { if let Ok(_) = crate::commands::add_channel(&ctx, &args) {
self.status_sender.send(1).unwrap_or_default(); self.send_new_state_default(&ctx);
}; };
} }
UiCommandDispatchActions::RemoveChannel(args) => { UiCommandDispatchActions::RemoveChannel(args) => {
if let Ok(_) = crate::commands::remove_channel(&ctx, &args) { if let Ok(_) = crate::commands::remove_channel(&ctx, &args) {
self.status_sender.send(1).unwrap_or_default(); 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);
}
}
} }
} }
UiCommandDispatchActions::MarkArticleRead(args) => { fn send_new_state_default(&mut self, ctx: &crate::commands::TrsEnv) {
if let Ok(_) = crate::commands::mark_read(&ctx, &args) { self.send_new_state(ctx, crate::args::ListChannelArgs { limit: None });
self.status_sender.send(1).unwrap_or_default();
}
}
} }
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();
} }
} }
} }