macOS: get latest TerminalEntity's kind, title & pwd

This fixes these being wrong after creating a terminal window for the first time.
This commit is contained in:
Lukas
2026-06-12 20:13:50 +02:00
parent 5659cef41f
commit e2832ae1fe
4 changed files with 89 additions and 6 deletions

View File

@@ -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 {

View File

@@ -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 {}

View File

@@ -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))
}
}

View File

@@ -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)
}