mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
created view for Battery module
This commit is contained in:
@@ -17,6 +17,6 @@ SPEC CHECKSUMS:
|
||||
Charts: ec1f57f9340054155691e84d4544a1d239d382c5
|
||||
LaunchAtLogin: 550b0cbbdaf1b13f87a0fab6a3f8e2fbafe067fe
|
||||
|
||||
PODFILE CHECKSUM: dee05cc24d20d667671a5f594bfbb948dcc3d791
|
||||
PODFILE CHECKSUM: b73e93f3b5879b0f0bf59fcb6d58288fb63aabc0
|
||||
|
||||
COCOAPODS: 1.7.1
|
||||
COCOAPODS: 1.7.5
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
628D2DE0AAA753E9F47625B0 /* Pods_Stats.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56B63995EBD3A1D1EBD3AF38 /* Pods_Stats.framework */; };
|
||||
9A09C89E22B3A7C90018426F /* Battery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C89D22B3A7C90018426F /* Battery.swift */; };
|
||||
9A09C8A022B3A7E20018426F /* BatteryReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C89F22B3A7E20018426F /* BatteryReader.swift */; };
|
||||
9A09C8A222B3D94D0018426F /* BatteryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C8A122B3D94D0018426F /* BatteryView.swift */; };
|
||||
9A09C8A222B3D94D0018426F /* BatteryWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C8A122B3D94D0018426F /* BatteryWidget.swift */; };
|
||||
9A1410F9229E721100D29793 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1410F8229E721100D29793 /* AppDelegate.swift */; };
|
||||
9A141100229E721200D29793 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9A1410FE229E721200D29793 /* Main.storyboard */; };
|
||||
9A426DB822C2B5EE00C064C4 /* macAppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A426DB722C2B5EE00C064C4 /* macAppUpdater.swift */; };
|
||||
@@ -24,6 +24,7 @@
|
||||
9A59AE56231EE02F007989D6 /* ChartMarker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A59AE55231EE02F007989D6 /* ChartMarker.swift */; };
|
||||
9A5B1CBF229E78F0008B9D3C /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5B1CBE229E78F0008B9D3C /* Observable.swift */; };
|
||||
9A5B1CC5229E7B40008B9D3C /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5B1CC4229E7B40008B9D3C /* Extensions.swift */; };
|
||||
9A606B482321025C00642F51 /* BatteryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A606B472321025C00642F51 /* BatteryView.swift */; };
|
||||
9A6CFC0122A1C9F5001E782D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9A6CFC0022A1C9F5001E782D /* Assets.xcassets */; };
|
||||
9A74D59722B44498004FE1FA /* Mini.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A74D59622B44498004FE1FA /* Mini.swift */; };
|
||||
9A79B36A22D3BEE600BF1C3A /* Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A79B36922D3BEE600BF1C3A /* Widget.swift */; };
|
||||
@@ -67,7 +68,7 @@
|
||||
56B63995EBD3A1D1EBD3AF38 /* Pods_Stats.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Stats.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9A09C89D22B3A7C90018426F /* Battery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Battery.swift; sourceTree = "<group>"; };
|
||||
9A09C89F22B3A7E20018426F /* BatteryReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryReader.swift; sourceTree = "<group>"; };
|
||||
9A09C8A122B3D94D0018426F /* BatteryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryView.swift; sourceTree = "<group>"; };
|
||||
9A09C8A122B3D94D0018426F /* BatteryWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryWidget.swift; sourceTree = "<group>"; };
|
||||
9A1410F5229E721100D29793 /* Stats.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stats.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9A1410F8229E721100D29793 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
9A1410FF229E721200D29793 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
@@ -84,6 +85,7 @@
|
||||
9A59AE55231EE02F007989D6 /* ChartMarker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartMarker.swift; sourceTree = "<group>"; };
|
||||
9A5B1CBE229E78F0008B9D3C /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = "<group>"; };
|
||||
9A5B1CC4229E7B40008B9D3C /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
|
||||
9A606B472321025C00642F51 /* BatteryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryView.swift; sourceTree = "<group>"; };
|
||||
9A6CFC0022A1C9F5001E782D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
9A74D59622B44498004FE1FA /* Mini.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mini.swift; sourceTree = "<group>"; };
|
||||
9A79B36922D3BEE600BF1C3A /* Widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Widget.swift; sourceTree = "<group>"; };
|
||||
@@ -126,6 +128,7 @@
|
||||
children = (
|
||||
9A09C89D22B3A7C90018426F /* Battery.swift */,
|
||||
9A09C89F22B3A7E20018426F /* BatteryReader.swift */,
|
||||
9A606B472321025C00642F51 /* BatteryView.swift */,
|
||||
);
|
||||
path = Battery;
|
||||
sourceTree = "<group>";
|
||||
@@ -214,7 +217,7 @@
|
||||
children = (
|
||||
9AF0F31922DA923100026AE6 /* Network */,
|
||||
9AF0F31822DA922800026AE6 /* Charts */,
|
||||
9A09C8A122B3D94D0018426F /* BatteryView.swift */,
|
||||
9A09C8A122B3D94D0018426F /* BatteryWidget.swift */,
|
||||
9A74D59622B44498004FE1FA /* Mini.swift */,
|
||||
9A79B36922D3BEE600BF1C3A /* Widget.swift */,
|
||||
);
|
||||
@@ -446,7 +449,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9A09C8A222B3D94D0018426F /* BatteryView.swift in Sources */,
|
||||
9A09C8A222B3D94D0018426F /* BatteryWidget.swift in Sources */,
|
||||
9A426DB822C2B5EE00C064C4 /* macAppUpdater.swift in Sources */,
|
||||
9A79B36E22D3BEF900BF1C3A /* Reader.swift in Sources */,
|
||||
9A59AE54231ED1AC007989D6 /* CPUView.swift in Sources */,
|
||||
@@ -470,6 +473,7 @@
|
||||
9A1410F9229E721100D29793 /* AppDelegate.swift in Sources */,
|
||||
9AF0F32722DA92DD00026AE6 /* NetworkDotsText.swift in Sources */,
|
||||
9AF0F32922DA92E800026AE6 /* NetworkArrowsText.swift in Sources */,
|
||||
9A606B482321025C00642F51 /* BatteryView.swift in Sources */,
|
||||
9AF0F31F22DA925700026AE6 /* BarChart.swift in Sources */,
|
||||
9AF0F31B22DA924000026AE6 /* LineChart.swift in Sources */,
|
||||
9A59AE56231EE02F007989D6 /* ChartMarker.swift in Sources */,
|
||||
|
||||
@@ -12,7 +12,7 @@ import LaunchAtLogin
|
||||
|
||||
let modules: Observable<[Module]> = Observable([CPU(), Memory(), Disk(), Battery(), Network()])
|
||||
let updater = macAppUpdater(user: "exelban", repo: "stats")
|
||||
let menu = NSPopover()
|
||||
let popover = NSPopover()
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@@ -26,8 +26,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}
|
||||
|
||||
menuBarButton.action = #selector(toggleMenu)
|
||||
menu.contentViewController = MainViewController.Init()
|
||||
menu.behavior = NSPopover.Behavior.transient
|
||||
popover.contentViewController = MainViewController.Init()
|
||||
popover.behavior = NSPopover.Behavior.transient
|
||||
|
||||
_ = MenuBar(menuBarItem, menuBarButton: menuBarButton)
|
||||
|
||||
@@ -72,19 +72,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}
|
||||
|
||||
@objc func toggleMenu(_ sender: Any?) {
|
||||
if menu.isShown {
|
||||
menu.performClose(sender)
|
||||
if popover.isShown {
|
||||
popover.performClose(sender)
|
||||
} else {
|
||||
if let button = self.menuBarItem.button {
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
menu.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
|
||||
menu.becomeFirstResponder()
|
||||
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
|
||||
popover.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ notification: Notification) {
|
||||
menu.performClose(self)
|
||||
popover.performClose(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,41 +28,23 @@ class Battery: Module {
|
||||
self.available = Observable(self.reader.available)
|
||||
self.active = Observable(defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true)
|
||||
self.percentageView = Observable(defaults.object(forKey: "\(self.name)_percentage") != nil ? defaults.bool(forKey: "\(self.name)_percentage") : false)
|
||||
self.view = BatteryView(frame: NSMakeRect(0, 0, widgetSize.width, widgetSize.height))
|
||||
self.view = BatteryWidget(frame: NSMakeRect(0, 0, widgetSize.width, widgetSize.height))
|
||||
initMenu()
|
||||
initWidget()
|
||||
initTab()
|
||||
}
|
||||
|
||||
func initTab() {
|
||||
self.tabView.view?.frame = NSRect(x: 0, y: 0, width: TabWidth, height: TabHeight)
|
||||
|
||||
let text: NSTextField = NSTextField(string: self.name)
|
||||
text.isEditable = false
|
||||
text.isSelectable = false
|
||||
text.isBezeled = false
|
||||
text.wantsLayer = true
|
||||
text.textColor = .labelColor
|
||||
text.canDrawSubviewsIntoLayer = true
|
||||
text.alignment = .natural
|
||||
text.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
||||
text.frame.origin.x = ((self.tabView.view?.frame.size.width)! - 50) / 2
|
||||
text.frame.origin.y = ((self.tabView.view?.frame.size.height)! - 22) / 2
|
||||
|
||||
self.tabView.view?.addSubview(text)
|
||||
}
|
||||
|
||||
func start() {
|
||||
if !self.reader.value.value.isEmpty {
|
||||
let value = self.reader.value!.value
|
||||
(self.view as! BatteryView).setCharging(value: value.first! > 0)
|
||||
(self.view as! BatteryWidget).setCharging(value: value.first! > 0)
|
||||
(self.view as! Widget).setValue(data: [abs(value.first!)])
|
||||
}
|
||||
|
||||
self.reader.start()
|
||||
self.reader.value.subscribe(observer: self) { (value, _) in
|
||||
if !value.isEmpty {
|
||||
(self.view as! BatteryView).setCharging(value: value.first! > 0)
|
||||
(self.view as! BatteryWidget).setCharging(value: value.first! > 0)
|
||||
(self.view as! Widget).setValue(data: [abs(value.first!)])
|
||||
}
|
||||
}
|
||||
@@ -70,7 +52,7 @@ class Battery: Module {
|
||||
|
||||
func initWidget() {
|
||||
self.active << false
|
||||
(self.view as! BatteryView).setPercentage(value: self.percentageView.value)
|
||||
(self.view as! BatteryWidget).setPercentage(value: self.percentageView.value)
|
||||
self.active << true
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,45 @@
|
||||
import Foundation
|
||||
import IOKit.ps
|
||||
|
||||
struct BatteryUsage {
|
||||
var powerSource: String = ""
|
||||
var state: String = ""
|
||||
var isCharged: Bool = false
|
||||
var capacity: Double = 0
|
||||
var cycles: Int = 0
|
||||
var health: Int = 0
|
||||
|
||||
var amperage: Int = 0
|
||||
var voltage: Double = 0
|
||||
var temperature: Double = 0
|
||||
|
||||
var ACwatts: Int = 0
|
||||
var ACstatus: Bool = false
|
||||
|
||||
var timeToEmpty: Int = 0
|
||||
var timeToCharge: Int = 0
|
||||
}
|
||||
|
||||
class BatteryReader: Reader {
|
||||
var value: Observable<[Double]>!
|
||||
var available: Bool = false
|
||||
var updateTimer: Timer!
|
||||
public var value: Observable<[Double]>!
|
||||
public var usage: Observable<BatteryUsage> = Observable(BatteryUsage())
|
||||
public var updateTimer: Timer!
|
||||
|
||||
private var service: io_connect_t = 0
|
||||
private var internalChecked: Bool = false
|
||||
private var hasInternalBattery: Bool = false
|
||||
|
||||
public var available: Bool {
|
||||
get {
|
||||
if !self.internalChecked {
|
||||
let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||
let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array
|
||||
self.hasInternalBattery = sources.count > 0
|
||||
self.internalChecked = true
|
||||
}
|
||||
return self.hasInternalBattery
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
self.value = Observable([])
|
||||
@@ -20,6 +55,7 @@ class BatteryReader: Reader {
|
||||
}
|
||||
|
||||
func start() {
|
||||
self.service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleSmartBattery"))
|
||||
if updateTimer != nil {
|
||||
return
|
||||
}
|
||||
@@ -32,25 +68,101 @@ class BatteryReader: Reader {
|
||||
}
|
||||
updateTimer.invalidate()
|
||||
updateTimer = nil
|
||||
|
||||
IOServiceClose(self.service)
|
||||
IOObjectRelease(self.service)
|
||||
}
|
||||
|
||||
@objc func read() {
|
||||
let psInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||
let psList = IOPSCopyPowerSourcesList(psInfo).takeRetainedValue() as [CFTypeRef]
|
||||
self.available = psList.count != 0
|
||||
|
||||
for ps in psList {
|
||||
if let psDesc = IOPSGetPowerSourceDescription(psInfo, ps).takeUnretainedValue() as? [String: Any] {
|
||||
let powerSourceState = (psDesc[kIOPSPowerSourceStateKey] as? String)
|
||||
let isCharged = (psDesc[kIOPSIsChargedKey] as? Bool)
|
||||
var cap: Float = Float(psDesc[kIOPSCurrentCapacityKey] as! Int) / 100
|
||||
if let list = IOPSGetPowerSourceDescription(psInfo, ps).takeUnretainedValue() as? Dictionary<String, Any> {
|
||||
let powerSource = list[kIOPSPowerSourceStateKey] as? String ?? "AC Power"
|
||||
let state = list[kIOPSBatteryHealthKey] as! String
|
||||
let isCharged = list[kIOPSIsChargedKey] as? Bool ?? false
|
||||
var cap = Float(list[kIOPSCurrentCapacityKey] as! Int) / 100
|
||||
|
||||
let timeToEmpty = Int(list[kIOPSTimeToEmptyKey] as! Int)
|
||||
let timeToCharged = Int(list[kIOPSTimeToFullChargeKey] as! Int)
|
||||
|
||||
let cycles = self.getIntValue("CycleCount" as CFString) ?? 0
|
||||
|
||||
if isCharged == nil && powerSourceState! == "Battery Power" {
|
||||
let maxCapacity = self.getIntValue("MaxCapacity" as CFString) ?? 1
|
||||
let designCapacity = self.getIntValue("DesignCapacity" as CFString) ?? 1
|
||||
|
||||
let amperage = self.getIntValue("Amperage" as CFString) ?? 0
|
||||
let voltage = self.getVoltage() ?? 0
|
||||
let temperature = self.getTemperature() ?? 0
|
||||
|
||||
var ACwatts: Int = 0
|
||||
if let ACDetails = IOPSCopyExternalPowerAdapterDetails() {
|
||||
if let ACList = ACDetails.takeUnretainedValue() as? Dictionary<String, Any> {
|
||||
ACwatts = Int(ACList[kIOPSPowerAdapterWattsKey] as! Int)
|
||||
}
|
||||
}
|
||||
let ACstatus = self.getBoolValue("IsCharging" as CFString) ?? false
|
||||
|
||||
self.usage << BatteryUsage(
|
||||
powerSource: powerSource,
|
||||
state: state,
|
||||
isCharged: isCharged,
|
||||
capacity: Double(cap),
|
||||
cycles: cycles,
|
||||
health: (100 * maxCapacity) / designCapacity,
|
||||
|
||||
amperage: amperage,
|
||||
voltage: voltage,
|
||||
temperature: temperature,
|
||||
|
||||
ACwatts: ACwatts,
|
||||
ACstatus: ACstatus,
|
||||
|
||||
timeToEmpty: timeToEmpty,
|
||||
timeToCharge: timeToCharged
|
||||
)
|
||||
|
||||
if powerSource == "Battery Power" {
|
||||
cap = 0 - cap
|
||||
}
|
||||
|
||||
self.value << [Double(cap)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getBoolValue(_ forIdentifier: CFString) -> Bool? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.service, forIdentifier, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as? Bool
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getIntValue(_ identifier: CFString) -> Int? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.service, identifier, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as? Int
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDoubleValue(_ identifier: CFString) -> Double? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.service, identifier, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as? Double
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getVoltage() -> Double? {
|
||||
if let value = self.getDoubleValue("Voltage" as CFString) {
|
||||
return value / 1000.0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTemperature() -> Double? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.service, "Temperature" as CFString, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as! Double / 100.0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
258
Stats/Modules/Battery/BatteryView.swift
Normal file
258
Stats/Modules/Battery/BatteryView.swift
Normal file
@@ -0,0 +1,258 @@
|
||||
//
|
||||
// BatteryView.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 05/09/2019.
|
||||
// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
extension Battery {
|
||||
|
||||
func initTab() {
|
||||
self.tabView.view?.frame = NSRect(x: 0, y: 0, width: TabWidth, height: 10)
|
||||
|
||||
makeMain()
|
||||
makeOverview()
|
||||
makeBattery()
|
||||
makePowerAdapter()
|
||||
}
|
||||
|
||||
func makeMain() {
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: TabHeight - stackHeight*3 - 4, width: TabWidth, height: stackHeight*3))
|
||||
vertical.orientation = .vertical
|
||||
|
||||
let level: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight))
|
||||
level.orientation = .horizontal
|
||||
level.distribution = .equalCentering
|
||||
let levelLabel = LabelField(string: "Level")
|
||||
let levelValue = ValueField(string: "0%")
|
||||
level.addView(levelLabel, in: .center)
|
||||
level.addView(levelValue, in: .center)
|
||||
|
||||
let source: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
source.orientation = .horizontal
|
||||
source.distribution = .equalCentering
|
||||
let sourceLabel = LabelField(string: "Source")
|
||||
let sourceValue = ValueField(string: "AC Power")
|
||||
source.addView(sourceLabel, in: .center)
|
||||
source.addView(sourceValue, in: .center)
|
||||
|
||||
let time: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
time.orientation = .horizontal
|
||||
time.distribution = .equalCentering
|
||||
let timeLabel = LabelField(string: "Time to charge")
|
||||
let timeValue = ValueField(string: "Calculating")
|
||||
time.addView(timeLabel, in: .center)
|
||||
time.addView(timeValue, in: .center)
|
||||
|
||||
vertical.addSubview(level)
|
||||
vertical.addSubview(source)
|
||||
vertical.addSubview(time)
|
||||
|
||||
self.tabView.view?.addSubview(vertical)
|
||||
|
||||
(self.reader as! BatteryReader).usage.subscribe(observer: self) { (value, _) in
|
||||
levelValue.stringValue = "\(Int(value.capacity * 100))%"
|
||||
sourceValue.stringValue = value.powerSource
|
||||
|
||||
if value.powerSource == "Battery Power" {
|
||||
timeLabel.stringValue = "Time to discharge"
|
||||
if value.timeToEmpty != -1 && value.timeToEmpty != 0 {
|
||||
timeValue.stringValue = Double(value.timeToEmpty*60).printSecondsToHoursMinutesSeconds()
|
||||
}
|
||||
} else {
|
||||
timeLabel.stringValue = "Time to charge"
|
||||
if value.timeToCharge != -1 && value.timeToCharge != 0 {
|
||||
timeValue.stringValue = Double(value.timeToCharge*60).printSecondsToHoursMinutesSeconds()
|
||||
}
|
||||
}
|
||||
|
||||
if value.timeToEmpty == -1 || value.timeToEmpty == -1 {
|
||||
timeValue.stringValue = "Calculating"
|
||||
}
|
||||
|
||||
if value.isCharged {
|
||||
timeValue.stringValue = "Fully charged"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeOverview() {
|
||||
let overviewLabel: NSView = NSView(frame: NSRect(x: 0, y: TabHeight - 102, width: TabWidth, height: 25))
|
||||
|
||||
overviewLabel.wantsLayer = true
|
||||
overviewLabel.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor
|
||||
|
||||
let overviewText: NSTextField = NSTextField(string: "Overview")
|
||||
overviewText.frame = NSRect(x: 0, y: 0, width: TabWidth, height: overviewLabel.frame.size.height - 4)
|
||||
overviewText.isEditable = false
|
||||
overviewText.isSelectable = false
|
||||
overviewText.isBezeled = false
|
||||
overviewText.wantsLayer = true
|
||||
overviewText.textColor = .darkGray
|
||||
overviewText.canDrawSubviewsIntoLayer = true
|
||||
overviewText.alignment = .center
|
||||
overviewText.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
overviewText.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
overviewLabel.addSubview(overviewText)
|
||||
self.tabView.view?.addSubview(overviewLabel)
|
||||
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: 184, width: TabWidth, height: stackHeight*3))
|
||||
vertical.orientation = .vertical
|
||||
|
||||
let cycles: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight))
|
||||
cycles.orientation = .horizontal
|
||||
cycles.distribution = .equalCentering
|
||||
let cyclesLabel = LabelField(string: "Cycles")
|
||||
let cyclesValue = ValueField(string: "0")
|
||||
cycles.addView(cyclesLabel, in: .center)
|
||||
cycles.addView(cyclesValue, in: .center)
|
||||
|
||||
let health: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
health.orientation = .horizontal
|
||||
health.distribution = .equalCentering
|
||||
let healthLabel = LabelField(string: "Health")
|
||||
let healthValue = ValueField(string: "Calculating")
|
||||
health.addView(healthLabel, in: .center)
|
||||
health.addView(healthValue, in: .center)
|
||||
|
||||
let state: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
state.orientation = .horizontal
|
||||
state.distribution = .equalCentering
|
||||
let stateLabel = LabelField(string: "State")
|
||||
let stateValue = ValueField(string: "Calculating")
|
||||
state.addView(stateLabel, in: .center)
|
||||
state.addView(stateValue, in: .center)
|
||||
|
||||
vertical.addSubview(cycles)
|
||||
vertical.addSubview(health)
|
||||
vertical.addSubview(state)
|
||||
|
||||
self.tabView.view?.addSubview(vertical)
|
||||
|
||||
(self.reader as! BatteryReader).usage.subscribe(observer: self) { (value, _) in
|
||||
cyclesValue.stringValue = "\(value.cycles)"
|
||||
stateValue.stringValue = value.state
|
||||
healthValue.stringValue = "\(value.health)%"
|
||||
}
|
||||
}
|
||||
|
||||
func makeBattery() {
|
||||
let batteryLabel: NSView = NSView(frame: NSRect(x: 0, y: TabHeight - 202, width: TabWidth, height: 25))
|
||||
|
||||
batteryLabel.wantsLayer = true
|
||||
batteryLabel.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor
|
||||
|
||||
let overviewText: NSTextField = NSTextField(string: "Battery")
|
||||
overviewText.frame = NSRect(x: 0, y: 0, width: TabWidth, height: batteryLabel.frame.size.height - 4)
|
||||
overviewText.isEditable = false
|
||||
overviewText.isSelectable = false
|
||||
overviewText.isBezeled = false
|
||||
overviewText.wantsLayer = true
|
||||
overviewText.textColor = .darkGray
|
||||
overviewText.canDrawSubviewsIntoLayer = true
|
||||
overviewText.alignment = .center
|
||||
overviewText.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
overviewText.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
batteryLabel.addSubview(overviewText)
|
||||
self.tabView.view?.addSubview(batteryLabel)
|
||||
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: TabHeight - 273, width: TabWidth, height: stackHeight*3))
|
||||
vertical.orientation = .vertical
|
||||
|
||||
let amperage: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight))
|
||||
amperage.orientation = .horizontal
|
||||
amperage.distribution = .equalCentering
|
||||
let amperageLabel = LabelField(string: "Amperage")
|
||||
let amperageValue = ValueField(string: "0 mA")
|
||||
amperage.addView(amperageLabel, in: .center)
|
||||
amperage.addView(amperageValue, in: .center)
|
||||
|
||||
let voltage: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
voltage.orientation = .horizontal
|
||||
voltage.distribution = .equalCentering
|
||||
let voltageLabel = LabelField(string: "Voltage")
|
||||
let voltageValue = ValueField(string: "0 V")
|
||||
voltage.addView(voltageLabel, in: .center)
|
||||
voltage.addView(voltageValue, in: .center)
|
||||
|
||||
let temperature: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
temperature.orientation = .horizontal
|
||||
temperature.distribution = .equalCentering
|
||||
let temperatureLabel = LabelField(string: "Temperature")
|
||||
let temperatureValue = ValueField(string: "0 °C")
|
||||
temperature.addView(temperatureLabel, in: .center)
|
||||
temperature.addView(temperatureValue, in: .center)
|
||||
|
||||
vertical.addSubview(amperage)
|
||||
vertical.addSubview(voltage)
|
||||
vertical.addSubview(temperature)
|
||||
|
||||
self.tabView.view?.addSubview(vertical)
|
||||
(self.reader as! BatteryReader).usage.subscribe(observer: self) { (value, _) in
|
||||
amperageValue.stringValue = "\(value.amperage) mA"
|
||||
voltageValue.stringValue = "\(value.voltage.roundTo(decimalPlaces: 2)) V"
|
||||
temperatureValue.stringValue = "\(value.temperature) °C"
|
||||
}
|
||||
}
|
||||
|
||||
func makePowerAdapter() {
|
||||
let powerAdapterLabel: NSView = NSView(frame: NSRect(x: 0, y: 52, width: TabWidth, height: 25))
|
||||
|
||||
powerAdapterLabel.wantsLayer = true
|
||||
powerAdapterLabel.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor
|
||||
|
||||
let overviewText: NSTextField = NSTextField(string: "Power adapter")
|
||||
overviewText.frame = NSRect(x: 0, y: 0, width: TabWidth, height: powerAdapterLabel.frame.size.height - 4)
|
||||
overviewText.isEditable = false
|
||||
overviewText.isSelectable = false
|
||||
overviewText.isBezeled = false
|
||||
overviewText.wantsLayer = true
|
||||
overviewText.textColor = .darkGray
|
||||
overviewText.canDrawSubviewsIntoLayer = true
|
||||
overviewText.alignment = .center
|
||||
overviewText.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
overviewText.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
powerAdapterLabel.addSubview(overviewText)
|
||||
self.tabView.view?.addSubview(powerAdapterLabel)
|
||||
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: 4, width: TabWidth, height: stackHeight*2))
|
||||
vertical.orientation = .vertical
|
||||
|
||||
let power: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
power.orientation = .horizontal
|
||||
power.distribution = .equalCentering
|
||||
let powerLabel = LabelField(string: "Power")
|
||||
let powerValue = ValueField(string: "0 W")
|
||||
power.addView(powerLabel, in: .center)
|
||||
power.addView(powerValue, in: .center)
|
||||
|
||||
let charging: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
charging.orientation = .horizontal
|
||||
charging.distribution = .equalCentering
|
||||
let chargingLabel = LabelField(string: "Is charging")
|
||||
let chargingValue = ValueField(string: "No")
|
||||
charging.addView(chargingLabel, in: .center)
|
||||
charging.addView(chargingValue, in: .center)
|
||||
|
||||
vertical.addSubview(power)
|
||||
vertical.addSubview(charging)
|
||||
|
||||
self.tabView.view?.addSubview(vertical)
|
||||
|
||||
(self.reader as! BatteryReader).usage.subscribe(observer: self) { (value, _) in
|
||||
powerValue.stringValue = value.powerSource == "Battery Power" ? "Not connected" : "\(value.ACwatts) W"
|
||||
chargingValue.stringValue = value.ACstatus ? "Yes" : "No"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class CPUReader: Reader {
|
||||
self.value = Observable([])
|
||||
|
||||
self.topProcess.launchPath = "/usr/bin/top"
|
||||
self.topProcess.arguments = ["-s", "1", "-o", "cpu", "-n", "5", "-stats", "pid,command,cpu"]
|
||||
self.topProcess.arguments = ["-s", "3", "-o", "cpu", "-n", "5", "-stats", "pid,command,cpu"]
|
||||
self.topProcess.standardOutput = pipe
|
||||
|
||||
mibKeys.withUnsafeBufferPointer() { mib in
|
||||
|
||||
@@ -121,24 +121,24 @@ extension CPU {
|
||||
let system: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight))
|
||||
system.orientation = .horizontal
|
||||
system.distribution = .equalCentering
|
||||
let systemLabel = labelField(string: "System")
|
||||
let systemValue = valueField(string: "0 %")
|
||||
let systemLabel = LabelField(string: "System")
|
||||
let systemValue = ValueField(string: "0 %")
|
||||
system.addView(systemLabel, in: .center)
|
||||
system.addView(systemValue, in: .center)
|
||||
|
||||
let user: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
user.orientation = .horizontal
|
||||
user.distribution = .equalCentering
|
||||
let userLabel = labelField(string: "User")
|
||||
let userValue = valueField(string: "0 %")
|
||||
let userLabel = LabelField(string: "User")
|
||||
let userValue = ValueField(string: "0 %")
|
||||
user.addView(userLabel, in: .center)
|
||||
user.addView(userValue, in: .center)
|
||||
|
||||
let idle: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
idle.orientation = .horizontal
|
||||
idle.distribution = .equalCentering
|
||||
let idleLabel = labelField(string: "Idle")
|
||||
let idleValue = valueField(string: "0 %")
|
||||
let idleLabel = LabelField(string: "Idle")
|
||||
let idleValue = ValueField(string: "0 %")
|
||||
idle.addView(idleLabel, in: .center)
|
||||
idle.addView(idleValue, in: .center)
|
||||
|
||||
@@ -220,39 +220,11 @@ extension CPU {
|
||||
let view: NSStackView = NSStackView(frame: NSRect(x: 10, y: CGFloat(num)*height, width: TabWidth - 20, height: height))
|
||||
view.orientation = .horizontal
|
||||
view.distribution = .equalCentering
|
||||
let viewLabel = labelField(string: label)
|
||||
let viewValue = valueField(string: value)
|
||||
let viewLabel = LabelField(string: label)
|
||||
let viewValue = ValueField(string: value)
|
||||
view.addView(viewLabel, in: .center)
|
||||
view.addView(viewValue, in: .center)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func labelField(string: String) -> NSTextField {
|
||||
let label: NSTextField = NSTextField(string: string)
|
||||
|
||||
label.isEditable = false
|
||||
label.isSelectable = false
|
||||
label.isBezeled = false
|
||||
label.textColor = .black
|
||||
label.alignment = .center
|
||||
label.font = NSFont.systemFont(ofSize: 12, weight: .regular)
|
||||
label.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
func valueField(string: String) -> NSTextField {
|
||||
let label: NSTextField = NSTextField(string: string)
|
||||
|
||||
label.isEditable = false
|
||||
label.isSelectable = false
|
||||
label.isBezeled = false
|
||||
label.textColor = .black
|
||||
label.alignment = .center
|
||||
label.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
||||
label.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
|
||||
return label
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class MemoryReader: Reader {
|
||||
var count = UInt32(MemoryLayout<host_basic_info_data_t>.size / MemoryLayout<integer_t>.size)
|
||||
|
||||
self.topProcess.launchPath = "/usr/bin/top"
|
||||
self.topProcess.arguments = ["-s", "1", "-o", "mem", "-n", "5", "-stats", "pid,command,mem"]
|
||||
self.topProcess.arguments = ["-s", "3", "-o", "mem", "-n", "5", "-stats", "pid,command,mem"]
|
||||
self.topProcess.standardOutput = pipe
|
||||
|
||||
let kerr: kern_return_t = withUnsafeMutablePointer(to: &stats) {
|
||||
@@ -79,8 +79,10 @@ class MemoryReader: Reader {
|
||||
let arr = line.condenseWhitespace().split(separator: " ")
|
||||
let pid = Int(arr[0]) ?? 0
|
||||
let command = String(arr[1])
|
||||
let usage = Double(arr[2].filter("01234567890.".contains))! * Double(1024 * 1024)
|
||||
let process = TopProcess(pid: pid, command: command, usage: usage)
|
||||
guard let usage = Double(arr[2].filter("01234567890.".contains)) else {
|
||||
return
|
||||
}
|
||||
let process = TopProcess(pid: pid, command: command, usage: usage * Double(1024 * 1024))
|
||||
processes.append(process)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,24 +121,24 @@ extension Memory {
|
||||
let total: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight))
|
||||
total.orientation = .horizontal
|
||||
total.distribution = .equalCentering
|
||||
let totalLabel = labelField(string: "Total")
|
||||
let totalValue = valueField(string: "0 GB")
|
||||
let totalLabel = LabelField(string: "Total")
|
||||
let totalValue = ValueField(string: "0 GB")
|
||||
total.addView(totalLabel, in: .center)
|
||||
total.addView(totalValue, in: .center)
|
||||
|
||||
let used: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
used.orientation = .horizontal
|
||||
used.distribution = .equalCentering
|
||||
let usedLabel = labelField(string: "Used")
|
||||
let usedValue = valueField(string: "0 GB")
|
||||
let usedLabel = LabelField(string: "Used")
|
||||
let usedValue = ValueField(string: "0 GB")
|
||||
used.addView(usedLabel, in: .center)
|
||||
used.addView(usedValue, in: .center)
|
||||
|
||||
let free: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
free.orientation = .horizontal
|
||||
free.distribution = .equalCentering
|
||||
let freeLabel = labelField(string: "Free")
|
||||
let freeValue = valueField(string: "0 GB")
|
||||
let freeLabel = LabelField(string: "Free")
|
||||
let freeValue = ValueField(string: "0 GB")
|
||||
free.addView(freeLabel, in: .center)
|
||||
free.addView(freeValue, in: .center)
|
||||
|
||||
@@ -220,39 +220,11 @@ extension Memory {
|
||||
let view: NSStackView = NSStackView(frame: NSRect(x: 10, y: CGFloat(num)*height, width: TabWidth - 20, height: height))
|
||||
view.orientation = .horizontal
|
||||
view.distribution = .equalCentering
|
||||
let viewLabel = labelField(string: label)
|
||||
let viewValue = valueField(string: value)
|
||||
let viewLabel = LabelField(string: label)
|
||||
let viewValue = ValueField(string: value)
|
||||
view.addView(viewLabel, in: .center)
|
||||
view.addView(viewValue, in: .center)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func labelField(string: String) -> NSTextField {
|
||||
let label: NSTextField = NSTextField(string: string)
|
||||
|
||||
label.isEditable = false
|
||||
label.isSelectable = false
|
||||
label.isBezeled = false
|
||||
label.textColor = .black
|
||||
label.alignment = .center
|
||||
label.font = NSFont.systemFont(ofSize: 12, weight: .regular)
|
||||
label.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
func valueField(string: String) -> NSTextField {
|
||||
let label: NSTextField = NSTextField(string: string)
|
||||
|
||||
label.isEditable = false
|
||||
label.isSelectable = false
|
||||
label.isBezeled = false
|
||||
label.textColor = .black
|
||||
label.alignment = .center
|
||||
label.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
||||
label.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
|
||||
return label
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ class MainViewController: NSViewController {
|
||||
|
||||
self.segmentsControl = NSSegmentedControl(labels: items, trackingMode: NSSegmentedControl.SwitchTracking.selectOne, target: self, action: #selector(switchTabs))
|
||||
self.segmentsControl.setSelected(true, forSegment: 0)
|
||||
// self.tabView.selectTabViewItem(at: 2)
|
||||
self.segmentsControl.segmentDistribution = .fillEqually
|
||||
|
||||
let button = NSButton(frame: NSRect(x: 0, y: 0, width: 26, height: 20))
|
||||
@@ -164,3 +165,32 @@ class MainViewController: NSViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func LabelField(string: String) -> NSTextField {
|
||||
let label: NSTextField = NSTextField(string: string)
|
||||
|
||||
label.isEditable = false
|
||||
label.isSelectable = false
|
||||
label.isBezeled = false
|
||||
label.textColor = .black
|
||||
label.alignment = .center
|
||||
label.font = NSFont.systemFont(ofSize: 12, weight: .regular)
|
||||
label.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
func ValueField(string: String) -> NSTextField {
|
||||
let label: NSTextField = NSTextField(string: string)
|
||||
|
||||
label.isEditable = false
|
||||
label.isSelectable = false
|
||||
label.isBezeled = false
|
||||
label.textColor = .black
|
||||
label.alignment = .center
|
||||
label.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
||||
label.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
class BatteryView: NSView, Widget {
|
||||
class BatteryWidget: NSView, Widget {
|
||||
var activeModule: Observable<Bool> = Observable(false)
|
||||
var size: CGFloat = widgetSize.width
|
||||
var name: String = ""
|
||||
@@ -120,6 +120,39 @@ public struct Units {
|
||||
}
|
||||
}
|
||||
|
||||
extension Double {
|
||||
|
||||
func secondsToHoursMinutesSeconds () -> (Int?, Int?, Int?) {
|
||||
let hrs = self / 3600
|
||||
let mins = (self.truncatingRemainder(dividingBy: 3600)) / 60
|
||||
let seconds = (self.truncatingRemainder(dividingBy:3600)).truncatingRemainder(dividingBy:60)
|
||||
return (Int(hrs) > 0 ? Int(hrs) : nil , Int(mins) > 0 ? Int(mins) : nil, Int(seconds) > 0 ? Int(seconds) : nil)
|
||||
}
|
||||
|
||||
func printSecondsToHoursMinutesSeconds () -> String {
|
||||
let time = self.secondsToHoursMinutesSeconds()
|
||||
|
||||
switch time {
|
||||
case (nil, let x? , let y?):
|
||||
return "\(x) min \(y) sec"
|
||||
case (nil, let x?, nil):
|
||||
return "\(x) min"
|
||||
case (let x?, nil, nil):
|
||||
return "\(x) hr"
|
||||
case (nil, nil, let x?):
|
||||
return "\(x) sec"
|
||||
case (let x?, nil, let z?):
|
||||
return "\(x) hr \(z) sec"
|
||||
case (let x?, let y?, nil):
|
||||
return "\(x) hr \(y) min"
|
||||
case (let x?, let y?, let z?):
|
||||
return "\(x) hr \(y) min \(z) sec"
|
||||
default:
|
||||
return "n/a"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
func condenseWhitespace() -> String {
|
||||
let components = self.components(separatedBy: .whitespacesAndNewlines)
|
||||
|
||||
Reference in New Issue
Block a user