pasteboard-textinsertion
Pasteboard & Text Insertion
Insert text INTO other applications. The core capability of prompt managers and text expanders.
Critical Constraints
- ❌ DO NOT forget to save/restore clipboard contents → ✅ Preserve user's clipboard before overwriting
- ❌ DO NOT skip the delay between clipboard set and paste simulation → ✅ Add 50ms delay minimum
- ❌ DO NOT use only one insertion method → ✅ Build a fallback chain: Paste → Typing → Accessibility
- ❌ DO NOT use Accessibility API without permission check → ✅ Always check
AXIsProcessTrusted()first
Insertion Methods (Ranked)
| Method | Reliability | Speed | Sandbox | Best For |
|---|---|---|---|---|
| Clipboard + Paste | ★★★★★ | Fast | ✅ | Default method |
| CGEvent Typing | ★★★★☆ | Slow | ⚠️ Needs Accessibility | Paste-blocking apps |
| Accessibility API | ★★★☆☆ | Fast | ⚠️ Needs Accessibility | Text fields only |
Method 1: Clipboard + Simulated Paste (Default)
import AppKit
import Carbon
class TextInserter {
func insertViaPaste(_ text: String, completion: (() -> Void)? = nil) {
let pasteboard = NSPasteboard.general
let previousContents = pasteboard.string(forType: .string)
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
self.simulateKeyPress(keyCode: 0x09, modifiers: .maskCommand) // Cmd+V
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
if let previous = previousContents {
pasteboard.clearContents()
pasteboard.setString(previous, forType: .string)
}
completion?()
}
}
}
func simulateKeyPress(keyCode: CGKeyCode, modifiers: CGEventFlags) {
let source = CGEventSource(stateID: .hidSystemState)
let keyDown = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true)
keyDown?.flags = modifiers
keyDown?.post(tap: .cghidEventTap)
let keyUp = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false)
keyUp?.flags = modifiers
keyUp?.post(tap: .cghidEventTap)
}
}
Method 2: CGEvent Character Typing (Fallback)
extension TextInserter {
func insertViaTyping(_ text: String, delayPerChar: TimeInterval = 0.01) {
let source = CGEventSource(stateID: .hidSystemState)
for char in text {
let str = String(char)
if let event = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: true) {
var unichar = str.utf16.first!
event.keyboardSetUnicodeString(stringLength: 1, unicodeString: &unichar)
event.post(tap: .cghidEventTap)
}
if let event = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: false) {
event.post(tap: .cghidEventTap)
}
Thread.sleep(forTimeInterval: delayPerChar)
}
}
}
Method 3: Accessibility API (Direct)
import ApplicationServices
extension TextInserter {
func insertViaAccessibility(_ text: String) -> Bool {
guard AXIsProcessTrusted() else { return false }
let systemWide = AXUIElementCreateSystemWide()
var focusedElement: CFTypeRef?
guard AXUIElementCopyAttributeValue(systemWide, kAXFocusedUIElementAttribute as CFString,
&focusedElement) == .success,
let element = focusedElement as! AXUIElement? else { return false }
// Verify it's a text field
var role: CFTypeRef?
AXUIElementCopyAttributeValue(element, kAXRoleAttribute as CFString, &role)
guard let roleStr = role as? String,
roleStr == kAXTextFieldRole || roleStr == kAXTextAreaRole else { return false }
// Get current value + selection, insert at selection
var currentValue: CFTypeRef?
AXUIElementCopyAttributeValue(element, kAXValueAttribute as CFString, ¤tValue)
let current = (currentValue as? String) ?? ""
var selectedRange: CFTypeRef?
AXUIElementCopyAttributeValue(element, kAXSelectedTextRangeAttribute as CFString, &selectedRange)
var newValue = current
if let range = selectedRange as? AXValue {
var cfRange = CFRange()
AXValueGetValue(range, .cfRange, &cfRange)
let start = current.index(current.startIndex, offsetBy: cfRange.location)
let end = current.index(start, offsetBy: cfRange.length)
newValue.replaceSubrange(start..<end, with: text)
} else {
newValue += text
}
return AXUIElementSetAttributeValue(element, kAXValueAttribute as CFString,
newValue as CFTypeRef) == .success
}
}
Variable Expansion System
class VariableExpander {
static let builtInVariables: [(pattern: String, replacement: () -> String)] = [
("{{cursor}}", { "" }), // Handled specially for cursor positioning
("{{clipboard}}", { NSPasteboard.general.string(forType: .string) ?? "" }),
("{{date}}", { DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .none) }),
("{{time}}", { DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .short) }),
("{{datetime}}", { DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .short) }),
("{{iso_date}}", { ISO8601DateFormatter().string(from: Date()) }),
("{{username}}", { NSFullUserName() }),
("{{uuid}}", { UUID().uuidString }),
]
func expand(_ text: String) -> (text: String, cursorOffset: Int?) {
var result = text
var cursorOffset: Int? = nil
for (pattern, replacement) in Self.builtInVariables {
if pattern == "{{cursor}}" {
if let range = result.range(of: pattern) {
cursorOffset = result.distance(from: result.startIndex, to: range.lowerBound)
result.replaceSubrange(range, with: "")
}
} else {
result = result.replacingOccurrences(of: pattern, with: replacement())
}
}
return (result, cursorOffset)
}
}
User-Prompted Variables
// Format: {{?variableName:Enter your name:DefaultValue}}
func findUserVariables(in text: String) -> [(name: String, prompt: String, defaultValue: String)] {
let pattern = #"\{\{\?(\w+):([^:]+):?([^}]*)\}\}"#
let regex = try! NSRegularExpression(pattern: pattern)
return regex.matches(in: text, range: NSRange(text.startIndex..., in: text)).compactMap { match in
guard let nameRange = Range(match.range(at: 1), in: text),
let promptRange = Range(match.range(at: 2), in: text) else { return nil }
let defaultRange = Range(match.range(at: 3), in: text)
return (String(text[nameRange]), String(text[promptRange]), defaultRange.map { String(text[$0]) } ?? "")
}
}
Cursor Positioning After Insertion
func insertWithCursor(_ text: String, inserter: TextInserter) {
let (expanded, cursorOffset) = VariableExpander().expand(text)
inserter.insertViaPaste(expanded) {
if let offset = cursorOffset {
let moveCount = expanded.count - offset
for _ in 0..<moveCount {
inserter.simulateKeyPress(keyCode: 0x7B, modifiers: []) // Left arrow
}
}
}
}
References
More from makgunay/claude-swift-skills
macos-app-structure
macOS application architecture patterns covering App protocol (@main), Scene types (WindowGroup, Window, Settings, MenuBarExtra), multi-window management, NSApplicationDelegateAdaptor for AppKit lifecycle hooks, Info.plist configuration (LSUIElement for menu bar apps, NSAccessibilityUsageDescription), entitlements for sandbox/hardened runtime, and project structure conventions. Use when scaffolding a new macOS app, configuring scenes and windows, setting up menu bar apps, or resolving macOS-specific lifecycle issues. Corrects the common LLM mistake of generating iOS-only app structures.
31macos-permissions
macOS permission handling for Accessibility (AXIsProcessTrusted), Screen Recording, Full Disk Access, input monitoring, camera, microphone, location, and contacts. Covers TCC (Transparency Consent and Control) database, graceful degradation when permissions are denied, permission prompting patterns, opening System Settings to the correct pane, detecting permission changes, and the privacy manifest (PrivacyInfo.xcprivacy) requirement. Use when implementing features that require system permissions, building permission onboarding flows, or handling denied permissions gracefully.
17appkit-bridge
Bridging AppKit components into SwiftUI macOS apps. Covers NSViewRepresentable and NSViewControllerRepresentable protocols for hosting AppKit views in SwiftUI, NSHostingView/NSHostingController for hosting SwiftUI in AppKit, NSPanel for floating windows, NSWindow configuration (styleMask, level, collectionBehavior), responder chain integration, NSEvent monitoring (global and local), NSAnimationContext for AppKit animations, NSPopover, NSStatusItem for menu bar, and NSGlassEffectView for AppKit Liquid Glass. Use when SwiftUI lacks a native equivalent, building floating panels, custom window chrome, or integrating legacy AppKit components.
15swiftui-core
Core SwiftUI patterns for macOS and iOS development including navigation (NavigationSplitView, NavigationStack), state management (@State, @Binding, @Environment, @Bindable with @Observable), the new customizable toolbar system (toolbar IDs, ToolbarSpacer, DefaultToolbarItem, searchToolbarBehavior, matchedTransitionSource, sharedBackgroundVisibility), styled text editing (TextEditor with AttributedString, AttributedTextSelection, transformAttributes, textFormattingDefinition), and layout patterns. Use when building any SwiftUI view, implementing navigation, managing state, creating toolbars, or building rich text editors. Corrects common LLM errors like using deprecated NavigationView, wrong state wrappers, and outdated toolbar APIs.
14global-hotkeys
System-wide keyboard shortcut registration on macOS using NSEvent monitoring (simple, app-level) and Carbon EventHotKey API (reliable, system-wide). Covers NSEvent.addGlobalMonitorForEvents and addLocalMonitorForEvents, CGEvent tap for keystroke simulation, Carbon RegisterEventHotKey for system-wide hotkeys, modifier flag handling (.deviceIndependentFlagsMask), common key code mappings, debouncing, Accessibility permission requirements (AXIsProcessTrusted), and SwiftUI .onKeyPress for in-app shortcuts. Use when implementing global keyboard shortcuts, hotkey-triggered panels, or system-wide key event monitoring.
11swiftui-webkit
Native SwiftUI WebKit integration with the new WebView struct and WebPage observable class. Covers WebView creation from URLs, WebPage for navigation control and state management, JavaScript execution (callJavaScript with arguments and content worlds), custom URL scheme handlers, navigation management (load, reload, back/forward), navigation decisions, text search (findNavigator), content capture (snapshots, PDF generation, web archives), and configuration (data stores, user agents, JS permissions). Use when embedding web content in SwiftUI apps instead of the old WKWebView + UIViewRepresentable/NSViewRepresentable bridge pattern. This is a brand new API — do NOT use the old WKWebView wrapping approach.
10