Skip to content

Commit

Permalink
Import OPML files (#32)
Browse files Browse the repository at this point in the history
* WIP: initial opml import

* clean up opml import

* add opml.rs, oops
  • Loading branch information
ckampfe authored Feb 18, 2024
1 parent ce19c29 commit c3741cd
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 47 deletions.
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

0 comments on commit c3741cd

Please sign in to comment.