Jon Parise 2025-12-17 21:41:52 +00:00 committed by GitHub
commit 8a544d7ee2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 61 additions and 7 deletions

View File

@ -1008,6 +1008,7 @@ ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t);
void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
bool ghostty_surface_process_exited(ghostty_surface_t);
void ghostty_surface_process_stop(ghostty_surface_t);
void ghostty_surface_refresh(ghostty_surface_t);
void ghostty_surface_draw(ghostty_surface_t);
void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);

View File

@ -343,14 +343,48 @@ class AppDelegate: NSObject,
return derivedConfig.shouldQuitAfterLastWindowClosed
}
/// Initiates graceful application termination.
///
/// We signal all child processes to stop and then wait for them to exit
/// (or for our timeout to expire) before terminating the application.
private func terminateGracefully() -> NSApplication.TerminateReply {
let surfaces = TerminalController.all.flatMap { $0.surfaceTree } + quickController.surfaceTree
surfaces.forEach { $0.stopProcess() }
let deadline = DispatchTime.now() + .milliseconds(500)
let pollInterval: DispatchTimeInterval = .milliseconds(50)
func waitForProcesses() {
if surfaces.allSatisfy({ $0.processExited }) {
NSApp.reply(toApplicationShouldTerminate: true)
return
}
if DispatchTime.now() >= deadline {
Ghostty.logger.info("child process deadline exceeded; terminating immediately")
NSApp.reply(toApplicationShouldTerminate: true)
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + pollInterval) {
waitForProcesses()
}
}
Ghostty.logger.debug("waiting for child processes to exit")
waitForProcesses()
return .terminateLater
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
let windows = NSApplication.shared.windows
if (windows.isEmpty) { return .terminateNow }
if windows.isEmpty { return terminateGracefully() }
// If we've already accepted to install an update, then we don't need to
// confirm quit. The user is already expecting the update to happen.
if updateController.isInstalling {
return .terminateNow
return terminateGracefully()
}
// This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't
@ -361,7 +395,7 @@ class AppDelegate: NSObject,
// here because I don't want to remove it in a patch release cycle but we should
// target removing it soon.
if (windows.allSatisfy { !$0.isVisible }) {
return .terminateNow
return terminateGracefully()
}
// If the user is shutting down, restarting, or logging out, we don't confirm quit.
@ -380,7 +414,7 @@ class AppDelegate: NSObject,
fallthrough
case kAEReallyLogOut:
return .terminateNow
return terminateGracefully()
default:
break
@ -389,7 +423,7 @@ class AppDelegate: NSObject,
}
// If our app says we don't need to confirm, we can exit now.
if (!ghostty.needsConfirmQuit) { return .terminateNow }
if !ghostty.needsConfirmQuit { return terminateGracefully() }
// We have some visible window. Show an app-wide modal to confirm quitting.
let alert = NSAlert()
@ -400,7 +434,7 @@ class AppDelegate: NSObject,
alert.alertStyle = .warning
switch (alert.runModal()) {
case .alertFirstButtonReturn:
return .terminateNow
return terminateGracefully()
default:
return .terminateCancel

View File

@ -169,6 +169,12 @@ extension Ghostty {
return ghostty_surface_process_exited(surface)
}
// Stops the child process.
func stopProcess() {
guard let surface = self.surface else { return }
ghostty_surface_process_stop(surface)
}
// Returns the inspector instance for this surface, or nil if the
// surface has been closed.
var inspector: ghostty_inspector_t? {

View File

@ -813,6 +813,14 @@ pub fn close(self: *Surface) void {
self.rt_surface.close(self.needsConfirmQuit());
}
/// Stop the child process by sending it the kill command (SIGHUP).
/// child_exited will be set once the child process has stopped.
pub fn stopProcess(self: *Surface) void {
switch (self.io.backend) {
.exec => |*exec| exec.subprocess.stop(),
}
}
/// Returns a mailbox that can be used to send messages to this surface.
inline fn surfaceMailbox(self: *Surface) Mailbox {
return .{

View File

@ -1542,6 +1542,11 @@ pub const CAPI = struct {
return surface.core_surface.child_exited;
}
/// Stops the child process by sending it the kill command (SIGHUP).
export fn ghostty_surface_process_stop(surface: *Surface) void {
surface.core_surface.stopProcess();
}
/// Returns true if the surface has a selection.
export fn ghostty_surface_has_selection(surface: *Surface) bool {
return surface.core_surface.hasSelection();