Merge 0bce07d92f into a4cb73db84
commit
8a544d7ee2
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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 .{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue