Thou shalt not cross 80 columns in thy file.
This commit is contained in:
@@ -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),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/Makefile
65
src/Makefile
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user