native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Align Ghostty accessibility with upstream

onevcat aa57f081 67042c61

+146 -3
+119 -3
supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift
··· 11 11 let length: UInt64 12 12 } 13 13 14 + private final class CachedValue<T> { 15 + private var value: T? 16 + private let fetch: () -> T 17 + private let duration: Duration 18 + private var expiryTask: Task<Void, Never>? 19 + 20 + init(duration: Duration, fetch: @escaping () -> T) { 21 + self.duration = duration 22 + self.fetch = fetch 23 + } 24 + 25 + deinit { 26 + expiryTask?.cancel() 27 + } 28 + 29 + func get() -> T { 30 + if let value { 31 + return value 32 + } 33 + 34 + let fetched = fetch() 35 + value = fetched 36 + expiryTask?.cancel() 37 + expiryTask = Task { [weak self] in 38 + guard let self else { return } 39 + try? await ContinuousClock().sleep(for: self.duration) 40 + guard !Task.isCancelled else { return } 41 + self.value = nil 42 + self.expiryTask = nil 43 + } 44 + return fetched 45 + } 46 + } 47 + 14 48 private let runtime: GhosttyRuntime 15 49 let id = UUID() 16 50 let bridge: GhosttySurfaceBridge ··· 34 68 private var eventMonitor: Any? 35 69 private var notificationObservers: [NSObjectProtocol] = [] 36 70 private var prevPressureStage: Int = 0 71 + private lazy var cachedScreenContents = CachedValue<String>(duration: .milliseconds(500)) { 72 + [weak self] in 73 + self?.readScreenContents() ?? "" 74 + } 37 75 var passwordInput: Bool = false { 38 76 didSet { 39 77 let input = SecureInput.shared ··· 97 135 normalized.removeLast() 98 136 } 99 137 return normalized 138 + } 139 + 140 + static func accessibilityLine(for index: Int, in content: String) -> Int { 141 + let clampedIndex = min(max(index, 0), content.count) 142 + let prefix = String(content.prefix(clampedIndex)) 143 + return max(0, prefix.components(separatedBy: .newlines).count - 1) 144 + } 145 + 146 + static func accessibilityString(for range: NSRange, in content: String) -> String? { 147 + guard let swiftRange = Range(range, in: content) else { return nil } 148 + return String(content[swiftRange]) 100 149 } 101 150 102 151 override var acceptsFirstResponder: Bool { true } ··· 352 401 } 353 402 354 403 override func accessibilityRole() -> NSAccessibility.Role? { 355 - // De facto role used by terminal emulators; AppKit doesn't provide a named constant for it. 356 - NSAccessibility.Role(rawValue: "AXTerminal") 404 + // Match Ghostty.app so speech/input tools can treat the surface as editable text. 405 + .textArea 357 406 } 358 407 359 408 override func accessibilityLabel() -> String? { ··· 369 418 } 370 419 371 420 override func accessibilityValue() -> Any? { 372 - bridge.state.pwd 421 + cachedScreenContents.get() 373 422 } 374 423 375 424 override func accessibilityHelp() -> String? { 376 425 accessibilityPaneIndexHelp 377 426 } 378 427 428 + override func accessibilitySelectedTextRange() -> NSRange { 429 + selectedRange() 430 + } 431 + 432 + override func accessibilitySelectedText() -> String? { 433 + guard let surface else { return nil } 434 + var text = ghostty_text_s() 435 + guard ghostty_surface_read_selection(surface, &text) else { return nil } 436 + defer { ghostty_surface_free_text(surface, &text) } 437 + let value = String(cString: text.text) 438 + return value.isEmpty ? nil : value 439 + } 440 + 441 + override func accessibilityNumberOfCharacters() -> Int { 442 + cachedScreenContents.get().count 443 + } 444 + 445 + override func accessibilityVisibleCharacterRange() -> NSRange { 446 + let content = cachedScreenContents.get() 447 + return NSRange(location: 0, length: content.count) 448 + } 449 + 450 + override func accessibilityLine(for index: Int) -> Int { 451 + Self.accessibilityLine(for: index, in: cachedScreenContents.get()) 452 + } 453 + 454 + override func accessibilityString(for range: NSRange) -> String? { 455 + Self.accessibilityString(for: range, in: cachedScreenContents.get()) 456 + } 457 + 458 + override func accessibilityAttributedString(for range: NSRange) -> NSAttributedString? { 459 + guard let surface else { return nil } 460 + guard let plainString = accessibilityString(for: range) else { return nil } 461 + 462 + var attributes: [NSAttributedString.Key: Any] = [:] 463 + if let fontRaw = ghostty_surface_quicklook_font(surface) { 464 + let font = Unmanaged<CTFont>.fromOpaque(fontRaw) 465 + attributes[.font] = font.takeUnretainedValue() 466 + font.release() 467 + } 468 + 469 + return NSAttributedString(string: plainString, attributes: attributes) 470 + } 471 + 379 472 override func becomeFirstResponder() -> Bool { 380 473 let result = super.becomeFirstResponder() 381 474 if result { ··· 401 494 } else { 402 495 NSAccessibility.post(element: self, notification: .focusedUIElementChanged) 403 496 } 497 + } 498 + 499 + private func readScreenContents() -> String { 500 + guard let surface else { return "" } 501 + var text = ghostty_text_s() 502 + let selection = ghostty_selection_s( 503 + top_left: ghostty_point_s( 504 + tag: GHOSTTY_POINT_SCREEN, 505 + coord: GHOSTTY_POINT_COORD_TOP_LEFT, 506 + x: 0, 507 + y: 0 508 + ), 509 + bottom_right: ghostty_point_s( 510 + tag: GHOSTTY_POINT_SCREEN, 511 + coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT, 512 + x: 0, 513 + y: 0 514 + ), 515 + rectangle: false 516 + ) 517 + guard ghostty_surface_read_text(surface, selection, &text) else { return "" } 518 + defer { ghostty_surface_free_text(surface, &text) } 519 + return String(cString: text.text) 404 520 } 405 521 406 522 override func keyDown(with event: NSEvent) {
+27
supacodeTests/GhosttySurfaceViewTests.swift
··· 1 + import Foundation 1 2 import Testing 2 3 3 4 @testable import supacode ··· 17 18 18 19 @Test func normalizedWorkingDirectoryPathKeepsRootPath() { 19 20 #expect(GhosttySurfaceView.normalizedWorkingDirectoryPath("/") == "/") 21 + } 22 + 23 + @Test func accessibilityLineCountsLineBreaksUpToIndex() { 24 + let content = "alpha\nbeta\ngamma" 25 + 26 + #expect(GhosttySurfaceView.accessibilityLine(for: 0, in: content) == 0) 27 + #expect(GhosttySurfaceView.accessibilityLine(for: 5, in: content) == 0) 28 + #expect(GhosttySurfaceView.accessibilityLine(for: 6, in: content) == 1) 29 + #expect(GhosttySurfaceView.accessibilityLine(for: content.count, in: content) == 2) 30 + } 31 + 32 + @Test func accessibilityStringReturnsSubstringForValidRange() { 33 + let content = "alpha\nbeta" 34 + 35 + #expect( 36 + GhosttySurfaceView.accessibilityString( 37 + for: NSRange(location: 6, length: 4), 38 + in: content 39 + ) == "beta" 40 + ) 41 + #expect( 42 + GhosttySurfaceView.accessibilityString( 43 + for: NSRange(location: 99, length: 1), 44 + in: content 45 + ) == nil 46 + ) 20 47 } 21 48 }