From cab58555c93c52a4d0f85a6065bd5de2ff3f4ffc Mon Sep 17 00:00:00 2001 From: bobrosoft Date: Fri, 20 Aug 2021 19:03:03 +0200 Subject: [PATCH] Add new "cpu" widget --- MTMR.xcodeproj/project.pbxproj | 8 + .../cpu.imageset/Contents.json | 21 ++ MTMR/Assets.xcassets/cpu.imageset/cpu.png | Bin 0 -> 1043 bytes MTMR/CPU.swift | 191 ++++++++++++++++++ MTMR/ItemsParsing.swift | 6 + MTMR/TouchBarController.swift | 4 + MTMR/Widgets/CPUBarItem.swift | 82 ++++++++ README.md | 16 +- 8 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 MTMR/Assets.xcassets/cpu.imageset/Contents.json create mode 100644 MTMR/Assets.xcassets/cpu.imageset/cpu.png create mode 100644 MTMR/CPU.swift create mode 100644 MTMR/Widgets/CPUBarItem.swift diff --git a/MTMR.xcodeproj/project.pbxproj b/MTMR.xcodeproj/project.pbxproj index e14e8aa..874daa9 100644 --- a/MTMR.xcodeproj/project.pbxproj +++ b/MTMR.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ 36FEF872235A1CFC00A0ABCE /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FEF871235A1CFC00A0ABCE /* AppSettings.swift */; }; 4CC9FEDC22FDEA65001512EB /* AMR_ANSIEscapeHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CC9FEDB22FDEA65001512EB /* AMR_ANSIEscapeHelper.m */; }; 4CDC6E5022FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */; }; + 4CF222EB26CC00470025BDA7 /* CPUBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF222EA26CC00470025BDA7 /* CPUBarItem.swift */; }; + 4CF222ED26CC43D50025BDA7 /* CPU.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF222EC26CC43D40025BDA7 /* CPU.swift */; }; 4CFF5E5C22E623DD00BFB1EE /* YandexWeatherBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */; }; 5DC6CA02241F92CB005CD5E8 /* Music.nowPlaying.scpt in Resources */ = {isa = PBXBuildFile; fileRef = 5DC6CA00241F92CB005CD5E8 /* Music.nowPlaying.scpt */; }; 5DC6CA03241F92CB005CD5E8 /* Music.next.scpt in Resources */ = {isa = PBXBuildFile; fileRef = 5DC6CA01241F92CB005CD5E8 /* Music.next.scpt */; }; @@ -104,6 +106,8 @@ 4CC9FEDA22FDEA65001512EB /* AMR_ANSIEscapeHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AMR_ANSIEscapeHelper.h; sourceTree = ""; }; 4CC9FEDB22FDEA65001512EB /* AMR_ANSIEscapeHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AMR_ANSIEscapeHelper.m; sourceTree = ""; }; 4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellScriptTouchBarItem.swift; sourceTree = ""; }; + 4CF222EA26CC00470025BDA7 /* CPUBarItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CPUBarItem.swift; sourceTree = ""; }; + 4CF222EC26CC43D40025BDA7 /* CPU.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CPU.swift; sourceTree = ""; }; 4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YandexWeatherBarItem.swift; sourceTree = ""; }; 5DC6CA00241F92CB005CD5E8 /* Music.nowPlaying.scpt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Music.nowPlaying.scpt; sourceTree = ""; }; 5DC6CA01241F92CB005CD5E8 /* Music.next.scpt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Music.next.scpt; sourceTree = ""; }; @@ -239,6 +243,7 @@ B0A7E9A9205D6AA400EEF070 /* KeyPress.swift */, 36FEF871235A1CFC00A0ABCE /* AppSettings.swift */, B059D623205E04F3006E6B86 /* CustomButtonTouchBarItem.swift */, + 4CF222EC26CC43D40025BDA7 /* CPU.swift */, 36C2ECD8207B74B4003CDA33 /* AppleScriptTouchBarItem.swift */, BAF5AB5824317CAF00B04904 /* SwipeItem.swift */, 4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */, @@ -310,6 +315,7 @@ 6042B6A62083E03A00C525C8 /* AppScrubberTouchBarItem.swift */, B04B7BB62087398C00C835D0 /* BatteryBarItem.swift */, 6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */, + 4CF222EA26CC00470025BDA7 /* CPUBarItem.swift */, 607EEA4C2087A8DA009DA5F0 /* CurrencyBarItem.swift */, B081732B213739FE005D4908 /* DnDBarItem.swift */, 60669B4220AD8FA80074E817 /* GroupBarItem.swift */, @@ -478,12 +484,14 @@ B0846A752220C968000288A7 /* NetworkBarItem.swift in Sources */, B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */, B08126F1217BE19000A98970 /* WidgetProtocol.swift in Sources */, + 4CF222EB26CC00470025BDA7 /* CPUBarItem.swift in Sources */, B0008E552080286C003AD4DD /* SupportHelpers.swift in Sources */, 6042B6A72083E03A00C525C8 /* AppScrubberTouchBarItem.swift in Sources */, BAF5AB5924317CAF00B04904 /* SwipeItem.swift in Sources */, B082B253205C7D8000BC04DC /* AppDelegate.swift in Sources */, B0F54A7A2295AC7D00B4C509 /* DarkModeBarItem.swift in Sources */, B08126EF217BD0B900A98970 /* PomodoroBarItem.swift in Sources */, + 4CF222ED26CC43D50025BDA7 /* CPU.swift in Sources */, B081732C213739FE005D4908 /* DnDBarItem.swift in Sources */, 60C44AFD20A373A100C0EC91 /* MusicBarItem.swift in Sources */, B059D624205E04F3006E6B86 /* CustomButtonTouchBarItem.swift in Sources */, diff --git a/MTMR/Assets.xcassets/cpu.imageset/Contents.json b/MTMR/Assets.xcassets/cpu.imageset/Contents.json new file mode 100644 index 0000000..73b14ac --- /dev/null +++ b/MTMR/Assets.xcassets/cpu.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "cpu.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/MTMR/Assets.xcassets/cpu.imageset/cpu.png b/MTMR/Assets.xcassets/cpu.imageset/cpu.png new file mode 100644 index 0000000000000000000000000000000000000000..ff394f69d71413f6e6b58f616f06d5acb8edcef3 GIT binary patch literal 1043 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!{sj1hxVjhk++Fhf|XUka0n?0@(X5Ay0nio_;Gv9|Bxep_&X9VNHFVqtn9rTX?NfY+uygl7Bo4| zXL2gjZjfVncKBUm{3C_hRoy|K`l63|&0WFu>q9}(j@07ctGO-Tx9UvEc_?nfm@IYq zoWCan15=Twi(^OypZDVrV<1!Ae7U`H+r7nvTPDnF>2&#hJfmg9Jr2hJG2!MN^HdaW+N&upG=C)U zTlEKOWT@Obu{lR=?yS!h8$;3*p9Te{1)mJ96ulbbnl_O+>+3P5 zSEdT)pMS4*nI#(Ou~c%Q)oXpP3sb@`wp{A@q#wMxV@s3~|5@&q=5151n)K^vx7*$U9D(_I1zow;Ai2)||X6A+=~ti{S&Y z$k=3u3fC#eGTzM;DY+(jP~7;7+d>!5|JkIir~gFcTrTg)OH+Cuu$=Rodw1PZO?{ms$x{x` zEf0S@(XQSgihKXT_xTy@NB{1y()0BaHpR=gZ4H$?tNB;oOS<{@@eiTvq+V?}B_6etEoo)U z$@TX%^AiH@9a*?%qHCpb|ED&!r@pbLq;F3#|NioRvZQIlueteMyZ5w29S$=aj< POx+Bgu6{1-oD!M.size / MemoryLayout.size) + +/// CPU Class +public class CPU: NSObject { + + //-------------------------------------------------------------------------- + // MARK: OPEN PROPERTY + //-------------------------------------------------------------------------- + +// /// Number of physical cores on this machine. +// public static var physicalCores: Int { +// get { +// return Int(System.hostBasicInfo.physical_cpu) +// } +// } +// +// /// Number of logical cores on this machine. Will be equal to physicalCores +// /// unless it has hyper-threading, in which case it will be double. +// public static var logicalCores: Int { +// get { +// return Int(System.hostBasicInfo.logical_cpu) +// } +// } + + //-------------------------------------------------------------------------- + // MARK: OPEN FUNCTIONS + //-------------------------------------------------------------------------- + + /// Get CPU usage of hole system (system, user, idle, nice). Determined by the delta between + /// the current and last call. + public static func systemUsage() -> (system: Double, + user: Double, + idle: Double, + nice: Double) { + let load = self.hostCPULoadInfo + + let userDiff = Double(load.cpu_ticks.0 - loadPrevious.cpu_ticks.0) + let sysDiff = Double(load.cpu_ticks.1 - loadPrevious.cpu_ticks.1) + let idleDiff = Double(load.cpu_ticks.2 - loadPrevious.cpu_ticks.2) + let niceDiff = Double(load.cpu_ticks.3 - loadPrevious.cpu_ticks.3) + + let totalTicks = sysDiff + userDiff + niceDiff + idleDiff + + let sys = sysDiff / totalTicks * 100.0 + let user = userDiff / totalTicks * 100.0 + let idle = idleDiff / totalTicks * 100.0 + let nice = niceDiff / totalTicks * 100.0 + + loadPrevious = load + + return (sys, user, idle, nice) + } + + + /// Get CPU usage of application,get from all thread + open class func applicationUsage() -> Double { + let threads = self.threadBasicInfos() + var result : Double = 0.0 + threads.forEach { (thread:thread_basic_info) in + if self.flag(thread) { + result += Double.init(thread.cpu_usage) / Double.init(TH_USAGE_SCALE); + } + } + return result * 100 + } + + //-------------------------------------------------------------------------- + // MARK: PRIVATE PROPERTY + //-------------------------------------------------------------------------- + + /// previous load of cpu + private static var loadPrevious = host_cpu_load_info() + + static var hostCPULoadInfo: host_cpu_load_info { + get { + var size = HOST_CPU_LOAD_INFO_COUNT + var hostInfo = host_cpu_load_info() + let result = withUnsafeMutablePointer(to: &hostInfo) { + $0.withMemoryRebound(to: integer_t.self, capacity: Int(size)) { + host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &size) + } + } + + #if DEBUG + if result != KERN_SUCCESS { + fatalError("ERROR - \(#file):\(#function) - kern_result_t = " + + "\(result)") + } + #endif + + return hostInfo + } + } + + //-------------------------------------------------------------------------- + // MARK: PRIVATE FUNCTION + //-------------------------------------------------------------------------- + + private class func flag(_ thread:thread_basic_info) -> Bool { + let foo = thread.flags & TH_FLAGS_IDLE + let number = NSNumber.init(value: foo) + return !Bool.init(truncating: number) + } + + private class func threadActPointers() -> [thread_act_t] { + var threads_act = [thread_act_t]() + + var threads_array: thread_act_array_t? = nil + var count = mach_msg_type_number_t() + + let result = task_threads(mach_task_self_, &(threads_array), &count) + + guard result == KERN_SUCCESS else { + return threads_act + } + + guard let array = threads_array else { + return threads_act + } + + for i in 0...size) + _ = vm_deallocate(mach_task_self_, vm_address_t(array.pointee), vm_size_t(krsize)); + return threads_act + } + + private class func threadBasicInfos() -> [thread_basic_info] { + var result = [thread_basic_info]() + + let thinfo : thread_info_t = thread_info_t.allocate(capacity: Int(THREAD_INFO_MAX)) + let thread_info_count = UnsafeMutablePointer.allocate(capacity: 128) + var basic_info_th: thread_basic_info_t? = nil + + for act_t in self.threadActPointers() { + thread_info_count.pointee = UInt32(THREAD_INFO_MAX); + let kr = thread_info(act_t ,thread_flavor_t(THREAD_BASIC_INFO),thinfo, thread_info_count); + if (kr != KERN_SUCCESS) { + return [thread_basic_info](); + } + basic_info_th = withUnsafePointer(to: &thinfo.pointee, { (ptr) -> thread_basic_info_t in + let int8Ptr = unsafeBitCast(ptr, to: thread_basic_info_t.self) + return int8Ptr + }) + if basic_info_th != nil { + result.append(basic_info_th!.pointee) + } + } + + return result + } + + //TODO: this function is used for get cpu usage of all thread,and this is in developing + private class func threadIdentifierInfos() -> [thread_identifier_info] { + var result = [thread_identifier_info]() + let thinfo : thread_info_t = thread_info_t.allocate(capacity: Int(THREAD_INFO_MAX)) + let thread_info_count = UnsafeMutablePointer.allocate(capacity: 128) + var identifier_info_th: thread_identifier_info_t? = nil + + for act_t in self.threadActPointers() { + thread_info_count.pointee = UInt32(THREAD_INFO_MAX); + let kr = thread_info(act_t ,thread_flavor_t(THREAD_IDENTIFIER_INFO),thinfo, thread_info_count); + if (kr != KERN_SUCCESS) { + return [thread_identifier_info](); + } + identifier_info_th = withUnsafePointer(to: &thinfo.pointee, { (ptr) -> thread_identifier_info_t in + let int8Ptr = unsafeBitCast(ptr, to: thread_identifier_info_t.self) + return int8Ptr + }) + if identifier_info_th != nil { + result.append(identifier_info_th!.pointee) + } + } + return result + } +} diff --git a/MTMR/ItemsParsing.swift b/MTMR/ItemsParsing.swift index 2b590ac..c7bcb78 100644 --- a/MTMR/ItemsParsing.swift +++ b/MTMR/ItemsParsing.swift @@ -261,6 +261,7 @@ enum ItemType: Decodable { case shellScriptTitledButton(source: SourceProtocol, refreshInterval: Double) case timeButton(formatTemplate: String, timeZone: String?, locale: String?) case battery + case cpu(refreshInterval: Double) case dock(autoResize: Bool, filter: String?) case volume case brightness(refreshInterval: Double) @@ -317,6 +318,7 @@ enum ItemType: Decodable { case shellScriptTitledButton case timeButton case battery + case cpu case dock case volume case brightness @@ -362,6 +364,10 @@ enum ItemType: Decodable { case .battery: self = .battery + + case .cpu: + let refreshInterval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 5.0 + self = .cpu(refreshInterval: refreshInterval) case .dock: let autoResize = try container.decodeIfPresent(Bool.self, forKey: .autoResize) ?? false diff --git a/MTMR/TouchBarController.swift b/MTMR/TouchBarController.swift index 9860ee6..cbf8b95 100644 --- a/MTMR/TouchBarController.swift +++ b/MTMR/TouchBarController.swift @@ -29,6 +29,8 @@ extension ItemType { return "com.toxblh.mtmr.timeButton." case .battery: return "com.toxblh.mtmr.battery." + case .cpu(refreshInterval: _): + return "com.toxblh.mtmr.cpu." case .dock(autoResize: _, filter: _): return "com.toxblh.mtmr.dock" case .volume: @@ -272,6 +274,8 @@ class TouchBarController: NSObject, NSTouchBarDelegate { barItem = TimeTouchBarItem(identifier: identifier, formatTemplate: template, timeZone: timeZone, locale: locale) case .battery: barItem = BatteryBarItem(identifier: identifier) + case let .cpu(refreshInterval: refreshInterval): + barItem = CPUBarItem(identifier: identifier, refreshInterval: refreshInterval) case let .dock(autoResize: autoResize, filter: regexString): if let regexString = regexString { guard let regex = try? NSRegularExpression(pattern: regexString, options: []) else { diff --git a/MTMR/Widgets/CPUBarItem.swift b/MTMR/Widgets/CPUBarItem.swift new file mode 100644 index 0000000..bd10bce --- /dev/null +++ b/MTMR/Widgets/CPUBarItem.swift @@ -0,0 +1,82 @@ +// +// CPUBarItem.swift +// MTMR +// +// Created by bobrosoft on 17/08/2021. +// Copyright © 2018 Anton Palgunov. All rights reserved. +// + +import Foundation + +class CPUBarItem: CustomButtonTouchBarItem { + private let refreshInterval: TimeInterval + private var refreshQueue: DispatchQueue? = DispatchQueue(label: "mtmr.cpu") + private let defaultSingleTapScript: NSAppleScript! = "activate application \"Activity Monitor\"\rtell application \"System Events\"\r\ttell process \"Activity Monitor\"\r\t\ttell radio button \"CPU\" of radio group 1 of group 2 of toolbar 1 of window 1 to perform action \"AXPress\"\r\tend tell\rend tell".appleScript + + init(identifier: NSTouchBarItem.Identifier, refreshInterval: TimeInterval) { + self.refreshInterval = refreshInterval + super.init(identifier: identifier, title: "⏳") + + // Set default image + if self.image == nil { + self.image = #imageLiteral(resourceName: "cpu").resize(maxSize: NSSize(width: 24, height: 24)); + } + + // Set default action + if actions.filter({ $0.trigger == .singleTap }).isEmpty { + actions.append(ItemAction( + trigger: .singleTap, + defaultTapAction + )) + } + + refreshAndSchedule() + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func refreshAndSchedule() { + DispatchQueue.main.async { + // Get CPU load + let usage = 100 - CPU.systemUsage().idle + guard usage.isFinite else { + return + } + + // Choose color based on CPU load + var color: NSColor? = nil + var bgColor: NSColor? = nil + if usage > 70 { + color = .black + bgColor = .yellow + } else if usage > 30 { + color = .yellow + } + + // Update layout + let attrTitle = NSMutableAttributedString.init(attributedString: String(format: "%.1f%%", usage).defaultTouchbarAttributedString) + if let color = color { + attrTitle.addAttributes([.foregroundColor: color], range: NSRange(location: 0, length: attrTitle.length)) + } + self.attributedTitle = attrTitle + self.backgroundColor = bgColor + } + + refreshQueue?.asyncAfter(deadline: .now() + refreshInterval) { [weak self] in + self?.refreshAndSchedule() + } + } + + func defaultTapAction() { + refreshQueue?.async { [weak self] in + self?.defaultSingleTapScript.executeAndReturnError(nil) + } + } + + deinit { + refreshQueue?.suspend() + refreshQueue = nil + } +} diff --git a/README.md b/README.md index bab0b02..f31717c 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ The pre-installed configuration contains less or more than you'll probably want, - timeButton - battery +- cpu - currency - weather - yandexWeather @@ -189,7 +190,7 @@ Example: > Note: script may return also colors using escape sequences (read more here https://misc.flogisoft.com/bash/tip_colors_and_formatting) > Only "16 Colors" mode supported atm. If background color returned, button will pick it up as own background color. -Example of "CPU load" button which also changes color based on load value. +Example of "CPU load" button which also changes color based on load value (Note: you can use native `cpu` plugin for that purpose which runs better): ```js { "type": "shellScriptTitledButton", @@ -246,6 +247,19 @@ To close a group, use the button: ## Native plugins +#### `cpu` + +> Shows current CPU load in percents, changes color based on load value. +> Has lower power consumption and more stable in comparison to shell-based solution. + +```js +{ + "type": "cpu", + "refreshInterval": 3, + "width": 80 +} +``` + #### `timeButton` > Attention! Works not all: https://en.wikipedia.org/wiki/List_of_time_zone_abbreviations