mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-10 11:19:45 +00:00
macOS: get latest TerminalEntity's kind, title, pwd and screenshot (#13007)
This fixes these being wrong after creating a terminal window for the first time. Found them when trying [App Intent Testing](https://developer.apple.com/documentation/AppIntentsTesting/testing-your-app-intents-code), and confirmed them with macOS 26 as well (which is expected) <img width="2560" height="546" alt="Xnip2026-06-13_20-48-50" src="https://github.com/user-attachments/assets/7b13217d-66f5-48e2-ad69-9a489c22ad82" /> ## AI Disclosure Claude drafted the initial version, i manually refined and tested them myself.
This commit is contained in:
@@ -104,6 +104,14 @@ class AppDelegate: NSObject,
|
||||
/// The current state of the quick terminal.
|
||||
private var quickTerminalControllerState: QuickTerminalState = .uninitialized
|
||||
|
||||
/// Whether the quick terminal has already been initialized.
|
||||
var quickControllerInitialized: Bool {
|
||||
if case .initialized = quickTerminalControllerState {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Our quick terminal. This starts out uninitialized and only initializes if used.
|
||||
var quickController: QuickTerminalController {
|
||||
switch quickTerminalControllerState {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import AppKit
|
||||
import AppIntents
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import os
|
||||
|
||||
private let logger = Logger(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: "AppIntents.TerminalEntity"
|
||||
)
|
||||
|
||||
struct TerminalEntity: AppEntity {
|
||||
let id: UUID
|
||||
@@ -68,6 +75,64 @@ struct TerminalEntity: AppEntity {
|
||||
self.kind = .normal
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for the surface to be updated then create an entity
|
||||
///
|
||||
/// The PTY/Config sets the title and pwd asynchronously shortly after the
|
||||
/// surface is created, so concurrently wait for the second published
|
||||
/// value of each (the first is the current value) before returning.
|
||||
///
|
||||
/// If a value never arrives, the timeout completes the publisher and
|
||||
/// we fall back to the current value.
|
||||
///
|
||||
/// Waiting for the title and pwd also gives the SurfaceView time to lay
|
||||
/// out, so the screenshot we capture afterwards reflects the rendered view.
|
||||
@MainActor
|
||||
init(view: Ghostty.SurfaceView) async {
|
||||
self.id = view.id
|
||||
self.tty = view.surfaceModel?.ttyName
|
||||
|
||||
let waitTimeout = DispatchQueue.SchedulerTimeType.Stride.seconds(1)
|
||||
let titleValues = view.$title.dropFirst()
|
||||
.setFailureType(to: Error.self)
|
||||
.timeout(waitTimeout, scheduler: DispatchQueue.main, customError: { EntityTimeoutError() })
|
||||
.handleEvents(receiveCompletion: { completion in
|
||||
if case .failure = completion {
|
||||
logger.error("failed to get terminal's title: timeout")
|
||||
}
|
||||
})
|
||||
.replaceError(with: view.title)
|
||||
.values
|
||||
let pwdValues = view.$pwd.dropFirst()
|
||||
.setFailureType(to: Error.self)
|
||||
.timeout(waitTimeout, scheduler: DispatchQueue.main, customError: { EntityTimeoutError() })
|
||||
.handleEvents(receiveCompletion: { completion in
|
||||
if case .failure = completion {
|
||||
logger.error("failed to get terminal's pwd: timeout")
|
||||
}
|
||||
})
|
||||
.replaceError(with: view.pwd)
|
||||
.values
|
||||
async let title = titleValues.first(where: { _ in true })
|
||||
async let pwd = pwdValues.first(where: { _ in true })
|
||||
|
||||
self.title = await title ?? ""
|
||||
self.workingDirectory = await pwd ?? ""
|
||||
|
||||
// Wait for the title and pwd then get latest pid and screenshots.
|
||||
// This should gave SurfaceView enough time to layout in the window and we can get the most recent process's PID
|
||||
// Determine the kind based on the window controller type
|
||||
if view.window?.windowController is QuickTerminalController {
|
||||
self.kind = .quick
|
||||
} else {
|
||||
self.kind = .normal
|
||||
}
|
||||
|
||||
self.pid = view.surfaceModel?.foregroundPID
|
||||
if let nsImage = ImageRenderer(content: view.screenshot()).nsImage {
|
||||
self.screenshot = nsImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TerminalEntity {
|
||||
@@ -127,3 +192,5 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct EntityTimeoutError: Error {}
|
||||
|
||||
@@ -112,7 +112,7 @@ struct NewTerminalIntent: AppIntent {
|
||||
withBaseConfig: config,
|
||||
withParent: parent?.window)
|
||||
if let view = newController.surfaceTree.root?.leftmostLeaf() {
|
||||
return .result(value: TerminalEntity(view))
|
||||
return .result(value: await TerminalEntity(view: view))
|
||||
}
|
||||
|
||||
case .tab:
|
||||
@@ -121,7 +121,7 @@ struct NewTerminalIntent: AppIntent {
|
||||
from: parent?.window,
|
||||
withBaseConfig: config)
|
||||
if let view = newController?.surfaceTree.root?.leftmostLeaf() {
|
||||
return .result(value: TerminalEntity(view))
|
||||
return .result(value: await TerminalEntity(view: view))
|
||||
}
|
||||
|
||||
case .splitLeft, .splitRight, .splitUp, .splitDown:
|
||||
@@ -135,7 +135,7 @@ struct NewTerminalIntent: AppIntent {
|
||||
direction: location.splitDirection!,
|
||||
baseConfig: config
|
||||
) {
|
||||
return .result(value: TerminalEntity(view))
|
||||
return .result(value: await TerminalEntity(view: view))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,14 +20,22 @@ struct QuickTerminalIntent: AppIntent {
|
||||
throw GhosttyIntentError.appUnavailable
|
||||
}
|
||||
|
||||
let wasInitialized = delegate.quickControllerInitialized
|
||||
|
||||
// This is safe to call even if it is already shown.
|
||||
let c = delegate.quickController
|
||||
|
||||
c.animateIn()
|
||||
|
||||
// Grab all our terminals
|
||||
let terminals = c.surfaceTree.root?.leaves().map {
|
||||
TerminalEntity($0)
|
||||
} ?? []
|
||||
var terminals: [TerminalEntity] = []
|
||||
for view in c.surfaceTree.root?.leaves() ?? [] {
|
||||
if wasInitialized {
|
||||
terminals.append(TerminalEntity(view))
|
||||
} else {
|
||||
terminals.append(await TerminalEntity(view: view))
|
||||
}
|
||||
}
|
||||
|
||||
return .result(value: terminals)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user