import AppKit import Foundation import SwiftUI /// Small, non-modal confirmations ("Path copied", "3 files converted"). /// Nothing here blocks the user's flow, which is the whole point of the app. enum Notifier { private static var toastWindow: NSWindow? private static var dismissWorkItem: DispatchWorkItem? @MainActor static func flash(title: String, body: String? = nil, symbol: String = "checkmark.circle.fill") { guard Settings.shared.showProgressWindow else { return } show(title: title, body: body, symbol: symbol, tone: .success, duration: 2.0) } @MainActor static func error(_ title: String, _ message: String) { show(title: title, body: message, symbol: "exclamationmark.triangle.fill", tone: .failure, duration: 6.0) } private enum Tone { case success, failure } @MainActor private static func show(title: String, body: String?, symbol: String, tone: Tone, duration: TimeInterval) { dismissWorkItem?.cancel() toastWindow?.close() let accent: Color = tone == .success ? .green : .orange let view = HStack(spacing: 10) { Image(systemName: symbol) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(accent) VStack(alignment: .leading, spacing: 2) { Text(title) .font(.system(size: 12, weight: .semibold)) .lineLimit(1) if let body, !body.isEmpty { Text(body) .font(.system(size: 11)) .foregroundStyle(.secondary) .lineLimit(2) .truncationMode(.middle) } } } .padding(.horizontal, 14) .padding(.vertical, 10) .frame(minWidth: 220, maxWidth: 420, alignment: .leading) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .strokeBorder(Color.primary.opacity(0.08)) ) let controller = NSHostingController(rootView: view) let window = NSWindow(contentViewController: controller) window.styleMask = [.borderless] window.isOpaque = false window.backgroundColor = .clear window.hasShadow = true window.level = .statusBar window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary] window.ignoresMouseEvents = true window.appearance = NSApp.effectiveAppearance let size = controller.view.fittingSize let screen = NSScreen.main ?? NSScreen.screens.first if let frame = screen?.visibleFrame { let origin = CGPoint(x: frame.midX - size.width / 2, y: frame.minY + 96) window.setFrame(CGRect(origin: origin, size: size), display: true) } window.orderFrontRegardless() toastWindow = window let work = DispatchWorkItem { [weak window] in NSAnimationContext.runAnimationGroup { context in context.duration = 0.25 window?.animator().alphaValue = 0 } completionHandler: { window?.close() } if toastWindow === window { toastWindow = nil } } dismissWorkItem = work DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: work) } }