Thou shalt not cross 80 columns in thy file.

This commit is contained in:
2025-03-20 13:44:11 -07:00
parent c846fc5a15
commit c1056fad93
14 changed files with 665 additions and 297 deletions

View File

@@ -23,7 +23,8 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
// MARK: - Views // MARK: - Views
private var appIconImage: NSImageView = { private var appIconImage: NSImageView = {
let image = NSImageView() let image = NSImageView()
image.image = NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath) image.image =
NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
image.imageScaling = .scaleAxesIndependently image.imageScaling = .scaleAxesIndependently
image.translatesAutoresizingMaskIntoConstraints = false image.translatesAutoresizingMaskIntoConstraints = false
return image return image
@@ -31,25 +32,35 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
private var appNameLabel: NSTextField = { private var appNameLabel: NSTextField = {
let textField = NSTextField() let textField = NSTextField()
textField.stringValue = (Bundle.main.infoDictionary?["CFBundleName"] as? String) ?? "NOT FOUND" textField.stringValue =
(Bundle.main.infoDictionary?["CFBundleName"] as? String)
??
"NOT FOUND"
textField.isEditable = false textField.isEditable = false
textField.isBezeled = false textField.isBezeled = false
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .center textField.alignment = .center
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .title1).pointSize, weight: .bold) textField.font = NSFont
.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .title1).pointSize,
weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
private var versionLabel: NSTextField = { private var versionLabel: NSTextField = {
let textField = NSTextField() let textField = NSTextField()
textField.stringValue = "Version \((Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "-.--")" textField.stringValue =
"Version \((Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "-.--")"
textField.isEditable = false textField.isEditable = false
textField.isBezeled = false textField.isBezeled = false
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .center textField.alignment = .center
textField.textColor = NSColor.systemGray textField.textColor = NSColor.systemGray
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .regular) textField.font = NSFont
.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .subheadline).pointSize,
weight: .regular)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -64,7 +75,10 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .center textField.alignment = .center
textField.textColor = NSColor.systemGray textField.textColor = NSColor.systemGray
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .regular) textField.font = NSFont
.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .subheadline).pointSize,
weight: .regular)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -143,39 +157,68 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
// App image. // App image.
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
appIconImage.widthAnchor.constraint(equalToConstant: 100), appIconImage.widthAnchor.constraint(equalToConstant: 100),
appIconImage.heightAnchor.constraint(equalTo: appIconImage.widthAnchor, multiplier: 1), appIconImage.heightAnchor
appIconImage.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing20), .constraint(equalTo: appIconImage.widthAnchor,
appIconImage.centerXAnchor.constraint(equalTo: view.centerXAnchor), multiplier: 1),
appIconImage.topAnchor
.constraint(equalTo: view.topAnchor,
constant: ViewConstants.spacing20),
appIconImage.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
]) ])
// Title // Title
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
appNameLabel.topAnchor.constraint(equalTo: appIconImage.bottomAnchor, constant: ViewConstants.spacing20), appNameLabel.topAnchor
appNameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), .constraint(equalTo: appIconImage.bottomAnchor,
constant: ViewConstants.spacing20),
appNameLabel.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
versionLabel.topAnchor.constraint(equalTo: appNameLabel.bottomAnchor, constant: ViewConstants.spacing2), versionLabel.topAnchor
versionLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), .constraint(equalTo: appNameLabel.bottomAnchor,
constant: ViewConstants.spacing2),
versionLabel.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
copyrightLabel.topAnchor.constraint(equalTo: versionLabel.bottomAnchor, constant: ViewConstants.spacing10), copyrightLabel.topAnchor
copyrightLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), .constraint(equalTo: versionLabel.bottomAnchor,
constant: ViewConstants.spacing10),
copyrightLabel.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
]) ])
// Buttons // Buttons
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
buttonsContainer.topAnchor.constraint(equalTo: copyrightLabel.bottomAnchor, constant: ViewConstants.spacing20), buttonsContainer.topAnchor
buttonsContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -ViewConstants.spacing20), .constraint(equalTo: copyrightLabel.bottomAnchor,
buttonsContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor), constant: ViewConstants.spacing20),
buttonsContainer.bottomAnchor
.constraint(equalTo: view.bottomAnchor,
constant: -ViewConstants.spacing20),
buttonsContainer.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
privacyButton.topAnchor.constraint(equalTo: buttonsContainer.topAnchor), privacyButton.topAnchor
privacyButton.bottomAnchor.constraint(equalTo: buttonsContainer.bottomAnchor), .constraint(equalTo: buttonsContainer.topAnchor),
privacyButton.leadingAnchor.constraint(equalTo: buttonsContainer.leadingAnchor), privacyButton.bottomAnchor
.constraint(equalTo: buttonsContainer.bottomAnchor),
privacyButton.leadingAnchor
.constraint(equalTo: buttonsContainer.leadingAnchor),
documentationButton.firstBaselineAnchor.constraint(equalTo: privacyButton.firstBaselineAnchor), documentationButton.firstBaselineAnchor
documentationButton.leadingAnchor.constraint(equalTo: privacyButton.trailingAnchor, constant: ViewConstants.spacing10), .constraint(equalTo: privacyButton.firstBaselineAnchor),
documentationButton.leadingAnchor
.constraint(equalTo: privacyButton.trailingAnchor,
constant: ViewConstants.spacing10),
websiteButton.firstBaselineAnchor.constraint(equalTo: privacyButton.firstBaselineAnchor), websiteButton.firstBaselineAnchor
websiteButton.leadingAnchor.constraint(equalTo: documentationButton.trailingAnchor, constant: ViewConstants.spacing10), .constraint(equalTo: privacyButton.firstBaselineAnchor),
websiteButton.trailingAnchor.constraint(equalTo: buttonsContainer.trailingAnchor), websiteButton.leadingAnchor
.constraint(equalTo: documentationButton.trailingAnchor,
constant: ViewConstants.spacing10),
websiteButton.trailingAnchor
.constraint(equalTo: buttonsContainer.trailingAnchor),
]) ])
} }

View File

