Skip to content

Instantly share code, notes, and snippets.

@xidiot
Created August 18, 2026 10:21
Show Gist options
  • Select an option

  • Save xidiot/438a8f385910318ccdc07d5bd713a276 to your computer and use it in GitHub Desktop.

Select an option

Save xidiot/438a8f385910318ccdc07d5bd713a276 to your computer and use it in GitHub Desktop.
macos_whitespace_move
#!/usr/bin/env swift
import Foundation
import AppKit
import ApplicationServices
func fail(_ message: String) -> Never {
fputs("ERROR: \(message)\n", stderr)
exit(1)
}
func axError(_ error: AXError) -> String {
switch error {
case .success: return "success"
case .failure: return "failure"
case .illegalArgument: return "illegalArgument"
case .invalidUIElement: return "invalidUIElement"
case .invalidUIElementObserver: return "invalidUIElementObserver"
case .cannotComplete: return "cannotComplete"
case .attributeUnsupported: return "attributeUnsupported"
case .actionUnsupported: return "actionUnsupported"
case .notificationUnsupported: return "notificationUnsupported"
case .notImplemented: return "notImplemented"
case .notificationAlreadyRegistered: return "notificationAlreadyRegistered"
case .notificationNotRegistered: return "notificationNotRegistered"
case .apiDisabled: return "apiDisabled"
case .noValue: return "noValue"
case .parameterizedAttributeUnsupported: return "parameterizedAttributeUnsupported"
case .notEnoughPrecision: return "notEnoughPrecision"
@unknown default: return "unknown(\(error.rawValue))"
}
}
guard CommandLine.arguments.count == 2 else {
fail("usage: whitespace-move previous|next")
}
let direction = CommandLine.arguments[1]
guard direction == "previous" || direction == "next" else {
fail("direction must be previous or next")
}
// Check Accessibility permission.
let trusted = AXIsProcessTrusted()
print("Accessibility trusted: \(trusted)")
guard trusted else {
fail("""
This process is NOT trusted for Accessibility.
Give the application launching this binary Accessibility permission
in System Settings → Privacy & Security → Accessibility.
""")
}
// Get the focused element.
let workspace = NSWorkspace.shared
guard let app = workspace.frontmostApplication else {
fail("No frontmost application")
}
print("Frontmost app: \(app.localizedName ?? "?")")
print("PID: \(app.processIdentifier)")
let appElement = AXUIElementCreateApplication(app.processIdentifier)
var focusedRef: CFTypeRef?
let focusedResult = AXUIElementCopyAttributeValue(
appElement,
kAXFocusedUIElementAttribute as CFString,
&focusedRef
)
print("Focused element result: \(axError(focusedResult))")
guard focusedResult == .success,
let focusedRef else {
fail("Could not get focused UI element")
}
let element = focusedRef as! AXUIElement
// Print role.
var roleRef: CFTypeRef?
let roleResult = AXUIElementCopyAttributeValue(
element,
kAXRoleAttribute as CFString,
&roleRef
)
print("Role result: \(axError(roleResult))")
print("Role: \(roleRef ?? "nil" as CFTypeRef)")
// Get text.
var valueRef: CFTypeRef?
let valueResult = AXUIElementCopyAttributeValue(
element,
kAXValueAttribute as CFString,
&valueRef
)
print("Value result: \(axError(valueResult))")
guard valueResult == .success,
let text = valueRef as? String else {
fail("Focused element does not expose text through kAXValueAttribute")
}
print("Text length: \(text.utf16.count)")
print("Text: \(text)")
// Get caret/selection.
var rangeRef: CFTypeRef?
let rangeResult = AXUIElementCopyAttributeValue(
element,
kAXSelectedTextRangeAttribute as CFString,
&rangeRef
)
print("Selected range result: \(axError(rangeResult))")
guard rangeResult == .success,
let rangeRef else {
fail("Focused element does not expose kAXSelectedTextRangeAttribute")
}
let axRange = rangeRef as! AXValue
var selectedRange = CFRange()
guard AXValueGetValue(
axRange,
.cfRange,
&selectedRange
) else {
fail("Could not decode selected text range")
}
print(
"Selection: location=\(selectedRange.location), length=\(selectedRange.length)"
)
let caret: Int
if selectedRange.length == 0 {
caret = selectedRange.location
} else if direction == "previous" {
caret = selectedRange.location
} else {
caret = selectedRange.location + selectedRange.length
}
let nsText = text as NSString
let length = nsText.length
func isWhitespace(_ index: Int) -> Bool {
guard index >= 0 && index < length else {
return false
}
let scalar = UnicodeScalar(nsText.character(at: index))!
return CharacterSet.whitespacesAndNewlines.contains(scalar)
}
var target: Int?
if direction == "previous" {
var i = min(caret - 1, length - 1)
// Skip whitespace immediately behind us.
while i >= 0 && isWhitespace(i) {
i -= 1
}
// Walk backwards to the start of this token.
while i >= 0 && !isWhitespace(i) {
i -= 1
}
target = i + 1
} else {
var i = max(caret, 0)
// Skip whitespace immediately ahead.
while i < length && isWhitespace(i) {
i += 1
}
// Walk forwards to the end of this token.
while i < length && !isWhitespace(i) {
i += 1
}
target = i
}
guard let target else {
print("No whitespace boundary found")
exit(0)
}
print("Moving from \(caret) → \(target)")
var newRange = CFRange(
location: target,
length: 0
)
guard let newAXRange = AXValueCreate(
.cfRange,
&newRange
) else {
fail("Could not create AX range")
}
let setResult = AXUIElementSetAttributeValue(
element,
kAXSelectedTextRangeAttribute as CFString,
newAXRange
)
print("Set range result: \(axError(setResult))")
guard setResult == .success else {
fail("Could not move cursor")
}
print("SUCCESS")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment