Skip to content

Accessibility: AXVisibleCharacterRange returns wrong data; AXRangeForPosition, AXBoundsForRange missed #9932

Description

@mitchellh

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

#2351
#4053
#3852

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

  1. AXRangeForPosition should return the character index at a given screen coordinate
  2. AXBoundsForRange should return screen bounds (CGRect) for a character range
  3. AXVisibleCharacterRange should 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

  1. AXRangeForPosition: Not implemented (returns nothing)
  2. AXBoundsForRange: Not implemented (returns nothing)
  3. 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

  1. Generate some text in Ghostty, any text.
  2. 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...
  3. sleep 3 && ./check_accessibility.swift
  4. 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/A

Additional 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

  1. added this to the 1.3.0 milestone on Dec 16, 2025
  2. added theissue type on Dec 16, 2025
  3. burakssen commented on Jan 23, 2026

    @burakssen

    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 correctly

    Is this correct? I feel like it is. I have to make the code more concise after that I'll open a pr about this.

  4. burakssen commented on Jan 23, 2026

    @burakssen

    The main idea is this:

    // Added this to SurfaceView so that we can trac the start of the visible content start
    private var cachedVisibleContentsStart: Int = 0
    
    ...
    
    init(...) {
       cachedVisibleContents = .init(duration: .milliseconds(500)) { [weak self] in
          ...
    
          // On here instead of returning visibleContent directly we calculate the visible content range start, with .backwards (going from bottom of the terminal to the last visible content on top)
          let visibleContent = String(cString: text.text)
    
          // Calculate the offset by finding where visible content starts in the full buffer.
          // We search from the end since the viewport is usually near the bottom.
          let fullContent = self.cachedScreenContents.get()
          if let range = fullContent.range(of: visibleContent, options: .backwards) {
              self.cachedVisibleContentsStart = fullContent.distance(from: fullContent.startIndex, to: range.lowerBound)
          } else {
              self.cachedVisibleContentsStart = 0
          }
    
          return visibleContent
       }
    }
    
    // And on here we basically use cachedVisibleContents to get the content length and use the cachedVisibleContentsStart as our starting point instead of 0 all the time.
    override func accessibilityVisibleCharacterRange() -> NSRange {
        let content = cachedVisibleContents.get()
        return NSRange(location: self.cachedVisibleContentsStart, length: content.count)
    }
  5. MrMage commented on Feb 23, 2026

    @MrMage

    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?

  6. mitchellh commented on Feb 23, 2026

    @mitchellh
    ContributorAuthor

    !vouch @MrMage

  7. mitchellh commented on Feb 23, 2026

    @mitchellh
    ContributorAuthor

    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?

    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.

  8. MrMage commented on Feb 26, 2026

    @MrMage

    @mitchellh Thanks! I've opened #10992 as a draft; would love to get some initial feedback on it (see the PR description for details).

  9. modified the milestones: 1.3.0, 1.4.0 on Mar 4, 2026
  10. MauScheff commented on Mar 10, 2026

    @MauScheff

    As a cotypist user, I feel heard, thanks @MrMage ! 🎉

  11. 6 remaining items

  12. MrMage commented on Apr 16, 2026

    @MrMage

    Forgive me if this isn't allowed - and I've never done something like this before - but Global Accessibility Awareness Day is on May 21 this year - and if we're able to get this (and by this ideally I mean real accessibility) appropriately merged by then I'd be willing to offer like a $50 gift card (or donation on behalf of or something, I don't know, like I said I've never done this before) to the maintainer and another for the author of the completed/merged PR.

    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.

  13. iGusev commented on May 17, 2026

    @iGusev

    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 AXVisibleCharacterRange behaviour and the missing AXRangeForPosition / AXBoundsForRange.

    Falling back to Terminal.app for those flows is itself the cleanest signal that this is worth landing. +1 on prioritising #10992 (#11196)

  14. wr commented on May 28, 2026

    @wr

    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.

  15. MauScheff commented on May 28, 2026

    @MauScheff
  16. elkinjosetm commented on May 28, 2026

    @elkinjosetm
  17. MauScheff commented on May 28, 2026

    @MauScheff
  18. elkinjosetm commented on May 28, 2026

    @elkinjosetm
  19. 00-kat commented on May 28, 2026

    @00-kat
    Contributor

    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.

  20. csullivan84 commented on Jul 17, 2026

    @csullivan84

    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: AXSelectedTextChanged correctly 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.

  21. vineetk1998 commented on Jul 31, 2026

    @vineetk1998

    Still reproduces on Ghostty 1.3.1 stable, and the failure mode is a little sharper than the "N/A" in the table above. AXBoundsForRange is not in the focused element's parameterized attribute list, and asking for it anyway returns -25213 (kAXErrorParameterizedAttributeUnsupported) rather than a wrong or empty rect:

    AXRole: AXTextArea
    parameterized: [AXStringForRange, AXAttributedStringForRange,
                    AXLineForIndex, AXReplaceRangeWithText]
    AXBoundsForRange(loc=0, len=0) -> err=-25213
    AXBoundsForRange(loc=0, len=1) -> err=-25213
    

    Terminal.app on the same machine answers err=0 with 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, AXNumberOfCharacters and AXLineForIndex are 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. AXBoundsForRange is 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.

  22. added a commit that references this issue on Aug 12, 2026
  23. Luodian commented on Aug 22, 2026

    @Luodian

    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 an AXTextArea, but selection and caret/range queries are incomplete.

    Could you clarify:

    1. Is the intended direction to implement the standard macOS AX text contract for terminal surfaces, including selected text, selected ranges, and change notifications?
    2. Or should third-party tools such as Typeless use another Ghostty-specific API or integration path?
    3. Is there a current branch or build that would be useful for testing?

    I would be happy to test a build against Typeless and report the exact AX calls that still fail.

  24. modified the milestones: 1.4.0, 1.5.0 on Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions