Skip to content

Commit

Permalink
Merge pull request #252 from wakatime/main
Browse files Browse the repository at this point in the history
Release v5.14.1
  • Loading branch information
alanhamlett authored Mar 27, 2024
2 parents 955c705 + fdf2747 commit 9ab67a4
Show file tree
Hide file tree
Showing 8 changed files with 384 additions and 318 deletions.
1 change: 1 addition & 0 deletions WakaTime/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, StatusBarDelegate {

private func showSettings() {
NSApp.activate(ignoringOtherApps: true)
settingsWindowController.settingsView.setBrowserVisibility()
settingsWindowController.showWindow(self)
}

Expand Down
281 changes: 38 additions & 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 @@ -493,6 +284,43 @@ extension AXUIElement {
}
}

func elementAtPosition(x: Float, y: Float) -> AXUIElement? {
var element: AXUIElement?
AXUIElementCopyElementAtPosition(self, x, y, &element)
return element
}

func elementAtPositionRelativeToWindow(x: CGFloat, y: CGFloat) -> AXUIElement? {
// swiftlint:disable force_unwrapping
let windowPositionData = getValue(for: kAXPositionAttribute)!
let windowSizeData = getValue(for: kAXSizeAttribute)!
// swiftlint:enable force_unwrapping

var windowPosition = CGPoint()
var windowSize = CGSize()

// swiftlint:disable force_cast
if !AXValueGetValue(windowPositionData as! AXValue, .cgPoint, &windowPosition) ||
!AXValueGetValue(windowSizeData as! AXValue, .cgSize, &windowSize) {
return nil
}
// swiftlint:enable force_cast

let globalX = windowPosition.x + x
let globalY = windowPosition.y + y

if globalX < windowPosition.x || globalX > windowPosition.x + windowSize.width ||
globalY < windowPosition.y || globalY > windowPosition.y + windowSize.height {
// Point is outside the window bounds
return nil
}

var element: AXUIElement?
let systemWideElement = AXUIElementCreateSystemWide()
AXUIElementCopyElementAtPosition(systemWideElement, Float(globalX), Float(globalY), &element)
return element
}

func debugPrintSubtree(element: AXUIElement? = nil, depth: Int = 0, highlight indexPath: [Int] = [], currentPath: [Int] = []) {
let element = element ?? self
if let children = element.children {
Expand Down Expand Up @@ -542,7 +370,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 +387,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
Loading

0 comments on commit 9ab67a4

Please sign in to comment.