Skip to content

Commit

Permalink
Merge pull request #249 from wakatime/misc/refactor-monitored-apps
Browse files Browse the repository at this point in the history
Refactor monitored apps and element extension
  • Loading branch information
alanhamlett authored Mar 27, 2024
2 parents 4e6ce58 + 921476e commit 7e7879d
Show file tree
Hide file tree
Showing 6 changed files with 309 additions and 316 deletions.
244 changes: 1 addition & 243 deletions WakaTime/Extensions/AXUIElementExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,208 +74,6 @@ extension AXUIElement {
// swiftlint:enable force_cast
}

func currentBrowserUrl(for app: MonitoredApp) -> String? {
var address: String?
switch app {
case .brave:
let addressField = findAddressField()
address = addressField?.value
case .chrome:
let addressField = findAddressField()
address = addressField?.value
case .firefox:
let addressField = findAddressField()
address = addressField?.value
case .linear:
let projectLabel = firstDescendantWhere { $0.value == "Project" }
let projectButton = projectLabel?.nextSibling?.firstDescendantWhere { $0.role == kAXButtonRole }
return projectButton?.rawTitle
case .safari:
let addressField = elementById(identifier: "WEB_BROWSER_ADDRESS_AND_SEARCH_FIELD")
address = addressField?.value
case .safaripreview:
let addressField = elementById(identifier: "WEB_BROWSER_ADDRESS_AND_SEARCH_FIELD")
address = addressField?.value
default: return nil
}
return address
}

func project(for app: MonitoredApp) -> String? {
guard let url = currentBrowserUrl(for: app) else { return nil }
return project(from: url)
}

func category(for app: MonitoredApp) -> Category? {
switch app {
case .arcbrowser:
return .browsing
case .brave:
return .browsing
case .canva:
return .designing
case .chrome:
return .browsing
case .figma:
return .designing
case .firefox:
return .browsing
case .imessage:
return .communicating
case .iterm2:
return .coding
case .linear:
return .planning
case .notes:
return .writingdocs
case .notion:
return .writingdocs
case .postman:
return .debugging
case .slack:
return .communicating
case .safari:
return .browsing
case .safaripreview:
return .browsing
case .tableplus:
return .debugging
case .terminal:
return .coding
case .warp:
return .coding
case .wecom:
return .communicating
case .whatsapp:
return .meeting
case .xcode:
fatalError("\(app.rawValue) should never use window title")
case .zoom:
return .meeting
}
}

func language(for app: MonitoredApp) -> String? {
switch app {
case .figma:
return "Figma Design"
case .postman:
return "HTTP Request"
default:
return nil
}
}

func entity(for monitoredApp: MonitoredApp, _ app: NSRunningApplication) -> String? {
if MonitoringManager.isAppBrowser(app) {
guard
let url = currentBrowserUrl(for: monitoredApp),
FilterManager.filterBrowsedSites(url)
else { return nil }

guard PropertiesManager.domainPreference == .domain else { return url }

return domainFromUrl(url)
}

return title(for: monitoredApp)
}

