Repository navigation
Accessibility: AXVisibleCharacterRange returns wrong data; AXRangeForPosition, AXBoundsForRange missed #9932
Description
Discussed in #9930
Originally posted by grishy December 16, 2025
Issue Description
Ghosttyâs accessibility implementation is missing attributes needed by assistive SW and third-party tools to query text at the cursor position, for example and more problematic, implement some API in wrong way.
I found this during debugging of my translation app when clicked on Ghostty app, which works across the system and has text access and fast tracking. Usually, it takes 10â20 ms in other apps, like Chrome or others, but here it took 3 seconds.
I used log show --last 1m to get a lot of text below in tests
This prevents screen readers and accessibility tools from efficiently working with Ghostty.
The main issue is that it returns the whole terminal blob in both cases: AXVisibleCharacterRange and AXValue .
Also, AXValue would be better if it did not return the full terminal context, if possible, only from the latest command run. For example, browsers return just a paragraph, not a whole page also for AXValue.
NOTE: For debug I suggest to use macOS buildin tool - https://developer.apple.com/documentation/accessibility/accessibility-inspector
Similar ticket
Comparison Behavior with Terminal.app
| Attribute | Terminal.app | Ghostty |
|---|---|---|
| AXVisibleCharacterRange | loc=3037623 len=12387 â
|
loc=0 len=709778 â (full buffer) |
| AXRangeForPosition | loc=3041844 len=1 â
|
N/A â |
| AXBoundsForRange | (686,409) 7x14 â
|
N/A â |
Terminal.app with 3M chars buffer correctly returns only visible viewport (~12K chars).
Ghostty with 700K chars buffer returns entire scrollback as "visible".
Expected Behavior
AXRangeForPositionshould return the character index at a given screen coordinateAXBoundsForRangeshould return screen bounds (CGRect) for a character rangeAXVisibleCharacterRangeshould return only the visible viewport range, not the entire scrollback buffer
Cursor at: (691, 417)
Querying accessibility...
=== Accessibility Diagnostic Report ===
Application: Terminal
Cursor: (691, 417)
Date: 2025-12-16T16:41:35Z
--- Attributes ---
[OK] AXRole
Value: AXTextArea
Time: 0.05ms
[OK] AXValue
Value: 3050007 chars
Time: 30.57ms
[OK] AXNumberOfCharacters
Value: 3050010
Time: 0.41ms
[OK] AXVisibleCharacterRange
Value: loc=3037623 len=12387
Time: 0.26ms
[OK] AXRangeForPosition
Value: loc=3041844 len=1
Time: 0.25ms
[OK] AXStringForRange
Value: " libinfo check path: <private>\n2025-12-16 19:36:32.724886+0300 0x749d11 Default 0x0 97327 0 Telegram: (Network) [com.apple.network:path] nw_path_libinfo_path_check [2A8AA2E8"...
Time: 0.17ms
[OK] AXBoundsForRange
Value: (686,409) 7x14
Time: 0.26ms
--- Summary ---
[OK] AXRangeForPosition works - fast lookup possible
[OK] AXVisibleCharacterRange works correctly
Actual Behavior
AXRangeForPosition: Not implemented (returns nothing)AXBoundsForRange: Not implemented (returns nothing)AXVisibleCharacterRange: Returns full buffer (loc=0 len=709778) instead of visible viewport
Cursor at: (619, 379)
Querying accessibility...
=== Accessibility Diagnostic Report ===
Application: Ghostty
Cursor: (619, 379)
Date: 2025-12-16T16:40:37Z
--- Attributes ---
[OK] AXRole
Value: AXTextArea
Time: 0.04ms
[OK] AXValue
Value: 709778 chars
Time: 4.33ms
[OK] AXNumberOfCharacters
Value: 709778
Time: 0.76ms
[OK] AXVisibleCharacterRange
Value: loc=0 len=709778
Time: 0.73ms
Note: BUG: Returns full buffer, not viewport
[FAIL] AXRangeForPosition
Value: N/A
Time: 0.05ms
[OK] AXStringForRange
Value: "2025-12-16 19:40:20.545677+0300 0x75bae1 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p: currentInfraTrafficType:9241 checking if re"...
Time: 0.02ms
[FAIL] AXBoundsForRange
Value: N/A
Time: 0.03ms
--- Summary ---
[FAIL] AXRangeForPosition not working - must use BoundsSearch fallback
[WARN] AXVisibleCharacterRange returns full buffer instead of viewport
Reproduction Steps
- Generate some text in Ghostty, any text.
- Take the script (attached). I asked you to generate based on my description, using the Opus 4.5 API, just for a proof of concept and debugging for this ticket. The original code was in Rust and complicated...
sleep 3 && ./check_accessibility.swift- Position your cursor over the terminal text within 3 seconds.
check_accessibility.swift
#!/usr/bin/env swift
//
// check_accessibility.swift
// Diagnostic tool for testing terminal accessibility attributes on macOS
//
// Usage:
// 1. chmod +x check_accessibility.swift
// 2. Run: sleep 3 && ./check_accessibility.swift
// 3. Position your cursor over the terminal text within 3 seconds
//
import AppKit
import ApplicationServices
import Foundation
// MARK: - Accessibility Helpers
struct DiagnosticResult {
let attribute: String
let supported: Bool
let value: String
let timing: Double
let notes: String
}
class AccessibilityDiagnostic {
private let element: AXUIElement
private var results: [DiagnosticResult] = []
init(element: AXUIElement) {
self.element = element
}
func getStringAttribute(_ attribute: String) -> (String?, Double) {
let start = CFAbsoluteTimeGetCurrent()
var value: CFTypeRef?
let error = AXUIElementCopyAttributeValue(element, attribute as CFString, &value)
let elapsed = (CFAbsoluteTimeGetCurrent() - start) * 1000
if error == .success, let cfString = value as? String {
return (cfString, elapsed)
}
return (nil, elapsed)
}
func getNumberAttribute(_ attribute: String) -> (Int?, Double) {
let start = CFAbsoluteTimeGetCurrent()
var value: CFTypeRef?
let error = AXUIElementCopyAttributeValue(element, attribute as CFString, &value)
let elapsed = (CFAbsoluteTimeGetCurrent() - start) * 1000
if error == .success, let number = value as? NSNumber {
return (number.intValue, elapsed)
}
return (nil, elapsed)
}
func getRangeAttribute(_ attribute: String) -> (CFRange?, Double) {
let start = CFAbsoluteTimeGetCurrent()
var value: CFTypeRef?
let error = AXUIElementCopyAttributeValue(element, attribute as CFString, &value)
let elapsed = (CFAbsoluteTimeGetCurrent() - start) * 1000
if error == .success, let axValue = value {
var range = CFRange(location: 0, length: 0)
if AXValueGetValue(axValue as! AXValue, .cfRange, &range) {
return (range, elapsed)
}
}
return (nil, elapsed)
}
func getRangeForPosition(_ point: CGPoint) -> (CFRange?, Double) {
let start = CFAbsoluteTimeGetCurrent()
var cgPoint = point
guard let pointValue = AXValueCreate(.cgPoint, &cgPoint) else {
return (nil, 0)
}
var result: CFTypeRef?
let error = AXUIElementCopyParameterizedAttributeValue(
element,
kAXRangeForPositionParameterizedAttribute as CFString,
pointValue,
&result
)
let elapsed = (CFAbsoluteTimeGetCurrent() - start) * 1000
if error == .success, let axValue = result {
var range = CFRange(location: 0, length: 0)
if AXValueGetValue(axValue as! AXValue, .cfRange, &range) {
return (range, elapsed)
}
}
return (nil, elapsed)
}
func getStringForRange(_ range: CFRange) -> (String?, Double) {
let start = CFAbsoluteTimeGetCurrent()
var cfRange = range
guard let rangeValue = AXValueCreate(.cfRange, &cfRange) else {
return (nil, 0)
}
var result: CFTypeRef?
let error = AXUIElementCopyParameterizedAttributeValue(
element,
kAXStringForRangeParameterizedAttribute as CFString,
rangeValue,
&result
)
let elapsed = (CFAbsoluteTimeGetCurrent() - start) * 1000
if error == .success, let str = result as? String {
return (str, elapsed)
}
return (nil, elapsed)
}
func getBoundsForRange(_ range: CFRange) -> (CGRect?, Double) {
let start = CFAbsoluteTimeGetCurrent()
var cfRange = range
guard let rangeValue = AXValueCreate(.cfRange, &cfRange) else {
return (nil, 0)
}
var result: CFTypeRef?
let error = AXUIElementCopyParameterizedAttributeValue(
element,
kAXBoundsForRangeParameterizedAttribute as CFString,
rangeValue,
&result
)
let elapsed = (CFAbsoluteTimeGetCurrent() - start) * 1000
if error == .success, let axValue = result {
var rect = CGRect.zero
if AXValueGetValue(axValue as! AXValue, .cgRect, &rect) {
return (rect, elapsed)
}
}
return (nil, elapsed)
}
func runDiagnostics(cursorPosition: CGPoint) -> [DiagnosticResult] {
results = []
// 1. AXRole
let (role, roleTime) = getStringAttribute(kAXRoleAttribute as String)
results.append(DiagnosticResult(
attribute: "AXRole",
supported: role != nil,
value: role ?? "N/A",
timing: roleTime,
notes: "Element type"
))
// 2. AXValue (full text)
let (value, valueTime) = getStringAttribute(kAXValueAttribute as String)
let valueLen = value?.count ?? 0
results.append(DiagnosticResult(
attribute: "AXValue",
supported: value != nil,
value: "\(valueLen) chars",
timing: valueTime,
notes: "Full text content"
))
// 3. AXNumberOfCharacters
let (charCount, charCountTime) = getNumberAttribute(kAXNumberOfCharactersAttribute as String)
results.append(DiagnosticResult(
attribute: "AXNumberOfCharacters",
supported: charCount != nil,
value: charCount.map { String($0) } ?? "N/A",
timing: charCountTime,
notes: "Total character count"
))
// 4. AXVisibleCharacterRange
let (visibleRange, visibleRangeTime) = getRangeAttribute(kAXVisibleCharacterRangeAttribute as String)
var visibleRangeNotes = "Visible text range"
if let vr = visibleRange, let total = charCount {
if vr.length == total {
visibleRangeNotes = "BUG: Returns full buffer, not viewport"
}
}
results.append(DiagnosticResult(
attribute: "AXVisibleCharacterRange",
supported: visibleRange != nil,
value: visibleRange.map { "loc=\($0.location) len=\($0.length)" } ?? "N/A",
timing: visibleRangeTime,
notes: visibleRangeNotes
))
// 5. AXRangeForPosition (CRITICAL)
let (rangeForPos, rangeForPosTime) = getRangeForPosition(cursorPosition)
var rangeNotes = "Char at cursor position"
var rangeSupported = rangeForPos != nil
if let r = rangeForPos {
if r.location == 0 && r.length == 0 {
rangeNotes = "BUG: Returns (0,0)"
rangeSupported = false
}
}
results.append(DiagnosticResult(
attribute: "AXRangeForPosition",
supported: rangeSupported,
value: rangeForPos.map { "loc=\($0.location) len=\($0.length)" } ?? "N/A",
timing: rangeForPosTime,
notes: rangeNotes
))
// 6. AXStringForRange
if let vr = visibleRange {
let testRange = CFRange(location: vr.location, length: min(500, vr.length))
let (str, strTime) = getStringForRange(testRange)
let preview = str.map { s -> String in
let trimmed = String(s.prefix(200)).replacingOccurrences(of: "\n", with: "\\n")
return "\"\(trimmed)\"" + (s.count > 200 ? "..." : "")
}
results.append(DiagnosticResult(
attribute: "AXStringForRange",
supported: str != nil,
value: preview ?? "N/A",
timing: strTime,
notes: "Extract text for range (first 500 chars)"
))
}
// 7. AXBoundsForRange
let testBoundsRange: CFRange
if let r = rangeForPos, r.length > 0 {
testBoundsRange = r
} else if let vr = visibleRange {
testBoundsRange = CFRange(location: vr.location, length: 1)
} else {
testBoundsRange = CFRange(location: 0, length: 1)
}
let (bounds, boundsTime) = getBoundsForRange(testBoundsRange)
results.append(DiagnosticResult(
attribute: "AXBoundsForRange",
supported: bounds != nil,
value: bounds.map { "(\(Int($0.origin.x)),\(Int($0.origin.y))) \(Int($0.width))x\(Int($0.height))" } ?? "N/A",
timing: boundsTime,
notes: "Screen bounds for range"
))
return results
}
func printResults(appName: String, cursorPosition: CGPoint) {
print("")
print("=== Accessibility Diagnostic Report ===")
print("")
print("Application: \(appName)")
print("Cursor: (\(Int(cursorPosition.x)), \(Int(cursorPosition.y)))")
print("Date: \(ISO8601DateFormatter().string(from: Date()))")
print("")
print("--- Attributes ---")
print("")
for result in results {
let status = result.supported ? "[OK]" : "[FAIL]"
let timing = String(format: "%.2fms", result.timing)
print("\(status) \(result.attribute)")
print(" Value: \(result.value)")
print(" Time: \(timing)")
if result.notes.contains("BUG") {
print(" Note: \(result.notes)")
}
print("")
}
print("--- Summary ---")
print("")
let rangeForPosResult = results.first { $0.attribute == "AXRangeForPosition" }
let visibleRangeResult = results.first { $0.attribute == "AXVisibleCharacterRange" }
if let rfp = rangeForPosResult {
if rfp.supported {
print("[OK] AXRangeForPosition works - fast lookup possible")
} else {
print("[FAIL] AXRangeForPosition not working - must use BoundsSearch fallback")
}
}
if let vr = visibleRangeResult {
if vr.notes.contains("BUG") {
print("[WARN] AXVisibleCharacterRange returns full buffer instead of viewport")
} else if vr.supported {
print("[OK] AXVisibleCharacterRange works correctly")
} else {
print("[FAIL] AXVisibleCharacterRange not supported")
}
}
print("")
}
}
// MARK: - Main
func main() {
// Check accessibility permission
guard AXIsProcessTrusted() else {
print("[ERROR] Accessibility permission not granted!")
print("Grant permission in: System Settings > Privacy & Security > Accessibility")
exit(1)
}
// Get cursor position
let mouseLocation = NSEvent.mouseLocation
guard let screen = NSScreen.main else {
print("[ERROR] Cannot get screen info")
exit(1)
}
let screenHeight = screen.frame.height
let cursorPosition = CGPoint(x: mouseLocation.x, y: screenHeight - mouseLocation.y)
print("Cursor at: (\(Int(cursorPosition.x)), \(Int(cursorPosition.y)))")
print("Querying accessibility...")
// Get element at cursor position
let systemWide = AXUIElementCreateSystemWide()
var element: AXUIElement?
let error = AXUIElementCopyElementAtPosition(
systemWide,
Float(cursorPosition.x),
Float(cursorPosition.y),
&element
)
guard error == .success, let el = element else {
print("[ERROR] No accessible element at cursor (error: \(error.rawValue))")
exit(1)
}
// Get app name
var pid: pid_t = 0
AXUIElementGetPid(el, &pid)
let appName = NSRunningApplication(processIdentifier: pid)?.localizedName ?? "Unknown"
// Run diagnostics
let diagnostic = AccessibilityDiagnostic(element: el)
_ = diagnostic.runDiagnostics(cursorPosition: cursorPosition)
diagnostic.printResults(appName: appName, cursorPosition: cursorPosition)
}
main()Ghostty Logs
No response
Ghostty Version
Ghostty 1.2.3
Version
- version: 1.2.3
- channel: stable
Build Config
- Zig version : 0.14.1
- build mode : builtin.OptimizeMode.ReleaseFast
- app runtime : apprt.Runtime.none
- font engine : font.main.Backend.coretext
- renderer : renderer.generic.Renderer(renderer.Metal)
- libxev : kqueue
OS Version Information
macOS 26.1 (25B78)
(Linux only) Display Server
None
(Linux only) Desktop Environment/Window Manager
No response
Minimal Ghostty Configuration
N/AAdditional Relevant Configuration
N/A
I acknowledge that:
- I have reviewed the FAQ and confirm that my issue is NOT among them.
- I have searched the Ghostty repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
- I have checked the "Preview" tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (
```) on separate lines.
Activity
burakssen commentedon Jan 23, 2026
I think I solved the issue while experimenting.
Cursor at: (843, 540) Querying accessibility... === Accessibility Diagnostic Report === Application: Ghostty Cursor: (843, 540) Date: 2026-01-23T20:35:39Z --- Attributes --- [OK] AXRole Value: AXTextArea Time: 0.04ms [OK] AXValue Value: 90088 chars Time: 1.80ms [OK] AXNumberOfCharacters Value: 90088 Time: 0.16ms [OK] AXVisibleCharacterRange Value: loc=89184 len=904 Time: 0.21ms [OK] AXRangeForPosition Value: loc=91289 len=1 Time: 0.04ms [OK] AXStringForRange Value: "Hello World\nHello World\nHello World\nHello World\nHello World\nHello World\nHello World\nHello World\nHello World\nHello World\nHello World\nHe^C\nââburakssen@Burak-MacBook-Pro ~/dev/personal/ghostty âđmainââš \nâ°"... Time: 0.03ms [OK] AXBoundsForRange Value: (840,530) 8x17 Time: 0.03ms --- Summary --- [OK] AXRangeForPosition works - fast lookup possible [OK] AXVisibleCharacterRange works correctlyIs this correct? I feel like it is. I have to make the code more concise after that I'll open a pr about this.
burakssen commentedon Jan 23, 2026
The main idea is this:
MrMage commentedon Feb 23, 2026
I am the developer of Cotypist, an AI autocomplete app for Mac. I've received some requests for autocomplete support in Ghostty (especially in conjunction with TUI coding agents like Claude Code and Codex). In order to show completions, however, Cotypist needs to know the current cursor position.
If you are open to it, I would be happy to try my hand at implementing this with Claude Code; I'll of course read, understand, review and verify the code myself before submitting a PR. However, I am not very familiar with the Ghostty codebase, so it is still possible that I might "put things in the wrong place" due to this, at least initially, even if the code itself is working.
Would such functionality be desired enough for you to look at a PR, and possibly point me in the right direction if I am doing things in a suboptimal way?
mitchellh commentedon Feb 23, 2026
!vouch @MrMage
Update VOUCHED list
Update VOUCHED list (#10968)
mitchellh commentedon Feb 23, 2026
Sure! You sound thoughtful so I vouched for you. Of course, if you run into any roadblocks or decide to not pursue this, that's totally fine; a vouch is not an obligation. Let me know if I can point you in the right direction.
MrMage commentedon Feb 26, 2026
@mitchellh Thanks! I've opened #10992 as a draft; would love to get some initial feedback on it (see the PR description for details).
Sync with upstream ghostty-org/ghostty (7 cmux patches on latest)
MauScheff commentedon Mar 10, 2026
As a cotypist user, I feel heard, thanks @MrMage ! ð
6 remaining items
MrMage commentedon Apr 16, 2026
That is a nice gesture! I would be in favor of a donation to a charity with a focus on accessibility instead of a gift card, though.
iGusev commentedon May 17, 2026
Adding another data point: hits us daily through TUI AI coding agents (Claude Code, Codex) - overlays like Cotypist would be a real productivity unlock on top of those, but they're blocked by the current
AXVisibleCharacterRangebehaviour and the missingAXRangeForPosition/AXBoundsForRange.Falling back to Terminal.app for those flows is itself the cleanest signal that this is worth landing. +1 on prioritising #10992 (#11196)
wr commentedon May 28, 2026
As someone working on a (non-AI) tool that relies on accessibility APIs (cursor position specifically) I would be grateful for a fix to this issue.
MauScheff commentedon May 28, 2026
elkinjosetm commentedon May 28, 2026
MauScheff commentedon May 28, 2026
elkinjosetm commentedon May 28, 2026
00-kat commentedon May 28, 2026
This isn't a Cotypist alternatives discussion forum, please stay on-topic. Even if it isn't relevant to you, it's still relevant to many other people, so voicing that you don't need a feature (or discussing whether you do) isn't particularly useful.
core: send selection_changed notification (#12902)
csullivan84 commentedon Jul 17, 2026
Iâm a blind VoiceOver user and tested this against the pinned upstream baseline. The difference is substantial: baseline Ghostty was not practical for terminal input or review, while the completed fork now passes 14 automated AX checks and all 5 hands-on VoiceOver checks.
The work covers viewport-correct
AXVisibleCharacterRange, UTF-16 indexing,AXRangeForPosition,AXBoundsForRange, logical line navigation, live-output notifications, real caret exposure, and concise Left/Right/Backspace/Delete feedback.One useful finding:
AXSelectedTextChangedcorrectly exposed the moving caret to machine probes, but VoiceOver still did not speak caret movement for Ghosttyâs custom view. Targeted low-priority character announcements were needed. Suppressing redundant value notifications during local echo also eliminated the repeated âselection replacedâ announcements.VO-L and VO-Up/Down now navigate lines, flood output can be interrupted without losing orientation, and VO-Home/End provide recoverable scrollback navigation.
Working fork and test harness:
https://github.com/csullivan84/ghostty-a11y
Sharing this as implementation and user-testing evidenceânot as a request to expedite review.
vineetk1998 commentedon Jul 31, 2026
Still reproduces on Ghostty 1.3.1 stable, and the failure mode is a little sharper than the "N/A" in the table above.
AXBoundsForRangeis not in the focused element's parameterized attribute list, and asking for it anyway returns -25213 (kAXErrorParameterizedAttributeUnsupported) rather than a wrong or empty rect:Terminal.app on the same machine answers
err=0with a real rect.One more use case beyond screen readers and autocomplete. I am building a clipboard manager with a shortcut that jumps the mouse pointer to the text caret, so you can get the pointer back to where you are working without reaching for the trackpad. It is a small motor-accessibility win and it works in every native text app, but it cannot work in Ghostty.
What stands out is how close Ghostty already is.
AXSelectedTextRange,AXValue,AXNumberOfCharactersandAXLineForIndexare all there, so Ghostty knows exactly which character the caret is on. There is just no way to ask where that character is on screen.AXBoundsForRangeis the one missing piece.There look to be three open PRs touching this area (#10992, #11196, #12881) and none merged yet. Is there one you would like contributors to converge on? I am happy to test whichever against a caret-following tool and report back with numbers.
Update VOUCHED list (#10968)
Luodian commentedon Aug 22, 2026
I use Typeless, a macOS voice input and writing tool, with Ghostty. Basic dictation can paste into the shell, but the correction-aware features do not work reliably: after Typeless inserts a word and I correct it manually, Typeless cannot learn that correction, and selected-text rewriting also fails.
The reason seems to be that these features need a standard macOS Accessibility text surface, not only a paste path. Typeless needs to read the focused text or selection and observe changes through
AXValue,AXSelectedText,AXSelectedTextRange, and related notifications. Terminal.app exposes enough of this for the same workflow; Ghostty currently exposes anAXTextArea, but selection and caret/range queries are incomplete.Could you clarify:
I would be happy to test a build against Typeless and report the exact AX calls that still fail.