This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady
What I Built
blurp is a keyboard-driven terminal app that turns one messy brain dump into separate, ready-to-send message drafts. You type "tell Anya I can't make it tonight, ask Dev whether the invoice was paid, and thank my professor about Monday" exactly once. A local open-weight model splits it into three individual drafts, each with its own tone, status, and body. You review, edit, copy, snooze, or complete them. Nothing is ever sent for you, and nothing ever leaves your machine: inference runs through your local Ollama, drafts live in a local SQLite file, and there is no account, API key, or hosted endpoint anywhere in the app.
Fitting challenge name, by the way.
Demo
Code
blurp
blurp is a local-first, keyboard-driven terminal workspace that turns one raw brain dump into separate, editable message drafts. You type a stream of unrelated thoughts; a local open-weight model splits them into individual messages, drafts each one, and flags whatever it cannot write responsibly as missing context. You review, edit, copy, snooze, or complete each draft Everything stays on your machine.
How it works
- Press
nand dump several unrelated intentions in one go. - blurp sends the dump to your local Ollama model with a strict system prompt that forbids inventing recipients, dates, amounts, or promises.
- The reply is parsed and validated in Rust: required fields are strict, and anything untrusted (tone, status) is normalized, so a model can never mark a draft completed or sneak in malformed data.
- Each draft lands in the workspace with its own status, tone, and a stable ID, and is saved to SQLite…
Rust + Ratatui for the TUI, Tokio for async, reqwest talking to the local Ollama API, rusqlite for storage, arboard for the clipboard.
How I Built It
The model is gemma4:e4b (open-weight Gemma), served by local Ollama and called non-streaming on /api/chat with format: "json". Any local instruct model works: BLURP_MODEL swaps it, and there is also gemma4-long:latest (which is my customized model) tested with a longer context window.
The interesting part is the contract between the app and the model. The system prompt is strict, and the Rust side trusts nothing:
flowchart LR
A[Brain dump] --> B[Review screen]
B --> C[Ollama chat, format json]
C -->|reply text| D[Parse and validate]
D -->|titles and bodies ok| E[Save to SQLite]
D -->|malformed| F[Recoverable error, dump kept]
C -->|Ollama down or slow| F
E --> G[Inbox drafts]
Validation rules:
- Required: at least one draft, non-empty title and body. A violation names the draft number and saves nothing.
- Normalized: unknown or missing tone falls back to professional. Status can only ever resolve to
draftorreview, so the model cannot mark something completed or snoozed. Model-supplied ids are ignored, the app assigns UUIDs. - Tolerant: code fences and prose around the JSON are stripped, a bare top-level array of drafts is accepted, nulls and empty strings are trimmed.
- Missing facts: the prompt forbids inventing recipients, dates, amounts, or promises. Anything unknown is listed in
missingDetailsand the draft lands in Inbox for review instead of pretending to be ready.
Generation never blocks the UI. It runs on a Tokio task, the footer shows a cancel hint, results come back through an event channel, and a generation counter discards replies from cancelled runs. Everything else is keyboard-first: / searches titles, recipients, and bodies, Ctrl+P opens a command palette, v pipes a draft through your $EDITOR, and Settings lists your installed models from the Ollama API and persists your pick in SQLite.
flowchart TD
R[Validated reply] --> U[App assigns UUIDs]
U --> S{status field}
S -->|draft| D[Drafts section]
S -->|review or anything else| I[Inbox, a human decides]
I --> H[Edit, copy, snooze, complete]
H --> P[(SQLite, survives restart)]
What it fails at:
- The first generation with a cold model load took about two minutes on my laptop. Warm regenerations run in around 20 seconds. The timeout is configurable via
BLURP_TIMEOUT_SECONDS. - Small models do try to invent things. The prompt forbids it and unparseable replies become retryable errors, but a confidently wrong draft can only be caught by a human, which is exactly why drafts land in review first.
- The per-draft
missingDetailslist is parsed and stored but not displayed in the UI yet. - Clipboard needs a desktop clipboard provider. Tested on Linux with Wayland only.
- No mouse, no sync, single user. On purpose.
Why Does Open Innovation Matter?
Personal messages are the most sensitive text you produce. Hand them to a closed API and your drafts, recipients, and half-thoughts become someone else's data. Open weights make three things possible that a hosted endpoint would not:
- Privacy by architecture, not by policy. After install, the app works with the network off, because the only destination is your own localhost.
- The model is a variable, not a vendor.
BLURP_MODELpoints at any local instruct model, and the strict contract means blurp does not care which weights answer. - Everything is inspectable. The prompt is a string in the repo and the validation is plain Rust, so "the model invented a date" is something you can fix and read, not a mystery behind someone's endpoint.
The Lazy Payoff
One dump, three drafts, then copy-paste each. That replaces opening three different apps, re-deciding who gets which fact, and retyping the shared context every time. During testing, the first invoice reminder came out too friendly; switching its tone to direct and regenerating only that draft took about 18 seconds and kept the recipient and facts intact. That is the whole product: think once, split automatically, send manually.
Prize Categories
Best Use of Gemma: Used Gemma 4 e2b & e4b models to test it out.




Top comments (0)