// swiftlint:disable cyclomatic_complexity
func title(for app: MonitoredApp) -> String? {
switch app {
case .arcbrowser:
fatalError("\(app.rawValue) should never use window title as entity")
case .brave:
fatalError("\(app.rawValue) should never use window title as entity")
case .canva:
guard
let title = extractPrefix(rawTitle, separator: " - ", minCount: 2),
title != "Canva",
title != "Home"
else { return nil }
return title
case .chrome:
fatalError("\(app.rawValue) should never use window title as entity")
case .figma:
guard
let title = extractPrefix(rawTitle, separator: ""),
title != "Figma",
title != "Drafts"
else { return nil }
return title
case .firefox:
fatalError("\(app.rawValue) should never use window title as entity")
case .imessage:
guard let title = extractPrefix(rawTitle, separator: " - ") else { return nil }
return title
case .iterm2:
guard let title = extractPrefix(rawTitle, separator: " - ") else { return nil }
return title
case .linear:
guard let title = extractPrefix(rawTitle, separator: " - ") else { return nil }
return title
case .notes:
// There's apparently two text editor implementations in Apple Notes. One uses a web view,
// the other appears to be a native implementation based on the `ICTK2MacTextView` class.
let webAreaElement = firstDescendantWhere { $0.role == "AXWebArea" }
if let webAreaElement {
// WebView-based implementation
let titleElement = webAreaElement.firstDescendantWhere { $0.role == kAXStaticTextRole }
return titleElement?.value
} else {
// ICTK2MacTextView
let textAreaElement = firstDescendantWhere { $0.role == kAXTextAreaRole }
if let value = textAreaElement?.value {
let title = extractPrefix(value, separator: "\n")
return title
}
return nil
}
case .notion:
guard let title = extractPrefix(rawTitle, separator: " - ") else { return nil }
return title
case .postman:
guard
let title = extractPrefix(rawTitle, separator: " - ", fullTitle: true),
title != "Postman"
else { return nil }

return title
case .slack:
guard let title = extractPrefix(rawTitle, separator: " - ") else { return nil }
return title
case .safari:
fatalError("\(app.rawValue) should never use window title as entity")
case .safaripreview:
fatalError("\(app.rawValue) should never use window title as entity")
case .tableplus:
guard let title = extractPrefix(rawTitle, separator: " - ") else { return nil }
return title
case .terminal:
guard let title = extractPrefix(rawTitle, separator: " - ") else { return nil }
return title
case .warp:
guard
let title = extractPrefix(rawTitle, separator: " - "),
title != "Warp"
else { return nil }
return title
case .wecom:
guard let title = extractPrefix(rawTitle, separator: " - ") else { return nil }
return title
case .whatsapp:
guard let title = extractPrefix(rawTitle, separator: " - ") else { return nil }
return title
case .xcode:
fatalError("\(app.rawValue) should never use window title as entity")
case .zoom:
guard let title = extractPrefix(rawTitle, separator: " - ") else { return nil }
return title
}
}
// swiftlint:enable cyclomatic_complexity

