mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
feat: add fan speed control panel to the popup (#152)
This commit is contained in:
@@ -39,12 +39,13 @@ public class Fans: Module {
|
||||
|
||||
private var fansReader: FansReader
|
||||
private var settingsView: Settings
|
||||
private let popupView: Popup = Popup()
|
||||
private let popupView: Popup
|
||||
|
||||
public init(_ smc: UnsafePointer<SMCService>) {
|
||||
self.smc = smc
|
||||
self.fansReader = FansReader(smc)
|
||||
self.settingsView = Settings("Fans", list: &self.fansReader.list)
|
||||
self.popupView = Popup(smc)
|
||||
|
||||
super.init(
|
||||
popup: self.popupView,
|
||||
|
||||
@@ -13,55 +13,33 @@ import Cocoa
|
||||
import ModuleKit
|
||||
import StatsKit
|
||||
|
||||
internal class Popup: NSView, Popup_p {
|
||||
private var list: [Int: FanView] = [:]
|
||||
|
||||
internal class Popup: NSStackView, Popup_p {
|
||||
public var sizeCallback: ((NSSize) -> Void)? = nil
|
||||
|
||||
public init() {
|
||||
super.init(frame: NSRect( x: 0, y: 0, width: Constants.Popup.width, height: 0))
|
||||
private var smc: UnsafePointer<SMCService>
|
||||
private var list: [Int: FanView] = [:]
|
||||
|
||||
public init(_ smc: UnsafePointer<SMCService>) {
|
||||
self.smc = smc
|
||||
|
||||
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 setup(_ values: [Fan]?) {
|
||||
guard values != nil else {
|
||||
return
|
||||
internal func setup(_ values: [Fan]) {
|
||||
values.forEach { (f: Fan) in
|
||||
let view = FanView(f, smc: self.smc, width: self.frame.width, callback: self.recalculateHeight)
|
||||
self.list[f.id] = view
|
||||
self.addArrangedSubview(view)
|
||||
}
|
||||
|
||||
self.subviews.forEach { (v: NSView) in
|
||||
v.removeFromSuperview()
|
||||
}
|
||||
|
||||
let fanViewHeight: CGFloat = 40
|
||||
let view: NSView = NSView(frame: NSRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.frame.width,
|
||||
height: ((fanViewHeight+Constants.Popup.margins)*CGFloat(values!.count))-Constants.Popup.margins
|
||||
))
|
||||
var i: CGFloat = 0
|
||||
|
||||
values!.reversed().forEach { (f: Fan) in
|
||||
let fanView = FanView(
|
||||
NSRect(
|
||||
x: 0,
|
||||
y: (fanViewHeight + Constants.Popup.margins) * i,
|
||||
width: self.frame.width,
|
||||
height: fanViewHeight
|
||||
),
|
||||
fan: f
|
||||
)
|
||||
self.list[f.id] = fanView
|
||||
view.addSubview(fanView)
|
||||
i += 1
|
||||
}
|
||||
self.addSubview(view)
|
||||
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: view.frame.height))
|
||||
self.sizeCallback?(self.frame.size)
|
||||
self.recalculateHeight()
|
||||
}
|
||||
|
||||
internal func usageCallback(_ values: [Fan]) {
|
||||
@@ -75,29 +53,66 @@ internal class Popup: NSView, Popup_p {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func recalculateHeight() {
|
||||
let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing
|
||||
if self.frame.size.height != h {
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: h))
|
||||
self.sizeCallback?(self.frame.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class FanView: NSView {
|
||||
private let fan: Fan
|
||||
private var mainView: NSView
|
||||
internal class FanView: NSStackView {
|
||||
public var sizeCallback: (() -> Void)
|
||||
|
||||
private var smc: UnsafePointer<SMCService>
|
||||
private var fan: Fan
|
||||
private var ready: Bool = false
|
||||
|
||||
private var valueField: NSTextField? = nil
|
||||
private var percentageField: NSTextField? = nil
|
||||
private var sliderValueField: NSTextField? = nil
|
||||
|
||||
private var ready: Bool = false
|
||||
private var slider: NSSlider? = nil
|
||||
private var controlView: NSView? = nil
|
||||
private var debouncer: DispatchWorkItem? = nil
|
||||
|
||||
public init(_ frame: NSRect, fan: Fan) {
|
||||
public init(_ fan: Fan, smc: UnsafePointer<SMCService>, width: CGFloat, callback: @escaping (() -> Void)) {
|
||||
self.fan = fan
|
||||
self.mainView = NSView(frame: NSRect(x: 5, y: 5, width: frame.width - 10, height: frame.height - 10))
|
||||
super.init(frame: frame)
|
||||
self.smc = smc
|
||||
self.sizeCallback = callback
|
||||
|
||||
let inset: CGFloat = 5
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: width - (inset*2), height: 0))
|
||||
|
||||
self.controlView = self.control()
|
||||
|
||||
self.orientation = .vertical
|
||||
self.alignment = .centerX
|
||||
self.distribution = .fillProportionally
|
||||
self.spacing = 0
|
||||
self.edgeInsets = NSEdgeInsets(
|
||||
top: inset,
|
||||
left: inset,
|
||||
bottom: inset,
|
||||
right: inset
|
||||
)
|
||||
self.wantsLayer = true
|
||||
self.layer?.cornerRadius = 2
|
||||
self.layer?.backgroundColor = NSColor.red.cgColor
|
||||
|
||||
self.addFirstRow()
|
||||
self.addSecondRow()
|
||||
self.addArrangedSubview(self.nameAndSpeed())
|
||||
self.addArrangedSubview(self.keyAndPercentage())
|
||||
self.addArrangedSubview(self.mode())
|
||||
|
||||
self.addSubview(self.mainView)
|
||||
if let view = self.controlView, fan.mode == .forced {
|
||||
self.addArrangedSubview(view)
|
||||
}
|
||||
|
||||
let h = self.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +) + (inset*2)
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: h))
|
||||
self.sizeCallback()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -108,47 +123,58 @@ internal class FanView: NSView {
|
||||
self.layer?.backgroundColor = isDarkMode ? NSColor(hexString: "#111111", alpha: 0.25).cgColor : NSColor(hexString: "#f5f5f5", alpha: 1).cgColor
|
||||
}
|
||||
|
||||
private func addFirstRow() {
|
||||
let row: NSView = NSView(frame: NSRect(x: 0, y: 14, width: self.mainView.frame.width, height: 16))
|
||||
private func nameAndSpeed() -> NSView {
|
||||
let row: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 16))
|
||||
row.heightAnchor.constraint(equalToConstant: row.bounds.height).isActive = true
|
||||
|
||||
let value = self.fan.formattedValue
|
||||
let valueWidth: CGFloat = 80
|
||||
|
||||
let nameField: NSTextField = TextView(frame: NSRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.mainView.frame.width - valueWidth,
|
||||
width: row.frame.width - valueWidth,
|
||||
height: row.frame.height
|
||||
))
|
||||
nameField.stringValue = self.fan.name
|
||||
nameField.cell?.truncatesLastVisibleLine = true
|
||||
|
||||
let valueField: NSTextField = TextView(frame: NSRect(
|
||||
x: self.mainView.frame.width - valueWidth,
|
||||
x: row.frame.width - valueWidth,
|
||||
y: 0,
|
||||
width: valueWidth,
|
||||
height: row.frame.height
|
||||
))
|
||||
valueField.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
||||
valueField.stringValue = value
|
||||
valueField.stringValue = self.fan.formattedValue
|
||||
valueField.alignment = .right
|
||||
|
||||
row.addSubview(nameField)
|
||||
row.addSubview(valueField)
|
||||
|
||||
self.mainView.addSubview(row)
|
||||
self.valueField = valueField
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
private func addSecondRow() {
|
||||
let row: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.mainView.frame.width, height: 14))
|
||||
private func keyAndPercentage() -> NSView {
|
||||
let row: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 14))
|
||||
row.heightAnchor.constraint(equalToConstant: row.bounds.height).isActive = true
|
||||
|
||||
let value = self.fan.value
|
||||
let percentage = "\((100*Int(value)) / Int(self.fan.maxSpeed))%"
|
||||
let percentageWidth: CGFloat = 40
|
||||
|
||||
let keyField: NSTextField = TextView(frame: NSRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: row.frame.width - percentageWidth,
|
||||
height: row.frame.height
|
||||
))
|
||||
keyField.font = NSFont.systemFont(ofSize: 11, weight: .light)
|
||||
keyField.textColor = .secondaryLabelColor
|
||||
keyField.stringValue = "Fan #\(self.fan.id)"
|
||||
keyField.alignment = .left
|
||||
|
||||
let percentageField: NSTextField = TextView(frame: NSRect(
|
||||
x: self.mainView.frame.width - percentageWidth,
|
||||
x: row.frame.width - percentageWidth,
|
||||
y: 0,
|
||||
width: percentageWidth,
|
||||
height: row.frame.height
|
||||
@@ -158,15 +184,150 @@ internal class FanView: NSView {
|
||||
percentageField.stringValue = percentage
|
||||
percentageField.alignment = .right
|
||||
|
||||
row.addSubview(keyField)
|
||||
row.addSubview(percentageField)
|
||||
self.mainView.addSubview(row)
|
||||
self.percentageField = percentageField
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
private func mode() -> NSView {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 30))
|
||||
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
|
||||
|
||||
let buttons = ModeButtons(frame: NSRect(
|
||||
x: 0,
|
||||
y: 4,
|
||||
width: view.frame.width,
|
||||
height: view.frame.height - 8
|
||||
), mode: self.fan.mode)
|
||||
buttons.callback = { [weak self] (mode: FanMode) in
|
||||
self?.fan.mode = mode
|
||||
if let fan = self?.fan {
|
||||
self?.smc.pointee.setFanMode(fan.id, mode: mode)
|
||||
}
|
||||
self?.toggleMode()
|
||||
}
|
||||
|
||||
let rootBtn: NSButton = NSButton(frame: NSRect(x: 0, y: 4, width: view.frame.width, height: view.frame.height - 8))
|
||||
rootBtn.title = "Control fan (root required)"
|
||||
rootBtn.setButtonType(.momentaryLight)
|
||||
rootBtn.isBordered = false
|
||||
rootBtn.target = self
|
||||
rootBtn.action = #selector(self.askForRoot)
|
||||
rootBtn.wantsLayer = true
|
||||
rootBtn.layer?.cornerRadius = 3
|
||||
rootBtn.layer?.borderWidth = 1
|
||||
rootBtn.layer?.borderColor = NSColor.lightGray.cgColor
|
||||
|
||||
view.addSubview(isRoot() ? buttons : rootBtn)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private func control() -> NSView {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 44))
|
||||
view.identifier = NSUserInterfaceItemIdentifier(rawValue: "control")
|
||||
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
|
||||
|
||||
let controls: NSStackView = NSStackView(frame: NSRect(x: 0, y: 14, width: view.frame.width, height: 30))
|
||||
controls.orientation = .horizontal
|
||||
controls.spacing = 0
|
||||
|
||||
let slider: NSSlider = NSSlider(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 26))
|
||||
slider.minValue = self.fan.minSpeed
|
||||
slider.doubleValue = self.fan.value
|
||||
slider.maxValue = self.fan.maxSpeed
|
||||
slider.isContinuous = true
|
||||
slider.action = #selector(self.speedChange)
|
||||
slider.target = self
|
||||
|
||||
let levels: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 14))
|
||||
|
||||
let minField: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 80, height: levels.frame.height))
|
||||
minField.font = NSFont.systemFont(ofSize: 11, weight: .light)
|
||||
minField.textColor = .secondaryLabelColor
|
||||
minField.stringValue = "\(LocalizedString("Min")): \(Int(self.fan.minSpeed))"
|
||||
minField.alignment = .left
|
||||
|
||||
let valueField: NSTextField = TextView(frame: NSRect(x: 80, y: 0, width: levels.frame.width - 160, height: levels.frame.height))
|
||||
valueField.font = NSFont.systemFont(ofSize: 11, weight: .light)
|
||||
valueField.textColor = .secondaryLabelColor
|
||||
valueField.alignment = .center
|
||||
|
||||
let maxField: NSTextField = TextView(frame: NSRect(x: levels.frame.width - 80, y: 0, width: 80, height: levels.frame.height))
|
||||
maxField.font = NSFont.systemFont(ofSize: 11, weight: .light)
|
||||
maxField.textColor = .secondaryLabelColor
|
||||
maxField.stringValue = "\(LocalizedString("Max")): \(Int(self.fan.maxSpeed))"
|
||||
maxField.alignment = .right
|
||||
|
||||
controls.addArrangedSubview(slider)
|
||||
|
||||
levels.addSubview(minField)
|
||||
levels.addSubview(valueField)
|
||||
levels.addSubview(maxField)
|
||||
|
||||
view.addSubview(controls)
|
||||
view.addSubview(levels)
|
||||
|
||||
self.slider = slider
|
||||
self.sliderValueField = valueField
|
||||
return view
|
||||
}
|
||||
|
||||
@objc private func askForRoot(_ sender: NSButton) {
|
||||
DispatchQueue.main.async {
|
||||
ensureRoot()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func speedChange(_ sender: NSSlider) {
|
||||
guard let field = self.sliderValueField else {
|
||||
return
|
||||
}
|
||||
|
||||
let value = sender.doubleValue
|
||||
field.stringValue = "\(Int(value)) RPM"
|
||||
field.textColor = .secondaryLabelColor
|
||||
|
||||
self.debouncer?.cancel()
|
||||
|
||||
let task = DispatchWorkItem { [weak self] in
|
||||
DispatchQueue.global(qos: .userInteractive).async { [weak self] in
|
||||
if let id = self?.fan.id {
|
||||
self?.smc.pointee.setFanSpeed(id, speed: Int(value))
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
field.textColor = .systemBlue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.debouncer = task
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3, execute: task)
|
||||
}
|
||||
|
||||
private func toggleMode() {
|
||||
guard let view = self.controlView else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.fan.mode == .automatic {
|
||||
view.removeFromSuperview()
|
||||
self.sliderValueField?.stringValue = ""
|
||||
self.slider?.doubleValue = self.fan.minSpeed
|
||||
} else if self.fan.mode == .forced {
|
||||
self.addArrangedSubview(view)
|
||||
}
|
||||
|
||||
let h = self.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +) + 10
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: h))
|
||||
self.sizeCallback()
|
||||
}
|
||||
|
||||
public func update(_ value: Fan) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if (self.window?.isVisible ?? false) || !self.ready {
|
||||
|
||||
if let view = self.valueField {
|
||||
view.stringValue = value.formattedValue
|
||||
}
|
||||
@@ -180,3 +341,60 @@ internal class FanView: NSView {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private class ModeButtons: NSStackView {
|
||||
public var callback: (FanMode) -> Void = {_ in }
|
||||
|
||||
private var autoBtn: NSButton = NSButton(title: "Automatic", target: nil, action: #selector(autoMode))
|
||||
private var manualBtn: NSButton = NSButton(title: "Manual", target: nil, action: #selector(manualMode))
|
||||
|
||||
public init(frame: NSRect, mode: FanMode) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.orientation = .horizontal
|
||||
self.alignment = .centerY
|
||||
self.distribution = .fillEqually
|
||||
|
||||
self.wantsLayer = true
|
||||
self.layer?.cornerRadius = 3
|
||||
self.layer?.borderWidth = 1
|
||||
self.layer?.borderColor = NSColor.lightGray.cgColor
|
||||
|
||||
self.autoBtn.setButtonType(.toggle)
|
||||
self.autoBtn.isBordered = false
|
||||
self.autoBtn.target = self
|
||||
self.autoBtn.state = mode == .automatic ? .on : .off
|
||||
|
||||
self.manualBtn.setButtonType(.toggle)
|
||||
self.manualBtn.isBordered = false
|
||||
self.manualBtn.target = self
|
||||
self.manualBtn.state = mode == .forced ? .on : .off
|
||||
|
||||
self.addArrangedSubview(self.autoBtn)
|
||||
self.addArrangedSubview(self.manualBtn)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc func autoMode(_ sender: NSButton) {
|
||||
if sender.state.rawValue == 0 {
|
||||
self.autoBtn.state = .on
|
||||
return
|
||||
}
|
||||
|
||||
self.manualBtn.state = .off
|
||||
self.callback(.automatic)
|
||||
}
|
||||
|
||||
@objc func manualMode(_ sender: NSButton) {
|
||||
if sender.state.rawValue == 0 {
|
||||
self.manualBtn.state = .on
|
||||
return
|
||||
}
|
||||
|
||||
self.autoBtn.state = .off
|
||||
self.callback(.forced)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"Yes" = "Yes";
|
||||
"No" = "No";
|
||||
"Automatic" = "Automatic";
|
||||
"Manual" = "Manual";
|
||||
"None" = "None";
|
||||
"Dots" = "Dots";
|
||||
"Arrows" = "Arrows";
|
||||
@@ -31,6 +32,8 @@
|
||||
"Short" = "Short";
|
||||
"Long" = "Long";
|
||||
"Statistics" = "Statistics";
|
||||
"Max" = "Max";
|
||||
"Min" = "Min";
|
||||
|
||||
// Alerts
|
||||
"New version available" = "New version available";
|
||||
|
||||
@@ -105,8 +105,6 @@ internal struct SMCVal_t {
|
||||
}
|
||||
|
||||
public class SMCService {
|
||||
public static let shared = SMCService()
|
||||
|
||||
private var conn: io_connect_t = 0
|
||||
|
||||
public init() {
|
||||
@@ -136,13 +134,6 @@ public class SMCService {
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
let result = self.close()
|
||||
if (result != kIOReturnSuccess) {
|
||||
print("error close smc connection: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
}
|
||||
}
|
||||
|
||||
public func close() -> kern_return_t{
|
||||
return IOServiceClose(conn)
|
||||
}
|
||||
@@ -355,28 +346,28 @@ public class SMCService {
|
||||
}
|
||||
|
||||
public func setFanMode(_ id: Int, mode: FanMode) {
|
||||
let currentMode = Int(self.getValue("FS! ") ?? 0)
|
||||
let fansMode = Int(self.getValue("FS! ") ?? 0)
|
||||
var newMode: UInt8 = 0
|
||||
|
||||
if (currentMode == 0 || currentMode == 1) && id == 0 && mode == .automatic {
|
||||
if fansMode == 0 && id == 0 && mode == .forced {
|
||||
newMode = 1
|
||||
} else if (currentMode == 0 || currentMode == 2) && id == 1 && mode == .automatic {
|
||||
} else if fansMode == 0 && id == 1 && mode == .forced {
|
||||
newMode = 2
|
||||
} else if currentMode == 1 && id == 0 && mode == .forced {
|
||||
} else if fansMode == 1 && id == 0 && mode == .automatic {
|
||||
newMode = 0
|
||||
} else if currentMode == 2 && id == 1 && mode == .forced {
|
||||
} else if fansMode == 1 && id == 1 && mode == .forced {
|
||||
newMode = 3
|
||||
} else if fansMode == 2 && id == 1 && mode == .automatic {
|
||||
newMode = 0
|
||||
} else if currentMode == 1 && id == 1 && mode == .automatic {
|
||||
} else if fansMode == 2 && id == 0 && mode == .forced {
|
||||
newMode = 3
|
||||
} else if currentMode == 2 && id == 0 && mode == .automatic {
|
||||
newMode = 3
|
||||
} else if currentMode == 3 && id == 0 && mode == .forced {
|
||||
} else if fansMode == 3 && id == 0 && mode == .automatic {
|
||||
newMode = 2
|
||||
} else if fansMode == 3 && id == 1 && mode == .automatic {
|
||||
newMode = 1
|
||||
} else if currentMode == 3 && id == 1 && mode == .forced {
|
||||
newMode = 2
|
||||
}
|
||||
|
||||
if currentMode == newMode {
|
||||
if fansMode == newMode {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -425,8 +416,6 @@ public class SMCService {
|
||||
return
|
||||
}
|
||||
|
||||
self.setFanMode(id, mode: .automatic)
|
||||
|
||||
var value = SMCVal_t("F\(id)Tg")
|
||||
value.dataSize = 2
|
||||
value.bytes = [UInt8(speed >> 6), UInt8((speed << 2) ^ ((speed >> 6) << 8)), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
|
||||
|
||||
@@ -924,14 +924,14 @@ public func isRoot() -> Bool {
|
||||
return getuid() == 0
|
||||
}
|
||||
|
||||
public func ensureRoot() -> Bool {
|
||||
public func ensureRoot() {
|
||||
if isRoot() {
|
||||
return true
|
||||
return
|
||||
}
|
||||
|
||||
let pwd = Bundle.main.bundleURL.absoluteString.replacingOccurrences(of: "file://", with: "")
|
||||
guard let script = NSAppleScript(source: "do shell script \"\(pwd)/Contents/MacOS/Stats > /dev/null 2>&1 &\" with administrator privileges") else {
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
var err: NSDictionary? = nil
|
||||
@@ -939,9 +939,9 @@ public func ensureRoot() -> Bool {
|
||||
|
||||
if err != nil {
|
||||
print("cannot run script as root: \(String(describing: err))")
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
NSApp.terminate(nil)
|
||||
return true
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user