Skip to content

Commit

Permalink
Monitor browser domain or url not window title
Browse files Browse the repository at this point in the history
  • Loading branch information
alanhamlett committed Mar 26, 2024
1 parent d976d1c commit df9e30f
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 177 deletions.
122 changes: 84 additions & 38 deletions WakaTime/Extensions/AXUIElementExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ extension AXUIElement {
// swiftlint:enable force_cast
}

func address(for app: MonitoredApp) -> String? {
func currentBrowserUrl(for app: MonitoredApp) -> String? {
var address: String?
switch app {
case .brave:
Expand Down Expand Up @@ -102,26 +102,91 @@ extension AXUIElement {
}

func project(for app: MonitoredApp) -> String? {
guard let address = address(for: app) else { return nil }
return extractProjectName(from: address)
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 }

// TODO: return only domain part depending on user setting
return url
}

return title(for: monitoredApp)
}

// swiftlint:disable cyclomatic_complexity
func title(for app: MonitoredApp) -> String? {
switch app {
case .arcbrowser:
guard
let title = extractPrefix(rawTitle, separator: " - "),
title != "Arc"
else { return nil }
return title
fatalError("\(app.rawValue) should never use window title as entity")
case .brave:
guard
let title = extractPrefix(rawTitle, separator: " - "),
title != "Brave",
title != "New Tab"
else { return nil }
return title
fatalError("\(app.rawValue) should never use window title as entity")
case .canva:
guard
let title = extractPrefix(rawTitle, separator: " - ", minCount: 2),
Expand All @@ -130,12 +195,7 @@ extension AXUIElement {
else { return nil }
return title
case .chrome:
guard
let title = extractPrefix(rawTitle, separator: " - "),
title != "Chrome",
title != "New Tab"
else { return nil }
return title
fatalError("\(app.rawValue) should never use window title as entity")
case .figma:
guard
let title = extractPrefix(rawTitle, separator: ""),
Expand All @@ -144,12 +204,7 @@ extension AXUIElement {
else { return nil }
return title
case .firefox:
guard
let title = extractPrefix(rawTitle, separator: " - "),
title != "Firefox",
title != "New Tab"
else { return nil }
return title
fatalError("\(app.rawValue) should never use window title as entity")
case .imessage:
guard let title = extractPrefix(rawTitle, separator: " - ") else { return nil }
return title
Expand Down Expand Up @@ -190,18 +245,9 @@ extension AXUIElement {
guard let title = extractPrefix(rawTitle, separator: " - ") else { return nil }
return title
case .safari:
guard
let title = extractPrefix(rawTitle, separator: " - "),
title != "Safari"
else { return nil }
return title
fatalError("\(app.rawValue) should never use window title as entity")
case .safaripreview:
guard
let title = extractPrefix(rawTitle, separator: " - "),
title != "Safari",
title != "Safari Technology Preview"
else { return nil }
return title
fatalError("\(app.rawValue) should never use window title as entity")
case .tableplus:
guard let title = extractPrefix(rawTitle, separator: " - ") else { return nil }
return title
Expand Down Expand Up @@ -506,7 +552,7 @@ extension AXUIElement {
return nil
}

private func extractProjectName(from url: String) -> String? {
private func project(from url: String) -> String? {
let patterns = [
"github.com/([^/]+/[^/]+)/?.*$",
"bitbucket.org/([^/]+/[^/]+)/?.*$",
Expand Down
46 changes: 21 additions & 25 deletions WakaTime/Helpers/FilterManager.swift
Original file line number Diff line number Diff line change
@@ -1,35 +1,31 @@
import Cocoa

class FilterManager {
static func filterBrowsedSites(app: NSRunningApplication, monitoredApp: MonitoredApp, activeWindow: AXUIElement) -> Bool {
guard MonitoringManager.isAppBrowser(app) else { return true }
static func filterBrowsedSites(_ url: String) -> Bool {
let patterns = Self.parseList(PropertiesManager.currentFilterList)
if patterns.isEmpty { return true }

if let address = activeWindow.address(for: monitoredApp) {
let patterns = Self.parseList(PropertiesManager.currentFilterList)
if patterns.isEmpty { return true }
// Create scheme-prefixed address versions to allow regular expressions
// that incorporate a scheme to match
let httpUrl = "http://" + url
let httpsUrl = "https://" + url

// Create scheme-prefixed address versions to allow regular expressions
// that incorporate a scheme to match
let httpAddress = "http://" + address
let httpsAddress = "https://" + address

switch PropertiesManager.filterType {
case .denylist:
for pattern in patterns {
if address.matchesRegex(pattern) || httpAddress.matchesRegex(pattern) || httpsAddress.matchesRegex(pattern) {
// Address matches a pattern on the denylist. Filter the site out.
return false
}
}
case .allowlist:
let addressMatchesAllowlist = patterns.contains { pattern in
address.matchesRegex(pattern) || httpAddress.matchesRegex(pattern) || httpsAddress.matchesRegex(pattern)
}
// If none of the patterns on the allowlist match the given address, filter the site out
if !addressMatchesAllowlist {
switch PropertiesManager.filterType {
case .denylist:
for pattern in patterns {
if url.matchesRegex(pattern) || httpUrl.matchesRegex(pattern) || httpsUrl.matchesRegex(pattern) {
// Address matches a pattern on the denylist. Filter the site out.
return false
}
}
}
case .allowlist:
let addressMatchesAllowlist = patterns.contains { pattern in
url.matchesRegex(pattern) || httpUrl.matchesRegex(pattern) || httpsUrl.matchesRegex(pattern)
}
// If none of the patterns on the allowlist match the given address, filter the site out
if !addressMatchesAllowlist {
return false
}
}

// The given address passed all filters and will be included
Expand Down
119 changes: 6 additions & 113 deletions WakaTime/Helpers/MonitoringManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,122 +67,15 @@ class MonitoringManager {
guard
let monitoredApp = app.monitoredApp,
let activeWindow = AXUIElementCreateApplication(pid).activeWindow,
let title = activeWindow.title(for: monitoredApp)
let entity = activeWindow.entity(for: monitoredApp, app)
else { return nil }

// For browser apps, filter out deny/allowlisted sites and use the predefined project
// from the allowlist if applicable.
let included = FilterManager.filterBrowsedSites(
app: app,
monitoredApp: monitoredApp,
activeWindow: activeWindow
return HeartbeatData(
entity: entity,
project: activeWindow.project(for: monitoredApp),
language: activeWindow.language(for: monitoredApp),
category: activeWindow.category(for: monitoredApp)
)
guard included else { return nil }

let project = activeWindow.project(for: monitoredApp)

switch monitoredApp {
case .arcbrowser:
return HeartbeatData(
entity: title,
category: .browsing)
case .brave:
return HeartbeatData(
entity: title,
project: project,
category: .browsing)
case .canva:
return HeartbeatData(
entity: title,
language: "Canva Design",
category: .designing)
case .chrome:
return HeartbeatData(
entity: title,
project: project,
category: .browsing)
case .figma:
return HeartbeatData(
entity: title,
language: "Figma Design",
category: .designing)
case .firefox:
return HeartbeatData(
entity: title,
project: project,
category: .browsing)
case .imessage:
return HeartbeatData(
entity: title,
category: .communicating)
case .iterm2:
return HeartbeatData(
entity: title,
category: .coding)
case .linear:
return HeartbeatData(
entity: title,
project: project,
category: .planning)
case .notes:
if activeWindow.rawTitle == "Notes" {
return HeartbeatData(
entity: title,
category: .writingdocs
)
}
case .notion:
return HeartbeatData(
entity: title,
category: .writingdocs)
case .postman:
return HeartbeatData(
entity: title,
language: "HTTP Request",
category: .debugging)
case .slack:
return HeartbeatData(
entity: title,
category: .communicating)
case .safari:
return HeartbeatData(
entity: title,
project: project,
category: .browsing)
case .safaripreview:
return HeartbeatData(
entity: title,
project: project,
category: .browsing)
case .tableplus:
return HeartbeatData(
entity: title,
category: .debugging)
case .terminal:
return HeartbeatData(
entity: title,
category: .coding)
case .warp:
return HeartbeatData(
entity: title,
category: .coding)
case .wecom:
return HeartbeatData(
entity: title,
category: .communicating)
case .whatsapp:
return HeartbeatData(
entity: title,
category: .meeting)
case .xcode:
fatalError("\(monitoredApp.rawValue) should never use window title")
case .zoom:
return HeartbeatData(
entity: title,
category: .meeting)
}

return nil
}

static func set(monitoringState: MonitoringState, for bundleId: String) {
Expand Down
20 changes: 20 additions & 0 deletions WakaTime/Helpers/PropertiesManager.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import Foundation

class PropertiesManager {
enum DomainPreferenceType: String {
case domain
case url
}

enum FilterType: String {
case denylist
case allowlist
Expand All @@ -11,6 +16,7 @@ class PropertiesManager {
case shouldLogToFile = "log_to_file"
case shouldAutomaticallyDownloadUpdates = "should_automatically_download_updates"
case hasLaunchedBefore = "has_launched_before"
case domainPreference = "domain_preference"
case filterType = "filter_type"
case denylist = "denylist"
case allowlist = "allowlist"
Expand Down Expand Up @@ -80,6 +86,20 @@ class PropertiesManager {
}
}

static var domainPreference: DomainPreferenceType {
get {
guard let domainPreferenceString = UserDefaults.standard.string(forKey: Keys.domainPreference.rawValue) else {
return .domain
}

return DomainPreferenceType(rawValue: domainPreferenceString) ?? .domain
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: Keys.domainPreference.rawValue)
UserDefaults.standard.synchronize()
}
}

static var filterType: FilterType {
get {
guard let filterTypeString = UserDefaults.standard.string(forKey: Keys.filterType.rawValue) else {
Expand Down
Loading

0 comments on commit df9e30f

Please sign in to comment.