M Tomatsupo.xcodeproj/project.pbxproj => Tomatsupo.xcodeproj/project.pbxproj +6 -2
@@ 14,6 14,7 @@
83EBE75225A42BCF002C9CBF /* DoNotDisturb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBE75025A42BCE002C9CBF /* DoNotDisturb.swift */; };
83EBE75725A44F39002C9CBF /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBE75625A44F39002C9CBF /* Model.swift */; };
83EBE76125A4644E002C9CBF /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBE76025A4644E002C9CBF /* Logger.swift */; };
+ 83EBE77525A4DFE6002C9CBF /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBE77425A4DFE6002C9CBF /* main.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ 29,6 30,7 @@
83EBE76025A4644E002C9CBF /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
83EBE76425A469D0002C9CBF /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
83EBE76525A46A1A002C9CBF /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
+ 83EBE77425A4DFE6002C9CBF /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ 65,6 67,7 @@
children = (
83EBE75025A42BCE002C9CBF /* DoNotDisturb.swift */,
83EBE73C25A422B3002C9CBF /* TomatsupoApp.swift */,
+ 83EBE77425A4DFE6002C9CBF /* main.swift */,
83EBE75625A44F39002C9CBF /* Model.swift */,
83EBE76025A4644E002C9CBF /* Logger.swift */,
83EBE73E25A422B3002C9CBF /* ContentView.swift */,
@@ 155,6 158,7 @@
files = (
83EBE75725A44F39002C9CBF /* Model.swift in Sources */,
83EBE76125A4644E002C9CBF /* Logger.swift in Sources */,
+ 83EBE77525A4DFE6002C9CBF /* main.swift in Sources */,
83EBE73F25A422B3002C9CBF /* ContentView.swift in Sources */,
83EBE75225A42BCF002C9CBF /* DoNotDisturb.swift in Sources */,
83EBE73D25A422B3002C9CBF /* TomatsupoApp.swift in Sources */,
@@ 297,7 301,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MACOSX_DEPLOYMENT_TARGET = 11.0;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = fm.stardust.app.Tomatsupo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
@@ 322,7 326,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MACOSX_DEPLOYMENT_TARGET = 11.0;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = fm.stardust.app.Tomatsupo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
M Tomatsupo/DoNotDisturb.swift => Tomatsupo/DoNotDisturb.swift +113 -15
@@ 9,18 9,33 @@
import Foundation
import Cocoa
-public struct DoNotDisturb {
- enum DoNotDisturbState {
- case enabled
- case original
- case disabled
+public enum DoNotDisturbState {
+ case enabled
+ case original
+ case disabled
+}
+
+public protocol DoNotDisturb {
+ var doNotDisturb: DoNotDisturbState {get set}
+}
+
+public class DoNotDisturbGenerator {
+ class func make() -> DoNotDisturb {
+ if #available(macOS 11, *) {
+ return DoNotDisturbBigSur()
+ } else {
+ return DoNotDisturbCatalina()
+ }
}
-
+}
+
+public struct DoNotDisturbBigSur: DoNotDisturb {
+
private var preservePrevious = UserDefaults(suiteName: "com.apple.ncprefs.plist")!.data(forKey: "dnd_prefs")!.hexEncodedString(options: Data.HexEncodingOptions.upperCase)
private let dndEnabled = "62706C6973743030D60102030405060708080A08085B646E644D6972726F7265645F100F646E64446973706C6179536C6565705F101E72657065617465644661636574696D6543616C6C73427265616B73444E445875736572507265665E646E64446973706C61794C6F636B5F10136661636574696D6543616E427265616B444E44090808D30B0C0D070F1057656E61626C6564546461746556726561736F6E093341C2B41C4FC9D3891001080808152133545D6C828384858C9499A0A1AAACAD00000000000001010000000000000013000000000000000000000000000000AE"
- var doNotDisturb: DoNotDisturbState {
+ public var doNotDisturb: DoNotDisturbState {
get {
return shell("defaults read com.apple.ncprefs.plist dnd_prefs").contains("000000ae") ? .enabled : .disabled
}
@@ 41,18 56,101 @@ extension Data {
let rawValue: Int
static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
}
-
+
func hexEncodedString(options: HexEncodingOptions = []) -> String {
let hexDigits = options.contains(.upperCase) ? "0123456789ABCDEF" : "0123456789abcdef"
- let utf8Digits = Array(hexDigits.utf8)
- return String(unsafeUninitializedCapacity: 2 * count) { (ptr) -> Int in
- var p = ptr.baseAddress!
+ if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) {
+ let utf8Digits = Array(hexDigits.utf8)
+ return String(unsafeUninitializedCapacity: 2 * count) { (ptr) -> Int in
+ var p = ptr.baseAddress!
+ for byte in self {
+ p[0] = utf8Digits[Int(byte / 16)]
+ p[1] = utf8Digits[Int(byte % 16)]
+ p += 2
+ }
+ return 2 * count
+ }
+ } else {
+ let utf16Digits = Array(hexDigits.utf16)
+ var chars: [unichar] = []
+ chars.reserveCapacity(2 * count)
for byte in self {
- p[0] = utf8Digits[Int(byte / 16)]
- p[1] = utf8Digits[Int(byte % 16)]
- p += 2
+ chars.append(utf16Digits[Int(byte / 16)])
+ chars.append(utf16Digits[Int(byte % 16)])
+ }
+ return String(utf16CodeUnits: chars, count: chars.count)
+ }
+ }
+}
+
+// The following code is Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
+// and used under the MIT license - https://github.com/sindresorhus/do-not-disturb
+public struct DoNotDisturbCatalina: DoNotDisturb{
+ private static let appId = "com.apple.notificationcenterui" as CFString
+
+ private func set(_ key: String, value: CFPropertyList?) {
+ CFPreferencesSetValue(key as CFString, value, DoNotDisturbCatalina.appId, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)
+ }
+
+ private func commitChanges() {
+ CFPreferencesSynchronize(DoNotDisturbCatalina.appId, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)
+ DistributedNotificationCenter.default().postNotificationName(NSNotification.Name("com.apple.notificationcenterui.dndprefs_changed"), object: nil, deliverImmediately: true)
+ }
+
+ private func restartNotificationCenter() {
+ NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.notificationcenterui").first?.forceTerminate()
+ }
+
+ private func enable() {
+ guard doNotDisturb == .disabled else {
+ return
+ }
+
+ set("doNotDisturb", value: true as CFPropertyList)
+ set("doNotDisturbDate", value: Date() as CFPropertyList)
+ commitChanges()
+ restartNotificationCenter()
+ }
+
+ private func disable() {
+ guard doNotDisturb == .enabled else {
+ return
+ }
+
+ set("doNotDisturb", value: false as CFPropertyList)
+ set("doNotDisturbDate", value: nil)
+ commitChanges()
+ restartNotificationCenter()
+ restoreMenubarIcon()
+ }
+
+ private func restoreMenubarIcon() {
+ set("dndStart", value: 0 as CFPropertyList)
+ set("dndEnd", value: 1440 as CFPropertyList)
+
+ // We need to sleep for a little bit, otherwise it doesn't take effect.
+ // It works with 0.3, but not with 0.2, so we're using 0.4 just to be sure.
+ usleep(useconds_t(0.4 * Double(USEC_PER_SEC)))
+
+ set("dndStart", value: nil)
+ set("dndEnd", value: nil)
+ commitChanges()
+ }
+
+ public var doNotDisturb: DoNotDisturbState {
+ get {
+ let state = CFPreferencesGetAppBooleanValue("doNotDisturb" as CFString, DoNotDisturbCatalina.appId, nil)
+ return state ? .enabled : .disabled
+ }
+ set {
+ switch newValue {
+ case .enabled:
+ enable()
+ case .disabled:
+ disable()
+ case .original:
+ disable() //TODO
}
- return 2 * count
}
}
}
M Tomatsupo/Logger.swift => Tomatsupo/Logger.swift +1 -0
@@ 8,6 8,7 @@
import Foundation
import os.log
+@available(OSX 11.0, *)
public extension Logger {
private static var subsystem = Bundle.main.bundleIdentifier!
static let notifications = Logger(subsystem: subsystem, category: "notifications")
M Tomatsupo/Model.swift => Tomatsupo/Model.swift +5 -1
@@ 35,7 35,11 @@ struct Model {
case .seconds(let dist):
return dist
default:
- Logger.async.info("Unhandled case in remainingSeconds")
+ if #available(OSX 11.0, *) {
+ Logger.async.info("Unhandled case in remainingSeconds")
+ } else {
+ // TODO: Logging on Catalina
+ }
return nil
}
}
M Tomatsupo/TomatsupoApp.swift => Tomatsupo/TomatsupoApp.swift +13 -5
@@ 8,12 8,11 @@
// - Add a cute window?
// - Menu bar icon color change?
-
import SwiftUI
import UserNotifications
import os.log
-@main
+@available(OSX 11.0, *)
struct TomatsupoApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
@@ 30,10 29,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var model = Model()
var workItem, dndWhileWorkingItem: NSMenuItem?
- var dnd: DoNotDisturb = DoNotDisturb()
+ var dnd: DoNotDisturb = DoNotDisturbGenerator.make()
var workTask: DispatchWorkItem?
func applicationDidFinishLaunching(_ notification: Notification) {
+
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
if let button = statusItem?.button {
@@ 108,7 108,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error {
- Logger.notifications.warning("Well that sucks - you didn't give us permission to notify you (\(error.localizedDescription)")
+ if #available(OSX 11.0, *) {
+ Logger.notifications.warning("Well that sucks - you didn't give us permission to notify you (\(error.localizedDescription)")
+ } else {
+ // Todo - earlier logging
+ }
return
}
}
@@ 126,7 130,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
notificationCenter.add(request, withCompletionHandler: { error in
if let error = error {
- Logger.notifications.error("Error notifying: \(error.localizedDescription)")
+ if #available(OSX 11.0, *) {
+ Logger.notifications.error("Error notifying: \(error.localizedDescription)")
+ } else {
+ // TODO: Logging on older OS versions
+ }
}
})
}
A Tomatsupo/main.swift => Tomatsupo/main.swift +15 -0
@@ 0,0 1,15 @@
+//
+// main.swift
+// Tomatsupo
+//
+// Created by Izzy Miller on 1/5/21.
+//
+
+import Foundation
+import AppKit
+
+let app = NSApplication.shared
+let delegate = AppDelegate()
+app.delegate = delegate
+
+_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)