diff --git a/Kit/module/notifications.swift b/Kit/module/notifications.swift index 534a4bcf..0d9add09 100644 --- a/Kit/module/notifications.swift +++ b/Kit/module/notifications.swift @@ -38,6 +38,12 @@ open class NotificationsWrapper: NSStackView { fatalError("init(coder:) has not been implemented") } + public func willTerminate() { + for id in self.ids { + removeNotification(id.key) + } + } + public func initIDs(_ ids: [String]) { for id in ids { let notificationID = "Stats_\(self.module)_\(id)" diff --git a/Kit/types.swift b/Kit/types.swift index 38166f1a..f870625d 100644 --- a/Kit/types.swift +++ b/Kit/types.swift @@ -255,6 +255,8 @@ public var isARM: Bool { public let notificationLevels: [KeyValue_t] = [ KeyValue_t(key: "", value: "Disabled"), + KeyValue_t(key: "0.03", value: "3%"), + KeyValue_t(key: "0.05", value: "5%"), KeyValue_t(key: "0.1", value: "10%"), KeyValue_t(key: "0.15", value: "15%"), KeyValue_t(key: "0.2", value: "20%"), diff --git a/Modules/Battery/main.swift b/Modules/Battery/main.swift index edd74329..de285d53 100644 --- a/Modules/Battery/main.swift +++ b/Modules/Battery/main.swift @@ -49,6 +49,7 @@ public class Battery: Module { private let popupView: Popup private let settingsView: Settings private let portalView: Portal + private let notificationsView: Notifications private var usageReader: UsageReader? = nil private var processReader: ProcessReader? = nil @@ -61,11 +62,13 @@ public class Battery: Module { self.settingsView = Settings("Battery") self.popupView = Popup("Battery") self.portalView = Portal("Battery") + self.notificationsView = Notifications(.battery) super.init( popup: self.popupView, settings: self.settingsView, - portal: self.portalView + portal: self.portalView, + notifications: self.notificationsView ) guard self.available else { return } @@ -107,10 +110,7 @@ public class Battery: Module { public override func willTerminate() { guard self.isAvailable() else { return } - - if let id = self.notificationID { - removeNotification(id) - } + self.notificationsView.willTerminate() } public override func isAvailable() -> Bool { @@ -122,10 +122,9 @@ public class Battery: Module { private func usageCallback(_ raw: Battery_Usage?) { guard let value = raw, self.enabled else { return } - self.checkLowNotification(value: value) - self.checkHighNotification(value: value) self.popupView.usageCallback(value) self.portalView.loadCallback(value) + self.notificationsView.usageCallback(value) self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in switch w.item { @@ -152,74 +151,4 @@ public class Battery: Module { } } } - - private func checkLowNotification(value: Battery_Usage) { - let level = Store.shared.string(key: "\(self.config.name)_lowLevelNotification", defaultValue: "0.15") - if level == "Disabled" { - return - } - - guard let notificationLevel = Double(level) else { - return - } - - if (value.level > notificationLevel || !value.isBatteryPowered) && self.lowLevelNotificationState { - if value.level > notificationLevel { - if let id = self.notificationID { - removeNotification(id) - self.notificationID = nil - } - self.lowLevelNotificationState = false - } - return - } - - if value.isCharging { - return - } - - if value.level <= notificationLevel && !self.lowLevelNotificationState { - var subtitle = localizedString("Battery remaining", "\(Int(value.level*100))") - if value.timeToEmpty > 0 { - subtitle += " (\(Double(value.timeToEmpty*60).printSecondsToHoursMinutesSeconds()))" - } - self.notificationID = showNotification(title: localizedString("Low battery"), subtitle: subtitle) - self.lowLevelNotificationState = true - } - } - - private func checkHighNotification(value: Battery_Usage) { - let level = Store.shared.string(key: "\(self.config.name)_highLevelNotification", defaultValue: "Disabled") - if level == "Disabled" { - return - } - - guard let notificationLevel = Double(level) else { - return - } - - if (value.level < notificationLevel || value.isBatteryPowered) && self.highLevelNotificationState { - if value.level < notificationLevel { - if let id = self.notificationID { - removeNotification(id) - self.notificationID = nil - } - self.highLevelNotificationState = false - } - return - } - - if !value.isCharging { - return - } - - if value.level >= notificationLevel && !self.highLevelNotificationState { - var subtitle = localizedString("Battery remaining to full charge", "\(Int((1-value.level)*100))") - if value.timeToCharge > 0 { - subtitle += " (\(Double(value.timeToCharge*60).printSecondsToHoursMinutesSeconds()))" - } - self.notificationID = showNotification(title: localizedString("High battery"), subtitle: subtitle) - self.highLevelNotificationState = true - } - } } diff --git a/Modules/Battery/notifications.swift b/Modules/Battery/notifications.swift new file mode 100644 index 00000000..b3a27e97 --- /dev/null +++ b/Modules/Battery/notifications.swift @@ -0,0 +1,87 @@ +// +// notifications.swift +// Battery +// +// Created by Serhiy Mytrovtsiy on 17/12/2023 +// Using Swift 5.0 +// Running on macOS 14.2 +// +// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa +import Kit + +class Notifications: NotificationsWrapper { + private let lowID: String = "low" + private let highID: String = "high" + private var lowLevel: String = "" + private var highLevel: String = "" + + public init(_ module: ModuleType) { + super.init(module, [self.lowID, self.highID]) + + if Store.shared.exist(key: "\(self.module)_lowLevelNotification") { + let value = Store.shared.string(key: "\(self.module)_lowLevelNotification", defaultValue: self.lowID) + Store.shared.set(key: "\(self.module)_notifications_low", value: value) + Store.shared.remove("\(self.module)_lowLevelNotification") + } + if Store.shared.exist(key: "\(self.module)_highLevelNotification") { + let value = Store.shared.string(key: "\(self.module)_highLevelNotification", defaultValue: self.highLevel) + Store.shared.set(key: "\(self.module)_notifications_high", value: value) + Store.shared.remove("\(self.module)_highLevelNotification") + } + + self.lowLevel = Store.shared.string(key: "\(self.module)_notifications_low", defaultValue: self.lowLevel) + self.highLevel = Store.shared.string(key: "\(self.module)_notifications_high", defaultValue: self.highLevel) + + self.addArrangedSubview(selectSettingsRow( + title: localizedString("Low level notification"), + action: #selector(self.changeLowLevel), + items: notificationLevels, + selected: self.lowLevel + )) + self.addArrangedSubview(selectSettingsRow( + title: localizedString("High level notification"), + action: #selector(self.changeHighLevel), + items: notificationLevels, + selected: self.highLevel + )) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + internal func usageCallback(_ value: Battery_Usage) { + if let threshold = Double(self.lowLevel) { + let title = localizedString("Low battery") + var subtitle = localizedString("Battery remaining", "\(Int(value.level*100))") + if value.timeToEmpty > 0 { + subtitle += " (\(Double(value.timeToEmpty*60).printSecondsToHoursMinutesSeconds()))" + } + self.checkDouble(id: self.lowID, value: value.level, threshold: threshold, title: title, subtitle: subtitle, less: true) + } + + if let threshold = Double(self.highLevel) { + let title = localizedString("High battery") + var subtitle = localizedString("Battery remaining to full charge", "\(Int((1-value.level)*100))") + if value.timeToCharge > 0 { + subtitle += " (\(Double(value.timeToCharge*60).printSecondsToHoursMinutesSeconds()))" + } + self.checkDouble(id: self.lowID, value: value.level, threshold: threshold, title: title, subtitle: subtitle) + } + } + + @objc private func changeLowLevel(_ sender: NSMenuItem) { + guard let key = sender.representedObject as? String else { return } + self.lowLevel = key.isEmpty ? "" : "\(Double(key) ?? 0)" + Store.shared.set(key: "\(self.module)_notifications_low", value: self.lowLevel) + } + + @objc private func changeHighLevel(_ sender: NSMenuItem) { + guard let key = sender.representedObject as? String else { return } + self.highLevel = key.isEmpty ? "" : "\(Double(key) ?? 0)" + Store.shared.set(key: "\(self.module)_notifications_high", value: self.highLevel) + } +} diff --git a/Modules/Battery/readers.swift b/Modules/Battery/readers.swift index 3f1c7f1d..18f19ca8 100644 --- a/Modules/Battery/readers.swift +++ b/Modules/Battery/readers.swift @@ -161,7 +161,6 @@ public class ProcessReader: Reader<[TopProcess]> { } let task = Process() - task.launchPath = "/bin/ps" task.launchPath = "/usr/bin/top" task.arguments = ["-o", "power", "-l", "2", "-n", "\(self.numberOfProcesses)", "-stats", "pid,command,power"] diff --git a/Modules/Battery/settings.swift b/Modules/Battery/settings.swift index 5e1202be..b66fbbae 100644 --- a/Modules/Battery/settings.swift +++ b/Modules/Battery/settings.swift @@ -21,42 +21,6 @@ internal class Settings: NSStackView, Settings_v { private var button: NSPopUpButton? private var numberOfProcesses: Int = 8 - private let lowLevelsList: [KeyValue_t] = [ - KeyValue_t(key: "Disabled", value: "Disabled"), - KeyValue_t(key: "3%", value: "3%"), - KeyValue_t(key: "5%", value: "5%"), - KeyValue_t(key: "10%", value: "10%"), - KeyValue_t(key: "15%", value: "15%"), - KeyValue_t(key: "20%", value: "20%"), - KeyValue_t(key: "25%", value: "25%"), - KeyValue_t(key: "30%", value: "30%"), - KeyValue_t(key: "40%", value: "40%"), - KeyValue_t(key: "50%", value: "50%"), - KeyValue_t(key: "60%", value: "60%") - ] - private let highLevelsList: [KeyValue_t] = [ - KeyValue_t(key: "Disabled", value: "Disabled"), - KeyValue_t(key: "50%", value: "50%"), - KeyValue_t(key: "60%", value: "60%"), - KeyValue_t(key: "70%", value: "70%"), - KeyValue_t(key: "75%", value: "75%"), - KeyValue_t(key: "80%", value: "80%"), - KeyValue_t(key: "85%", value: "85%"), - KeyValue_t(key: "90%", value: "90%"), - KeyValue_t(key: "95%", value: "95%"), - KeyValue_t(key: "97%", value: "97%"), - KeyValue_t(key: "100%", value: "100%") - ] - private var lowLevelNotification: String { - get { - return Store.shared.string(key: "\(self.title)_lowLevelNotification", defaultValue: "0.15") - } - } - private var highLevelNotification: String { - get { - return Store.shared.string(key: "\(self.title)_highLevelNotification", defaultValue: "Disabled") - } - } private var timeFormat: String = "short" public init(_ title: String) { @@ -84,20 +48,6 @@ internal class Settings: NSStackView, Settings_v { public func load(widgets: [widget_t]) { self.subviews.forEach{ $0.removeFromSuperview() } - self.addArrangedSubview(selectSettingsRow( - title: localizedString("Low level notification"), - action: #selector(changeUpdateIntervalLow), - items: self.lowLevelsList, - selected: self.lowLevelNotification == "Disabled" ? self.lowLevelNotification : "\(Int((Double(self.lowLevelNotification) ?? 0)*100))%" - )) - - self.addArrangedSubview(selectSettingsRow( - title: localizedString("High level notification"), - action: #selector(changeUpdateIntervalHigh), - items: self.highLevelsList, - selected: self.highLevelNotification == "Disabled" ? self.highLevelNotification : "\(Int((Double(self.highLevelNotification) ?? 0)*100))%" - )) - self.addArrangedSubview(selectSettingsRowV1( title: localizedString("Number of top processes"), action: #selector(changeNumberOfProcesses), @@ -115,30 +65,6 @@ internal class Settings: NSStackView, Settings_v { } } - @objc private func changeUpdateIntervalLow(_ sender: NSMenuItem) { - guard let key = sender.representedObject as? String else { - return - } - - if key == "Disabled" { - Store.shared.set(key: "\(self.title)_lowLevelNotification", value: key) - } else if let value = Double(key.replacingOccurrences(of: "%", with: "")) { - Store.shared.set(key: "\(self.title)_lowLevelNotification", value: "\(value/100)") - } - } - - @objc private func changeUpdateIntervalHigh(_ sender: NSMenuItem) { - guard let key = sender.representedObject as? String else { - return - } - - if key == "Disabled" { - Store.shared.set(key: "\(self.title)_highLevelNotification", value: key) - } else if let value = Double(key.replacingOccurrences(of: "%", with: "")) { - Store.shared.set(key: "\(self.title)_highLevelNotification", value: "\(value/100)") - } - } - @objc private func changeNumberOfProcesses(_ sender: NSMenuItem) { if let value = Int(sender.title) { self.numberOfProcesses = value diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index a185050c..babe2b4a 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 5C23BC1029A3B5AE00DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0F29A3B5AE00DBA990 /* portal.swift */; }; 5C5647F82A3F6B100098FFE9 /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5647F72A3F6B100098FFE9 /* Telemetry.swift */; }; 5C8E001029269C7F0027C75A /* protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE493829265055000F2856 /* protocol.swift */; }; + 5CD342F42B2F2FB700225631 /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD342F32B2F2FB700225631 /* notifications.swift */; }; 5CF2210D2B1E7EAF006C583F /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2210C2B1E7EAF006C583F /* notifications.swift */; }; 5CF221132B1E8078006C583F /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF221122B1E8078006C583F /* notifications.swift */; }; 5CF221152B1F4792006C583F /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF221142B1F4792006C583F /* notifications.swift */; }; @@ -400,6 +401,7 @@ 5C23BC0F29A3B5AE00DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; }; 5C5647F72A3F6B100098FFE9 /* Telemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Telemetry.swift; sourceTree = ""; }; 5C9F90A02A76B30500D41748 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; + 5CD342F32B2F2FB700225631 /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; }; 5CF2210C2B1E7EAF006C583F /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; }; 5CF221122B1E8078006C583F /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; }; 5CF221142B1F4792006C583F /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; }; @@ -993,6 +995,7 @@ 5EE8037E29C36BDD0063D37D /* portal.swift */, 9ABFF913248C30A800C9041A /* popup.swift */, 9AD64FA124BF86C100419D59 /* settings.swift */, + 5CD342F32B2F2FB700225631 /* notifications.swift */, 9ABFF8F9248BEBCB00C9041A /* Info.plist */, 9ABFF904248BEC0B00C9041A /* config.plist */, ); @@ -1426,7 +1429,7 @@ New, ); LastSwiftUpdateCheck = 1410; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = "Serhiy Mytrovtsiy"; TargetAttributes = { 5C22299C29CCB3C400F00E69 = { @@ -1859,6 +1862,7 @@ 9ABFF914248C30A800C9041A /* popup.swift in Sources */, 5EE8037F29C36BDD0063D37D /* portal.swift in Sources */, 9AD64FA224BF86C100419D59 /* settings.swift in Sources */, + 5CD342F42B2F2FB700225631 /* notifications.swift in Sources */, 9ABFF903248BEBD700C9041A /* main.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Stats.xcodeproj/xcshareddata/xcschemes/SMC.xcscheme b/Stats.xcodeproj/xcshareddata/xcschemes/SMC.xcscheme index f16ea553..421837a1 100644 --- a/Stats.xcodeproj/xcshareddata/xcschemes/SMC.xcscheme +++ b/Stats.xcodeproj/xcshareddata/xcschemes/SMC.xcscheme @@ -1,6 +1,6 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 515 + 516 Description Simple macOS system monitor in your menu bar LSApplicationCategoryType