Compare commits
No commits in common. "31501d65d812f45cd6620b938a03dd688828b918" and "7605dcf3b645def663094f4cd48c1001e45e58a5" have entirely different histories.
31501d65d8
...
7605dcf3b6
93
Cargo.lock
generated
93
Cargo.lock
generated
@ -201,7 +201,6 @@ dependencies = [
|
||||
"crossterm_winapi",
|
||||
"derive_more",
|
||||
"document-features",
|
||||
"futures-core",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix 1.0.7",
|
||||
@ -389,21 +388,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
@ -420,34 +404,12 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
@ -466,10 +428,8 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
@ -662,7 +622,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@ -812,17 +772,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
@ -1497,16 +1446,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
@ -1661,31 +1600,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.1"
|
||||
version = "1.45.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
|
||||
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
"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",
|
||||
"socket2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1710,14 +1635,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.16"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
|
||||
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
@ -1792,14 +1716,11 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"argh",
|
||||
"crossterm 0.29.0",
|
||||
"futures",
|
||||
"open",
|
||||
"ratatui",
|
||||
"reqwest",
|
||||
"rusqlite",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
|
||||
@ -5,15 +5,12 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
argh = "0.1.13"
|
||||
crossterm = { version = "0.29.0", features = ["event-stream"] }
|
||||
futures = "0.3.31"
|
||||
crossterm = "0.29.0"
|
||||
open = "5.3.2"
|
||||
ratatui = "0.29.0"
|
||||
reqwest = { version = "0.12.20", features = ["blocking"] }
|
||||
rusqlite = { version = "0.36.0", features = ["bundled", "time"] }
|
||||
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"
|
||||
|
||||
[profile.release]
|
||||
|
||||
20
src/main.rs
20
src/main.rs
@ -8,18 +8,15 @@ pub mod parser;
|
||||
pub mod persistence;
|
||||
pub mod ui;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
fn main() -> Result<()> {
|
||||
let args = argh::from_env::<TrsArgs>();
|
||||
let db_name = "test3";
|
||||
let mut ctx = TrsEnv::new("test3")?;
|
||||
match args.sub_command {
|
||||
TrsSubCommand::AddChannel(args) => {
|
||||
let mut ctx = TrsEnv::new(db_name)?;
|
||||
commands::add_channel(&mut ctx, &args)?;
|
||||
Ok(())
|
||||
}
|
||||
TrsSubCommand::ListChannels(args) => {
|
||||
let mut ctx = TrsEnv::new(db_name)?;
|
||||
let channels = commands::list_channels(&mut ctx, &args)?;
|
||||
for channel in channels {
|
||||
println!(
|
||||
@ -30,12 +27,8 @@ async fn main() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
TrsSubCommand::RemoveChannel(args) => {
|
||||
let mut ctx = TrsEnv::new("test3")?;
|
||||
commands::remove_channel(&mut ctx, &args)
|
||||
}
|
||||
TrsSubCommand::RemoveChannel(args) => commands::remove_channel(&mut ctx, &args),
|
||||
TrsSubCommand::GetArticles(args) => {
|
||||
let mut ctx = TrsEnv::new(db_name)?;
|
||||
let channels = commands::get_articles_by_channel(&mut ctx, &args)?;
|
||||
for channel in channels {
|
||||
println!(
|
||||
@ -56,10 +49,7 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
TrsSubCommand::MarkRead(args) => {
|
||||
let mut ctx = TrsEnv::new("test3")?;
|
||||
commands::mark_read(&mut ctx, &args)
|
||||
}
|
||||
TrsSubCommand::Ui(args) => ui::ui(&args, db_name).await,
|
||||
TrsSubCommand::MarkRead(args) => commands::mark_read(&mut ctx, &args),
|
||||
TrsSubCommand::Ui(args) => ui::ui(ctx, &args),
|
||||
}
|
||||
}
|
||||
|
||||
127
src/ui.rs
127
src/ui.rs
@ -8,28 +8,27 @@ pub mod title;
|
||||
|
||||
use std::{
|
||||
io::Stdout,
|
||||
sync::mpsc::{channel, Sender},
|
||||
time::Duration,
|
||||
sync::mpsc::{channel, Receiver, Sender},
|
||||
thread, time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
args::{self, UiArgs},
|
||||
args::{self, ListChannelArgs, UiArgs},
|
||||
commands::{self, TrsEnv},
|
||||
error::{Result, TrsError},
|
||||
persistence::RssChannelD,
|
||||
};
|
||||
use articles::ArticlesWidget;
|
||||
use channels::ChannelsWidget;
|
||||
use controls::ControlsWidget;
|
||||
use crossterm::event::{self, KeyEventKind};
|
||||
use crossterm::event;
|
||||
use debug::DebugWidget;
|
||||
use executor::UiCommandExecutor;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders},
|
||||
};
|
||||
use title::TitleWidget;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
|
||||
pub struct AppState {
|
||||
exit: bool,
|
||||
@ -43,7 +42,7 @@ pub struct AppState {
|
||||
show_add_channel_ui: bool,
|
||||
add_channel: String,
|
||||
dispatcher: Sender<UiCommandDispatchActions>,
|
||||
receiver: UnboundedReceiver<Event>,
|
||||
receiver: Receiver<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
@ -83,13 +82,11 @@ pub enum UiCommandDispatchActions {
|
||||
AddChannel(args::AddChannelArgs),
|
||||
RemoveChannel(args::RemoveChannelArgs),
|
||||
MarkArticleRead(args::MarkReadArgs),
|
||||
ListChannels(args::ListChannelArgs),
|
||||
}
|
||||
|
||||
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);
|
||||
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(),
|
||||
@ -102,81 +99,33 @@ 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: tdispatch,
|
||||
receiver: rupdate,
|
||||
};
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
std::thread::spawn(move || {
|
||||
let mut executor = UiCommandExecutor::new(app_recv, executor_dispatch);
|
||||
executor.run(db_name);
|
||||
let ctx_cloned = ctx.clone();
|
||||
let executor = UiCommandExecutor::new(rdispatch, tupdate);
|
||||
let executor_handle = thread::spawn(move || {
|
||||
executor.run(ctx_cloned);
|
||||
});
|
||||
|
||||
app_state
|
||||
.dispatcher
|
||||
.send(UiCommandDispatchActions::ListChannels(
|
||||
args::ListChannelArgs { limit: None },
|
||||
))
|
||||
.map_err(|e| TrsError::Error(format!("Unable to send initial app: {}", e)))?;
|
||||
let channels = commands::list_channels(&ctx, &ListChannelArgs { limit: None })?;
|
||||
app_state.channels = channels;
|
||||
|
||||
loop {
|
||||
draw(&app_state, &mut terminal)?;
|
||||
handle_events(&mut app_state).await?;
|
||||
handle_events(&mut app_state, &ctx)?;
|
||||
if app_state.exit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
drop(app_state);
|
||||
executor_handle.join().unwrap();
|
||||
ratatui::restore();
|
||||
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<()> {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
@ -189,29 +138,20 @@ fn draw(app_state: &AppState, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||
|
||||
pub enum Event {
|
||||
UserInput(crossterm::event::Event),
|
||||
BackendEvent(BackendEvent),
|
||||
ReloadState,
|
||||
Tick,
|
||||
}
|
||||
|
||||
pub enum BackendEvent {
|
||||
ReloadState(Vec<RssChannelD>),
|
||||
}
|
||||
|
||||
async fn handle_events(state: &mut AppState) -> Result<()> {
|
||||
let event = state.receiver.recv().await;
|
||||
let Some(event) = event else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
fn handle_events(state: &mut AppState, ctx: &TrsEnv) -> Result<()> {
|
||||
let event = get_event(state)?;
|
||||
match event {
|
||||
Event::UserInput(event) => {
|
||||
handle_user_input(state, event)?;
|
||||
}
|
||||
Event::BackendEvent(backend_event) => match backend_event {
|
||||
BackendEvent::ReloadState(channels) => {
|
||||
state.channels = channels;
|
||||
}
|
||||
},
|
||||
Event::ReloadState => {
|
||||
let channels = commands::list_channels(&ctx, &ListChannelArgs { limit: None })?;
|
||||
state.channels = channels;
|
||||
}
|
||||
Event::Tick => {}
|
||||
};
|
||||
|
||||
@ -231,6 +171,21 @@ fn handle_user_input(state: &mut AppState, event: event::Event) -> Result<()> {
|
||||
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> {
|
||||
app_state: &'a AppState,
|
||||
}
|
||||
|
||||
@ -1,32 +1,26 @@
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{commands::TrsEnv, ui::BackendEvent};
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
use super::UiCommandDispatchActions;
|
||||
|
||||
pub struct UiCommandExecutor {
|
||||
pub app_recv: Receiver<UiCommandDispatchActions>,
|
||||
pub executor_dispatch: UnboundedSender<BackendEvent>,
|
||||
pub command_receiver: Receiver<UiCommandDispatchActions>,
|
||||
pub status_sender: Sender<u64>,
|
||||
}
|
||||
|
||||
impl UiCommandExecutor {
|
||||
pub fn new(
|
||||
app_recv: Receiver<UiCommandDispatchActions>,
|
||||
executor_dispatch: UnboundedSender<BackendEvent>,
|
||||
command_receiver: Receiver<UiCommandDispatchActions>,
|
||||
status_sender: Sender<u64>,
|
||||
) -> Self {
|
||||
UiCommandExecutor {
|
||||
app_recv,
|
||||
executor_dispatch,
|
||||
command_receiver,
|
||||
status_sender,
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
pub fn run(&self, ctx: crate::commands::TrsEnv) -> () {
|
||||
loop {
|
||||
let action = self.app_recv.recv();
|
||||
let action = self.command_receiver.recv();
|
||||
let Ok(action) = action else {
|
||||
break;
|
||||
};
|
||||
@ -34,39 +28,22 @@ impl UiCommandExecutor {
|
||||
match action {
|
||||
UiCommandDispatchActions::AddChannel(args) => {
|
||||
if let Ok(_) = crate::commands::add_channel(&ctx, &args) {
|
||||
self.send_new_state_default(&ctx);
|
||||
self.status_sender.send(1).unwrap_or_default();
|
||||
};
|
||||
}
|
||||
|
||||
UiCommandDispatchActions::RemoveChannel(args) => {
|
||||
if let Ok(_) = crate::commands::remove_channel(&ctx, &args) {
|
||||
self.send_new_state_default(&ctx);
|
||||
self.status_sender.send(1).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
UiCommandDispatchActions::MarkArticleRead(args) => {
|
||||
if let Ok(_) = crate::commands::mark_read(&ctx, &args) {
|
||||
self.send_new_state_default(&ctx);
|
||||
self.status_sender.send(1).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user