diff --git a/src/App.zig b/src/App.zig index 41a7887b2..314d0b25b 100644 --- a/src/App.zig +++ b/src/App.zig @@ -45,6 +45,12 @@ quit: bool, /// same font configuration. font_grid_set: font.SharedGridSet, +// Used to rate limit desktop notifications. Some platforms (notably macOS) will +// run out of resources if desktop notifications are sent too fast and the OS +// will kill Ghostty. +last_notification_time: ?std.time.Instant = null, +last_notification_digest: u64 = 0, + /// Initialize the main app instance. This creates the main window, sets /// up the renderer state, compiles the shaders, etc. This is the primary /// "startup" logic. diff --git a/src/Surface.zig b/src/Surface.zig index 10e312502..88f6e18f0 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3512,9 +3512,47 @@ fn completeClipboardReadOSC52( } fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const u8) !void { - if (@hasDecl(apprt.Surface, "showDesktopNotification")) { - try self.rt_surface.showDesktopNotification(title, body); - } else log.warn("runtime doesn't support desktop notifications", .{}); + if (comptime !@hasDecl(apprt.Surface, "showDesktopNotification")) { + log.warn("runtime doesn't support desktop notifications", .{}); + return; + } + + // Wyhash is used to hash the contents of the desktop notification to limit + // how fast identical notifications can be sent sequentially. + const hash_algorithm = std.hash.Wyhash; + + const now = try std.time.Instant.now(); + + // Set a limit of one desktop notification per second so that the OS + // doesn't kill us when we run out of resources. + if (self.app.last_notification_time) |last| { + if (now.since(last) < 1 * std.time.ns_per_s) { + log.warn("rate limiting desktop notifications", .{}); + return; + } + } + + const new_digest = d: { + var hash = hash_algorithm.init(0); + hash.update(title); + hash.update(body); + break :d hash.final(); + }; + + // Set a limit of one notification per five seconds for desktop + // notifications with identical content. + if (self.app.last_notification_time) |last| { + if (self.app.last_notification_digest == new_digest) { + if (now.since(last) < 5 * std.time.ns_per_s) { + log.warn("suppressing identical desktop notification", .{}); + return; + } + } + } + + self.app.last_notification_time = now; + self.app.last_notification_digest = new_digest; + try self.rt_surface.showDesktopNotification(title, body); } pub const face_ttf = @embedFile("font/res/JetBrainsMono-Regular.ttf");