Read this first when starting a session in this repo. Captures conventions, commands, and current state so you don't have to rediscover them.
Native macOS ad blocker wrapping Brave's adblock-rust. SwiftUI Liquid Glass dashboard plus bundled Safari and Chrome extensions, all driven by a shared declarativeNetRequest ruleset. See README.md for the public pitch.
./scripts/fetch-filterlists.sh # EasyList + EasyPrivacy + Fanboy's Annoyance + Peter Lowe's list
./scripts/build-rules.sh # compile filter lists -> safari-ext/rules.json + filterlists-meta.json
cargo run --example test_blocker --release # Rust validation harness (8 cases)
./scripts/build-swift-bridge.sh # build Rust lib + vendor C header into engine-swift/
swift test --package-path engine-swift # XCTest suite for the FFI bridge
./safari-ext/scripts/sync-filters.sh # copy filter lists into the Safari bundle (only needed before WASM phase 4)
./chrome-ext/scripts/sync-filters.sh # same for the Chrome bundle
node test-chrome-functional.js # Playwright end-to-end test of the Chrome ext (requires `npm install`)
xcodebuild -project mac-app/Mane/Mane.xcodeproj -scheme Mane -allowProvisioningUpdates buildengine-rs/— Rust crate wrappingadblock-rust, with C ABI insrc/ffi.rsengine-wasm/— Rust crate withwasm-bindgen, currently unused at runtime; returns for cosmetic filtering in phase 4engine-swift/— Swift package wrapping the C ABI (ManeEngineclass)safari-ext/— Safari Web Extension source;filters/,engine/,rules.jsonare gitignored regenerated artefactschrome-ext/— Chrome MV3 standalone (storage-driven, no native messaging);filters/,rules.json,_metadata/are gitignoredmac-app/Mane/Mane.xcodeproj— Xcode project: container app (SwiftUI dashboard) andMane Extensiontargetscripts/—fetch-filterlists.sh,build-rules.sh,build-swift-bridge.sh,build-wasm-engine.shtest-extension*.js,test-chrome-functional.js— Playwright harnesses; output in gitignoredtest-output/
- The Cargo workspace's
target/lives at the repo root, not under each member crate. Linker paths must reflect this (e.g.Package.swiftuses-L../target/releasefrom insideengine-swift/). - Filter lists are gitignored. New sources go in
scripts/fetch-filterlists.shand thelistsarray inengine-rs/examples/compile_rules.rs, not committed wholesale. - The cbindgen-generated C header (
engine-rs/include/mane_engine.h) and its vendored copy in the Swift package are both gitignored; the bridge script regenerates them. - FFI function bodies must be wrapped in
catch_unwind(AssertUnwindSafe(...)). A Rust panic crossing the C boundary is undefined behaviour; we catch and return a sentinel (null pointer or false). - Swift imports C opaque struct pointers (
typedef struct Engine Engine;→Engine*) asOpaquePointerdirectly. Don't wrap withUnsafePointer<Pointee>— Swift will complain about type mismatches. - Do not use
UserDefaults(suiteName:)for the dashboard ↔ extension bridge. macOS CFPreferences rejects the "AnyUser + ByHost + non-system Container" combination that UserDefaults silently uses for App Group suites, so writes look fine but reads return stale data. The bridge uses plain JSON files (mane-control.json,mane-stats.json) in the App Group container viaFileManager.containerURL(forSecurityApplicationGroupIdentifier:). - App Group identifier:
group.com.albassam.mane. Apple Developer Program team:K465H4V2A2. Both targets are sandboxed, both carry the App Group entitlement, the local Mac is registered as a development device on the portal, and both App IDs and the App Group are registered with the App Groups capability linked. - British English in user-facing text and most documentation. Code/API names follow the language's convention (snake_case in Rust, camelCase in Swift).
- No em dashes anywhere. No AI-isms ("comprehensive", "robust", "leverage", "seamlessly", "let's…"). No Claude co-author trailers in commits.
Phase 3 complete on Safari and Chrome. Both surfaces block ads end-to-end and the dashboard plus popups are wired up. Phase 4 is closing catch-rate gaps and starting cosmetic filtering.
- Blocking is dNR-driven on both browsers.
engine-rs/examples/compile_rules.rsreadsfilterlists/, calls Brave'sFilterSet::into_content_blocking(), maps each rule to declarativeNetRequest format, writessafari-ext/rules.jsonplussafari-ext/filterlists-meta.json. Background scripts don't run the engine at runtime. - Ruleset: 122,026 rules from EasyList + EasyPrivacy + Fanboy's Annoyance + Peter Lowe's list, ~15 MB.
- Safari dashboard ↔ extension bridge: plain JSON files in
~/Library/Group Containers/group.com.albassam.mane/. Dashboard polls the control file every 3s for popup-initiated toggles. - Chrome (
chrome-ext/): storage-driven, no native messaging. Popup is the only UI surface. - Apple Developer Program signing is set up. "Allow Unsigned Extensions" is no longer required.
- Catch rate: a handful of trackers slip through the four-list set. Build
filterlists/mane-custom.txtwith||permutive.com^,||permutive.app^,||cm.g.doubleclick.net^,||stats.g.doubleclick.net^,||www3.doubleclick.net^,||googletagmanager.com^. Per-site allow/deny rules for Forbes-style anti-adblock retry loops. getMatchedRulesundercounts: live Chromium test showed 4–11 matches reported viagetMatchedRules({})while the request diff showed 200+ requests prevented. The dashboard's "Today blocked" count uses the same API surface and likely undercounts on Safari too. Need to verify and switch to awebNavigation-derived tally oronRuleMatchedDebugin dev builds.- Cosmetic filtering, element picker, per-site pause: all phase 4. Cosmetic rules need
engine-wasm/back online.
- macOS 26 (Tahoe) or later (required for Liquid Glass)
- Xcode 26 or later
- Rust stable (
rustup) - Node 22+ for the Chrome extension and Playwright testing
Apple Developer Program signing is in place (team K465H4V2A2). Signed and notarized builds are the default plan from phase 7, not a "later" concern.