Skip to content

Latest commit

 

History

History
167 lines (130 loc) · 11.3 KB

CONTRIBUTING.md

File metadata and controls

167 lines (130 loc) · 11.3 KB

Contributing to Blurt

Thanks for taking a look. Blurt is a small, open-source macOS dictation app, and contributions of all sizes are welcome — bug reports, fixes, docs, or a new idea.

Before you start

Blurt is macOS-only (Intel or Apple Silicon, AppKit + AVFoundation). You need a Mac with Xcode to build, test, or run it — see Local setup below. On Linux you can still read and edit the Swift source, but you can't build or verify it locally; CI on macos-26 is the authority on green.

AGENTS.md is the canonical guide to how the code is laid out — the engine package, the AppKit shell, the dictation pipeline, and the intentional design decisions behind them. Read it before a non-trivial change so you don't reintroduce something that was deliberately removed.

Local setup

You'll need:

  • A Mac (Intel or Apple Silicon) on macOS 15+ (26 recommended).
  • Xcode 26+ — the full app, not just the Command Line Tools. xcode-select -p should print a path inside Xcode.app; if it doesn't, run sudo xcode-select -s /Applications/Xcode.app.
  • Homebrew, which bootstrap.sh uses to install everything else.
  • An AssemblyAI API key (free tier is plenty) if you want to actually dictate with your build. Blurt's setup wizard asks for it on first launch and stores it in the Keychain.

Then:

git clone https://github.com/AssemblyAI/blurt.git
cd blurt
scripts/bootstrap.sh        # install the toolchain from Brewfile
scripts/dev-build.sh        # build Blurt and install it to /Applications
open -a "Blurt Dev"

That's the whole setup. Everything else is one of these scripts:

Script What it does
scripts/bootstrap.sh brew bundle install from Brewfile — xcodegen, swiftlint, prettier, shellcheck, markdownlint-cli2, periphery, xcbeautify, node — then npm ci --prefix tools/jscpd for the pinned jscpd, and turns on the repo's pre-commit hook (.githooks/pre-commit: formats and lints staged Markdown), and verifies each one answers on PATH, because check.sh skips a missing linter rather than failing
scripts/dev-build.sh Clean signed Debug-Local build, installed as /Applications/Blurt Dev.app. The everyday loop.
scripts/check.sh The full health check CI runs. The source of truth for "is this green?"
scripts/check.sh --portable Just the platform-independent subset (docs, site, scripts, workflows) — the part that runs off a Mac
swift test Engine unit tests only (Swift Testing), no app build
scripts/uitest.sh The XCUITest suite that drives the real app
scripts/leaks.sh Whole-app leak check

dev-build.sh installs to /Applications on purpose: macOS refuses to register Accessibility and Input-Monitoring grants for an app living in a build directory, so Blurt is unusable from DerivedData. It builds the Debug-Local configuration — a debug build with the UI-test scaffolding compiled out, so it's the real app.

Dev builds are a separate app

Every debug configuration builds under the bundle id dev.alex.blurt.dev and installs as /Applications/Blurt Dev.app; only Release is dev.alex.blurt / Blurt.app. So a dev build sits beside a released Blurt rather than replacing it — you can run either, and each has its own Privacy & Security rows, its own settings, and its own Dock icon. Grant "Blurt Dev" microphone and Accessibility once; they stick across rebuilds.

Each keeps its own AssemblyAI key in the Keychain — blurt for the release, blurt-dev for the dev build — so a dev build can't read, overwrite or (through Settings → Advanced → Reset) delete the key you dictate with. You'll enter the key once in each, and neither one triggers the "another app wants to use your confidential information" prompt the shared item used to. The Library (saved transcripts, recordings, read-aloud clips) is split the same way: ~/Library/Application Support/Blurt for the release, Blurt Dev beside it for the dev build.

scripts/reset-install.sh wipes the release and scripts/reset-dev-install.sh wipes the dev build — its Library included, so it starts empty; the release's history is kept — each leaving the other alone (both run scripts/reset-lib.sh). Settings → Advanced → Reset does the same from inside the app, for whichever build you're running — and restarts it, so the permission prompts come back.

A note on signing

dev-build.sh signs with an Apple Development certificate from your login keychain. Adding any Apple ID under Xcode → Settings → Accounts gets you one for free. (The Developer ID release key is a different thing entirely and lives in a separate, normally-locked keychain used only by releases — see RELEASE.md.)

If you'd rather not set up a certificate, build ad-hoc signed instead:

xcodebuild -project App/Blurt/Blurt.xcodeproj -scheme Blurt \
  -configuration Debug-Local -destination 'platform=macOS,arch=arm64' \
  CODE_SIGN_IDENTITY="-" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=YES build

That skips the /Applications install, and an ad-hoc signature changes on every build — macOS keys the Accessibility grant to the signature, so you'd re-grant permissions after each rebuild. (Blurt notices the signature changed and clears the orphaned grant at launch; without that the Blurt Dev row stays switched on in System Settings while the app keeps reporting it as denied.) Fine for checking that a change compiles and launches; dev-build.sh is the better daily loop.

After editing App/Blurt/project.yml

Run xcodegen generate and commit the regenerated Blurt.xcodeproj/project.pbxproj alongside it. check.sh fails on drift between the two. Never hand-edit the .pbxproj — it's generated.

The iPhone app

App/BlurtiOS is the iPhone app and the keyboard it ships: the app listens and transcribes, the keyboard inserts the words (iOS lets no keyboard use the microphone). Its logic lives in BlurtiOSCore, apart from its views the way BlurtEngine is apart from the Mac shell.

xcodebuild -downloadPlatform iOS   # one-time: the simulator runtime this Xcode builds for
scripts/ios-sim.sh                 # build, boot, install, grant the mic, launch (repo root)
scripts/ios-check.sh               # its gates: tests + coverage floor, swiftlint analyze, periphery

No team or signing is needed for the simulator. Two things trip people up:

  • The runtime has to match Xcode's iOS SDK. Simulators on an older runtime still show in the list but are refused at build time.
  • The simulator dictates through your Mac's microphone, so macOS has to allow the Simulator app the microphone (System Settings › Privacy & Security › Microphone), with the right input picked under the Simulator's I/O › Audio Input. Refused, it records silence rather than failing.

To use the keyboard: Settings › Apps › Blurt › Keyboards — turn on Blurt, then Allow Full Access — and Start listening in Blurt. A phone needs a paid team for the App Group; App/BlurtiOS/README.md has that, the tests and the design loop.

Working without a Mac

You can read and edit Swift, and scripts/check.sh --portable fully verifies docs, site, script, and workflow changes. It skips the entire Swift side and says so on its last line, so don't read a green portable run as "it builds" — open the PR and let CI on macos-26 answer that.

Pull requests

  • Branch off main and keep each PR focused on one thing.
  • Make sure scripts/check.sh passes (or, if you're on Linux, say so in the PR and let CI verify).
  • Match the surrounding code: 2-space indent, Swift Testing for tests, no new external dependencies in the engine.
  • Write a clear description of what changed and why.

Every PR that touches code gets an installable dev build: CI builds Blurt.app from your branch and a bot comments a download link, so a reviewer can try the change rather than imagine it. The build is ad-hoc signed and not notarized, so it needs its quarantine flag cleared, and its permissions granted once per build — the comment spells out the commands.

Reporting bugs and ideas

Open an issue using one of the templates. For bugs, include your macOS version and steps to reproduce. For a security issue, don't open a public issue — see SECURITY.md.

By contributing, you agree your work is licensed under the repo's MIT License.