max / balanced_breakfast
- Co-Authored-By
- Claude Opus 4.6 <noreply@anthropic.com>
55 files changed,
+5685 insertions,
-1702 deletions
| @@ -217,6 +217,7 @@ | |||
| 217 | 217 | "chrono", | |
| 218 | 218 | "html2text", | |
| 219 | 219 | "rand 0.8.5", | |
| 220 | + | "readable-readability", | |
| 220 | 221 | "regex", | |
| 221 | 222 | "rhai", | |
| 222 | 223 | "roxmltree", | |
| @@ -252,6 +253,7 @@ | |||
| 252 | 253 | "bb-db", | |
| 253 | 254 | "bb-interface", | |
| 254 | 255 | "chrono", | |
| 256 | + | "regex", | |
| 255 | 257 | "serde_json", | |
| 256 | 258 | "sqlx", | |
| 257 | 259 | "thiserror 1.0.69", | |
| @@ -701,6 +703,23 @@ | |||
| 701 | 703 | "typenum", | |
| 702 | 704 | ] | |
| 703 | 705 | ||
| 706 | + | [[package]] | |
| 707 | + | name = "cssparser" | |
| 708 | + | version = "0.27.2" | |
| 709 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 710 | + | checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" | |
| 711 | + | dependencies = [ | |
| 712 | + | "cssparser-macros", | |
| 713 | + | "dtoa-short", | |
| 714 | + | "itoa 0.4.8", | |
| 715 | + | "matches", | |
| 716 | + | "phf 0.8.0", | |
| 717 | + | "proc-macro2", | |
| 718 | + | "quote", | |
| 719 | + | "smallvec", | |
| 720 | + | "syn 1.0.109", | |
| 721 | + | ] | |
| 722 | + | ||
| 704 | 723 | [[package]] | |
| 705 | 724 | name = "cssparser" | |
| 706 | 725 | version = "0.29.6" | |
| @@ -709,7 +728,7 @@ | |||
| 709 | 728 | dependencies = [ | |
| 710 | 729 | "cssparser-macros", | |
| 711 | 730 | "dtoa-short", | |
| 712 | - | "itoa", | |
| 731 | + | "itoa 1.0.17", | |
| 713 | 732 | "matches", | |
| 714 | 733 | "phf 0.10.1", | |
| 715 | 734 | "proc-macro2", | |
| @@ -1658,6 +1677,20 @@ | |||
| 1658 | 1677 | "unicode-width", | |
| 1659 | 1678 | ] | |
| 1660 | 1679 | ||
| 1680 | + | [[package]] | |
| 1681 | + | name = "html5ever" | |
| 1682 | + | version = "0.25.2" | |
| 1683 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1684 | + | checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" | |
| 1685 | + | dependencies = [ | |
| 1686 | + | "log", | |
| 1687 | + | "mac", | |
| 1688 | + | "markup5ever 0.10.1", | |
| 1689 | + | "proc-macro2", | |
| 1690 | + | "quote", | |
| 1691 | + | "syn 1.0.109", | |
| 1692 | + | ] | |
| 1693 | + | ||
| 1661 | 1694 | [[package]] | |
| 1662 | 1695 | name = "html5ever" | |
| 1663 | 1696 | version = "0.27.0" | |
| @@ -1691,7 +1724,7 @@ | |||
| 1691 | 1724 | checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" | |
| 1692 | 1725 | dependencies = [ | |
| 1693 | 1726 | "bytes", | |
| 1694 | - | "itoa", | |
| 1727 | + | "itoa 1.0.17", | |
| 1695 | 1728 | ] | |
| 1696 | 1729 | ||
| 1697 | 1730 | [[package]] | |
| @@ -1737,7 +1770,7 @@ | |||
| 1737 | 1770 | "http", | |
| 1738 | 1771 | "http-body", | |
| 1739 | 1772 | "httparse", | |
| 1740 | - | "itoa", | |
| 1773 | + | "itoa 1.0.17", | |
| 1741 | 1774 | "pin-project-lite", | |
| 1742 | 1775 | "pin-utils", | |
| 1743 | 1776 | "smallvec", | |
| @@ -2020,6 +2053,12 @@ | |||
| 2020 | 2053 | "once_cell", | |
| 2021 | 2054 | ] | |
| 2022 | 2055 | ||
| 2056 | + | [[package]] | |
| 2057 | + | name = "itoa" | |
| 2058 | + | version = "0.4.8" | |
| 2059 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2060 | + | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" | |
| 2061 | + | ||
| 2023 | 2062 | [[package]] | |
| 2024 | 2063 | name = "itoa" | |
| 2025 | 2064 | version = "1.0.17" | |
| @@ -2124,16 +2163,28 @@ | |||
| 2124 | 2163 | "zeroize", | |
| 2125 | 2164 | ] | |
| 2126 | 2165 | ||
| 2166 | + | [[package]] | |
| 2167 | + | name = "kuchiki" | |
| 2168 | + | version = "0.8.1" | |
| 2169 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2170 | + | checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358" | |
| 2171 | + | dependencies = [ | |
| 2172 | + | "cssparser 0.27.2", | |
| 2173 | + | "html5ever 0.25.2", | |
| 2174 | + | "matches", | |
| 2175 | + | "selectors 0.22.0", | |
| 2176 | + | ] | |
| 2177 | + | ||
| 2127 | 2178 | [[package]] | |
| 2128 | 2179 | name = "kuchikiki" | |
| 2129 | 2180 | version = "0.8.8-speedreader" | |
| 2130 | 2181 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2131 | 2182 | checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" | |
| 2132 | 2183 | dependencies = [ | |
| 2133 | - | "cssparser", | |
| 2184 | + | "cssparser 0.29.6", | |
| 2134 | 2185 | "html5ever 0.29.1", | |
| 2135 | 2186 | "indexmap 2.13.0", | |
| 2136 | - | "selectors", | |
| 2187 | + | "selectors 0.24.0", | |
| 2137 | 2188 | ] | |
| 2138 | 2189 | ||
| 2139 | 2190 | [[package]] | |
| @@ -2246,6 +2297,20 @@ | |||
| 2246 | 2297 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2247 | 2298 | checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" | |
| 2248 | 2299 | ||
| 2300 | + | [[package]] | |
| 2301 | + | name = "markup5ever" | |
| 2302 | + | version = "0.10.1" | |
| 2303 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2304 | + | checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" | |
| 2305 | + | dependencies = [ | |
| 2306 | + | "log", | |
| 2307 | + | "phf 0.8.0", | |
| 2308 | + | "phf_codegen 0.8.0", | |
| 2309 | + | "string_cache", | |
| 2310 | + | "string_cache_codegen", | |
| 2311 | + | "tendril", | |
| 2312 | + | ] | |
| 2313 | + | ||
| 2249 | 2314 | [[package]] | |
| 2250 | 2315 | name = "markup5ever" | |
| 2251 | 2316 | version = "0.12.1" | |
| @@ -2915,7 +2980,9 @@ | |||
| 2915 | 2980 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2916 | 2981 | checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" | |
| 2917 | 2982 | dependencies = [ | |
| 2983 | + | "phf_macros 0.8.0", | |
| 2918 | 2984 | "phf_shared 0.8.0", | |
| 2985 | + | "proc-macro-hack", | |
| 2919 | 2986 | ] | |
| 2920 | 2987 | ||
| 2921 | 2988 | [[package]] | |
| @@ -2989,6 +3056,20 @@ | |||
| 2989 | 3056 | "rand 0.8.5", | |
| 2990 | 3057 | ] | |
| 2991 | 3058 | ||
| 3059 | + | [[package]] | |
| 3060 | + | name = "phf_macros" | |
| 3061 | + | version = "0.8.0" | |
| 3062 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3063 | + | checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" | |
| 3064 | + | dependencies = [ | |
| 3065 | + | "phf_generator 0.8.0", | |
| 3066 | + | "phf_shared 0.8.0", | |
| 3067 | + | "proc-macro-hack", | |
| 3068 | + | "proc-macro2", | |
| 3069 | + | "quote", | |
| 3070 | + | "syn 1.0.109", | |
| 3071 | + | ] | |
| 3072 | + | ||
| 2992 | 3073 | [[package]] | |
| 2993 | 3074 | name = "phf_macros" | |
| 2994 | 3075 | version = "0.10.0" | |
| @@ -3346,6 +3427,20 @@ | |||
| 3346 | 3427 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3347 | 3428 | checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" | |
| 3348 | 3429 | ||
| 3430 | + | [[package]] | |
| 3431 | + | name = "readable-readability" | |
| 3432 | + | version = "0.4.0" | |
| 3433 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3434 | + | checksum = "c17015928a25bff296b0471dfa7a784e406664e1d091781db66e885b18708a8d" | |
| 3435 | + | dependencies = [ | |
| 3436 | + | "html5ever 0.25.2", | |
| 3437 | + | "kuchiki", | |
| 3438 | + | "lazy_static", | |
| 3439 | + | "log", | |
| 3440 | + | "regex", | |
| 3441 | + | "url", | |
| 3442 | + | ] | |
| 3443 | + | ||
| 3349 | 3444 | [[package]] | |
| 3350 | 3445 | name = "redox_syscall" | |
| 3351 | 3446 | version = "0.5.18" | |
| @@ -3759,6 +3854,26 @@ | |||
| 3759 | 3854 | "libc", | |
| 3760 | 3855 | ] | |
| 3761 | 3856 | ||
| 3857 | + | [[package]] | |
| 3858 | + | name = "selectors" | |
| 3859 | + | version = "0.22.0" | |
| 3860 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3861 | + | checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" | |
| 3862 | + | dependencies = [ | |
| 3863 | + | "bitflags 1.3.2", | |
| 3864 | + | "cssparser 0.27.2", | |
| 3865 | + | "derive_more", | |
| 3866 | + | "fxhash", | |
| 3867 | + | "log", | |
| 3868 | + | "matches", | |
| 3869 | + | "phf 0.8.0", | |
| 3870 | + | "phf_codegen 0.8.0", | |
| 3871 | + | "precomputed-hash", | |
| 3872 | + | "servo_arc 0.1.1", | |
| 3873 | + | "smallvec", | |
| 3874 | + | "thin-slice", | |
| 3875 | + | ] | |
| 3876 | + | ||
| 3762 | 3877 | [[package]] | |
| 3763 | 3878 | name = "selectors" | |
| 3764 | 3879 | version = "0.24.0" | |
| @@ -3766,14 +3881,14 @@ | |||
| 3766 | 3881 | checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" | |
| 3767 | 3882 | dependencies = [ | |
| 3768 | 3883 | "bitflags 1.3.2", | |
| 3769 | - | "cssparser", | |
| 3884 | + | "cssparser 0.29.6", | |
| 3770 | 3885 | "derive_more", | |
| 3771 | 3886 | "fxhash", | |
| 3772 | 3887 | "log", | |
| 3773 | 3888 | "phf 0.8.0", | |
| 3774 | 3889 | "phf_codegen 0.8.0", | |
| 3775 | 3890 | "precomputed-hash", | |
| 3776 | - | "servo_arc", | |
| 3891 | + | "servo_arc 0.2.0", | |
| 3777 | 3892 | "smallvec", | |
| 3778 | 3893 | ] | |
| 3779 | 3894 | ||
| @@ -3846,7 +3961,7 @@ | |||
| 3846 | 3961 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3847 | 3962 | checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" | |
| 3848 | 3963 | dependencies = [ | |
| 3849 | - | "itoa", | |
| 3964 | + | "itoa 1.0.17", | |
| 3850 | 3965 | "memchr", | |
| 3851 | 3966 | "serde", | |
| 3852 | 3967 | "serde_core", | |
| @@ -3889,7 +4004,7 @@ | |||
| 3889 | 4004 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" | |
| 3890 | 4005 | dependencies = [ | |
| 3891 | 4006 | "form_urlencoded", | |
| 3892 | - | "itoa", | |
| 4007 | + | "itoa 1.0.17", | |
| 3893 | 4008 | "ryu", | |
| 3894 | 4009 | "serde", | |
| 3895 | 4010 | ] | |
| @@ -3947,6 +4062,16 @@ | |||
| 3947 | 4062 | "syn 2.0.114", | |
| 3948 | 4063 | ] | |
| 3949 | 4064 | ||
| 4065 | + | [[package]] | |
| 4066 | + | name = "servo_arc" | |
| 4067 | + | version = "0.1.1" | |
| 4068 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4069 | + | checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" | |
| 4070 | + | dependencies = [ | |
| 4071 | + | "nodrop", | |
| 4072 | + | "stable_deref_trait", | |
| 4073 | + | ] | |
| 4074 | + | ||
| 3950 | 4075 | [[package]] | |
| 3951 | 4076 | name = "servo_arc" | |
| 3952 | 4077 | version = "0.2.0" | |
| @@ -4285,7 +4410,7 @@ | |||
| 4285 | 4410 | "hex", | |
| 4286 | 4411 | "hkdf", | |
| 4287 | 4412 | "hmac", | |
| 4288 | - | "itoa", | |
| 4413 | + | "itoa 1.0.17", | |
| 4289 | 4414 | "log", | |
| 4290 | 4415 | "md-5", | |
| 4291 | 4416 | "memchr", | |
| @@ -4326,7 +4451,7 @@ | |||
| 4326 | 4451 | "hkdf", | |
| 4327 | 4452 | "hmac", | |
| 4328 | 4453 | "home", | |
| 4329 | - | "itoa", | |
| 4454 | + | "itoa 1.0.17", | |
| 4330 | 4455 | "log", | |
| 4331 | 4456 | "md-5", | |
| 4332 | 4457 | "memchr", | |
| @@ -4474,7 +4599,7 @@ | |||
| 4474 | 4599 | ||
| 4475 | 4600 | [[package]] | |
| 4476 | 4601 | name = "synckit-client" | |
| 4477 | - | version = "0.1.0" | |
| 4602 | + | version = "0.2.0" | |
| 4478 | 4603 | dependencies = [ | |
| 4479 | 4604 | "argon2", | |
| 4480 | 4605 | "base64 0.22.1", | |
| @@ -4927,6 +5052,12 @@ | |||
| 4927 | 5052 | "utf-8", | |
| 4928 | 5053 | ] | |
| 4929 | 5054 | ||
| 5055 | + | [[package]] | |
| 5056 | + | name = "thin-slice" | |
| 5057 | + | version = "0.1.1" | |
| 5058 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5059 | + | checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" | |
| 5060 | + | ||
| 4930 | 5061 | [[package]] | |
| 4931 | 5062 | name = "thin-vec" | |
| 4932 | 5063 | version = "0.2.14" | |
| @@ -4992,7 +5123,7 @@ | |||
| 4992 | 5123 | checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" | |
| 4993 | 5124 | dependencies = [ | |
| 4994 | 5125 | "deranged", | |
| 4995 | - | "itoa", | |
| 5126 | + | "itoa 1.0.17", | |
| 4996 | 5127 | "num-conv", | |
| 4997 | 5128 | "powerfmt", | |
| 4998 | 5129 | "serde_core", |
| @@ -10,7 +10,7 @@ | |||
| 10 | 10 | default-members = ["src-tauri"] | |
| 11 | 11 | ||
| 12 | 12 | [workspace.package] | |
| 13 | - | version = "0.1.0" | |
| 13 | + | version = "0.2.1" | |
| 14 | 14 | edition = "2021" | |
| 15 | 15 | authors = ["BalancedBreakfast Contributors"] | |
| 16 | 16 | license-file = "LICENSE" | |
| @@ -46,4 +46,4 @@ | |||
| 46 | 46 | bb-core = { path = "crates/bb-core" } | |
| 47 | 47 | bb-feed = { path = "crates/bb-feed" } | |
| 48 | 48 | bb-db = { path = "crates/bb-db" } | |
| 49 | - | synckit-client = { path = "../../synckit-client" } | |
| 49 | + | synckit-client = { path = "../synckit-client" } |
| @@ -47,247 +47,16 @@ | |||
| 47 | 47 | ||
| 48 | 48 | ## Plugin Authoring | |
| 49 | 49 | ||
| 50 | - | Plugins (called "bussers") are `.rhai` script files. Drop a `.rhai` file into the plugins directory and it is loaded on next launch -- no compilation required. | |
| 50 | + | Plugins ("bussers") are `.rhai` script files. Drop one into the plugins directory and it loads on next launch. | |
| 51 | 51 | ||
| 52 | - | **Plugin directory locations:** | |
| 53 | - | - Development: `plugins/` at the project root | |
| 54 | - | - Production: `<app_config_dir>/plugins/` (e.g. `~/Library/Application Support/com.balancedbreakfast.app/plugins/` on macOS) | |
| 52 | + | - **Dev:** `plugins/` at the project root | |
| 53 | + | - **Prod:** `<app_config_dir>/plugins/` (e.g. `~/Library/Application Support/com.balancedbreakfast.app/plugins/` on macOS) | |
| 55 | 54 | ||
| 56 | - | ### Required Functions | |
| 57 | - | ||
| 58 | - | Every plugin must define four functions: | |
| 59 | - | ||
| 60 | - | ```rhai | |
| 61 | - | fn id() { "my-source" } | |
| 62 | - | ||
| 63 | - | fn name() { "My Source" } | |
| 64 | - | ||
| 65 | - | fn config_schema() { | |
| 66 | - | #{ | |
| 67 | - | description: "What this plugin does.", | |
| 68 | - | fields: [ | |
| 69 | - | #{ | |
| 70 | - | key: "feed_url", | |
| 71 | - | label: "Feed URL", | |
| 72 | - | field_type: "url", | |
| 73 | - | required: true, | |
| 74 | - | description: "Help text shown below the field", | |
| 75 | - | placeholder: "https://example.com/feed", | |
| 76 | - | default_value: "" | |
| 77 | - | } | |
| 78 | - | ] | |
| 79 | - | } | |
| 80 | - | } | |
| 81 | - | ||
| 82 | - | fn fetch(config, cursor) { | |
| 83 | - | // config is a map with keys from your schema + `feeds` array + `feed_url` shorthand | |
| 84 | - | // cursor is a string on subsequent pages, or () on first fetch | |
| 85 | - | #{ | |
| 86 | - | items: [], | |
| 87 | - | has_more: false, | |
| 88 | - | next_cursor: () | |
| 89 | - | } | |
| 90 | - | } | |
| 91 | - | ``` | |
| 92 | - | ||
| 93 | - | ### Optional: capabilities() | |
| 94 | - | ||
| 95 | - | Return a map to advertise what your plugin supports. All fields default to `false`/`900` if omitted. | |
| 96 | - | ||
| 97 | - | ```rhai | |
| 98 | - | fn capabilities() { | |
| 99 | - | #{ | |
| 100 | - | supports_pagination: true, | |
| 101 | - | supports_search: false, | |
| 102 | - | supports_date_filter: false, | |
| 103 | - | supports_read_state: false, | |
| 104 | - | supports_starring: false, | |
| 105 | - | requires_auth: false, | |
| 106 | - | fetch_interval_secs: 900 // auto-fetch interval; 900 = 15 min, 0 = disable | |
| 107 | - | } | |
| 108 | - | } | |
| 109 | - | ``` | |
| 110 | - | ||
| 111 | - | `fetch_interval_secs` controls how often the background scheduler re-fetches this source. The default is 900 seconds (15 minutes). Set to 0 to disable auto-fetch entirely for a plugin. | |
| 112 | - | ||
| 113 | - | ### Config Schema Fields | |
| 114 | - | ||
| 115 | - | Each field in the `fields` array is a map with these keys: | |
| 116 | - | ||
| 117 | - | | Key | Type | Required | Notes | | |
| 118 | - | |-----|------|----------|-------| | |
| 119 | - | | `key` | string | yes | Config key passed to `fetch()` via `config.key` | | |
| 120 | - | | `label` | string | yes | Display label in the UI | | |
| 121 | - | | `field_type` | string | yes | One of: `text`, `textarea`, `secret`, `url`, `number`, `toggle`, `select` | | |
| 122 | - | | `required` | bool | no | Whether the field must be filled (default `false`) | | |
| 123 | - | | `description` | string | no | Help text shown below the field | | |
| 124 | - | | `default_value` | string | no | Pre-filled value (use `default_value`, not `default`, since `default` is a reserved word in Rhai) | | |
| 125 | - | | `placeholder` | string | no | Placeholder text | | |
| 126 | - | | `options` | array | no | String array of choices for `select` fields | | |
| 127 | - | ||
| 128 | - | ### Fetch Return Shape | |
| 129 | - | ||
| 130 | - | `fetch(config, cursor)` must return a map: | |
| 131 | - | ||
| 132 | - | ```rhai | |
| 133 | - | #{ | |
| 134 | - | items: [ | |
| 135 | - | #{ | |
| 136 | - | id: "unique-item-id", // string, or #{ source: "my-source", item_id: "123" } | |
| 137 | - | bite: #{ | |
| 138 | - | author: "Author Name", // shown in the item list | |
| 139 | - | text: "Headline or summary", // primary display text | |
| 140 | - | secondary: "42 pts", // optional, shown as secondary info | |
| 141 | - | indicator: "icon" // optional, type indicator | |
| 142 | - | }, | |
| 143 | - | content: #{ | |
| 144 | - | title: "Full Title", // optional | |
| 145 | - | body: "<p>HTML body</p>", // optional | |
| 146 | - | url: "https://example.com" // optional, link to original | |
| 147 | - | }, | |
| 148 | - | meta: #{ | |
| 149 | - | source_name: "My Source", // display name in source list | |
| 150 | - | published_at: 1700000000, // Unix timestamp (seconds) | |
| 151 | - | score: 42, // optional, for score-based ordering | |
| 152 | - | tags: ["tag1", "tag2"] // optional | |
| 153 | - | } | |
| 154 | - | } | |
| 155 | - | ], | |
| 156 | - | has_more: false, // true if more pages available | |
| 157 | - | next_cursor: () // opaque string passed to next fetch() call, or () if done | |
| 158 | - | } | |
| 159 | - | ``` | |
| 160 | - | ||
| 161 | - | All sub-maps (`bite`, `content`, `meta`) are optional. Missing fields fall back to sensible defaults (empty strings, current timestamp, plugin ID as source name). | |
| 162 | - | ||
| 163 | - | ### Host Functions | |
| 164 | - | ||
| 165 | - | These functions are available to all Rhai scripts: | |
| 166 | - | ||
| 167 | - | **HTTP** | |
| 168 | - | ||
| 169 | - | | Function | Signature | Description | | |
| 170 | - | |----------|-----------|-------------| | |
| 171 | - | | `http_get` | `(url: string) -> string` | GET request, returns response body as string | | |
| 172 | - | | `http_get_json` | `(url: string) -> Dynamic` | GET request, returns parsed JSON as a Rhai map/array | | |
| 173 | - | ||
| 174 | - | **Parsing** | |
| 175 | - | ||
| 176 | - | | Function | Signature | Description | | |
| 177 | - | |----------|-----------|-------------| | |
| 178 | - | | `parse_feed` | `(input: string) -> map` | Auto-detects RSS/Atom/JSON Feed, returns `#{ title, link, entries }` where each entry has `id`, `title`, `summary`, `link`, `published`, `author`, `tags` | | |
| 179 | - | | `parse_xml` | `(xml: string) -> map` | Parses XML into nested maps with `tag`, `text`, `attrs`, `children` keys | | |
| 180 | - | | `parse_json` | `(json: string) -> Dynamic` | Parses a JSON string into Rhai values | | |
| 181 | - | ||
| 182 | - | **String Utilities** | |
| 183 | - | ||
| 184 | - | | Function | Signature | Description | | |
| 185 | - | |----------|-----------|-------------| | |
| 186 | - | | `truncate` | `(text, max_len) -> string` | Truncate with ellipsis (`...`) | | |
| 187 | - | | `str_contains` | `(text, pattern) -> bool` | Substring check | | |
| 188 | - | | `str_split` | `(text, separator) -> array` | Split into string array | | |
| 189 | - | | `str_replace` | `(text, from, to) -> string` | Replace all occurrences | | |
| 190 | - | | `str_trim` | `(text) -> string` | Trim whitespace | | |
| 191 | - | | `html_to_text` | `(html) -> string` | Strip HTML tags to plain text | | |
| 192 | - | ||
| 193 | - | **Date/Time** | |
| 194 | - | ||
| 195 | - | | Function | Signature | Description | | |
| 196 | - | |----------|-----------|-------------| | |
| 197 | - | | `timestamp_now` | `() -> int` | Current UTC Unix timestamp (seconds) | | |
| 198 | - | | `parse_datetime` | `(date_str) -> int` | Parse RFC 3339 or RFC 2822 date to Unix timestamp | | |
| 199 | - | ||
| 200 | - | **Other** | |
| 201 | - | ||
| 202 | - | | Function | Signature | Description | | |
| 203 | - | |----------|-----------|-------------| | |
| 204 | - | | `parse_int` | `(text) -> int or ()` | Parse string to integer; returns `()` on failure | | |
| 205 | - | | `strip_tracking` | `(url) -> string` | Remove UTM and other tracking query parameters | | |
| 206 | - | | `debug_print` | `(value) -> ()` | Print to the debug log (visible in dev console) | | |
| 207 | - | ||
| 208 | - | ### Minimal Example | |
| 209 | - | ||
| 210 | - | A complete plugin that fetches an RSS feed: | |
| 211 | - | ||
| 212 | - | ```rhai | |
| 213 | - | fn id() { "my-rss" } | |
| 214 | - | fn name() { "My RSS Plugin" } | |
| 215 | - | ||
| 216 | - | fn config_schema() { | |
| 217 | - | #{ | |
| 218 | - | description: "A simple RSS reader.", | |
| 219 | - | fields: [ | |
| 220 | - | #{ | |
| 221 | - | key: "feed_url", | |
| 222 | - | label: "Feed URL", | |
| 223 | - | field_type: "url", | |
| 224 | - | required: true | |
| 225 | - | } | |
| 226 | - | ] | |
| 227 | - | } | |
| 228 | - | } | |
| 229 | - | ||
| 230 | - | fn fetch(config, cursor) { | |
| 231 | - | let xml = http_get(config.feed_url); | |
| 232 | - | let feed = parse_feed(xml); | |
| 233 | - | let items = []; | |
| 234 | - | ||
| 235 | - | for entry in feed.entries { | |
| 236 | - | let published = timestamp_now(); | |
| 237 | - | if entry.published != () { | |
| 238 | - | published = entry.published; | |
| 239 | - | } | |
| 240 | - | ||
| 241 | - | items.push(#{ | |
| 242 | - | id: entry.id, | |
| 243 | - | bite: #{ author: feed.title, text: truncate(entry.title, 100) }, | |
| 244 | - | content: #{ title: entry.title, body: entry.summary, url: entry.link }, | |
| 245 | - | meta: #{ source_name: feed.title, published_at: published } | |
| 246 | - | }); | |
| 247 | - | } | |
| 248 | - | ||
| 249 | - | #{ items: items, has_more: false } | |
| 250 | - | } | |
| 251 | - | ``` | |
| 252 | - | ||
| 253 | - | ### Sandbox Limits | |
| 254 | - | ||
| 255 | - | Rhai scripts run with safety limits to prevent hangs: | |
| 256 | - | ||
| 257 | - | - **100,000 max operations** per execution (a typical RSS fetch uses 1k-5k) | |
| 258 | - | - **128 max expression depth** for both expressions and function calls | |
| 259 | - | ||
| 260 | - | Scripts that exceed these limits are terminated with an error. | |
| 261 | - | ||
| 262 | - | ### JSON API Example | |
| 263 | - | ||
| 264 | - | For sources with JSON APIs (no XML), use `http_get_json` instead of `parse_feed`: | |
| 265 | - | ||
| 266 | - | ```rhai | |
| 267 | - | fn fetch(config, cursor) { | |
| 268 | - | let data = http_get_json("https://api.example.com/posts"); | |
| 269 | - | let items = []; | |
| 270 | - | ||
| 271 | - | for post in data { | |
| 272 | - | items.push(#{ | |
| 273 | - | id: "" + post.id, | |
| 274 | - | bite: #{ author: post.author, text: truncate(post.title, 100) }, | |
| 275 | - | content: #{ title: post.title, body: post.body, url: post.url }, | |
| 276 | - | meta: #{ source_name: "Example", published_at: parse_datetime(post.date) } | |
| 277 | - | }); | |
| 278 | - | } | |
| 279 | - | ||
| 280 | - | #{ items: items, has_more: false } | |
| 281 | - | } | |
| 282 | - | ``` | |
| 55 | + | Every plugin defines four functions (`id`, `name`, `config_schema`, `fetch`) plus an optional `capabilities()`. Full authoring guide with field types, return shapes, host functions, and examples: [docs/plugin_authoring.md](docs/plugin_authoring.md). | |
| 283 | 56 | ||
| 284 | 57 | ## Bundled Plugins | |
| 285 | 58 | ||
| 286 | - | Three plugins ship with the app: | |
| 287 | - | ||
| 288 | - | - **rss.rhai** -- RSS, Atom, and JSON Feed support | |
| 289 | - | - **hackernews.rhai** -- Hacker News stories (Top, New, Best, Ask, Show, Jobs) | |
| 290 | - | - **arxiv.rhai** -- arXiv papers by category | |
| 59 | + | Three plugins ship with the app: **rss.rhai** (RSS/Atom/JSON Feed), **hackernews.rhai** (HN stories), **arxiv.rhai** (arXiv papers). | |
| 291 | 60 | ||
| 292 | 61 | ## License | |
| 293 | 62 |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "balanced-breakfast-desktop" | |
| 3 | - | version = "0.1.0" | |
| 3 | + | version = "0.2.1" | |
| 4 | 4 | edition = "2021" | |
| 5 | 5 | ||
| 6 | 6 | [[bin]] | |
| @@ -69,6 +69,7 @@ | |||
| 69 | 69 | chrono.workspace = true | |
| 70 | 70 | serde_json.workspace = true | |
| 71 | 71 | roxmltree.workspace = true | |
| 72 | + | toml.workspace = true | |
| 72 | 73 | ||
| 73 | 74 | [features] | |
| 74 | 75 | default = ["custom-protocol"] |
| @@ -38,3 +38,6 @@ | |||
| 38 | 38 | ||
| 39 | 39 | # Regex for HTML URL rewriting | |
| 40 | 40 | regex = "1" | |
| 41 | + | ||
| 42 | + | # Article extraction (reader view) | |
| 43 | + | readable-readability = "0.4" |
| @@ -12,6 +12,7 @@ | |||
| 12 | 12 | thiserror.workspace = true | |
| 13 | 13 | tracing.workspace = true | |
| 14 | 14 | chrono.workspace = true | |
| 15 | + | regex = "1" | |
| 15 | 16 | ||
| 16 | 17 | [dev-dependencies] | |
| 17 | 18 | serde_json.workspace = true |