AI News
Loading...

QuillFix

100% Local v1.0.0 By Sol AI November 2026 ~12 min read

QuillFix is a macOS menu bar app that turns the Tab key into a spell-and-grammar fixer. Click into any text field — Mail, Safari, TextEdit, Notes, Slack — type something sloppy, press Tab, and the line is quietly corrected by a model running on your own machine. No selecting, no copying, no pasting. Just type and Tab.

✨ ⌨️

QuillFix 1.0.0

Menu Bar App · Local Ollama · MIT Licensed

What is QuillFix?

QuillFix is a native macOS menu bar app — no dock icon, no window, just a tiny wand icon in your status bar. It installs a system-wide CGEvent tap that watches for the Tab key. When you press Tab inside a text field, QuillFix suppresses the keypress, reads the text from the cursor to the end of the line through the macOS Accessibility API, sends it to a local language model for correction, writes the corrected text back into the field, and then re-injects the Tab so focus moves on exactly as it normally would.

The whole thing lives in one Swift file of roughly 900 lines. It has no sandbox, no accounts, no telemetry, and no network calls except to localhost.

⌨️ Tab Does The Work

No select-all, no copy, no paste. Press Tab and the line from your cursor to end-of-line gets rewritten in place.

🔒 Runs Locally

The Ollama model runs on your Mac. The only network traffic goes to http://localhost:11434.

🧠 Any Ollama Model

The settings window lists whatever ollama list returns. Default is qwen3:14b.

🎚️ Tunable Temperature

A slider from 0.0 to 1.0. Default 0.1 — precise, boring, correct. That is the point.

✏️ Your Own System Prompt

A full-text editor for the copy-editor instruction the model receives. Make it sound like you, or like Hemingway.

📊 Live Status

The menu bar menu shows the last result — "Fixed: 24 → 26 chars", "No changes needed", or the error.

🔁 No-Op When Nothing's Wrong

If the model returns text identical to what you typed, QuillFix writes nothing. The Tab still moves focus.

🧹 Single File, No Xcode

One QuillFix.swift, one build.sh. Compiled with swiftc straight into a .app bundle.

Privacy Promise

Your text is read from the focused text field, sent to your own Ollama server on localhost, and written straight back. It never leaves your Mac. There is no account, no sign-in, no analytics, no error reporting, and no phone-home code anywhere in the source. The repo is MIT licensed — go read it and check for yourself.

Requirements

QuillFix has a short, honest list of needs. Check them before you start, because two of them are dealbreakers.

Requirement Detail Where it comes from
macOS 13.0+ Ventura or newer. Apple Silicon is what the project is developed and tested on. LSMinimumSystemVersion = 13.0 in the generated Info.plist
Accessibility permission Absolutely required. Without it QuillFix cannot read or write any text field. macOS Security framework — QuillFix calls AXIsProcessTrusted() before every fix
Ollama running locally Either ollama serve in the background, or the Ollama desktop app running. HTTP calls to http://localhost:11434
A pulled model At least one model in ollama list. The code default is qwen3:14b. GET /api/tags populates the model picker
Swift toolchain Only if you are building from source — swiftc from the Xcode Command Line Tools. build.sh calls swiftc -O directly

Accessibility is not optional

QuillFix is a keyboard-event interceptor that reads and writes text in other people's apps. macOS treats that as sensitive, so it must be granted explicitly: System Settings → Privacy & Security → Accessibility → enable QuillFix. There is no way around it, and that is exactly how you should want it to be.

Installation

QuillFix ships as source, not as a signed download. You clone it, you build it, you run it. The build script wraps the compiled binary in a proper .app bundle with the right Info.plist.

Step 1: Install Ollama and pull a model

# Install Ollama
brew install ollama

# Start the local server (or just launch the Ollama desktop app)
ollama serve

# Pull a model — any of these are fast enough for real-time use
ollama pull qwen3:14b      # the code default
ollama pull gemma4:31b     # the README's recommendation
ollama pull phi4-mini      # the speed demon

Check it is alive:

curl http://localhost:11434/api/tags
ollama list

Step 2: Clone and build

git clone https://github.com/TheSolai/quillfix.git
cd quillfix
./build.sh

The script compiles QuillFix.swift and assembles the bundle at:

build/QuillFix.app

Under the hood, build.sh runs:

swiftc -O QuillFix.swift \
  -o build/QuillFix.app/Contents/MacOS/QuillFix \
  -framework AppKit -framework SwiftUI -framework Carbon

Do NOT launch it with open

This is the one big gotcha. Launching via open build/QuillFix.app starts the app inside a LaunchServices security context where the CGEvent tap is blocked — QuillFix appears in the menu bar and then does absolutely nothing. The build script and the source header both say so explicitly. Run the binary directly:

# Correct — run the binary directly
./build/QuillFix.app/Contents/MacOS/QuillFix

# Or install to /Applications and run it from there
cp -R build/QuillFix.app /Applications/
/Applications/QuillFix.app/Contents/MacOS/QuillFix &

Step 3: Uninstall (later)

# Remove the app
rm -rf /Applications/QuillFix.app

# Clear settings
defaults delete com.quillfix.app

QuillFix stores its settings in UserDefaults under the key QuillFix.settings.v1, inside the com.quillfix.app domain — so defaults delete com.quillfix.app wipes it cleanly.

Granting Accessibility Permission

On first launch QuillFix checks AXIsProcessTrusted(). If permission has not been granted, it pops an alert titled "QuillFix needs Accessibility permission" with an Open System Settings button that deep-links straight to the right pane.

  1. Launch QuillFix: ./build/QuillFix.app/Contents/MacOS/QuillFix
  2. The permission alert appears (or click the wand icon in the menu bar → ⚠ Grant Accessibility permission…).
  3. Choose Open System Settings — this opens System Settings → Privacy & Security → Accessibility directly.
  4. Find QuillFix in the list and switch it on. If it isn't listed yet, click + and select the binary at build/QuillFix.app/Contents/MacOS/QuillFix.
  5. Quit and relaunch QuillFix so the trust check runs again against the freshly granted permission.
System Settings — Privacy & Security — Accessibility
┌─ Accessibility ──────────────────────────────────────────┐
│                                                              │
│  Allow these apps to control your computer:                 │
│                                                              │
│   ☑ QuillFix              ← the one you care about          │
│   ☐ Terminal                                                │
│                                                              │
│  [ + ]  [ – ]  ⋯                                             │
└──────────────────────────────────────────────────────────────┘

How to tell it worked

Click the wand icon in your menu bar. If the permission is missing, the very first item in the menu reads ⚠ Grant Accessibility permission…. If it is granted, that warning disappears and you get a clean menu with the engine line, Settings, About and Quit.

Your First Tab Fix

Once the permission is granted and Ollama is running, this takes about fifteen seconds.

  1. Click into any text field — a Mail draft, a Safari search box, a TextEdit document, a Slack message box.
  2. Type a deliberately imperfect sentence, for example i goes to the store yesterday.
  3. Put the cursor at the start of the line (or wherever you want the fix to begin) and press Tab.
  4. QuillFix rewrites the line from the cursor to the end of the line and drops the caret into the next field.
TextEdit — before and after
BEFORE   i goes to the store yesterday
         ^ cursor here
         ⇣ press Tab

AFTER    I went to the store yesterday.
         ⇣ focus moved to the next field

What the menu bar tells you

Click the wand icon after a fix and you'll see the last status line, straight from the source:

  • Fixed: 24 → 26 chars — the rewrite landed.
  • No changes needed — the model returned exactly what you typed, so nothing was written.
  • Nothing to fix on this line — the rest of the line was blank.
  • Couldn't read text field — the Accessibility API could not find a text element under the focused one.
  • Model returned empty — the model handed back nothing usable.
  • Ollama error — plus the underlying error, e.g. Network: The request timed out.

Terminal and iTerm are excluded on purpose

QuillFix explicitly refuses to touch Terminal and iTerm2. The source checks the frontmost app's bundle identifier and bails out for com.apple.Terminal and com.googlecode.iterm2. Rewriting a shell command out from under you is exactly the kind of "helpful" that ruins your afternoon.

How the Interception Works

Verified against QuillFix.swift. Here is the actual pipeline, top to bottom.

1. The CGEvent tap

On launch, TabWatcher.start() schedules a Core Foundation timer roughly half a second later. When it fires, setupTap() creates a CGEvent.tapCreate with:

  • Mask: keyDown events only.
  • Location: it tries .cgAnnotatedSessionEventTap first (macOS 13.4+), then falls back to .cgSessionEventTap.
  • Place / options: .headInsertEventTap with .defaultTap — a default tap, which means you can actually suppress events (a .listenOnly tap could not).

The tap's run loop source is attached to the main run loop in .commonModes. If macOS ever disables the tap (tapDisabledByTimeout or tapDisabledByUserInput), the callback re-enables it rather than dying.

2. The callback

The tap callback fires on the WindowServer's dedicated tap thread — not the main thread. It filters for key code 48 (Tab) on keyDown, logs CG: Tab key intercepted, calls watcher.onTabKeyDown(), and then returns nil — which suppresses the Tab. The app never sees it.

3. The lock and the hop

onTabKeyDown() takes an NSLock and sets isProcessing = true, so a second Tab during an in-flight fix is ignored rather than queued. It then captures the onTab closure and hops to the main actor via Task { @MainActor in … }.

Known sharp edge: the tap-thread hop