var document: String? {
guard let ref = getValue(for: kAXDocumentAttribute) else { return nil }
// swiftlint:disable force_cast
Expand Down Expand Up @@ -321,13 +119,6 @@ extension AXUIElement {
return nil
}

func domainFromUrl(_ url: String) -> String? {
guard let host = URL(stringWithoutScheme: url)?.host else { return nil }
let domain = host.replacingOccurrences(of: "^www.", with: "", options: .regularExpression)
guard let port = URL(stringWithoutScheme: url)?.port else { return domain }
return "\(domain):\(port)"
}

// Traverses the element's subtree (breadth-first) until visitor() returns false or traversal is completed
func traverseDown(visitor: (AXUIElement) -> Bool) {
var queue: [AXUIElement] = [self]
Expand Down Expand Up @@ -542,7 +333,7 @@ extension AXUIElement {
)
}

private func extractPrefix(_ str: String?, separator: String, minCount: Int? = nil, fullTitle: Bool = false) -> String? {
func extractPrefix(_ str: String?, separator: String, minCount: Int? = nil, fullTitle: Bool = false) -> String? {
guard let str = str else { return nil }

let parts = str.components(separatedBy: separator)
Expand All @@ -559,39 +350,6 @@ extension AXUIElement {
}
return nil
}

private func project(from url: String) -> String? {
let patterns = [
"github.com/([^/]+/[^/]+)/?.*$",
"bitbucket.org/([^/]+/[^/]+)/?.*$",
"app.circleci.com/.*/?(github|bitbucket|gitlab)/([^/]+/[^/]+)/?.*$",
"app.travis-ci.com/(github|bitbucket|gitlab)/([^/]+/[^/]+)/?.*$",
"app.travis-ci.org/(github|bitbucket|gitlab)/([^/]+/[^/]+)/?.*$"
]

for pattern in patterns {
do {
let regex = try NSRegularExpression(pattern: pattern)
let nsrange = NSRange(url.startIndex..<url.endIndex, in: url)
if let match = regex.firstMatch(in: url, options: [], range: nsrange) {
// Adjusted to capture the right group based on the pattern.
// The group index might be 2 if the pattern includes a platform prefix before the project name.
let groupIndex = pattern.contains("(github|bitbucket|gitlab)") ? 2 : 1
let range = match.range(at: groupIndex)

if range.location != NSNotFound, let range = Range(range, in: url) {
return String(url[range])
}
}
} catch {
Logging.default.log("Regex error: \(error)")
continue
}
}

// Return nil if no pattern matches
return nil
}
}

enum AXUIElementNotification {
Expand Down
69 changes: 0 additions & 69 deletions WakaTime/Extensions/NSRunningApplicationExtension.swift
Original file line number Diff line number Diff line change
@@ -1,74 +1,5 @@
import Cocoa

enum MonitoredApp: String, CaseIterable {
case arcbrowser = "company.thebrowser.Browser"
case brave = "com.brave.Browser"
case canva = "com.canva.CanvaDesktop"
case chrome = "com.google.Chrome"
case figma = "com.figma.Desktop"
case firefox = "org.mozilla.firefox"
case imessage = "com.apple.MobileSMS"
case iterm2 = "com.googlecode.iterm2"
case linear = "com.linear"
case notes = "com.apple.Notes"
case notion = "notion.id"
case postman = "com.postmanlabs.mac"
case safari = "com.apple.Safari"
case safaripreview = "com.apple.SafariTechnologyPreview"
case slack = "com.tinyspeck.slackmacgap"
case tableplus = "com.tinyapp.TablePlus"
case terminal = "com.apple.Terminal"
case warp = "dev.warp.Warp-Stable"
case wecom = "com.tencent.WeWorkMac"
case whatsapp = "net.whatsapp.WhatsApp"
case xcode = "com.apple.dt.Xcode"
case zoom = "us.zoom.xos"

init?(from bundleId: String) {
if let app = MonitoredApp(rawValue: bundleId) {
self = app
} else if let app = MonitoredApp(rawValue: bundleId.replacingOccurrences(of: "-setapp$", with: "", options: .regularExpression)) {
self = app
} else {
return nil
}
}

static var allBundleIds: [String] {
MonitoredApp.allCases.map { $0.rawValue }
}

static let electronAppIds = [
MonitoredApp.figma.rawValue,
MonitoredApp.slack.rawValue,
]

static let browserAppIds = [
MonitoredApp.arcbrowser.rawValue,
MonitoredApp.brave.rawValue,
MonitoredApp.chrome.rawValue,
MonitoredApp.firefox.rawValue,
MonitoredApp.safari.rawValue,
MonitoredApp.safaripreview.rawValue,
]

// list apps which are enabled by default on first run
static let defaultEnabledApps = [
MonitoredApp.canva.rawValue,
MonitoredApp.figma.rawValue,
MonitoredApp.linear.rawValue,
MonitoredApp.notes.rawValue,
MonitoredApp.notion.rawValue,
MonitoredApp.postman.rawValue,
MonitoredApp.tableplus.rawValue,
MonitoredApp.xcode.rawValue,
MonitoredApp.zoom.rawValue,
]

// list apps which we aren't yet able to track, so they're hidden from the Monitored Apps menu
static let unsupportedAppIds = [String]()
}

extension NSRunningApplication {
var monitoredApp: MonitoredApp? {
guard let bundleId = bundleIdentifier else { return nil }
Expand Down
8 changes: 4 additions & 4 deletions WakaTime/Helpers/MonitoringManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ class MonitoringManager {
guard
let monitoredApp = app.monitoredApp,
let activeWindow = AXUIElementCreateApplication(pid).activeWindow,
let entity = activeWindow.entity(for: monitoredApp, app)
let entity = monitoredApp.entity(for: activeWindow, app)
else { return nil }

return HeartbeatData(
entity: entity,
project: activeWindow.project(for: monitoredApp),
language: activeWindow.language(for: monitoredApp),
category: activeWindow.category(for: monitoredApp)
project: monitoredApp.project(for: activeWindow),
language: monitoredApp.language,
category: monitoredApp.category
)
}

Expand Down
File renamed without changes.
Loading

0 comments on commit 7e7879d

Please sign in to comment.