@@ -22,7 +22,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// MARK: - Notifications // MARK: - Notifications
private func setupNotifications() { private func setupNotifications() {
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(wakeUp), name: NSWorkspace.didWakeNotification, object: nil) NSWorkspace.shared.notificationCenter
.addObserver(self, selector: #selector(wakeUp),
name: NSWorkspace.didWakeNotification,
object: nil)
} }
@objc private func wakeUp() { @objc private func wakeUp() {

View File

@@ -40,7 +40,8 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate {
private var globalEventMonitor: EventMonitor? private var globalEventMonitor: EventMonitor?
override init() { override init() {
self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) self.statusItem = NSStatusBar.system
.statusItem(withLength: NSStatusItem.variableLength)
self.statusItem.isVisible = true self.statusItem.isVisible = true
self.panel = PopoverPanel(viewController: CmdViewController()) self.panel = PopoverPanel(viewController: CmdViewController())
@@ -48,38 +49,49 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate {
// Events // Events
// Shows panel and keeps the button highlighted. // Shows panel and keeps the button highlighted.
localEventMonitor = LocalEventMonitor(mask: [.leftMouseDown]) { [weak self] event in localEventMonitor = LocalEventMonitor(mask: [.leftMouseDown])
if let button = self?.statusItem.button, event.window == button.window, !event.modifierFlags.contains(.command) { { [weak self] event in
self?.statusButtonPressed(button) if let button = self?.statusItem.button,
return nil event.window == button.window,
} !event.modifierFlags.contains(.command)
{
return event self?.statusButtonPressed(button)
} return nil
// On click and drag, the button should panel should desappear and un-highlight.
localDragEventMonitor = LocalEventMonitor(mask: [.leftMouseDragged, .keyDown]) { [weak self] event in
let modifiers = event.modifierFlags.rawValue
if let panel = self?.panel, panel.isKeyWindow {
if modsContains(keys: OSCmd, in: modifiers) && event.type == .leftMouseDragged {
self?.panel.resignKey()
} }
}
return event return event
} }
// On click and drag, the button should panel should desappear and
// un-highlight.
localDragEventMonitor =
LocalEventMonitor(mask: [.leftMouseDragged, .keyDown])
{ [weak self] event in
let modifiers = event.modifierFlags.rawValue
if let panel = self?.panel, panel.isKeyWindow {
if modsContains(keys: OSCmd, in: modifiers) &&
event.type == .leftMouseDragged
{
self?.panel.resignKey()
}
}
return event
}
// NOTE: The need for this seems a bit questionable. Maybe, this // NOTE: The need for this seems a bit questionable. Maybe, this
// works properly without global event monitor in MacOS Sequoia? // works properly without global event monitor in MacOS Sequoia?
// Resign key whenever clicking outside of panel, thus hiding the // Resign key whenever clicking outside of panel, thus hiding the
// panel and un-highlighting the button. // panel and un-highlighting the button.
// In order to receive events, the program needs to be codesigned. // In order to receive events, the program needs to be codesigned.
globalEventMonitor = GlobalEventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in globalEventMonitor =
if let panel = self?.panel, panel.isKeyWindow { GlobalEventMonitor(mask: [.leftMouseDown, .rightMouseDown])
panel.resignKey() { [weak self] event in
} if let panel = self?.panel, panel.isKeyWindow {
} panel.resignKey()
}
}
if let statusButton = statusItem.button, if let statusButton = statusItem.button,
let statusButtonWindow = statusButton.window { let statusButtonWindow = statusButton.window {
@@ -118,7 +130,8 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate {
private func dismissPanel() { private func dismissPanel() {
NSAnimationContext.runAnimationGroup { context in NSAnimationContext.runAnimationGroup { context in
context.duration = 0.3 context.duration = 0.3
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) context.timingFunction =
CAMediaTimingFunction(name: .easeInEaseOut)
panel.animator().alphaValue = 0 panel.animator().alphaValue = 0
} completionHandler: { [weak self] in } completionHandler: { [weak self] in
self?.panel.orderOut(nil) self?.panel.orderOut(nil)
@@ -129,32 +142,26 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate {
} }
private func setPanelPosition() { private func setPanelPosition() {
guard let statusItemView = statusItem.button?.window else { guard let scrn = NSScreen.main,
panel.center() let iFrame = statusItem.button?.window?.frame
return else { return }
var x = iFrame.origin.x
let y = scrn.visibleFrame.height + scrn.visibleFrame.origin.y
if (iFrame.origin.x + panel.frame.width) >
(scrn.visibleFrame.origin.x +
scrn.visibleFrame.width)
{
x = iFrame.origin.x + iFrame.width - panel.frame.width
}
if (iFrame.origin.x - panel.frame.width) <
(scrn.visibleFrame.origin.x)
{
x = scrn.visibleFrame.origin.x
} }
var targetRect = statusItemView.frame panel.setFrameTopLeftPoint(NSPoint(x: x, y: y))
if let screen = statusItemView.screen {
let panelWidth = panel.frame.width
if statusItemView.frame.origin.x + panelWidth > screen.visibleFrame.width {
targetRect.origin.x += statusItemView.frame.width
targetRect.origin.x -= panelWidth
targetRect.origin.x += Metrics.windowBorderSize
} else {
targetRect.origin.x -= Metrics.windowBorderSize
}
} else {
targetRect.origin.x -= Metrics.windowBorderSize
}
if targetRect.origin.x < 0 {
targetRect.origin.x = 0
}
panel.setFrameTopLeftPoint(targetRect.origin)
} }
private func setButtonHighlighted(to highlight: Bool) { private func setButtonHighlighted(to highlight: Bool) {
@@ -163,7 +170,9 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate {
func setImage(title: String?, description: String) { func setImage(title: String?, description: String) {
if title != nil { if title != nil {
statusItem.button!.image = NSImage(systemSymbolName: title!, accessibilityDescription: description) statusItem.button!.image =
NSImage(systemSymbolName: title!,
accessibilityDescription: description)
return return
} }
statusItem.button!.image = nil statusItem.button!.image = nil
@@ -178,17 +187,23 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate {
} }
func setContents(to text: String) { func setContents(to text: String) {
guard let viewContoller = panel.contentViewController as? CmdViewController else { return } guard let viewContoller =
panel.contentViewController as? CmdViewController
else { return }
viewContoller.setText(text) viewContoller.setText(text)
} }
func setFile(_ file: CmdFile) { func setFile(_ file: CmdFile) {
guard let viewContoller = panel.contentViewController as? CmdViewController else { return } guard let viewContoller =
panel.contentViewController as? CmdViewController
else { return }
viewContoller.setFile(file) viewContoller.setFile(file)
} }
func windowDidResize(_ notification: Notification) { func windowDidResize(_ notification: Notification) {
if let cmdPanel = notification.object as? NSPanel, panel == cmdPanel { if let cmdPanel = notification.object as? NSPanel,
panel == cmdPanel, cmdPanel.isVisible
{
setPanelPosition() setPanelPosition()
} }
} }
@@ -196,7 +211,8 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate {
func windowDidMove(_ notification: Notification) { func windowDidMove(_ notification: Notification) {
if let statusBarButtonWindow = notification.object as? NSWindow, if let statusBarButtonWindow = notification.object as? NSWindow,
let buttonWindow = statusItem.button?.window, let buttonWindow = statusItem.button?.window,
buttonWindow == statusBarButtonWindow buttonWindow == statusBarButtonWindow,
panel.isVisible
{ {
setPanelPosition() setPanelPosition()
} }

View File

@@ -47,7 +47,10 @@ class CircularProgressView: NSView {
) )
backgroundLayer.path = bgMutablePath backgroundLayer.path = bgMutablePath
backgroundLayer.fillColor = NSColor.clear.cgColor backgroundLayer.fillColor = NSColor.clear.cgColor
// backgroundLayer.strokeColor = NSColor(name: nil) { getColors(.lightGray, .darkGray, for: $0) }.cgColor // backgroundLayer.strokeColor =
// NSColor(name: nil) {
// getColors(.lightGray, .darkGray, for: $0)
// }.cgColor
backgroundLayer.strokeColor = backgroundColor.cgColor backgroundLayer.strokeColor = backgroundColor.cgColor
backgroundLayer.lineWidth = lineWidth backgroundLayer.lineWidth = lineWidth
@@ -64,7 +67,8 @@ class CircularProgressView: NSView {
foregroundLayer.lineCap = .round foregroundLayer.lineCap = .round
foregroundLayer.lineWidth = lineWidth foregroundLayer.lineWidth = lineWidth
foregroundLayer.strokeEnd = value foregroundLayer.strokeEnd = value
foregroundLayer.transform = CATransform3DMakeRotation(CGFloat(Double.pi / 2), 0, 0, 1) foregroundLayer.transform =
CATransform3DMakeRotation(CGFloat(Double.pi / 2), 0, 0, 1)
layer?.addSublayer(backgroundLayer) layer?.addSublayer(backgroundLayer)
layer?.addSublayer(foregroundLayer) layer?.addSublayer(foregroundLayer)
@@ -73,7 +77,10 @@ class CircularProgressView: NSView {
private func advanceProgress(to value: Double) { private func advanceProgress(to value: Double) {
let animation = CABasicAnimation(keyPath: "strokeEnd") let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = animationSpeed animation.duration = animationSpeed
animation.fromValue = foregroundLayer.presentation()?.value(forKeyPath: "strokeEnd") ?? value animation.fromValue =
foregroundLayer.presentation()?.value(forKeyPath: "strokeEnd")
??
value
animation.toValue = value animation.toValue = value
foregroundLayer.strokeEnd = CGFloat(value) foregroundLayer.strokeEnd = CGFloat(value)
foregroundLayer.add(animation, forKey: "pathAnimation") foregroundLayer.add(animation, forKey: "pathAnimation")

View File

@@ -5,7 +5,8 @@ class CmdFile {
private(set) var lastExec = Int(Date().timeIntervalSince1970) private(set) var lastExec = Int(Date().timeIntervalSince1970)
var untilNextExec: Int { var untilNextExec: Int {
get { get {
let res = reloadTime - (Int(Date().timeIntervalSince1970) - lastExec) let res =
reloadTime - (Int(Date().timeIntervalSince1970) - lastExec)
if res <= 0 { return reloadTime } if res <= 0 { return reloadTime }
return res return res
} }
@@ -40,10 +41,13 @@ class CmdFile {
let execResult = execute(atPath: url) let execResult = execute(atPath: url)
DispatchQueue.main.async { DispatchQueue.main.async {
if execResult.title.isEmpty { if execResult.title.isEmpty {
self?.statusItem.setImage(title: "exclamationmark.triangle.fill", description: "Warning") self?.statusItem
.setImage(title: "exclamationmark.triangle.fill",
description: "Warning")
self?.statusItem.setTitle("") self?.statusItem.setTitle("")
} else { } else {
self?.statusItem.setImage(title: nil, description: "") self?.statusItem
.setImage(title: nil, description: "")
self?.statusItem.setTitle(execResult.0) self?.statusItem.setTitle(execResult.0)
} }
self?.statusItem.setContents(to: execResult.body) self?.statusItem.setContents(to: execResult.body)
@@ -102,23 +106,40 @@ class CmdManager {
private func getPaths() { private func getPaths() {
let manager = FileManager.default let manager = FileManager.default
let path = manager.homeDirectoryForCurrentUser.appending(path: ".cmdbar") let path =
if let contents = try? manager.contentsOfDirectory(atPath: path.path()) { manager.homeDirectoryForCurrentUser.appending(path: ".cmdbar")
if let contents =
try? manager.contentsOfDirectory(atPath: path.path())
{
for content in contents { for content in contents {
let filePath = path.appending(path: content) let filePath = path.appending(path: content)
let domains = filePath.path().components(separatedBy: ".") let domains = filePath.path().components(separatedBy: ".")
if (isValidTimeFormat(domains[domains.count - 1])) { if (isValidTimeFormat(domains[domains.count - 1])) {
paths.append(CmdFile(url: filePath, reloadTime: intervalToSeconds(from: domains[domains.count - 1]))) paths.append(
} else if domains.last == "sh" && isValidTimeFormat(domains[domains.count - 2]) { CmdFile(url: filePath,
paths.append(CmdFile(url: filePath, reloadTime: intervalToSeconds(from: domains[domains.count - 2]))) reloadTime: intervalToSeconds(
from: domains[domains.count - 1])
)
)
} else if domains.last == "sh" &&
isValidTimeFormat(domains[domains.count - 2])
{
paths.append(
CmdFile(url: filePath,
reloadTime: intervalToSeconds(
from: domains[domains.count - 2]
)
)
)
} }
} }
} }
} }
private func setupMenu() { private func setupMenu() {
defaultStatusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) defaultStatusItem = NSStatusBar.system
.statusItem(withLength: NSStatusItem.variableLength)
if let btn = defaultStatusItem?.button { if let btn = defaultStatusItem?.button {
let image = NSApp.applicationIconImage let image = NSApp.applicationIconImage
image!.size = NSSize(width: 22, height: 22) image!.size = NSSize(width: 22, height: 22)
@@ -126,13 +147,18 @@ class CmdManager {
} }
guard defaultStatusItem != nil else { return } guard defaultStatusItem != nil else { return }
let menu = NSMenu() let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Reload", action: #selector(reloadWidgets), keyEquivalent: "")) menu.addItem(NSMenuItem(title: "Reload",
action: #selector(reloadWidgets), keyEquivalent: ""))
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "About", action: #selector(showAbout), keyEquivalent: "")) menu.addItem(NSMenuItem(title: "About",
action: #selector(showAbout), keyEquivalent: ""))
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Check for Updates...", action: #selector(checkForUpdates), keyEquivalent: "")) menu.addItem(NSMenuItem(title: "Check for Updates...",
action: #selector(checkForUpdates), keyEquivalent: ""))
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) menu.addItem(NSMenuItem(title: "Quit",
action: #selector(NSApplication.terminate(_:)),
keyEquivalent: "q"))
defaultStatusItem!.menu = menu defaultStatusItem!.menu = menu
} }

View File

@@ -16,6 +16,9 @@ class CmdViewController: NSViewController {
private var timer: DispatchSourceTimer? private var timer: DispatchSourceTimer?
private var keyboardEvents: EventMonitor? private var keyboardEvents: EventMonitor?
private var maxWidthContraint: NSLayoutConstraint!
private var maxHeightContraint: NSLayoutConstraint!
private var visualEffectView: NSView = { private var visualEffectView: NSView = {
let visualEffect = NSVisualEffectView() let visualEffect = NSVisualEffectView()
visualEffect.blendingMode = .behindWindow visualEffect.blendingMode = .behindWindow
@@ -38,14 +41,20 @@ class CmdViewController: NSViewController {
private var textLabel: NSTextField = { private var textLabel: NSTextField = {
let textField = NSTextField() let textField = NSTextField()
textField.backgroundColor = .clear // NOTE: Setting drawsBackground = false messes with geometry. Also, setting a custom cell, messes up, too. // NOTE: Setting drawsBackground = false messes with geometry.
// Also, setting a custom cell, messes up, too.
textField.backgroundColor = .clear
textField.isSelectable = true textField.isSelectable = true
textField.isHidden = false textField.isHidden = false
textField.isEditable = false textField.isEditable = false
textField.isBezeled = false textField.isBezeled = false
textField.alignment = .left textField.alignment = .left
textField.textColor = NSColor(name: nil) { getColors(.black, .white, for: $0) } textField.textColor =
textField.font = NSFont.monospacedSystemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .regular) NSColor(name: nil) { getColors(.black, .white, for: $0) }
textField.font = NSFont
.monospacedSystemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .body).pointSize,
weight: .regular)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -60,8 +69,12 @@ class CmdViewController: NSViewController {
textField.isBezeled = false textField.isBezeled = false
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .left textField.alignment = .left
textField.textColor = NSColor(name: nil) { getColors(.darkGray, .lightGray, for: $0) } textField.textColor =
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .bold) NSColor(name: nil) { getColors(.darkGray, .lightGray, for: $0) }
textField.font = NSFont
.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .subheadline).pointSize,
weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -80,15 +93,19 @@ class CmdViewController: NSViewController {
textField.isBezeled = false textField.isBezeled = false
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .center textField.alignment = .center
textField.textColor = NSColor(name: nil) { getColors(.darkGray, .lightGray, for: $0) } textField.textColor =
textField.font = NSFont(descriptor: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body), size: 0) NSColor(name: nil) { getColors(.darkGray, .lightGray, for: $0) }
textField.font =
NSFont(descriptor: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .body), size: 0)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
private var quitButton: NSButton = { private var quitButton: NSButton = {
let button = NSButton() let button = NSButton()
button.image = systemImage("xmark.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemRed])) button.image = systemImage("xmark.circle.fill", .title2, .large,
.init(paletteColors: [.white, .systemRed]))
button.isBordered = false button.isBordered = false
button.action = #selector(terminateApp) button.action = #selector(terminateApp)
button.sizeToFit() button.sizeToFit()
@@ -99,7 +116,9 @@ class CmdViewController: NSViewController {
private var reloadButton: NSButton = { private var reloadButton: NSButton = {
let button = NSButton() let button = NSButton()
button.image = systemImage("arrow.clockwise.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemBlue])) button.image = systemImage("arrow.clockwise.circle.fill", .title2,
.large,
.init(paletteColors: [.white, .systemBlue]))
button.isBordered = false button.isBordered = false
button.action = #selector(reloadWidget) button.action = #selector(reloadWidget)
button.sizeToFit() button.sizeToFit()
@@ -110,7 +129,8 @@ class CmdViewController: NSViewController {
private var startButton: NSButton = { private var startButton: NSButton = {
let button = NSButton() let button = NSButton()
button.image = systemImage("sun.max.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemOrange])) button.image = systemImage("sun.max.circle.fill", .title2, .large,
.init(paletteColors: [.white, .systemOrange]))
button.isBordered = false button.isBordered = false
button.action = #selector(openAtLogin) button.action = #selector(openAtLogin)
button.sizeToFit() button.sizeToFit()
@@ -121,7 +141,8 @@ class CmdViewController: NSViewController {
private var aboutButton: NSButton = { private var aboutButton: NSButton = {
let button = NSButton() let button = NSButton()
button.image = systemImage("info.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemGray])) button.image = systemImage("info.circle.fill", .title2, .large,
.init(paletteColors: [.white, .systemGray]))
button.isBordered = false button.isBordered = false
button.action = #selector(showAbout) button.action = #selector(showAbout)
button.sizeToFit() button.sizeToFit()
@@ -132,7 +153,12 @@ class CmdViewController: NSViewController {
private var updateButton: NSButton = { private var updateButton: NSButton = {
let button = NSButton() let button = NSButton()
button.image = NSImage(systemSymbolName: "arrow.down.circle.fill", accessibilityDescription: nil)?.withSymbolConfiguration(NSImage.SymbolConfiguration(textStyle: .title2, scale: .large) .applying(.init(paletteColors: [.white, .systemGreen]))) // TODO: WHY??!!!
button.image = NSImage(systemSymbolName: "arrow.down.circle.fill",
accessibilityDescription: nil)?
.withSymbolConfiguration(NSImage
.SymbolConfiguration(textStyle: .title2, scale: .large)
.applying(.init(paletteColors: [.white, .systemGreen])))
button.isBordered = false button.isBordered = false
button.action = #selector(updateApp) button.action = #selector(updateApp)
button.sizeToFit() button.sizeToFit()
@@ -148,7 +174,8 @@ class CmdViewController: NSViewController {
blurView.material = .sheet blurView.material = .sheet
blurView.state = .active blurView.state = .active
blurView.wantsLayer = true blurView.wantsLayer = true
blurView.layer?.borderColor = NSColor.systemGray.withAlphaComponent(0.05).cgColor blurView.layer?.borderColor =
NSColor.systemGray.withAlphaComponent(0.05).cgColor
blurView.layer?.borderWidth = 1.5 blurView.layer?.borderWidth = 1.5
blurView.translatesAutoresizingMaskIntoConstraints = false blurView.translatesAutoresizingMaskIntoConstraints = false
return blurView return blurView
@@ -190,9 +217,19 @@ class CmdViewController: NSViewController {
self.view = QuietView() self.view = QuietView()
} }
override func viewWillAppear() {
maxWidthContraint.constant =
NSScreen.main!.visibleFrame.size.width * 0.7
maxHeightContraint.constant =
NSScreen.main!.visibleFrame.size.height * 0.8
}
override func viewDidAppear() { override func viewDidAppear() {
super.viewDidAppear() super.viewDidAppear()
// TODO: Check so that there are weird behaviors on the
textLabelScrollView.flashScrollers()
startReloadTextTimer() startReloadTextTimer()
keyboardEvents?.start() keyboardEvents?.start()
@@ -207,7 +244,8 @@ class CmdViewController: NSViewController {
keyboardEvents?.stop() keyboardEvents?.stop()
if let editor = textLabel.currentEditor() { textLabel.endEditing(editor) } if let editor = textLabel.currentEditor()
{ textLabel.endEditing(editor) }
} }
private func setContraints() { private func setContraints() {
@@ -220,7 +258,9 @@ class CmdViewController: NSViewController {
func setup() { func setup() {
openAtLoginToggle() openAtLoginToggle()
buttonsBackground.layer?.cornerRadius = quitButton.bounds.height / Metrics.buttonSpacing + Metrics.buttonSpacing buttonsBackground.layer?.cornerRadius =
quitButton.bounds.height / Metrics.buttonSpacing +
Metrics.buttonSpacing
setupKeyEvents() setupKeyEvents()
} }
@@ -230,110 +270,198 @@ class CmdViewController: NSViewController {
progressView.widthAnchor.constraint(equalToConstant: 16), progressView.widthAnchor.constraint(equalToConstant: 16),
progressView.heightAnchor.constraint(equalToConstant: 16), progressView.heightAnchor.constraint(equalToConstant: 16),
progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Metrics.padding), progressView.leadingAnchor
progressView.centerYAnchor.constraint(equalTo: reloadsText.centerYAnchor), .constraint(equalTo: view.leadingAnchor,
constant: Metrics.padding),
progressView.centerYAnchor
.constraint(equalTo: reloadsText.centerYAnchor),
]) ])
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
reloadsText.leadingAnchor.constraint(equalTo: progressView.trailingAnchor, constant: Metrics.buttonSpacing), reloadsText.leadingAnchor
reloadsText.centerYAnchor.constraint(equalTo: buttonsBackground.centerYAnchor), .constraint(equalTo: progressView.trailingAnchor,
reloadsText.trailingAnchor.constraint(equalTo: buttonsBackground.leadingAnchor, constant: -Metrics.buttonSpacing), constant: Metrics.buttonSpacing),
reloadsText.centerYAnchor
.constraint(equalTo: buttonsBackground.centerYAnchor),
reloadsText.trailingAnchor
.constraint(equalTo: buttonsBackground.leadingAnchor,
constant: -Metrics.buttonSpacing),
]) ])
} }
private func setViewConstraints() { private func setViewConstraints() {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor), visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor), visualEffectView.bottomAnchor
visualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor), .constraint(equalTo: view.bottomAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor), visualEffectView.leadingAnchor
.constraint(equalTo: view.leadingAnchor),
visualEffectView.trailingAnchor
.constraint(equalTo: view.trailingAnchor),
]) ])
} }
private func setButtonConstraints() { private func setButtonConstraints() {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
quitButton.widthAnchor.constraint(equalToConstant: quitButton.bounds.width), quitButton.widthAnchor
quitButton.heightAnchor.constraint(equalToConstant: quitButton.bounds.height), .constraint(equalToConstant: quitButton.bounds.width),
reloadButton.widthAnchor.constraint(equalToConstant: reloadButton.bounds.width), quitButton.heightAnchor
reloadButton.heightAnchor.constraint(equalToConstant: reloadButton.bounds.height), .constraint(equalToConstant: quitButton.bounds.height),
startButton.widthAnchor.constraint(equalToConstant: startButton.bounds.width), reloadButton.widthAnchor
startButton.heightAnchor.constraint(equalToConstant: startButton.bounds.height), .constraint(equalToConstant: reloadButton.bounds.width),
aboutButton.widthAnchor.constraint(equalToConstant: aboutButton.bounds.width), reloadButton.heightAnchor
aboutButton.heightAnchor.constraint(equalToConstant: aboutButton.bounds.height), .constraint(equalToConstant: reloadButton.bounds.height),
updateButton.widthAnchor.constraint(equalToConstant: updateButton.bounds.width), startButton.widthAnchor
updateButton.heightAnchor.constraint(equalToConstant: updateButton.bounds.height), .constraint(equalToConstant: startButton.bounds.width),
startButton.heightAnchor
.constraint(equalToConstant: startButton.bounds.height),
aboutButton.widthAnchor
.constraint(equalToConstant: aboutButton.bounds.width),
aboutButton.heightAnchor
.constraint(equalToConstant: aboutButton.bounds.height),
updateButton.widthAnchor
.constraint(equalToConstant: updateButton.bounds.width),
updateButton.heightAnchor
.constraint(equalToConstant: updateButton.bounds.height),
buttonsContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Metrics.padding), buttonsContainer.trailingAnchor
buttonsContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -Metrics.padding), .constraint(equalTo: view.trailingAnchor,
constant: -Metrics.padding),
buttonsContainer.bottomAnchor
.constraint(equalTo: view.bottomAnchor,
constant: -Metrics.padding),
buttonsBackground.leadingAnchor.constraint(equalTo: buttonsContainer.leadingAnchor), buttonsBackground.leadingAnchor
buttonsBackground.trailingAnchor.constraint(equalTo: buttonsContainer.trailingAnchor), .constraint(equalTo: buttonsContainer.leadingAnchor),
buttonsBackground.topAnchor.constraint(equalTo: buttonsContainer.topAnchor), buttonsBackground.trailingAnchor
buttonsBackground.bottomAnchor.constraint(equalTo: buttonsContainer.bottomAnchor), .constraint(equalTo: buttonsContainer.trailingAnchor),
buttonsBackground.topAnchor
.constraint(equalTo: buttonsContainer.topAnchor),
buttonsBackground.bottomAnchor
.constraint(equalTo: buttonsContainer.bottomAnchor),
quitButton.trailingAnchor.constraint(equalTo: buttonsContainer.trailingAnchor, constant: -Metrics.buttonSpacing), quitButton.trailingAnchor
quitButton.topAnchor.constraint(equalTo: buttonsContainer.topAnchor, constant: Metrics.buttonSpacing), .constraint(equalTo: buttonsContainer.trailingAnchor,
quitButton.bottomAnchor.constraint(equalTo: buttonsContainer.bottomAnchor, constant: -Metrics.buttonSpacing), constant: -Metrics.buttonSpacing),
quitButton.topAnchor
.constraint(equalTo: buttonsContainer.topAnchor,
constant: Metrics.buttonSpacing),
quitButton.bottomAnchor
.constraint(equalTo: buttonsContainer.bottomAnchor,
constant: -Metrics.buttonSpacing),
reloadButton.firstBaselineAnchor.constraint(equalTo: quitButton.firstBaselineAnchor), reloadButton.firstBaselineAnchor
reloadButton.trailingAnchor.constraint(equalTo: quitButton.leadingAnchor, constant: -Metrics.buttonSpacing), .constraint(equalTo: quitButton.firstBaselineAnchor),
reloadButton.trailingAnchor
.constraint(equalTo: quitButton.leadingAnchor,
constant: -Metrics.buttonSpacing),
startButton.firstBaselineAnchor.constraint(equalTo: reloadButton.firstBaselineAnchor), startButton.firstBaselineAnchor
startButton.trailingAnchor.constraint(equalTo: reloadButton.leadingAnchor, constant: -Metrics.buttonSpacing), .constraint(equalTo: reloadButton.firstBaselineAnchor),
startButton.trailingAnchor
.constraint(equalTo: reloadButton.leadingAnchor,
constant: -Metrics.buttonSpacing),
aboutButton.firstBaselineAnchor.constraint(equalTo: startButton.firstBaselineAnchor), aboutButton.firstBaselineAnchor
aboutButton.trailingAnchor.constraint(equalTo: startButton.leadingAnchor, constant: -Metrics.buttonSpacing), .constraint(equalTo: startButton.firstBaselineAnchor),
aboutButton.trailingAnchor
.constraint(equalTo: startButton.leadingAnchor,
constant: -Metrics.buttonSpacing),
updateButton.firstBaselineAnchor.constraint(equalTo: startButton.firstBaselineAnchor), updateButton.firstBaselineAnchor
updateButton.leadingAnchor.constraint(equalTo: buttonsContainer.leadingAnchor, constant: Metrics.buttonSpacing), .constraint(equalTo: startButton.firstBaselineAnchor),
updateButton.trailingAnchor.constraint(equalTo: aboutButton.leadingAnchor, constant: -Metrics.buttonSpacing), updateButton.leadingAnchor
.constraint(equalTo: buttonsContainer.leadingAnchor,
constant: Metrics.buttonSpacing),
updateButton.trailingAnchor
.constraint(equalTo: aboutButton.leadingAnchor,
constant: -Metrics.buttonSpacing),
]) ])
} }
private func setTextConstraints() { private func setTextConstraints() {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
statusText.centerXAnchor.constraint(equalTo: view.centerXAnchor), statusText.centerXAnchor
statusText.centerYAnchor.constraint(equalTo: view.centerYAnchor), .constraint(equalTo: view.centerXAnchor),
statusText
.centerYAnchor.constraint(equalTo: view.centerYAnchor),
]) ])
let trailing = textLabel.trailingAnchor.constraint(equalTo: textLabelScrollView.trailingAnchor) let trailing =
textLabel.trailingAnchor
.constraint(equalTo: textLabelScrollView.trailingAnchor)
trailing.priority = .init(600) trailing.priority = .init(600)
let bottom = textLabel.bottomAnchor.constraint(equalTo: textLabelScrollView.bottomAnchor) let bottom =
textLabel.bottomAnchor
.constraint(equalTo: textLabelScrollView.bottomAnchor)
bottom.priority = .init(600) bottom.priority = .init(600)
maxWidthContraint =
textLabelScrollView.widthAnchor
.constraint(lessThanOrEqualToConstant: Metrics.contentMaxWidth)
maxHeightContraint =
textLabelScrollView.heightAnchor
.constraint(lessThanOrEqualToConstant: Metrics.contentMaxHeight)
textLabel.setContentHuggingPriority(.required, for: .horizontal) textLabel.setContentHuggingPriority(.required, for: .horizontal)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
textLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: Metrics.contentMinWidth), textLabel.widthAnchor
textLabelScrollView.widthAnchor.constraint(lessThanOrEqualToConstant: Metrics.contentMaxWidth), .constraint(
greaterThanOrEqualToConstant: Metrics.contentMinWidth
),
// textLabelScrollView.widthAnchor
// .constraint(
// lessThanOrEqualToConstant: Metrics.contentMaxWidth
// ),
maxWidthContraint,
textLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: Metrics.contentMinHeight), textLabel.heightAnchor
textLabelScrollView.heightAnchor.constraint(lessThanOrEqualToConstant: Metrics.contentMaxHeight), .constraint(
greaterThanOrEqualToConstant: Metrics.contentMinHeight
),
// textLabelScrollView.heightAnchor
// .constraint(
// lessThanOrEqualToConstant: Metrics.contentMaxHeight
// ),
maxHeightContraint,
textLabel.topAnchor.constraint(equalTo: textLabelScrollView.topAnchor), textLabel.topAnchor
.constraint(equalTo: textLabelScrollView.topAnchor),
bottom, bottom,
textLabel.leadingAnchor.constraint(equalTo: textLabelScrollView.leadingAnchor), textLabel.leadingAnchor
.constraint(equalTo: textLabelScrollView.leadingAnchor),
trailing, trailing,
textLabelScrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10), textLabelScrollView.topAnchor
textLabelScrollView.bottomAnchor.constraint(equalTo: buttonsContainer.topAnchor, constant: -10), .constraint(equalTo: view.topAnchor, constant: 10),
textLabelScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10), textLabelScrollView.bottomAnchor
textLabelScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10), .constraint(equalTo: buttonsContainer.topAnchor,
constant: -10),
textLabelScrollView.leadingAnchor
.constraint(equalTo: view.leadingAnchor, constant: 10),
textLabelScrollView.trailingAnchor
.constraint(equalTo: view.trailingAnchor, constant: -10),
]) ])
} }
private func resetReloadButton() { private func resetReloadButton() {
reloadButton.image = systemImage("arrow.clockwise.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemBlue])) reloadButton.image =
systemImage("arrow.clockwise.circle.fill", .title2, .large,
.init(paletteColors: [.white, .systemBlue]))
reloadButton.action = #selector(reloadWidget) reloadButton.action = #selector(reloadWidget)
reloadButton.toolTip = "Reload Current" reloadButton.toolTip = "Reload Current"
} }
private func startReloadTextTimer() { private func startReloadTextTimer() {
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global()) timer =
DispatchSource.makeTimerSource(queue: DispatchQueue.global())
timer?.schedule(deadline: .now(), repeating: .milliseconds(500)) timer?.schedule(deadline: .now(), repeating: .milliseconds(500))
timer?.setEventHandler { [weak self] in timer?.setEventHandler { [weak self] in
DispatchQueue.main.async { DispatchQueue.main.async {
guard let controller = self, let file = controller.cmdFile else { return } guard let controller = self,
let file = controller.cmdFile
else { return }
controller.setUntil("\(stringifySeconds(file.untilNextExec))") controller.setUntil("\(stringifySeconds(file.untilNextExec))")
controller.progressView.value = Double(file.untilNextExec) controller.progressView.value = Double(file.untilNextExec)
} }
@@ -342,45 +470,55 @@ class CmdViewController: NSViewController {
} }
private func setupKeyEvents() { private func setupKeyEvents() {
keyboardEvents = LocalEventMonitor(mask: [.flagsChanged, .keyDown], handler: { [weak self] event in keyboardEvents = LocalEventMonitor(mask: [.flagsChanged, .keyDown],
let modifiers = event.modifierFlags.rawValue handler: { [weak self] event in
let key = event.keyCode let modifiers = event.modifierFlags.rawValue
let key = event.keyCode
if modsContains(keys: OSShift, in: modifiers) { if modsContains(keys: OSShift, in: modifiers) {
self?.reloadButton.image = systemImage("arrow.clockwise.circle.fill", .title2, .large,.init(paletteColors: [.white, .systemCyan])) self?.reloadButton.image =
self?.reloadButton.action = #selector(self?.reloadWidgets) systemImage("arrow.clockwise.circle.fill", .title2,
self?.reloadButton.toolTip = "Reload All" .large,
} else { .init(paletteColors: [.white, .systemCyan]))
self?.reloadButton.image = systemImage("arrow.clockwise.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemBlue])) self?.reloadButton.action =
self?.reloadButton.action = #selector(self?.reloadWidget) #selector(self?.reloadWidgets)
self?.reloadButton.toolTip = "Reload Current" self?.reloadButton.toolTip = "Reload All"
} } else {
self?.reloadButton.image =
systemImage("arrow.clockwise.circle.fill", .title2,
.large,
.init(paletteColors: [.white, .systemBlue]))
self?.reloadButton.action =
#selector(self?.reloadWidget)
self?.reloadButton.toolTip = "Reload Current"
}
if modsContains(keys: OSCmd, in: modifiers) { if modsContains(keys: OSCmd, in: modifiers) {
if key == kVK_ANSI_Q { if key == kVK_ANSI_Q {
self?.terminateApp() self?.terminateApp()
} else if key == kVK_ANSI_W { } else if key == kVK_ANSI_W {
self?.view.superview?.window?.resignKey() self?.view.superview?.window?.resignKey()
} else if key == kVK_ANSI_R { } else if key == kVK_ANSI_R {
self?.reloadWidget() self?.reloadWidget()
} else if key == kVK_ANSI_C { } else if key == kVK_ANSI_C {
self?.textLabel.currentEditor()?.copy(nil) self?.textLabel.currentEditor()?.copy(nil)
}
} else
if modsContains(keys: OSCmd | OSShift, in: modifiers) {
if key == kVK_ANSI_R {
// NOTE: We need to resign the window in order to prevent the program from intercepting the events, which result in sounds
// beign made for missing performKeyEquivalent.
self?.view.window?.resignKey()
self?.reloadWidgets()
}
} else if modsContainsNone(in: modifiers) {
if key == kVK_ANSI_Q || key == kVK_Escape {
self?.view.superview?.window?.resignKey()
}
} }
} else if modsContains(keys: OSCmd | OSShift, in: modifiers) {
if key == kVK_ANSI_R {
// NOTE: We need to resign the window in order to prevent the program from intercepting the events, which result in sounds
// beign made for missing performKeyEquivalent.
self?.view.window?.resignKey()
self?.reloadWidgets()
}
} else if modsContainsNone(in: modifiers) {
if key == kVK_ANSI_Q || key == kVK_Escape {
self?.view.superview?.window?.resignKey()
}
}
return event return event
}) })
} }
func setFile(_ file: CmdFile) { func setFile(_ file: CmdFile) {
@@ -438,10 +576,14 @@ class CmdViewController: NSViewController {
private func openAtLoginToggle() { private func openAtLoginToggle() {
if SMAppService.mainApp.status == .enabled { if SMAppService.mainApp.status == .enabled {
startButton.toolTip = "Open at Login — ON" startButton.toolTip = "Open at Login — ON"
startButton.image = systemImage("sunrise.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemOrange])) startButton.image =
systemImage("sunrise.circle.fill", .title2, .large,
.init(paletteColors: [.white, .systemOrange]))
} else { } else {
startButton.toolTip = "Open at Login — OFF" startButton.toolTip = "Open at Login — OFF"
startButton.image = systemImage("sun.max.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemOrange])) startButton.image =
systemImage("sun.max.circle.fill", .title2, .large,
.init(paletteColors: [.white, .systemOrange]))
} }
} }

View File

@@ -35,7 +35,8 @@ final class LocalEventMonitor: EventMonitor {
} }
override func start() { override func start() {
monitor = NSEvent.addLocalMonitorForEvents(matching: mask, handler: handler) monitor = NSEvent.addLocalMonitorForEvents(matching: mask,
handler: handler)
} }
} }
@@ -50,6 +51,7 @@ final class GlobalEventMonitor: EventMonitor {
} }
override func start() { override func start() {
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) monitor = NSEvent.addGlobalMonitorForEvents(matching: mask,
handler: handler)
} }
} }

View File

@@ -72,7 +72,8 @@ func intervalToSeconds(from timeString: String) -> Int {
// Regular expression pattern to match "1s", "5m", "1h", "1d" // Regular expression pattern to match "1s", "5m", "1h", "1d"
func isValidTimeFormat(_ timeString: String) -> Bool { func isValidTimeFormat(_ timeString: String) -> Bool {
let pattern = #"^\d+[smhd]$"# let pattern = #"^\d+[smhd]$"#
return NSPredicate(format: "SELF MATCHES %@", pattern).evaluate(with: timeString) return NSPredicate(format: "SELF MATCHES %@", pattern)
.evaluate(with: timeString)
} }
// Turn seconds into a time string. // Turn seconds into a time string.
@@ -104,7 +105,8 @@ func execute(atPath path: URL) -> (title: String, body: String) {
if !output.isEmpty { if !output.isEmpty {
let result = output.components(separatedBy: "\n") let result = output.components(separatedBy: "\n")
if result.count >= 1 { if result.count >= 1 {
return (result.first!, result.dropFirst().joined(separator: "\n")) return (result.first!,
result.dropFirst().joined(separator: "\n"))
} }
} }
} catch ExecError.failed { } catch ExecError.failed {
@@ -124,8 +126,10 @@ func execute(atPath path: URL) -> (title: String, body: String) {
// Executes a file. // Executes a file.
private func executeFile(path: URL) throws -> String { private func executeFile(path: URL) throws -> String {
if !FileManager.default.fileExists(atPath: path.path()) { throw ExecError.doesntExist } if !FileManager.default.fileExists(atPath: path.path())
if !FileManager.default.isExecutableFile(atPath: path.path()) { throw ExecError.noPermission } { throw ExecError.doesntExist }
if !FileManager.default.isExecutableFile(atPath: path.path())
{ throw ExecError.noPermission }
let process = Process() let process = Process()
process.executableURL = path process.executableURL = path
@@ -164,21 +168,33 @@ private func executeFile(path: URL) throws -> String {
return "" return ""
} }
func getColors(_ light: NSColor, _ dark: NSColor, for appearance: NSAppearance) -> NSColor { func getColors(_ light: NSColor, _ dark: NSColor,
for appearance: NSAppearance) -> NSColor
{
switch appearance.name { switch appearance.name {
case .aqua, .vibrantLight, .accessibilityHighContrastAqua, .accessibilityHighContrastVibrantLight: case .aqua, .vibrantLight, .accessibilityHighContrastAqua,
.accessibilityHighContrastVibrantLight:
return light return light
case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark: case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua,
.accessibilityHighContrastVibrantDark:
return dark return dark
default: default:
return NSColor.white return NSColor.white
} }
} }
// ex: systemImage("sunrise.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemOrange])) // ex: systemImage("sunrise.circle.fill", .title2, .large,
func systemImage(_ name: String, _ size: NSFont.TextStyle, _ scale: NSImage.SymbolScale, _ configuration: NSImage.SymbolConfiguration) -> NSImage? { // .init(paletteColors: [.white, .systemOrange]))
func systemImage(_ name: String, _ size: NSFont.TextStyle,
_ scale: NSImage.SymbolScale,
_ configuration: NSImage.SymbolConfiguration) -> NSImage?
{
return NSImage(systemSymbolName: name, accessibilityDescription: nil)? return NSImage(systemSymbolName: name, accessibilityDescription: nil)?
.withSymbolConfiguration(NSImage.SymbolConfiguration(textStyle: size, scale: scale).applying(configuration)) .withSymbolConfiguration(
NSImage.SymbolConfiguration(
textStyle: size, scale: scale).applying(configuration
)
)
} }
func openLink(_ url: String) { func openLink(_ url: String) {
@@ -187,14 +203,18 @@ func openLink(_ url: String) {
} }
fileprivate extension Notification.Name { fileprivate extension Notification.Name {
static let beginMenuTracking = Notification.Name("com.apple.HIToolbox.beginMenuTrackingNotification") static let beginMenuTracking =
static let endMenuTracking = Notification.Name("com.apple.HIToolbox.endMenuTrackingNotification") Notification.Name("com.apple.HIToolbox.beginMenuTrackingNotification")
static let endMenuTracking =
Notification.Name("com.apple.HIToolbox.endMenuTrackingNotification")
} }
func persistMenuBar(_ state: Bool) { func persistMenuBar(_ state: Bool) {
if state { if state {
DistributedNotificationCenter.default().post(name: .beginMenuTracking, object: nil) DistributedNotificationCenter.default()
.post(name: .beginMenuTracking, object: nil)
} else { } else {
DistributedNotificationCenter.default().post(name: .endMenuTracking, object: nil) DistributedNotificationCenter.default()
.post(name: .endMenuTracking, object: nil)
} }
} }

View File

@@ -7,9 +7,13 @@ XCODE_PATH = $(shell xcode-select --print-path)
EXEC = CmdBar EXEC = CmdBar
SRCMODULES = UpdateManager.swift AboutViewController.swift AppDelegate.swift CBMenuBarItem.swift CircularProgressView.swift \ SRCMODULES = UpdateManager.swift AboutViewController.swift \
CmdManager.swift CmdViewController.swift CopyableTextView.swift EditableNSTextField.swift EventMonitor.swift Helpers.swift \ AppDelegate.swift CBMenuBarItem.swift \
MenulessWindow.swift PopoverPanel.swift QuietView.swift UpdateViewController.swift main.swift CircularProgressView.swift CmdManager.swift \
CmdViewController.swift CopyableTextView.swift \
EditableNSTextField.swift EventMonitor.swift Helpers.swift \
MenulessWindow.swift PopoverPanel.swift QuietView.swift \
UpdateViewController.swift main.swift
ARMOBJMODULES = $(addprefix ./arm64/,$(SRCMODULES:.swift=.o)) ARMOBJMODULES = $(addprefix ./arm64/,$(SRCMODULES:.swift=.o))
X86OBJMODULES = $(addprefix ./x86_64/,$(SRCMODULES:.swift=.o)) X86OBJMODULES = $(addprefix ./x86_64/,$(SRCMODULES:.swift=.o))
@@ -19,40 +23,51 @@ FRAMEWORKS = -framework AppKit -framework ServiceManagement
LIBS = -lzip LIBS = -lzip
zip: zip:
@$(MAKE) -C libs/Zip/Zip FLAGS=$(FLAGS) CFLAGS=$(CFLAGS) MACOS_VERSION=$(MACOS_VERSION) all @$(MAKE) -C libs/Zip/Zip FLAGS=$(FLAGS) CFLAGS=$(CFLAGS) \
MACOS_VERSION=$(MACOS_VERSION) all
cmdbar_updater: cmdbar_updater:
@$(MAKE) -C updater FLAGS=$(FLAGS) all @$(MAKE) -C updater FLAGS=$(FLAGS) all
@cp updater/$@ . @cp updater/$@ .
./arm64/%.o: %.swift ./arm64/%.o: %.swift
swift -frontend -c $(if $(DEBUG), -D DEBUG,) -target arm64-apple-macos$(MACOS_VERSION) $(FLAGS) \ swift -frontend -c $(if $(DEBUG), -D DEBUG,) \
-target arm64-apple-macos$(MACOS_VERSION) $(FLAGS) \
-I./libs/Zip/Zip -I./libs/Zip/Zip/arm64 -L./libs/Zip/Zip/arm64 \ -I./libs/Zip/Zip -I./libs/Zip/Zip/arm64 -L./libs/Zip/Zip/arm64 \
-primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) -sdk $(SDK) -module-name $(EXEC) \ -primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) \
-o $@ -emit-module && touch $@ -sdk $(SDK) -module-name $(EXEC) -o $@ -emit-module && touch $@
ifdef UNIVERSAL ifdef UNIVERSAL
./x86_64/%.o: %.swift ./x86_64/%.o: %.swift
@swift -frontend -c $(if $(DEBUG), -D DEBUG,) -target x86_64-apple-macos$(MACOS_VERSION) $(FLAGS) \ @swift -frontend -c $(if $(DEBUG), -D DEBUG,) \
-target x86_64-apple-macos$(MACOS_VERSION) $(FLAGS) \
-I./libs/Zip/Zip -I./libs/Zip/Zip/x86_64 -L./libs/Zip/Zip/x86_64 \ -I./libs/Zip/Zip -I./libs/Zip/Zip/x86_64 -L./libs/Zip/Zip/x86_64 \
-primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) -sdk $(SDK) -module-name $(EXEC) \ -primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) \
-o $@ -emit-module && touch $@ -sdk $(SDK) -module-name $(EXEC) -o $@ -emit-module && touch $@
endif endif
./arm64/$(EXEC): $(ARMOBJMODULES) ./arm64/$(EXEC): $(ARMOBJMODULES)
@ld -syslibroot $(SDK) -lSystem -arch arm64 -macos_version_min $(MACOS_VERSION).0 \ @ld -syslibroot $(SDK) -lSystem -arch arm64 \
-macos_version_min $(MACOS_VERSION).0 \
/Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \ /Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \
-sectcreate __TEXT __info_plist Info.plist -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \ -sectcreate __TEXT __info_plist Info.plist \
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib/swift -no_objc_category_merging -L $(XCODE_PATH) -rpath \ -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \
Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx ./arm64/main.o $(filter-out ./arm64/main.o, $(ARMOBJMODULES)) \ /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib/swift \
-no_objc_category_merging -L $(XCODE_PATH) \
-rpath Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
./arm64/main.o $(filter-out ./arm64/main.o, $(ARMOBJMODULES)) \
./libs/Zip/Zip/arm64/libzip.a -o $@ ./libs/Zip/Zip/arm64/libzip.a -o $@
ifdef UNIVERSAL ifdef UNIVERSAL
./x86_64/$(EXEC): $(X86OBJMODULES) ./x86_64/$(EXEC): $(X86OBJMODULES)
@ld -syslibroot $(SDK) -lSystem -arch x86_64 -macos_version_min $(MACOS_VERSION).0 \ @ld -syslibroot $(SDK) -lSystem -arch x86_64 \
-macos_version_min $(MACOS_VERSION).0 \
/Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \ /Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \
-sectcreate __TEXT __info_plist Info.plist -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \ -sectcreate __TEXT __info_plist Info.plist \
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib/swift -no_objc_category_merging -L $(XCODE_PATH) -rpath \ -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \
Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx ./x86_64/main.o $(filter-out ./x86_64/main.o, $(X86OBJMODULES)) \ /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib/swift \
-no_objc_category_merging -L $(XCODE_PATH) \
-rpath Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
./x86_64/main.o $(filter-out ./x86_64/main.o, $(X86OBJMODULES)) \
./libs/Zip/Zip/x86_64/libzip.a -o $@ ./libs/Zip/Zip/x86_64/libzip.a -o $@
endif endif
@@ -72,20 +87,22 @@ $(EXEC).app: $(EXEC)
cp resources/AppIcon.icns $@/Contents/Resources/ && \ cp resources/AppIcon.icns $@/Contents/Resources/ && \
cp $(EXEC) $@/Contents/MacOS/ && \ cp $(EXEC) $@/Contents/MacOS/ && \
cp cmdbar_updater $@/Contents/MacOS/ && \ cp cmdbar_updater $@/Contents/MacOS/ && \
$(if $(DEBUG), codesign --entitlements Grapp.entitlements -s ${APPLE_DEVELOPMENT} -f --timestamp -o runtime $(EXEC).app, \ $(if $(DEBUG), codesign --entitlements Grapp.entitlements \
codesign -s ${APPLE_DEVELOPER_ID_APPLICATION} -f --timestamp -o runtime $(EXEC).app) -s ${APPLE_DEVELOPMENT} -f --timestamp -o runtime $(EXEC).app, \
codesign -s ${APPLE_DEVELOPER_ID_APPLICATION} -f --timestamp \
-o runtime $(EXEC).app)
all: zip cmdbar_updater $(EXEC).app all: zip cmdbar_updater $(EXEC).app
clear:
clear
kill: kill:
-pkill $(EXEC) -pkill $(EXEC)
run: clear kill all run: kill all
./$(EXEC) ./$(EXEC)
open: kill all
open $(EXEC).app
clean: clean:
rm -rf $(EXEC) $(EXEC).app cmdbar_updater arm64 x86_64 rm -rf $(EXEC) $(EXEC).app cmdbar_updater arm64 x86_64
mkdir arm64 x86_64 mkdir arm64 x86_64

View File

@@ -23,7 +23,9 @@ class MenulessWindow: NSWindow {
let key = event.keyCode let key = event.keyCode
if event.type == NSEvent.EventType.keyDown { if event.type == NSEvent.EventType.keyDown {
if modsContains(keys: OSCmd, in: modifiers) && key == kVK_ANSI_W { if modsContains(keys: OSCmd, in: modifiers) &&
key == kVK_ANSI_W
{
performClose(nil) performClose(nil)
return true return true
} }

View File

@@ -6,7 +6,8 @@ class PopoverPanel: NSPanel {
init(viewController: NSViewController) { init(viewController: NSViewController) {
super.init( super.init(
contentRect: CGRect(x: 0, y: 0, width: 100, height: 100), contentRect: CGRect(x: 0, y: 0, width: 100, height: 100),
styleMask: [.titled, .nonactivatingPanel, .utilityWindow, .fullSizeContentView], styleMask: [.titled, .nonactivatingPanel, .utilityWindow,
.fullSizeContentView],
backing: .buffered, backing: .buffered,
defer: false defer: false
) )
@@ -22,7 +23,8 @@ class PopoverPanel: NSPanel {
titlebarAppearsTransparent = true titlebarAppearsTransparent = true
animationBehavior = .none animationBehavior = .none
collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary, .transient] collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary,
.transient]
isReleasedWhenClosed = false isReleasedWhenClosed = false
hidesOnDeactivate = false hidesOnDeactivate = false

View File

@@ -19,7 +19,12 @@ final class UpdateManager {
private var observation: NSKeyValueObservation? private var observation: NSKeyValueObservation?
func currentVersion() -> Double? { func currentVersion() -> Double? {
guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { return nil } guard let version =
Bundle.main.infoDictionary?["CFBundleShortVersionString"]
as?
String
else { return nil }
if let version = Double(version) if let version = Double(version)
{ return version } { return version }
else else
@@ -27,7 +32,9 @@ final class UpdateManager {
} }
func latestVersion() async -> VersionModel? { func latestVersion() async -> VersionModel? {
guard let url = URL(string: EndpointConstants.versionURL) else { return nil } guard let url = URL(string: EndpointConstants.versionURL)
else { return nil }
do { do {
let config = URLSessionConfiguration.default let config = URLSessionConfiguration.default
config.timeoutIntervalForResource = 5.0 config.timeoutIntervalForResource = 5.0
@@ -35,12 +42,15 @@ final class UpdateManager {
let (data, _) = try await session.data(from: url) let (data, _) = try await session.data(from: url)
// let (data, _) = try await URLSession.shared.data(from: url) // let (data, _) = try await URLSession.shared.data(from: url)
let response = try JSONDecoder().decode(VersionModel.self, from: data) let response =
try JSONDecoder().decode(VersionModel.self, from: data)
return response.ok ? response : nil return response.ok ? response : nil
} catch { return nil } } catch { return nil }
} }
func isUpdateAvailable() async -> (available: Bool?, model: VersionModel?) { func isUpdateAvailable() async -> (available: Bool?,
model: VersionModel?)
{
guard let curVersion = currentVersion(), guard let curVersion = currentVersion(),
let fetVersion = await latestVersion() let fetVersion = await latestVersion()
else { return (nil, nil) } else { return (nil, nil) }
@@ -51,10 +61,14 @@ final class UpdateManager {
return (false, nil) return (false, nil)
} }
func downloadUpdate(completionHandler: @escaping (_ data: Resulting<Bool, String>) -> Void) { func downloadUpdate(
guard let url = URL(string: EndpointConstants.appURLZip) else { completionHandler(.failure); return } completionHandler: @escaping (_ data: Resulting<Bool, String>) -> Void
) {
guard let url = URL(string: EndpointConstants.appURLZip)
else { completionHandler(.failure); return }
let task = URLSession.shared.downloadTask(with: url) { (tempURL, response, error) in let task = URLSession.shared.downloadTask(with: url)
{ (tempURL, response, error) in
guard guard
let tempURL = tempURL, let tempURL = tempURL,
error == nil, error == nil,
@@ -69,7 +83,10 @@ final class UpdateManager {
var userTmpURL = FileManager.default.temporaryDirectory var userTmpURL = FileManager.default.temporaryDirectory
var isDirectory = ObjCBool(true) var isDirectory = ObjCBool(true)
guard fileManager.fileExists(atPath: userTmpURL.path(), isDirectory: &isDirectory) else { completionHandler(.failure); return } guard fileManager.fileExists(atPath: userTmpURL.path(),
isDirectory: &isDirectory)
else { completionHandler(.failure); return }
userTmpURL = userTmpURL.appendingPathComponent("CmdBar.zip") userTmpURL = userTmpURL.appendingPathComponent("CmdBar.zip")
if fileManager.fileExists(atPath: userTmpURL.path()) { if fileManager.fileExists(atPath: userTmpURL.path()) {
do { do {
@@ -88,18 +105,28 @@ final class UpdateManager {
return return
} }
} }
observation = task.progress.observe(\.fractionCompleted, changeHandler: { [weak self] progress, _ in observation =
guard let delegate = self?.delegate else { return } task.progress.observe(\.fractionCompleted, changeHandler:
delegate.downloadProgressChanged(progress.fractionCompleted) { [weak self] progress, _ in
}) guard let delegate = self?.delegate else { return }
delegate
.downloadProgressChanged(progress.fractionCompleted)
}
)
task.resume() task.resume()
} }
func extractUpdate(completionHandler: @escaping (_ data: Resulting<Bool, String>) -> Void) { func extractUpdate(
completionHandler: @escaping (_ data: Resulting<Bool, String>) -> Void
) {
let fileManager = FileManager.default let fileManager = FileManager.default
let userTmpURL = fileManager.temporaryDirectory.appending(path: "CmdBar.zip") let userTmpURL =
fileManager.temporaryDirectory.appending(path: "CmdBar.zip")
do { do {
try Zip.unzipFile(userTmpURL, destination: FileManager.default.temporaryDirectory, overwrite: true, password: nil) { [weak self] progress in try Zip.unzipFile(userTmpURL,
destination: FileManager.default.temporaryDirectory,
overwrite: true, password: nil)
{ [weak self] progress in
guard let delegate = self?.delegate else { return } guard let delegate = self?.delegate else { return }
delegate.unzipProgressChanged(progress) delegate.unzipProgressChanged(progress)
} }
@@ -112,13 +139,16 @@ final class UpdateManager {
} }
func removeUpdate() { func removeUpdate() {
let tmp = FileManager.default.temporaryDirectory.appending(path: "CmdBar.app") let tmp = FileManager.default
.temporaryDirectory.appending(path: "CmdBar.app")
if !existsAtPath(tmp.path()) { return } if !existsAtPath(tmp.path()) { return }
try? FileManager.default.removeItem(at: tmp) try? FileManager.default.removeItem(at: tmp)
} }
func installUpdate() { func installUpdate() {
if let url = Bundle.main.url(forAuxiliaryExecutable: "cmdbar_updater") { if let url =
Bundle.main.url(forAuxiliaryExecutable: "cmdbar_updater")
{
let task = Process() let task = Process()
task.executableURL = url task.executableURL = url
try? task.run() try? task.run()
@@ -130,7 +160,9 @@ final class UpdateManager {
} }
} }
fileprivate func existsAtPath(_ path: String, isDirectory: Bool = false) -> Bool { fileprivate func existsAtPath(_ path: String,
isDirectory: Bool = false) -> Bool
{
var isDirectory: ObjCBool = ObjCBool(isDirectory) var isDirectory: ObjCBool = ObjCBool(isDirectory)
let exists = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) let exists = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)
return exists return exists

View File

@@ -17,7 +17,8 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate {
private var appIconImage: NSImageView = { private var appIconImage: NSImageView = {
let image = NSImageView() let image = NSImageView()
image.image = NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath) image.image =
NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
image.imageScaling = .scaleAxesIndependently image.imageScaling = .scaleAxesIndependently
image.translatesAutoresizingMaskIntoConstraints = false image.translatesAutoresizingMaskIntoConstraints = false
return image return image
@@ -29,8 +30,12 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate {
textField.isBezeled = false textField.isBezeled = false
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .left textField.alignment = .left
textField.textColor = NSColor(name: nil) { getColors(.black, .white, for: $0) } textField.textColor =
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .bold) NSColor(name: nil) { getColors(.black, .white, for: $0) }
textField.font = NSFont
.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .body).pointSize,
weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -44,7 +49,10 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate {
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .left textField.alignment = .left
textField.textColor = .gray textField.textColor = .gray
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .bold) textField.font = NSFont
.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .body).pointSize,
weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -56,7 +64,10 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate {
(scrollableTextView.documentView as? NSTextView)?.isEditable = false (scrollableTextView.documentView as? NSTextView)?.isEditable = false
(scrollableTextView.documentView as? NSTextView)?.textColor = .gray (scrollableTextView.documentView as? NSTextView)?.textColor = .gray
(scrollableTextView.documentView as? NSTextView)?.font = (scrollableTextView.documentView as? NSTextView)?.font =
NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .regular) NSFont
.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .body).pointSize,
weight: .regular)
scrollableTextView.translatesAutoresizingMaskIntoConstraints = false scrollableTextView.translatesAutoresizingMaskIntoConstraints = false
return scrollableTextView return scrollableTextView
}() }()
@@ -124,26 +135,49 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate {
stackView.setHuggingPriority(.required, for: .vertical) stackView.setHuggingPriority(.required, for: .vertical)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
appIconImage.widthAnchor.constraint(equalToConstant: 70), appIconImage.widthAnchor
appIconImage.heightAnchor.constraint(equalTo: appIconImage.widthAnchor,multiplier: 1), .constraint(equalToConstant: 70),
appIconImage.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing10), appIconImage.heightAnchor
appIconImage.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing10), .constraint(equalTo: appIconImage.widthAnchor,
multiplier: 1),
appIconImage.topAnchor
.constraint(equalTo: view.topAnchor,
constant: ViewConstants.spacing10),
appIconImage.leadingAnchor
.constraint(equalTo: view.leadingAnchor,
constant: ViewConstants.spacing10),
statusLabel.topAnchor.constraint(equalTo: appIconImage.topAnchor, constant: ViewConstants.spacing10), statusLabel.topAnchor
statusLabel.leadingAnchor.constraint(equalTo: appIconImage.trailingAnchor, constant: ViewConstants.spacing10), .constraint(equalTo: appIconImage.topAnchor,
constant: ViewConstants.spacing10),
statusLabel.leadingAnchor
.constraint(equalTo: appIconImage.trailingAnchor,
constant: ViewConstants.spacing10),
subStatusLabel.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: ViewConstants.spacing10), subStatusLabel.topAnchor
subStatusLabel.leadingAnchor.constraint(equalTo: statusLabel.leadingAnchor), .constraint(equalTo: statusLabel.bottomAnchor,
constant: ViewConstants.spacing10),
subStatusLabel.leadingAnchor
.constraint(equalTo: statusLabel.leadingAnchor),
changelogLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor), changelogLabel.widthAnchor
.constraint(equalTo: stackView.widthAnchor),
changelogLabel.heightAnchor.constraint(equalToConstant: 150), changelogLabel.heightAnchor.constraint(equalToConstant: 150),
stackView.topAnchor.constraint(equalTo: subStatusLabel.bottomAnchor, constant: ViewConstants.spacing2), stackView.topAnchor
stackView.leadingAnchor.constraint(equalTo: statusLabel.leadingAnchor), .constraint(equalTo: subStatusLabel.bottomAnchor,
view.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: ViewConstants.spacing20), constant: ViewConstants.spacing2),
view.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: ViewConstants.spacing20), stackView.leadingAnchor
.constraint(equalTo: statusLabel.leadingAnchor),
view.trailingAnchor
.constraint(equalTo: stackView.trailingAnchor,
constant: ViewConstants.spacing20),
view.bottomAnchor
.constraint(equalTo: stackView.bottomAnchor,
constant: ViewConstants.spacing20),
buttonsStackView.heightAnchor.constraint(equalTo: updateButton.heightAnchor), buttonsStackView.heightAnchor
.constraint(equalTo: updateButton.heightAnchor),
]) ])
} }
@@ -181,33 +215,41 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate {
self?.statusLabel.stringValue = UpdateStatus.latest.rawValue self?.statusLabel.stringValue = UpdateStatus.latest.rawValue
break break
case .checking: case .checking:
self?.statusLabel.stringValue = UpdateStatus.checking.rawValue self?.statusLabel.stringValue =
UpdateStatus.checking.rawValue
self?.updateButton.keyEquivalent = "" self?.updateButton.keyEquivalent = ""
self?.updateButton.isEnabled = false self?.updateButton.isEnabled = false
case .available: case .available:
self?.updateButton.title = "Download" self?.updateButton.title = "Download"
self?.updateButton.keyEquivalent = "" self?.updateButton.keyEquivalent = ""
self?.statusLabel.stringValue = UpdateStatus.available.rawValue self?.statusLabel.stringValue =
UpdateStatus.available.rawValue
self?.updateButton.action = #selector(self!.downloadUpdate) self?.updateButton.action = #selector(self!.downloadUpdate)
case .downloading: case .downloading:
self?.updateButton.isEnabled = false self?.updateButton.isEnabled = false
self?.updateButton.title = "Install" self?.updateButton.title = "Install"
self?.progressIndicator.isHidden = false self?.progressIndicator.isHidden = false
self?.statusLabel.stringValue = UpdateStatus.downloading.rawValue self?.statusLabel.stringValue =
UpdateStatus.downloading.rawValue
case .extracting: case .extracting:
self?.updateButton.isEnabled = false self?.updateButton.isEnabled = false
self?.updateButton.title = "Install" self?.updateButton.title = "Install"
self?.progressIndicator.isHidden = false self?.progressIndicator.isHidden = false
self?.statusLabel.stringValue = UpdateStatus.extracting.rawValue self?.statusLabel.stringValue =
UpdateStatus.extracting.rawValue
case .install: case .install:
self?.updateButton.title = "Install" self?.updateButton.title = "Install"
self?.progressIndicator.isHidden = false self?.progressIndicator.isHidden = false
self?.statusLabel.stringValue = UpdateStatus.install.rawValue self?.statusLabel.stringValue =
self?.updateButton.action = #selector(self!.installUpdate) UpdateStatus.install.rawValue
self?.updateButton.action =
#selector(self!.installUpdate)
case .checkFailed: case .checkFailed:
self?.statusLabel.stringValue = UpdateStatus.checkFailed.rawValue self?.statusLabel.stringValue =
UpdateStatus.checkFailed.rawValue
case .installFailed: case .installFailed:
self?.statusLabel.stringValue = UpdateStatus.installFailed.rawValue self?.statusLabel.stringValue =
UpdateStatus.installFailed.rawValue
self?.updateButton.action = #selector(self!.checkForUpdate) self?.updateButton.action = #selector(self!.checkForUpdate)
} }
} }
@@ -227,9 +269,15 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate {
self?.subStatusLabel.isHidden = false self?.subStatusLabel.isHidden = false
self?.changelogLabel.isHidden = false self?.changelogLabel.isHidden = false
for (i, change) in model.changes.enumerated() { for (i, change) in model.changes.enumerated() {
(self?.changelogLabel.documentView as? NSTextView)?.string += "\u{2022} \(change)" (
self?.changelogLabel.documentView as?
NSTextView
)?.string += "\u{2022} \(change)"
if i < model.changes.count-1 { if i < model.changes.count-1 {
(self?.changelogLabel.documentView as? NSTextView)?.string += "\n" (
self?.changelogLabel.documentView as?
NSTextView
)?.string += "\n"
} }
} }
} }

View File

@@ -7,13 +7,18 @@
import AppKit import AppKit
fileprivate func existsAtPath(_ path: String, isDirectory: Bool = false) -> Bool { fileprivate func existsAtPath(_ path: String,
isDirectory: Bool = false) -> Bool
{
var isDirectory: ObjCBool = ObjCBool(isDirectory) var isDirectory: ObjCBool = ObjCBool(isDirectory)
let exists = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) let exists =
FileManager.default.fileExists(atPath: path,
isDirectory: &isDirectory)
return exists return exists
} }
let tmp = FileManager.default.temporaryDirectory.appending(path: "CmdBar.app") let tmp =
FileManager.default.temporaryDirectory.appending(path: "CmdBar.app")
let app = Bundle.main.bundleURL let app = Bundle.main.bundleURL
if !existsAtPath(tmp.path(), isDirectory: true) { exit(1) } if !existsAtPath(tmp.path(), isDirectory: true) { exit(1) }
@@ -28,6 +33,9 @@ do {
} }
let configuration = NSWorkspace.OpenConfiguration() let configuration = NSWorkspace.OpenConfiguration()
NSWorkspace.shared.openApplication(at: app, configuration: configuration, completionHandler: nil) NSWorkspace.shared.openApplication(at: app, configuration: configuration,
completionHandler: nil)
usleep(100000*2) // Allow 2 seconds to let `NSWorkspace.shared.openApplication` finish launching our app. // Allow 2 seconds to let `NSWorkspace.shared.openApplication` finish
// launching our app.
usleep(100000*2)