The project's own HANDOFF.md documents a real problem: in Swift 6, dispatching from a CGEvent tap callback thread to the main actor can silently never execute. The planned fix — swapping the Task for a CFRunLoopSource — is fully designed in DESIGN.md and still open in TODO.md under Priority 1. If Tab interception feels dead on your machine, check the log file before you blame the model.

4. Reading the field

AXHelper.readFocusedText() asks the system-wide AX element for kAXFocusedUIElementAttribute, then walks the element tree breadth-first until it finds a child whose kAXValueAttribute is a non-empty String — that is your text field. It then reads kAXSelectedTextRangeAttribute and takes the range's location as the cursor position.

FixRunner.runFix() converts that cursor into a line range with NSString.lineRange(for:) and takes the substring of that range. If the trimmed result is empty, it sets status "Nothing to fix on this line" and stops.

What the code actually does vs. what the README says

Worth knowing precisely: lineRange(for:) returns the entire line containing the cursor — not just the tail from the cursor onward. So the model receives the whole line, but the write-back step replaces only the range from the cursor to the end of the line. In practice the safest habit is to put the cursor at the start of the line before pressing Tab; then the two ranges line up exactly and the result is what the model returned.

5. The model call

The OllamaClient actor POSTs to /api/generate on your configured Ollama URL with a 60-second timeout:

POST http://localhost:11434/api/generate
Content-Type: application/json

{
  "model":    "qwen3:14b",
  "system":   "You are a careful copy editor. Fix spelling, punctuation, and
               obvious grammar mistakes in the text. Preserve the writer's
               voice, formatting, and meaning. Return ONLY the corrected text
               — no commentary, no quotes, no preamble.",
  "prompt":   "Fix the spelling, punctuation, and grammar in the following text.
               Return ONLY the corrected text.\n\n---\n<your line>\n---\n\nCorrected:",
  "stream":   false,
  "options":  { "temperature": 0.1 }
}

The response's response field is then run through a small stripWrappers() cleaner that strips chatty preambles ("Here's the corrected text:", "Corrected:", "Fixed:", "Here is the corrected version:") and a wrapping pair of quotes, then trims whitespace.

6. Writing the fix back

AXHelper.replaceRange(start:end:newText:) sets kAXSelectedTextRangeAttribute to the range from the cursor to the end of the line, then sets kAXSelectedTextAttribute to the corrected string — which replaces exactly that selection. If that path fails, it falls back to reading the whole kAXValue, doing a NSString range replacement in memory, and writing the full new value back with kAXValueAttribute.

7. Re-injecting Tab

Finally, reinjectTab() builds a fresh CGEventSource(stateID: .hidSystemState), creates a Tab keyDown and a Tab keyUp with virtual key 48, and posts both to .cghidEventTap. The focused app receives a normal Tab and moves focus, so the app keeps working the way you expect. The isProcessing flag is then cleared after a one-second cooldown.

Where the log lives

Every step above is logged to /var/folders/…/T/quillfix.log (the system temporary directory) and mirrored to stderr. Watch it live with:

tail -f "$(getconf DARWIN_USER_TEMP_DIR)/quillfix.log"

Choosing an Engine

Open the settings window from the menu bar (Settings…, shortcut ⌘,) and you'll find the Engine picker at the top. It is a segmented control with two options: Ollama (local) and Apple Intelligence.

Ollama (local) — the working engine

This is the default and the one that actually does the work today. The Ollama panel gives you:

  • URL — defaults to http://localhost:11434. Point it at a remote box if you really want to, though that is no longer "local".
  • Model — a dropdown populated from GET /api/tags. Hit Refresh after pulling a new model. If the list is empty, the app tells you to pull one.
  • Temp — a 0.0–1.0 slider, default 0.10. Lower is more faithful to your wording; higher lets the model improvise.
  • Test connection — hits the API and reports "✓ Connected — N model(s)" or the error.

Below the Ollama panel sits the System prompt editor — a monospaced text field holding the instruction sent as the system parameter. Edit it freely; the default copy-editor prompt is shown in full in the previous section.

Apple Intelligence — a placeholder, not yet implemented

Honest disclosure: in the current source, the Apple Intelligence engine is a stub. The enum is literally this:

@MainActor
enum AppleIntelligence {
    static func fix(text: String) async -> String? {
        return nil
    }
}

It returns nil, so selecting that engine produces the status "AI not available" and the error "Apple Intelligence unavailable" — every fix fails. The README describes macOS Writing Tools integration on macOS 26+, and DESIGN.md lists it as a "placeholder toggle, not yet implemented". Treat it as a reserved slot, not a feature.

Also not wired up: the notification toggle

The settings window has a "Show status in menu bar" toggle bound to showNotifications, and the value is persisted — but no code path currently reads that flag to hide or show anything. The menu bar item and the status line are always present. Treat the toggle as stored-but-unused rather than a working preference.

