Skip to content

Commit

Permalink
Merge pull request #238 from wakatime/main
Browse files Browse the repository at this point in the history
Release v5.12.0
  • Loading branch information
alanhamlett authored Mar 25, 2024
2 parents fb941c3 + d976d1c commit c515817
Show file tree
Hide file tree
Showing 11 changed files with 539 additions and 115 deletions.
30 changes: 24 additions & 6 deletions WakaTime/Extensions/AXUIElementExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ extension AXUIElement {
// swiftlint:enable force_cast
}

var nextSibling: AXUIElement? {
guard let parentChildren = self.parent?.children, let currentIndex = parentChildren.firstIndex(of: self) else { return nil }
let nextIndex = currentIndex + 1
guard parentChildren.indices.contains(nextIndex) else { return nil }
return parentChildren[nextIndex]
}

var previousSibling: AXUIElement? {
guard let parentChildren = self.parent?.children, let currentIndex = parentChildren.firstIndex(of: self) else { return nil }
let previousIndex = currentIndex - 1
guard parentChildren.indices.contains(previousIndex) else { return nil }
return parentChildren[previousIndex]
}

var id: String? {
guard let ref = getValue(for: kAXIdentifierAttribute) else { return nil }
// swiftlint:disable force_cast
Expand Down Expand Up @@ -60,7 +74,7 @@ extension AXUIElement {
// swiftlint:enable force_cast
}

func project(for app: MonitoredApp) -> String? {
func address(for app: MonitoredApp) -> String? {
var address: String?
switch app {
case .brave:
Expand All @@ -72,6 +86,10 @@ extension AXUIElement {
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
Expand All @@ -80,12 +98,12 @@ extension AXUIElement {
address = addressField?.value
default: return nil
}
return address
}

if let address {
return extractProjectName(from: address)
}

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

// swiftlint:disable cyclomatic_complexity
Expand Down
13 changes: 10 additions & 3 deletions WakaTime/Extensions/NSRunningApplicationExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ enum MonitoredApp: String, CaseIterable {
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,
Expand All @@ -57,9 +66,7 @@ enum MonitoredApp: String, CaseIterable {
]

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

extension NSRunningApplication {
Expand Down
15 changes: 15 additions & 0 deletions WakaTime/Extensions/StringExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

extension String {
func matchesRegex(_ pattern: String) -> Bool {
if let regex = try? NSRegularExpression(pattern: pattern) {
let range = NSRange(location: 0, length: self.utf16.count)
return regex.firstMatch(in: self, options: [], range: range) != nil
}
return false
}

func trim() -> String {
self.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}
}
6 changes: 6 additions & 0 deletions WakaTime/Helpers/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ class Dependencies {
}
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(decoding: outputData, as: UTF8.self)

// disable updating wakatime-cli when it was built from source
if output.trim() == "<local-build>" {
return true
}

let version: String?
if let regex = try? NSRegularExpression(pattern: "([0-9]+\\.[0-9]+\\.[0-9]+)"),
let match = regex.firstMatch(in: output, range: NSRange(output.startIndex..., in: output)),
Expand Down
46 changes: 46 additions & 0 deletions WakaTime/Helpers/FilterManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Cocoa

class FilterManager {
static func filterBrowsedSites(app: NSRunningApplication, monitoredApp: MonitoredApp, activeWindow: AXUIElement) -> Bool {
guard MonitoringManager.isAppBrowser(app) else { 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 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 {
return false
}
}
}

// The given address passed all filters and will be included
return true
}

private static func parseList(_ listString: String) -> [String] {
Self.sanitizeList(listString.components(separatedBy: "\n"))
}

private static func sanitizeList(_ urls: [String]) -> [String] {
urls.map { $0.trimmingCharacters(in: CharacterSet.whitespaces) }
}
}
20 changes: 20 additions & 0 deletions WakaTime/Helpers/MonitoringManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ class MonitoringManager {
return bundleId == MonitoredApp.xcode.rawValue
}

static func isAppBrowser(for bundleId: String) -> Bool {
MonitoredApp.browserAppIds.contains(bundleId)
}

static func isAppBrowser(_ app: NSRunningApplication) -> Bool {
guard let bundleId = app.bundleIdentifier else { return false }

return isAppBrowser(for: bundleId)
}

static func heartbeatData(_ app: NSRunningApplication) -> HeartbeatData? {
let pid = app.processIdentifier

Expand All @@ -60,6 +70,15 @@ class MonitoringManager {
let title = activeWindow.title(for: monitoredApp)
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
)
guard included else { return nil }

let project = activeWindow.project(for: monitoredApp)

switch monitoredApp {
Expand Down Expand Up @@ -103,6 +122,7 @@ class MonitoringManager {
case .linear:
return HeartbeatData(
entity: title,
project: project,
category: .planning)
case .notes:
if activeWindow.rawTitle == "Notes" {
Expand Down
65 changes: 65 additions & 0 deletions WakaTime/Helpers/PropertiesManager.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import Foundation

class PropertiesManager {
enum FilterType: String {
case denylist
case allowlist
}

enum Keys: String {
case shouldLaunchOnLogin = "launch_on_login"
case shouldLogToFile = "log_to_file"
case shouldAutomaticallyDownloadUpdates = "should_automatically_download_updates"
case hasLaunchedBefore = "has_launched_before"
case filterType = "filter_type"
case denylist = "denylist"
case allowlist = "allowlist"
}

static var shouldLaunchOnLogin: Bool {
Expand Down Expand Up @@ -71,4 +79,61 @@ class PropertiesManager {
UserDefaults.standard.synchronize()
}
}

static var filterType: FilterType {
get {
guard let filterTypeString = UserDefaults.standard.string(forKey: Keys.filterType.rawValue) else {
return .allowlist
}

return FilterType(rawValue: filterTypeString) ?? .denylist
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: Keys.filterType.rawValue)
UserDefaults.standard.synchronize()
}
}

static var denylist: String {
get {
guard let denylist = UserDefaults.standard.string(forKey: Keys.denylist.rawValue) else {
return ""
}

return denylist
}
set {
UserDefaults.standard.set(newValue, forKey: Keys.denylist.rawValue)
UserDefaults.standard.synchronize()
}
}

static var allowlist: String {
get {
guard let allowlist = UserDefaults.standard.string(forKey: Keys.allowlist.rawValue) else {
return
"https?://(\\w\\.)*github\\.com/\n" +
"https?://(\\w\\.)*gitlab\\.com/\n" +
"^stackoverflow\\.com/\n" +
"^docs\\.python\\.org/\n" +
"https?://(\\w\\.)*golang\\.org/\n" +
"https?://(\\w\\.)*go\\.dev/\n" +
"https?://(\\w\\.)*npmjs\\.com/\n" +
"https?//localhost[:\\d+]?/"
}

return allowlist
}
set {
UserDefaults.standard.set(newValue, forKey: Keys.allowlist.rawValue)
UserDefaults.standard.synchronize()
}
}

static var currentFilterList: String {
switch Self.filterType {
case .denylist: return Self.denylist
case .allowlist: return Self.allowlist
}
}
}
Loading

0 comments on commit c515817

Please sign in to comment.