Skip to main content

max / balanced_breakfast

Make the Supernote push an optional feature, off by default Pushing an article to a Ratta Supernote is useful to the few readers who own one and dead weight to everyone else, so it now sits behind a `supernote` feature. A default build drops supernote-push and its mDNS responder entirely; ureq stays because feed fetching already uses it. The DTO keeps `supernote_eligible` in both builds, always false without the feature, so the response shape does not depend on how the app was compiled and the frontend hides the button on its existing check. Gating the field would have made the JSON vary by build. Also fix a stale test: it asserted `selection` resolves, but that token and `row-stripe` were pruned upstream by theme-common's 0.7.0 usage audit. Nothing here consumes them. The test now asserts they stay absent, so a silent reappearance is noticed. It had gone unnoticed because this repo could not build until supernote-push was cloned.
Author: Max Johnson <me@maxj.phd> · 2026-07-19 20:41 UTC
Commit: d8e1b6adcd5bdb97f1c6416d80ac535ab8bb1ad3
Parent: 4ebdf82
7 files changed, +201 insertions, -10 deletions
M Cargo.lock +167 -5
@@ -199,6 +199,7 @@ dependencies = [
199 199 "bb-db",
200 200 "bb-feed",
201 201 "bb-interface",
202 + "bb-pdf",
202 203 "chrono",
203 204 "futures",
204 205 "makeover",
@@ -211,6 +212,7 @@ dependencies = [
211 212 "serde_json",
212 213 "sha2",
213 214 "sqlx",
215 + "supernote-push",
214 216 "synckit-client",
215 217 "tagtree",
216 218 "tauri",
@@ -260,7 +262,7 @@ dependencies = [
260 262 "futures",
261 263 "keyring",
262 264 "parking_lot",
263 - "pter",
265 + "pter 0.1.0",
264 266 "rand 0.8.5",
265 267 "readable-readability",
266 268 "regex",
@@ -317,6 +319,16 @@ dependencies = [
317 319 ]
318 320
319 321 [[package]]
322 + name = "bb-pdf"
323 + version = "0.3.1"
324 + dependencies = [
325 + "printpdf",
326 + "pter 0.1.1",
327 + "pulldown-cmark",
328 + "thiserror 1.0.69",
329 + ]
330 +
331 + [[package]]
320 332 name = "bitflags"
321 333 version = "1.3.2"
322 334 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -380,6 +392,17 @@ dependencies = [
380 392 ]
381 393
382 394 [[package]]
395 + name = "bstr"
396 + version = "1.13.0"
397 + source = "registry+https://github.com/rust-lang/crates.io-index"
398 + checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530"
399 + dependencies = [
400 + "memchr",
401 + "regex-automata",
402 + "serde_core",
403 + ]
404 +
405 + [[package]]
383 406 name = "bumpalo"
384 407 version = "3.19.1"
385 408 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2038,7 +2061,7 @@ dependencies = [
2038 2061 "libc",
2039 2062 "percent-encoding",
2040 2063 "pin-project-lite",
2041 - "socket2",
2064 + "socket2 0.6.2",
2042 2065 "system-configuration",
2043 2066 "tokio",
2044 2067 "tower-service",
@@ -2189,6 +2212,16 @@ dependencies = [
2189 2212 ]
2190 2213
2191 2214 [[package]]
2215 + name = "if-addrs"
2216 + version = "0.13.4"
2217 + source = "registry+https://github.com/rust-lang/crates.io-index"
2218 + checksum = "69b2eeee38fef3aa9b4cc5f1beea8a2444fc00e7377cafae396de3f5c2065e24"
2219 + dependencies = [
2220 + "libc",
2221 + "windows-sys 0.59.0",
2222 + ]
2223 +
2224 + [[package]]
2192 2225 name = "include_dir"
2193 2226 version = "0.7.4"
2194 2227 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2500,6 +2533,12 @@ dependencies = [
2500 2533 ]
2501 2534
2502 2535 [[package]]
2536 + name = "linked-hash-map"
2537 + version = "0.5.6"
2538 + source = "registry+https://github.com/rust-lang/crates.io-index"
2539 + checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
2540 +
2541 + [[package]]
2503 2542 name = "linux-keyutils"
2504 2543 version = "0.2.5"
2505 2544 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2537,6 +2576,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2537 2576 checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
2538 2577
2539 2578 [[package]]
2579 + name = "lopdf"
2580 + version = "0.31.0"
2581 + source = "registry+https://github.com/rust-lang/crates.io-index"
2582 + checksum = "07c8e1b6184b1b32ea5f72f572ebdc40e5da1d2921fa469947ff7c480ad1f85a"
2583 + dependencies = [
2584 + "encoding_rs",
2585 + "flate2",
2586 + "itoa 1.0.17",
2587 + "linked-hash-map",
2588 + "log",
2589 + "md5",
2590 + "pom",
2591 + "time",
2592 + "weezl",
2593 + ]
2594 +
2595 + [[package]]
2540 2596 name = "mac"
2541 2597 version = "0.1.1"
2542 2598 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2544,9 +2600,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
2544 2600
2545 2601 [[package]]
2546 2602 name = "makeover"
2547 - version = "0.9.0"
2603 + version = "0.10.0"
2548 2604 source = "registry+https://github.com/rust-lang/crates.io-index"
2549 - checksum = "8901217cbffb659a961ac7b63fbfb72bacd4eda13b1d5a7329a9ffed54616165"
2605 + checksum = "c62c909826835a6a4212081a4c95ef2db0d07c0cb1a550849c2e779b962f093e"
2550 2606 dependencies = [
2551 2607 "include_dir",
2552 2608 "serde",
@@ -2657,6 +2713,25 @@ dependencies = [
2657 2713 ]
2658 2714
2659 2715 [[package]]
2716 + name = "md5"
2717 + version = "0.7.0"
2718 + source = "registry+https://github.com/rust-lang/crates.io-index"
2719 + checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
2720 +
2721 + [[package]]
2722 + name = "mdns-sd"
2723 + version = "0.11.5"
2724 + source = "registry+https://github.com/rust-lang/crates.io-index"
2725 + checksum = "8fe7c11a1eb3cfbfcf702d1601c1f5f4c102cdc8665b8a557783ef634741676e"
2726 + dependencies = [
2727 + "flume",
2728 + "if-addrs",
2729 + "log",
2730 + "polling",
2731 + "socket2 0.5.10",
2732 + ]
2733 +
2734 + [[package]]
2660 2735 name = "memchr"
2661 2736 version = "2.8.0"
2662 2737 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3212,6 +3287,15 @@ dependencies = [
3212 3287 ]
3213 3288
3214 3289 [[package]]
3290 + name = "owned_ttf_parser"
3291 + version = "0.19.0"
3292 + source = "registry+https://github.com/rust-lang/crates.io-index"
3293 + checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4"
3294 + dependencies = [
3295 + "ttf-parser",
3296 + ]
3297 +
3298 + [[package]]
3215 3299 name = "pango"
3216 3300 version = "0.18.3"
3217 3301 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3566,6 +3650,22 @@ dependencies = [
3566 3650 ]
3567 3651
3568 3652 [[package]]
3653 + name = "polling"
3654 + version = "2.8.0"
3655 + source = "registry+https://github.com/rust-lang/crates.io-index"
3656 + checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
3657 + dependencies = [
3658 + "autocfg",
3659 + "bitflags 1.3.2",
3660 + "cfg-if",
3661 + "concurrent-queue",
3662 + "libc",
3663 + "log",
3664 + "pin-project-lite",
3665 + "windows-sys 0.48.0",
3666 + ]
3667 +
3668 + [[package]]
3569 3669 name = "poly1305"
3570 3670 version = "0.8.0"
3571 3671 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3589,6 +3689,15 @@ dependencies = [
3589 3689 ]
3590 3690
3591 3691 [[package]]
3692 + name = "pom"
3693 + version = "3.4.0"
3694 + source = "registry+https://github.com/rust-lang/crates.io-index"
3695 + checksum = "6c972d8f86e943ad532d0b04e8965a749ad1d18bb981a9c7b3ae72fe7fd7744b"
3696 + dependencies = [
3697 + "bstr",
3698 + ]
3699 +
3700 + [[package]]
3592 3701 name = "portable-atomic"
3593 3702 version = "1.13.1"
3594 3703 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3625,6 +3734,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
3625 3734 checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
3626 3735
3627 3736 [[package]]
3737 + name = "printpdf"
3738 + version = "0.7.0"
3739 + source = "registry+https://github.com/rust-lang/crates.io-index"
3740 + checksum = "c30a4cc87c3ca9a98f4970db158a7153f8d1ec8076e005751173c57836380b1d"
3741 + dependencies = [
3742 + "js-sys",
3743 + "lopdf",
3744 + "owned_ttf_parser",
3745 + "time",
3746 + ]
3747 +
3748 + [[package]]
3628 3749 name = "proc-macro-crate"
3629 3750 version = "1.3.1"
3630 3751 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3702,6 +3823,13 @@ dependencies = [
3702 3823 ]
3703 3824
3704 3825 [[package]]
3826 + name = "pter"
3827 + version = "0.1.1"
3828 + dependencies = [
3829 + "scraper",
3830 + ]
3831 +
3832 + [[package]]
3705 3833 name = "pulldown-cmark"
3706 3834 version = "0.12.2"
3707 3835 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4774,6 +4902,16 @@ dependencies = [
4774 4902
4775 4903 [[package]]
4776 4904 name = "socket2"
4905 + version = "0.5.10"
4906 + source = "registry+https://github.com/rust-lang/crates.io-index"
4907 + checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
4908 + dependencies = [
4909 + "libc",
4910 + "windows-sys 0.52.0",
4911 + ]
4912 +
4913 + [[package]]
4914 + name = "socket2"
4777 4915 version = "0.6.2"
4778 4916 source = "registry+https://github.com/rust-lang/crates.io-index"
4779 4917 checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
@@ -5136,6 +5274,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
5136 5274 checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
5137 5275
5138 5276 [[package]]
5277 + name = "supernote-push"
5278 + version = "0.1.0"
5279 + dependencies = [
5280 + "mdns-sd",
5281 + "percent-encoding",
5282 + "serde",
5283 + "serde_json",
5284 + "thiserror 2.0.18",
5285 + "ureq",
5286 + ]
5287 +
5288 + [[package]]
5139 5289 name = "swift-rs"
5140 5290 version = "1.0.7"
5141 5291 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5835,7 +5985,7 @@ dependencies = [
5835 5985 "libc",
5836 5986 "mio",
5837 5987 "pin-project-lite",
5838 - "socket2",
5988 + "socket2 0.6.2",
5839 5989 "tokio-macros",
5840 5990 "windows-sys 0.61.2",
5841 5991 ]
@@ -6127,6 +6277,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
6127 6277 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
6128 6278
6129 6279 [[package]]
6280 + name = "ttf-parser"
6281 + version = "0.19.2"
6282 + source = "registry+https://github.com/rust-lang/crates.io-index"
6283 + checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1"
6284 +
6285 + [[package]]
6130 6286 name = "typeid"
6131 6287 version = "1.0.3"
6132 6288 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6640,6 +6796,12 @@ dependencies = [
6640 6796 ]
6641 6797
6642 6798 [[package]]
6799 + name = "weezl"
6800 + version = "0.1.12"
6801 + source = "registry+https://github.com/rust-lang/crates.io-index"
6802 + checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
6803 +
6804 + [[package]]
6643 6805 name = "whoami"
6644 6806 version = "1.6.1"
6645 6807 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -24,7 +24,7 @@ bb-db.workspace = true
24 24 bb-pdf.workspace = true
25 25
26 26 # Supernote push (LAN e-ink upload)
27 - supernote-push.workspace = true
27 + supernote-push = { workspace = true, optional = true }
28 28
29 29 # Tauri
30 30 tauri = { version = "2.10.2", features = [] }
@@ -101,3 +101,7 @@ synckit-client = { workspace = true, features = ["testing"] }
101 101 [features]
102 102 default = ["custom-protocol"]
103 103 custom-protocol = ["tauri/custom-protocol"]
104 + # Push an article to a Ratta Supernote over the LAN. Off by default: most
105 + # readers do not own one, and enabling it pulls an HTTP client and an mDNS
106 + # responder in for a device they will never point at.
107 + supernote = ["dep:supernote-push"]
@@ -15,7 +15,7 @@
15 15 * Apply a theme's resolved intent tokens as CSS variables on :root. Each
16 16 * intent key becomes `--{key}` (e.g. `surface-page` -> `--surface-page`);
17 17 * styles.css aliases BB's brand vocabulary over them. The interactive
18 - * states (hover/active/selection/contrast) are resolved by theme-common in
18 + * states (hover/active/selection/contrast) are resolved by makeover in
19 19 * OKLab — no JS color math here anymore.
20 20 * @param {Object<string, string>} intents - Map of intent token -> hex value.
21 21 */
@@ -100,7 +100,9 @@ pub struct ItemDetailResponse {
100 100 /// Whether the user has starred/favourited this item.
101 101 pub is_starred: bool,
102 102 /// Whether the body is worth converting to PDF and pushing to a Supernote.
103 - /// Frontend uses this to gate the "Send to Supernote" button.
103 + /// Frontend uses this to gate the "Send to Supernote" button. Always false
104 + /// when the `supernote` feature is off, so the button stays hidden without
105 + /// the frontend needing to know how the app was built.
104 106 pub supernote_eligible: bool,
105 107 }
106 108
@@ -179,6 +181,22 @@ fn feed_item_to_summary(item: FeedItem) -> ItemSummaryResponse {
179 181 }
180 182
181 183 /// Convert a database feed item to a full detail response.
184 + /// Whether an item is worth pushing to a Supernote.
185 + ///
186 + /// Defined in both builds so `ItemDetailResponse` keeps one shape: without the
187 + /// `supernote` feature this is always false, the frontend's eligibility check
188 + /// fails, and the button never renders. Gating the field itself would change
189 + /// the JSON the frontend parses depending on how the app was compiled.
190 + #[cfg(feature = "supernote")]
191 + fn supernote_eligible(body: Option<&str>) -> bool {
192 + crate::commands::supernote::is_eligible(body)
193 + }
194 +
195 + #[cfg(not(feature = "supernote"))]
196 + fn supernote_eligible(_body: Option<&str>) -> bool {
197 + false
198 + }
199 +
182 200 fn item_to_detail(item: &bb_db::DbFeedItem) -> ItemDetailResponse {
183 201 ItemDetailResponse {
184 202 id: item.id.to_string(),
@@ -205,7 +223,7 @@ fn item_to_detail(item: &bb_db::DbFeedItem) -> ItemDetailResponse {
205 223 }).collect(),
206 224 is_read: item.is_read,
207 225 is_starred: item.is_starred,
208 - supernote_eligible: crate::commands::supernote::is_eligible(item.body.as_deref()),
226 + supernote_eligible: supernote_eligible(item.body.as_deref()),
209 227 }
210 228 }
211 229
@@ -8,6 +8,7 @@ pub mod opml;
8 8 pub mod plugins;
9 9 pub mod query_feeds;
10 10 pub mod sources;
11 + #[cfg(feature = "supernote")]
11 12 pub mod supernote;
12 13 pub mod sync;
13 14 pub mod themes;
@@ -20,6 +21,7 @@ pub use opml::*;
20 21 pub use plugins::*;
21 22 pub use query_feeds::*;
22 23 pub use sources::*;
24 + #[cfg(feature = "supernote")]
23 25 pub use supernote::*;
24 26 pub use sync::*;
25 27 pub use themes::*;
@@ -128,6 +128,7 @@ pub fn build_app() -> tauri::Builder<tauri::Wry> {
128 128 commands::is_bookmarked,
129 129 commands::get_bookmark_count,
130 130 commands::export_bookmark_html,
131 + #[cfg(feature = "supernote")]
131 132 commands::push_item_to_supernote,
132 133 ])
133 134 }
@@ -52,8 +52,12 @@ six = "#4cc9f0"
52 52 assert_eq!(t.hex("category-five"), Some("#9d4edd"));
53 53 // Derived states are present.
54 54 assert!(t.hex("action-hover").is_some());
55 - assert!(t.hex("selection").is_some());
56 55 assert!(t.hex("content-on-action").is_some());
56 + // `selection` and `row-stripe` were pruned upstream by the 0.7.0 usage
57 + // audit (no consumers); anything needing them derives inline via `mix()`.
58 + // Asserted absent so a silent reappearance is noticed rather than assumed.
59 + assert!(t.hex("selection").is_none());
60 + assert!(t.hex("row-stripe").is_none());
57 61 }
58 62
59 63 #[test]