mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
feat: added an option to sync clock with ntp server and moved reader to separate file
This commit is contained in:
@@ -23,20 +23,12 @@ public struct Clock_t: Codable {
|
|||||||
public var value: Date? = nil
|
public var value: Date? = nil
|
||||||
|
|
||||||
var popupIndex: Int {
|
var popupIndex: Int {
|
||||||
get {
|
get { Store.shared.int(key: "clock_\(self.id)_popupIndex", defaultValue: -1) }
|
||||||
Store.shared.int(key: "clock_\(self.id)_popupIndex", defaultValue: -1)
|
set { Store.shared.set(key: "clock_\(self.id)_popupIndex", value: newValue) }
|
||||||
}
|
|
||||||
set {
|
|
||||||
Store.shared.set(key: "clock_\(self.id)_popupIndex", value: newValue)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var popupState: Bool {
|
var popupState: Bool {
|
||||||
get {
|
get { Store.shared.bool(key: "clock_\(self.id)_popupState", defaultValue: true) }
|
||||||
Store.shared.bool(key: "clock_\(self.id)_popupState", defaultValue: true)
|
set { Store.shared.set(key: "clock_\(self.id)_popupState", value: newValue) }
|
||||||
}
|
|
||||||
set {
|
|
||||||
Store.shared.set(key: "clock_\(self.id)_popupState", value: newValue)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func formatted() -> String {
|
public func formatted() -> String {
|
||||||
@@ -47,12 +39,6 @@ public struct Clock_t: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class ClockReader: Reader<Date> {
|
|
||||||
public override func read() {
|
|
||||||
self.callback(Date())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Clock: Module {
|
public class Clock: Module {
|
||||||
private let popupView: Popup = Popup(.clock)
|
private let popupView: Popup = Popup(.clock)
|
||||||
private let portalView: Portal
|
private let portalView: Portal
|
||||||
@@ -85,6 +71,12 @@ public class Clock: Module {
|
|||||||
self?.callback(value)
|
self?.callback(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.settingsView.callback = { [weak self] in
|
||||||
|
guard let self, self.enabled, let reader = self.reader else { return }
|
||||||
|
reader.stop()
|
||||||
|
reader.start()
|
||||||
|
}
|
||||||
|
|
||||||
self.setReaders([self.reader])
|
self.setReaders([self.reader])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
123
Modules/Clock/reader.swift
Normal file
123
Modules/Clock/reader.swift
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
//
|
||||||
|
// reader.swift
|
||||||
|
// Stats
|
||||||
|
//
|
||||||
|
// Created by Serhiy Mytrovtsiy on 05/03/2026
|
||||||
|
// Using Swift 6.0
|
||||||
|
// Running on macOS 26.3
|
||||||
|
//
|
||||||
|
// Copyright © 2026 Serhiy Mytrovtsiy. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Kit
|
||||||
|
|
||||||
|
internal class ClockReader: Reader<Date> {
|
||||||
|
private let title: String = ModuleType.clock.stringValue
|
||||||
|
|
||||||
|
private let queue = DispatchQueue(label: "eu.exelban.Stats.Clock.ntp.sync", qos: .default)
|
||||||
|
private var _offset: TimeInterval = 0
|
||||||
|
private var offset: TimeInterval {
|
||||||
|
get { self.queue.sync { self._offset } }
|
||||||
|
set { self.queue.sync { self._offset = newValue } }
|
||||||
|
}
|
||||||
|
private var now: Date { Date().addingTimeInterval(self.offset) }
|
||||||
|
|
||||||
|
private var ntpSync: Bool {
|
||||||
|
get { Store.shared.bool(key: "\(self.title)_ntpSync", defaultValue: false) }
|
||||||
|
set { Store.shared.set(key: "\(self.title)_ntpSync", value: newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var ntpServer: String {
|
||||||
|
get { Store.shared.string(key: "\(self.title)_ntpServer", defaultValue: "pool.ntp.org") }
|
||||||
|
set { Store.shared.set(key: "\(self.title)_ntpServer", value: newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func setup() {
|
||||||
|
self.syncWithNTP()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func read() {
|
||||||
|
let date = self.ntpSync ? self.now : Date()
|
||||||
|
|
||||||
|
self.callback(date)
|
||||||
|
|
||||||
|
if Calendar.current.component(.second, from: date) == 0 {
|
||||||
|
self.syncWithNTP()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func syncWithNTP() {
|
||||||
|
guard self.ntpSync else {
|
||||||
|
self.offset = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let server = self.ntpServer
|
||||||
|
self.queue.async { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
guard let serverDate = self.requestTime(server: server) else { return }
|
||||||
|
let newOffset = serverDate.timeIntervalSince(Date())
|
||||||
|
self._offset = newOffset
|
||||||
|
self.alignOffset = newOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestTime(server: String, timeout: TimeInterval = 2.0) -> Date? {
|
||||||
|
let host = CFHostCreateWithName(nil, server as CFString).takeRetainedValue()
|
||||||
|
var resolved: DarwinBoolean = false
|
||||||
|
let started = CFHostStartInfoResolution(host, .addresses, nil)
|
||||||
|
guard started else { return nil }
|
||||||
|
|
||||||
|
guard
|
||||||
|
let unmanaged = CFHostGetAddressing(host, &resolved),
|
||||||
|
resolved.boolValue,
|
||||||
|
let addresses = unmanaged.takeUnretainedValue() as? [Data],
|
||||||
|
let first = addresses.first
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
let socketFD = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
|
||||||
|
guard socketFD >= 0 else { return nil }
|
||||||
|
defer { close(socketFD) }
|
||||||
|
|
||||||
|
var tv = timeval(tv_sec: Int(timeout), tv_usec: 0)
|
||||||
|
setsockopt(socketFD, SOL_SOCKET, SO_RCVTIMEO, &tv, socklen_t(MemoryLayout<timeval>.size))
|
||||||
|
|
||||||
|
var addrStorage = sockaddr_storage()
|
||||||
|
first.withUnsafeBytes { raw in
|
||||||
|
guard let base = raw.baseAddress else { return }
|
||||||
|
memcpy(&addrStorage, base, min(raw.count, MemoryLayout<sockaddr_storage>.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
guard addrStorage.ss_family == sa_family_t(AF_INET) else { return nil }
|
||||||
|
withUnsafeMutablePointer(to: &addrStorage) {
|
||||||
|
$0.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { p in
|
||||||
|
p.pointee.sin_port = in_port_t(123).bigEndian
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var packet = Data(count: 48)
|
||||||
|
packet[0] = 0x1B
|
||||||
|
let sent = packet.withUnsafeBytes { ptr in
|
||||||
|
withUnsafePointer(to: &addrStorage) {
|
||||||
|
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
|
||||||
|
sendto(socketFD, ptr.baseAddress, ptr.count, 0, sa, socklen_t(MemoryLayout<sockaddr_in>.size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard sent == 48 else { return nil }
|
||||||
|
|
||||||
|
var recvBuf = Data(count: 48)
|
||||||
|
let received = recvBuf.withUnsafeMutableBytes { ptr in
|
||||||
|
recv(socketFD, ptr.baseAddress, ptr.count, 0)
|
||||||
|
}
|
||||||
|
guard received >= 48 else { return nil }
|
||||||
|
|
||||||
|
let seconds1900: UInt32 = recvBuf.withUnsafeBytes { ptr in
|
||||||
|
let b = ptr.bindMemory(to: UInt8.self)
|
||||||
|
return (UInt32(b[40]) << 24) | (UInt32(b[41]) << 16) | (UInt32(b[42]) << 8) | UInt32(b[43])
|
||||||
|
}
|
||||||
|
|
||||||
|
return Date(timeIntervalSince1970: TimeInterval(seconds1900) - 2_208_988_800)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,9 +22,7 @@ internal class Settings: NSStackView, Settings_v, NSTableViewDelegate, NSTableVi
|
|||||||
|
|
||||||
private var cachedList: [Clock_t] = []
|
private var cachedList: [Clock_t] = []
|
||||||
private var list: [Clock_t] {
|
private var list: [Clock_t] {
|
||||||
get {
|
get { self.cachedList }
|
||||||
return self.cachedList
|
|
||||||
}
|
|
||||||
set {
|
set {
|
||||||
self.cachedList = newValue
|
self.cachedList = newValue
|
||||||
|
|
||||||
@@ -47,9 +45,13 @@ internal class Settings: NSStackView, Settings_v, NSTableViewDelegate, NSTableVi
|
|||||||
private var footerView: NSStackView? = nil
|
private var footerView: NSStackView? = nil
|
||||||
private var deleteButton: NSButton? = nil
|
private var deleteButton: NSButton? = nil
|
||||||
|
|
||||||
|
private var ntpSync: Bool = false
|
||||||
|
|
||||||
public init(_ module: ModuleType) {
|
public init(_ module: ModuleType) {
|
||||||
self.title = module.stringValue
|
self.title = module.stringValue
|
||||||
|
|
||||||
|
self.ntpSync = Store.shared.bool(key: "\(self.title)_ntpSync", defaultValue: self.ntpSync)
|
||||||
|
|
||||||
super.init(frame: NSRect.zero)
|
super.init(frame: NSRect.zero)
|
||||||
|
|
||||||
if let objects = Store.shared.data(key: "\(self.title)_list") {
|
if let objects = Store.shared.data(key: "\(self.title)_list") {
|
||||||
@@ -109,6 +111,13 @@ internal class Settings: NSStackView, Settings_v, NSTableViewDelegate, NSTableVi
|
|||||||
let separator = NSBox()
|
let separator = NSBox()
|
||||||
separator.boxType = .separator
|
separator.boxType = .separator
|
||||||
|
|
||||||
|
self.addArrangedSubview(PreferencesSection([
|
||||||
|
PreferencesRow(localizedString("Sync with NTP server"), component: switchView(
|
||||||
|
action: #selector(self.toggleNTPSync),
|
||||||
|
state: self.ntpSync
|
||||||
|
))
|
||||||
|
]))
|
||||||
|
|
||||||
self.addArrangedSubview(self.scrollView)
|
self.addArrangedSubview(self.scrollView)
|
||||||
self.addArrangedSubview(separator)
|
self.addArrangedSubview(separator)
|
||||||
self.addArrangedSubview(self.footer())
|
self.addArrangedSubview(self.footer())
|
||||||
@@ -272,4 +281,9 @@ internal class Settings: NSStackView, Settings_v, NSTableViewDelegate, NSTableVi
|
|||||||
@objc private func openFormatHelp(_ sender: NSButton) {
|
@objc private func openFormatHelp(_ sender: NSButton) {
|
||||||
NSWorkspace.shared.open(URL(string: "https://www.nsdateformatter.com")!)
|
NSWorkspace.shared.open(URL(string: "https://www.nsdateformatter.com")!)
|
||||||
}
|
}
|
||||||
|
@objc func toggleNTPSync(_ sender: NSControl) {
|
||||||
|
self.ntpSync = controlState(sender)
|
||||||
|
Store.shared.set(key: "\(self.title)_ntpSync", value: self.ntpSync)
|
||||||
|
self.callback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
5CA518382B543FE600EBCCC4 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA518372B543FE600EBCCC4 /* portal.swift */; };
|
5CA518382B543FE600EBCCC4 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA518372B543FE600EBCCC4 /* portal.swift */; };
|
||||||
5CAA50722C8E417700B13E13 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CAA50712C8E417700B13E13 /* Text.swift */; };
|
5CAA50722C8E417700B13E13 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CAA50712C8E417700B13E13 /* Text.swift */; };
|
||||||
5CB3878A2C35A7110030459D /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB387892C35A7110030459D /* widget.swift */; };
|
5CB3878A2C35A7110030459D /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB387892C35A7110030459D /* widget.swift */; };
|
||||||
|
5CC3B4E52F5A033000775E2C /* reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC3B4E42F5A032E00775E2C /* reader.swift */; };
|
||||||
5CCA5CD52D4E8DB3002917F0 /* libIOReport.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1E45552D11D66200525864 /* libIOReport.tbd */; };
|
5CCA5CD52D4E8DB3002917F0 /* libIOReport.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1E45552D11D66200525864 /* libIOReport.tbd */; };
|
||||||
5CD342F42B2F2FB700225631 /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD342F32B2F2FB700225631 /* notifications.swift */; };
|
5CD342F42B2F2FB700225631 /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD342F32B2F2FB700225631 /* notifications.swift */; };
|
||||||
5CE7E78C2C318512006BC92C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE7E78B2C318512006BC92C /* WidgetKit.framework */; };
|
5CE7E78C2C318512006BC92C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE7E78B2C318512006BC92C /* WidgetKit.framework */; };
|
||||||
@@ -548,6 +549,7 @@
|
|||||||
5CA518372B543FE600EBCCC4 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = "<group>"; };
|
5CA518372B543FE600EBCCC4 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = "<group>"; };
|
||||||
5CAA50712C8E417700B13E13 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = "<group>"; };
|
5CAA50712C8E417700B13E13 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = "<group>"; };
|
||||||
5CB387892C35A7110030459D /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = "<group>"; };
|
5CB387892C35A7110030459D /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = "<group>"; };
|
||||||
|
5CC3B4E42F5A032E00775E2C /* reader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = reader.swift; sourceTree = "<group>"; };
|
||||||
5CD342F32B2F2FB700225631 /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = "<group>"; };
|
5CD342F32B2F2FB700225631 /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = "<group>"; };
|
||||||
5CE7E78A2C318512006BC92C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
5CE7E78A2C318512006BC92C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
5CE7E78B2C318512006BC92C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
5CE7E78B2C318512006BC92C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||||
@@ -867,6 +869,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5C2229A829CCB41900F00E69 /* main.swift */,
|
5C2229A829CCB41900F00E69 /* main.swift */,
|
||||||
|
5CC3B4E42F5A032E00775E2C /* reader.swift */,
|
||||||
5C2229B729CE3F3300F00E69 /* popup.swift */,
|
5C2229B729CE3F3300F00E69 /* popup.swift */,
|
||||||
5C044F792B3DE6F3005F6951 /* portal.swift */,
|
5C044F792B3DE6F3005F6951 /* portal.swift */,
|
||||||
5C2229AE29CDC08700F00E69 /* settings.swift */,
|
5C2229AE29CDC08700F00E69 /* settings.swift */,
|
||||||
@@ -1717,7 +1720,7 @@
|
|||||||
New,
|
New,
|
||||||
);
|
);
|
||||||
LastSwiftUpdateCheck = 1540;
|
LastSwiftUpdateCheck = 1540;
|
||||||
LastUpgradeCheck = 2620;
|
LastUpgradeCheck = 2630;
|
||||||
ORGANIZATIONNAME = "Serhiy Mytrovtsiy";
|
ORGANIZATIONNAME = "Serhiy Mytrovtsiy";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
5C22299C29CCB3C400F00E69 = {
|
5C22299C29CCB3C400F00E69 = {
|
||||||
@@ -2021,6 +2024,7 @@
|
|||||||
files = (
|
files = (
|
||||||
5C2229AF29CDC08700F00E69 /* settings.swift in Sources */,
|
5C2229AF29CDC08700F00E69 /* settings.swift in Sources */,
|
||||||
5C044F7A2B3DE6F3005F6951 /* portal.swift in Sources */,
|
5C044F7A2B3DE6F3005F6951 /* portal.swift in Sources */,
|
||||||
|
5CC3B4E52F5A033000775E2C /* reader.swift in Sources */,
|
||||||
5C2229B829CE3F3300F00E69 /* popup.swift in Sources */,
|
5C2229B829CE3F3300F00E69 /* popup.swift in Sources */,
|
||||||
5C2229A929CCB41900F00E69 /* main.swift in Sources */,
|
5C2229A929CCB41900F00E69 /* main.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user