diff --git a/CHANGELOG.md b/CHANGELOG.md index 3767580..7b2fcbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,13 @@ A great way to contribute is to improve the README or documentation! - Add the import of OPML files (https://github.com/ckampfe/russ/pull/32) - Implement `ctrl-d` and `ctrl-u` for page scrolling, thank you @Jaco-Minnaar (https://github.com/ckampfe/russ/pull/31) +- Big internal rearchitecture to make event handling easier to understand and adapt - Clean up entry insertion code, gets rid of some nasty query string building. - Bump `ratatui` to `0.26` and fix a few resulting breakages - Bump `html2text` to `0.12` - Bump `rusqlite` to `0.31` - Bump `r2d2_sqlite` to `0.24` +- Bump `webbrowser` to `1` - Bump lots of transitive dependencies ## 0.5.0 diff --git a/Cargo.lock b/Cargo.lock index aeb18c0..a0c19c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "once_cell", @@ -22,9 +22,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -43,47 +43,48 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.12" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -91,44 +92,38 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "atom_syndication" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "571832dcff775e26562e8e6930cd483de5587301d40d3a3b85d532b6383e15a7" +checksum = "f2f34613907f31c9dbef0240156db3c9263f34842b6e1a8999d2304ea62c8a30" dependencies = [ "chrono", "diligent-date-parser", - "quick-xml 0.30.0", + "quick-xml", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "bitflags" -version = "1.3.2" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block" @@ -136,17 +131,26 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" -version = "3.15.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "calloop" @@ -154,7 +158,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" dependencies = [ - "bitflags 2.4.2", + "bitflags", "log", "polling", "rustix", @@ -191,12 +195,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cesu8" @@ -212,21 +213,21 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.0", + "windows-targets 0.52.5", ] [[package]] name = "clap" -version = "4.5.1" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -234,9 +235,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -246,14 +247,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.66", ] [[package]] @@ -274,15 +275,15 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "combine" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", @@ -303,9 +304,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -342,18 +343,18 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -361,7 +362,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.4.2", + "bitflags", "crossterm_winapi", "libc", "mio", @@ -386,19 +387,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" -[[package]] -name = "dashmap" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" -dependencies = [ - "cfg-if", - "hashbrown 0.12.3", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "diligent-date-parser" version = "0.1.4" @@ -440,30 +428,30 @@ dependencies = [ [[package]] name = "downcast-rs" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "either" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -483,9 +471,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -522,9 +510,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -533,9 +521,9 @@ dependencies = [ [[package]] name = "hard-xml" -version = "1.34.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fafc2bcb74049535eb6fab49eb20164a427867a9e809516ef95a98e961164432" +checksum = "a344e0cef8802f37dc47f17c01a04354d3e66d9f6c8744108b0912f616efe266" dependencies = [ "hard-xml-derive", "jetscii", @@ -546,11 +534,11 @@ dependencies = [ [[package]] name = "hard-xml-derive" -version = "1.34.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a345b327da51b997c94f841d9db6b2d292c7632713bd8a1b8b191e8b819df7" +checksum = "1bfae7cdfe23e50ea96929ccf1948d9ae1d8608353556461e5de247463d3a4f6" dependencies = [ - "bitflags 1.3.2", + "bitflags", "proc-macro2", "quote", "syn 1.0.109", @@ -558,15 +546,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -574,11 +556,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown 0.14.3", + "hashbrown", ] [[package]] @@ -587,11 +569,17 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "home" @@ -604,31 +592,29 @@ dependencies = [ [[package]] name = "html2text" -version = "0.12.0" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f0de8bd2c9fe69eb3fa29e41ae1396c9d2e1b807735acaecafb4c86888674f" +checksum = "8c66ee488a63a92237d5b48875b7e05bb293be8fb2894641c8118b60c08ab5ef" dependencies = [ - "dashmap", "html5ever", "markup5ever", "tendril", "thiserror", "unicode-width", - "xml5ever", ] [[package]] name = "html5ever" -version = "0.26.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" dependencies = [ "log", "mac", "markup5ever", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.66", ] [[package]] @@ -665,10 +651,10 @@ dependencies = [ ] [[package]] -name = "indoc" -version = "2.0.4" +name = "is_terminal_polyfill" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] name = "itertools" @@ -681,9 +667,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jetscii" @@ -715,9 +701,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -736,29 +722,28 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-targets 0.52.5", ] [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.4.2", + "bitflags", "libc", - "redox_syscall", ] [[package]] @@ -774,15 +759,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -790,17 +775,17 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "lru" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ - "hashbrown 0.14.3", + "hashbrown", ] [[package]] @@ -820,9 +805,9 @@ dependencies = [ [[package]] name = "markup5ever" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" dependencies = [ "log", "phf", @@ -834,9 +819,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memmap2" @@ -849,18 +834,18 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", @@ -876,15 +861,15 @@ checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "new_debug_unreachable" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -919,6 +904,40 @@ dependencies = [ "objc_id", ] +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + [[package]] name = "objc_id" version = "0.1.1" @@ -953,9 +972,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -963,22 +982,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" @@ -988,21 +1007,21 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "phf" -version = "0.10.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_shared", + "phf_shared 0.11.2", ] [[package]] name = "phf_codegen" -version = "0.10.0" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.11.2", + "phf_shared 0.11.2", ] [[package]] @@ -1011,7 +1030,17 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ - "phf_shared", + "phf_shared 0.10.0", + "rand", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", "rand", ] @@ -1024,11 +1053,20 @@ dependencies = [ "siphasher", ] +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pkg-config" @@ -1038,12 +1076,13 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polling" -version = "3.5.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9" +checksum = "5e6a007746f34ed64099e88783b0ae369eaa3da6392868ba262e2af9b8fbaea1" dependencies = [ "cfg-if", "concurrent-queue", + "hermit-abi", "pin-project-lite", "rustix", "tracing", @@ -1064,37 +1103,28 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" dependencies = [ "unicode-ident", ] -[[package]] -name = "quick-xml" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" -dependencies = [ - "encoding_rs", - "memchr", -] - [[package]] name = "quick-xml" version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ + "encoding_rs", "memchr", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1153,44 +1183,38 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.1" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" dependencies = [ - "bitflags 2.4.2", + "bitflags", "cassowary", "compact_str", "crossterm", - "indoc", "itertools", "lru", "paste", "stability", "strum", "unicode-segmentation", + "unicode-truncate", "unicode-width", ] -[[package]] -name = "raw-window-handle" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" - [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 1.3.2", + "bitflags", ] [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", @@ -1214,11 +1238,11 @@ dependencies = [ [[package]] name = "rss" -version = "2.0.7" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7b2c77eb4450d7d5f98df52c381cd6c4e19b75dad9209a9530b85a44510219a" +checksum = "2f374fd66bb795938b78c021db1662d43a8ffbc42ec1ac25429fc4833b732751" dependencies = [ - "quick-xml 0.30.0", + "quick-xml", ] [[package]] @@ -1227,7 +1251,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags 2.4.2", + "bitflags", "chrono", "fallible-iterator", "fallible-streaming-iterator", @@ -1263,11 +1287,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.4.2", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -1276,9 +1300,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.2" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring", @@ -1290,15 +1314,15 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.3.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ "ring", "rustls-pki-types", @@ -1307,15 +1331,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -1349,22 +1373,22 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.196" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.66", ] [[package]] @@ -1390,9 +1414,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -1414,9 +1438,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smithay-client-toolkit" @@ -1424,7 +1448,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" dependencies = [ - "bitflags 2.4.2", + "bitflags", "calloop", "calloop-wayland-source", "cursor-icon", @@ -1445,9 +1469,9 @@ dependencies = [ [[package]] name = "smithay-clipboard" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb62b280ce5a5cba847669933a0948d00904cf83845c944eae96a4738cea1a6" +checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" dependencies = [ "libc", "smithay-client-toolkit", @@ -1462,12 +1486,12 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "stability" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" dependencies = [ "quote", - "syn 1.0.109", + "syn 2.0.66", ] [[package]] @@ -1485,7 +1509,7 @@ dependencies = [ "new_debug_unreachable", "once_cell", "parking_lot", - "phf_shared", + "phf_shared 0.10.0", "precomputed-hash", "serde", ] @@ -1496,38 +1520,38 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.10.0", + "phf_shared 0.10.0", "proc-macro2", "quote", ] [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", - "syn 2.0.49", + "syn 2.0.66", ] [[package]] @@ -1549,9 +1573,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.49" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -1571,22 +1595,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.66", ] [[package]] @@ -1634,9 +1658,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] @@ -1647,11 +1671,21 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-truncate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" +dependencies = [ + "itertools", + "unicode-width", +] + [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "untrusted" @@ -1661,9 +1695,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.6" +version = "2.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35" +checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" dependencies = [ "base64", "flate2", @@ -1701,9 +1735,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", "rand", @@ -1723,9 +1757,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -1739,9 +1773,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1749,24 +1783,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.66", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1774,28 +1808,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wayland-backend" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" +checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07" dependencies = [ "cc", "downcast-rs", @@ -1807,11 +1841,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" +checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133" dependencies = [ - "bitflags 2.4.2", + "bitflags", "rustix", "wayland-backend", "wayland-scanner", @@ -1823,16 +1857,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.4.2", + "bitflags", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.1" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ce5fa868dd13d11a0d04c5e2e65726d0897be8de247c0c5a65886e283231ba" +checksum = "a206e8b2b53b1d3fcb9428fec72bc278ce539e2fa81fe2bfc1ab27703d5187b9" dependencies = [ "rustix", "wayland-client", @@ -1845,7 +1879,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.4.2", + "bitflags", "wayland-backend", "wayland-client", "wayland-scanner", @@ -1857,7 +1891,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ - "bitflags 2.4.2", + "bitflags", "wayland-backend", "wayland-client", "wayland-protocols", @@ -1866,20 +1900,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" +checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565" dependencies = [ "proc-macro2", - "quick-xml 0.31.0", + "quick-xml", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" +checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12" dependencies = [ "dlib", "log", @@ -1889,9 +1923,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -1899,17 +1933,18 @@ dependencies = [ [[package]] name = "webbrowser" -version = "0.8.12" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b2391658b02c27719fc5a0a73d6e696285138e8b12fba9d4baa70451023c71" +checksum = "425ba64c1e13b1c6e8c5d2541c8fac10022ca584f33da781db01b5756aef1f4e" dependencies = [ + "block2", "core-foundation", "home", "jni", "log", "ndk-context", - "objc", - "raw-window-handle", + "objc2", + "objc2-foundation", "url", "web-sys", ] @@ -1941,11 +1976,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1960,7 +1995,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.5", ] [[package]] @@ -1987,7 +2022,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.5", ] [[package]] @@ -2022,17 +2057,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -2049,9 +2085,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -2067,9 +2103,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -2085,9 +2121,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -2103,9 +2145,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -2121,9 +2163,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -2139,9 +2181,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -2157,9 +2199,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "wsl" @@ -2169,9 +2211,9 @@ checksum = "f8dab7ac864710bdea6594becbea5b5050333cf34fefb0dc319567eb347950d4" [[package]] name = "x11-clipboard" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613c2be3e772af2bbb57c5a94413675f5ec668bac00a71ada2ced28c420ef087" +checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" dependencies = [ "libc", "x11rb", @@ -2179,9 +2221,9 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", "rustix", @@ -2190,9 +2232,9 @@ dependencies = [ [[package]] name = "x11rb-protocol" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xcursor" @@ -2206,17 +2248,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" -[[package]] -name = "xml5ever" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650" -dependencies = [ - "log", - "mac", - "markup5ever", -] - [[package]] name = "xmlparser" version = "0.13.6" @@ -2225,26 +2256,26 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.66", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index 48ff600..a94fbeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ rss = { version = "2.0", default-features = false } rusqlite = { version = "0.31", features = ["bundled", "chrono"] } ratatui = "0.26" ureq = "2.9" -webbrowser = "0.8" +webbrowser = "1" wsl = "0.1" [profile.release] diff --git a/src/app.rs b/src/app.rs index 67a128c..2c5d628 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,8 +1,9 @@ +//! The main application state is managed here, in `App`. + use crate::modes::{Mode, ReadMode, Selected}; use crate::util; use anyhow::Result; use copypasta::{ClipboardContext, ClipboardProvider}; -use crossterm::event::{KeyCode, KeyModifiers}; use ratatui::{backend::CrosstermBackend, Terminal}; use std::sync::{Arc, Mutex}; @@ -37,20 +38,21 @@ impl App { delegate_to_locked_inner![ (error_flash_is_empty, bool), (feed_ids, Result>), - (feed_subscription_input, String), (force_redraw, Result<()>), (http_client, ureq::Agent), (mode, Mode), (selected, Selected), - (selected_feed_id, crate::rss::FeedId), (open_link_in_browser, Result<()>), + (should_quit, bool), + (refresh_feed, Result<()>), + (subscribe_to_feed, Result<()>), + (feed_subscription_input_is_empty, bool) ]; delegate_to_locked_mut_inner![ (clear_error_flash, ()), (clear_flash, ()), (on_down, Result<()>), - (on_enter, Result<()>), (on_left, Result<()>), (on_right, Result<()>), (on_up, Result<()>), @@ -65,14 +67,16 @@ impl App { (toggle_read, Result<()>), (toggle_read_mode, Result<()>), (update_current_feed_and_entries, Result<()>), + (select_and_show_current_entry, Result<()>) ]; pub fn new( options: crate::ReadOptions, - event_s: std::sync::mpsc::Sender>, + event_tx: std::sync::mpsc::Sender>, + io_tx: std::sync::mpsc::Sender, ) -> Result { Ok(App { - inner: Arc::new(Mutex::new(AppImpl::new(options, event_s)?)), + inner: Arc::new(Mutex::new(AppImpl::new(options, event_tx, io_tx)?)), }) } @@ -91,7 +95,7 @@ impl App { if inner.entry_column_width != new_width { inner.entry_column_width = new_width; - inner.on_enter().unwrap_or_else(|e| { + inner.select_and_show_current_entry().unwrap_or_else(|e| { inner.error_flash = vec![e]; }) } @@ -104,34 +108,9 @@ impl App { Ok(()) } - pub fn on_key(&self, keycode: KeyCode, modifiers: KeyModifiers) -> Result<()> { - match (keycode, modifiers) { - // movement - (KeyCode::Left, _) | (KeyCode::Char('h'), _) => self.on_left(), - (KeyCode::Down, _) | (KeyCode::Char('j'), _) => self.on_down(), - (KeyCode::Up, _) | (KeyCode::Char('k'), _) => self.on_up(), - (KeyCode::Right, _) | (KeyCode::Char('l'), _) => self.on_right(), - (KeyCode::PageUp, _) | (KeyCode::Char('u'), KeyModifiers::CONTROL) => { - self.page_up(); - Ok(()) - } - (KeyCode::PageDown, _) | (KeyCode::Char('d'), KeyModifiers::CONTROL) => { - self.page_down(); - Ok(()) - } - // modes, selections, editing, etc. - (KeyCode::Enter, _) => self.on_enter(), - (KeyCode::Char('?'), _) => self.toggle_help(), - (KeyCode::Char('a'), _) => self.toggle_read_mode(), - (KeyCode::Char('e'), _) | (KeyCode::Char('i'), _) => { - let mut inner = self.inner.lock().unwrap(); - inner.mode = Mode::Editing; - Ok(()) - } - (KeyCode::Char('c'), _) => self.put_current_link_in_clipboard(), - (KeyCode::Char('o'), _) => self.open_link_in_browser(), - _ => Ok(()), - } + pub fn set_should_quit(&mut self, should_quit: bool) { + let mut inner = self.inner.lock().unwrap(); + inner.should_quit = should_quit } pub fn set_flash(&self, flash: String) { @@ -159,6 +138,31 @@ impl App { let feeds = feeds.into(); inner.feeds = feeds; } + + pub(crate) fn refresh_feeds(&self) -> Result<()> { + let feed_ids = self.feed_ids()?; + let inner = self.inner.lock().unwrap(); + inner + .io_tx + .send(crate::io::Action::RefreshFeeds(feed_ids))?; + Ok(()) + } + + pub(crate) fn break_io_thread(&self) -> Result<()> { + let inner = self.inner.lock().unwrap(); + inner.io_tx.send(crate::io::Action::Break)?; + Ok(()) + } + + pub(crate) fn has_entries(&self) -> bool { + let inner = self.inner.lock().unwrap(); + !inner.entries.items.is_empty() + } + + pub(crate) fn has_current_entry(&self) -> bool { + let inner = self.inner.lock().unwrap(); + inner.current_entry_meta.is_some() + } } #[derive(Debug)] @@ -171,8 +175,8 @@ pub struct AppImpl { pub current_feed: Option, pub feeds: util::StatefulList, // entry stuff - pub current_entry_meta: Option, - pub entries: util::StatefulList, + pub current_entry_meta: Option, + pub entries: util::StatefulList, pub entry_selection_position: usize, pub current_entry_text: String, pub entry_scroll_position: u16, @@ -189,14 +193,16 @@ pub struct AppImpl { pub error_flash: Vec, pub feed_subscription_input: String, pub flash: Option, - event_s: std::sync::mpsc::Sender>, + event_tx: std::sync::mpsc::Sender>, + io_tx: std::sync::mpsc::Sender, pub is_wsl: bool, } impl AppImpl { pub fn new( options: crate::ReadOptions, - event_s: std::sync::mpsc::Sender>, + event_tx: std::sync::mpsc::Sender>, + io_tx: std::sync::mpsc::Sender, ) -> Result { let mut conn = rusqlite::Connection::open(&options.database_path)?; @@ -206,7 +212,7 @@ impl AppImpl { crate::rss::initialize_db(&mut conn)?; let feeds: util::StatefulList = vec![].into(); - let entries: util::StatefulList = vec![].into(); + let entries: util::StatefulList = vec![].into(); // default to having nothing selected, // as it's possible we are starting for the first time, // with an empty feeds db @@ -236,8 +242,9 @@ impl AppImpl { show_help: true, entry_selection_position: 0, flash: None, - event_s, + event_tx, is_wsl, + io_tx, }; app.update_feeds()?; @@ -346,7 +353,7 @@ impl AppImpl { }; } - fn get_selected_entry(&self) -> Option> { + fn get_selected_entry_content(&self) -> Option> { self.entries.state.selected().and_then(|selected_idx| { self.entries .items @@ -356,7 +363,7 @@ impl AppImpl { }) } - fn get_selected_entry_meta(&self) -> Option> { + fn get_selected_entry_meta(&self) -> Option> { self.entries.state.selected().and_then(|selected_idx| { self.entries .items @@ -400,53 +407,59 @@ impl AppImpl { } } - pub fn on_enter(&mut self) -> Result<()> { - match self.selected { - Selected::Entries | Selected::Entry(_) => { - if !self.entries.items.is_empty() { - if let Some(entry_meta) = &self.current_entry_meta { - if let Some(entry) = self.get_selected_entry() { - let entry = entry?; - let empty_string = - String::from("No content or description tag provided."); - - // try content tag first, - // if there is not content tag, - // go to description tag, - // if no description tag, - // use empty string. - // TODO figure out what to actually do if there are neither - let entry_html = entry - .content - .as_ref() - .or(entry.description.as_ref()) - .or(Some(&empty_string)); - - // minimum is 1 - let line_length = if self.entry_column_width >= 5 { - self.entry_column_width - 4 - } else { - 1 - }; - - if let Some(html) = entry_html { - let text = - html2text::from_read(html.as_bytes(), line_length.into()); - self.entry_lines_len = text.matches('\n').count(); - self.current_entry_text = text; - } else { - self.current_entry_text = String::new(); - } - } - - self.selected = Selected::Entry(entry_meta.clone()); - } - } + pub(crate) fn select_and_show_current_entry(&mut self) -> Result<()> { + if let Some(entry_meta) = &self.current_entry_meta { + let entry_meta = entry_meta.clone(); + + if let Some(entry) = self.get_selected_entry_content() { + let entry = entry?; + let empty_string = String::from("No content or description tag provided."); + + // try content tag first, + // if there is not content tag, + // go to description tag, + // if no description tag, + // use empty string. + // TODO figure out what to actually do if there are neither + let entry_html = entry + .content + .as_ref() + .or(entry.description.as_ref()) + .or(Some(&empty_string)); + + // minimum is 1 + let line_length = if self.entry_column_width >= 5 { + self.entry_column_width - 4 + } else { + 1 + }; - Ok(()) + if let Some(html) = entry_html { + let text = html2text::from_read(html.as_bytes(), line_length.into()); + self.entry_lines_len = text.matches('\n').count(); + self.current_entry_text = text; + } else { + self.current_entry_text = String::new(); + } } - _ => Ok(()), + + self.selected = Selected::Entry(entry_meta); } + + Ok(()) + } + + pub(crate) fn refresh_feed(&self) -> Result<()> { + let feed_id = self.selected_feed_id(); + self.io_tx.send(crate::io::Action::RefreshFeed(feed_id))?; + Ok(()) + } + + pub(crate) fn subscribe_to_feed(&self) -> Result<()> { + let feed_subscription_input = self.feed_subscription_input(); + self.io_tx + .send(crate::io::Action::SubscribeToFeed(feed_subscription_input))?; + Ok(()) } pub fn toggle_help(&mut self) -> Result<()> { @@ -466,6 +479,10 @@ impl AppImpl { self.feed_subscription_input.pop(); } + pub fn feed_subscription_input_is_empty(&self) -> bool { + self.feed_subscription_input.is_empty() + } + pub fn feed_subscription_input(&self) -> String { self.feed_subscription_input.clone() } @@ -497,8 +514,7 @@ impl AppImpl { } pub fn toggle_read(&mut self) -> Result<()> { - let selected = self.selected.clone(); - match selected { + match &self.selected { Selected::Entry(entry) => { entry.toggle_read(&self.conn)?; self.selected = Selected::Entries; @@ -601,6 +617,10 @@ impl AppImpl { } } + fn should_quit(&self) -> bool { + self.should_quit + } + pub fn on_left(&mut self) -> Result<()> { match self.selected { Selected::Feeds => (), @@ -655,7 +675,7 @@ impl AppImpl { } Ok(()) } - Selected::Entries => self.on_enter(), + Selected::Entries => self.select_and_show_current_entry(), Selected::Entry(_) => Ok(()), Selected::None => Ok(()), } @@ -690,6 +710,6 @@ impl AppImpl { } pub fn force_redraw(&self) -> Result<()> { - self.event_s.send(crate::Event::Tick).map_err(|e| e.into()) + self.event_tx.send(crate::Event::Tick).map_err(|e| e.into()) } } diff --git a/src/io.rs b/src/io.rs new file mode 100644 index 0000000..60a4234 --- /dev/null +++ b/src/io.rs @@ -0,0 +1,197 @@ +//! This module provides a way to asynchronously refresh feeds, using threads + +use crate::app::App; +use crate::modes::Mode; +use crate::ReadOptions; +use anyhow::Result; + +pub(crate) enum Action { + Break, + RefreshFeed(crate::rss::FeedId), + RefreshFeeds(Vec), + SubscribeToFeed(String), + ClearFlash, +} + +/// A loop to process `io::Action` messages. +pub(crate) fn io_loop( + app: App, + io_tx: std::sync::mpsc::Sender, + io_rx: std::sync::mpsc::Receiver, + options: &ReadOptions, +) -> Result<()> { + let manager = r2d2_sqlite::SqliteConnectionManager::file(&options.database_path); + let connection_pool = r2d2::Pool::new(manager)?; + + while let Ok(event) = io_rx.recv() { + match event { + Action::Break => break, + Action::RefreshFeed(feed_id) => { + let now = std::time::Instant::now(); + + app.set_flash("Refreshing feed...".to_string()); + app.force_redraw()?; + + refresh_feeds(&app, &connection_pool, &[feed_id], |_app, fetch_result| { + if let Err(e) = fetch_result { + app.push_error_flash(e) + } + })?; + + app.update_current_feed_and_entries()?; + let elapsed = now.elapsed(); + app.set_flash(format!("Refreshed feed in {elapsed:?}")); + app.force_redraw()?; + clear_flash_after(io_tx.clone(), options.flash_display_duration_seconds); + } + Action::RefreshFeeds(feed_ids) => { + let now = std::time::Instant::now(); + + app.set_flash("Refreshing all feeds...".to_string()); + app.force_redraw()?; + + let all_feeds_len = feed_ids.len(); + let mut successfully_refreshed_len = 0usize; + + refresh_feeds(&app, &connection_pool, &feed_ids, |app, fetch_result| { + match fetch_result { + Ok(_) => successfully_refreshed_len += 1, + Err(e) => app.push_error_flash(e), + } + })?; + + { + app.update_current_feed_and_entries()?; + + let elapsed = now.elapsed(); + app.set_flash(format!( + "Refreshed {successfully_refreshed_len}/{all_feeds_len} feeds in {elapsed:?}" + )); + app.force_redraw()?; + } + + clear_flash_after(io_tx.clone(), options.flash_display_duration_seconds); + } + Action::SubscribeToFeed(feed_subscription_input) => { + let now = std::time::Instant::now(); + + app.set_flash("Subscribing to feed...".to_string()); + app.force_redraw()?; + + let mut conn = connection_pool.get()?; + let r = crate::rss::subscribe_to_feed( + &app.http_client(), + &mut conn, + &feed_subscription_input, + ); + + if let Err(e) = r { + app.push_error_flash(e); + continue; + } + + match crate::rss::get_feeds(&conn) { + Ok(feeds) => { + { + app.reset_feed_subscription_input(); + app.set_feeds(feeds); + app.select_feeds(); + app.update_current_feed_and_entries()?; + + let elapsed = now.elapsed(); + app.set_flash(format!("Subscribed in {elapsed:?}")); + app.set_mode(Mode::Normal); + app.force_redraw()?; + } + + clear_flash_after(io_tx.clone(), options.flash_display_duration_seconds); + } + Err(e) => { + app.push_error_flash(e); + } + } + } + Action::ClearFlash => { + app.clear_flash(); + } + } + } + + Ok(()) +} + +/// Refreshes the feeds of the given `feed_ids` by splitting them into +/// chunks based on the number of available CPUs. +/// Each chunk is then passed to its own thread, +/// where each feed_id in the chunk has its feed refreshed synchronously on that thread. +fn refresh_feeds( + app: &App, + connection_pool: &r2d2::Pool, + feed_ids: &[crate::rss::FeedId], + mut refresh_result_handler: F, +) -> Result<()> +where + F: FnMut(&App, anyhow::Result<()>), +{ + let chunks = chunkify_for_threads(feed_ids, num_cpus::get() * 2); + + let join_handles: Vec<_> = chunks + .map(|chunk| { + let pool_get_result = connection_pool.get(); + let http_client = app.http_client(); + let chunk = chunk.to_owned(); + + std::thread::spawn(move || -> Result>> { + let mut conn = pool_get_result?; + + let results = chunk + .into_iter() + .map(|feed_id| crate::rss::refresh_feed(&http_client, &mut conn, feed_id)) + .collect(); + + Ok::>, anyhow::Error>(results) + }) + }) + .collect(); + + for join_handle in join_handles { + let chunk_results = join_handle + .join() + .expect("unable to join worker thread to io thread"); + for chunk_result in chunk_results? { + refresh_result_handler(app, chunk_result) + } + } + + Ok(()) +} + +/// split items into chunks, +/// with the idea being that each chunk will be run on its own thread +fn chunkify_for_threads( + items: &[T], + minimum_number_of_threads: usize, +) -> impl Iterator { + // example: 25 items / 16 threads = chunk size of 1 + // example: 100 items / 16 threads = chunk size of 6 + // example: 10 items / 16 threads = chunk size of 0 (handled later) + // + // due to usize floor division, it's possible chunk_size would be 0, + // so ensure it is at least 1 + let chunk_size = (items.len() / minimum_number_of_threads).max(1); + + // now we have (len / chunk_size) chunks, + // example: + // 25 items / chunks size of 1 = 25 chunks + // 100 items / chunk size of 6 = 16 chunks + items.chunks(chunk_size) +} + +/// clear the flash after a given duration +fn clear_flash_after(tx: std::sync::mpsc::Sender, duration: std::time::Duration) { + std::thread::spawn(move || { + std::thread::sleep(duration); + tx.send(Action::ClearFlash) + .expect("Unable to send IOCommand::ClearFlash"); + }); +} diff --git a/src/main.rs b/src/main.rs index f885e4f..7465408 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use crate::modes::{Mode, Selected}; use anyhow::Result; use app::App; use clap::{Parser, Subcommand}; -use crossterm::event; +use crossterm::event::{self, KeyEvent}; use crossterm::event::{Event as CEvent, KeyCode, KeyModifiers}; use crossterm::execute; use crossterm::terminal::{ @@ -18,15 +18,22 @@ use std::sync::mpsc; use std::{thread, time}; mod app; +mod io; mod modes; mod opml; mod rss; mod ui; mod util; -pub enum Event { - Input(I), - Tick, +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), + } } /// A TUI RSS reader with vim-like controls and a local-first, offline-first focus @@ -155,175 +162,9 @@ fn get_database_path(database_path: &Option) -> std::io::Result), - SubscribeToFeed(String), - ClearFlash, -} - -fn io_loop( - app: App, - sx: mpsc::Sender, - rx: mpsc::Receiver, - options: &ReadOptions, -) -> Result<()> { - use IoCommand::*; - - let manager = r2d2_sqlite::SqliteConnectionManager::file(&options.database_path); - let connection_pool = r2d2::Pool::new(manager)?; - - while let Ok(event) = rx.recv() { - match event { - Break => break, - RefreshFeed(feed_id) => { - let now = std::time::Instant::now(); - - app.set_flash("Refreshing feed...".to_string()); - app.force_redraw()?; - - refresh_feeds(&app, &connection_pool, &[feed_id], |_app, fetch_result| { - if let Err(e) = fetch_result { - app.push_error_flash(e) - } - })?; - - app.update_current_feed_and_entries()?; - let elapsed = now.elapsed(); - app.set_flash(format!("Refreshed feed in {elapsed:?}")); - app.force_redraw()?; - clear_flash_after(sx.clone(), options.flash_display_duration_seconds); - } - RefreshFeeds(feed_ids) => { - let now = std::time::Instant::now(); - - app.set_flash("Refreshing all feeds...".to_string()); - app.force_redraw()?; - - let all_feeds_len = feed_ids.len(); - let mut successfully_refreshed_len = 0usize; - - refresh_feeds(&app, &connection_pool, &feed_ids, |app, fetch_result| { - match fetch_result { - Ok(_) => successfully_refreshed_len += 1, - Err(e) => app.push_error_flash(e), - } - })?; - - { - app.update_current_feed_and_entries()?; - - let elapsed = now.elapsed(); - app.set_flash(format!( - "Refreshed {successfully_refreshed_len}/{all_feeds_len} feeds in {elapsed:?}" - )); - app.force_redraw()?; - } - - clear_flash_after(sx.clone(), options.flash_display_duration_seconds); - } - SubscribeToFeed(feed_subscription_input) => { - let now = std::time::Instant::now(); - - app.set_flash("Subscribing to feed...".to_string()); - app.force_redraw()?; - - let mut conn = connection_pool.get()?; - let r = crate::rss::subscribe_to_feed( - &app.http_client(), - &mut conn, - &feed_subscription_input, - ); - - if let Err(e) = r { - app.push_error_flash(e); - continue; - } - - match crate::rss::get_feeds(&conn) { - Ok(feeds) => { - { - app.reset_feed_subscription_input(); - app.set_feeds(feeds); - app.select_feeds(); - app.update_current_feed_and_entries()?; - - let elapsed = now.elapsed(); - app.set_flash(format!("Subscribed in {elapsed:?}")); - app.set_mode(Mode::Normal); - app.force_redraw()?; - } - - clear_flash_after(sx.clone(), options.flash_display_duration_seconds); - } - Err(e) => { - app.push_error_flash(e); - } - } - } - ClearFlash => { - app.clear_flash(); - } - } - } - - Ok(()) -} - -fn refresh_feeds( - app: &App, - connection_pool: &r2d2::Pool, - feed_ids: &[crate::rss::FeedId], - mut refresh_result_handler: F, -) -> Result<()> -where - F: FnMut(&App, anyhow::Result<()>), -{ - let min_number_of_threads = num_cpus::get() * 2; - let chunk_size = feed_ids.len() / min_number_of_threads; - // due to usize floor division, it's possible chunk_size would be 0, - // so ensure it is at least 1 - let chunk_size = chunk_size.max(1); - let chunks = feed_ids.chunks(chunk_size); - - let join_handles: Vec<_> = chunks - .map(|chunk_feed_ids| { - let pool_get_result = connection_pool.get(); - let http = app.http_client(); - let chunk_feed_ids = chunk_feed_ids.to_owned(); - - thread::spawn(move || -> Result>> { - let mut results = vec![]; - let mut conn = pool_get_result?; - - for feed_id in chunk_feed_ids.into_iter() { - results.push(crate::rss::refresh_feed(&http, &mut conn, feed_id)) - } - - Ok::>, anyhow::Error>(results) - }) - }) - .collect(); - - for join_handle in join_handles { - let chunk_results = join_handle - .join() - .expect("unable to join worker thread to io thread"); - for chunk_result in chunk_results? { - refresh_result_handler(app, chunk_result) - } - } - - Ok(()) -} - -fn clear_flash_after(sx: mpsc::Sender, duration: time::Duration) { - thread::spawn(move || { - thread::sleep(duration); - sx.send(IoCommand::ClearFlash) - .expect("Unable to send IOCommand::ClearFlash"); - }); +pub enum Event { + Input(I), + Tick, } fn run_reader(options: ReadOptions) -> Result<()> { @@ -338,10 +179,12 @@ fn run_reader(options: ReadOptions) -> Result<()> { terminal.hide_cursor()?; // Setup input handling - let (tx, rx) = mpsc::channel(); - let tx_clone = tx.clone(); + let (event_tx, event_rx) = mpsc::channel(); + + let event_tx_clone = event_tx.clone(); let tick_rate = time::Duration::from_millis(options.tick_rate); + thread::spawn(move || { let mut last_tick = time::Instant::now(); loop { @@ -350,12 +193,13 @@ fn run_reader(options: ReadOptions) -> Result<()> { .expect("Unable to poll for Crossterm event") { if let CEvent::Key(key) = event::read().expect("Unable to read Crossterm event") { - tx.send(Event::Input(key)) + event_tx + .send(Event::Input(key)) .expect("Unable to send Crossterm Key input event"); } } if last_tick.elapsed() >= tick_rate { - tx.send(Event::Tick).expect("Unable to send tick"); + event_tx.send(Event::Tick).expect("Unable to send tick"); last_tick = time::Instant::now(); } } @@ -363,92 +207,44 @@ fn run_reader(options: ReadOptions) -> Result<()> { let options_clone = options.clone(); - let app = App::new(options, tx_clone)?; + let (io_tx, io_rx) = mpsc::channel(); - let cloned_app = app.clone(); + let io_tx_clone = io_tx.clone(); - terminal.clear()?; + let mut app = App::new(options, event_tx_clone, io_tx)?; - let (io_s, io_r) = mpsc::channel(); + let cloned_app = app.clone(); - let io_s_clone = io_s.clone(); + terminal.clear()?; // spawn this thread to handle receiving messages to performing blocking network and db IO let io_thread = thread::spawn(move || -> Result<()> { - io_loop(cloned_app, io_s_clone, io_r, &options_clone) + io::io_loop(cloned_app, io_tx_clone, io_rx, &options_clone) }); - // MAIN THREAD IS DRAW THREAD + // this is basically "the Elm Architecture". + // + // more or less: + // ui <- current_state + // action <- current_state + event + // new_state <- current_state + action loop { - let mode = { - app.draw(&mut terminal)?; - app.mode() - }; - - match mode { - Mode::Normal => match rx.recv()? { - Event::Input(event) => match (event.code, event.modifiers) { - // These first few keycodes are handled inline - // because they talk to either the IO thread or the terminal. - // All other keycodes are handled in the final `on_key` - // wildcard pattern, as they do neither. - (KeyCode::Char('q'), _) - | (KeyCode::Char('c'), KeyModifiers::CONTROL) - | (KeyCode::Esc, _) => { - if !app.error_flash_is_empty() { - app.clear_error_flash(); - } else { - disable_raw_mode()?; - execute!(terminal.backend_mut(), LeaveAlternateScreen)?; - terminal.show_cursor()?; - io_s.send(IoCommand::Break)?; - break; - } - } - (KeyCode::Char('r'), KeyModifiers::NONE) => match &app.selected() { - Selected::Feeds => { - let feed_id = app.selected_feed_id(); - io_s.send(IoCommand::RefreshFeed(feed_id))?; - } - _ => app.toggle_read()?, - }, - (KeyCode::Char('x'), KeyModifiers::NONE) => { - let feed_ids = app.feed_ids()?; - io_s.send(IoCommand::RefreshFeeds(feed_ids))?; - } - // handle all other normal-mode keycodes here - (keycode, modifiers) => { - // Manually match out the on_key result here - // and show errors in the error flash, - // because these on_key actions can fail - // in such a way that the app can continue. - if let Err(e) = app.on_key(keycode, modifiers) { - app.push_error_flash(e); - } - } - }, - Event::Tick => (), - }, - Mode::Editing => match rx.recv()? { - Event::Input(event) => match event.code { - KeyCode::Enter => { - let feed_subscription_input = { app.feed_subscription_input() }; - io_s.send(IoCommand::SubscribeToFeed(feed_subscription_input))?; - } - KeyCode::Char(c) => { - app.push_feed_subscription_input(c); - } - KeyCode::Backspace => app.pop_feed_subscription_input(), - KeyCode::Delete => { - app.delete_feed()?; - } - KeyCode::Esc => { - app.set_mode(Mode::Normal); - } - _ => {} - }, - Event::Tick => (), - }, + app.draw(&mut terminal)?; + + let event = event_rx.recv()?; + + let action = get_action(&app, event); + + if let Some(action) = action { + update(&mut app, action)?; + } + + if app.should_quit() { + app.break_io_thread()?; + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.show_cursor()?; + break; } } @@ -459,13 +255,125 @@ fn run_reader(options: ReadOptions) -> Result<()> { Ok(()) } -fn main() -> Result<()> { - let options = Options::parse(); - - let validated_options = options.subcommand.validate()?; +enum Action { + Quit, + MoveLeft, + MoveDown, + MoveUp, + MoveRight, + PageUp, + PageDown, + RefreshAll, + RefreshFeed, + ToggleHelp, + ToggleReadMode, + EnterEditingMode, + OpenLinkInBrowser, + CopyLinkToClipboard, + Tick, + SubscribeToFeed, + PushInputChar(char), + DeleteInputChar, + DeleteFeed, + EnterNormalMode, + ClearErrorFlash, + SelectAndShowCurrentEntry, + ToggleReadStatus, +} - match validated_options { - ValidatedOptions::Import(options) => crate::opml::import(options), - ValidatedOptions::Read(options) => run_reader(options), +fn get_action(app: &App, event: Event) -> Option { + match app.mode() { + Mode::Normal => match event { + Event::Input(keypress) => match (keypress.code, keypress.modifiers) { + (KeyCode::Char('q'), _) + | (KeyCode::Char('c'), KeyModifiers::CONTROL) + | (KeyCode::Esc, _) => { + if !app.error_flash_is_empty() { + Some(Action::ClearErrorFlash) + } else { + Some(Action::Quit) + } + } + (KeyCode::Char('r'), KeyModifiers::NONE) => match app.selected() { + Selected::Feeds => Some(Action::RefreshFeed), + _ => Some(Action::ToggleReadStatus), + }, + (KeyCode::Char('x'), KeyModifiers::NONE) => Some(Action::RefreshAll), + (KeyCode::Left, _) | (KeyCode::Char('h'), _) => Some(Action::MoveLeft), + (KeyCode::Right, _) | (KeyCode::Char('l'), _) => Some(Action::MoveRight), + (KeyCode::Down, _) | (KeyCode::Char('j'), _) => Some(Action::MoveDown), + (KeyCode::Up, _) | (KeyCode::Char('k'), _) => Some(Action::MoveUp), + (KeyCode::PageUp, _) | (KeyCode::Char('u'), KeyModifiers::CONTROL) => { + Some(Action::PageUp) + } + (KeyCode::PageDown, _) | (KeyCode::Char('d'), KeyModifiers::CONTROL) => { + Some(Action::PageDown) + } + (KeyCode::Enter, _) => match app.selected() { + Selected::Entries | Selected::Entry(_) => { + if app.has_entries() && app.has_current_entry() { + Some(Action::SelectAndShowCurrentEntry) + } else { + None + } + } + _ => None, + }, + (KeyCode::Char('?'), _) => Some(Action::ToggleHelp), + (KeyCode::Char('a'), _) => Some(Action::ToggleReadMode), + (KeyCode::Char('e'), _) | (KeyCode::Char('i'), _) => Some(Action::EnterEditingMode), + (KeyCode::Char('c'), _) => Some(Action::CopyLinkToClipboard), + (KeyCode::Char('o'), _) => Some(Action::OpenLinkInBrowser), + _ => None, + }, + Event::Tick => Some(Action::Tick), + }, + Mode::Editing => match event { + Event::Input(keypress) => match keypress.code { + KeyCode::Enter => { + if !app.feed_subscription_input_is_empty() { + Some(Action::SubscribeToFeed) + } else { + None + } + } + KeyCode::Char(c) => Some(Action::PushInputChar(c)), + KeyCode::Backspace => Some(Action::DeleteInputChar), + KeyCode::Delete => Some(Action::DeleteFeed), + KeyCode::Esc => Some(Action::EnterNormalMode), + _ => None, + }, + Event::Tick => Some(Action::Tick), + }, } } + +fn update(app: &mut App, action: Action) -> Result<()> { + match action { + Action::Tick => (), + Action::Quit => app.set_should_quit(true), + Action::RefreshAll => app.refresh_feeds()?, + Action::RefreshFeed => app.refresh_feed()?, + Action::MoveLeft => app.on_left()?, + Action::MoveDown => app.on_down()?, + Action::MoveUp => app.on_up()?, + Action::MoveRight => app.on_right()?, + Action::PageUp => app.page_up(), + Action::PageDown => app.page_down(), + Action::ToggleHelp => app.toggle_help()?, + Action::ToggleReadMode => app.toggle_read_mode()?, + Action::ToggleReadStatus => app.toggle_read()?, + Action::EnterEditingMode => app.set_mode(Mode::Editing), + Action::CopyLinkToClipboard => app.put_current_link_in_clipboard()?, + Action::OpenLinkInBrowser => app.open_link_in_browser()?, + Action::SubscribeToFeed => app.subscribe_to_feed()?, + Action::PushInputChar(c) => app.push_feed_subscription_input(c), + Action::DeleteInputChar => app.pop_feed_subscription_input(), + Action::DeleteFeed => app.delete_feed()?, + Action::EnterNormalMode => app.set_mode(Mode::Normal), + Action::ClearErrorFlash => app.clear_error_flash(), + Action::SelectAndShowCurrentEntry => app.select_and_show_current_entry()?, + }; + + Ok(()) +} diff --git a/src/modes.rs b/src/modes.rs index dd8a511..07c887c 100644 --- a/src/modes.rs +++ b/src/modes.rs @@ -1,8 +1,11 @@ +//! Russ is modal, and these are the modes it can be in. + +/// what type of object is currently selected #[derive(Clone, Debug)] pub enum Selected { Feeds, Entries, - Entry(crate::rss::EntryMeta), + Entry(crate::rss::EntryMetadata), None, } diff --git a/src/opml.rs b/src/opml.rs index b75a91a..109fc1b 100644 --- a/src/opml.rs +++ b/src/opml.rs @@ -1,3 +1,5 @@ +//! Import OPML feed lists into Russ + use crate::ImportOptions; use anyhow::{Context, Result}; diff --git a/src/rss.rs b/src/rss.rs index 3816981..dce7ab4 100644 --- a/src/rss.rs +++ b/src/rss.rs @@ -1,16 +1,70 @@ +//! The functions and datatypes in this module all for the retrieval and storage +//! of RSS/Atom feeds in Russ' SQLite database. + use crate::modes::ReadMode; use anyhow::{bail, Context, Result}; use atom_syndication as atom; use chrono::prelude::{DateTime, Utc}; use rss::Channel; use rusqlite::params; -use rusqlite::types::ToSqlOutput; +use rusqlite::types::{FromSql, ToSqlOutput}; use std::collections::HashSet; use std::fmt::Display; use std::str::FromStr; -type EntryId = i64; -pub type FeedId = i64; +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct EntryId(i64); + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct FeedId(i64); + +impl From for EntryId { + fn from(value: i64) -> Self { + Self(value) + } +} + +impl rusqlite::ToSql for EntryId { + fn to_sql(&self) -> rusqlite::Result> { + Ok(self.0.into()) + } +} + +impl FromSql for EntryId { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Ok(Self(value.as_i64()?)) + } +} + +impl Display for EntryId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for FeedId { + fn from(value: i64) -> Self { + Self(value) + } +} + +impl rusqlite::ToSql for FeedId { + fn to_sql(&self) -> rusqlite::Result> { + Ok(self.0.into()) + } +} + +impl FromSql for FeedId { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Ok(Self(value.as_i64()?)) + } +} + +impl Display for FeedId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} #[derive(Clone, Copy, Debug)] pub enum FeedKind { @@ -58,6 +112,10 @@ impl FromStr for FeedKind { } } +/// Feed metadata. +/// Entries are stored separately. +/// The `id` of this type corresponds to `feed_id` on +/// `Entry` and `EntryMeta`. #[derive(Clone, Debug)] pub struct Feed { pub id: FeedId, @@ -71,44 +129,45 @@ pub struct Feed { pub latest_etag: Option, } -#[derive(Clone, Debug)] -pub struct Entry { - pub id: EntryId, - pub feed_id: FeedId, - pub title: Option, - pub author: Option, - pub pub_date: Option>, - pub description: Option, - pub content: Option, - pub link: Option, - pub read_at: Option>, - pub inserted_at: chrono::DateTime, - pub updated_at: chrono::DateTime, +/// This exists: +/// 1. So we can validate an incoming Atom/RSS feed +/// 2. So we can insert it into the database +struct IncomingFeed { + title: Option, + feed_link: Option, + link: Option, + feed_kind: FeedKind, + latest_etag: Option, +} + +/// This exists: +/// 1. So we can validate an incoming Atom/RSS feed entry +/// 2. So we can insert it into the database +struct IncomingEntry { + title: Option, + author: Option, + pub_date: Option>, + description: Option, + content: Option, + link: Option, } -impl From<&atom::Entry> for Entry { +impl From<&atom::Entry> for IncomingEntry { fn from(entry: &atom::Entry) -> Self { Self { - id: -1, - feed_id: -1, title: Some(entry.title().to_string()), author: entry.authors().first().map(|author| author.name.to_owned()), pub_date: entry.published().map(|date| date.with_timezone(&Utc)), description: None, content: entry.content().and_then(|content| content.value.to_owned()), link: entry.links().first().map(|link| link.href().to_string()), - read_at: None, - inserted_at: Utc::now(), - updated_at: Utc::now(), } } } -impl From<&rss::Item> for Entry { +impl From<&rss::Item> for IncomingEntry { fn from(entry: &rss::Item) -> Self { Self { - id: -1, - feed_id: -1, title: entry.title().map(|title| title.to_owned()), author: entry.author().map(|author| author.to_owned()), pub_date: entry.pub_date().and_then(parse_datetime), @@ -117,15 +176,18 @@ impl From<&rss::Item> for Entry { .map(|description| description.to_owned()), content: entry.content().map(|content| content.to_owned()), link: entry.link().map(|link| link.to_owned()), - read_at: None, - inserted_at: Utc::now(), - updated_at: Utc::now(), } } } +/// Metadata for an entry. +/// +/// This type exists so we can load entry metadata for lots of +/// entries, without having to load all of the content for those entries, +/// as we only ever need an entry's content in memory when we are displaying +/// the currently selected entry. #[derive(Clone, Debug)] -pub struct EntryMeta { +pub struct EntryMetadata { pub id: EntryId, pub feed_id: FeedId, pub title: Option, @@ -137,7 +199,7 @@ pub struct EntryMeta { pub updated_at: chrono::DateTime, } -impl EntryMeta { +impl EntryMetadata { pub fn toggle_read(&self, conn: &rusqlite::Connection) -> Result<()> { if self.read_at.is_none() { self.mark_as_read(conn) @@ -169,8 +231,8 @@ fn parse_datetime(s: &str) -> Option> { } struct FeedAndEntries { - pub feed: Feed, - pub entries: Vec, + pub feed: IncomingFeed, + pub entries: Vec, } impl FeedAndEntries { @@ -189,15 +251,11 @@ impl FromStr for FeedAndEntries { fn from_str(s: &str) -> Result { match atom::Feed::from_str(s) { Ok(atom_feed) => { - let feed = Feed { - id: 0, + let feed = IncomingFeed { title: Some(atom_feed.title.to_string()), feed_link: None, link: atom_feed.links.first().map(|link| link.href().to_string()), feed_kind: FeedKind::Atom, - refreshed_at: None, - inserted_at: Utc::now(), - updated_at: Utc::now(), latest_etag: None, }; @@ -212,15 +270,11 @@ impl FromStr for FeedAndEntries { Err(_e) => match Channel::from_str(s) { Ok(channel) => { - let feed = Feed { - id: 0, + let feed = IncomingFeed { title: Some(channel.title().to_string()), feed_link: None, link: Some(channel.link().to_string()), feed_kind: FeedKind::Rss, - refreshed_at: None, - inserted_at: Utc::now(), - updated_at: Utc::now(), latest_etag: None, }; @@ -312,13 +366,13 @@ fn fetch_feed( let content = response.into_string()?; - let mut feed = FeedAndEntries::from_str(&content)?; + let mut feed_and_entries = FeedAndEntries::from_str(&content)?; - feed.set_latest_etag(etag); + feed_and_entries.set_latest_etag(etag); - feed.set_feed_link(url); + feed_and_entries.set_feed_link(url); - Ok(FeedResponse::CacheMiss(feed)) + Ok(FeedResponse::CacheMiss(feed_and_entries)) } // the etags match, it is the same feed we already have 304 => Ok(FeedResponse::CacheHit), @@ -449,7 +503,7 @@ pub fn initialize_db(conn: &mut rusqlite::Connection) -> Result<()> { }) } -fn create_feed(tx: &rusqlite::Transaction, feed: &Feed) -> Result { +fn create_feed(tx: &rusqlite::Transaction, feed: &IncomingFeed) -> Result { let feed_id = tx.query_row::( "INSERT INTO feeds (title, link, feed_link, feed_kind) VALUES (?1, ?2, ?3, ?4) @@ -472,7 +526,7 @@ pub fn delete_feed(conn: &mut rusqlite::Connection, feed_id: FeedId) -> Result<( fn add_entries_to_feed( tx: &rusqlite::Transaction, feed_id: FeedId, - entries: &[Entry], + entries: &[IncomingEntry], ) -> Result<()> { if !entries.is_empty() { let now = Utc::now(); @@ -617,7 +671,7 @@ pub fn get_feed_ids(conn: &rusqlite::Connection) -> Result> { Ok(ids) } -pub fn get_entry_meta(conn: &rusqlite::Connection, entry_id: EntryId) -> Result { +pub fn get_entry_meta(conn: &rusqlite::Connection, entry_id: EntryId) -> Result { let result = conn.query_row( "SELECT id, @@ -632,7 +686,7 @@ pub fn get_entry_meta(conn: &rusqlite::Connection, entry_id: EntryId) -> Result< FROM entries WHERE id=?1", [entry_id], |row| { - Ok(EntryMeta { + Ok(EntryMetadata { id: row.get(0)?, feed_id: row.get(1)?, title: row.get(2)?, @@ -668,7 +722,7 @@ pub fn get_entries_metas( conn: &rusqlite::Connection, read_mode: &ReadMode, feed_id: FeedId, -) -> Result> { +) -> Result> { let read_at_predicate = match read_mode { ReadMode::ShowUnread => "\nAND read_at IS NULL", ReadMode::ShowRead => "\nAND read_at IS NOT NULL", @@ -697,7 +751,7 @@ pub fn get_entries_metas( let mut statement = conn.prepare(&query)?; let mut entries = vec![]; for entry in statement.query_map([feed_id], |row| { - Ok(EntryMeta { + Ok(EntryMetadata { id: row.get(0)?, feed_id: row.get(1)?, title: row.get(2)?, @@ -799,10 +853,10 @@ mod tests { let mut conn = rusqlite::Connection::open_in_memory().unwrap(); initialize_db(&mut conn).unwrap(); subscribe_to_feed(&http_client, &mut conn, ZCT).unwrap(); - let feed_id = 1; + let feed_id = 1.into(); let old_entries = get_entries_metas(&conn, &ReadMode::ShowUnread, feed_id).unwrap(); refresh_feed(&http_client, &mut conn, feed_id).unwrap(); - let e = get_entry_meta(&conn, 1).unwrap(); + let e = get_entry_meta(&conn, 1.into()).unwrap(); e.mark_as_read(&conn).unwrap(); let new_entries = get_entries_metas(&conn, &ReadMode::ShowUnread, feed_id).unwrap(); diff --git a/src/ui.rs b/src/ui.rs index 3f10eab..c63bc0a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,3 +1,5 @@ +//! How the UI is rendered, with the Ratatui library. + use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Span, Text}; @@ -7,7 +9,7 @@ use std::rc::Rc; use crate::app::AppImpl; use crate::modes::{Mode, ReadMode, Selected}; -use crate::rss::EntryMeta; +use crate::rss::EntryMetadata; const PINK: Color = Color::Rgb(255, 150, 167); @@ -104,7 +106,7 @@ fn draw_first_run_helper(f: &mut Frame, area: Rect) { f.render_widget(paragraph, area); } -fn draw_entry_info(f: &mut Frame, area: Rect, entry_meta: &EntryMeta) { +fn draw_entry_info(f: &mut Frame, area: Rect, entry_meta: &EntryMetadata) { let mut text = String::new(); if let Some(item) = &entry_meta.title { text.push_str("Title: "); @@ -377,18 +379,18 @@ fn draw_entry(f: &mut Frame, area: Rect, app: &mut AppImpl) { } else { panic!("draw_entry should only be called when app.selected was Selected::Entry") }; - let default_entry_title = "No entry title".to_string(); - let default_feed_title = "No feed title".to_string(); - let entry_title = entry_meta.title.as_ref().unwrap_or(&default_entry_title); + let entry_title = entry_meta.title.as_deref().unwrap_or("No entry title"); let feed_title = app .current_feed .as_ref() - .and_then(|feed| feed.title.as_ref()) - .unwrap_or(&default_feed_title); + .and_then(|feed| feed.title.as_deref()) + .unwrap_or("No feed title"); - let mut title = entry_title.to_owned(); + let mut title = String::new(); + title.reserve_exact(entry_title.len() + feed_title.len() + 3); + title.push_str(entry_title); title.push_str(" - "); title.push_str(feed_title); diff --git a/src/util.rs b/src/util.rs index 35973e7..b07654e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,5 @@ +//! miscellaneous functions that feel like they don't fit anywhere else + use ratatui::widgets::ListState; #[derive(Debug)]