A MacOS menu bar applet that displays the current lunar phase.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

417 lines
12 KiB

//
// AppDelegate.swift
// LunaMac
//
// Created by Claire Davis on 9/21/22.
// Copyright © 2022 A Better Geek. All rights reserved.
//
/*
====== TO DO =======
* auto-update at a set interval
* make help menu item work (show the window)
* fix alignment of status bar icon (it's too high)
*/
import Cocoa
import Foundation
import CoreFoundation
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
// static array of possible lunar values
let phaseArray:[String:[String:NSString]] = [
"new" : [
"nice" : "New Moon",
"start" : "0",
"end" : "1",
"icon" : "🌑"
],
"waxc" : [
"nice" : "Waxing Crescent",
"start" : "1",
"end" : "6.38264692644",
"icon" : "🌒"
],
"firstq" : [
"nice" : "First Quarter",
"start" : "6.38264692644",
"end" : "8.38264692644",
"icon" : "🌓"
],
"waxg" : [
"nice" : "Waxing Gibbous",
"start" : "8.38264692644",
"end" : "13.76529385288",
"icon" : "🌔"
],
"full" : [
"nice" : "Full Moon",
"start" : "13.76529385288",
"end" : "15.76529385288",
"icon" : "🌕"
],
"wang" : [
"nice" : "Waning Gibbous",
"start" : "15.76529385288",
"end" : "21.14794077932",
"icon" : "🌖"
],
"lastq" : [
"nice" : "Last Quarter",
"start" : "21.14794077932",
"end" : "23.14794077932",
"icon" : "🌗"
],
"wanc" : [
"nice" : "Waning Crescent",
"start" : "23.14794077932",
"end" : "28.53058770576",
"icon" : "🌘"
],
"new2" : [
"nice" : "New Moon",
"start" : "28.53058770576",
"end" : "29.53058770576",
"icon" : "🌑"
],
"default" : [
"nice" : "Failed to calculate.",
"icon" : ""
]
]
@IBAction func clickClick(_ sender: NSButtonCell) {
// show changelog
showLog()
}
// get system-wide menu bar
let statusBar = NSStatusBar.system
// make sure applet stays in memory
var statusBarItem: NSStatusItem?
// create a menu for the icon
let statusBarMenu = NSMenu(title: "LunaMac Menu")
// create empty timer for event loop
var lunaTimer = Timer()
// is the info box open?
var infoOpen = false
// is the changelog open?
var logOpen = false
// user settings
let defaults = UserDefaults.standard
func applicationDidFinishLaunching(_ aNotification: Notification) {
// set icon size to square
statusBarItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
// build the icon and menu
updateIcon()
// listen for theme changes (dark and light)
themeListener()
// start the the timer for keeping the icon updated automatically
startTimer()
}
// start the timer for auto-updating the icon
func startTimer() {
lunaTimer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(updateIcon), userInfo: nil, repeats: true)
RunLoop.current.add(lunaTimer, forMode: RunLoop.Mode.common)
}
@objc func menuWillOpen(_ menu: NSMenu) {
lunaTimer.invalidate()
}
@objc func menuDidClose(_ menu: NSMenu) {
startTimer()
}
@objc func buildMenu(key: String = "default") {
statusBarMenu.removeAllItems()
statusBarMenu.delegate = self
// filename suffix
/*
-dark dark mode
-alt-dark dark mode inverted
-emoji emoji
*/
var suffix = ""
// if key is new2, rename to new
var key2 = key.contains("2") ? "new" : key
// if iconPref is set to emoji
if (defaults.string(forKey: "iconPref") == "emoji") {
suffix += "-emoji"
}
// otherwise use monochrome icons
else {
// check for dark mode and set the right image file
if #available(OSX 10.14, *) {
let darkMode = UserDefaults.standard.string(forKey: "AppleInterfaceStyle")
if darkMode == "Dark" {
// if theme is set to dark alt make adjustments
if (defaults.string(forKey: "iconPref") == "darkalt") {
switch (key2) {
case "new" :
key2 = "full"
break
case "full" :
key2 = "new"
break
case "firstq" :
key2 = "lastq"
break
case "lastq" :
key2 = "firstq"
break
case "wanc", "wang", "waxc", "waxg" :
suffix += "-alt"
break
default :
break
}
}
suffix += "-dark"
}
}
}
// set image filename
let imgName = key2 + suffix + ".png"
statusBarItem?.button?.image = NSImage(named: imgName)
// add the menu to the menu bar icon
statusBarItem?.menu = statusBarMenu
let topTxt = phaseArray["default"]?["nice"] as String? ?? "Failed to calculate."
// add menu items to the menu
if (!topTxt.isEmpty) {
statusBarMenu.addItem(
withTitle: phaseArray[key]?["nice"] as String? ?? topTxt,
action: nil,
keyEquivalent: ""
)
}
statusBarMenu.addItem(NSMenuItem.separator())
statusBarMenu.addItem(
withTitle: "Update moon phase",
action: #selector(AppDelegate.updateIcon),
keyEquivalent: ""
)
statusBarMenu.addItem(NSMenuItem.separator())
// create menu item to hold submenu
let prefMenu = NSMenu(title: "Prefs")
// default
let optDefault = NSMenuItem(
title: "Default",
action: #selector(setPref),
keyEquivalent: ""
)
// alternate dark
let optDarkAlt = NSMenuItem(
title: "Alternate Dark",
action: #selector(setPref),
keyEquivalent: ""
)
// emoji
let optEmoji = NSMenuItem(
title: "Emoji",
action: #selector(setPref),
keyEquivalent: ""
)
// make sure the right menu item is checked
switch (defaults.string(forKey: "iconPref")) {
case "default" :
// standard icons
optDefault.state = NSControl.StateValue.on
break
case "darkalt" :
// inverted dark mode icons
optDarkAlt.state = NSControl.StateValue.on
break
case "emoji" :
// emoji icons
optEmoji.state = NSControl.StateValue.on
break
default :
// default to standard icons
optDefault.state = NSControl.StateValue.on
break
}
prefMenu.addItem(optDefault)
prefMenu.addItem(optDarkAlt)
prefMenu.addItem(optEmoji)
let prefMenuItem = NSMenuItem(
title: "Icon Style",
action: nil,
keyEquivalent: ""
)
statusBarMenu.addItem(prefMenuItem)
statusBarMenu.setSubmenu(prefMenu, for: prefMenuItem)
statusBarMenu.addItem(
withTitle: "About LunaMac...",
action: #selector(AppDelegate.showAbout),
keyEquivalent: ""
)
statusBarMenu.addItem(
withTitle: "Quit LunaMac",
action: #selector(AppDelegate.quitApp),
keyEquivalent: ""
)
}
@objc func setPref(sender: NSMenuItem) {
// sender.title tells us what to do
switch (sender.title) {
case "Default" :
defaults.set("default", forKey: "iconPref")
break
case "Alternate Dark" :
defaults.set("darkalt", forKey: "iconPref")
break
case "Emoji" :
defaults.set("emoji", forKey: "iconPref")
break
default:
break
}
// rebuild the menu
updateIcon()
}
@objc func showAbout() {
if (!infoOpen) {
// create infobox window object
let aboutWin = NSWindowController(windowNibName: "InfoBox")
aboutWin.loadWindow()
}
NSApp.activate(ignoringOtherApps: true)
}
@objc func showLog() {
if (!logOpen) {
// create changelog window object
// this creates duplicate windows
let logWin = NSWindowController(windowNibName: "Changelog")
logWin.loadWindow()
}
NSApp.activate(ignoringOtherApps: true)
}
// update menu bar icon
@objc func updateIcon() {
// current system time in UTC
let sysDate = Date()
// get baseline date
// let baseDateStr = "2000-01-06 18:14:00 +0000"
// create date object from baseDate
// date components
var bdc = DateComponents()
bdc.year = 2000
bdc.month = 1
bdc.day = 6
bdc.hour = 18
bdc.minute = 14
// create date object
let ucal = Calendar(identifier: .gregorian)
let baseDate = ucal.date(from: bdc)
// days in a lunar cycle
let lunarDays = 29.53058770576
// seconds in a lunar cycle
let lunarSecs = lunarDays * (24 * 60 * 60)
// calculate seconds between sysDate and baseDate
// if baseDate is nil, default to current system time
let totalSecs = sysDate.timeIntervalSince(baseDate ?? Date())
if (totalSecs.sign != .minus) {
// a positive number is valid
// % calculates seconds of current cycle
let currSecs = totalSecs.truncatingRemainder(dividingBy: lunarSecs)
let currFrac = currSecs / lunarSecs
let currDays = currFrac * lunarDays
var newArr = [String:[String:NSString]]()
newArr = phaseArray.filter{
findPhase(
start: $0.value["start"]?.doubleValue ?? 0,
end: $0.value["end"]?.doubleValue ?? 0,
curr: currDays
)
}
// we want newArr[0].value which is [String:NSString]
let theKey = newArr.first?.key ?? "default"
buildMenu(key: theKey)
} else {
// if dateDiff is negative, something went wrong.
buildMenu()
}
}
@objc func findPhase(start: Double, end: Double, curr: Double) -> Bool {
if ((curr >= start) && (curr <= end)) {
return true
} else {
return false
}
}
// quit application
@objc func quitApp() {
NSApp.terminate(self)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func themeListener() {
DistributedNotificationCenter.default.addObserver(
self,
selector: #selector(updateIcon),
name: .AppleInterfaceThemeChangedNotification,
object: nil
)
}
}
extension Notification.Name {
static let AppleInterfaceThemeChangedNotification = Notification.Name("AppleInterfaceThemeChangedNotification")
}