Skip to main content

max / balanced_breakfast

16.1 KB · 442 lines History Blame Raw
1 #!/usr/bin/env python3
2 """Generate Balanced Breakfast pitch PDF."""
3
4 from fpdf import FPDF
5
6 # Colors (warm breakfast palette)
7 BG = (250, 248, 245) # Warm cream background
8 TEXT = (61, 50, 37) # Dark brown text
9 ACCENT = (232, 168, 65) # Egg-yolk yellow accent
10 TOMATO = (201, 75, 75) # Tomato red (secondary accent)
11 MUTED = (154, 139, 120) # Muted brown text
12 WHITE = (255, 255, 255)
13 DARK = (50, 40, 30) # Dark brown (title page bg)
14 GREEN = (107, 155, 90) # Success green
15 SECTION_BG = (245, 240, 232)
16
17
18 class PitchPDF(FPDF):
19 def header(self):
20 pass
21
22 def footer(self):
23 if self.page_no() > 1:
24 self.set_y(-15)
25 self.set_font("Helvetica", "I", 8)
26 self.set_text_color(*MUTED)
27 self.cell(0, 10, f"Balanced Breakfast | Page {self.page_no()}", align="C")
28
29 def section_title(self, title):
30 self.set_font("Helvetica", "B", 16)
31 self.set_text_color(*TEXT)
32 self.cell(0, 10, title, new_x="LMARGIN", new_y="NEXT")
33 # Yolk underline
34 self.set_draw_color(*ACCENT)
35 self.set_line_width(1.5)
36 self.line(self.l_margin, self.get_y(), self.l_margin + 50, self.get_y())
37 self.ln(6)
38
39 def feature_block(self, icon, title, bullets):
40 y_start = self.get_y()
41 # Title
42 self.set_font("Helvetica", "B", 12)
43 self.set_text_color(*TEXT)
44 self.cell(0, 7, f"{icon} {title}", new_x="LMARGIN", new_y="NEXT")
45 self.ln(1)
46 # Bullets
47 self.set_font("Helvetica", "", 9.5)
48 self.set_text_color(*MUTED)
49 for bullet in bullets:
50 self.set_x(self.l_margin + 6)
51 self.multi_cell(self.epw - 6, 4.5, f"- {bullet}", new_x="LMARGIN", new_y="NEXT")
52 self.ln(3)
53
54
55 def build_pdf():
56 pdf = PitchPDF()
57 pdf.set_auto_page_break(auto=True, margin=20)
58 pdf.set_margins(20, 15, 20)
59
60 # =========================================================
61 # PAGE 1 - Title / Hero
62 # =========================================================
63 pdf.add_page()
64
65 # Background fill
66 pdf.set_fill_color(*DARK)
67 pdf.rect(0, 0, 210, 297, "F")
68
69 # "BB" logo text
70 pdf.set_y(60)
71 pdf.set_font("Helvetica", "B", 72)
72 pdf.set_text_color(*ACCENT)
73 pdf.cell(0, 30, "BB", align="C", new_x="LMARGIN", new_y="NEXT")
74
75 # App name
76 pdf.set_font("Helvetica", "B", 32)
77 pdf.set_text_color(*WHITE)
78 pdf.cell(0, 16, "Balanced Breakfast", align="C", new_x="LMARGIN", new_y="NEXT")
79
80 # Tagline
81 pdf.ln(6)
82 pdf.set_font("Helvetica", "", 14)
83 pdf.set_text_color(200, 190, 175)
84 pdf.cell(0, 8, "When it comes to media and news,", align="C", new_x="LMARGIN", new_y="NEXT")
85 pdf.cell(0, 8, "it's good to be a picky eater.", align="C", new_x="LMARGIN", new_y="NEXT")
86
87 # Divider
88 pdf.ln(12)
89 pdf.set_draw_color(*ACCENT)
90 pdf.set_line_width(1)
91 pdf.line(70, pdf.get_y(), 140, pdf.get_y())
92 pdf.ln(12)
93
94 # Value props
95 pdf.set_font("Helvetica", "", 11)
96 pdf.set_text_color(210, 200, 185)
97 props = [
98 "RSS, Hacker News, arXiv -- all in one unified timeline",
99 "Extend with custom sources via Rhai plugins -- no recompilation",
100 "Your data stays local -- no accounts, no cloud, no tracking",
101 "Native performance -- built with Rust and Tauri, not Electron",
102 "Free with no limits -- no feed caps, no premium tiers",
103 "macOS, Windows, and Linux",
104 ]
105 for prop in props:
106 pdf.cell(0, 7, prop, align="C", new_x="LMARGIN", new_y="NEXT")
107
108 # Bottom
109 pdf.set_y(255)
110 pdf.set_font("Helvetica", "I", 10)
111 pdf.set_text_color(140, 128, 110)
112 pdf.cell(0, 6, "Alpha Release -- March 2026", align="C", new_x="LMARGIN", new_y="NEXT")
113 pdf.cell(0, 6, "Source-available -- PolyForm Noncommercial 1.0.0", align="C", new_x="LMARGIN", new_y="NEXT")
114
115 # =========================================================
116 # PAGE 2 - Core Features
117 # =========================================================
118 pdf.add_page()
119
120 pdf.section_title("Core Features")
121
122 pdf.feature_block("FEEDS", "Feed Aggregation", [
123 "Subscribe to any RSS or Atom feed URL",
124 "Follow Hacker News stories: Top, New, Best, Ask HN, Show HN, Jobs",
125 "Track arXiv papers by category (cs.AI, cs.LG, cs.CL, cs.CV, stat.ML)",
126 "Unified timeline across all sources with per-source emoji indicators",
127 "Auto-fetch in background every 15 minutes (configurable per plugin)",
128 "Manual refresh with Cmd+R for immediate fetch",
129 ])
130
131 pdf.feature_block("READING", "Reading & Browsing", [
132 "Three-panel layout: sources sidebar, items list, detail panel",
133 "Item metadata: title, author, body preview, score, published date",
134 "HTML content converted to readable text",
135 "Open original URLs in your browser with o or Enter",
136 "Keyboard-driven: j/k navigate, s star, r read/unread, / search",
137 ])
138
139 pdf.feature_block("ORGANIZE", "Organization", [
140 "Mark items as read/unread, star/favorite for later",
141 "Filter views: All items, Unread only, Starred only",
142 "Per-source filtering with unread counts in sidebar",
143 "Sort: Newest first, By score, Unread first, Starred first",
144 "Client-side text search across title, body, and author",
145 "Paginated loading (50 items at a time)",
146 ])
147
148 pdf.feature_block("MANAGE", "Feed Management", [
149 "Add new feeds from any loaded plugin without restarting",
150 "Delete individual feeds or all feeds from a source",
151 "OPML import (Cmd+I) to migrate from other feed readers",
152 "OPML export (Cmd+E) for backup or migration",
153 "Automatic duplicate feed detection on import",
154 "Toast notifications on fetch errors",
155 ])
156
157 # =========================================================
158 # PAGE 3 - Plugin System & Data
159 # =========================================================
160 pdf.add_page()
161
162 pdf.section_title("Plugin System")
163
164 pdf.set_font("Helvetica", "", 10.5)
165 pdf.set_text_color(*TEXT)
166 pdf.multi_cell(0, 5.5,
167 "Balanced Breakfast uses a Rhai scripting engine for feed source plugins. "
168 "Drop a .rhai file into the plugins directory and it loads on next launch -- "
169 "no compilation, no app restart required."
170 )
171 pdf.ln(4)
172
173 pdf.feature_block("PLUGINS", "How It Works", [
174 "Three built-in plugins: RSS/Atom, Hacker News, arXiv",
175 "Each plugin defines its own config schema (text, URL, number, toggle, select)",
176 "Plugin capabilities: pagination, search, date filtering, custom fetch intervals",
177 "Host functions for HTTP requests, feed parsing, string utilities, date/time",
178 "100,000 max operations per execution (security sandbox)",
179 "Config validation ensures correct inputs before fetching",
180 ])
181
182 pdf.feature_block("EXTEND", "Write Your Own", [
183 "Four required functions: id(), name(), config_schema(), fetch()",
184 "Optional capabilities() to declare what your plugin supports",
185 "Use http_get() and parse_feed() for RSS sources",
186 "Use http_get_json() for JSON API sources",
187 "Full documentation and minimal examples included",
188 ])
189
190 pdf.ln(4)
191 pdf.section_title("Keyboard Shortcuts")
192
193 pdf.set_font("Helvetica", "", 10)
194 pdf.set_text_color(*MUTED)
195 shortcuts = [
196 "j / k -- Navigate items (vim-style)",
197 "s -- Star / unstar item",
198 "r -- Toggle read / unread",
199 "/ -- Focus search",
200 "o or Enter -- Open original URL in browser",
201 "Escape -- Close detail panel",
202 "Cmd+R -- Refresh all feeds",
203 "Cmd+N -- Add new feed",
204 "Cmd+I -- Import OPML",
205 "Cmd+E -- Export OPML",
206 "Cmd+1/2/3 -- View all / unread / starred",
207 ]
208 for s in shortcuts:
209 pdf.cell(0, 5.5, f" {s}", new_x="LMARGIN", new_y="NEXT")
210
211 # =========================================================
212 # PAGE 4 - Why BB + Comparison
213 # =========================================================
214 pdf.add_page()
215
216 pdf.section_title("Why Balanced Breakfast?")
217
218 pdf.set_font("Helvetica", "", 10.5)
219 pdf.set_text_color(*TEXT)
220 pdf.multi_cell(0, 5.5,
221 "Most feed readers are cloud-only, subscription-based, and limited to RSS. "
222 "Balanced Breakfast is a native desktop app that aggregates RSS, Hacker News, "
223 "arXiv, and any source you can script -- all offline, all local, all yours."
224 )
225 pdf.ln(6)
226
227 # Comparison table
228 pdf.set_font("Helvetica", "B", 11)
229 pdf.set_text_color(*TEXT)
230 pdf.cell(0, 8, "How it compares:", new_x="LMARGIN", new_y="NEXT")
231 pdf.ln(2)
232
233 headers = ["", "BB", "Feedly", "NNW", "Reeder", "Miniflux", "Newsboat"]
234 col_w = [38, 22, 22, 22, 22, 22, 22]
235
236 # Header row
237 pdf.set_font("Helvetica", "B", 8)
238 pdf.set_fill_color(*DARK)
239 pdf.set_text_color(*WHITE)
240 for i, h in enumerate(headers):
241 pdf.cell(col_w[i], 7, h, border=1, fill=True, align="C")
242 pdf.ln()
243
244 rows = [
245 ["Free (no limits)", "Yes", "100", "Yes", "10", "Self", "Yes"],
246 ["Native desktop", "Yes", "No", "Mac", "Mac", "No", "Term"],
247 ["Plugin system", "Yes", "No", "No", "No", "No", "Macros"],
248 ["HN first-class", "Yes", "No", "No", "No", "No", "No"],
249 ["arXiv first-class", "Yes","No", "No", "No", "No", "No"],
250 ["Cross-platform", "Yes", "Web", "Apple","Apple","Web", "Unix"],
251 ["Local-only data", "Yes", "No", "Opt", "Opt", "Self", "Yes"],
252 ["No account", "Yes", "No", "Opt", "Opt", "Self", "Yes"],
253 ["OPML support", "Yes", "Yes", "Yes", "No", "Yes", "Yes"],
254 ["Keyboard-driven", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes"],
255 ]
256
257 pdf.set_font("Helvetica", "", 8)
258 for row in rows:
259 for i, val in enumerate(row):
260 if i == 0:
261 pdf.set_font("Helvetica", "B", 8)
262 pdf.set_text_color(*TEXT)
263 pdf.set_fill_color(*SECTION_BG)
264 else:
265 pdf.set_font("Helvetica", "", 8)
266 if val == "Yes" and i == 1:
267 pdf.set_text_color(*GREEN)
268 pdf.set_fill_color(*WHITE)
269 elif val == "Yes":
270 pdf.set_text_color(80, 80, 80)
271 pdf.set_fill_color(*WHITE)
272 elif val == "No":
273 pdf.set_text_color(180, 180, 180)
274 pdf.set_fill_color(*WHITE)
275 else:
276 pdf.set_text_color(*MUTED)
277 pdf.set_fill_color(*WHITE)
278 pdf.cell(col_w[i], 6, val, border=1, fill=True, align="C" if i > 0 else "L")
279 pdf.ln()
280
281 pdf.ln(8)
282
283 # Pricing note
284 pdf.set_font("Helvetica", "B", 12)
285 pdf.set_text_color(*TEXT)
286 pdf.cell(0, 8, "Pricing", new_x="LMARGIN", new_y="NEXT")
287 pdf.set_draw_color(*ACCENT)
288 pdf.set_line_width(1.5)
289 pdf.line(pdf.l_margin, pdf.get_y(), pdf.l_margin + 30, pdf.get_y())
290 pdf.ln(4)
291
292 pdf.set_font("Helvetica", "", 10.5)
293 pdf.set_text_color(*TEXT)
294 pdf.multi_cell(0, 5.5,
295 "Balanced Breakfast is free during the alpha period with no feed limits "
296 "and no feature gating. No accounts, no subscriptions, no data collection."
297 )
298
299 pdf.ln(8)
300
301 # Tech
302 pdf.set_font("Helvetica", "B", 12)
303 pdf.set_text_color(*TEXT)
304 pdf.cell(0, 8, "Built With", new_x="LMARGIN", new_y="NEXT")
305 pdf.set_draw_color(*ACCENT)
306 pdf.line(pdf.l_margin, pdf.get_y(), pdf.l_margin + 30, pdf.get_y())
307 pdf.ln(4)
308
309 pdf.set_font("Helvetica", "", 10)
310 pdf.set_text_color(*MUTED)
311 tech_items = [
312 "Rust backend (Tauri 2) -- native performance, small binary, low memory",
313 "SQLite database -- content-addressable item storage, no duplicates",
314 "Rhai scripting engine -- extensible plugin system, no recompilation",
315 "Vanilla JavaScript frontend -- no framework bloat",
316 "142+ automated tests across the workspace",
317 ]
318 for item in tech_items:
319 pdf.cell(0, 6, f" {item}", new_x="LMARGIN", new_y="NEXT")
320
321 # =========================================================
322 # PAGE 5 - Getting Started
323 # =========================================================
324 pdf.add_page()
325
326 pdf.section_title("Get Balanced Breakfast")
327
328 # macOS
329 pdf.set_font("Helvetica", "B", 13)
330 pdf.set_text_color(*TEXT)
331 pdf.cell(0, 8, "macOS", new_x="LMARGIN", new_y="NEXT")
332 pdf.ln(2)
333
334 pdf.set_font("Helvetica", "", 10.5)
335 pdf.set_text_color(*MUTED)
336 pdf.multi_cell(0, 5.5,
337 "The macOS app will be shared with you directly as a .dmg file. "
338 "Open the DMG and drag Balanced Breakfast to your Applications folder."
339 )
340 pdf.ln(2)
341 pdf.set_font("Helvetica", "I", 9.5)
342 pdf.multi_cell(0, 5,
343 "Note: Since the app is not yet on the Mac App Store, macOS may show a security "
344 "warning on first launch. Right-click the app and choose \"Open\" to bypass this, "
345 "or go to System Settings > Privacy & Security and click \"Open Anyway\"."
346 )
347
348 pdf.ln(8)
349
350 # Windows / Linux
351 pdf.set_font("Helvetica", "B", 13)
352 pdf.set_text_color(*TEXT)
353 pdf.cell(0, 8, "Windows & Linux", new_x="LMARGIN", new_y="NEXT")
354 pdf.ln(2)
355
356 pdf.set_font("Helvetica", "", 10.5)
357 pdf.set_text_color(*MUTED)
358 pdf.multi_cell(0, 5.5,
359 "Balanced Breakfast is built with Tauri 2 and supports Windows and Linux. "
360 "Binaries for these platforms will be available soon."
361 )
362
363 pdf.ln(8)
364
365 # Getting started
366 pdf.set_font("Helvetica", "B", 13)
367 pdf.set_text_color(*TEXT)
368 pdf.cell(0, 8, "Quick Start", new_x="LMARGIN", new_y="NEXT")
369 pdf.ln(2)
370
371 steps = [
372 ("1.", "Launch the app",
373 "Open Balanced Breakfast. The three-panel layout appears: "
374 "sources on the left, items in the center, detail on the right."),
375 ("2.", "Add your first feed",
376 "Press Cmd+N or click Add Feed. Choose a plugin (RSS, Hacker News, or arXiv), "
377 "enter the configuration, and click Add."),
378 ("3.", "Browse your feeds",
379 "Items appear in the center panel. Use j/k to navigate, Enter to read, "
380 "s to star, r to mark read/unread."),
381 ("4.", "Import existing feeds",
382 "Coming from another reader? Press Cmd+I to import an OPML file. "
383 "All your RSS subscriptions transfer instantly."),
384 ("5.", "Add custom sources",
385 "Drop a .rhai plugin file into the plugins directory to add any source "
386 "you can script -- news APIs, forums, journals, anything."),
387 ]
388
389 for num, title, desc in steps:
390 pdf.set_font("Helvetica", "B", 11)
391 pdf.set_text_color(*TEXT)
392 pdf.cell(8, 6, num)
393 pdf.cell(0, 6, title, new_x="LMARGIN", new_y="NEXT")
394 pdf.set_x(pdf.l_margin + 8)
395 pdf.set_font("Helvetica", "", 9.5)
396 pdf.set_text_color(*MUTED)
397 pdf.multi_cell(pdf.epw - 8, 4.8, desc, new_x="LMARGIN", new_y="NEXT")
398 pdf.ln(3)
399
400 pdf.ln(4)
401
402 # Feedback
403 pdf.set_font("Helvetica", "B", 13)
404 pdf.set_text_color(*TEXT)
405 pdf.cell(0, 8, "Sending Feedback", new_x="LMARGIN", new_y="NEXT")
406 pdf.ln(2)
407
408 pdf.set_font("Helvetica", "", 10.5)
409 pdf.set_text_color(*MUTED)
410 pdf.multi_cell(0, 5.5,
411 "Your feedback is invaluable during this alpha period. You can:"
412 )
413 pdf.ln(2)
414
415 feedback = [
416 "Take a screenshot in the app and share it with any notes",
417 "Message me directly -- bug reports, feature ideas, plugin requests",
418 "Export your OPML and share it if you hit import/export issues",
419 ]
420 for item in feedback:
421 pdf.set_x(pdf.l_margin + 4)
422 pdf.set_font("Helvetica", "", 10)
423 pdf.multi_cell(pdf.epw - 4, 5, f"- {item}", new_x="LMARGIN", new_y="NEXT")
424
425 pdf.ln(10)
426
427 # Thank you
428 pdf.set_font("Helvetica", "B", 14)
429 pdf.set_text_color(*TEXT)
430 pdf.cell(0, 10, "Thank you for trying Balanced Breakfast!", align="C", new_x="LMARGIN", new_y="NEXT")
431
432 # =========================================================
433 # OUTPUT
434 # =========================================================
435 output_path = "/Users/max/Git/active/balanced_breakfast/BalancedBreakfast-Pitch.pdf"
436 pdf.output(output_path)
437 print(f"PDF generated: {output_path}")
438
439
440 if __name__ == "__main__":
441 build_pdf()
442