From ee010feffc052f4e8686f8b67ad6d3b5754efe19 Mon Sep 17 00:00:00 2001 From: Moaz Ahmad Date: Thu, 26 Feb 2026 22:24:23 +0300 Subject: [PATCH 1/5] feat(sound): implement mute audio feedback Add SoundFeedbackManager to play pre-loaded NSSound native audio cues on mute state changes with zero latency. --- .../Support/SoundFeedbackManager.swift | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 toggleMute/toggleMute/Support/SoundFeedbackManager.swift diff --git a/toggleMute/toggleMute/Support/SoundFeedbackManager.swift b/toggleMute/toggleMute/Support/SoundFeedbackManager.swift new file mode 100644 index 0000000..b8e1d50 --- /dev/null +++ b/toggleMute/toggleMute/Support/SoundFeedbackManager.swift @@ -0,0 +1,41 @@ +// Plays a short audio cue on mute/unmute using AppKit's NSSound — zero extra frameworks. + +import Cocoa + +final class SoundFeedbackManager { + + static let shared = SoundFeedbackManager() + private var preferences = Preferences() + + // Pre-load both sounds once so playback is instant. + private let muteSound: NSSound? = { + if let s = NSSound(named: NSSound.Name("Tink")) { + s.volume = 0.6 + return s + } + return nil + }() + + private let unmuteSound: NSSound? = { + if let s = NSSound(named: NSSound.Name("Pop")) { + s.volume = 0.6 + return s + } + return nil + }() + + private init() {} + + func playMute() { + guard preferences.muteSoundEnabled else { return } + // Stop any previous playback first so rapid toggling doesn't queue up sounds + muteSound?.stop() + muteSound?.play() + } + + func playUnmute() { + guard preferences.muteSoundEnabled else { return } + unmuteSound?.stop() + unmuteSound?.play() + } +} From 52150572bc44110280fa3ff5a42cadabcca4a1bf Mon Sep 17 00:00:00 2001 From: Moaz Ahmad Date: Thu, 26 Feb 2026 22:24:31 +0300 Subject: [PATCH 2/5] feat(hud): add mute overlay Implement borderless NSPanel with hardware-accelerated NSVisualEffectView and CABasicAnimation for native system-level HUD appearance. --- .../Controllers/MuteHUDWindowController.swift | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 toggleMute/toggleMute/Controllers/MuteHUDWindowController.swift diff --git a/toggleMute/toggleMute/Controllers/MuteHUDWindowController.swift b/toggleMute/toggleMute/Controllers/MuteHUDWindowController.swift new file mode 100644 index 0000000..c8772e1 --- /dev/null +++ b/toggleMute/toggleMute/Controllers/MuteHUDWindowController.swift @@ -0,0 +1,140 @@ +import Cocoa + +final class MuteHUDWindowController { + + static let shared = MuteHUDWindowController() + private var preferences = Preferences() + + private let hudSize = CGSize(width: 200, height: 200) + + private lazy var hudWindow: NSPanel = { + let panel = NSPanel( + contentRect: NSRect(origin: .zero, size: hudSize), + styleMask: [.borderless, .nonactivatingPanel], + backing: .buffered, + defer: true + ) + panel.level = .modalPanel + panel.isOpaque = false + panel.backgroundColor = .clear + panel.hasShadow = false + panel.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle] + panel.isMovable = false + panel.contentView = buildContentView() + return panel + }() + + private var iconView: NSImageView! + private var label: NSTextField! + + private var dismissWork: DispatchWorkItem? + + private init() {} + + // MARK: - Public API + + func show(muted: Bool) { + guard preferences.showHudEnabled else { return } + + let window = hudWindow + + // Update content (using contentTintColor for macOS 11 compatibility) + let symbolName = muted ? "mic.slash.fill" : "mic.fill" + if let img = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil) { + let cfg = NSImage.SymbolConfiguration(pointSize: 72, weight: .regular) + iconView.image = img.withSymbolConfiguration(cfg) + } + iconView.contentTintColor = muted ? .systemRed : .white + label.stringValue = muted ? "Muted" : "Unmuted" + + positionWindow() + + dismissWork?.cancel() + + if let layer = window.contentView?.layer { + layer.removeAllAnimations() + layer.opacity = 1.0 + } + + // Immediately make visible + window.alphaValue = 1.0 + window.orderFrontRegardless() + + // Schedule fade-out after 1.0 s + let work = DispatchWorkItem { [weak self] in + self?.fadeOut() + } + dismissWork = work + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: work) + } + + // MARK: - Private helpers + + private func buildContentView() -> NSView { + let container = NSView(frame: NSRect(origin: .zero, size: hudSize)) + container.wantsLayer = true + + let blur = NSVisualEffectView(frame: container.bounds) + blur.material = .hudWindow + blur.blendingMode = .behindWindow + blur.state = .active + blur.wantsLayer = true + blur.layer?.cornerRadius = 18 + blur.layer?.masksToBounds = true + blur.autoresizingMask = [.width, .height] + container.addSubview(blur) + + let iv = NSImageView(frame: NSRect(x: 0, y: 76, width: hudSize.width, height: 80)) + iv.imageScaling = .scaleProportionallyUpOrDown + iv.autoresizingMask = [.minXMargin, .maxXMargin] + blur.addSubview(iv) + iconView = iv + + let tf = NSTextField(frame: NSRect(x: 0, y: 32, width: hudSize.width, height: 24)) + tf.isEditable = false + tf.isBordered = false + tf.isBezeled = false + tf.drawsBackground = false + tf.alignment = .center + tf.font = .systemFont(ofSize: 18, weight: .bold) + tf.textColor = NSColor(white: 1.0, alpha: 0.85) + tf.autoresizingMask = [.minXMargin, .maxXMargin] + blur.addSubview(tf) + label = tf + + return container + } + + private func positionWindow() { + guard let screen = NSScreen.main else { return } + let screenFrame = screen.visibleFrame + let x = screenFrame.midX - hudSize.width / 2 + let y = screenFrame.minY + 70 + hudWindow.setFrameOrigin(NSPoint(x: x, y: y)) + } + + private func fadeOut() { + guard let layer = hudWindow.contentView?.layer else { return } + + CATransaction.begin() + CATransaction.setCompletionBlock { [weak self] in + // When animation completes naturally (not cancelled), hide the window + if layer.opacity == 0 { + self?.hudWindow.orderOut(nil) + } + } + + let anim = CABasicAnimation(keyPath: "opacity") + anim.fromValue = 1.0 + anim.toValue = 0.0 + anim.duration = 0.3 + anim.timingFunction = CAMediaTimingFunction(name: .easeIn) + anim.fillMode = .forwards + anim.isRemovedOnCompletion = false + + layer.opacity = 0.0 + layer.add(anim, forKey: "fade") + + CATransaction.commit() + } +} From 383019b24aeff1d3a7bf11acea8e7597c6fe4a60 Mon Sep 17 00:00:00 2001 From: Moaz Ahmad Date: Thu, 26 Feb 2026 22:24:41 +0300 Subject: [PATCH 3/5] feat(core): integrate audio and visual feedback into app lifecycle Wire sound and HUD managers into TouchBarController and AppDelegate. Add user preference toggles to the Settings UI. --- .../toggleMute/Bootstrap/AppDelegate.swift | 6 +++ .../Controllers/SettingsViewController.swift | 15 +++++- .../Controllers/TouchBarController.swift | 20 ++++++-- .../Storyboard/Controllers.storyboard | 46 +++++++++++++++---- .../toggleMute/Support/Preferences.swift | 16 +++++++ 5 files changed, 89 insertions(+), 14 deletions(-) diff --git a/toggleMute/toggleMute/Bootstrap/AppDelegate.swift b/toggleMute/toggleMute/Bootstrap/AppDelegate.swift index d69e47c..09b8454 100644 --- a/toggleMute/toggleMute/Bootstrap/AppDelegate.swift +++ b/toggleMute/toggleMute/Bootstrap/AppDelegate.swift @@ -122,6 +122,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele touchBarController.configureUI() + // Pre-warm singletons so NSPanel and NSSound are ready before first toggle + DispatchQueue.main.async { + _ = SoundFeedbackManager.shared + _ = MuteHUDWindowController.shared + } + KeyboardShortcuts.onKeyDown(for: .toggleMuteShortcut) { self.touchBarController.toggleMuteState() print("start") diff --git a/toggleMute/toggleMute/Controllers/SettingsViewController.swift b/toggleMute/toggleMute/Controllers/SettingsViewController.swift index f5ec46c..df2e78e 100644 --- a/toggleMute/toggleMute/Controllers/SettingsViewController.swift +++ b/toggleMute/toggleMute/Controllers/SettingsViewController.swift @@ -13,6 +13,8 @@ class SettingsViewController: NSViewController { @IBOutlet var launchAtLoginCheckBox: NSButton! @IBOutlet weak var redMenuBarIconCheckBox: NSButton! @IBOutlet weak var redMenuBarBackgroundCheckBox: NSButton! + @IBOutlet weak var muteSoundCheckBox: NSButton! + @IBOutlet weak var showHudCheckBox: NSButton! @IBOutlet var quitButton: NSButton! @IBOutlet weak var shortcutSubView: NSView! @IBOutlet weak var versionLabel: NSTextField! @@ -65,7 +67,10 @@ class SettingsViewController: NSViewController { } else { redMenuBarIconCheckBox.state = .off } - + + muteSoundCheckBox.state = preferences.muteSoundEnabled ? .on : .off + showHudCheckBox.state = preferences.showHudEnabled ? .on : .off + let recorder = KeyboardShortcuts.RecorderCocoa(for: .toggleMuteShortcut) recorder.translatesAutoresizingMaskIntoConstraints = false @@ -131,6 +136,14 @@ class SettingsViewController: NSViewController { } + @IBAction func didTouchMuteSound(_ sender: NSButton) { + preferences.muteSoundEnabled = sender.state == .on + } + + @IBAction func didTouchShowHud(_ sender: NSButton) { + preferences.showHudEnabled = sender.state == .on + } + @IBAction func didTouchClose(_ sender: Any) { NSApplication.shared.terminate(nil) diff --git a/toggleMute/toggleMute/Controllers/TouchBarController.swift b/toggleMute/toggleMute/Controllers/TouchBarController.swift index c146df1..3581c94 100644 --- a/toggleMute/toggleMute/Controllers/TouchBarController.swift +++ b/toggleMute/toggleMute/Controllers/TouchBarController.swift @@ -54,10 +54,10 @@ class TouchBarController { if(isMuted) { defaults.set(false, forKey: "isMuted") - toggleMuteStateHard(setMute: true) + toggleMuteStateHard(setMute: true, playFeedback: false) } else { defaults.set(true, forKey: "isMuted") - toggleMuteStateHard(setMute: false) + toggleMuteStateHard(setMute: false, playFeedback: false) } } @@ -99,7 +99,7 @@ class TouchBarController { } - func toggleMuteStateHard(setMute: Bool) { + func toggleMuteStateHard(setMute: Bool, playFeedback: Bool = true) { let button = delegateController.statusItem.button isMuted = defaults.bool(forKey: "isMuted") @@ -122,6 +122,13 @@ class TouchBarController { } setNewVolume(newValue: unmuteVal) + + DispatchQueue.main.async { + if playFeedback { + SoundFeedbackManager.shared.playUnmute() + MuteHUDWindowController.shared.show(muted: false) + } + } } else if(setMute && !isMuted) { @@ -141,6 +148,13 @@ class TouchBarController { if(redMenuBarIconBackground){ button?.layer?.backgroundColor = CGColor(red: 1.0, green: 0, blue: 0 , alpha: 1.0) } + + DispatchQueue.main.async { + if playFeedback { + SoundFeedbackManager.shared.playMute() + MuteHUDWindowController.shared.show(muted: true) + } + } } diff --git a/toggleMute/toggleMute/Resources/Storyboard/Controllers.storyboard b/toggleMute/toggleMute/Resources/Storyboard/Controllers.storyboard index 3a7651d..16c526f 100644 --- a/toggleMute/toggleMute/Resources/Storyboard/Controllers.storyboard +++ b/toggleMute/toggleMute/Resources/Storyboard/Controllers.storyboard @@ -27,11 +27,11 @@ - + - + - + @@ -77,7 +77,7 @@ - + @@ -97,11 +97,11 @@ - + - + @@ -109,7 +109,7 @@ - + @@ -136,10 +136,36 @@ + + + + + + diff --git a/toggleMute/toggleMute/Support/Preferences.swift b/toggleMute/toggleMute/Support/Preferences.swift index 6a61f6f..a962109 100644 --- a/toggleMute/toggleMute/Support/Preferences.swift +++ b/toggleMute/toggleMute/Support/Preferences.swift @@ -18,6 +18,22 @@ struct Preferences { } } + var muteSoundEnabled: Bool { + get { defaults.bool(forKey: #function) } + set { + defaults.set(newValue, forKey: #function) + didChange() + } + } + + var showHudEnabled: Bool { + get { defaults.bool(forKey: #function) } + set { + defaults.set(newValue, forKey: #function) + didChange() + } + } + private var appURL: URL { Bundle.main.bundleURL } var launchAtLoginEnabled: Bool { From ff572b62574e60de49c57e6a27604e4f1b2a0017 Mon Sep 17 00:00:00 2001 From: Moaz Ahmad Date: Thu, 26 Feb 2026 22:25:54 +0300 Subject: [PATCH 4/5] build(xcode): register new swift source files Add SoundFeedbackManager and MuteHUDWindowController to PBXBuildFile, PBXFileReference, and the main build target. --- toggleMute/toggleMute.xcodeproj/project.pbxproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/toggleMute/toggleMute.xcodeproj/project.pbxproj b/toggleMute/toggleMute.xcodeproj/project.pbxproj index 87361e4..1b72e96 100644 --- a/toggleMute/toggleMute.xcodeproj/project.pbxproj +++ b/toggleMute/toggleMute.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ BD391AE9263C3F5600848049 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = BD391AE8263C3F5600848049 /* LaunchAtLogin */; }; C3C528C02DF0162800BBC393 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = C3C528BF2DF0162800BBC393 /* KeyboardShortcuts */; }; C3DA000A2B88C00900B6BCCA /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DA00092B88C00900B6BCCA /* EventMonitor.swift */; }; + MH001A0001000001 /* SoundFeedbackManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = MH001A0001000000 /* SoundFeedbackManager.swift */; }; + MH001A0002000001 /* MuteHUDWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = MH001A0002000000 /* MuteHUDWindowController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -51,6 +53,8 @@ 44EB8BD723AC2ABD005A4A0B /* TouchBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBarController.swift; sourceTree = ""; }; 44EB8BDB23AC350D005A4A0B /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; C3DA00092B88C00900B6BCCA /* EventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMonitor.swift; sourceTree = ""; }; + MH001A0001000000 /* SoundFeedbackManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundFeedbackManager.swift; sourceTree = ""; }; + MH001A0002000000 /* MuteHUDWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteHUDWindowController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -73,6 +77,7 @@ 447B1DF8236795F100587E3A /* MainController.swift */, 44EB8BDB23AC350D005A4A0B /* SettingsViewController.swift */, 44EB8BD723AC2ABD005A4A0B /* TouchBarController.swift */, + MH001A0002000000 /* MuteHUDWindowController.swift */, ); path = Controllers; sourceTree = ""; @@ -132,6 +137,7 @@ 449F398223173003008A0DBD /* Preferences.swift */, 44D551DD2390830E0065505A /* SettingsController-Bridging-Header.h */, 44EB8BD623AC28E3005A4A0B /* NSTouchBar-Private.h */, + MH001A0001000000 /* SoundFeedbackManager.swift */, ); path = Support; sourceTree = ""; @@ -274,6 +280,8 @@ 449F398D23173610008A0DBD /* SettingsController.swift in Sources */, 449F398323173003008A0DBD /* Preferences.swift in Sources */, C3DA000A2B88C00900B6BCCA /* EventMonitor.swift in Sources */, + MH001A0001000001 /* SoundFeedbackManager.swift in Sources */, + MH001A0002000001 /* MuteHUDWindowController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 09f11201226be8ebc958843a5ca4d0d9b3055791 Mon Sep 17 00:00:00 2001 From: Moaz Ahmad Date: Fri, 27 Feb 2026 12:49:24 +0300 Subject: [PATCH 5/5] feat: add adaptive light and dark mode support to HUD --- .../toggleMute/Controllers/MuteHUDWindowController.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/toggleMute/toggleMute/Controllers/MuteHUDWindowController.swift b/toggleMute/toggleMute/Controllers/MuteHUDWindowController.swift index c8772e1..2438d02 100644 --- a/toggleMute/toggleMute/Controllers/MuteHUDWindowController.swift +++ b/toggleMute/toggleMute/Controllers/MuteHUDWindowController.swift @@ -44,7 +44,7 @@ final class MuteHUDWindowController { let cfg = NSImage.SymbolConfiguration(pointSize: 72, weight: .regular) iconView.image = img.withSymbolConfiguration(cfg) } - iconView.contentTintColor = muted ? .systemRed : .white + iconView.contentTintColor = muted ? .systemRed : .labelColor label.stringValue = muted ? "Muted" : "Unmuted" positionWindow() @@ -75,7 +75,7 @@ final class MuteHUDWindowController { container.wantsLayer = true let blur = NSVisualEffectView(frame: container.bounds) - blur.material = .hudWindow + blur.material = .popover blur.blendingMode = .behindWindow blur.state = .active blur.wantsLayer = true @@ -97,7 +97,7 @@ final class MuteHUDWindowController { tf.drawsBackground = false tf.alignment = .center tf.font = .systemFont(ofSize: 18, weight: .bold) - tf.textColor = NSColor(white: 1.0, alpha: 0.85) + tf.textColor = .labelColor tf.autoresizingMask = [.minXMargin, .maxXMargin] blur.addSubview(tf) label = tf