bd44b5aa08
Test / build-libghostty-vt (x86_64-windows-gnu) (push) Has been cancelled
Test / build-libghostty-vt-macos (aarch64-ios) (push) Has been cancelled
Test / build-libghostty-vt-macos (aarch64-macos) (push) Has been cancelled
Test / build-libghostty-vt-macos (x86_64-macos) (push) Has been cancelled
Test / build-nix (namespace-profile-ghostty-md-arm64) (push) Has been cancelled
Test / build-dist (push) Has been cancelled
Test / GTK x11=true wayland=true (push) Has been cancelled
Test / Build -Dsimd=false (push) Has been cancelled
Test / Build -Dsimd=true (push) Has been cancelled
Test / Build -Dsentry=false (push) Has been cancelled
Test / Build -Dsentry=true (push) Has been cancelled
Test / zig-fmt (push) Has been cancelled
Test / GitHub Actions Pins (push) Has been cancelled
Test / prettier (push) Has been cancelled
Test / swiftlint (push) Has been cancelled
Test / alejandra (push) Has been cancelled
Test / typos (push) Has been cancelled
Nix / Required Checks: Nix (push) Has been cancelled
Nix / check-zig-cache-hash (push) Has been cancelled
Test / skip (push) Has been cancelled
Test / Required Checks: Test (push) Has been cancelled
Test / build-bench (push) Has been cancelled
Test / list-examples (push) Has been cancelled
Test / Example ${{ matrix.dir }} (Windows) (push) Has been cancelled
Test / build-cmake (push) Has been cancelled
Test / test-lib-vt-pkgconfig (push) Has been cancelled
Test / build-flatpak (push) Has been cancelled
Test / build-snap (push) Has been cancelled
Test / build-libghostty-vt (aarch64-linux) (push) Has been cancelled
Test / build-libghostty-vt (aarch64-macos) (push) Has been cancelled
Test / build-libghostty-vt (wasm32-freestanding) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-linux) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-linux-musl) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-macos) (push) Has been cancelled
Test / build-libghostty-vt-android (aarch64-linux-android) (push) Has been cancelled
Test / build-libghostty-vt-android (arm-linux-androideabi) (push) Has been cancelled
Test / build-libghostty-vt-android (x86_64-linux-android) (push) Has been cancelled
Test / build-libghostty-vt-windows (push) Has been cancelled
Test / build-libghostty-windows-gnu (push) Has been cancelled
Test / build-linux (namespace-profile-ghostty-md) (push) Has been cancelled
Test / build-linux (namespace-profile-ghostty-md-arm64) (push) Has been cancelled
Test / build-linux-libghostty (push) Has been cancelled
Test / build-nix (namespace-profile-ghostty-md) (push) Has been cancelled
Test / build-dist-lib-vt (push) Has been cancelled
Test / trigger-snap (push) Has been cancelled
Test / trigger-flatpak (push) Has been cancelled
Test / build-macos (push) Has been cancelled
Test / build-macos-freetype (push) Has been cancelled
Test / test (push) Has been cancelled
Test / test-lib-vt (push) Has been cancelled
Test / GTK x11=false wayland=false (push) Has been cancelled
Test / GTK x11=true wayland=false (push) Has been cancelled
Test / GTK x11=false wayland=true (push) Has been cancelled
Test / test-macos (push) Has been cancelled
Test / test-windows (push) Has been cancelled
Test / Build -Di18n=false (push) Has been cancelled
Test / Build -Di18n=true (push) Has been cancelled
Test / Build test/fuzz-libghostty (push) Has been cancelled
Test / shellcheck (push) Has been cancelled
Test / translations (push) Has been cancelled
Test / blueprint-compiler (push) Has been cancelled
Test / Test pkg/wuffs (push) Has been cancelled
Test / Test build on Debian 13 (push) Has been cancelled
Test / valgrind (push) Has been cancelled
Test / Example ${{ matrix.dir }} (push) Has been cancelled
197 lines
6.0 KiB
Swift
197 lines
6.0 KiB
Swift
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
|
|
|
|
@Property(title: "Title")
|
|
var title: String
|
|
|
|
@Property(title: "Working Directory")
|
|
var workingDirectory: String?
|
|
|
|
@Property(title: "PID")
|
|
var pid: Int?
|
|
|
|
@Property(title: "TTY")
|
|
var tty: String?
|
|
|
|
@Property(title: "Kind")
|
|
var kind: Kind
|
|
|
|
var screenshot: NSImage?
|
|
|
|
static var typeDisplayRepresentation: TypeDisplayRepresentation {
|
|
TypeDisplayRepresentation(name: "Terminal")
|
|
}
|
|
|
|
@MainActor
|
|
var displayRepresentation: DisplayRepresentation {
|
|
var rep = DisplayRepresentation(title: "\(title)")
|
|
if let screenshot,
|
|
let data = screenshot.tiffRepresentation {
|
|
rep.image = .init(data: data)
|
|
}
|
|
|
|
return rep
|
|
}
|
|
|
|
/// Returns the view associated with this entity. This may no longer exist.
|
|
@MainActor
|
|
var surfaceView: Ghostty.SurfaceView? {
|
|
Self.defaultQuery.all.first { $0.id == self.id }
|
|
}
|
|
|
|
@MainActor
|
|
var surfaceModel: Ghostty.Surface? {
|
|
surfaceView?.surfaceModel
|
|
}
|
|
|
|
static var defaultQuery = TerminalQuery()
|
|
|
|
@MainActor
|
|
init(_ view: Ghostty.SurfaceView) {
|
|
self.id = view.id
|
|
self.title = view.title
|
|
self.workingDirectory = view.pwd
|
|
self.pid = view.surfaceModel?.foregroundPID
|
|
self.tty = view.surfaceModel?.ttyName
|
|
if let nsImage = ImageRenderer(content: view.screenshot()).nsImage {
|
|
self.screenshot = nsImage
|
|
}
|
|
|
|
// Determine the kind based on the window controller type
|
|
if view.window?.windowController is QuickTerminalController {
|
|
self.kind = .quick
|
|
} else {
|
|
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 {
|
|
enum Kind: String, AppEnum {
|
|
case normal
|
|
case quick
|
|
|
|
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Kind")
|
|
|
|
static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [
|
|
.normal: .init(title: "Normal"),
|
|
.quick: .init(title: "Quick")
|
|
]
|
|
}
|
|
}
|
|
|
|
struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery {
|
|
@MainActor
|
|
func entities(for identifiers: [TerminalEntity.ID]) async throws -> [TerminalEntity] {
|
|
return all.filter {
|
|
identifiers.contains($0.id)
|
|
}.map {
|
|
TerminalEntity($0)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
func entities(matching string: String) async throws -> [TerminalEntity] {
|
|
return all.filter {
|
|
$0.title.localizedCaseInsensitiveContains(string)
|
|
}.map {
|
|
TerminalEntity($0)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
func allEntities() async throws -> [TerminalEntity] {
|
|
return all.map { TerminalEntity($0) }
|
|
}
|
|
|
|
@MainActor
|
|
func suggestedEntities() async throws -> [TerminalEntity] {
|
|
return try await allEntities()
|
|
}
|
|
|
|
@MainActor
|
|
var all: [Ghostty.SurfaceView] {
|
|
// Find all of our terminal windows. This will include the quick terminal
|
|
// but only if it was previously opened.
|
|
let controllers = NSApp.windows.compactMap {
|
|
$0.windowController as? BaseTerminalController
|
|
}
|
|
|
|
// Get all our surfaces
|
|
return controllers.flatMap {
|
|
$0.surfaceTree.root?.leaves() ?? []
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct EntityTimeoutError: Error {}
|