Skip to content

Commit

Permalink
feat: started work on macOS swift-strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
modderme123 authored and ErikBjare committed Sep 26, 2022
1 parent b445c34 commit fc423c1
Showing 1 changed file with 285 additions and 0 deletions.
285 changes: 285 additions & 0 deletions aw_watcher_window/macos.swift
@@ -0,0 +1,285 @@
import Cocoa
import ScriptingBridge

@objc protocol ChromeTab {
@objc optional var URL: String { get }
@objc optional var title: String { get }
}

@objc protocol ChromeWindow {
@objc optional var activeTab: ChromeTab { get }
@objc optional var mode: String { get }
}

extension SBObject: ChromeWindow, ChromeTab {}

@objc protocol ChromeProtocol {
@objc optional func windows() -> [ChromeWindow]
}

extension SBApplication: ChromeProtocol {}

struct NetworkMessage: Codable {
var app: String
var title: String
var url: String?
}

struct Heartbeat: Codable {
var timestamp: Date
var data: NetworkMessage
}

struct Bucket: Codable {
var client: String
var type: String
var hostname: String
}

let clientHostname = ProcessInfo.processInfo.hostName
let clientName = "aw-watcher-window2"
let bucketName = "\(clientName)_\(clientHostname)"

let main = MainThing()
var oldHeartbeatData: NetworkMessage?

let encoder = JSONEncoder()
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

encoder.dateEncodingStrategy = .custom({ date, encoder in
var container = encoder.singleValueContainer()
let dateString = formatter.string(from: date)
try container.encode(dateString)
})

start()
RunLoop.main.run()

func start() {
guard checkAccess() else {
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
start()
}
return
}

createBucket()

NSWorkspace.shared.notificationCenter.addObserver(
main, selector: #selector(main.focusedAppChanged),
name: NSWorkspace.didActivateApplicationNotification,
object: nil)
main.focusedAppChanged()
detectIdle()
}

func detectIdle() {
let seconds = 15.0 - SystemIdleTime()!
if seconds < 0.0 {
sendHeartbeat(data: NetworkMessage(app: "", title: ""))

var monitor: Any?
monitor = NSEvent.addGlobalMonitorForEvents(matching: [
.mouseMoved, .leftMouseDown, .rightMouseDown, .keyDown,
]) { e in
NSEvent.removeMonitor(monitor!)
if let oldMenu = oldHeartbeatData { sendHeartbeat(data: oldMenu) }
detectIdle()
}

return
}

DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
detectIdle()
}
}

func createBucket() {
let payload = try! encoder.encode(
Bucket(client: clientName, type: "currentwindow", hostname: clientHostname))

let url = URL(string: "http://localhost:5600/api/0/buckets/\(bucketName)")!
Task {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
let (_, response) = try await URLSession.shared.upload(for: urlRequest, from: payload)
guard (200...299).contains((response as! HTTPURLResponse).statusCode) else {
print("Failed to create bucket")
return
}
}
}

func sendHeartbeat(data: NetworkMessage) {
let payload0 = oldHeartbeatData.map {
try! encoder.encode(Heartbeat(timestamp: Date.now, data: $0))
}
let payload = try! encoder.encode(Heartbeat(timestamp: Date.now, data: data))

let url = URL(string: "http://localhost:5600/api/0/buckets/\(bucketName)/heartbeat?pulsetime=999999")!
Task {
if let payload0 = payload0 {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
let (_, response) = try await URLSession.shared.upload(for: urlRequest, from: payload0)
guard (200...299).contains((response as! HTTPURLResponse).statusCode) else {
print("Failed to send window exit heartbeat")
return
}
}

var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
let (_, response) = try await URLSession.shared.upload(for: urlRequest, from: payload)
guard (200...299).contains((response as! HTTPURLResponse).statusCode) else {
print("Failed to send window enter heartbeat")
return
}

}
}

