import AppKit final class SplashTipWindowController: NSWindowController { private let imageView = NSImageView() private let tipLabel = NSTextField(labelWithString: "Tip: Press Cmd+Opt+Shift+P to change modes") private let counterLabel = NSTextField(labelWithString: "0.0") private var timer: DispatchSourceTimer? private var startTime: DispatchTime? private let displayDuration: TimeInterval = 5.0 init() { let style: NSWindow.StyleMask = [.borderless] let initialRect = NSRect(x: 0, y: 0, width: 520, height: 360) let window = NSWindow(contentRect: initialRect, styleMask: style, backing: .buffered, defer: false) super.init(window: window) window.isReleasedWhenClosed = false // Light theme background window.backgroundColor = NSColor(calibratedWhite: 1.0, alpha: 0.96) window.isOpaque = false window.hasShadow = true window.level = .floating window.collectionBehavior = [.canJoinAllSpaces] window.titleVisibility = .hidden window.titlebarAppearsTransparent = true window.appearance = NSAppearance(named: .aqua) let content = NSView() content.wantsLayer = true content.translatesAutoresizingMaskIntoConstraints = false window.contentView = content imageView.translatesAutoresizingMaskIntoConstraints = false imageView.imageScaling = .scaleProportionallyUpOrDown imageView.animates = true tipLabel.translatesAutoresizingMaskIntoConstraints = false tipLabel.font = NSFont.systemFont(ofSize: 14, weight: .semibold) tipLabel.textColor = .labelColor tipLabel.alignment = .center tipLabel.lineBreakMode = .byWordWrapping tipLabel.maximumNumberOfLines = 2 counterLabel.translatesAutoresizingMaskIntoConstraints = false counterLabel.font = NSFont.monospacedSystemFont(ofSize: 10, weight: .regular) counterLabel.textColor = NSColor(white: 0.1, alpha: 0.85) counterLabel.alignment = .left content.addSubview(imageView) content.addSubview(tipLabel) content.addSubview(counterLabel) NSLayoutConstraint.activate([ content.widthAnchor.constraint(equalToConstant: initialRect.width), content.heightAnchor.constraint(equalToConstant: initialRect.height), imageView.topAnchor.constraint(equalTo: content.topAnchor, constant: 20), imageView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20), imageView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20), imageView.heightAnchor.constraint(equalToConstant: 260), tipLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 12), tipLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20), tipLabel.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20), counterLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 6), counterLabel.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -4) ]) // Load background/splash image from config (bundled in Resources) if present let bgName = Config.shared.uiBackground let candidates: [(String, String?)] = [ (bgName, nil), ("prole-type", "gif") ] var loaded = false for (res, ext) in candidates { if let url = Bundle.main.url(forResource: res, withExtension: ext), let img = NSImage(contentsOf: url) { imageView.image = img loaded = true break } } if !loaded { // Fallback: show placeholder text if the resource is missing let placeholder = NSTextField(labelWithString: "Splash background not found") placeholder.textColor = .secondaryLabelColor placeholder.alignment = .center placeholder.translatesAutoresizingMaskIntoConstraints = false content.addSubview(placeholder) NSLayoutConstraint.activate([ placeholder.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), placeholder.centerYAnchor.constraint(equalTo: imageView.centerYAnchor) ]) } // Click anywhere to dismiss early let clickRecognizer = NSClickGestureRecognizer(target: self, action: #selector(dismissSplash)) content.addGestureRecognizer(clickRecognizer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show() { guard let screen = NSScreen.main, let window = window else { return } let frame = window.frame let x = screen.visibleFrame.midX - frame.width / 2 let y = screen.visibleFrame.midY - frame.height / 2 window.setFrame(NSRect(x: x, y: y, width: frame.width, height: frame.height), display: true) window.orderFrontRegardless() NSApp.activate(ignoringOtherApps: false) // Start counter timer startTime = .now() let timer = DispatchSource.makeTimerSource(queue: .main) timer.schedule(deadline: .now(), repeating: .milliseconds(100)) timer.setEventHandler { [weak self] in self?.tick() } timer.resume() self.timer = timer // Auto dismiss after duration DispatchQueue.main.asyncAfter(deadline: .now() + displayDuration) { [weak self] in self?.dismissSplash() } } private func tick() { guard let start = startTime else { return } let elapsed = DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds let seconds = Double(elapsed) / 1_000_000_000.0 let whole = Int(seconds) let tenths = Int((seconds - Double(whole)) * 10.0) counterLabel.stringValue = String(format: "%d.%d", whole, tenths) } @objc private func dismissSplash() { timer?.cancel() timer = nil window?.orderOut(nil) close() } }