diff --git a/src/AboutViewController.swift b/src/AboutViewController.swift index c559c33..2f568dd 100644 --- a/src/AboutViewController.swift +++ b/src/AboutViewController.swift @@ -23,7 +23,8 @@ class AboutViewController: NSViewController, NSTextFieldDelegate { // MARK: - Views private var appIconImage: 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.translatesAutoresizingMaskIntoConstraints = false return image @@ -31,25 +32,35 @@ class AboutViewController: NSViewController, NSTextFieldDelegate { private var appNameLabel: 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.isBezeled = false textField.drawsBackground = false 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 return textField }() private var versionLabel: 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.isBezeled = false textField.drawsBackground = false textField.alignment = .center 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 return textField }() @@ -64,7 +75,10 @@ class AboutViewController: NSViewController, NSTextFieldDelegate { textField.drawsBackground = false textField.alignment = .center 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 return textField }() @@ -143,39 +157,68 @@ class AboutViewController: NSViewController, NSTextFieldDelegate { // App image. NSLayoutConstraint.activate([ appIconImage.widthAnchor.constraint(equalToConstant: 100), - appIconImage.heightAnchor.constraint(equalTo: appIconImage.widthAnchor, multiplier: 1), - appIconImage.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing20), - appIconImage.centerXAnchor.constraint(equalTo: view.centerXAnchor), + appIconImage.heightAnchor + .constraint(equalTo: appIconImage.widthAnchor, + multiplier: 1), + appIconImage.topAnchor + .constraint(equalTo: view.topAnchor, + constant: ViewConstants.spacing20), + appIconImage.centerXAnchor + .constraint(equalTo: view.centerXAnchor), ]) // Title NSLayoutConstraint.activate([ - appNameLabel.topAnchor.constraint(equalTo: appIconImage.bottomAnchor, constant: ViewConstants.spacing20), - appNameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + appNameLabel.topAnchor + .constraint(equalTo: appIconImage.bottomAnchor, + constant: ViewConstants.spacing20), + appNameLabel.centerXAnchor + .constraint(equalTo: view.centerXAnchor), - versionLabel.topAnchor.constraint(equalTo: appNameLabel.bottomAnchor, constant: ViewConstants.spacing2), - versionLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + versionLabel.topAnchor + .constraint(equalTo: appNameLabel.bottomAnchor, + constant: ViewConstants.spacing2), + versionLabel.centerXAnchor + .constraint(equalTo: view.centerXAnchor), - copyrightLabel.topAnchor.constraint(equalTo: versionLabel.bottomAnchor, constant: ViewConstants.spacing10), - copyrightLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + copyrightLabel.topAnchor + .constraint(equalTo: versionLabel.bottomAnchor, + constant: ViewConstants.spacing10), + copyrightLabel.centerXAnchor + .constraint(equalTo: view.centerXAnchor), ]) // Buttons NSLayoutConstraint.activate([ - buttonsContainer.topAnchor.constraint(equalTo: copyrightLabel.bottomAnchor, constant: ViewConstants.spacing20), - buttonsContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -ViewConstants.spacing20), - buttonsContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor), + buttonsContainer.topAnchor + .constraint(equalTo: copyrightLabel.bottomAnchor, + 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.bottomAnchor.constraint(equalTo: buttonsContainer.bottomAnchor), - privacyButton.leadingAnchor.constraint(equalTo: buttonsContainer.leadingAnchor), + privacyButton.topAnchor + .constraint(equalTo: buttonsContainer.topAnchor), + privacyButton.bottomAnchor + .constraint(equalTo: buttonsContainer.bottomAnchor), + privacyButton.leadingAnchor + .constraint(equalTo: buttonsContainer.leadingAnchor), - documentationButton.firstBaselineAnchor.constraint(equalTo: privacyButton.firstBaselineAnchor), - documentationButton.leadingAnchor.constraint(equalTo: privacyButton.trailingAnchor, constant: ViewConstants.spacing10), + documentationButton.firstBaselineAnchor + .constraint(equalTo: privacyButton.firstBaselineAnchor), + documentationButton.leadingAnchor + .constraint(equalTo: privacyButton.trailingAnchor, + constant: ViewConstants.spacing10), - websiteButton.firstBaselineAnchor.constraint(equalTo: privacyButton.firstBaselineAnchor), - websiteButton.leadingAnchor.constraint(equalTo: documentationButton.trailingAnchor, constant: ViewConstants.spacing10), - websiteButton.trailingAnchor.constraint(equalTo: buttonsContainer.trailingAnchor), + websiteButton.firstBaselineAnchor + .constraint(equalTo: privacyButton.firstBaselineAnchor), + websiteButton.leadingAnchor + .constraint(equalTo: documentationButton.trailingAnchor, + constant: ViewConstants.spacing10), + websiteButton.trailingAnchor + .constraint(equalTo: buttonsContainer.trailingAnchor), ]) } diff --git a/src/AppDelegate.swift b/src/AppDelegate.swift index fa26920..2860e83 100644 --- a/src/AppDelegate.swift +++ b/src/AppDelegate.swift @@ -22,7 +22,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { // MARK: - Notifications 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() { diff --git a/src/CBMenuBarItem.swift b/src/CBMenuBarItem.swift index 357b086..af4858a 100644 --- a/src/CBMenuBarItem.swift +++ b/src/CBMenuBarItem.swift @@ -40,7 +40,8 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate { private var globalEventMonitor: EventMonitor? override init() { - self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + self.statusItem = NSStatusBar.system + .statusItem(withLength: NSStatusItem.variableLength) self.statusItem.isVisible = true self.panel = PopoverPanel(viewController: CmdViewController()) @@ -48,38 +49,49 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate { // Events // Shows panel and keeps the button highlighted. - localEventMonitor = LocalEventMonitor(mask: [.leftMouseDown]) { [weak self] event in - if let button = self?.statusItem.button, event.window == button.window, !event.modifierFlags.contains(.command) { - self?.statusButtonPressed(button) - return nil - } - - 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() + localEventMonitor = LocalEventMonitor(mask: [.leftMouseDown]) + { [weak self] event in + if let button = self?.statusItem.button, + event.window == button.window, + !event.modifierFlags.contains(.command) + { + self?.statusButtonPressed(button) + return nil } - } - 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 // works properly without global event monitor in MacOS Sequoia? // Resign key whenever clicking outside of panel, thus hiding the // panel and un-highlighting the button. // In order to receive events, the program needs to be codesigned. - globalEventMonitor = GlobalEventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in - if let panel = self?.panel, panel.isKeyWindow { - panel.resignKey() - } - } + globalEventMonitor = + GlobalEventMonitor(mask: [.leftMouseDown, .rightMouseDown]) + { [weak self] event in + if let panel = self?.panel, panel.isKeyWindow { + panel.resignKey() + } + } if let statusButton = statusItem.button, let statusButtonWindow = statusButton.window { @@ -118,7 +130,8 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate { private func dismissPanel() { NSAnimationContext.runAnimationGroup { context in context.duration = 0.3 - context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + context.timingFunction = + CAMediaTimingFunction(name: .easeInEaseOut) panel.animator().alphaValue = 0 } completionHandler: { [weak self] in self?.panel.orderOut(nil) @@ -129,32 +142,26 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate { } private func setPanelPosition() { - guard let statusItemView = statusItem.button?.window else { - panel.center() - return + guard let scrn = NSScreen.main, + let iFrame = statusItem.button?.window?.frame + 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 - - 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) + panel.setFrameTopLeftPoint(NSPoint(x: x, y: y)) } private func setButtonHighlighted(to highlight: Bool) { @@ -163,7 +170,9 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate { func setImage(title: String?, description: String) { if title != nil { - statusItem.button!.image = NSImage(systemSymbolName: title!, accessibilityDescription: description) + statusItem.button!.image = + NSImage(systemSymbolName: title!, + accessibilityDescription: description) return } statusItem.button!.image = nil @@ -178,17 +187,23 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate { } 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) } 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) } 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() } } @@ -196,7 +211,8 @@ final class CBMenuBarItem: NSObject, NSWindowDelegate { func windowDidMove(_ notification: Notification) { if let statusBarButtonWindow = notification.object as? NSWindow, let buttonWindow = statusItem.button?.window, - buttonWindow == statusBarButtonWindow + buttonWindow == statusBarButtonWindow, + panel.isVisible { setPanelPosition() } diff --git a/src/CircularProgressView.swift b/src/CircularProgressView.swift index 1a47670..7d12408 100644 --- a/src/CircularProgressView.swift +++ b/src/CircularProgressView.swift @@ -47,7 +47,10 @@ class CircularProgressView: NSView { ) backgroundLayer.path = bgMutablePath 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.lineWidth = lineWidth @@ -64,7 +67,8 @@ class CircularProgressView: NSView { foregroundLayer.lineCap = .round foregroundLayer.lineWidth = lineWidth 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(foregroundLayer) @@ -73,7 +77,10 @@ class CircularProgressView: NSView { private func advanceProgress(to value: Double) { let animation = CABasicAnimation(keyPath: "strokeEnd") animation.duration = animationSpeed - animation.fromValue = foregroundLayer.presentation()?.value(forKeyPath: "strokeEnd") ?? value + animation.fromValue = + foregroundLayer.presentation()?.value(forKeyPath: "strokeEnd") + ?? + value animation.toValue = value foregroundLayer.strokeEnd = CGFloat(value) foregroundLayer.add(animation, forKey: "pathAnimation") diff --git a/src/CmdManager.swift b/src/CmdManager.swift index 7322e7a..8f8cab4 100644 --- a/src/CmdManager.swift +++ b/src/CmdManager.swift @@ -5,7 +5,8 @@ class CmdFile { private(set) var lastExec = Int(Date().timeIntervalSince1970) var untilNextExec: Int { get { - let res = reloadTime - (Int(Date().timeIntervalSince1970) - lastExec) + let res = + reloadTime - (Int(Date().timeIntervalSince1970) - lastExec) if res <= 0 { return reloadTime } return res } @@ -40,10 +41,13 @@ class CmdFile { let execResult = execute(atPath: url) DispatchQueue.main.async { 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("") } else { - self?.statusItem.setImage(title: nil, description: "") + self?.statusItem + .setImage(title: nil, description: "") self?.statusItem.setTitle(execResult.0) } self?.statusItem.setContents(to: execResult.body) @@ -102,23 +106,40 @@ class CmdManager { private func getPaths() { let manager = FileManager.default - let path = manager.homeDirectoryForCurrentUser.appending(path: ".cmdbar") - if let contents = try? manager.contentsOfDirectory(atPath: path.path()) { + let path = + manager.homeDirectoryForCurrentUser.appending(path: ".cmdbar") + if let contents = + try? manager.contentsOfDirectory(atPath: path.path()) + { for content in contents { let filePath = path.appending(path: content) let domains = filePath.path().components(separatedBy: ".") if (isValidTimeFormat(domains[domains.count - 1])) { - paths.append(CmdFile(url: filePath, 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]))) + paths.append( + CmdFile(url: filePath, + 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() { - defaultStatusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + defaultStatusItem = NSStatusBar.system + .statusItem(withLength: NSStatusItem.variableLength) if let btn = defaultStatusItem?.button { let image = NSApp.applicationIconImage image!.size = NSSize(width: 22, height: 22) @@ -126,13 +147,18 @@ class CmdManager { } guard defaultStatusItem != nil else { return } 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(title: "About", action: #selector(showAbout), keyEquivalent: "")) + menu.addItem(NSMenuItem(title: "About", + action: #selector(showAbout), keyEquivalent: "")) 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(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) + menu.addItem(NSMenuItem(title: "Quit", + action: #selector(NSApplication.terminate(_:)), + keyEquivalent: "q")) defaultStatusItem!.menu = menu } diff --git a/src/CmdViewController.swift b/src/CmdViewController.swift index d585aac..4ab61de 100644 --- a/src/CmdViewController.swift +++ b/src/CmdViewController.swift @@ -16,6 +16,9 @@ class CmdViewController: NSViewController { private var timer: DispatchSourceTimer? private var keyboardEvents: EventMonitor? + private var maxWidthContraint: NSLayoutConstraint! + private var maxHeightContraint: NSLayoutConstraint! + private var visualEffectView: NSView = { let visualEffect = NSVisualEffectView() visualEffect.blendingMode = .behindWindow @@ -38,14 +41,20 @@ class CmdViewController: NSViewController { private var textLabel: 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.isHidden = false textField.isEditable = false textField.isBezeled = false textField.alignment = .left - textField.textColor = NSColor(name: nil) { getColors(.black, .white, for: $0) } - textField.font = NSFont.monospacedSystemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .regular) + textField.textColor = + NSColor(name: nil) { getColors(.black, .white, for: $0) } + textField.font = NSFont + .monospacedSystemFont(ofSize: NSFontDescriptor + .preferredFontDescriptor(forTextStyle: .body).pointSize, + weight: .regular) textField.translatesAutoresizingMaskIntoConstraints = false return textField }() @@ -60,8 +69,12 @@ class CmdViewController: NSViewController { textField.isBezeled = false textField.drawsBackground = false textField.alignment = .left - textField.textColor = NSColor(name: nil) { getColors(.darkGray, .lightGray, for: $0) } - textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .bold) + textField.textColor = + NSColor(name: nil) { getColors(.darkGray, .lightGray, for: $0) } + textField.font = NSFont + .systemFont(ofSize: NSFontDescriptor + .preferredFontDescriptor(forTextStyle: .subheadline).pointSize, + weight: .bold) textField.translatesAutoresizingMaskIntoConstraints = false return textField }() @@ -80,15 +93,19 @@ class CmdViewController: NSViewController { textField.isBezeled = false textField.drawsBackground = false textField.alignment = .center - textField.textColor = NSColor(name: nil) { getColors(.darkGray, .lightGray, for: $0) } - textField.font = NSFont(descriptor: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body), size: 0) + textField.textColor = + NSColor(name: nil) { getColors(.darkGray, .lightGray, for: $0) } + textField.font = + NSFont(descriptor: NSFontDescriptor + .preferredFontDescriptor(forTextStyle: .body), size: 0) textField.translatesAutoresizingMaskIntoConstraints = false return textField }() private var quitButton: 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.action = #selector(terminateApp) button.sizeToFit() @@ -99,7 +116,9 @@ class CmdViewController: NSViewController { private var reloadButton: 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.action = #selector(reloadWidget) button.sizeToFit() @@ -110,7 +129,8 @@ class CmdViewController: NSViewController { private var startButton: 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.action = #selector(openAtLogin) button.sizeToFit() @@ -121,7 +141,8 @@ class CmdViewController: NSViewController { private var aboutButton: 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.action = #selector(showAbout) button.sizeToFit() @@ -132,7 +153,12 @@ class CmdViewController: NSViewController { private var updateButton: 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.action = #selector(updateApp) button.sizeToFit() @@ -148,7 +174,8 @@ class CmdViewController: NSViewController { blurView.material = .sheet blurView.state = .active 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.translatesAutoresizingMaskIntoConstraints = false return blurView @@ -190,9 +217,19 @@ class CmdViewController: NSViewController { 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() { super.viewDidAppear() + // TODO: Check so that there are weird behaviors on the + textLabelScrollView.flashScrollers() + startReloadTextTimer() keyboardEvents?.start() @@ -207,7 +244,8 @@ class CmdViewController: NSViewController { keyboardEvents?.stop() - if let editor = textLabel.currentEditor() { textLabel.endEditing(editor) } + if let editor = textLabel.currentEditor() + { textLabel.endEditing(editor) } } private func setContraints() { @@ -220,7 +258,9 @@ class CmdViewController: NSViewController { func setup() { openAtLoginToggle() - buttonsBackground.layer?.cornerRadius = quitButton.bounds.height / Metrics.buttonSpacing + Metrics.buttonSpacing + buttonsBackground.layer?.cornerRadius = + quitButton.bounds.height / Metrics.buttonSpacing + + Metrics.buttonSpacing setupKeyEvents() } @@ -230,110 +270,198 @@ class CmdViewController: NSViewController { progressView.widthAnchor.constraint(equalToConstant: 16), progressView.heightAnchor.constraint(equalToConstant: 16), - progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Metrics.padding), - progressView.centerYAnchor.constraint(equalTo: reloadsText.centerYAnchor), + progressView.leadingAnchor + .constraint(equalTo: view.leadingAnchor, + constant: Metrics.padding), + progressView.centerYAnchor + .constraint(equalTo: reloadsText.centerYAnchor), ]) NSLayoutConstraint.activate([ - reloadsText.leadingAnchor.constraint(equalTo: progressView.trailingAnchor, constant: Metrics.buttonSpacing), - reloadsText.centerYAnchor.constraint(equalTo: buttonsBackground.centerYAnchor), - reloadsText.trailingAnchor.constraint(equalTo: buttonsBackground.leadingAnchor, constant: -Metrics.buttonSpacing), + reloadsText.leadingAnchor + .constraint(equalTo: progressView.trailingAnchor, + constant: Metrics.buttonSpacing), + reloadsText.centerYAnchor + .constraint(equalTo: buttonsBackground.centerYAnchor), + reloadsText.trailingAnchor + .constraint(equalTo: buttonsBackground.leadingAnchor, + constant: -Metrics.buttonSpacing), ]) } private func setViewConstraints() { NSLayoutConstraint.activate([ visualEffectView.topAnchor.constraint(equalTo: view.topAnchor), - visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - visualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - visualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + visualEffectView.bottomAnchor + .constraint(equalTo: view.bottomAnchor), + visualEffectView.leadingAnchor + .constraint(equalTo: view.leadingAnchor), + visualEffectView.trailingAnchor + .constraint(equalTo: view.trailingAnchor), ]) } private func setButtonConstraints() { NSLayoutConstraint.activate([ - quitButton.widthAnchor.constraint(equalToConstant: quitButton.bounds.width), - quitButton.heightAnchor.constraint(equalToConstant: quitButton.bounds.height), - reloadButton.widthAnchor.constraint(equalToConstant: reloadButton.bounds.width), - reloadButton.heightAnchor.constraint(equalToConstant: reloadButton.bounds.height), - startButton.widthAnchor.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), + quitButton.widthAnchor + .constraint(equalToConstant: quitButton.bounds.width), + quitButton.heightAnchor + .constraint(equalToConstant: quitButton.bounds.height), + reloadButton.widthAnchor + .constraint(equalToConstant: reloadButton.bounds.width), + reloadButton.heightAnchor + .constraint(equalToConstant: reloadButton.bounds.height), + startButton.widthAnchor + .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.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -Metrics.padding), + buttonsContainer.trailingAnchor + .constraint(equalTo: view.trailingAnchor, + constant: -Metrics.padding), + buttonsContainer.bottomAnchor + .constraint(equalTo: view.bottomAnchor, + constant: -Metrics.padding), - buttonsBackground.leadingAnchor.constraint(equalTo: buttonsContainer.leadingAnchor), - buttonsBackground.trailingAnchor.constraint(equalTo: buttonsContainer.trailingAnchor), - buttonsBackground.topAnchor.constraint(equalTo: buttonsContainer.topAnchor), - buttonsBackground.bottomAnchor.constraint(equalTo: buttonsContainer.bottomAnchor), + buttonsBackground.leadingAnchor + .constraint(equalTo: buttonsContainer.leadingAnchor), + buttonsBackground.trailingAnchor + .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.topAnchor.constraint(equalTo: buttonsContainer.topAnchor, constant: Metrics.buttonSpacing), - quitButton.bottomAnchor.constraint(equalTo: buttonsContainer.bottomAnchor, constant: -Metrics.buttonSpacing), + quitButton.trailingAnchor + .constraint(equalTo: buttonsContainer.trailingAnchor, + 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.trailingAnchor.constraint(equalTo: quitButton.leadingAnchor, constant: -Metrics.buttonSpacing), + reloadButton.firstBaselineAnchor + .constraint(equalTo: quitButton.firstBaselineAnchor), + reloadButton.trailingAnchor + .constraint(equalTo: quitButton.leadingAnchor, + constant: -Metrics.buttonSpacing), - startButton.firstBaselineAnchor.constraint(equalTo: reloadButton.firstBaselineAnchor), - startButton.trailingAnchor.constraint(equalTo: reloadButton.leadingAnchor, constant: -Metrics.buttonSpacing), + startButton.firstBaselineAnchor + .constraint(equalTo: reloadButton.firstBaselineAnchor), + startButton.trailingAnchor + .constraint(equalTo: reloadButton.leadingAnchor, + constant: -Metrics.buttonSpacing), - aboutButton.firstBaselineAnchor.constraint(equalTo: startButton.firstBaselineAnchor), - aboutButton.trailingAnchor.constraint(equalTo: startButton.leadingAnchor, constant: -Metrics.buttonSpacing), + aboutButton.firstBaselineAnchor + .constraint(equalTo: startButton.firstBaselineAnchor), + aboutButton.trailingAnchor + .constraint(equalTo: startButton.leadingAnchor, + constant: -Metrics.buttonSpacing), - updateButton.firstBaselineAnchor.constraint(equalTo: startButton.firstBaselineAnchor), - updateButton.leadingAnchor.constraint(equalTo: buttonsContainer.leadingAnchor, constant: Metrics.buttonSpacing), - updateButton.trailingAnchor.constraint(equalTo: aboutButton.leadingAnchor, constant: -Metrics.buttonSpacing), + updateButton.firstBaselineAnchor + .constraint(equalTo: startButton.firstBaselineAnchor), + updateButton.leadingAnchor + .constraint(equalTo: buttonsContainer.leadingAnchor, + constant: Metrics.buttonSpacing), + updateButton.trailingAnchor + .constraint(equalTo: aboutButton.leadingAnchor, + constant: -Metrics.buttonSpacing), ]) } private func setTextConstraints() { NSLayoutConstraint.activate([ - statusText.centerXAnchor.constraint(equalTo: view.centerXAnchor), - statusText.centerYAnchor.constraint(equalTo: view.centerYAnchor), + statusText.centerXAnchor + .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) - let bottom = textLabel.bottomAnchor.constraint(equalTo: textLabelScrollView.bottomAnchor) + let bottom = + textLabel.bottomAnchor + .constraint(equalTo: textLabelScrollView.bottomAnchor) bottom.priority = .init(600) + maxWidthContraint = + textLabelScrollView.widthAnchor + .constraint(lessThanOrEqualToConstant: Metrics.contentMaxWidth) + maxHeightContraint = + textLabelScrollView.heightAnchor + .constraint(lessThanOrEqualToConstant: Metrics.contentMaxHeight) + textLabel.setContentHuggingPriority(.required, for: .horizontal) NSLayoutConstraint.activate([ - textLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: Metrics.contentMinWidth), - textLabelScrollView.widthAnchor.constraint(lessThanOrEqualToConstant: Metrics.contentMaxWidth), + textLabel.widthAnchor + .constraint( + greaterThanOrEqualToConstant: Metrics.contentMinWidth + ), + // textLabelScrollView.widthAnchor + // .constraint( + // lessThanOrEqualToConstant: Metrics.contentMaxWidth + // ), + maxWidthContraint, - textLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: Metrics.contentMinHeight), - textLabelScrollView.heightAnchor.constraint(lessThanOrEqualToConstant: Metrics.contentMaxHeight), + textLabel.heightAnchor + .constraint( + greaterThanOrEqualToConstant: Metrics.contentMinHeight + ), + // textLabelScrollView.heightAnchor + // .constraint( + // lessThanOrEqualToConstant: Metrics.contentMaxHeight + // ), + maxHeightContraint, - textLabel.topAnchor.constraint(equalTo: textLabelScrollView.topAnchor), + textLabel.topAnchor + .constraint(equalTo: textLabelScrollView.topAnchor), bottom, - textLabel.leadingAnchor.constraint(equalTo: textLabelScrollView.leadingAnchor), + textLabel.leadingAnchor + .constraint(equalTo: textLabelScrollView.leadingAnchor), trailing, - textLabelScrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10), - textLabelScrollView.bottomAnchor.constraint(equalTo: buttonsContainer.topAnchor, constant: -10), - textLabelScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10), - textLabelScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10), + textLabelScrollView.topAnchor + .constraint(equalTo: view.topAnchor, constant: 10), + textLabelScrollView.bottomAnchor + .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() { - 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.toolTip = "Reload Current" } private func startReloadTextTimer() { - timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global()) + timer = + DispatchSource.makeTimerSource(queue: DispatchQueue.global()) timer?.schedule(deadline: .now(), repeating: .milliseconds(500)) timer?.setEventHandler { [weak self] in 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.progressView.value = Double(file.untilNextExec) } @@ -342,45 +470,55 @@ class CmdViewController: NSViewController { } private func setupKeyEvents() { - keyboardEvents = LocalEventMonitor(mask: [.flagsChanged, .keyDown], handler: { [weak self] event in - let modifiers = event.modifierFlags.rawValue - let key = event.keyCode + keyboardEvents = LocalEventMonitor(mask: [.flagsChanged, .keyDown], + handler: { [weak self] event in + let modifiers = event.modifierFlags.rawValue + let key = event.keyCode - if modsContains(keys: OSShift, in: modifiers) { - self?.reloadButton.image = systemImage("arrow.clockwise.circle.fill", .title2, .large,.init(paletteColors: [.white, .systemCyan])) - self?.reloadButton.action = #selector(self?.reloadWidgets) - 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: OSShift, in: modifiers) { + self?.reloadButton.image = + systemImage("arrow.clockwise.circle.fill", .title2, + .large, + .init(paletteColors: [.white, .systemCyan])) + self?.reloadButton.action = + #selector(self?.reloadWidgets) + 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 key == kVK_ANSI_Q { - self?.terminateApp() - } else if key == kVK_ANSI_W { - self?.view.superview?.window?.resignKey() - } else if key == kVK_ANSI_R { - self?.reloadWidget() - } else if key == kVK_ANSI_C { - self?.textLabel.currentEditor()?.copy(nil) + if modsContains(keys: OSCmd, in: modifiers) { + if key == kVK_ANSI_Q { + self?.terminateApp() + } else if key == kVK_ANSI_W { + self?.view.superview?.window?.resignKey() + } else if key == kVK_ANSI_R { + self?.reloadWidget() + } else if key == kVK_ANSI_C { + 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) { @@ -438,10 +576,14 @@ class CmdViewController: NSViewController { private func openAtLoginToggle() { if SMAppService.mainApp.status == .enabled { 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 { 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])) } } diff --git a/src/EventMonitor.swift b/src/EventMonitor.swift index 277dd41..fce82e2 100644 --- a/src/EventMonitor.swift +++ b/src/EventMonitor.swift @@ -35,7 +35,8 @@ final class LocalEventMonitor: EventMonitor { } 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() { - monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) + monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, + handler: handler) } } diff --git a/src/Helpers.swift b/src/Helpers.swift index 9d1a5d6..709ba2a 100644 --- a/src/Helpers.swift +++ b/src/Helpers.swift @@ -72,7 +72,8 @@ func intervalToSeconds(from timeString: String) -> Int { // Regular expression pattern to match "1s", "5m", "1h", "1d" func isValidTimeFormat(_ timeString: String) -> Bool { 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. @@ -104,7 +105,8 @@ func execute(atPath path: URL) -> (title: String, body: String) { if !output.isEmpty { let result = output.components(separatedBy: "\n") if result.count >= 1 { - return (result.first!, result.dropFirst().joined(separator: "\n")) + return (result.first!, + result.dropFirst().joined(separator: "\n")) } } } catch ExecError.failed { @@ -124,8 +126,10 @@ func execute(atPath path: URL) -> (title: String, body: String) { // Executes a file. private func executeFile(path: URL) throws -> String { - if !FileManager.default.fileExists(atPath: path.path()) { throw ExecError.doesntExist } - if !FileManager.default.isExecutableFile(atPath: path.path()) { throw ExecError.noPermission } + if !FileManager.default.fileExists(atPath: path.path()) + { throw ExecError.doesntExist } + if !FileManager.default.isExecutableFile(atPath: path.path()) + { throw ExecError.noPermission } let process = Process() process.executableURL = path @@ -164,21 +168,33 @@ private func executeFile(path: URL) throws -> String { return "" } -func getColors(_ light: NSColor, _ dark: NSColor, for appearance: NSAppearance) -> NSColor { +func getColors(_ light: NSColor, _ dark: NSColor, + for appearance: NSAppearance) -> NSColor +{ switch appearance.name { - case .aqua, .vibrantLight, .accessibilityHighContrastAqua, .accessibilityHighContrastVibrantLight: + case .aqua, .vibrantLight, .accessibilityHighContrastAqua, + .accessibilityHighContrastVibrantLight: return light - case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark: + case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, + .accessibilityHighContrastVibrantDark: return dark default: return NSColor.white } } -// ex: systemImage("sunrise.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemOrange])) -func systemImage(_ name: String, _ size: NSFont.TextStyle, _ scale: NSImage.SymbolScale, _ configuration: NSImage.SymbolConfiguration) -> NSImage? { +// ex: systemImage("sunrise.circle.fill", .title2, .large, +// .init(paletteColors: [.white, .systemOrange])) +func systemImage(_ name: String, _ size: NSFont.TextStyle, + _ scale: NSImage.SymbolScale, + _ configuration: NSImage.SymbolConfiguration) -> NSImage? +{ 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) { @@ -187,14 +203,18 @@ func openLink(_ url: String) { } fileprivate extension Notification.Name { - static let beginMenuTracking = Notification.Name("com.apple.HIToolbox.beginMenuTrackingNotification") - static let endMenuTracking = Notification.Name("com.apple.HIToolbox.endMenuTrackingNotification") + static let beginMenuTracking = + Notification.Name("com.apple.HIToolbox.beginMenuTrackingNotification") + static let endMenuTracking = + Notification.Name("com.apple.HIToolbox.endMenuTrackingNotification") } func persistMenuBar(_ state: Bool) { if state { - DistributedNotificationCenter.default().post(name: .beginMenuTracking, object: nil) + DistributedNotificationCenter.default() + .post(name: .beginMenuTracking, object: nil) } else { - DistributedNotificationCenter.default().post(name: .endMenuTracking, object: nil) + DistributedNotificationCenter.default() + .post(name: .endMenuTracking, object: nil) } } diff --git a/src/Makefile b/src/Makefile index 7708e24..86398ae 100644 --- a/src/Makefile +++ b/src/Makefile @@ -7,9 +7,13 @@ XCODE_PATH = $(shell xcode-select --print-path) EXEC = CmdBar -SRCMODULES = UpdateManager.swift AboutViewController.swift AppDelegate.swift CBMenuBarItem.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 +SRCMODULES = UpdateManager.swift AboutViewController.swift \ + AppDelegate.swift CBMenuBarItem.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)) X86OBJMODULES = $(addprefix ./x86_64/,$(SRCMODULES:.swift=.o)) @@ -19,40 +23,51 @@ FRAMEWORKS = -framework AppKit -framework ServiceManagement LIBS = -lzip 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: @$(MAKE) -C updater FLAGS=$(FLAGS) all @cp updater/$@ . ./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 \ - -primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) -sdk $(SDK) -module-name $(EXEC) \ - -o $@ -emit-module && touch $@ + -primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) \ + -sdk $(SDK) -module-name $(EXEC) -o $@ -emit-module && touch $@ ifdef UNIVERSAL ./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 \ - -primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) -sdk $(SDK) -module-name $(EXEC) \ - -o $@ -emit-module && touch $@ + -primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) \ + -sdk $(SDK) -module-name $(EXEC) -o $@ -emit-module && touch $@ endif ./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 \ - -sectcreate __TEXT __info_plist Info.plist -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \ - /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)) \ + -sectcreate __TEXT __info_plist Info.plist \ + -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \ + /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 $@ ifdef UNIVERSAL ./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 \ - -sectcreate __TEXT __info_plist Info.plist -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \ - /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)) \ + -sectcreate __TEXT __info_plist Info.plist \ + -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \ + /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 $@ endif @@ -72,20 +87,22 @@ $(EXEC).app: $(EXEC) cp resources/AppIcon.icns $@/Contents/Resources/ && \ cp $(EXEC) $@/Contents/MacOS/ && \ cp cmdbar_updater $@/Contents/MacOS/ && \ - $(if $(DEBUG), codesign --entitlements Grapp.entitlements -s ${APPLE_DEVELOPMENT} -f --timestamp -o runtime $(EXEC).app, \ - codesign -s ${APPLE_DEVELOPER_ID_APPLICATION} -f --timestamp -o runtime $(EXEC).app) + $(if $(DEBUG), codesign --entitlements Grapp.entitlements \ + -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 -clear: - clear - kill: -pkill $(EXEC) -run: clear kill all +run: kill all ./$(EXEC) +open: kill all + open $(EXEC).app + clean: rm -rf $(EXEC) $(EXEC).app cmdbar_updater arm64 x86_64 mkdir arm64 x86_64 diff --git a/src/MenulessWindow.swift b/src/MenulessWindow.swift index f45b604..fa50571 100644 --- a/src/MenulessWindow.swift +++ b/src/MenulessWindow.swift @@ -23,7 +23,9 @@ class MenulessWindow: NSWindow { let key = event.keyCode 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) return true } diff --git a/src/PopoverPanel.swift b/src/PopoverPanel.swift index 12d9258..dce4705 100644 --- a/src/PopoverPanel.swift +++ b/src/PopoverPanel.swift @@ -6,7 +6,8 @@ class PopoverPanel: NSPanel { init(viewController: NSViewController) { super.init( contentRect: CGRect(x: 0, y: 0, width: 100, height: 100), - styleMask: [.titled, .nonactivatingPanel, .utilityWindow, .fullSizeContentView], + styleMask: [.titled, .nonactivatingPanel, .utilityWindow, + .fullSizeContentView], backing: .buffered, defer: false ) @@ -22,7 +23,8 @@ class PopoverPanel: NSPanel { titlebarAppearsTransparent = true animationBehavior = .none - collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary, .transient] + collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary, + .transient] isReleasedWhenClosed = false hidesOnDeactivate = false diff --git a/src/UpdateManager.swift b/src/UpdateManager.swift index 0a36304..9f51681 100644 --- a/src/UpdateManager.swift +++ b/src/UpdateManager.swift @@ -19,7 +19,12 @@ final class UpdateManager { private var observation: NSKeyValueObservation? 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) { return version } else @@ -27,7 +32,9 @@ final class UpdateManager { } 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 { let config = URLSessionConfiguration.default config.timeoutIntervalForResource = 5.0 @@ -35,12 +42,15 @@ final class UpdateManager { let (data, _) = try await session.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 } catch { return nil } } - func isUpdateAvailable() async -> (available: Bool?, model: VersionModel?) { + func isUpdateAvailable() async -> (available: Bool?, + model: VersionModel?) + { guard let curVersion = currentVersion(), let fetVersion = await latestVersion() else { return (nil, nil) } @@ -51,10 +61,14 @@ final class UpdateManager { return (false, nil) } - func downloadUpdate(completionHandler: @escaping (_ data: Resulting) -> Void) { - guard let url = URL(string: EndpointConstants.appURLZip) else { completionHandler(.failure); return } + func downloadUpdate( + completionHandler: @escaping (_ data: Resulting) -> 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 let tempURL = tempURL, error == nil, @@ -69,7 +83,10 @@ final class UpdateManager { var userTmpURL = FileManager.default.temporaryDirectory 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") if fileManager.fileExists(atPath: userTmpURL.path()) { do { @@ -88,18 +105,28 @@ final class UpdateManager { return } } - observation = task.progress.observe(\.fractionCompleted, changeHandler: { [weak self] progress, _ in - guard let delegate = self?.delegate else { return } - delegate.downloadProgressChanged(progress.fractionCompleted) - }) + observation = + task.progress.observe(\.fractionCompleted, changeHandler: + { [weak self] progress, _ in + guard let delegate = self?.delegate else { return } + delegate + .downloadProgressChanged(progress.fractionCompleted) + } + ) task.resume() } - func extractUpdate(completionHandler: @escaping (_ data: Resulting) -> Void) { + func extractUpdate( + completionHandler: @escaping (_ data: Resulting) -> Void + ) { let fileManager = FileManager.default - let userTmpURL = fileManager.temporaryDirectory.appending(path: "CmdBar.zip") + let userTmpURL = + fileManager.temporaryDirectory.appending(path: "CmdBar.zip") 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 } delegate.unzipProgressChanged(progress) } @@ -112,13 +139,16 @@ final class UpdateManager { } 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 } try? FileManager.default.removeItem(at: tmp) } func installUpdate() { - if let url = Bundle.main.url(forAuxiliaryExecutable: "cmdbar_updater") { + if let url = + Bundle.main.url(forAuxiliaryExecutable: "cmdbar_updater") + { let task = Process() task.executableURL = url 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) let exists = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) return exists diff --git a/src/UpdateViewController.swift b/src/UpdateViewController.swift index 72ec768..cd50a6a 100644 --- a/src/UpdateViewController.swift +++ b/src/UpdateViewController.swift @@ -17,7 +17,8 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate { private var appIconImage: 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.translatesAutoresizingMaskIntoConstraints = false return image @@ -29,8 +30,12 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate { textField.isBezeled = false textField.drawsBackground = false textField.alignment = .left - textField.textColor = NSColor(name: nil) { getColors(.black, .white, for: $0) } - textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .bold) + textField.textColor = + NSColor(name: nil) { getColors(.black, .white, for: $0) } + textField.font = NSFont + .systemFont(ofSize: NSFontDescriptor + .preferredFontDescriptor(forTextStyle: .body).pointSize, + weight: .bold) textField.translatesAutoresizingMaskIntoConstraints = false return textField }() @@ -44,7 +49,10 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate { textField.drawsBackground = false textField.alignment = .left 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 return textField }() @@ -56,7 +64,10 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate { (scrollableTextView.documentView as? NSTextView)?.isEditable = false (scrollableTextView.documentView as? NSTextView)?.textColor = .gray (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 return scrollableTextView }() @@ -124,26 +135,49 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate { stackView.setHuggingPriority(.required, for: .vertical) NSLayoutConstraint.activate([ - appIconImage.widthAnchor.constraint(equalToConstant: 70), - appIconImage.heightAnchor.constraint(equalTo: appIconImage.widthAnchor,multiplier: 1), - appIconImage.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing10), - appIconImage.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing10), + appIconImage.widthAnchor + .constraint(equalToConstant: 70), + appIconImage.heightAnchor + .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.leadingAnchor.constraint(equalTo: appIconImage.trailingAnchor, constant: ViewConstants.spacing10), + statusLabel.topAnchor + .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.leadingAnchor.constraint(equalTo: statusLabel.leadingAnchor), + subStatusLabel.topAnchor + .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), - stackView.topAnchor.constraint(equalTo: subStatusLabel.bottomAnchor, constant: ViewConstants.spacing2), - stackView.leadingAnchor.constraint(equalTo: statusLabel.leadingAnchor), - view.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: ViewConstants.spacing20), - view.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: ViewConstants.spacing20), + stackView.topAnchor + .constraint(equalTo: subStatusLabel.bottomAnchor, + constant: ViewConstants.spacing2), + 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 break case .checking: - self?.statusLabel.stringValue = UpdateStatus.checking.rawValue + self?.statusLabel.stringValue = + UpdateStatus.checking.rawValue self?.updateButton.keyEquivalent = "" self?.updateButton.isEnabled = false case .available: self?.updateButton.title = "Download" self?.updateButton.keyEquivalent = "" - self?.statusLabel.stringValue = UpdateStatus.available.rawValue + self?.statusLabel.stringValue = + UpdateStatus.available.rawValue self?.updateButton.action = #selector(self!.downloadUpdate) case .downloading: self?.updateButton.isEnabled = false self?.updateButton.title = "Install" self?.progressIndicator.isHidden = false - self?.statusLabel.stringValue = UpdateStatus.downloading.rawValue + self?.statusLabel.stringValue = + UpdateStatus.downloading.rawValue case .extracting: self?.updateButton.isEnabled = false self?.updateButton.title = "Install" self?.progressIndicator.isHidden = false - self?.statusLabel.stringValue = UpdateStatus.extracting.rawValue + self?.statusLabel.stringValue = + UpdateStatus.extracting.rawValue case .install: self?.updateButton.title = "Install" self?.progressIndicator.isHidden = false - self?.statusLabel.stringValue = UpdateStatus.install.rawValue - self?.updateButton.action = #selector(self!.installUpdate) + self?.statusLabel.stringValue = + UpdateStatus.install.rawValue + self?.updateButton.action = + #selector(self!.installUpdate) case .checkFailed: - self?.statusLabel.stringValue = UpdateStatus.checkFailed.rawValue + self?.statusLabel.stringValue = + UpdateStatus.checkFailed.rawValue case .installFailed: - self?.statusLabel.stringValue = UpdateStatus.installFailed.rawValue + self?.statusLabel.stringValue = + UpdateStatus.installFailed.rawValue self?.updateButton.action = #selector(self!.checkForUpdate) } } @@ -227,9 +269,15 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate { self?.subStatusLabel.isHidden = false self?.changelogLabel.isHidden = false 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 { - (self?.changelogLabel.documentView as? NSTextView)?.string += "\n" + ( + self?.changelogLabel.documentView as? + NSTextView + )?.string += "\n" } } } diff --git a/src/updater/main.swift b/src/updater/main.swift index 9ca2db6..2ba708c 100644 --- a/src/updater/main.swift +++ b/src/updater/main.swift @@ -7,13 +7,18 @@ 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) - let exists = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) + let exists = + FileManager.default.fileExists(atPath: path, + isDirectory: &isDirectory) 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 if !existsAtPath(tmp.path(), isDirectory: true) { exit(1) } @@ -28,6 +33,9 @@ do { } 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)