Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Usage: russ <COMMAND>
Commands:
read Read your feeds
import Import feeds from an OPML document
sync Sync all feeds and exit
help Print this message or the help of the given subcommand(s)

Options:
Expand All @@ -97,6 +98,42 @@ Options:
RSS/Atom network request timeout in seconds [default: 5]
-h, --help
Print help

## sync mode

Run a full refresh of all feeds and exit without launching the TUI. Useful for cron jobs or scripting.

```console
$ russ sync -h
Sync all feeds and exit

Usage: russ sync [OPTIONS]

Options:
-d, --database-path <DATABASE_PATH>
Override where `russ` stores and reads feeds. By default, the feeds database on Linux this will be at `XDG_DATA_HOME/russ/feeds.db` or `$HOME/.local/share/russ/feeds.db`. On MacOS it will be at `$HOME/Library/Application Support/russ/feeds.db`. On Windows it will be at `{FOLDERID_LocalAppData}/russ/data/feeds.db`
-n, --network-timeout <NETWORK_TIMEOUT>
RSS/Atom network request timeout in seconds [default: 5]
-h, --help
Print help
```

Examples:

```console
# Refresh using the default database and default timeout
$ russ sync

# Refresh with a custom database path
$ russ sync -d /path/to/feeds.db

# Increase network timeout to 15 seconds
$ russ sync -n 15

# Cron (runs hourly)
$ crontab -e
0 * * * * /usr/local/bin/russ sync -d /home/you/.local/share/russ/feeds.db >> /home/you/russ-sync.log 2>&1
```
```

## import OPML mode
Expand Down
4 changes: 4 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub struct App {
impl App {
delegate_to_locked_inner![
(error_flash_is_empty, bool),
(error_flash, Vec<String>),
(feed_ids, Result<Vec<crate::rss::FeedId>>),
(force_redraw, Result<()>),
(http_client, ureq::Agent),
Expand Down Expand Up @@ -199,6 +200,9 @@ pub struct AppImpl {
}

impl AppImpl {
pub fn error_flash(&self) -> Vec<String> {
self.error_flash.iter().map(|e| format!("{e:?}")).collect()
}
pub fn new(
options: crate::ReadOptions,
event_tx: std::sync::mpsc::Sender<crate::Event<crossterm::event::KeyEvent>>,
Expand Down
71 changes: 71 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ fn main() -> Result<()> {
match validated_options {
ValidatedOptions::Import(options) => crate::opml::import(options),
ValidatedOptions::Read(options) => run_reader(options),
ValidatedOptions::Sync(options) => run_sync(options),
}
}

Expand Down Expand Up @@ -80,6 +81,15 @@ enum Command {
#[arg(short, long, default_value = "5", value_parser = parse_seconds)]
network_timeout: time::Duration,
},
/// Sync all feeds and exit (no TUI)
Sync {
/// Override where `russ` stores and reads feeds.
#[arg(short, long)]
database_path: Option<PathBuf>,
/// RSS/Atom network request timeout in seconds
#[arg(short, long, default_value = "5", value_parser = parse_seconds)]
network_timeout: time::Duration,
},
}

impl Command {
Expand Down Expand Up @@ -112,6 +122,16 @@ impl Command {
network_timeout: *network_timeout,
}))
}
Command::Sync {
database_path,
network_timeout,
} => {
let database_path = get_database_path(database_path)?;
Ok(ValidatedOptions::Sync(SyncOptions {
database_path,
network_timeout: *network_timeout,
}))
}
}
}
}
Expand All @@ -126,6 +146,7 @@ fn parse_seconds(s: &str) -> Result<time::Duration, std::num::ParseIntError> {
enum ValidatedOptions {
Read(ReadOptions),
Import(ImportOptions),
Sync(SyncOptions),
}

#[derive(Clone, Debug)]
Expand All @@ -143,6 +164,12 @@ struct ImportOptions {
network_timeout: time::Duration,
}

#[derive(Debug)]
struct SyncOptions {
database_path: PathBuf,
network_timeout: time::Duration,
}

fn get_database_path(database_path: &Option<PathBuf>) -> std::io::Result<PathBuf> {
let database_path = if let Some(database_path) = database_path {
database_path.to_owned()
Expand Down Expand Up @@ -255,6 +282,50 @@ fn run_reader(options: ReadOptions) -> Result<()> {
Ok(())
}

fn run_sync(options: SyncOptions) -> Result<()> {
// Reuse the IO architecture without starting the TUI.
let (event_tx, _event_rx) = mpsc::channel();
let (io_tx, io_rx) = mpsc::channel();

// Build ReadOptions with defaults needed by App::new
let read_opts = ReadOptions {
database_path: options.database_path,
tick_rate: 250,
flash_display_duration_seconds: time::Duration::from_secs(4),
network_timeout: options.network_timeout,
};

let app = App::new(read_opts.clone(), event_tx.clone(), io_tx.clone())?;
let cloned_app = app.clone();

// Spawn IO thread
let io_thread = thread::spawn(move || -> Result<()> {
io::io_loop(cloned_app, io_tx, io_rx, &read_opts)
});

// Trigger refresh all feeds
app.refresh_feeds()?;

// Allow IO to process request(s) before breaking.
// Wait approximately the network timeout, then signal break.
thread::sleep(options.network_timeout + time::Duration::from_millis(200));
app.break_io_thread()?;

// Wait for IO thread to finish
io_thread
.join()
.expect("Unable to join IO thread to main thread")?;

// Print any errors collected during sync to stderr
if !app.error_flash_is_empty() {
for e in app.error_flash() {
eprintln!("{e}");
}
}

Ok(())
}

enum Action {
Quit,
MoveLeft,
Expand Down