diff --git a/Kit/extensions.swift b/Kit/extensions.swift
index 0c2d7466..e708f738 100644
--- a/Kit/extensions.swift
+++ b/Kit/extensions.swift
@@ -593,3 +593,19 @@ public extension Data {
return withUnsafeBytes { $0.load(as: sockaddr_in.self) }
}
}
+
+public extension Date {
+ func convertToTimeZone(_ timeZone: TimeZone) -> Date {
+ return addingTimeInterval(TimeInterval(timeZone.secondsFromGMT(for: self) - TimeZone.current.secondsFromGMT(for: self)))
+ }
+}
+
+public extension TimeZone {
+ init(fromUTC: String) {
+ if let utc = Int(fromUTC), let tz = TimeZone(secondsFromGMT: utc*3600) {
+ self = tz
+ } else {
+ self = TimeZone.current
+ }
+ }
+}
diff --git a/Modules/Clock/config.plist b/Modules/Clock/config.plist
new file mode 100644
index 00000000..ed007513
--- /dev/null
+++ b/Modules/Clock/config.plist
@@ -0,0 +1,36 @@
+
+
+
+
+ Name
+ Clock
+ State
+
+ Symbol
+ clock.fill
+ Widgets
+
+ label
+
+ Default
+
+ Title
+ CLK
+ Order
+ 0
+
+ sensors
+
+ Default
+
+ Preview
+
+ Values
+ 23.03.2023 13:45
+
+ Order
+ 1
+
+
+
+
diff --git a/Modules/Clock/main.swift b/Modules/Clock/main.swift
new file mode 100644
index 00000000..ce7dd125
--- /dev/null
+++ b/Modules/Clock/main.swift
@@ -0,0 +1,133 @@
+//
+// main.swift
+// Clock
+//
+// Created by Serhiy Mytrovtsiy on 23/03/2023
+// Using Swift 5.0
+// Running on macOS 13.2
+//
+// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
+//
+
+import Foundation
+import Kit
+
+public struct Clock_t: Codable {
+ public var id: String = UUID().uuidString
+ public var enabled: Bool = true
+
+ public var name: String
+ public var format: String
+ public var tz: String
+
+ public var value: Date? = nil
+
+ public func formatted() -> String {
+ let formatter = DateFormatter()
+ formatter.dateFormat = self.format
+ formatter.timeZone = TimeZone(fromUTC: self.tz)
+ return formatter.string(from: self.value ?? Date())
+ }
+}
+
+internal class ClockReader: Reader {
+ public override func read() {
+ self.callback(Date())
+ }
+}
+
+public class Clock: Module {
+ private let popupView: Popup = Popup()
+ private let settingsView: Settings = Settings()
+
+ private var reader: ClockReader = ClockReader()
+
+ private var list: [Clock_t] {
+ if let objects = Store.shared.data(key: "\(Clock.title)_list") {
+ let decoder = JSONDecoder()
+ if let objectsDecoded = try? decoder.decode(Array.self, from: objects) as [Clock_t] {
+ return objectsDecoded
+ }
+ }
+ return [Clock.local]
+ }
+
+ public init() {
+ super.init(
+ popup: self.popupView,
+ settings: self.settingsView
+ )
+ guard self.available else { return }
+
+ self.reader.callbackHandler = { [unowned self] value in
+ guard let value else { return }
+ self.callback(value)
+ }
+
+ self.addReader(self.reader)
+ self.reader.readyCallback = { [unowned self] in
+ self.readyHandler()
+ }
+ }
+
+ private func callback(_ value: Date) {
+ var clocks: [Clock_t] = self.list.filter({ $0.enabled })
+ var list: [Stack_t] = []
+
+ for (i, c) in clocks.enumerated() {
+ clocks[i].value = value
+ list.append(Stack_t(key: c.name, value: clocks[i].formatted()))
+ }
+
+ DispatchQueue.main.async(execute: {
+ self.popupView.callback(clocks)
+ })
+
+ self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in
+ switch w.item {
+ case let widget as SensorsWidget: widget.setValues(list)
+ default: break
+ }
+ }
+ }
+}
+
+extension Clock {
+ static let title: String = "Clock"
+ static var local: Clock_t {
+ Clock_t(name: localizedString("Local time"), format: "YYYY-MM-DD HH:mm:ss", tz: "local")
+ }
+ static var zones: [KeyValue_t] {
+ [
+ KeyValue_t(key: "local", value: "Local"),
+ KeyValue_t(key: "separator", value: "separator"),
+ KeyValue_t(key: "-12", value: "UTC-12:00"),
+ KeyValue_t(key: "-11", value: "UTC-11:00"),
+ KeyValue_t(key: "-10", value: "UTC-10:00"),
+ KeyValue_t(key: "-9", value: "UTC-9:00"),
+ KeyValue_t(key: "-8", value: "UTC-8:00"),
+ KeyValue_t(key: "-7", value: "UTC-7:00"),
+ KeyValue_t(key: "-6", value: "UTC-6:00"),
+ KeyValue_t(key: "-5", value: "UTC-5:00"),
+ KeyValue_t(key: "-4", value: "UTC-4:00"),
+ KeyValue_t(key: "-3", value: "UTC-3:00"),
+ KeyValue_t(key: "-2", value: "UTC-2:00"),
+ KeyValue_t(key: "-1", value: "UTC-1:00"),
+ KeyValue_t(key: "0", value: "UTC"),
+ KeyValue_t(key: "1", value: "UTC+1:00"),
+ KeyValue_t(key: "2", value: "UTC+2:00"),
+ KeyValue_t(key: "3", value: "UTC+3:00"),
+ KeyValue_t(key: "4", value: "UTC+4:00"),
+ KeyValue_t(key: "5", value: "UTC+5:00"),
+ KeyValue_t(key: "6", value: "UTC+6:00"),
+ KeyValue_t(key: "7", value: "UTC+7:00"),
+ KeyValue_t(key: "8", value: "UTC+8:00"),
+ KeyValue_t(key: "9", value: "UTC+9:00"),
+ KeyValue_t(key: "10", value: "UTC+10:00"),
+ KeyValue_t(key: "11", value: "UTC+11:00"),
+ KeyValue_t(key: "12", value: "UTC+12:00"),
+ KeyValue_t(key: "13", value: "UTC+13:00"),
+ KeyValue_t(key: "14", value: "UTC+14:00")
+ ]
+ }
+}
diff --git a/Modules/Clock/popup.swift b/Modules/Clock/popup.swift
new file mode 100644
index 00000000..f349de7e
--- /dev/null
+++ b/Modules/Clock/popup.swift
@@ -0,0 +1,215 @@
+//
+// popup.swift
+// Clock
+//
+// Created by Serhiy Mytrovtsiy on 24/03/2023
+// Using Swift 5.0
+// Running on macOS 13.2
+//
+// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
+//
+
+import Cocoa
+import Kit
+
+internal class Popup: PopupWrapper {
+ public init() {
+ super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
+
+ self.orientation = .vertical
+ self.spacing = Constants.Popup.margins
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ internal func callback(_ list: [Clock_t]) {
+ defer {
+ let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing
+ if h > 0 && self.frame.size.height != h {
+ self.setFrameSize(NSSize(width: self.frame.width, height: h))
+ self.sizeCallback?(self.frame.size)
+ }
+ }
+
+ var views = self.subviews.filter{ $0 is ClockView }.compactMap{ $0 as? ClockView }
+ if list.count < views.count && !views.isEmpty {
+ views.forEach{ $0.removeFromSuperview() }
+ views = []
+ }
+
+ list.forEach { (c: Clock_t) in
+ if let view = views.first(where: { $0.clock.id == c.id }) {
+ view.update(c)
+ } else {
+ self.addArrangedSubview(ClockView(width: self.frame.width, clock: c))
+ }
+ }
+ }
+}
+
+private class ClockView: NSStackView {
+ public var clock: Clock_t
+
+ open override var intrinsicContentSize: CGSize {
+ return CGSize(width: self.bounds.width, height: self.bounds.height)
+ }
+
+ private var ready: Bool = false
+
+ private let clockView: ClockChart = ClockChart()
+ private let nameField: NSTextField = TextView()
+ private let timeField: NSTextField = TextView()
+
+ init(width: CGFloat, clock: Clock_t) {
+ self.clock = clock
+
+ super.init(frame: NSRect(x: 0, y: 0, width: width, height: 44))
+
+ self.orientation = .horizontal
+ self.spacing = 5
+ self.edgeInsets = NSEdgeInsets(
+ top: 5,
+ left: 5,
+ bottom: 5,
+ right: 5
+ )
+ self.wantsLayer = true
+ self.layer?.cornerRadius = 2
+
+ self.clockView.widthAnchor.constraint(equalToConstant: 34).isActive = true
+
+ let container: NSStackView = NSStackView()
+ container.orientation = .vertical
+ container.spacing = 2
+ container.distribution = .fillEqually
+ container.alignment = .left
+
+ self.nameField.font = NSFont.systemFont(ofSize: 11, weight: .light)
+ self.setTZ()
+ self.nameField.cell?.truncatesLastVisibleLine = true
+
+ self.timeField.font = NSFont.systemFont(ofSize: 13, weight: .regular)
+ self.timeField.stringValue = clock.formatted()
+ self.timeField.cell?.truncatesLastVisibleLine = true
+
+ container.addArrangedSubview(self.nameField)
+ container.addArrangedSubview(self.timeField)
+
+ self.addArrangedSubview(self.clockView)
+ self.addArrangedSubview(container)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func updateLayer() {
+ self.layer?.backgroundColor = isDarkMode ? NSColor(hexString: "#111111", alpha: 0.25).cgColor : NSColor(hexString: "#f5f5f5", alpha: 1).cgColor
+ }
+
+ private func setTZ() {
+ self.nameField.stringValue = "\(self.clock.name)"
+ if let tz = Clock.zones.first(where: { $0.key == self.clock.tz }) {
+ self.nameField.stringValue += " (\(tz.value))"
+ }
+ }
+
+ public func update(_ newClock: Clock_t) {
+ if self.clock.tz != newClock.tz {
+ self.clock = newClock
+ self.setTZ()
+ }
+
+ if (self.window?.isVisible ?? false) || !self.ready {
+ self.timeField.stringValue = newClock.formatted()
+ if let value = newClock.value {
+ self.clockView.setValue(value.convertToTimeZone(TimeZone(fromUTC: newClock.tz)))
+ }
+ self.ready = true
+ }
+ }
+}
+
+private class ClockChart: NSView {
+ private var color: NSColor = Color.systemAccent.additional as! NSColor
+
+ private let calendar = Calendar.current
+ private var hour: Int!
+ private var minute: Int!
+ private var second: Int!
+
+ private let hourLayer = CALayer()
+ private let minuteLayer = CALayer()
+ private let secondsLayer = CALayer()
+ private let pinLayer = CAShapeLayer()
+
+ override init(frame: CGRect = NSRect.zero) {
+ super.init(frame: frame)
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ }
+
+ public override func draw(_ dirtyRect: NSRect) {
+ super.draw(dirtyRect)
+
+ guard (self.hour != nil), (self.minute != nil), (self.second != nil) else { return }
+
+ let context = NSGraphicsContext.current!.cgContext
+ context.saveGState()
+ context.setFillColor(self.color.cgColor)
+ context.fillEllipse(in: dirtyRect)
+ context.restoreGState()
+
+ let anchor = CGPoint(x: 0.5, y: 0)
+ let center = CGPoint(x: dirtyRect.size.width / 2, y: dirtyRect.size.height / 2)
+
+ let hourAngle: CGFloat = CGFloat(Double(hour) * (360.0 / 12.0)) + CGFloat(Double(minute) * (1.0 / 60.0) * (360.0 / 12.0))
+ let minuteAngle: CGFloat = CGFloat(minute) * CGFloat(360.0 / 60.0)
+ let secondsAngle: CGFloat = CGFloat(self.second) * CGFloat(360.0 / 60.0)
+
+ self.hourLayer.backgroundColor = NSColor.white.cgColor
+ self.hourLayer.anchorPoint = anchor
+ self.hourLayer.position = center
+ self.hourLayer.bounds = CGRect(x: 0, y: 0, width: 3, height: dirtyRect.size.width / 2 - 7)
+ self.hourLayer.transform = CATransform3DMakeRotation(-hourAngle / 180 * CGFloat(Double.pi), 0, 0, 1)
+ self.layer?.addSublayer(self.hourLayer)
+
+ self.minuteLayer.backgroundColor = NSColor.white.cgColor
+ self.minuteLayer.anchorPoint = anchor
+ self.minuteLayer.position = center
+ self.minuteLayer.bounds = CGRect(x: 0, y: 0, width: 2, height: dirtyRect.size.width / 2 - 4)
+ self.minuteLayer.transform = CATransform3DMakeRotation(-minuteAngle / 180 * CGFloat(Double.pi), 0, 0, 1)
+ self.layer?.addSublayer(self.minuteLayer)
+
+ self.secondsLayer.backgroundColor = NSColor.red.cgColor
+ self.secondsLayer.anchorPoint = anchor
+ self.secondsLayer.position = center
+ self.secondsLayer.bounds = CGRect(x: 0, y: 0, width: 1, height: dirtyRect.size.width / 2 - 2)
+ self.secondsLayer.transform = CATransform3DMakeRotation(-secondsAngle / 180 * CGFloat(Double.pi), 0, 0, 1)
+ self.layer?.addSublayer(self.secondsLayer)
+
+ self.pinLayer.fillColor = NSColor.white.cgColor
+ self.pinLayer.anchorPoint = anchor
+ self.pinLayer.path = CGMutablePath(roundedRect: CGRect(
+ x: center.x - 3 / 2,
+ y: center.y - 3 / 2,
+ width: 3,
+ height: 3
+ ), cornerWidth: 4, cornerHeight: 4, transform: nil)
+ self.layer?.addSublayer(self.pinLayer)
+ }
+
+ public func setValue(_ value: Date) {
+ self.hour = self.calendar.component(.hour, from: value)
+ self.minute = self.calendar.component(.minute, from: value)
+ self.second = self.calendar.component(.second, from: value)
+
+ DispatchQueue.main.async(execute: {
+ self.display()
+ })
+ }
+}
diff --git a/Modules/Clock/settings.swift b/Modules/Clock/settings.swift
new file mode 100644
index 00000000..ae9545c5
--- /dev/null
+++ b/Modules/Clock/settings.swift
@@ -0,0 +1,267 @@
+//
+// settings.swift
+// Clock
+//
+// Created by Serhiy Mytrovtsiy on 24/03/2023
+// Using Swift 5.0
+// Running on macOS 13.2
+//
+// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
+//
+
+import Cocoa
+import Kit
+
+let nameColumnID = NSUserInterfaceItemIdentifier(rawValue: "name")
+let formatColumnID = NSUserInterfaceItemIdentifier(rawValue: "format")
+let tzColumnID = NSUserInterfaceItemIdentifier(rawValue: "tz")
+let statusColumnID = NSUserInterfaceItemIdentifier(rawValue: "status")
+
+internal class Settings: NSStackView, Settings_v, NSTableViewDelegate, NSTableViewDataSource, NSTextFieldDelegate {
+ public var callback: (() -> Void) = {}
+
+ private var list: [Clock_t] {
+ get {
+ if let objects = Store.shared.data(key: "\(Clock.title)_list") {
+ let decoder = JSONDecoder()
+ if let objectsDecoded = try? decoder.decode(Array.self, from: objects) as [Clock_t] {
+ return objectsDecoded
+ }
+ }
+ return [Clock.local]
+ }
+ set {
+ if newValue.isEmpty {
+ Store.shared.remove("\(Clock.title)_list")
+ } else {
+ let encoder = JSONEncoder()
+ if let encoded = try? encoder.encode(newValue){
+ Store.shared.set(key: "\(Clock.title)_list", value: encoded)
+ }
+ }
+ }
+ }
+
+ private var selectedRow: Int = -1
+
+ private let scrollView = NSScrollView()
+ private let tableView = NSTableView()
+ private var footerView: NSStackView? = nil
+ private var deleteButton: NSButton? = nil
+
+ public init() {
+ super.init(frame: NSRect.zero)
+
+ self.orientation = .vertical
+ self.distribution = .gravityAreas
+ self.edgeInsets = NSEdgeInsets(
+ top: Constants.Settings.margin,
+ left: Constants.Settings.margin,
+ bottom: Constants.Settings.margin,
+ right: Constants.Settings.margin
+ )
+ self.spacing = 0
+
+ self.scrollView.documentView = self.tableView
+ self.scrollView.hasHorizontalScroller = false
+ self.scrollView.hasVerticalScroller = true
+ self.scrollView.autohidesScrollers = true
+ self.scrollView.backgroundColor = NSColor.clear
+ self.scrollView.drawsBackground = true
+
+ self.tableView.frame = self.scrollView.bounds
+ self.tableView.delegate = self
+ self.tableView.dataSource = self
+ self.tableView.allowsMultipleSelection = false
+ self.tableView.focusRingType = .none
+ self.tableView.gridColor = .gridColor
+ self.tableView.columnAutoresizingStyle = .firstColumnOnlyAutoresizingStyle
+ self.tableView.allowsColumnResizing = false
+ self.tableView.gridStyleMask = [.solidVerticalGridLineMask, .solidHorizontalGridLineMask]
+ self.tableView.usesAlternatingRowBackgroundColors = true
+ if #available(macOS 11.0, *) {
+ self.tableView.style = .plain
+ }
+ self.tableView.rowHeight = 27
+
+ let nameColumn = NSTableColumn(identifier: nameColumnID)
+ nameColumn.headerCell.title = localizedString("Name")
+ nameColumn.headerCell.alignment = .center
+ let formatColumn = NSTableColumn(identifier: formatColumnID)
+ formatColumn.headerCell.title = localizedString("Format")
+ formatColumn.headerCell.alignment = .center
+ formatColumn.width = 160
+ let tzColumn = NSTableColumn(identifier: tzColumnID)
+ tzColumn.headerCell.title = localizedString("Time zone")
+ tzColumn.headerCell.alignment = .center
+ tzColumn.width = 132
+ let statusColumn = NSTableColumn(identifier: statusColumnID)
+ statusColumn.headerCell.title = ""
+ statusColumn.width = 16
+
+ self.tableView.addTableColumn(nameColumn)
+ self.tableView.addTableColumn(formatColumn)
+ self.tableView.addTableColumn(tzColumn)
+ self.tableView.addTableColumn(statusColumn)
+
+ let separator = NSBox()
+ separator.boxType = .separator
+
+ self.addArrangedSubview(self.scrollView)
+ self.addArrangedSubview(separator)
+ self.addArrangedSubview(self.footer())
+
+ NSLayoutConstraint.activate([
+ self.scrollView.heightAnchor.constraint(equalToConstant: 278)
+ ])
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func footer() -> NSView {
+ let view = NSStackView()
+ view.heightAnchor.constraint(equalToConstant: 27).isActive = true
+ view.spacing = 0
+ view.orientation = .horizontal
+
+ var addButton: NSButton {
+ let btn = NSButton()
+ btn.widthAnchor.constraint(equalToConstant: 27).isActive = true
+ btn.heightAnchor.constraint(equalToConstant: 27).isActive = true
+ btn.bezelStyle = .regularSquare
+ btn.translatesAutoresizingMaskIntoConstraints = false
+ if #available(macOS 11.0, *) {
+ btn.image = iconFromSymbol(name: "plus", scale: .large)
+ } else {
+ btn.title = "Add"
+ }
+ btn.isBordered = false
+ btn.action = #selector(self.addNewClock)
+ btn.target = self
+ btn.toolTip = localizedString("Add new clock")
+ btn.focusRingType = .none
+ return btn
+ }
+ var deleteButton: NSButton {
+ let btn = NSButton()
+ btn.widthAnchor.constraint(equalToConstant: 27).isActive = true
+ btn.heightAnchor.constraint(equalToConstant: 27).isActive = true
+ btn.bezelStyle = .regularSquare
+ btn.translatesAutoresizingMaskIntoConstraints = false
+ if #available(macOS 11.0, *) {
+ btn.image = iconFromSymbol(name: "minus", scale: .large)
+ } else {
+ btn.title = "Add"
+ }
+ btn.isBordered = false
+ btn.action = #selector(self.deleteClock)
+ btn.target = self
+ btn.toolTip = localizedString("Delete selected clock")
+ btn.focusRingType = .none
+ return btn
+ }
+ self.deleteButton = deleteButton
+
+ view.addArrangedSubview(addButton)
+ view.addArrangedSubview(NSView())
+
+ self.footerView = view
+ return view
+ }
+
+ func load(widgets: [Kit.widget_t]) {
+ self.tableView.reloadData()
+ }
+
+ func numberOfRows(in tableView: NSTableView) -> Int {
+ return self.list.count
+ }
+
+ func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
+ guard let id = tableColumn?.identifier else { return nil }
+ let cell = NSTableCellView()
+ let item = self.list[row]
+
+ switch id {
+ case nameColumnID, formatColumnID:
+ let text: NSTextField = NSTextField()
+ text.identifier = id
+ text.drawsBackground = false
+ text.isBordered = false
+ text.sizeToFit()
+ text.delegate = self
+ text.stringValue = id == nameColumnID ? item.name : item.format
+ text.translatesAutoresizingMaskIntoConstraints = false
+
+ cell.addSubview(text)
+ text.widthAnchor.constraint(equalTo: cell.widthAnchor).isActive = true
+ text.centerYAnchor.constraint(equalTo: cell.centerYAnchor).isActive = true
+ case tzColumnID:
+ let select: NSPopUpButton = selectView(action: #selector(self.toggleTZ), items: Clock.zones, selected: item.tz)
+ select.identifier = NSUserInterfaceItemIdentifier("\(row)")
+ select.sizeToFit()
+ cell.addSubview(select)
+ case statusColumnID:
+ let button: NSButton = NSButton(frame: NSRect(x: 0, y: 5, width: 10, height: 10))
+ button.identifier = NSUserInterfaceItemIdentifier("\(row)")
+ button.setButtonType(.switch)
+ button.state = item.enabled ? .on : .off
+ button.action = #selector(self.toggleClock)
+ button.title = ""
+ button.isBordered = false
+ button.isTransparent = false
+ button.target = self
+ button.sizeToFit()
+ cell.addSubview(button)
+ default: break
+ }
+
+ return cell
+ }
+
+ func controlTextDidChange(_ notification: Notification) {
+ if let textField = notification.object as? NSTextField, let id = textField.identifier {
+ let i = self.tableView.selectedRow
+ switch id {
+ case nameColumnID:
+ self.list[i].name = textField.stringValue
+ case formatColumnID:
+ self.list[i].format = textField.stringValue
+ default: return
+ }
+ }
+ }
+
+ func tableViewSelectionDidChange(_ notification: Notification) {
+ if self.tableView.selectedRow == -1 {
+ self.deleteButton?.removeFromSuperview()
+ } else {
+ if let btn = self.deleteButton {
+ self.footerView?.addArrangedSubview(btn)
+ }
+ }
+ }
+
+ @objc private func toggleTZ(_ sender: NSMenuItem) {
+ guard let key = sender.representedObject as? String, let id = sender.identifier, let i = Int(id.rawValue) else { return }
+ self.list[i].tz = key
+ }
+
+ @objc private func toggleClock(_ sender: NSButton) {
+ guard let id = sender.identifier, let i = Int(id.rawValue) else { return }
+ self.list[i].enabled = sender.state == NSControl.StateValue.on
+ }
+
+ @objc private func addNewClock(_ sender: Any) {
+ self.list.append(Clock_t(name: "Clock \(self.list.count)", format: Clock.local.format, tz: Clock.local.tz))
+ self.tableView.reloadData()
+ }
+ @objc private func deleteClock(_ sender: Any) {
+ guard self.tableView.selectedRow != -1 else { return }
+ self.list.remove(at: self.tableView.selectedRow)
+ self.tableView.reloadData()
+ self.deleteButton?.removeFromSuperview()
+ }
+}
diff --git a/Modules/Disk/header.h b/Modules/Disk/header.h
new file mode 100644
index 00000000..92c3bdde
--- /dev/null
+++ b/Modules/Disk/header.h
@@ -0,0 +1,12 @@
+//
+// Header.h
+// Disk
+//
+// Created by Serhiy Mytrovtsiy on 25/03/2023
+// Using Swift 5.0
+// Running on macOS 13.2
+//
+// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
+//
+
+#include
diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj
index 34679b8f..89e58ca8 100644
--- a/Stats.xcodeproj/project.pbxproj
+++ b/Stats.xcodeproj/project.pbxproj
@@ -9,6 +9,14 @@
/* Begin PBXBuildFile section */
5C0A2A8A292A5B4D009B4C1F /* SMJobBlessUtil.py in Resources */ = {isa = PBXBuildFile; fileRef = 5C0A2A89292A5B4D009B4C1F /* SMJobBlessUtil.py */; };
5C21D80B296C7B81005BA16D /* CombinedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C21D80A296C7B81005BA16D /* CombinedView.swift */; };
+ 5C2229A329CCB3C400F00E69 /* Clock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C22299D29CCB3C400F00E69 /* Clock.framework */; };
+ 5C2229A429CCB3C400F00E69 /* Clock.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5C22299D29CCB3C400F00E69 /* Clock.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 5C2229A929CCB41900F00E69 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2229A829CCB41900F00E69 /* main.swift */; };
+ 5C2229AB29CCB53E00F00E69 /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5C2229AA29CCB53E00F00E69 /* config.plist */; };
+ 5C2229AF29CDC08700F00E69 /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2229AE29CDC08700F00E69 /* settings.swift */; };
+ 5C2229B029CDFBF600F00E69 /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; };
+ 5C2229B829CE3F3300F00E69 /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2229B729CE3F3300F00E69 /* popup.swift */; };
+ 5C2229BD29CF685A00F00E69 /* header.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C2229BB29CF66B100F00E69 /* header.h */; };
5C23BC0229A0102500DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0129A0102500DBA990 /* portal.swift */; };
5C23BC0429A014AC00DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0329A014AC00DBA990 /* portal.swift */; };
5C23BC0829A03D1200DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0729A03D1200DBA990 /* portal.swift */; };
@@ -145,6 +153,20 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
+ 5C2229A129CCB3C400F00E69 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 9A1410ED229E721100D29793 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 5C22299C29CCB3C400F00E69;
+ remoteInfo = Clock;
+ };
+ 5C2229B229CDFBF600F00E69 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 9A1410ED229E721100D29793 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D;
+ remoteInfo = Kit;
+ };
9A11AAD4266FD77F000C1C05 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
@@ -310,6 +332,7 @@
dstSubfolderSpec = 10;
files = (
9AF9EE0A24648751005D2270 /* Disk.framework in Embed Frameworks */,
+ 5C2229A429CCB3C400F00E69 /* Clock.framework in Embed Frameworks */,
9A81C75E2449A41400825D92 /* RAM.framework in Embed Frameworks */,
9AE29ADD249A50350071B02D /* Sensors.framework in Embed Frameworks */,
9A11AAD7266FD77F000C1C05 /* Bluetooth.framework in Embed Frameworks */,
@@ -355,6 +378,12 @@
4921436D25319699000A1C47 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; };
5C0A2A89292A5B4D009B4C1F /* SMJobBlessUtil.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = SMJobBlessUtil.py; sourceTree = ""; };
5C21D80A296C7B81005BA16D /* CombinedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombinedView.swift; sourceTree = ""; };
+ 5C22299D29CCB3C400F00E69 /* Clock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Clock.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 5C2229A829CCB41900F00E69 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
+ 5C2229AA29CCB53E00F00E69 /* config.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = config.plist; sourceTree = ""; };
+ 5C2229AE29CDC08700F00E69 /* settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = settings.swift; sourceTree = ""; };
+ 5C2229B729CE3F3300F00E69 /* popup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = popup.swift; sourceTree = ""; };
+ 5C2229BB29CF66B100F00E69 /* header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = header.h; sourceTree = ""; };
5C23BC0129A0102500DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; };
5C23BC0329A014AC00DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; };
5C23BC0729A03D1200DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; };
@@ -516,6 +545,14 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
+ 5C22299A29CCB3C400F00E69 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 5C2229B029CDFBF600F00E69 /* Kit.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
5CFE492429264DF1000F2856 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -536,6 +573,7 @@
buildActionMask = 2147483647;
files = (
9AF9EE0924648751005D2270 /* Disk.framework in Frameworks */,
+ 5C2229A329CCB3C400F00E69 /* Clock.framework in Frameworks */,
9AE29ADC249A50350071B02D /* Sensors.framework in Frameworks */,
9ABFF8FD248BEBCB00C9041A /* Battery.framework in Frameworks */,
9A11AAD6266FD77F000C1C05 /* Bluetooth.framework in Frameworks */,
@@ -635,6 +673,17 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 5C22299E29CCB3C400F00E69 /* Clock */ = {
+ isa = PBXGroup;
+ children = (
+ 5C2229A829CCB41900F00E69 /* main.swift */,
+ 5C2229B729CE3F3300F00E69 /* popup.swift */,
+ 5C2229AE29CDC08700F00E69 /* settings.swift */,
+ 5C2229AA29CCB53E00F00E69 /* config.plist */,
+ );
+ path = Clock;
+ sourceTree = "";
+ };
5CFE492829264DF1000F2856 /* Helper */ = {
isa = PBXGroup;
children = (
@@ -690,6 +739,7 @@
9A11AACF266FD77F000C1C05 /* Bluetooth.framework */,
9AAC5E2A280ACC120043D892 /* Tests.xctest */,
5CFE492729264DF1000F2856 /* eu.exelban.Stats.SMC.Helper */,
+ 5C22299D29CCB3C400F00E69 /* Clock.framework */,
);
name = Products;
sourceTree = "";
@@ -909,6 +959,7 @@
9A3E17CD247A94AF00449CD1 /* Net */,
9ABFF8F7248BEBCB00C9041A /* Battery */,
9A11AAD0266FD77F000C1C05 /* Bluetooth */,
+ 5C22299E29CCB3C400F00E69 /* Clock */,
);
path = Modules;
sourceTree = "";
@@ -963,6 +1014,7 @@
9AB7FD7B246B48DB00387FDA /* settings.swift */,
9AF9EE0524648751005D2270 /* Info.plist */,
9AF9EE12246492E8005D2270 /* config.plist */,
+ 5C2229BB29CF66B100F00E69 /* header.h */,
);
path = Disk;
sourceTree = "";
@@ -970,6 +1022,13 @@
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
+ 5C22299829CCB3C400F00E69 /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
9A11AACA266FD77F000C1C05 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
@@ -1030,12 +1089,32 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
+ 5C2229BD29CF685A00F00E69 /* header.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
+ 5C22299C29CCB3C400F00E69 /* Clock */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 5C2229A529CCB3C400F00E69 /* Build configuration list for PBXNativeTarget "Clock" */;
+ buildPhases = (
+ 5C22299829CCB3C400F00E69 /* Headers */,
+ 5C22299929CCB3C400F00E69 /* Sources */,
+ 5C22299A29CCB3C400F00E69 /* Frameworks */,
+ 5C22299B29CCB3C400F00E69 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 5C2229B329CDFBF600F00E69 /* PBXTargetDependency */,
+ );
+ name = Clock;
+ productName = Clock;
+ productReference = 5C22299D29CCB3C400F00E69 /* Clock.framework */;
+ productType = "com.apple.product-type.framework";
+ };
5CFE492629264DF1000F2856 /* Helper */ = {
isa = PBXNativeTarget;
buildConfigurationList = 5CFE492D29264DF1000F2856 /* Build configuration list for PBXNativeTarget "Helper" */;
@@ -1098,6 +1177,7 @@
9A97CED02537331B00742D8F /* PBXTargetDependency */,
9A2846FD2666A9CC00EC1F6D /* PBXTargetDependency */,
9A11AAD5266FD77F000C1C05 /* PBXTargetDependency */,
+ 5C2229A229CCB3C400F00E69 /* PBXTargetDependency */,
);
name = Stats;
packageProductDependencies = (
@@ -1325,6 +1405,10 @@
LastUpgradeCheck = 1420;
ORGANIZATIONNAME = "Serhiy Mytrovtsiy";
TargetAttributes = {
+ 5C22299C29CCB3C400F00E69 = {
+ CreatedOnToolsVersion = 14.2;
+ LastSwiftMigration = 1420;
+ };
5CFE492629264DF1000F2856 = {
CreatedOnToolsVersion = 14.1;
};
@@ -1447,6 +1531,7 @@
9ABFF8F5248BEBCB00C9041A /* Battery */,
9AE29AD4249A50350071B02D /* Sensors */,
9A11AACE266FD77F000C1C05 /* Bluetooth */,
+ 5C22299C29CCB3C400F00E69 /* Clock */,
9AAC5E29280ACC120043D892 /* Tests */,
5CFE492629264DF1000F2856 /* Helper */,
);
@@ -1454,6 +1539,14 @@
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
+ 5C22299B29CCB3C400F00E69 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 5C2229AB29CCB53E00F00E69 /* config.plist in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
9A11AACD266FD77F000C1C05 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -1578,6 +1671,16 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
+ 5C22299929CCB3C400F00E69 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 5C2229AF29CDC08700F00E69 /* settings.swift in Sources */,
+ 5C2229B829CE3F3300F00E69 /* popup.swift in Sources */,
+ 5C2229A929CCB41900F00E69 /* main.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
5CFE492329264DF1000F2856 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -1767,6 +1870,16 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
+ 5C2229A229CCB3C400F00E69 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 5C22299C29CCB3C400F00E69 /* Clock */;
+ targetProxy = 5C2229A129CCB3C400F00E69 /* PBXContainerItemProxy */;
+ };
+ 5C2229B329CDFBF600F00E69 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 9A2846F62666A9CC00EC1F6D /* Kit */;
+ targetProxy = 5C2229B229CDFBF600F00E69 /* PBXContainerItemProxy */;
+ };
9A11AAD5266FD77F000C1C05 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 9A11AACE266FD77F000C1C05 /* Bluetooth */;
@@ -1903,6 +2016,81 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
+ 5C2229A629CCB3C400F00E69 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
+ CODE_SIGN_STYLE = Manual;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=macosx*]" = RP2S87B72W;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = eu.exelban.Stats.Clock;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Debug;
+ };
+ 5C2229A729CCB3C400F00E69 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
+ CODE_SIGN_STYLE = Manual;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=macosx*]" = RP2S87B72W;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = eu.exelban.Stats.Clock;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Release;
+ };
5CFE492B29264DF1000F2856 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -2908,6 +3096,7 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = Modules/Disk/header.h;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -2943,6 +3132,7 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = Modules/Disk/header.h;
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
@@ -2952,6 +3142,15 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
+ 5C2229A529CCB3C400F00E69 /* Build configuration list for PBXNativeTarget "Clock" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 5C2229A629CCB3C400F00E69 /* Debug */,
+ 5C2229A729CCB3C400F00E69 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
5CFE492D29264DF1000F2856 /* Build configuration list for PBXNativeTarget "Helper" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift
index 84e43504..1e2dd543 100755
--- a/Stats/AppDelegate.swift
+++ b/Stats/AppDelegate.swift
@@ -19,6 +19,7 @@ import Battery
import Sensors
import GPU
import Bluetooth
+import Clock
let updater = Updater(github: "exelban/stats", url: "https://api.serhiy.io/v1/stats/release/latest")
var modules: [Module] = [
@@ -29,7 +30,8 @@ var modules: [Module] = [
Sensors(),
Network(),
Battery(),
- Bluetooth()
+ Bluetooth(),
+ Clock()
]
@main