Retext
A macOS text expansion app with AI-powered snippet generation. Type a trigger like ;sig anywhere and it expands to your full text—or let AI write it for you.
What Is Retext?
Retext is a native macOS text expansion app that helps you type less and do more. Type a short trigger like ;sig anywhere on your Mac, and it instantly expands to your full email signature, code snippet, or any text you've saved. It works system-wide—email, Slack, VS Code, browsers, anywhere you type.
What makes Retext different is AI-powered snippet generation. Describe what you want, and Claude AI writes the content for you in real-time via streaming. No more staring at a blank text field trying to craft the perfect canned response.
Key Features
- System-wide expansion — Works in any macOS app with instant replacement, no popups
- AI-powered generation — Pro users can describe what they want and Claude writes it with real-time streaming
- Global hotkey — Press Control+Space to summon Retext from anywhere
- Tag organization — Organize snippets with tags, filter with one click
- Smart validation — Prevents duplicates, catches infinite loops, warns about problematic triggers
- Local storage — Snippets stored locally in UserDefaults, no cloud sync required
Architecture
The project spans a native macOS app and a backend API:
Retext/ # macOS app (Swift/SwiftUI)
├── App/ # Entry point, AppDelegate, Sparkle updates
├── Models/ # Data models, state management, hotkey manager
├── Views/ # Three-column NavigationSplitView layout
├── Controllers/ # Persistence, validation
├── Features/
│ ├── Updates/ # Sparkle auto-update integration
│ └── AIGeneration/ # Proxy service client, SSE stream parsing
└── TextReplacer.swift # Core keyboard interception engine
retext-api/ # Backend (Next.js on Vercel)
├── app/api/generate/ # AI generation endpoint with SSE streaming
└── lib/ # License validation, rate limiting, Redis cache
The macOS UI uses SwiftUI's NavigationSplitView for a native three-column layout: tags on the left, snippet list in the middle, and detail/editor on the right. Three @ObservableObject models coordinate state between panels, with selection bindings keeping everything in sync.
The Hard Problems
Keyboard Interception
The core challenge was intercepting keystrokes system-wide without affecting typing performance. The solution uses CGEvent.tapCreate() to create a global event tap on a background .userInteractive queue.
The engine maintains a 50-character rolling buffer with thread-safe access via a serial DispatchQueue. When the buffer matches a trigger like ;hello, it simulates backspaces to delete the trigger, then pastes the replacement via the clipboard (saving and restoring the user's clipboard contents after a 300ms delay).
Smart buffer trimming was tricky—when the buffer exceeds 50 characters, it trims to preserve any potential trigger by finding the last ; character, rather than just dropping the oldest character.
Infinite Loop Detection
Imagine a snippet called addr with content "My address is ;addr". When it triggers, it types out the content... which contains ;addr... which triggers again. Forever.
The solution was real-time validation that catches this before saving. The app checks if your replacement text contains your own trigger (case-insensitive) and blocks the save with a clear error. This is one of several validation rules—others include empty title/content, title length limits, and warnings for titles with spaces or common words that might trigger accidentally.
Distribution Outside the Mac App Store
Since the app needs Accessibility permissions for keyboard interception, it can't be sandboxed, which means it can't go on the Mac App Store. This opened up a whole world of complexity:
- Code signing with a Developer ID certificate (not Mac App Store distribution)
- Notarization with Apple so users don't see scary "unidentified developer" warnings
- Stapling the notarization ticket to the DMG for offline verification
- Sparkle for automatic updates since there's no App Store to handle it
I built a full CI/CD pipeline with GitHub Actions that handles all of this automatically. Push a version tag like v1.0.5 and GitHub Actions builds, signs, notarizes, creates a DMG, publishes a GitHub release, and updates the Sparkle appcast. It also supports beta releases (v1.0.5-beta1) and internal test builds via workflow dispatch.
AI Generation via Backend Proxy
AI snippet generation is a Pro-only feature that routes through a backend proxy service I built with Next.js and deployed on Vercel. This architecture keeps the Anthropic API key secure on the server while providing a seamless streaming experience to users.
The request pipeline:
- Client sends request with license key in the Authorization header
- Backend validates the license via Polar.sh (cached in Upstash Redis for 5 minutes)
- Rate limiting checks (tier-aware, Redis-backed)
- Request forwarded to Anthropic's Messages API
- SSE stream relayed back to the client in real-time
The proxy handles all the complexity—license validation, rate limiting, usage tracking—while the macOS client just needs to authenticate and consume the stream. Users see text appear progressively in the editor and can stop generation mid-stream or regenerate if they want a different result.
Global Hotkey Registration
The global hotkey (Control+Space by default) required diving into Carbon's legacy RegisterEventHotKey API—there's no modern Swift/SwiftUI equivalent for system-wide keyboard shortcuts. The HotkeyManager singleton handles registration, and a custom HotkeyRecorderView lets users set their preferred shortcut.
The KeyCombo model bridges between NSEvent.ModifierFlags and Carbon modifier constants, storing key codes and modifiers in a Codable format for persistence.
What I Learned
This was my first real Swift/SwiftUI project, and I learned a lot:
- SwiftUI architecture —
NavigationSplitViewfor three-column layouts,@ObservableObjectfor state management, customLayoutimplementations for wrapping tag chips - Low-level keyboard APIs —
CGEventtaps, thread-safe buffer management, simulating keypresses - macOS permissions — Accessibility permissions, entitlements, non-sandboxed distribution
- Apple distribution — Developer ID signing, notarization, Sparkle for updates
- Full-stack streaming — Building an SSE proxy with Next.js that relays Anthropic streams to a native client, parsing with
URLSession.bytes(for:) - Backend services — License validation, rate limiting, and usage tracking with Upstash Redis
- CI/CD for macOS — GitHub Actions with keychain setup, certificate handling, and multi-stage release workflows
Business Model
Retext uses a freemium model:
- Free: Up to 3 snippets
- Pro: Unlimited snippets + AI generation ($19/month or $99/year)
Licensing is handled through Polar.sh, with license validation cached in Upstash Redis (5-minute TTL). The app validates stored license keys on launch and the backend proxy re-validates on each AI request to enforce tier-aware rate limits.
Tech Stack
- macOS App: Swift, SwiftUI (NavigationSplitView)
- Keyboard: CGEvent API
- Updates: Sparkle
- Storage: UserDefaults
- Backend: Next.js on Vercel
- Cache: Upstash Redis
- AI: Anthropic Claude API (SSE streaming)
- Licensing: Polar.sh
- CI/CD: GitHub Actions