diff --git a/Cargo.lock b/Cargo.lock index a9fcfe6..4eb1c17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "argh" version = "0.1.13" @@ -55,6 +61,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "backtrace" version = "0.3.75" @@ -94,6 +106,21 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.27" @@ -109,6 +136,29 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -125,6 +175,105 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.0.7", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -136,6 +285,21 @@ dependencies = [ "syn", ] +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -319,6 +483,8 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -331,6 +497,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "1.3.1" @@ -535,6 +707,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -566,6 +744,25 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] +name = "instability" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -582,6 +779,15 @@ dependencies = [ "serde", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -615,6 +821,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -627,12 +839,37 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + [[package]] name = "memchr" version = "2.7.5" @@ -661,6 +898,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -741,6 +979,35 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -798,6 +1065,36 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags", +] + [[package]] name = "reqwest" version = "0.12.20" @@ -880,6 +1177,19 @@ version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.0.7" @@ -889,7 +1199,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] @@ -947,6 +1257,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "2.11.1" @@ -1020,6 +1336,36 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.10" @@ -1048,6 +1394,40 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -1115,7 +1495,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -1246,6 +1626,8 @@ name = "trs" version = "0.1.0" dependencies = [ "argh", + "crossterm 0.29.0", + "ratatui", "reqwest", "rusqlite", "xml-rs", @@ -1263,6 +1645,35 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "untrusted" version = "0.9.0" @@ -1397,6 +1808,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 2946787..7f9fdf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,8 @@ edition = "2021" [dependencies] argh = "0.1.13" +crossterm = "0.29.0" +ratatui = "0.29.0" reqwest = { version = "0.12.20", features = ["blocking"] } rusqlite = { version = "0.36.0", features = ["bundled"] } xml-rs = "0.8.26" diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e79c28 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Terminal RSs + +- Support both Atom and RSS +- https://www.rssboard.org/rss-specification +- https://www.rfc-editor.org/rfc/rfc4287 diff --git a/docs/design.markdown b/docs/design.markdown deleted file mode 100644 index 8ae24e0..0000000 --- a/docs/design.markdown +++ /dev/null @@ -1,24 +0,0 @@ -# Terminal RSs - - -Keep the goal simple. - -- just list the unread articles -- Have a way to mark an article as read/unread -- https://www.rssboard.org/rss-specification - -## Database - -Channels = Id | FeedName | Link | Atom/RSS | checksum - -Articles = Id | ChannelId | Title | Link (unique) | Description | Published | Read - -https://sqlite.org/lang_upsert.html - -INSERT INTO Articles(ChannelId, Title, Link, Description, Published, 0) - VALUES('Alice','704-555-1212','2018-05-08') - ON CONFLICT(name) DO UPDATE SET - Read=0, - Published=excluded.Published - WHERE excluded.Published>Articles.Published; - diff --git a/src/args.rs b/src/args.rs index f902b4c..c2fe7be 100644 --- a/src/args.rs +++ b/src/args.rs @@ -12,6 +12,7 @@ pub struct TrsArgs { pub enum TrsSubCommand { AddChannel(AddChannelArgs), ListChannels(ListChannelArgs), + RemoveChannel(RemoveChannelArgs), } /// Add a new RSS channel @@ -32,6 +33,15 @@ pub struct ListChannelArgs { pub limit: Option, } +/// Delete an RSS channel +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "remove")] +pub struct RemoveChannelArgs { + /// delete the channel with this id + #[argh(option)] + pub id: u32, +} + pub fn valid_url(url: &str) -> Result { if url.starts_with("http://") || url.starts_with("https://") { Ok(url.to_string()) diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..9e17806 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,80 @@ +use crate::{ + args::{AddChannelArgs, ListChannelArgs, RemoveChannelArgs, TrsArgs, TrsSubCommand}, + error::TrsError, + parser, + persistence::Db, +}; + +pub fn execute(mut db: Db, args: &TrsArgs) -> Result<(), TrsError> { + let sub_command = &args.sub_command; + match sub_command { + TrsSubCommand::AddChannel(add_args) => add_channel(&mut db, add_args), + TrsSubCommand::ListChannels(list_args) => list_channels(&mut db, list_args), + TrsSubCommand::RemoveChannel(delete_args) => delete_channel(&mut db, delete_args), + } +} + +fn add_channel(db: &mut Db, args: &AddChannelArgs) -> Result<(), TrsError> { + let client = reqwest::blocking::Client::new(); + let rss = client.get(&args.link).send().map_err(|e| { + TrsError::ReqwestError( + e, + "Unable to download provided RSS channel link".to_string(), + ) + })?; + + // TODO: Streaming read + let bytes = rss.bytes().map_err(|e| { + TrsError::ReqwestError(e, "Unable to read bytes from RSS response".to_string()) + })?; + + let xml_source_stream = xml::ParserConfig::new() + .ignore_invalid_encoding_declarations(true) + .create_reader(&bytes[..]); + let channel = parser::parse_rss_channel(xml_source_stream)?; + db.add_channel + .execute((channel.title, &args.link, channel.description)) + .map_err(|e| TrsError::SqlError(e, "Failed to insert channel into database".to_string()))?; + + Ok(()) +} + +fn list_channels(conn: &mut Db, args: &ListChannelArgs) -> Result<(), TrsError> { + let channels_iter = + conn.list_channels + .query_map([args.limit.unwrap_or_else(|| 999)], |row| { + Ok(( + row.get::<_, i64>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + )) + })?; + + for row in channels_iter { + let (id, name, link, description) = row?; + println!( + "ID: {}, Name: {}, Link: {}, Description: {}", + id, name, link, description + ); + } + + Ok(()) +} + +fn delete_channel(db: &mut Db, args: &RemoveChannelArgs) -> Result<(), TrsError> { + let rows_affected = db + .remove_channel + .execute([args.id]) + .map_err(|e| TrsError::SqlError(e, "Failed to delete channel from database".to_string()))?; + + if rows_affected == 0 { + return Err(TrsError::Error(format!( + "No channel found with ID: {}", + args.id + ))); + } + + println!("Channel with ID {} deleted successfully.", args.id); + Ok(()) +} diff --git a/src/error.rs b/src/error.rs index 7ecab83..452f57b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,8 +1,11 @@ use std::fmt::Display; +pub type Result = std::result::Result; + #[derive(Debug)] pub enum TrsError { Error(String), + TuiError(std::io::Error), XmlRsError(xml::reader::Error, String), SqlError(rusqlite::Error, String), ReqwestError(reqwest::Error, String), @@ -26,8 +29,9 @@ impl Display for TrsError { f, "{}", match self { - TrsError::Error(msg) => format!("XML Parse Error: {}", msg), - TrsError::XmlRsError(err, msg) => format!("{} XML Rs error {}", msg, err), + TrsError::Error(msg) => format!("{}", msg), + TrsError::TuiError(err) => format!("TUI Error: {}", err), + TrsError::XmlRsError(err, msg) => format!("XML Rs Error: {} {}", msg, err), TrsError::SqlError(err, msg) => format!("SQL Error: {} - {}", err, msg), TrsError::ReqwestError(err, msg) => format!("Reqwest Error: {} - {}", err, msg), } diff --git a/src/main.rs b/src/main.rs index 3b6227c..39f2f83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,107 +1,25 @@ -use std::env; - -use args::{TrsArgs, TrsSubCommand}; -use error::TrsError; -use rusqlite::Connection; +use args::TrsArgs; +use error::Result; pub mod args; +pub mod commands; pub mod error; pub mod parser; +pub mod persistence; +pub mod ui; + +fn main() -> Result<()> { + if std::env::args().len() < 2 { + let terminal = ratatui::init(); + ui::ui(terminal)?; + ratatui::restore(); + return Ok(()); + } -fn main() -> Result<(), TrsError> { let args = argh::from_env::(); - - let conn = init_db()?; - match args.sub_command { - TrsSubCommand::AddChannel(add_channel_args) => { - add_channel(&conn, &add_channel_args.link)?; - } - TrsSubCommand::ListChannels(list_channel_args) => { - list_channels(&conn, list_channel_args.limit)?; - } - } - - Ok(()) -} - -fn add_channel(conn: &Connection, link: &str) -> Result<(), TrsError> { - let client = reqwest::blocking::Client::new(); - let rss = client.get(link).send().map_err(|e| { - TrsError::ReqwestError( - e, - "Unable to download provided RSS channel link".to_string(), - ) - })?; - - // TODO: Streaming read - let bytes = rss.bytes().map_err(|e| { - TrsError::ReqwestError(e, "Unable to read bytes from RSS response".to_string()) - })?; - - let xml_source_stream = xml::ParserConfig::new() - .ignore_invalid_encoding_declarations(true) - .create_reader(&bytes[..]); - let channel = parser::parse_rss_channel(xml_source_stream)?; - let mut stmt = - conn.prepare( - "INSERT INTO Channels (name, link, description) VALUES (?1, ?2, ?3) ON CONFLICT(link) DO UPDATE SET name=?1, description=?3")?; - stmt.execute((channel.title, link, channel.description)) - .map_err(|e| TrsError::SqlError(e, "Failed to insert channel into database".to_string()))?; - - Ok(()) -} - -fn list_channels(conn: &Connection, limit: Option) -> Result<(), TrsError> { - let mut stmt = conn.prepare("SELECT id, name, link, description FROM Channels")?; - let channels_iter = stmt.query_map([], |row| { - Ok(( - row.get::<_, i64>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - )) - })?; - - for row in channels_iter { - let (id, name, link, description) = row?; - println!( - "ID: {}, Name: {}, Link: {}, Description: {}", - id, name, link, description - ); - } - - Ok(()) -} - -fn init_db() -> Result { - let home_dir = env::home_dir(); - let db_dir = home_dir - .map(|dir| dir.join(".config").join("trs")) - .ok_or(TrsError::Error( - "Unable to determine home directory".to_string(), - ))?; - - match std::fs::create_dir_all(&db_dir) { - Ok(_) => {} - Err(e) => { - return Err(TrsError::Error(format!( - "Failed to create database directory: {}", - e - ))); - } - } - - let db_file = db_dir.join("test.db"); - let conn = Connection::open(db_file)?; - conn.execute( - "CREATE TABLE IF NOT EXISTS Channels ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - link TEXT NOT NULL UNIQUE, - description TEXT - )", - (), - ) - .map_err(|e| TrsError::SqlError(e, "Failed to create Channels table".to_string()))?; - - Ok(conn) + let conn = persistence::init_connection()?; + let db = persistence::init_db(&conn)?; + commands::execute(db, &args).map_err(|e| { + eprintln!("Error executing command: {}", e); + e + }) } diff --git a/src/parser.rs b/src/parser.rs index 1ed7f89..180e8e1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,6 +2,7 @@ use std::io::Read; use xml::{reader::XmlEvent, EventReader}; +use crate::error::Result; use crate::error::TrsError; pub struct RssChannel { @@ -21,7 +22,7 @@ impl RssChannel { } } - fn update_channel_field(&mut self, field: &XmlTagField, value: String) -> Result<(), TrsError> { + fn update_channel_field(&mut self, field: &XmlTagField, value: String) -> Result<()> { let last_article = self.articles.last_mut(); let no_item_error = || { TrsError::Error(format!( @@ -103,9 +104,7 @@ const FIELD_TAG_MAPPINGS: [XmlTagField; 6] = [ XmlTagField::mapping("item > pubDate", "pubDate", XmlField::ItemPubDate), ]; -pub fn parse_rss_channel( - xml_source_stream: EventReader, -) -> Result { +pub fn parse_rss_channel(xml_source_stream: EventReader) -> Result { let mut channel = RssChannel::new(); let mut tag_prefix = ""; let mut current_field: Option<&XmlTagField> = None; diff --git a/src/persistence.rs b/src/persistence.rs new file mode 100644 index 0000000..b7c13b7 --- /dev/null +++ b/src/persistence.rs @@ -0,0 +1,78 @@ +use std::env; + +use rusqlite::{Connection, Statement}; + +use crate::error::Result; +use crate::error::TrsError; + +const CREATE_TABLE: &'static str = "CREATE TABLE IF NOT EXISTS Channels ( \ + id INTEGER PRIMARY KEY, \ + name TEXT NOT NULL, \ + link TEXT NOT NULL UNIQUE, \ + description TEXT \ +)"; + +const ADD_CHANNEL: &'static str = "INSERT INTO Channels (name, link, description) \ + VALUES (?1, ?2, ?3)\ + ON CONFLICT(link) DO UPDATE SET name=?1, description=?3"; +const REMOVE_CHANNEL: &'static str = "DELETE FROM Channels WHERE id = ?1"; +const LIST_CHANNELS: &'static str = "SELECT id, name, link, description FROM Channels LIMIT ?1"; + +pub struct Db<'a> { + pub add_channel: Statement<'a>, + pub remove_channel: Statement<'a>, + pub list_channels: Statement<'a>, +} + +macro_rules! prepare_sql { + ($conn:expr, $sql:expr) => { + $conn.prepare($sql).map_err(|e| { + TrsError::SqlError(e, format!("Failed to prepare SQL statement: {}", $sql)) + }) + }; +} + +impl<'a> Db<'a> { + fn create(conn: &'a Connection) -> Result { + let add_channel = prepare_sql!(conn, ADD_CHANNEL)?; + let remove_channel = prepare_sql!(conn, REMOVE_CHANNEL)?; + let list_channels = prepare_sql!(conn, LIST_CHANNELS)?; + Ok(Db { + add_channel, + remove_channel, + list_channels, + }) + } +} + +pub fn init_db(conn: &Connection) -> Result { + conn.execute(CREATE_TABLE, ()) + .map_err(|e| TrsError::SqlError(e, "Failed to create Channels table".to_string()))?; + Db::create(conn) +} + +pub fn init_connection() -> Result { + let home_dir = env::home_dir(); + let db_dir = home_dir + .map(|dir| dir.join(".config").join("trs")) + .ok_or(TrsError::Error( + "Unable to determine home directory".to_string(), + ))?; + + match std::fs::create_dir_all(&db_dir) { + Ok(_) => {} + Err(e) => { + return Err(TrsError::Error(format!( + "Failed to create database directory: {}", + e + ))); + } + } + + let db_file = db_dir.join("test.db"); + let conn = Connection::open(db_file)?; + conn.execute(CREATE_TABLE, ()) + .map_err(|e| TrsError::SqlError(e, "Failed to create Channels table".to_string()))?; + + Ok(conn) +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..d283042 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,38 @@ +use std::io::Stdout; + +use crate::error::{Result, TrsError}; +use crossterm::event::{self, Event, KeyEventKind}; +use ratatui::{prelude::CrosstermBackend, Terminal}; + +struct AppState { + exit: bool, +} + +pub fn ui(mut terminal: Terminal>) -> Result<()> { + let mut app_state = AppState { exit: false }; + loop { + handle_events(&mut app_state)?; + + if app_state.exit { + break; + } + + draw(&app_state, &mut terminal)?; + } + Ok(()) +} + +fn draw(app_state: &AppState, terminal: &mut Terminal>) -> Result<()> { + todo!() +} + +fn handle_events(state: &mut AppState) -> Result<()> { + let event = event::read().map_err(|e| TrsError::TuiError(e))?; + match event { + Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { + state.exit = true; + } + _ => {} + }; + Ok(()) +}