diff --git a/include/ghostty.h b/include/ghostty.h index afc20bb3f..5ec1885b0 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -1115,6 +1115,8 @@ GHOSTTY_API void ghostty_surface_set_focus(ghostty_surface_t, bool); GHOSTTY_API void ghostty_surface_set_occlusion(ghostty_surface_t, bool); GHOSTTY_API void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); GHOSTTY_API ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t); +GHOSTTY_API uint64_t ghostty_surface_foreground_pid(ghostty_surface_t); +GHOSTTY_API ghostty_string_s ghostty_surface_tty_name(ghostty_surface_t); GHOSTTY_API void ghostty_surface_set_color_scheme(ghostty_surface_t, ghostty_color_scheme_e); GHOSTTY_API ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, diff --git a/macos/Ghostty.sdef b/macos/Ghostty.sdef index bdfc501fb..c84cf2f6c 100644 --- a/macos/Ghostty.sdef +++ b/macos/Ghostty.sdef @@ -90,6 +90,8 @@ + + diff --git a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift index a2c4abea0..1a0a0acb5 100644 --- a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift @@ -11,6 +11,12 @@ struct TerminalEntity: AppEntity { @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 @@ -49,6 +55,8 @@ struct TerminalEntity: AppEntity { 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 } diff --git a/macos/Sources/Features/AppleScript/ScriptTerminal.swift b/macos/Sources/Features/AppleScript/ScriptTerminal.swift index 2cdde382e..202eb662b 100644 --- a/macos/Sources/Features/AppleScript/ScriptTerminal.swift +++ b/macos/Sources/Features/AppleScript/ScriptTerminal.swift @@ -11,6 +11,8 @@ import AppKit /// - `property id` -> `@objc(id)` getter below. /// - `property title` -> `@objc(title)` getter below. /// - `property working directory` -> `@objc(workingDirectory)` getter below. +/// - `property pid` -> `@objc(pid)` getter below. +/// - `property tty` -> `@objc(tty)` getter below. /// /// We keep only a weak reference to the underlying `SurfaceView` so this /// wrapper never extends the terminal's lifetime. @@ -53,6 +55,20 @@ final class ScriptTerminal: NSObject { return surfaceView?.pwd ?? "" } + /// Exposed as the AppleScript `pid` property. + @objc(pid) + var pid: Int { + guard NSApp.isAppleScriptEnabled else { return 0 } + return surfaceView?.surfaceModel?.foregroundPID ?? 0 + } + + /// Exposed as the AppleScript `tty` property. + @objc(tty) + var tty: String { + guard NSApp.isAppleScriptEnabled else { return "" } + return surfaceView?.surfaceModel?.ttyName ?? "" + } + /// Used by command handling (`perform action ... on `). func perform(action: String) -> Bool { guard NSApp.isAppleScriptEnabled else { return false } diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index b072db15e..440b1b373 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -92,6 +92,21 @@ extension Ghostty { ghostty_surface_mouse_captured(surface) } + /// The PID of the foreground process group attached to the PTY. + @MainActor + var foregroundPID: Int? { + let pid = ghostty_surface_foreground_pid(surface) + guard pid != 0 else { return nil } + return Int(exactly: pid) + } + + /// The PTY device name for this surface. + @MainActor + var ttyName: String? { + let ttyName = AllocatedString(ghostty_surface_tty_name(surface)).string + return ttyName.isEmpty ? nil : ttyName + } + /// Send a mouse button event to the terminal. /// /// This sends a complete mouse button event including the button state (press/release), diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 519a35f2b..ad20cf465 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -20,6 +20,7 @@ const CoreInspector = @import("../inspector/main.zig").Inspector; const CoreSurface = @import("../Surface.zig"); const configpkg = @import("../config.zig"); const Config = configpkg.Config; +const String = @import("../main_c.zig").String; const log = std.log.scoped(.embedded_window); @@ -1709,6 +1710,23 @@ pub const CAPI = struct { }; } + /// Returns the PID of the foreground process for the surface PTY. + export fn ghostty_surface_foreground_pid(surface: *Surface) u64 { + return surface.core_surface.getProcessInfo(.foreground_pid) orelse 0; + } + + /// Returns the PTY name for the surface. The returned string must be + /// freed by the caller via ghostty_string_free. + export fn ghostty_surface_tty_name(surface: *Surface) String { + const tty_name = surface.core_surface.getProcessInfo(.tty_name) orelse return .empty; + const copy = surface.app.core_app.alloc.dupeZ(u8, tty_name) catch |err| { + log.err("error allocating tty name err={}", .{err}); + return .empty; + }; + + return .fromSlice(copy); + } + /// Update the color scheme of the surface. export fn ghostty_surface_set_color_scheme(surface: *Surface, scheme_raw: c_int) void { const scheme = std.meta.intToEnum(apprt.ColorScheme, scheme_raw) catch { diff --git a/src/config/CApi.zig b/src/config/CApi.zig index 6e835b4a4..7d3366c93 100644 --- a/src/config/CApi.zig +++ b/src/config/CApi.zig @@ -2,7 +2,7 @@ const builtin = @import("builtin"); const std = @import("std"); const inputpkg = @import("../input.zig"); const state = &@import("../global.zig").state; -const c = @import("../main_c.zig"); +const String = @import("../main_c.zig").String; const Config = @import("Config.zig"); const c_get = @import("c_get.zig"); @@ -132,7 +132,7 @@ export fn ghostty_config_get_diagnostic(self: *Config, idx: u32) Diagnostic { return .{ .message = message.ptr }; } -export fn ghostty_config_open_path() c.String { +export fn ghostty_config_open_path() String { const path = edit.openPath(state.alloc) catch |err| { log.err("error opening config in editor err={}", .{err}); return .empty;