Skip to content

fix: replace Ghostty AppleScript with Core Graphics + Accessibility API#286

Merged
Wirasm merged 4 commits into
mainfrom
kild/issue-269-ghostty-cg-api
Feb 9, 2026
Merged

fix: replace Ghostty AppleScript with Core Graphics + Accessibility API#286
Wirasm merged 4 commits into
mainfrom
kild/issue-269-ghostty-cg-api

Conversation

@Wirasm

@Wirasm Wirasm commented Feb 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Ghostty uses GPU rendering which bypasses the native widget tree that macOS System Events relies on, causing focus, hide, and is_window_open to always fail (0 windows reported)
  • Replaced all 6 AppleScript-based functions with Core Graphics API (via xcap) for window enumeration and macOS Accessibility API for window manipulation
  • Added terminal/native/ module as a clean abstraction layer with fallback strategies when AX API is unavailable

Changes

File Change
crates/kild-core/Cargo.toml Added xcap, accessibility-sys, core-foundation as macOS-only deps
crates/kild-core/src/terminal/native/types.rs New NativeWindowInfo struct
crates/kild-core/src/terminal/native/macos.rs CG window enumeration + AX focus/minimize with AppleScript fallbacks
crates/kild-core/src/terminal/native/mod.rs Platform-gated module declaration
crates/kild-core/src/terminal/mod.rs Register native module
crates/kild-core/src/terminal/errors.rs Added NativeWindowError variant
crates/kild-core/src/terminal/backends/ghostty.rs Replaced 6 AppleScript functions + with_ghostty_window with find_ghostty_native_window + native API calls

Test plan

  • cargo fmt --check passes
  • cargo clippy --all -- -D warnings passes
  • cargo test --all passes (all 120+ tests)
  • cargo build --all succeeds
  • Manual: kild focus <branch> brings Ghostty window to foreground
  • Manual: kild hide <branch> minimizes Ghostty window
  • Manual: kild list shows correct status detection
  • Manual: Stop/open cycle preserves focus/hide functionality

Fixes #269

@Wirasm

Wirasm commented Feb 9, 2026

Copy link
Copy Markdown
Owner Author

Self Code Review

Summary

Solid fix that correctly replaces the broken System Events approach with Core Graphics API for window enumeration and Accessibility API for window manipulation. The implementation follows proven patterns from kild-peek-core and includes proper fallback strategies.

Findings

Strengths

  • Clean module separation (terminal/native/) with platform-gating
  • Proper Core Foundation memory management with SAFETY comments
  • Fallback chain: AX API -> AppleScript activation for focus, AX API -> System Events hide for minimize
  • Preserves existing PID-based session lookup for title change scenarios
  • Removed ~500 lines of AppleScript code that fundamentally couldn't work with Ghostty

Issues Found & Fixed

  • macos.rs - PID cast from u32 to i32 used as i32 which silently wraps on overflow. Fixed with i32::try_from(p).ok() for safe bounds checking
  • macos.rs - Empty title_contains string would match all windows via str::contains(""). Added guard to return None for empty titles

