Sean Mandable

Projects

Shipped and in-progress personal projects across iOS, macOS, and the web.

Brown Sign (iOS, 2025)

You know those brown signs on the highway, the ones that point to state parks, historical sites, scenic overlooks, the occasional mansion or covered bridge? I kept seeing them, wondering what they were, and not doing anything about it. Googling “that brown sign I just passed near Middletown” is not a real workflow. So most of the time I just drove on.

Brown Sign is the app I wanted to exist. Point your iPhone at the sign, tap the button, and a few seconds later you have the Wikipedia article, a map, a summary, a Look Around street-level view, and directions. If you’re riding shotgun, import the sign from a photo someone snapped, or just type the name. And because the moment passes at 65 mph, the scanner launches from everywhere: Siri, Control Center, the Lock Screen, the Action button. Or skip the signs and open the Nearby tab to see the landmarks around you on a map. The list is curated down to landmarks, so you get the covered bridges and battlefields without the local middle school, and pins accumulate as you pan across a state.

What’s underneath

OCR runs on-device via Apple’s Vision framework, sorting multi-line text top-to-bottom by bounding box so “Wadsworth Mansion / 2 MI” stays in the right order. The OCR output goes to Apple Intelligence (FoundationModels, iOS 26+) for normalization, which strips directions and distances and leaves just the landmark name. That cleaned-up name hits a four-source resolver: Wikipedia geosearch within 10 km, Wikipedia text search, NPS as a fallback for historic sites without Wikipedia coverage, and Wikidata for structured enrichment.

Candidates get filtered through three passes: a Wikidata P31 blocklist that drops bands, films, food, hoaxes, and people; a place-indicator whitelist; and a default-reject for anything that doesn’t earn its way through. Title-token overlap catches Wikipedia body-text matches that sneak past. Results sort exact-title-match first, then by distance, so “Wadsworth” near Middletown, Connecticut returns the mansion down the road instead of a city in Ohio. The top candidate gets a second enrichment pass: Apple Intelligence polishes the Wikipedia extract into a 2-3 sentence summary and scores the match confidence, so a shaky match is flagged as one instead of presented as certain. The article image downloads and resizes for offline browsing.

The Nearby tab runs a different pipeline: a curated Wikidata SPARQL query (heritage designations and a landmark-type allowlist, English Wikipedia article required, plus a gate that drops operating schools and hospitals) at a radius you pick, 2 to 25 miles, cached on disk so the tab opens instantly next launch. Landmarks you don’t care about can be hidden, and un-hidden when you change your mind.

No third-party Swift packages. Stock Apple frameworks all the way: SwiftUI + UIKit where needed, SwiftData for history, AVFoundation for the camera, CoreLocation, MapKit, SafariServices. Every external call fails silently to nil. The app works end-to-end without API keys, location permission, or Apple Intelligence.

Privacy

There are no accounts, analytics, ads, or tracking. Camera images are processed on-device. Location is used only to rank nearby results. Search queries go to Wikipedia, Wikidata, and NPS with no personal identifiers attached.

Status

Shipped to the App Store (currently v2.0), and still in active development because I keep thinking of new features.

Download on the App Store (opens in new tab) GitHub (opens in new tab) (MIT)


Debt Descent (iOS & macOS, 2026)

Every debt-payoff tool I tried made me pick a side before I understood the trade. The free web calculators want one balance and one rate and hand back a single number; the apps that do more want to log into your bank. I wanted to put my actual cards and loans in, run the Snowball and the Avalanche method side by side, and see the actual dates and the real interest each one costs, without any of it leaving my own machine.

Debt Descent is that planner, native on iPhone, iPad, and Mac. You add your debts, or point it at a statement PDF or scan the paper one with the camera and let it read them. Set what extra you can put toward them each month and it runs the payoff strategies at once: Snowball for the fast, motivating wins; Avalanche for the least interest; plus five more ordering methods. A loan in deferment (a student loan, say) goes in with the date the deferment ends; it sits out of the plan until then, nothing due, and rejoins on its own. It plots every balance falling to zero, tells you your debt-free date and what each method saves, and lets you model windfalls, refinances, and “what if I paid this down right now” against your own numbers, without anything leaving the device.

It’s also built for the months after the plan is made. Log payments (or edit them later) and it tracks your progress, including the interest your paydown has already saved. A debt you’ve covered this cycle says so instead of asking again. When it’s worth showing off, a progress card turns your paydown into an image (percent paid off, the descent line, your debt-free date) sized for a feed post, a square, or a story, with a switch that keeps the dollar amounts off it. Post it, or send it to whoever is cheering you on. If one monthly payment covers several debts, a debt-management plan for instance, you group them and log the payment once. It splits to the cent, follows the servicer’s schedule when there is one, and records the fee the plan keeps instead of letting it flatter the projection. When you take a refinance you modeled, you apply it in one step rather than re-entering everything. And a Left over card takes your take-home pay, at whatever cadence you’re paid, and the bills the plan doesn’t track (rent, insurance), and shows what’s left by the week, month, or year.

What’s underneath

It’s one universal SwiftUI app: a single codebase that builds for iOS, iPadOS, and macOS and forks only where the platforms differ, like a tab bar on the phone, a sidebar on the Mac, and drag-to-reorder on the Mac versus Move up / Move down in a debt’s menu on touch. The payoff engine is pure and separated from the UI, and it’s frozen against golden test vectors generated from the original reference logic, so the math can’t drift from one release to the next.

Statement import runs on device: PDFKit pulls the text from a PDF, VisionKit’s document scanner captures a paper statement with the camera (iPhone and iPad; a Mac imports a photo instead, and a Continuity Camera capture counts), Vision handles scanned pages and photos, and Apple Intelligence (FoundationModels) extracts the balance, APR, minimum, and due date, with no upload and no API key. Every figure the model returns has to appear verbatim in the text it was shown, so a blurry photo can’t invent a balance. Data lives in SwiftData. The promo-APR handling models a 0%-intro card reverting to its full rate on the day the promo ends, and the deferred-interest tracking warns you before a store-card promo turns into a retroactive interest bill. Exports (PDF, calendar .ics, JSON) and the due-date reminders are all built on stock Apple frameworks. The progress card renders through SwiftUI’s ImageRenderer against its own fixed dark palette, because adaptive colors resolve against whatever appearance the app happens to be in, and a consistent card has to carry its own.

Privacy

It has no accounts or analytics, and no servers of my own. Your data lives on your device and, if you’re signed into iCloud, syncs through your own private iCloud, where I can’t see it. Statements (PDFs, photos, camera scans) are read on device and never uploaded. The app talks only to Apple: iCloud for your own sync, and the App Store to confirm the one-time Pro purchase. (Full privacy policy and support.)

Status

On the App Store (currently v1.16). Free to use: add debts, run Snowball and Avalanche, see your debt-free date, import or scan a statement, group the debts one payment covers, back everything up to JSON, track your progress against the plan, ask Siri for your debt-free date or keep it on a widget, and share the progress card. A one-time $14.99 Pro unlock (no subscription, shared across all your devices) adds the five extra methods (one of them a custom order you set yourself), the what-if tools, planned windfalls, refinance modeling and one-step apply, amortization schedules, PDF and calendar export, and due-date reminders. Universal across iPhone, iPad, and Mac (iOS/iPadOS 26 or macOS 26 and later).

Download on the App Store (opens in new tab)

Free calculators that run in your browser, nothing saved or sent: Snowball & Avalanche, deferred interest, balance transfer, and debt management plan. There are also write-ups on how it compares with Undebt.it and Debt Payoff Planner.


ReKey (macOS, 2026)

Every browser has a “check your passwords” feature now, and so does Apple Passwords. The catch is that each one only looks at its own store. I’ve used Chrome, then Firefox, and now Arc, so the passwords I’d reused for years were spread across four or five different exports, and no single checkup could see across them. The other option is to hand the whole list to a password manager’s cloud and let it confirm what I already suspected. I wanted to find the reused and breached ones myself, on my own machine, without uploading anything.

ReKey is a local password health auditor for macOS. You export a CSV from each browser you’ve used (Chrome, Arc, Firefox, Apple Passwords), drop them in, and it finds the reused and compromised passwords, groups them by site worst-first, generates strong replacements, and walks you to each site’s change page. You approve every change, and ReKey never changes a password itself. It generates the new one and opens the change page; you make the change, and your browser’s own save prompt stores it.

What’s underneath

The whole thing is built so your plaintext passwords never leave the machine. They live in memory only and are never written to disk, logged, or sent anywhere. Each one is held in a small wrapper type whose debug and log output is redacted, so a password can’t be printed or interpolated into a log by accident. The single exception is the breach check, which uses Have I Been Pwned’s k-anonymity API: ReKey hashes the password with SHA-1 and sends only the first five characters of that hash. The server returns every breached hash sharing those five characters, and the comparison happens locally. The password, the full hash, and the account it belongs to never leave the device.

Import detects each browser’s format by its column headers rather than their position, and parses with a full RFC 4180 parser that handles quoted fields, embedded commas and newlines, escaped quotes, and the byte-order mark some exports include. URLs are canonicalized to their registrable domain through a vendored Public Suffix List, so accounts.google.com and mail.google.com group under google.com, and bbc.co.uk resolves correctly instead of collapsing to co.uk. Reuse is found across every imported browser at once, so a password you used in Chrome and again in Firefox shows up as a single cluster.

Replacement passwords come from the system CSPRNG (SecRandomCopyBytes), with rejection sampling so there’s no modulo bias and at least one character from each class you’ve enabled. Length, character classes, an avoid-look-alikes option, and a letters-and-digits-only mode for picky sites are all adjustable, and there’s a diceware passphrase mode built on the EFF wordlist.

Getting you to the right change page is its own problem. ReKey tries the site’s /.well-known/change-password path, the same convention Safari and Chrome use, but plenty of servers answer 200 to every URL, so a 200 there proves nothing. To catch that, it also requests a random path that cannot exist, and trusts the change page only if the real path succeeds while the nonsense path returns a not-found. If that fails it falls back to a small curated map, and last of all to the site’s home page with a note to find the setting yourself. It resolves one site at a time, and only when you choose to fix that one, so importing your accounts never pings every domain on your list. ReKey never asks an LLM for a URL. A wrong change-password link that looks right would be worse than the home-page fallback, so it doesn’t guess.

When you approve a fix, the new password goes to the clipboard marked concealed, using the convention that clipboard-history managers like Maccy and Raycast honor, so it stays out of their history. It clears itself about ninety seconds later, and it does that by checking the clipboard’s change count instead of reading the contents back, so it neither keeps the plaintext alive for the wait nor trips the macOS prompt for reading the clipboard. If you’ve copied something else in the meantime, it leaves it alone.

The app is sandboxed and never writes to any browser store or Apple Passwords. The one thing it ever puts in the keychain is a single random key of its own, used to fingerprint its bookkeeping, and never a password. Changing a password usually updates the saved login in place, but sometimes the browser saves a second copy and leaves the old one behind with the dead password. Clearing that out is a separate, opt-in command-line tool called rekey-cleanup, kept outside the sandboxed app and out of the App Store build altogether; it ships with the GitHub .dmg. It matches and deletes on the plaintext index fields alone, so it never reads the encrypted password blob or needs any decryption key. It dry-runs by default, refuses to run while the browser is open, backs up the store before touching it, and does its deletes inside a transaction. The app shows and copies the exact command for a site you’ve fixed, and you run it yourself in Terminal.

Every piece of logic (the parser, the audit, the generator, the breach client, the reset router) is free of SwiftUI and unit-tested on its own. The network clients sit behind protocols, so the engines test with no network and the live clients are wired in only at the app layer. There are 288 tests, including ones that cover the privacy guarantees.

Privacy

It has no accounts, analytics, telemetry, or crash reporting. The only network calls ReKey makes are the Have I Been Pwned prefix query and the on-demand lookup of a single change-password URL when you decide to fix that account. App Sandbox is on, with a short list of entitlements: outgoing network, read-write access to the files you pick (so it can securely overwrite and delete the source CSV after import), and a single folder bookmark used only if you turn on the optional auto-import watcher.

Status

ReKey is on the Mac App Store (currently v1.1.2), and is also available as a signed, notarized .dmg from GitHub Releases. The App Store version is the auditor, with the fix queue behind a one-time purchase that unlocks fixing and the destructive cleanup tooling left out, since a sandboxed app can’t write to other apps’ data. The GitHub build is the whole app. Both are universal binaries (Apple Silicon and Intel) for macOS 15 or later, written in Swift 6, and it builds from source with Xcode or a one-command signing script. I built it for my own password cleanup and kept going on it.

Download on the App Store (opens in new tab) GitHub (opens in new tab) Download (opens in new tab)


ModernPAR (macOS, 2026)

MacPAR deLuxe is the Mac app for PAR files. It has held that spot for a long time, mostly because nobody else bothered. PAR files add recovery data to a set of files, so when a piece goes missing or arrives corrupted you can rebuild it instead of starting over. I’ve used MacPAR deLuxe to verify and repair file sets for years. It’s Intel-only, it hasn’t shipped an update in a long while, and it stops working once Apple retires Rosetta 2.

ModernPAR replaces it. It verifies and repairs PAR2 and PAR1 sets, creates new recovery sets, and extracts RAR and zip archives, including multi-volume, password-protected, and self-extracting RAR sets. Drop a .par2 file or its folder on the window and it verifies, repairs any damage, then extracts the archive inside, with per-file status and the “need N more recovery blocks” math along the way. Drag-and-drop, open-with, and completion notifications that jump you to the file in Finder. It’s arm64 native, built in Swift 6 and SwiftUI.

What’s underneath

The PAR2 work runs on par2cmdline-turbo, the fast fork of the standard PAR2 engine. It’s C++, and instead of shipping it as a separate binary and shelling out, I embedded it in-process as a C++ SwiftPM target behind an exception-catching extern "C" shim. Swift consumes it as a plain C module, so none of the C++ leaks into the rest of the app. RAR extraction uses RARLAB’s UnRAR 7.2.4 the same way, and zip goes through the system libarchive. All three are wrapped as plain C modules, which means no target in the project uses C++ interop at all. A subprocess engine sits behind the same protocol as a designed-in fallback.

PAR1 is a different story. There’s no modern library for it, so the PAR1 encoder and the read-only parser are pure Swift, including the GF(2⁸) Reed-Solomon math. The output is byte-identical to the original Intel helper, and that helper reads my output back as valid. That comparison is one of the tests, so if the output ever drifts from the reference, the build goes red.

Licensing drove a few of the structural calls. par2cmdline-turbo is GPL, which is fine because ModernPAR is itself GPL-2.0. UnRAR carries a field-of-use restriction that makes it GPL-incompatible, so it lives in its own separately-licensed component and never merges into the GPL engine’s link unit. Every Mach-O in the bundle is arm64-only, including the embedded Sparkle 2 framework, and CI fails the build if anything Intel sneaks in. 342 tests, counting the byte-for-byte oracle checks and the license-compliance gates.

On the app side, every engine (verify, repair, create, RAR, zip) streams the same event type through an AsyncStream into one coalesced model on the main actor, so a single window renders all of them. The scene is a WindowGroup keyed on a folder-scoped session instead of a DocumentGroup, because the thing on screen is a long-running session and doesn’t fit the document model.

Licensing

ModernPAR is free software under GPL-2.0-or-later. That’s the license that lets it embed the GPL PAR2 engine in-process. The full third-party breakdown is in the repo.

Installing

v1.0.0 is available as a signed, notarized .dmg from GitHub Releases. Sandboxed, Hardened Runtime, macOS 14 or later. It updates itself through Sparkle. Still actively developed.

GitHub (opens in new tab) Download (opens in new tab)


Saddle (macOS, 2025)

My external drives kept making noise. They spun up for no reason, chattered in the background, and asked for attention they didn’t need. Most of the time they weren’t doing anything at all (I’d go days without reading or writing anything to them), but there they were, audible, occasionally waking from sleep for reasons known only to macOS.

The Finder’s answer to this is that you can eject a drive, but you can’t mount one. To remount you open Disk Utility, find the drive in the sidebar, and click Mount. Every time. For every drive. And if you have four external drives you want to manage as a group, which I do, it’s four round-trips through two apps for something that should be a single click.

Saddle is a macOS menu bar app that fixes this. One icon, real-time mount status for every external drive, click to mount or unmount. Group drives together and batch-mount or batch-unmount the whole group. Configure launch actions so drives auto-mount or auto-unmount when the app starts, and again when the Mac wakes from sleep. Drives you unmounted stay unmounted: when macOS remounts one on its own after a USB hiccup, Saddle puts it back. Exclude drives you don’t want managed. Give drives friendly aliases because “Untitled 3” is not a name.

What’s underneath

Native SwiftUI with no dependencies; even the update check is a plain call to the GitHub Releases API. The disk work happens in a small privileged helper the app talks to over XPC, with DiskArbitration callbacks detecting connect and disconnect in real time (no lag between plugging a drive in and seeing it in the menu) and a periodic reconciliation sweep as a backstop. Login item registration goes through macOS ServiceManagement. Notarized by Apple. Runs on macOS 13.5 (Ventura) or later.

Groups, aliases, launch actions, and exclusions all live in a settings window with tabs and forms, and nothing is configured through defaults-write commands or a JSON file. I run it on my own drives every day, so the settings had to be something I’d actually want to use.

Installing

Current release is 1.4.2. Available as a signed, notarized .dmg from GitHub, or via Homebrew:

brew tap smandable/tap && brew install --cask saddle

GitHub (opens in new tab) Download (opens in new tab)


Kelvin Caliper (in development, planned commercial release)

Hardware colorimeters cost $170-300 and most people never buy one. Modern iPhone cameras have better sensors than you’d expect and shoot RAW DNG, which exposes enough linear color information to do useful display calibration. Kelvin Caliper is a Mac + iOS app pair built on that observation: use the iPhone you already own as the measurement device, match the white point and gamma across your monitors, and apply the corrections directly.

The headline feature is multi-monitor matching. Pick one display as the reference, match the others to it. Absolute accuracy matters less than making your second and third monitors look like your main one.

What’s underneath

This is the design the spike exists to de-risk. None of it is built yet.

On the Mac: SwiftUI for the UI, ColorSync for ICC vcgt LUT generation and assignment (the fallback path for displays that don’t cooperate), and IOAVService for DDC/CI control on Apple Silicon, the same approach used by m1ddc, BetterDisplay, and Lunar after Apple broke the public IOI2CInterface on M-series Macs. VCP codes 0x10, 0x16/0x18/0x1A for brightness, red, green, blue gain.

On the iPhone: SwiftUI for the UI, AVFoundation for RAW DNG capture with fully locked exposure, white balance, and focus, and the Vision framework for AR-style capture guidance (VNDetectRectanglesRequest, which is lighter weight for this use case than full ARKit).

Pairing happens over Multipeer Connectivity, with no cloud, account, or network round-trip. The iPhone captures, sends the measurement to the Mac over the local peer connection, and the Mac does the math: per-device characterization, camera RGB to CIE XYZ, corrections applied as DDC writes or ICC vcgt LUT assignments depending on what the display supports.

Current state

I ran a validation spike: three throwaway pieces built to answer one question, whether iPhone-based measurements are repeatable and consistent enough to be useful. A SwiftUI Mac app that displays full-screen color patches in sequence. A SwiftUI iPhone app that captures Apple ProRAW Linear DNGs with locked exposure, white balance, and focus. And a Python analyzer (tifffile + imagecodecs + numpy + exifread) that decodes the DNGs and computes white point and gamma.

The first thing the spike showed is that Apple’s ProRAW “RAW” isn’t raw. The pixels arrive demosaiced, white-balanced, and already characterized into linear Display P3. Treat them as sensor RGB and apply the embedded ColorMatrix tags and you get garbage, about 9100K with a negative Duv. Treat them as the linear P3 they are and the numbers land: pointed at a factory-calibrated Dell U3224KB, an iPhone 16 Pro read the white point at 6544K, Duv +0.003, against a nominal 6500K D65 target. One measurement, but the early signal I was looking for.

Status

Parked mid-spike while ReKey and Debt Descent took the queue. The plan hasn’t changed: commercial release on the Mac App Store. The design notes are public; the code lands there as the spike graduates into the app itself. Please don’t fork it for commercial use when it does.

GitHub (opens in new tab)


Also

MovieDB (opens in new tab) is a local movie library cataloging app I built for myself. Angular 21 + TypeScript front end on AG Grid, PHP/MySQL backend, FFprobe for metadata extraction, duplicate detection across mounted volumes, batch filename normalization. It’s here because it’s the thing I use to keep my movie collection from slowly devolving into chaos.

CLI utilities. A handful of small PHP command-line tools I wrote to solve specific file-management problems and kept around because they’re still useful: consolidate_movies (opens in new tab) (groups episodic video files across multiple drives and consolidates each title onto a single drive, with space balancing), convert_to_mp4 (opens in new tab) (batch video conversion with corruption detection, junk/static detection via entropy analysis, and post-convert validation), de-obfuscate (opens in new tab) (renames obfuscated files using their parent directory name), delete_low_res (opens in new tab) (finds and removes video files below a width threshold), and mac-cleanup (opens in new tab) (a safe, single-file macOS cleanup script with per-step reporting). All of them dry-run-by-default, because I’ve learned that lesson.