class MainThing {
var observer: AXObserver?
var oldWindow: AXUIElement?
var idle = false

func windowTitleChanged(
_ axObserver: AXObserver,
axElement: AXUIElement,
notification: CFString
) {

let frontmost = NSWorkspace.shared.frontmostApplication!
var windowTitle: AnyObject?
AXUIElementCopyAttributeValue(axElement, kAXTitleAttribute as CFString, &windowTitle)

var data = NetworkMessage(app: frontmost.localizedName!, title: windowTitle as? String ?? "")

if frontmost.localizedName == "Google Chrome" {
let chromeObject: ChromeProtocol = SBApplication.init(bundleIdentifier: "com.google.Chrome")!

let frontWindow = chromeObject.windows!()[0]
let activeTab = frontWindow.activeTab!

if frontWindow.mode == "incognito" {
data = NetworkMessage(app: "", title: "")
} else {
data.url = activeTab.URL
if let title = activeTab.title { data.title = title }
}
}

sendHeartbeat(data: data)
oldHeartbeatData = data
}

@objc func focusedWindowChanged(_ observer: AXObserver, window: AXUIElement) {
if oldWindow != nil {
AXObserverRemoveNotification(
observer, oldWindow!, kAXFocusedWindowChangedNotification as CFString)
}

let selfPtr = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
AXObserverAddNotification(observer, window, kAXTitleChangedNotification as CFString, selfPtr)

windowTitleChanged(
observer, axElement: window, notification: kAXTitleChangedNotification as CFString)

oldWindow = window
}

@objc func focusedAppChanged() {
if observer != nil {
CFRunLoopRemoveSource(
RunLoop.current.getCFRunLoop(),
AXObserverGetRunLoopSource(observer!),
CFRunLoopMode.defaultMode)
}

let frontmost = NSWorkspace.shared.frontmostApplication!
let pid = frontmost.processIdentifier
let focusedApp = AXUIElementCreateApplication(pid)

AXObserverCreate(
pid,
{
(
_ axObserver: AXObserver,
axElement: AXUIElement,
notification: CFString,
userData: UnsafeMutableRawPointer?
) -> Void in
guard let userData = userData else {
print("Missing userData")
return
}
let application = Unmanaged<MainThing>.fromOpaque(userData).takeUnretainedValue()
if notification == kAXFocusedWindowChangedNotification as CFString {
application.focusedWindowChanged(axObserver, window: axElement)
} else {
application.windowTitleChanged(
axObserver, axElement: axElement, notification: notification)
}
}, &observer)

let selfPtr = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
AXObserverAddNotification(
observer!, focusedApp, kAXFocusedWindowChangedNotification as CFString, selfPtr)

CFRunLoopAddSource(
RunLoop.current.getCFRunLoop(),
AXObserverGetRunLoopSource(observer!),
CFRunLoopMode.defaultMode)

var focusedWindow: AnyObject?
AXUIElementCopyAttributeValue(focusedApp, kAXFocusedWindowAttribute as CFString, &focusedWindow)

if focusedWindow != nil {
focusedWindowChanged(observer!, window: focusedWindow as! AXUIElement)
}
}
}

func checkAccess() -> Bool {
let checkOptPrompt = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString
let options = [checkOptPrompt: true]
let accessEnabled = AXIsProcessTrustedWithOptions(options as CFDictionary?)
return accessEnabled
}

func SystemIdleTime() -> Double? {
var iterator: io_iterator_t = 0
defer { IOObjectRelease(iterator) }
guard
IOServiceGetMatchingServices(kIOMainPortDefault, IOServiceMatching("IOHIDSystem"), &iterator)
== KERN_SUCCESS
else {
return nil
}

let entry: io_registry_entry_t = IOIteratorNext(iterator)
defer { IOObjectRelease(entry) }
guard entry != 0 else { return nil }

var unmanagedDict: Unmanaged<CFMutableDictionary>? = nil
defer { unmanagedDict?.release() }
guard
IORegistryEntryCreateCFProperties(entry, &unmanagedDict, kCFAllocatorDefault, 0) == KERN_SUCCESS
else { return nil }
guard let dict = unmanagedDict?.takeUnretainedValue() else { return nil }

let key: CFString = "HIDIdleTime" as CFString
let value = CFDictionaryGetValue(dict, Unmanaged.passUnretained(key).toOpaque())
let number: CFNumber = unsafeBitCast(value, to: CFNumber.self)
var nanoseconds: Int64 = 0
guard CFNumberGetValue(number, CFNumberType.sInt64Type, &nanoseconds) else { return nil }
let interval = Double(nanoseconds) / Double(NSEC_PER_SEC)

return interval
}

0 comments on commit fc423c1

Please sign in to comment.