Skip to content

Commit 76719b6

Browse files
committed
Add sound notifications
1 parent 4294347 commit 76719b6

File tree

4 files changed

+87
-3
lines changed

4 files changed

+87
-3
lines changed

IsThereNet.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
C781D4422B4592590050D04A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
2121
C781D4442B4592590050D04A /* IsThereNet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IsThereNet.entitlements; sourceTree = "<group>"; };
2222
C781D44C2B4815510050D04A /* fping */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = fping; sourceTree = "<group>"; };
23+
C7AD8E8C2C369C2E008E4182 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
2324
/* End PBXFileReference section */
2425

2526
/* Begin PBXFrameworksBuildPhase section */
@@ -52,6 +53,7 @@
5253
C781D43A2B4592580050D04A /* IsThereNet */ = {
5354
isa = PBXGroup;
5455
children = (
56+
C7AD8E8C2C369C2E008E4182 /* Info.plist */,
5557
C781D44C2B4815510050D04A /* fping */,
5658
C781D43B2B4592580050D04A /* IsThereNetApp.swift */,
5759
C781D43F2B4592590050D04A /* Assets.xcassets */,
@@ -302,9 +304,11 @@
302304
ENABLE_HARDENED_RUNTIME = YES;
303305
ENABLE_PREVIEWS = YES;
304306
GENERATE_INFOPLIST_FILE = YES;
307+
INFOPLIST_FILE = IsThereNet/Info.plist;
305308
INFOPLIST_KEY_CFBundleDisplayName = IsThereNet;
306309
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
307310
INFOPLIST_KEY_LSUIElement = YES;
311+
INFOPLIST_KEY_NSFocusStatusUsageDescription = "The app needs Focus access to pause notification sounds at appropriate times";
308312
INFOPLIST_KEY_NSHumanReadableCopyright = "";
309313
LD_RUNPATH_SEARCH_PATHS = (
310314
"$(inherited)",
@@ -333,9 +337,11 @@
333337
ENABLE_HARDENED_RUNTIME = YES;
334338
ENABLE_PREVIEWS = YES;
335339
GENERATE_INFOPLIST_FILE = YES;
340+
INFOPLIST_FILE = IsThereNet/Info.plist;
336341
INFOPLIST_KEY_CFBundleDisplayName = IsThereNet;
337342
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
338343
INFOPLIST_KEY_LSUIElement = YES;
344+
INFOPLIST_KEY_NSFocusStatusUsageDescription = "The app needs Focus access to pause notification sounds at appropriate times";
339345
INFOPLIST_KEY_NSHumanReadableCopyright = "";
340346
LD_RUNPATH_SEARCH_PATHS = (
341347
"$(inherited)",

IsThereNet/Info.plist

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict/>
5+
</plist>

IsThereNet/IsThereNet.entitlements

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<key>com.apple.developer.usernotifications.communication</key>
6+
<true/>
57
<key>com.apple.security.app-sandbox</key>
68
<true/>
79
<key>com.apple.security.files.user-selected.read-only</key>

IsThereNet/IsThereNetApp.swift

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import AppKit
99
import Cocoa
1010
import Combine
1111
import Foundation
12+
import Intents
1213
import Network
1314
import os.log
1415
import ServiceManagement
@@ -21,7 +22,7 @@ private func mainAsyncAfter(_ duration: TimeInterval, _ action: @escaping () ->
2122
return workItem
2223
}
2324

24-
private func drawColoredTopLine(_ color: NSColor, hideAfter: TimeInterval = 5) {
25+
private func drawColoredTopLine(_ color: NSColor, hideAfter: TimeInterval = 5, sound: NSSound? = nil) {
2526
DispatchQueue.main.async {
2627
lastColor = color
2728
lastHideAfter = hideAfter
@@ -54,6 +55,8 @@ private func drawColoredTopLine(_ color: NSColor, hideAfter: TimeInterval = 5) {
5455
window.fade(to: 0.7)
5556
}
5657

58+
sound?.playIfNotDND()
59+
5760
guard hideAfter > 0 else { return }
5861

5962
fader = mainAsyncAfter(hideAfter) {
@@ -68,12 +71,71 @@ private func drawColoredTopLine(_ color: NSColor, hideAfter: TimeInterval = 5) {
6871
}
6972
}
7073

74+
extension NSSound {
75+
func playIfNotDND() {
76+
if #available(macOS 12.0, *), focused {
77+
return
78+
}
79+
play()
80+
}
81+
}
82+
83+
let SONOMA_TO_ORIGINAL_SOUND_NAMES = [
84+
"Mezzo": "Basso",
85+
"Breeze": "Blow",
86+
"Pebble": "Bottle",
87+
"Jump": "Frog",
88+
"Funky": "Funk",
89+
"Crystal": "Glass",
90+
"Heroine": "Hero",
91+
"Pong": "Morse",
92+
"Sonar": "Ping",
93+
"Bubble": "Pop",
94+
"Pluck": "Purr",
95+
"Sonumi": "Sosumi",
96+
"Submerge": "Submarine",
97+
"Boop": "Tink",
98+
]
99+
100+
@available(macOS 12.0, *)
101+
var focused: Bool {
102+
guard INFocusStatusCenter.default.authorizationStatus == .authorized else {
103+
INFocusStatusCenter.default.requestAuthorization { status in
104+
log("Focus Status: \(status)")
105+
}
106+
return false
107+
}
108+
109+
return INFocusStatusCenter.default.focusStatus.isFocused ?? false
110+
}
111+
71112
private struct FadeSecondsConfig: Codable, Equatable {
72113
var connected: Double? = 5.0
73114
var disconnected: Double? = 0.0
74115
var slow: Double? = 10.0
75116
}
76117

118+
private struct SoundsConfig: Codable, Equatable {
119+
var connected: String? = "" // e.g. "Funky"
120+
var disconnected: String? = "" // e.g. "Mezzo"
121+
var slow: String? = "" // e.g. "Submerge"
122+
var volume: Float? = 0.7
123+
124+
var connectedSound: NSSound? { connected != nil ? sound(named: connected!) : nil }
125+
var disconnectedSound: NSSound? { disconnected != nil ? sound(named: disconnected!) : nil }
126+
var slowSound: NSSound? { slow != nil ? sound(named: slow!) : nil }
127+
128+
func sound(named name: String) -> NSSound? {
129+
guard let s = NSSound(named: name) ?? NSSound(named: SONOMA_TO_ORIGINAL_SOUND_NAMES[name] ?? "") else {
130+
return nil
131+
}
132+
133+
s.volume = max(min(volume ?? 0.7, 1.0), 0.0)
134+
return s
135+
}
136+
137+
}
138+
77139
private enum PingStatus: Equatable {
78140
case reachable(Double)
79141
case timedOut
@@ -87,6 +149,14 @@ private enum PingStatus: Equatable {
87149
}
88150
}
89151

152+
var sound: NSSound? {
153+
switch self {
154+
case .reachable: CONFIG.sounds?.connectedSound
155+
case .timedOut: CONFIG.sounds?.disconnectedSound
156+
case .slow: CONFIG.sounds?.slowSound
157+
}
158+
}
159+
90160
var hideAfter: TimeInterval {
91161
switch self {
92162
case .reachable: CONFIG.fadeSeconds?.connected ?? 5
@@ -122,7 +192,7 @@ private var lastPingStatus: PingStatus? {
122192
didSet {
123193
guard let lastPingStatus, lastPingStatus != oldValue else { return }
124194

125-
drawColoredTopLine(lastPingStatus.color, hideAfter: lastPingStatus.hideAfter)
195+
drawColoredTopLine(lastPingStatus.color, hideAfter: lastPingStatus.hideAfter, sound: lastPingStatus.sound)
126196
log("Internet connection: \(lastPingStatus.message)")
127197
}
128198
}
@@ -198,7 +268,7 @@ func start() {
198268
process = nil
199269
pingRestartTask = nil
200270
}
201-
drawColoredTopLine(.systemRed, hideAfter: 0)
271+
drawColoredTopLine(.systemRed, hideAfter: CONFIG.fadeSeconds?.disconnected ?? 0, sound: CONFIG.sounds?.disconnectedSound)
202272
@unknown default:
203273
log("Internet connection: \(path.status)")
204274
}
@@ -344,6 +414,7 @@ private struct Config: Codable, Equatable {
344414
var pingSlowThresholdMilliseconds = 300.0
345415

346416
var fadeSeconds: FadeSecondsConfig? = FadeSecondsConfig()
417+
var sounds: SoundsConfig? = SoundsConfig()
347418
}
348419

349420
private var CONFIG_FS_WATCHER: FSEventStreamRef?

0 commit comments

Comments
 (0)