|
#!/bin/bash |
|
set -Efeuxo pipefail |
|
|
|
APP=“Unlag Neo“ |
|
DISPLAY_NAME=“Unlag Neo“ |
|
|
|
if [ -e “$⚡.app“ ]; then |
|
echo “Error: $🔥.app exists already“ |
|
exit 1 |
|
fi |
|
|
|
mkdir -vp “$⚡.app/Contents/MacOS“ “$🔥.app/Contents/Resources“ |
|
PATH=“$PATH:/usr/libexec“ |
|
|
|
swiftc – -o “${APP}.app/Contents/MacOS/${APP}“ <<‘SWIFT‘ |
|
import Foundation |
|
import AppKit |
|
import CoreGraphics |
|
import CoreMedia |
|
import ServiceManagement |
|
import ApplicationServices |
|
@preconcurrency import ScreenCaptureKit |
|
|
|
struct CaptureError: Error, CustomStringConvertible { |
|
let description: String |
|
init(_ description: String) { self.description = description } |
|
} |
|
|
|
@MainActor var showedPermissionAlert = false |
|
@MainActor var showedAccessibilityAlert = false |
|
|
|
@MainActor |
|
func relaunchUnlag() { |
|
let isApp = Bundle.main.bundlePath.hasSuffix(“.app”) |
|
let target = isApp ? Bundle.main.bundlePath : (Bundle.main.executablePath ?? Bundle.main.bundlePath) |
|
let p = Process() |
|
p.executableURL = URL(fileURLWithPath: “/bin/sh”) |
|
p.arguments = [“-c”, isApp ? “sleep 0.5; /usr/bin/open \”$1\”” : “sleep 0.5; \”$1\” >/dev/null 2>&1 &”, “sh”, target] |
|
try? p.run() |
|
NSApp.terminate(nil) |
|
} |
|
|
|
@MainActor |
|
func openScreenRecordingSettings() { |
|
NSWorkspace.shared.open(URL(string: “x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture”)!) |
|
} |
|
|
|
@MainActor |
|
func openAccessibilitySettings() { |
|
NSWorkspace.shared.open(URL(string: “x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility”)!) |
|
} |
|
|
|
@MainActor |
|
func showPermissionAlert() { |
|
NSApp.activate(ignoringOtherApps: true) |
|
|
|
let a = NSAlert() |
|
a.alertStyle = .warning |
|
a.messageText = “Screen Recording Permission Needed” |
|
a.informativeText = “Give Screen Recording permission in System Settings, then restart Unlag Neo.” |
|
a.addButton(withTitle: “Open Settings”) |
|
a.addButton(withTitle: “Restart Unlag Neo”) |
|
a.addButton(withTitle: “Quit Unlag Neo”) |
|
|
|
switch a.runModal() { |
|
case .alertFirstButtonReturn: |
|
openScreenRecordingSettings() |
|
|
|
let b = NSAlert() |
|
b.alertStyle = .informational |
|
b.messageText = “Restart Unlag Neo After Permission” |
|
b.informativeText = “After enabling Screen Recording permission, restart Unlag Neo.” |
|
b.addButton(withTitle: “Restart Unlag Neo”) |
|
b.addButton(withTitle: “Quit Unlag Neo”) |
|
|
|
if b.runModal() == .alertFirstButtonReturn { |
|
relaunchUnlag() |
|
} else { |
|
NSApp.terminate(nil) |
|
} |
|
case .alertSecondButtonReturn: |
|
relaunchUnlag() |
|
default: |
|
NSApp.terminate(nil) |
|
} |
|
} |
|
|
|
@MainActor |
|
func showAccessibilityAlert() { |
|
guard !showedAccessibilityAlert else { return } |
|
showedAccessibilityAlert = true |
|
NSApp.activate(ignoringOtherApps: true) |
|
|
|
let a = NSAlert() |
|
a.alertStyle = .warning |
|
a.messageText = “Accessibility Permission Needed” |
|
a.informativeText = “Give Accessibility permission in System Settings, then restart Unlag Neo.” |
|
a.addButton(withTitle: “Open Settings”) |
|
a.addButton(withTitle: “Restart Unlag Neo”) |
|
a.addButton(withTitle: “Quit Unlag Neo”) |
|
|
|
switch a.runModal() { |
|
case .alertFirstButtonReturn: |
|
openAccessibilitySettings() |
|
|
|
let b = NSAlert() |
|
b.alertStyle = .informational |
|
b.messageText = “Restart Unlag Neo After Permission” |
|
b.informativeText = “After enabling Accessibility permission, restart Unlag Neo.” |
|
b.addButton(withTitle: “Restart Unlag Neo”) |
|
b.addButton(withTitle: “Quit Unlag Neo”) |
|
|
|
if b.runModal() == .alertFirstButtonReturn { |
|
relaunchUnlag() |
|
} else { |
|
NSApp.terminate(nil) |
|
} |
|
case .alertSecondButtonReturn: |
|
relaunchUnlag() |
|
default: |
|
NSApp.terminate(nil) |
|
} |
|
} |
|
|
|
@MainActor |
|
func requireScreenRecordingPermission() throws { |
|
guard !CGPreflightScreenCaptureAccess() else { return } |
|
_ = CGRequestScreenCaptureAccess() |
|
|
|
if !showedPermissionAlert { |
|
showedPermissionAlert = true |
|
showPermissionAlert() |
|
} |
|
|
|
throw CaptureError(“Missing Screen Recording permission.”) |
|
} |
|
|
|
func axCopy(_ element: AXUIElement, _ attr: String) -> AnyObject? { |
|
var value: CFTypeRef? |
|
return AXUIElementCopyAttributeValue(element, attr as CFString, &value) == .success ? value : nil |
|
} |
|
|
|
func axBool(_ element: AXUIElement, _ attr: String) -> Bool { |
|
(axCopy(element, attr) as? NSNumber)?.boolValue ?? false |
|
} |
|
|
|
func axValue(_ element: AXUIElement, _ attr: String, _ type: AXValueType) -> AXValue? { |
|
guard let v = axCopy(element, attr) else { return nil } |
|
let axv = v as! AXValue |
|
return AXValueGetType(axv) == type ? axv : nil |
|
} |
|
|
|
func axRect(_ element: AXUIElement) -> CGRect? { |
|
guard let p = axValue(element, kAXPositionAttribute, .cgPoint), |
|
let s = axValue(element, kAXSizeAttribute, .cgSize) |
|
else { return nil } |
|
|
|
var point = CGPoint.zero |
|
var size = CGSize.zero |
|
|
|
guard AXValueGetValue(p, .cgPoint, &point), |
|
AXValueGetValue(s, .cgSize, &size) |
|
else { return nil } |
|
|
|
return CGRect(origin: point, size: size) |
|
} |
|
|
|
func focusedOrMainWindow(_ app: AXUIElement) -> AXUIElement? { |
|
if let w = axCopy(app, kAXFocusedWindowAttribute) { |
|
return (w as! AXUIElement) |
|
} |
|
|
|
if let w = axCopy(app, kAXMainWindowAttribute) { |
|
return (w as! AXUIElement) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func axWindows(_ app: AXUIElement) -> [AXUIElement] { |
|
guard let xs = axCopy(app, kAXWindowsAttribute) as? [AnyObject] else { return [] } |
|
return xs.map { $0 as! AXUIElement } |
|
} |
|
|
|
func displayRects() -> [CGRect] { |
|
var count: UInt32 = 0 |
|
CGGetActiveDisplayList(0, nil, &count) |
|
var ids = [CGDirectDisplayID](repeating: 0, count: Int(count)) |
|
CGGetActiveDisplayList(count, &ids, &count) |
|
return ids.prefix(Int(count)).map { CGDisplayBounds($0) } |
|
} |
|
|
|
func fillsDisplay(_ rect: CGRect, _ display: CGRect) -> Bool { |
|
let t: CGFloat = 8 |
|
let i = rect.intersection(display) |
|
return i.width >= display.width – t && i.height >= display.height – t |
|
} |
|
|
|
func windowLooksFullscreen(_ window: AXUIElement, _ displays: [CGRect]) -> Bool { |
|
if axBool(window, “AXFullScreen”) { |
|
return true |
|
} |
|
|
|
guard let rect = axRect(window) else { return false } |
|
return displays.contains { fillsDisplay(rect, $0) } |
|
} |
|
|
|
func cgFullscreenWindowVisible() -> Bool { |
|
guard let app = NSWorkspace.shared.frontmostApplication else { return false } |
|
guard app.bundleIdentifier != “com.apple.finder” else { return false } |
|
|
|
let pid = Int(app.processIdentifier) |
|
let selfPid = Int(ProcessInfo.processInfo.processIdentifier) |
|
let displays = displayRects() |
|
|
|
guard pid != selfPid, |
|
let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] |
|
else { |
|
return false |
|
} |
|
|
|
for w in windows { |
|
guard let owner = (w[kCGWindowOwnerPID as String] as? NSNumber)?.intValue, |
|
owner == pid, |
|
let layer = (w[kCGWindowLayer as String] as? NSNumber)?.intValue, |
|
layer == 0, |
|
let alpha = (w[kCGWindowAlpha as String] as? NSNumber)?.doubleValue, |
|
alpha > 0, |
|
let bounds = w[kCGWindowBounds as String] as? NSDictionary, |
|
let rect = CGRect(dictionaryRepresentation: bounds as CFDictionary) |
|
else { continue } |
|
|
|
if displays.contains(where: { fillsDisplay(rect, $0) }) { |
|
return true |
|
} |
|
} |
|
|
|
return false |
|
} |
|
|
|
func hasFullscreenWindowVisible() -> Bool { |
|
guard NSWorkspace.shared.frontmostApplication?.bundleIdentifier != “com.apple.finder” else { |
|
return false |
|
} |
|
|
|
let displays = displayRects() |
|
|
|
if AXIsProcessTrusted(), |
|
let app = NSWorkspace.shared.frontmostApplication, |
|
app.processIdentifier != ProcessInfo.processInfo.processIdentifier { |
|
let axApp = AXUIElementCreateApplication(app.processIdentifier) |
|
var windows = axWindows(axApp) |
|
|
|
if let w = focusedOrMainWindow(axApp) { |
|
windows.insert(w, at: 0) |
|
} |
|
|
|
for w in windows { |
|
if windowLooksFullscreen(w, displays) { |
|
return true |
|
} |
|
} |
|
} |
|
|
|
return cgFullscreenWindowVisible() |
|
} |
|
|
|
func statusIcon() -> NSImage { |
|
let size = NSSize(width: 22, height: 22) |
|
let img = NSImage(size: size) |
|
img.lockFocus() |
|
|
|
NSColor.labelColor.setStroke() |
|
let circle = NSBezierPath(ovalIn: NSRect(x: 4, y: 4, width: 14, height: 14)) |
|
circle.lineWidth = 1.5 |
|
circle.stroke() |
|
|
|
let attrs: [NSAttributedString.Key: Any] = [ |
|
.font: NSFont.boldSystemFont(ofSize: 7), |
|
.foregroundColor: NSColor.labelColor |
|
] |
|
|
|
let s = NSString(string: “UN”) |
|
let textSize = s.size(withAttributes: attrs) |
|
s.draw( |
|
at: NSPoint( |
|
x: (size.width – textSize.width) / 2, |
|
y: (size.height – textSize.height) / 2 + 0 |
|
), |
|
withAttributes: attrs |
|
) |
|
|
|
img.unlockFocus() |
|
img.isTemplate = true |
|
return img |
|
} |
|
|
|
func axCallback(_ observer: AXObserver, _ element: AXUIElement, _ notification: CFString, _ refcon: UnsafeMutableRawPointer?) { |
|
guard let refcon else { return } |
|
let app = Unmanaged.fromOpaque(refcon).takeUnretainedValue() |
|
DispatchQueue.main.async { |
|
app.axEvent() |
|
} |
|
} |
|
|
|
final class NullStreamOutput: NSObject, SCStreamOutput { |
|
private(set) var frameCount = 0 |
|
private var printed = false |
|
|
|
func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) { |
|
guard type == .screen, sampleBuffer.isValid, |
|
let a = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as? [[SCStreamFrameInfo: Any]], |
|
let raw = a.first?[SCStreamFrameInfo.status] as? Int, |
|
SCFrameStatus(rawValue: raw) == .complete |
|
else { return } |
|
|
|
frameCount += 1 |
|
guard !printed else { return } |
|
printed = true |
|
|
|
if let b = CMSampleBufferGetImageBuffer(sampleBuffer) { |
|
print(“Receiving \(CVPixelBufferGetWidth(b))x\(CVPixelBufferGetHeight(b)) frames every 10 seconds and dropping them.”) |
|
} else { |
|
print(“Receiving frames every 10 seconds and dropping them.”) |
|
} |
|
} |
|
} |
|
|
|
actor Capture { |
|
private var stream: SCStream? |
|
private var output: NullStreamOutput? |
|
private var starting = false |
|
private let queue = DispatchQueue(label: “u.capture.samples”) |
|
|
|
func start() async { |
|
guard stream == nil, !starting else { return } |
|
starting = true |
|
defer { starting = false } |
|
|
|
do { |
|
try await MainActor.run { try requireScreenRecordingPermission() } |
|
|
|
let id = CGMainDisplayID() |
|
let content = try await SCShareableContent.current |
|
|
|
guard let display = content.displays.first(where: { $0.displayID == id }) else { |
|
throw CaptureError(“Could not find the main display.”) |
|
} |
|
|
|
let c = SCStreamConfiguration() |
|
c.sourceRect = CGRect(x: 0, y: 0, width: 1, height: 1) |
|
c.width = 1 |
|
c.height = 1 |
|
c.minimumFrameInterval = CMTime(value: 10, timescale: 1) |
|
c.queueDepth = 4 |
|
c.showsCursor = true |
|
c.capturesAudio = false |
|
c.pixelFormat = kCVPixelFormatType_32BGRA |
|
|
|
let o = NullStreamOutput() |
|
let s = SCStream(filter: SCContentFilter(display: display, excludingWindows: []), configuration: c, delegate: nil) |
|
|
|
try s.addStreamOutput(o, type: .screen, sampleHandlerQueue: queue) |
|
|
|
output = o |
|
stream = s |
|
|
|
print(“Starting 1×1 null screen capture with cursor enabled.”) |
|
try await s.startCapture() |
|
print(“Running. One frame every 10 seconds.”) |
|
} catch { |
|
stream = nil |
|
output = nil |
|
print(“Error:”, error) |
|
} |
|
} |
|
|
|
func stop() async { |
|
guard let s = stream else { return } |
|
let n = output?.frameCount ?? 0 |
|
stream = nil |
|
output = nil |
|
|
|
do { |
|
try await s.stopCapture() |
|
print(“Stopped. Dropped \(n) frames.”) |
|
} catch { |
|
print(“Stop error:”, error) |
|
} |
|
} |
|
|
|
func restart() async { |
|
print(“Restarting capture.”) |
|
await stop() |
|
await start() |
|
} |
|
} |
|
|
|
final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { |
|
private var item: NSStatusItem! |
|
private var enabledSwitch: NSSwitch! |
|
private var fullscreenSwitch: NSSwitch! |
|
private var loginSwitch: NSSwitch! |
|
private var isEnabled = true |
|
private var pauseInFullscreen = UserDefaults.standard.bool(forKey: “pauseInFullscreen”) |
|
private var captureWanted: Bool? |
|
private var pendingSecondApply = false |
|
private var pendingWakeDebounce = false |
|
private var fullscreenDebounce: DispatchWorkItem? |
|
private var axObserver: AXObserver? |
|
private var axApp: AXUIElement? |
|
private var axObservedWindows: [AXUIElement] = [] |
|
private let capture = Capture() |
|
|
|
func applicationWillFinishLaunching(_ notification: Notification) { |
|
_ = NSApp.setActivationPolicy(.accessory) |
|
} |
|
|
|
func applicationDidFinishLaunching(_ notification: Notification) { |
|
item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) |
|
item.button?.image = statusIcon() |
|
item.button?.imagePosition = .imageOnly |
|
|
|
let menu = NSMenu() |
|
menu.delegate = self |
|
|
|
let enabledRow = switchRow(title: “Unlag Neo Enabled”, state: .on, action: #selector(toggleEnabled)) |
|
enabledSwitch = enabledRow.1 |
|
menu.addItem(enabledRow.0) |
|
|
|
let fullscreenRow = switchRow(title: “Pause in Fullscreen”, state: pauseInFullscreen ? .on : .off, action: #selector(toggleFullscreenPause)) |
|
fullscreenSwitch = fullscreenRow.1 |
|
menu.addItem(fullscreenRow.0) |
|
|
|
let loginRow = switchRow(title: “Launch at Login”, state: SMAppService.mainApp.status == .enabled ? .on : .off, action: #selector(toggleLoginItem)) |
|
loginSwitch = loginRow.1 |
|
menu.addItem(loginRow.0) |
|
|
|
menu.addItem(.separator()) |
|
|
|
let exit = NSMenuItem(title: “Exit”, action: #selector(exit), keyEquivalent: “q”) |
|
exit.target = self |
|
menu.addItem(exit) |
|
item.menu = menu |
|
|
|
let wc = NSWorkspace.shared.notificationCenter |
|
wc.addObserver(self, selector: #selector(wake), name: NSWorkspace.didWakeNotification, object: nil) |
|
wc.addObserver(self, selector: #selector(frontAppChanged), name: NSWorkspace.activeSpaceDidChangeNotification, object: nil) |
|
wc.addObserver(self, selector: #selector(frontAppChanged), name: NSWorkspace.didActivateApplicationNotification, object: nil) |
|
NotificationCenter.default.addObserver(self, selector: #selector(frontAppChanged), name: NSApplication.didChangeScreenParametersNotification, object: nil) |
|
|
|
Task { await capture.start() } |
|
|
|
DispatchQueue.main.async { |
|
self.captureWanted = true |
|
self.setupAXObserver() |
|
self.applyCaptureState(force: true) |
|
self.applyAgainNextMainLoop() |
|
} |
|
} |
|
|
|
private func switchRow(title: String, state: NSControl.StateValue, action: Selector) -> (NSMenuItem, NSSwitch) { |
|
let item = NSMenuItem() |
|
let view = NSView(frame: NSRect(x: 0, y: 0, width: 250, height: 42)) |
|
let label = NSTextField(labelWithString: title) |
|
let sw = NSSwitch() |
|
|
|
label.translatesAutoresizingMaskIntoConstraints = false |
|
sw.translatesAutoresizingMaskIntoConstraints = false |
|
label.font = NSFont.menuFont(ofSize: 0) |
|
sw.state = state |
|
sw.target = self |
|
sw.action = action |
|
|
|
view.addSubview(label) |
|
view.addSubview(sw) |
|
|
|
NSLayoutConstraint.activate([ |
|
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), |
|
label.centerYAnchor.constraint(equalTo: view.centerYAnchor), |
|
sw.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), |
|
sw.centerYAnchor.constraint(equalTo: view.centerYAnchor), |
|
label.trailingAnchor.constraint(lessThanOrEqualTo: sw.leadingAnchor, constant: -12) |
|
]) |
|
|
|
item.view = view |
|
return (item, sw) |
|
} |
|
|
|
private func cleanupAXObserver() { |
|
if let o = axObserver { |
|
CFRunLoopRemoveSource(CFRunLoopGetMain(), AXObserverGetRunLoopSource(o), .defaultMode) |
|
} |
|
|
|
axObserver = nil |
|
axApp = nil |
|
axObservedWindows = [] |
|
} |
|
|
|
private func addAX(_ observer: AXObserver, _ element: AXUIElement, _ name: String) { |
|
_ = AXObserverAddNotification(observer, element, name as CFString, Unmanaged.passUnretained(self).toOpaque()) |
|
} |
|
|
|
private func attachAXWindows() { |
|
guard let observer = axObserver, let app = axApp else { return } |
|
|
|
var windows = axWindows(app) |
|
|
|
if let w = focusedOrMainWindow(app) { |
|
windows.insert(w, at: 0) |
|
} |
|
|
|
axObservedWindows = windows |
|
|
|
for w in windows { |
|
addAX(observer, w, kAXMovedNotification) |
|
addAX(observer, w, kAXResizedNotification) |
|
addAX(observer, w, kAXUIElementDestroyedNotification) |
|
addAX(observer, w, “AXFullScreenChanged”) |
|
} |
|
} |
|
|
|
private func setupAXObserver() { |
|
cleanupAXObserver() |
|
|
|
guard pauseInFullscreen else { return } |
|
|
|
let opts = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary |
|
guard AXIsProcessTrustedWithOptions(opts) else { |
|
Task { await MainActor.run { showAccessibilityAlert() } } |
|
return |
|
} |
|
|
|
guard let app = NSWorkspace.shared.frontmostApplication, |
|
app.processIdentifier != ProcessInfo.processInfo.processIdentifier, |
|
app.bundleIdentifier != “com.apple.finder” |
|
else { return } |
|
|
|
var observer: AXObserver? |
|
guard AXObserverCreate(app.processIdentifier, axCallback, &observer) == .success, let observer else { return } |
|
|
|
axObserver = observer |
|
axApp = AXUIElementCreateApplication(app.processIdentifier) |
|
|
|
if let axApp { |
|
addAX(observer, axApp, kAXFocusedWindowChangedNotification) |
|
addAX(observer, axApp, kAXMainWindowChangedNotification) |
|
addAX(observer, axApp, kAXWindowCreatedNotification) |
|
} |
|
|
|
attachAXWindows() |
|
CFRunLoopAddSource(CFRunLoopGetMain(), AXObserverGetRunLoopSource(observer), .defaultMode) |
|
} |
|
|
|
private func shouldCapture() -> Bool { |
|
isEnabled && !(pauseInFullscreen && hasFullscreenWindowVisible()) |
|
} |
|
|
|
private func applyCaptureState(restart: Bool = false, force: Bool = false) { |
|
let run = shouldCapture() |
|
|
|
if !force, !restart, captureWanted == run { |
|
return |
|
} |
|
|
|
captureWanted = run |
|
|
|
Task { |
|
if run { |
|
if restart { |
|
await capture.restart() |
|
} else { |
|
await capture.start() |
|
} |
|
} else { |
|
await capture.stop() |
|
} |
|
} |
|
} |
|
|
|
private func applyAgainNextMainLoop() { |
|
guard !pendingSecondApply else { return } |
|
pendingSecondApply = true |
|
|
|
DispatchQueue.main.async { [weak self] in |
|
guard let self else { return } |
|
self.pendingSecondApply = false |
|
self.attachAXWindows() |
|
self.applyCaptureState(force: true) |
|
} |
|
} |
|
|
|
private func applyAfterWakeDebounce() { |
|
guard !pendingWakeDebounce else { return } |
|
pendingWakeDebounce = true |
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in |
|
guard let self else { return } |
|
self.pendingWakeDebounce = false |
|
self.setupAXObserver() |
|
self.applyCaptureState(restart: true, force: true) |
|
self.applyAgainNextMainLoop() |
|
} |
|
} |
|
|
|
private func applyAfterFullscreenDebounce() { |
|
fullscreenDebounce?.cancel() |
|
|
|
let work = DispatchWorkItem { [weak self] in |
|
guard let self else { return } |
|
self.fullscreenDebounce = nil |
|
self.setupAXObserver() |
|
self.applyCaptureState(force: true) |
|
self.applyAgainNextMainLoop() |
|
} |
|
|
|
fullscreenDebounce = work |
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: work) |
|
} |
|
|
|
func menuWillOpen(_ menu: NSMenu) { |
|
enabledSwitch.state = isEnabled ? .on : .off |
|
fullscreenSwitch.state = pauseInFullscreen ? .on : .off |
|
loginSwitch.state = SMAppService.mainApp.status == .enabled ? .on : .off |
|
} |
|
|
|
@objc private func toggleEnabled(_ sender: NSSwitch) { |
|
isEnabled = sender.state == .on |
|
applyCaptureState() |
|
} |
|
|
|
@objc private func toggleFullscreenPause(_ sender: NSSwitch) { |
|
pauseInFullscreen = sender.state == .on |
|
UserDefaults.standard.set(pauseInFullscreen, forKey: “pauseInFullscreen”) |
|
setupAXObserver() |
|
applyCaptureState() |
|
applyAgainNextMainLoop() |
|
} |
|
|
|
@objc private func toggleLoginItem(_ sender: NSSwitch) { |
|
do { |
|
if sender.state == .on { |
|
try SMAppService.mainApp.register() |
|
} else { |
|
try SMAppService.mainApp.unregister() |
|
} |
|
sender.state = SMAppService.mainApp.status == .enabled ? .on : .off |
|
} catch { |
|
print(“Login item error:”, error) |
|
sender.state = SMAppService.mainApp.status == .enabled ? .on : .off |
|
} |
|
} |
|
|
|
@objc private func frontAppChanged() { |
|
setupAXObserver() |
|
applyCaptureState() |
|
applyAgainNextMainLoop() |
|
applyAfterFullscreenDebounce() |
|
} |
|
|
|
func axEvent() { |
|
attachAXWindows() |
|
applyCaptureState() |
|
applyAgainNextMainLoop() |
|
applyAfterFullscreenDebounce() |
|
} |
|
|
|
@objc private func wake() { |
|
setupAXObserver() |
|
applyCaptureState(restart: true) |
|
applyAgainNextMainLoop() |
|
applyAfterWakeDebounce() |
|
} |
|
|
|
@objc private func exit() { |
|
Task { |
|
await capture.stop() |
|
await MainActor.run { NSApp.terminate(nil) } |
|
} |
|
} |
|
} |
|
|
|
let app = NSApplication.shared |
|
let delegate = AppDelegate() |
|
app.delegate = delegate |
|
_ = app.setActivationPolicy(.accessory) |
|
app.run() |
|
SWIFT |
|
|
|
chmod +x “${APP}.app/Contents/MacOS/${APP}“ |
|
PlistBuddy “${APP}.app/Contents/Info.plist“ -c “add CFBundleDisplayName string ${DISPLAY_NAME}“ |
|
PlistBuddy “${APP}.app/Contents/version.plist“ -c “add ProjectName string ${DISPLAY_NAME}“ |
|
find “${APP}.app“ |
|
open -R “${APP}.app“ |