Skip to main content

max / balanced_breakfast

Version bump to 0.2.1
Co-Authored-By
Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-12 02:03 UTC
Commit: 1f74722199c4e3e0c6a155b62bcf0ae8c6710ac8
Parent: 1ff7450
55 files changed, +5685 insertions, -1702 deletions
M Cargo.lock +144 -13
@@ -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",
M Cargo.toml +2 -2
@@ -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" }
M README.md +5 -236
@@ -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
D docs/todo.md -127