~izzy/Tomatsupo

f891e235e82f44f659a408db99b335694133c485 — Izzy Miller a month ago ff735d2
Add support for macOS 10.15
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)