Checklist

  • Fix addresses root cause (CG API sees Ghostty windows, System Events doesn't)
  • Code follows codebase patterns (event naming, error types, module structure)
  • Tests cover the change (struct construction, not-found case, existing tests pass)
  • No obvious bugs introduced
  • Memory management follows Core Foundation ownership rules
  • Review findings addressed in follow-up commit

@Wirasm

Wirasm commented Feb 9, 2026

Copy link
Copy Markdown
Owner Author

PR Review Summary

Reviewed by 6 specialized agents: code-reviewer, silent-failure-hunter, type-design-analyzer, pr-test-analyzer, comment-analyzer, docs-impact-agent.


Critical Issues (5 found)

Agent Issue Location
silent-failure-hunter PID conversion failures silently swallowedi32::try_from(p).ok() hides overflow errors. If PID > i32::MAX, window found but focus/minimize fails with cryptic "no PID available" instead of explaining the conversion failure. native/macos.rs:67, 154
silent-failure-hunter Window enumeration errors hidden in loopsErr(_) => continue skips windows with API errors silently. User sees "window not found" with no indication that windows were skipped due to CG API failures. native/macos.rs:47-64, 117-138
silent-failure-hunter Empty title guard is silent — Returns Ok(None) without logging. Caller reports "window not found" with no indication the search was invalid from the start. native/macos.rs:35-37
pr-test-analyzer AX API fallback paths have zero test coverage — The 511-line native/macos.rs module (CG enumeration → AX manipulation → AppleScript fallback) has no tests. Fallback behavior is the primary way this feature works for Ghostty users. native/macos.rs (entire module)
comment-analyzer Stale function reference — Comment references focus_by_pid which was removed in this PR. Code now uses find_window_by_pid. ghostty.rs:61

Important Issues (5 found)

Agent Issue Location
silent-failure-hunter AX fallback degrades silently — AX API raises specific window, AppleScript fallback activates entire app (all windows). User gets less precise behavior without knowing. Same for minimize: fallback hides ALL windows, not just target. native/macos.rs:189-207, 235-252
silent-failure-hunter AppleScript errors lack actionable context — "Failed to activate Ghostty: [stderr]" doesn't guide user. Should hint at common causes (app not running, permissions, scripting disabled). native/macos.rs:464-474, 501-509
silent-failure-hunter AX window not found error is generic — "No AX window found matching title 'X'" doesn't say how many windows were checked or suggest title may have changed. native/macos.rs:360
type-design-analyzer NativeWindowInfo lacks encapsulation (4.75/10) — All fields public with no validation. Allows construction with id: 0, empty strings, negative PIDs. Recent empty-title bugfix (6251993) could have been prevented by constructor validation. native/types.rs:4-15
pr-test-analyzer Empty title guard and PID overflow have no regression tests — These were follow-up fixes (commit 6251993) but lack tests to prevent regression. native/macos.rs:35-37, 67, 154

Suggestions (5 found)

Agent Suggestion Location
type-design-analyzer Consider NonZeroU32 for window ID to make invalid state unrepresentable native/types.rs:5
comment-analyzer Add rationale comment for PID u32→i32 conversion (xcap returns u32, macOS APIs use i32) native/macos.rs:67
comment-analyzer Add rationale comment explaining empty title would match all windows native/macos.rs:35-37
comment-analyzer Document "open = not minimized" semantic choice for clarity ghostty.rs:379
docs-impact-agent Optional: clarify CLAUDE.md logging example that applescript_executing is iTerm/Terminal.app only (Ghostty now uses native APIs) CLAUDE.md:278

Strengths

  • Excellent SAFETY documentation for all unsafe FFI calls (Core Foundation ownership rules)
  • Clean architecture: native/ module properly isolated from backend logic
  • Type-safe PID conversions with i32::try_from() instead of as casts
  • Comprehensive structured logging following core.terminal.* convention
  • Well-documented fallback strategies and ANSI escape sequence usage
  • Platform-gated compilation with #[cfg(target_os = "macos")]
  • Good pkill pattern escaping tests preventing regex injection
  • Error type properly integrated with KildError trait (NativeWindowError scores 7.25/10)

Documentation Updates

None required. CLAUDE.md architecture section remains accurate — terminal/native/ is an internal module, not a top-level concern.


Verdict: NEEDS FIXES

The code quality is high (code-reviewer passed with no issues), but the "No Silent Failures" principle from CLAUDE.md is violated in 3 critical locations. All three will produce "window not found" errors that hide the real problem.

Recommended Actions

  1. Add warn! logging to PID conversion failures, skipped windows in loops, and empty title guard (3 critical silent failures)
  2. Fix stale comment referencing removed focus_by_pid function
  3. Add unit tests for empty title guard behavior and PID overflow handling (both were follow-up fixes without regression tests)
  4. Improve error messages with actionable hints for AppleScript failures and AX window-not-found
  5. Consider NativeWindowInfo constructor validation (prevents the class of bug that was just fixed)

…PI (#269)

Ghostty uses GPU rendering which bypasses the native widget tree that
macOS System Events relies on, causing focus, hide, and is_window_open
to always fail. This replaces the 6 AppleScript-based functions with
Core Graphics API (via xcap) for window enumeration and the macOS
Accessibility API for window manipulation.

Changes:
- Add terminal/native/ module with CG enumeration + AX manipulation
- Replace focus_by_pid/title, hide_by_pid/title, check_window_by_pid/title
  with find_ghostty_native_window + native::focus_window/minimize_window
- Add NativeWindowError variant to TerminalError
- Add xcap, accessibility-sys, core-foundation as macOS-only deps

Fixes #269
- Use i32::try_from instead of `as i32` for PID casts from xcap u32
- Guard against empty title_contains in find_window to prevent matching
  all windows when session ID is somehow empty
…sages

- Add warn! logging for empty title guard, PID conversion failures
- Add debug! logging for skipped windows in enumeration loops
- Improve AX fallback messages to note degraded behavior (app-level
  activation instead of window-specific, hides all windows not just target)
- Add actionable hints to AppleScript error messages (is app running?
  check automation permissions?)
- Include window count in AX window-not-found error
- Fix stale focus_by_pid comment reference
- Upgrade PID parse failure to warn when output is non-empty
- Add semantics comment for open=not-minimized logic
- Add doc comments for NativeWindowError and NativeWindowInfo
- Add tests: empty title guard, nonexistent app, PID overflow,
  NativeWindowError variant
@Wirasm Wirasm force-pushed the kild/issue-269-ghostty-cg-api branch from 6251993 to 8026784 Compare February 9, 2026 08:13
@Wirasm Wirasm merged commit 768e8bd into main Feb 9, 2026
6 checks passed
@Wirasm Wirasm deleted the kild/issue-269-ghostty-cg-api branch February 9, 2026 08:26
Wirasm added a commit that referenced this pull request Feb 11, 2026
After the Core Graphics migration (#286), `kild focus` could not restore
a window previously minimized via `kild hide` because the AX API focus
path only raised the window without unsetting kAXMinimizedAttribute.

Changes:
- Add UnminimizeAndRaise variant to WindowAction enum
- Add ax_unminimize_and_raise_window function mirroring existing pattern
- Check is_minimized in focus_window and unminimize before raising
- Add test for new WindowAction variant

Fixes #289
Wirasm added a commit that referenced this pull request Feb 11, 2026
* Investigate issue #289: Ghostty focus/hide after CG migration

Root cause: focus_window doesn't unminimize — AXRaised + activate_app
don't undo kAXMinimizedAttribute set by kild hide. Need to add
UnminimizeAndRaise action that sets kAXMinimized=false before raising.

* fix: unminimize Ghostty window before raising on focus (#289)

After the Core Graphics migration (#286), `kild focus` could not restore
a window previously minimized via `kild hide` because the AX API focus
path only raised the window without unsetting kAXMinimizedAttribute.

Changes:
- Add UnminimizeAndRaise variant to WindowAction enum
- Add ax_unminimize_and_raise_window function mirroring existing pattern
- Check is_minimized in focus_window and unminimize before raising
- Add test for new WindowAction variant

Fixes #289

* fix: add specific unminimize failure logging and exhaustive match test

Add dedicated focus_unminimize_failed log event to distinguish unminimize
failures from general AX raise failures — helps debug Ghostty AX quirks.

Replace array length test with exhaustive match test for WindowAction
variants, providing stronger compile-time guarantees.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Ghostty windows invisible to System Events — replace AppleScript with Core Graphics API

1 participant