Hotkey

There is no configurable hotkey. Tab (key code 48) is hard-coded, and Shift+Tab support is still an open item in TODO.md under Priority 3. One trigger, one meaning: Tab fixes, Tab moves on.

Troubleshooting

"Nothing happens when I press Tab"

  • Are you running it with open? That is the number one cause. Launch the binary directly: ./build/QuillFix.app/Contents/MacOS/QuillFix.
  • Is Accessibility granted? If not, the menu shows the ⚠ Grant Accessibility permission… warning.
  • Is the app in Terminal or iTerm? Both are deliberately excluded — QuillFix will not touch them.
  • Check the log for CG: tap ACTIVE. If you see CG: FATAL — no tap location available, macOS refused to create the event tap — usually a permissions or signing problem.
  • Watch for the main-thread hop stall described in HANDOFF.md: logs show CG: Tab key intercepted and CG: processing Tab but never reach App: onTab fired. That is the known Swift 6 tap-thread issue, and the CFRunLoopSource fix is documented but not yet applied in the code.

"Ollama error" / "Can't reach Ollama"

  • Is Ollama running? curl http://localhost:11434/api/tags.
  • Did you start the server with ollama serve? The CLI does not always daemonise on install.
  • Is the URL in Settings right? It defaults to http://localhost:11434 with no trailing slash — a trailing slash is fine, but a wrong port is not.
  • Have you pulled a model? An empty ollama list means the model dropdown will be empty and the fix will fail.

"Ollama is running but returned no models"

The settings window shows this exact message when /api/tags responds but the model list comes back empty. Fix it with ollama pull qwen3:14b, then press Refresh in the model row.

The fix takes too long

Requests time out after 60 seconds. The first request to a cold model pays the model load time, so the very first Tab after launching Ollama feels sluggish. Subsequent fixes are much faster. A smaller model helps too — phi4-mini or qwen3:14b over something like a 30B-class model.

The text got mangled or duplicated

QuillFix asks the model for the corrected text and writes it over the range from the cursor to the end of the line. If your model paraphrased instead of correcting, the rewritten line may differ more than you expected. Lower the Temp slider toward 0.0, or tighten the System prompt to demand that only spelling and punctuation change.

Nothing changes at all

If the model's reply is byte-identical to what you typed, QuillFix reports "No changes needed" and writes nothing. That is the designed behaviour, not a bug — it means your line was already clean, or the model agreed with you.

Reset everything

# Clear QuillFix settings
defaults delete com.quillfix.app

# Remove the app
rm -rf /Applications/QuillFix.app

Privacy

QuillFix exists because the obvious way to fix your typing is to send it to a cloud service. QuillFix refuses to do that.

What actually happens to your text

  1. The Accessibility API reads the focused text field's value from your running app.
  2. The line from the cursor to end-of-line is sent to http://localhost:11434 — a process on your own machine.
  3. Your local model returns the corrected text.
  4. The corrected text is written back into the field via the Accessibility API.

There is no other network call in the app. No analytics, no crash reporting, no update pings, no accounts, no licence server, no telemetry of any kind. And because you control the Ollama URL, you control where it goes — point it at localhost and nothing leaves the machine.

Privacy Promise

All AI processing happens on your device. Your text is never sent to a cloud provider, because there is no cloud provider in the code. The source is MIT licensed and public — read it, fork it, run it on an air-gapped Mac. The only thing QuillFix asks for is the macOS Accessibility permission, and it uses it for exactly one thing: reading and rewriting the text field you are typing in.

One honest caveat

Accessibility permission is broad by nature — it can read and control any app on your Mac, not just text fields. QuillFix uses it narrowly (focused element, text value, selected range), but it does technically hold that power. If you want to be careful, grant the permission, use the app, and revoke it in System Settings when you are done. Nothing is left running once the permission is revoked.

Credits & Links

QuillFix is open source and MIT licensed.

Built with

  • AppKit — NSStatusItem menu bar UI, NSAlert, NSWorkspace
  • SwiftUI — the settings window
  • CoreGraphics — CGEvent.tapCreate, CGEventSource, HID event posting
  • Accessibility API — AXUIElement read/write of the focused text field
  • Core Foundation — CFRunLoopSource and CFRunLoopTimer plumbing
  • Ollama — the local model server
  • Swift — compiled straight with swiftc, no Xcode project

Model credits

Thank you

QuillFix is the smallest possible answer to a very modern problem: your typing deserves better than a spell-checker that only knows about misspellings, and your prose deserves better than a cloud service. Fix it locally, fix it with Tab, keep it on your machine. That's the whole idea.

💬 Join the Discussion

Share your thoughts. Requires a free GitHub account.