Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import OPML files #32

Merged
merged 3 commits into from
Feb 18, 2024
Merged
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
55 changes: 55 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ diligent-date-parser = "0.1"
directories = "5"
html2text = "0.12"
num_cpus = "1.16"
opml = "1.1"
r2d2 = "0.8"
r2d2_sqlite = "0.23"
rss = { version = "2.0", default-features = false }
Expand Down
4 changes: 2 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ impl App {
];

pub fn new(
options: crate::Options,
options: crate::ReadOptions,
event_s: std::sync::mpsc::Sender<crate::Event<crossterm::event::KeyEvent>>,
) -> Result<App> {
Ok(App {
Expand Down Expand Up @@ -195,7 +195,7 @@ pub struct AppImpl {

impl AppImpl {
pub fn new(
options: crate::Options,
options: crate::ReadOptions,
event_s: std::sync::mpsc::Sender<crate::Event<crossterm::event::KeyEvent>>,
) -> Result<AppImpl> {
let mut conn = rusqlite::Connection::open(&options.database_path)?;
Expand Down
147 changes: 104 additions & 43 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use crate::modes::{Mode, Selected};
use anyhow::Result;
use app::App;
use clap::Parser;
use clap::{Parser, Subcommand};
use crossterm::event;
use crossterm::event::{Event as CEvent, KeyCode, KeyModifiers};
use crossterm::execute;
Expand All @@ -19,6 +19,7 @@ use std::{thread, time};

mod app;
mod modes;
mod opml;
mod rss;
mod ui;
mod util;
Expand All @@ -28,39 +29,83 @@ pub enum Event<I> {
Tick,
}

// Only used to take input at the boundary.
// Turned into `Options` with `to_options()`.
/// A TUI RSS reader with vim-like controls and a local-first, offline-first focus
#[derive(Clone, Debug, Parser)]
#[derive(Debug, Parser)]
#[command(author, version, about, name = "russ")]
struct CliOptions {
/// 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`.
#[arg(short, long)]
database_path: Option<PathBuf>,
/// time in ms between two ticks
#[arg(short, long, default_value = "250")]
tick_rate: u64,
/// number of seconds to show the flash message before clearing it
#[arg(short, long, default_value = "4", value_parser = parse_seconds)]
flash_display_duration_seconds: time::Duration,
/// RSS/Atom network request timeout in seconds
#[arg(short, long, default_value = "5", value_parser = parse_seconds)]
network_timeout: time::Duration,
struct Options {
#[command(subcommand)]
subcommand: Command,
}

impl CliOptions {
fn to_options(&self) -> std::io::Result<Options> {
let database_path = get_database_path(self)?;
/// Only used to take input at the boundary.
/// Turned into `ValidatedOptions` with `validate()`.
#[derive(Debug, Subcommand)]
enum Command {
/// Read your feeds
Read {
/// 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`.
#[arg(short, long)]
database_path: Option<PathBuf>,
/// time in ms between two ticks
#[arg(short, long, default_value = "250")]
tick_rate: u64,
/// number of seconds to show the flash message before clearing it
#[arg(short, long, default_value = "4", value_parser = parse_seconds)]
flash_display_duration_seconds: time::Duration,
/// RSS/Atom network request timeout in seconds
#[arg(short, long, default_value = "5", value_parser = parse_seconds)]
network_timeout: time::Duration,
},
/// Import feeds from an OPML document
Import {
/// 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`.
#[arg(short, long)]
database_path: Option<PathBuf>,
#[arg(short, long)]
opml_path: PathBuf,
/// RSS/Atom network request timeout in seconds
#[arg(short, long, default_value = "5", value_parser = parse_seconds)]
network_timeout: time::Duration,
},
}

Ok(Options {
database_path,
tick_rate: self.tick_rate,
flash_display_duration_seconds: self.flash_display_duration_seconds,
network_timeout: self.network_timeout,
})
impl Command {
fn validate(&self) -> std::io::Result<ValidatedOptions> {
match self {
Command::Read {
database_path,
tick_rate,
flash_display_duration_seconds,
network_timeout,
} => {
let database_path = get_database_path(database_path)?;

Ok(ValidatedOptions::Read(ReadOptions {
database_path,
tick_rate: *tick_rate,
flash_display_duration_seconds: *flash_display_duration_seconds,
network_timeout: *network_timeout,
}))
}
Command::Import {
database_path,
opml_path,
network_timeout,
} => {
let database_path = get_database_path(database_path)?;
Ok(ValidatedOptions::Import(ImportOptions {
database_path,
opml_path: opml_path.to_owned(),
network_timeout: *network_timeout,
}))
}
}
}
}

Expand All @@ -69,21 +114,30 @@ fn parse_seconds(s: &str) -> Result<time::Duration, std::num::ParseIntError> {
Ok(time::Duration::from_secs(as_u64))
}

/// internal, validated options
/// internal, validated options for the normal reader mode
#[derive(Debug)]
enum ValidatedOptions {
Read(ReadOptions),
Import(ImportOptions),
}

#[derive(Clone, Debug)]
pub struct Options {
/// feed database path
struct ReadOptions {
database_path: PathBuf,
/// time in ms between two ticks
tick_rate: u64,
/// number of seconds to show the flash message before clearing it
flash_display_duration_seconds: time::Duration,
/// RSS/Atom network request timeout in seconds
network_timeout: time::Duration,
}

fn get_database_path(cli_options: &CliOptions) -> std::io::Result<PathBuf> {
let database_path = if let Some(database_path) = cli_options.database_path.as_ref() {
#[derive(Debug)]
struct ImportOptions {
database_path: PathBuf,
opml_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()
} else {
let mut database_path = directories::ProjectDirs::from("", "", "russ")
Expand Down Expand Up @@ -113,7 +167,7 @@ fn io_loop(
app: App,
sx: mpsc::Sender<IoCommand>,
rx: mpsc::Receiver<IoCommand>,
options: &Options,
options: &ReadOptions,
) -> Result<()> {
use IoCommand::*;

Expand Down Expand Up @@ -272,11 +326,7 @@ fn clear_flash_after(sx: mpsc::Sender<IoCommand>, duration: time::Duration) {
});
}

fn main() -> Result<()> {
let cli_options: CliOptions = CliOptions::parse();

let options = cli_options.to_options()?;

fn run_reader(options: ReadOptions) -> Result<()> {
enable_raw_mode()?;

let mut stdout = stdout();
Expand Down Expand Up @@ -408,3 +458,14 @@ fn main() -> Result<()> {

Ok(())
}

fn main() -> Result<()> {
let options = Options::parse();

let validated_options = options.subcommand.validate()?;

match validated_options {
ValidatedOptions::Import(options) => crate::opml::import(options),
ValidatedOptions::Read(options) => run_reader(options),
}
}
Loading
Loading