Full Code of wakatime/macos-wakatime for AI

main afc494fe4dea cached
59 files
27.1 MB
39.2k tokens
1 requests
Download .txt
Repository: wakatime/macos-wakatime
Branch: main
Commit: afc494fe4dea
Files: 59
Total size: 27.1 MB

Directory structure:
gitextract_1hnk5wnk/

├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── support-app-request.yaml
│   └── workflows/
│       ├── on_pull_request_linter.yml
│       └── on_push.yml
├── .gitignore
├── .swiftlint.yml
├── .vscode/
│   └── settings.json
├── AUTHORS
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── Scripts/
│   ├── Firebase/
│   │   └── upload-dSYM.sh
│   └── Lint/
│       └── swiftlint
├── WakaTime/
│   ├── AppDelegate.swift
│   ├── ConfigFile.swift
│   ├── Controls/
│   │   └── WKTextField.swift
│   ├── Extensions/
│   │   ├── AXObserverExtension.swift
│   │   ├── AXUIElementExtension.swift
│   │   ├── BundleExtension.swift
│   │   ├── NSRunningApplicationExtension.swift
│   │   ├── OptionalExtension.swift
│   │   ├── ProcessExtension.swift
│   │   ├── StringExtension.swift
│   │   └── URLExtension.swift
│   ├── Helpers/
│   │   ├── Accessibility.swift
│   │   ├── AppInfo.swift
│   │   ├── Dependencies.swift
│   │   ├── EventSourceObserver.swift
│   │   ├── FilterManager.swift
│   │   ├── MonitoringManager.swift
│   │   ├── PropertiesManager.swift
│   │   └── SettingsManager.swift
│   ├── Resources/
│   │   ├── Assets.xcassets/
│   │   │   ├── AppIcon.appiconset/
│   │   │   │   └── Contents.json
│   │   │   ├── Contents.json
│   │   │   ├── WakaTime.imageset/
│   │   │   │   └── Contents.json
│   │   │   └── WakaTimeDisabled.imageset/
│   │   │       └── Contents.json
│   │   └── GoogleService-Info.plist
│   ├── Utils/
│   │   ├── Atomic.swift
│   │   ├── Logging.swift
│   │   ├── ObjC.h
│   │   └── ObjC.m
│   ├── Views/
│   │   ├── MonitoredAppsView.swift
│   │   └── SettingsView.swift
│   ├── WakaTime-Bridging-Header.h
│   ├── WakaTime-Info.plist
│   ├── WakaTime.entitlements
│   ├── WakaTime.swift
│   ├── Watchers/
│   │   ├── FileSavedWatcher.swift
│   │   ├── MonitoredApp.swift
│   │   └── Watcher.swift
│   ├── WindowControllers/
│   │   ├── MonitoredAppsWindowController.swift
│   │   └── SettingsWindowController.swift
│   └── main.swift
├── WakaTime Helper/
│   ├── AppDelegate.swift
│   ├── Logging.swift
│   ├── WakaTime Helper-Info.plist
│   └── main.swift
├── bin/
│   └── prepare_changelog.sh
└── project.yml

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================



================================================
FILE: .github/ISSUE_TEMPLATE/support-app-request.yaml
================================================
name: Support app request
description: Suggest a new app for tracking
title: "Support new app: XXX"
labels: enhancement
body:
  - type: input
    id: bundleid
    attributes:
      label: BundleId of the app
      description: Build this app in Xcode and it prints bundleIds of all running apps in the output window
    validations:
      required: true
  - type: textarea
    id: titles
    attributes:
      label: Example window titles
      description: List some example window titles of the app, which is used to parse the filename for your dashboard
    validations:
      required: true


================================================
FILE: .github/workflows/on_pull_request_linter.yml
================================================
name: Tests

on: pull_request

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      -
        name: Lint allowed branch names
        uses: lekterable/branchlint-action@1.2.0
        with:
          allowed: |
            /^(.+:)?bugfix/.+/i
            /^(.+:)?docs?/.+/i
            /^(.+:)?feature/.+/i
            /^(.+:)?hotfix/.+/i
            /^(.+:)?major/.+/i
            /^(.+:)?misc/.+/i
            /^(.+:)?main$/i
      -
        name: Block fixup/squash commits
        uses: xt0rted/block-autosquash-commits-action@v2
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
      -
        # Run only for release branch
        if: ${{ github.base_ref == 'release' }}
        name: Check for changelog pattern
        uses: gandarez/check-pr-body-action@v1.0.3
        with:
          pr_number: ${{ github.event.number }}
          contains: 'Changelog:'
          not_contains: '`'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/on_push.yml
================================================
name: Release

on:
  pull_request:
    types: [opened, reopened, ready_for_review, synchronize]
  push:
    branches: [main, release]
    tags-ignore: "**"

jobs:
  test:
    name: Tests and Build
    runs-on: macos-15
    steps:
      -
        name: Checkout
        uses: actions/checkout@v3
      -
        name: Install xcodegen via Homebrew for linting and building xcode project
        run: brew install xcodegen
      -
        name: Generate project
        run: xcodegen
      -
        name: Build app to run linters
        run: xcodebuild -scheme WakaTime -configuration Debug -destination 'generic/platform=macOS' ONLY_ACTIVE_ARCH=NO ARCHS='arm64 x86_64' build

  version:
    name: Version
    concurrency: tagging
    if: ${{ github.ref == 'refs/heads/release' || github.ref == 'refs/heads/main' }}
    runs-on: ubuntu-latest
    needs: [test]
    outputs:
      semver: ${{ steps.format.outputs.semver }}
      semver_tag: ${{ steps.semver-tag.outputs.semver_tag }}
      ancestor_tag: ${{ steps.semver-tag.outputs.ancestor_tag }}
      is_prerelease: ${{ steps.semver-tag.outputs.is_prerelease }}
    steps:
      -
        name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      -
        name: Calculate semver tag
        id: semver-tag
        uses: gandarez/semver-action@master
        with:
          prefix: v
          prerelease_id: alpha
          develop_branch_name: main
          main_branch_name: release
          major_pattern: "(?i)^(.+:)?(major/.+)"
      -
        name: Format
        id: format
        run: |
          echo "${{ steps.semver-tag.outputs.semver_tag }}"
          ver=`echo "${{ steps.semver-tag.outputs.semver_tag }}" | sed 's/^v//'`
          echo "$ver"
          echo "semver=$ver" >> $GITHUB_OUTPUT
      -
        name: Create tag
        uses: actions/github-script@v6
        with:
          github-token: ${{ github.token }}
          script: |
            github.rest.git.createRef({
              owner: context.repo.owner,
              repo: context.repo.repo,
              ref: "refs/tags/${{ steps.semver-tag.outputs.semver_tag }}",
              sha: context.sha
            })

  sign:
    name: Sign Apple app
    needs: [version]
    runs-on: macos-15
    steps:
      -
        name: Checkout
        uses: actions/checkout@v3
      -
        name: Update project.yml
        uses: fjogeleit/yaml-update-action@main
        with:
          valueFile: 'project.yml'
          changes:  |
            {
              "targets.WakaTime.settings.CURRENT_PROJECT_VERSION": "${{ needs.version.outputs.semver }}",
              "targets.WakaTime.settings.MARKETING_VERSION": "${{ needs.version.outputs.semver }}"
            }
          commitChange: false
      -
        name: Install xcodegen via Homebrew for linting and building xcode project
        run: brew install xcodegen
      -
        name: Generate project
        run: xcodegen
      -
        name: Build app
        id: build
        run: |
          xcodebuild -scheme WakaTime -configuration Release -destination 'generic/platform=macOS' ONLY_ACTIVE_ARCH=NO ARCHS='arm64 x86_64' build
          app=`find /Users/runner/Library/Developer/Xcode/DerivedData/ -name WakaTime.app`
          echo "$app"
          lipo -info "$app/Contents/MacOS/WakaTime"
          directory=`dirname $app`
          echo "$directory"
          echo "directory=$directory" >> $GITHUB_OUTPUT
      -
        name: Verify universal platform
        run: |
          app=`find /Users/runner/Library/Developer/Xcode/DerivedData/ -name WakaTime.app`
          echo "$app"
          lipo -info "$app/Contents/MacOS/WakaTime"
          BIN="$app/Contents/MacOS/WakaTime"
          archs="$(lipo -archs "$BIN" 2>/dev/null || true)"
          echo "Reported architectures: $archs"
          for required in arm64 x86_64; do
            echo "$archs" | grep -qw "$required" || {
              echo "❌ Missing required architecture: $required"
              exit 1
            }
          done
      -
        name: Import Code-Signing Certificates
        uses: Apple-Actions/import-codesign-certs@v1
        with:
          # The certificates in a PKCS12 file encoded as a base64 string
          p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
          # The password used to import the PKCS12 file.
          p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
      - name: Codesign
        env:
          APP_SIGNING_IDENTITY: ${{ secrets.APPLE_DEVELOPER_IDENTITY }}
        run: |
          codesign --force --deep --timestamp --options runtime --sign "$APP_SIGNING_IDENTITY" "${{ steps.build.outputs.directory }}/WakaTime.app"
      -
        name: Store Credentials
        env:
          NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          NOTARIZATION_APPLE_ID: ${{ secrets.AC_USERNAME }}
          NOTARIZATION_PWD: ${{ secrets.AC_PASSWORD }}
        run: xcrun notarytool store-credentials "notarytool-profile" --apple-id "$NOTARIZATION_APPLE_ID" --team-id "$NOTARIZATION_TEAM_ID" --password "$NOTARIZATION_PWD"
      -
        name: Notarize Helper
        run: |
          ditto -c -k --keepParent "${{ steps.build.outputs.directory }}/WakaTime.app/Contents/Library/LoginItems/WakaTime Helper.app" helper.zip
          xcrun notarytool submit helper.zip --keychain-profile "notarytool-profile" --wait
          xcrun stapler staple "${{ steps.build.outputs.directory }}/WakaTime.app/Contents/Library/LoginItems/WakaTime Helper.app"
      -
        name: Notarize App
        run: |
          ditto -c -k --keepParent ${{ steps.build.outputs.directory }}/WakaTime.app main.zip
          xcrun notarytool submit main.zip --keychain-profile "notarytool-profile" --wait
          xcrun stapler staple ${{ steps.build.outputs.directory }}/WakaTime.app
      -
        name: Zip
        run: ditto -c -k --sequesterRsrc --keepParent ${{ steps.build.outputs.directory }}/WakaTime.app WakaTime.zip
      -
        name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: app
          path: ./WakaTime.zip
      -
        name: Remove tag if failure
        if: ${{ failure() }}
        uses: actions/github-script@v6
        with:
          github-token: ${{ github.token }}
          script: |
            github.rest.git.deleteRef({
              owner: context.repo.owner,
              repo: context.repo.repo,
              ref: "tags/${{ needs.version.outputs.semver_tag }}"
            })

  changelog:
    name: Changelog
    runs-on: ubuntu-latest
    needs: [version, sign]
    outputs:
      changelog: ${{ steps.changelog.outputs.changelog }}
    steps:
      -
        name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      -
        if: ${{ github.ref == 'refs/heads/main' }}
        name: Changelog for main
        uses: gandarez/changelog-action@v1.2.0
        id: changelog-main
        with:
          current_tag: ${{ github.sha }}
          previous_tag: ${{ needs.version.outputs.ancestor_tag }}
          exclude: |
            ^Merge pull request .*
      -
        if: ${{ github.ref == 'refs/heads/release' }}
        name: Get related pull request
        uses: 8BitJonny/gh-get-current-pr@2.2.0
        id: changelog-release
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
      -
        name: Prepare changelog
        id: changelog
        run: |
          echo "${{ steps.changelog-main.outputs.changelog || steps.changelog-release.outputs.pr_body }}" > changelog.txt
          ./bin/prepare_changelog.sh $(echo ${GITHUB_REF#refs/heads/}) "$(cat changelog.txt)"
      -
        name: Remove tag if failure
        if: ${{ failure() }}
        uses: actions/github-script@v6
        with:
          github-token: ${{ github.token }}
          script: |
            github.rest.git.deleteRef({
              owner: context.repo.owner,
              repo: context.repo.repo,
              ref: "tags/${{ needs.version.outputs.semver_tag }}"
            })

  release:
    name: Release
    runs-on: macos-15
    needs: [version, sign, changelog]
    steps:
      -
        name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      -
        name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          name: app
          path: ./
      -
        name: Prepare release folder
        id: prepare
        run: |
          mkdir release
          mv ./WakaTime.zip release/macos-wakatime.zip
      -
        name: "Create release"
        uses: softprops/action-gh-release@master
        with:
          name: ${{ needs.version.outputs.semver_tag }}
          tag_name: ${{ needs.version.outputs.semver_tag }}
          body: "## Changelog\n${{ needs.changelog.outputs.changelog }}"
          prerelease: ${{ needs.version.outputs.is_prerelease }}
          target_commitish: ${{ github.sha }}
          draft: false
          files: |
            ./release/macos-wakatime.zip
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      -
        name: Remove tag if failure
        if: ${{ failure() }}
        uses: actions/github-script@v6
        with:
          github-token: ${{ github.token }}
          script: |
            github.rest.git.deleteRef({
              owner: context.repo.owner,
              repo: context.repo.repo,
              ref: "tags/${{ needs.version.outputs.semver_tag }}"
            })


================================================
FILE: .gitignore
================================================
.DS_Store
*.xcodeproj
xcuserdata/
Mint/
.build/
build/
Package.resolved


================================================
FILE: .swiftlint.yml
================================================
#
# .swiftlint.yml
#
#

disabled_rules:
  - inclusive_language
  - nesting
  - redundant_string_enum_value
  - todo
  - trailing_comma
  - type_body_length
  - vertical_parameter_alignment
  # 36 false positives. Will disable weak_delegate rule for now
  - weak_delegate

opt_in_rules:
  - array_init
  - closure_end_indentation
  - closure_spacing
  - contains_over_first_not_nil
  - empty_count
  - explicit_init
  - fatal_error_message
  - first_where
  - force_unwrapping
  - implicit_return
  - literal_expression_end_indentation
  - operator_usage_whitespace
  - overridden_super_call
  - override_in_extension
  - private_outlet
  - redundant_nil_coalescing
  - sorted_first_last
  - strict_fileprivate
  - trailing_closure
  - unneeded_parentheses_in_closure_argument

included:
  - WakaTime

force_cast: error
force_try: error
force_unwrapping: error

trailing_whitespace:
  ignores_empty_lines: false
  severity: warning
trailing_newline: error
trailing_semicolon: error

vertical_whitespace:
  max_empty_lines: 1
  severity: warning

comma: error
colon:
  severity: error
opening_brace: error
empty_count: error
legacy_constructor: error
statement_position:
  statement_mode: default
  severity: error
legacy_constant: error

type_name:
  min_length: 3
  max_length:
    warning: 45
    error: 50
  excluded:
    - T

identifier_name:
  max_length:
    warning: 40
    error: 50
  min_length:
    error: 3
  excluded:
    - x
    - y
    - z
    - i
    - j
    - at
    - on
    - id
    - db
    - rs
    - to
    - in
    - me
    - up
    - dx
    - dy
    - preferredInterfaceOrientationForPresentation

function_parameter_count:
  warning: 10
  error: 10

line_length:
  warning: 140
  error: 140

function_body_length:
  warning: 150
  error: 200

file_length:
  warning: 1000
  error: 1000

cyclomatic_complexity:
  warning: 30
  error: 30

large_tuple:
  warning: 4
  error: 5

switch_case_alignment:
  indented_cases: true

reporter: 'xcode'

custom_rules:
  comments_space:
    name: 'Space After Comment'
    regex: '(^ *//\w+)'
    message: 'There should be a space after //'
    severity: warning

  empty_first_line:
    name: 'Empty First Line'
    regex: '(^[ a-zA-Z ]*(?:protocol|extension|class|struct|func) [ a-zA-Z0-9:,<>\.\(\)\"-=`]*\{\n( *)?\n)'
    message: 'There should not be an empty line after a declaration'
    severity: error

  empty_line_after_guard:
    name: 'Empty Line After Guard'
    regex: '(^ *guard[ a-zA-Z0-9=?.\(\),><!`]*\{[ a-zA-Z0-9=?.\(\),><!`\"]*\}\n *(?!(?:return|guard))\S+)'
    message: 'There should be an empty line after a guard'
    severity: error

  empty_line_after_super:
    name: 'Empty Line After Super'
    regex: '(^ *super\.[ a-zA-Z0-9=?.\(\)\{\}:,><!`\"]*\n *(?!(?:\}|return))\S+)'
    message: 'There should be an empty line after super'
    severity: error


================================================
FILE: .vscode/settings.json
================================================
{
    "lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB",
    "githubPullRequests.ignoredPullRequestBranches": [
        "main"
    ]
}

================================================
FILE: AUTHORS
================================================
WakaTime is written and maintained by Alan Hamlett and various contributors:

- Alan Hamlett <alan.hamlett@gmail.com>
- Carlos Henrique Gandarez <gandarez@gmail.com>
- Michael Mavris <@MMavrisPaleBlue>
- Tobias Lensing <@starbugs>
- Chris Pastl <chris@crispybits.app>


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

## Setup

This project depends on the [xcodegen](https://github.com/yonaskolb/XcodeGen?tab=readme-ov-file#installing) command line tool.

```bash
git clone git@github.com:wakatime/macos-wakatime.git
cd macos-wakatime
xcodegen
```

Then open the `WakaTime.xcodeproj` in [Xcode 15.2](https://developer.apple.com/services-account/download?path=/Developer_Tools/Xcode_15.2/Xcode_15.2.xip).
Currently there’s a bug in new Swift compiler versions, so the largest Xcode version working with this app is 15.2.

## Branches

This project currently has two branches

- `main` - Default branch for every new `feature` or `fix`
- `release` - Branch for production releases and hotfixes

## Testing and Linting

Build with `Xcode` before creating any pull requests, or your PR won’t pass the automated checks.

## SwiftLint

To fix linter warning(s), run `swiftlint --fix`.

## Branching Strategy

We require specific branch name prefixes for PRs:

- `^major/.+` - `major`
- `^feature/.+` - `minor`
- `^bugfix/.+` - `patch`
- `^docs?/.+` - `build`
- `^misc/.+` - `build`

More info at [wakatime/semver-action](https://github.com/wakatime/semver-action#branch-names).

## Pull Requests

- Big changes, changes to the API, or changes with backward compatibility trade-offs should be first discussed in the Slack.
- Search [existing pull requests](https://github.com/wakatime/macos-wakatime/pulls) to see if one has already been submitted for this change. Search the [issues](https://github.com/wakatime/macos-wakatime/issues?q=is%3Aissue) to see if there has been a discussion on this topic and whether your pull request can close any issues.
- Code formatting should be consistent with the style used in the existing code.
- Don't leave commented out code. A record of this code is already preserved in the commit history.
- All commits must be atomic. This means that the commit completely accomplishes a single task. Each commit should result in fully functional code. Multiple tasks should not be combined in a single commit, but a single task should not be split over multiple commits (e.g. one commit per file modified is not a good practice). For more information see <http://www.freshconsulting.com/atomic-commits>.
- Each pull request should address a single bug fix or feature. This may consist of multiple commits. If you have multiple, unrelated fixes or enhancements to contribute, submit them as separate pull requests.
- Commit messages:
  - Use the [imperative mood](http://chris.beams.io/posts/git-commit/#imperative) in the title. For example: "Apply editor.indent preference"
  - Capitalize the title.
  - Do not end the title with a period.
  - Separate title from the body with a blank line. If you're committing via GitHub or GitHub Desktop this will be done automatically.
  - Wrap body at 72 characters.
  - Completely explain the purpose of the commit. Include a rationale for the change, any caveats, side-effects, etc.
  - If your pull request fixes an issue in the issue tracker, use the [closes/fixes/resolves syntax](https://help.github.com/articles/closing-issues-via-commit-messages) in the body to indicate this.
  - See <http://chris.beams.io/posts/git-commit> for more tips on writing good commit messages.
- Pull request title and description should follow the same guidelines as commit messages.
- Rebasing pull requests is OK and encouraged. After submitting your pull request some changes may be requested. Prefer using [git fixup](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupltcommitgt) rather than adding orphan extra commits to the pull request, then do a push to your fork. As soon as your PR gets approved one of us will merge it by rebasing and squashing any residuary commits that were pushed while reviewing. This will help to keep the commit history of the repository clean.

## Troubleshooting

If you have trouble building off `main` branch, try:

* close Xcode
* `rm -rf ~/Library/Developer/Xcode/DerivedData/WakaTime*`
* `rm -rf ./WakaTime.xcodeproj`
* `xcodegen`
* Open the project in Xcode
* Under `Signing & Capabilities`, set your `Team`

To read local user preferences, run:

    defaults read macos-wakatime.WakaTime

Any question join us on [Slack](https://wakaslack.herokuapp.com/).


================================================
FILE: LICENSE
================================================
BSD 3-Clause License

Copyright (c) 2023 Alan Hamlett.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
  notice, this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer
  in the documentation and/or other materials provided
  with the distribution.

* Neither the names of WakaTime, nor the names of its
  contributors may be used to endorse or promote products derived
  from this software without specific prior written permission.

THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: README.md
================================================
# macos-wakatime

Mac system tray app for automatic time tracking and metrics generated from your Xcode activity.

## Install

1. Download the [latest release](https://github.com/wakatime/macos-wakatime/releases/latest/download/macos-wakatime.zip).
2. Move `WakaTime.app` into your `Applications` folder, and run `WakaTime.app`.
3. Enter your [WakaTime API Key][api key], then press `Save`.
4. Use Xcode like normal and your coding activity will be displayed on your [WakaTime dashboard][dashboard]

## Usage

Keep the app running in your system tray, and your Xcode usage will show on your [WakaTime dashboard][dashboard].

## Building from Source

1. Run `xcodegen` to generate the project.
2. Open the project with Xcode.
3. Click Run (⌘+R).

If you run into Accessibility problems, try running `sudo tccutil reset Accessibility`.

## Uninstall

To uninstall, move `WakaTime.app` into your mac Trash.

If you don’t use any other WakaTime plugins, run `rm -r ~/.wakatime*`.

## Supported Apps

WakaTime for Mac can track the time you spend in any app on your mac. It’s a catch-all when we don’t have a plugin for your IDE or app.

We add support for specific apps when a custom category, project, or entity type is necessary.
For example, when Slack needs the `communicating` category or Figma needs the `designing` category.
Only request support for a new app when it needs a custom category, or we can detect the project from the window title.

Before requesting support for a new app, first check the [list of supported apps][supported apps].

## Contributing

Pull requests and issues are welcome!
See [Contributing][contributing] for more details.
Many thanks to all [contributors][authors]!

Made with :heart: by the WakaTime Team.

[api key]: https://wakatime.com/api-key
[dashboard]: https://wakatime.com/
[contributing]: CONTRIBUTING.md
[authors]: AUTHORS
[supported apps]: https://github.com/wakatime/macos-wakatime/blob/main/WakaTime/Watchers/MonitoredApp.swift#L3


================================================
FILE: Scripts/Firebase/upload-dSYM.sh
================================================
"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"

================================================
FILE: Scripts/Lint/swiftlint
================================================
[File too large to display: 26.9 MB]

================================================
FILE: WakaTime/AppDelegate.swift
================================================
import AppUpdater
import Cocoa
import UserNotifications

class AppDelegate: NSObject, NSApplicationDelegate, StatusBarDelegate, UNUserNotificationCenterDelegate {
    var window: NSWindow!
    var statusBarItem: NSStatusItem!
    let menu = NSMenu()
    var statusBarA11yItem: NSMenuItem!
    var statusBarA11ySeparator: NSMenuItem!
    var statusBarA11yStatus: Bool = true
    var settingsWindowController = SettingsWindowController()
    var monitoredAppsWindowController = MonitoredAppsWindowController()
    var wakaTime: WakaTime?

    @Atomic var lastTodayTime = 0
    @Atomic var lastTodayText = ""
    @Atomic var lastBrowserWarningTime = 0

    let updater = AppUpdater(owner: "wakatime", repo: "macos-wakatime")

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Configure logging to a log file if activated by the user
        if PropertiesManager.shouldLogToFile {
            Logging.default.activateLoggingToFile()
        }

        Logging.default.log("Starting WakaTime")

        // Handle deep links
        let eventManager = NSAppleEventManager.shared()
        eventManager.setEventHandler(
            self,
            andSelector: #selector(handleGetURL(_:withReplyEvent:)),
            forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL)
        )

        let statusBar = NSStatusBar.system
        statusBarItem = statusBar.statusItem(withLength: NSStatusItem.variableLength)
        statusBarItem.button?.image = NSImage(named: NSImage.Name("WakaTime"))

        // refresh code time text when status bar icon clicked
        statusBarItem.button?.target = self
        statusBarItem.button?.action = #selector(AppDelegate.onClick(_:))
        statusBarItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])

        statusBarA11yItem = NSMenuItem(
            title: "* A11y permission needed *",
            action: #selector(AppDelegate.a11yClicked(_:)),
            keyEquivalent: "")
        statusBarA11yItem.isHidden = true
        menu.addItem(statusBarA11yItem)
        statusBarA11ySeparator = NSMenuItem.separator()
        menu.addItem(statusBarA11ySeparator)
        statusBarA11ySeparator.isHidden = true
        menu.addItem(withTitle: "Dashboard", action: #selector(AppDelegate.dashboardClicked(_:)), keyEquivalent: "")
        menu.addItem(withTitle: "Settings", action: #selector(AppDelegate.settingsClicked(_:)), keyEquivalent: "")
        menu.addItem(
            withTitle: "Monitored Apps",
            action: #selector(AppDelegate.monitoredAppsClicked(_:)),
            keyEquivalent: "")
        menu.addItem(NSMenuItem.separator())
        menu.addItem(
            withTitle: "Check for Updates",
            action: #selector(AppDelegate.checkForUpdatesClicked(_:)),
            keyEquivalent: "")
        menu.addItem(NSMenuItem.separator())
        menu.addItem(withTitle: "Quit", action: #selector(AppDelegate.quitClicked(_:)), keyEquivalent: "")

        wakaTime = WakaTime(self)

        settingsWindowController.settingsView.delegate = self

        Task.detached(priority: .background) {
            self.fetchToday()
        }

        // request notifications permission
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
            guard granted else {
                if let msg = error?.localizedDescription {
                    Logging.default.log(msg)
                }
                return
            }
        }
    }

    func applicationWillTerminate(_ notification: Notification) {
        Logging.default.log("WakaTime will terminate")
    }

    @objc func handleGetURL(_ event: NSAppleEventDescriptor, withReplyEvent replyEvent: NSAppleEventDescriptor) {
        // Handle deep links
        guard let urlString = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue,
              let url = URL(string: urlString),
              url.scheme == "wakatime",
              let link = DeepLink(rawValue: url.host ?? "")
        else { return }

        switch link {
            case .settings:
                showSettings()
            case .monitoredApps:
                showMonitoredApps()
        }
    }

    @objc func dashboardClicked(_ sender: AnyObject) {
        let defaultUrl = "https://wakatime.com/"

        var url = ConfigFile.getSetting(section: "settings", key: "api_url") ?? defaultUrl
        if url.isEmpty {
            url = defaultUrl
        }

        url = url
            .replacingOccurrences(of: "://api.", with: "://")
            .replacingOccurrences(of: "/api/v1", with: "")
            .replacingOccurrences(of: "^api\\.", with: "", options: .regularExpression)
            .replacingOccurrences(of: "/api", with: "")

        if let url = URL(string: url) {
            NSWorkspace.shared.open(url)
        } else {
            if url != defaultUrl {
                if let url = URL(string: defaultUrl) {
                    NSWorkspace.shared.open(url)
                }
            }
        }
    }

    @objc func settingsClicked(_ sender: AnyObject) {
        showSettings()
    }

    @objc func monitoredAppsClicked(_ sender: AnyObject) {
        showMonitoredApps()
    }

    @objc func checkForUpdatesClicked(_ sender: AnyObject) {
        updater.check {
            self.toastNotification("Updating to latest release")
        }.catch(policy: .allErrors) { error in
            if error.isCancelled {
                let alert = NSAlert()
                alert.messageText = "Up to date"
                alert.informativeText = "You have the latest version (\(Bundle.main.version))."
                alert.alertStyle = NSAlert.Style.warning
                alert.addButton(withTitle: "OK")
                alert.runModal()
            } else {
                Logging.default.log(String(describing: error))
                let alert = NSAlert()
                alert.messageText = "Error"
                let max = 200
                if error.localizedDescription.count <= max {
                    alert.informativeText = error.localizedDescription
                } else {
                    alert.informativeText = String(error.localizedDescription.prefix(max).appending("…"))
                }
                alert.alertStyle = NSAlert.Style.warning
                alert.addButton(withTitle: "OK")
                alert.runModal()
            }
        }
    }

    @objc func a11yClicked(_ sender: AnyObject) {
        a11yStatusChanged(Accessibility.requestA11yPermission())
    }

    @objc func quitClicked(_ sender: AnyObject) {
        NSApplication.shared.terminate(self)
    }

    @objc func onClick(_ sender: NSStatusItem) {
        Task.detached(priority: .background) {
            self.fetchToday()
        }
        // statusBarItem.popUpMenu(menu)
        statusBarItem.menu = menu
    }

    func a11yStatusChanged(_ hasPermission: Bool) {
        guard statusBarA11yStatus != hasPermission else { return }

        statusBarA11yStatus = hasPermission
        if hasPermission {
            statusBarItem.button?.image = NSImage(named: NSImage.Name("WakaTime"))
        } else {
            statusBarItem.button?.image = NSImage(named: NSImage.Name("WakaTimeDisabled"))
        }
        statusBarA11yItem.isHidden = hasPermission
        statusBarA11ySeparator.isHidden = hasPermission
    }

    private func checkBrowserDuplicateTracking() {
        // Warn about using both Browser extension and Mac app tracking a browser at same time, once per 12 hrs
        let time = Int(NSDate().timeIntervalSince1970)
        if time - lastBrowserWarningTime > Dependencies.twelveHours && MonitoringManager.isMonitoringBrowsing {
            Task {
                if let browser = await Dependencies.recentBrowserExtension() {
                    lastBrowserWarningTime = time
                    delegate.toastNotification("Warning: WakaTime \(browser) extension detected. " +
                        "It’s recommended to only track browsing activity with the \(browser) " +
                        "extension or Mac Desktop app, but not both.")
                }
            }
        }
    }

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

    private func showMonitoredApps() {
        NSApp.activate(ignoringOtherApps: true)
        monitoredAppsWindowController.showWindow(self)
    }

    internal func toastNotification(_ title: String) {
        let content = UNMutableNotificationContent()
        content.title = title
        content.body = " "

        let uuidString = UUID().uuidString
        let request = UNNotificationRequest(
        identifier: uuidString,
        content: content, trigger: nil)

        let notificationCenter = UNUserNotificationCenter.current()
        notificationCenter.delegate = self

        notificationCenter.requestAuthorization(options: [.alert, .sound]) { granted, _ in
            guard granted else { return }

            DispatchQueue.main.async {
                notificationCenter.add(request)
            }
        }
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        if #available(macOS 11.0, *) {
            completionHandler([.banner, .sound])
        } else {
            completionHandler([.alert, .sound]) // Fallback for older macOS versions
        }
    }

    private func setText(_ text: String) {
        DispatchQueue.main.async {
            Logging.default.log("Set status bar text: \(text)")
            self.statusBarItem.button?.title = text.isEmpty ? text : " " + text
        }
    }

    internal func fetchToday() {
        guard PropertiesManager.shouldDisplayTodayInStatusBar else {
            setText("")
            return
        }

        let time = Int(NSDate().timeIntervalSince1970)
        guard lastTodayTime + 120 < time else {
            setText(lastTodayText)
            return
        }

        lastTodayTime = time

        let cli = NSString.path(
            withComponents: ConfigFile.resourcesFolder + ["wakatime-cli"]
        )
        let process = Process()
        process.launchPath = cli
        let args = [
            "--today",
            "--today-hide-categories",
            "true",
            "--plugin",
            "macos-wakatime/" + Bundle.main.version,
        ]

        Logging.default.log("Fetching coding activity for Today from api: \(args)")

        process.arguments = args
        let pipe = Pipe()
        process.standardOutput = pipe
        process.standardError = FileHandle.nullDevice

        do {
            try process.execute()
        } catch {
            Logging.default.log("Failed to run wakatime-cli fetching Today coding activity: \(error)")
            return
        }

        let handle = pipe.fileHandleForReading
        let data = handle.readDataToEndOfFile()
        let text = (String(data: data, encoding: String.Encoding.utf8) ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
        lastTodayText = text
        setText(text)

        checkBrowserDuplicateTracking()
    }
}


================================================
FILE: WakaTime/ConfigFile.swift
================================================
import Foundation

struct ConfigFile {
    private static var userHome: [String] {
        FileManager.default.homeDirectoryForCurrentUser.pathComponents
    }

    public static var resourcesFolder: [String] {
        userHome + [".wakatime"]
    }

    private static var filePath: String {
        NSString.path(withComponents: userHome + [".wakatime.cfg"])
    }

    private static var filePathInternal: String {
        NSString.path(withComponents: resourcesFolder + ["wakatime-internal.cfg"])
    }

    static func getSetting(section: String, key: String, internalConfig: Bool = false) -> String? {
        let file = internalConfig ? filePathInternal : filePath
        let contents: String
        do {
            contents = try String(contentsOfFile: file)
        } catch {
            Logging.default.log("Failed reading \(file): " + error.localizedDescription)
            return nil
        }
        let lines = contents.split(separator: "\n")

        var currentSection = ""
        for line in lines {
            if line.hasPrefix("[") && line.hasSuffix("]") {
                currentSection = String(line.dropFirst().dropLast())
            } else if currentSection == section {
                let parts = line.split(separator: "=", maxSplits: 2)
                if parts.count == 2 && parts[0].trimmingCharacters(in: .whitespacesAndNewlines) == key {
                    return String(parts[1].trimmingCharacters(in: .whitespacesAndNewlines))
                }
            }
        }
        return nil
    }

    static func setSetting(section: String, key: String, val: String, internalConfig: Bool = false) {
        let file = internalConfig ? filePathInternal : filePath
        let contents: String
        do {
            contents = try String(contentsOfFile: file)
        } catch {
            contents = "[" + section + "]\n" + key + " = " + val
            do {
                try contents.write(to: URL(fileURLWithPath: file), atomically: true, encoding: .utf8)
            } catch {
                assertionFailure("Failed writing to URL: \(file), Error: " + error.localizedDescription)
            }
        }

        let lines = contents.split(separator: "\n")
        var output: [String] = []
        var currentSection = ""
        var found = false
        for line in lines {
            if line.hasPrefix("[") && line.hasSuffix("]") {
                if currentSection == section && !found {
                    output.append(key + " = " + val)
                    found = true
                }
                output.append(String(line))
                currentSection = String(line.dropFirst().dropLast())
            } else if currentSection == section {
                let parts = line.split(separator: "=", maxSplits: 2)
                if parts.count == 2 && parts[0].trimmingCharacters(in: .whitespacesAndNewlines) == key {
                    if !found {
                        output.append(key + " = " + val)
                        found = true
                    }
                } else {
                    output.append(String(line))
                }
            } else {
                output.append(String(line))
            }
        }

        if !found {
            if currentSection != section {
                output.append("[" + section + "]")
            }
            output.append(key + " = " + val)
        }

        do {
            try output.joined(separator: "\n").write(to: URL(fileURLWithPath: file), atomically: true, encoding: .utf8)
        } catch {
            assertionFailure("Failed writing to URL: \(file), Error: " + error.localizedDescription)
        }
    }
}


================================================
FILE: WakaTime/Controls/WKTextField.swift
================================================
import AppKit

class WKTextField: NSTextField {
    override func performKeyEquivalent(with event: NSEvent) -> Bool {
        if event.type == NSEvent.EventType.keyDown {
            let modifierFlags = event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue
            if modifierFlags == NSEvent.ModifierFlags.command.rawValue {
                switch event.charactersIgnoringModifiers?.first {
                    case "x":
                        if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) { return true }
                    case "c":
                        if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) { return true }
                    case "v":
                        if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) { return true }
                    case "a":
                        if NSApp.sendAction(#selector(NSText.selectAll(_:)), to: nil, from: self) { return true }
                    case "z":
                        if NSApp.sendAction(Selector(("undo:")), to: nil, from: self) { return true }
                    default:
                        break
                }
            } else if modifierFlags == NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue {
                if NSApp.sendAction(Selector(("redo:")), to: nil, from: self) { return true }
            }
        }
        return super.performKeyEquivalent(with: event)
    }
}


================================================
FILE: WakaTime/Extensions/AXObserverExtension.swift
================================================
import AppKit

extension AXObserver {
    static func create(appID: pid_t, callback: AXObserverCallback) throws -> AXObserver {
        var observer: AXObserver?
        let error = AXObserverCreate(appID, callback, &observer)

        guard error == .success else { throw AXObserverError.createFailed(error) }
        guard let observer else { throw AXObserverError.createFailed(error) }

        return observer
    }

    func add(notification: String, element: AXUIElement, refcon: UnsafeMutableRawPointer?) throws {
        let error = AXObserverAddNotification(self, element, notification as CFString, refcon)
        guard error == .success else {
            Logging.default.log("Add notification \(notification) failed: \(error.rawValue)")
            throw AXObserverError.addNotificationFailed(error)
        }

        // Logging.default.log("Added notification \(notification) to observer \(self)")
    }

    func remove(notification: String, element: AXUIElement) throws {
        let error = AXObserverRemoveNotification(self, element, notification as CFString)
        guard error == .success else {
            Logging.default.log("Remove notification \(notification) failed: \(error.rawValue)")
            throw AXObserverError.removeNotificationFailed(error)
        }

        // Logging.default.log("Removed notification \(notification) from observer \(self)")
    }

    func addToRunLoop(mode: CFRunLoopMode = .defaultMode) {
        CFRunLoopAddSource(RunLoop.current.getCFRunLoop(), AXObserverGetRunLoopSource(self), mode)
        // Logging.default.log("Added observer \(self) to run loop")
    }

    func removeFromRunLoop(mode: CFRunLoopMode = .defaultMode) {
        CFRunLoopRemoveSource(RunLoop.current.getCFRunLoop(), AXObserverGetRunLoopSource(self), mode)
        // Logging.default.log("Removed observer \(self) from run loop")
    }
}

private enum AXObserverError: Error {
    case createFailed(AXError)
    case addNotificationFailed(AXError)
    case removeNotificationFailed(AXError)
}


================================================
FILE: WakaTime/Extensions/AXUIElementExtension.swift
================================================
import AppKit

struct AXPatternElement {
    var role: String?
    var subrole: String?
    var id: String?
    var title: String?
    var value: String?
    var children: [AXPatternElement] = []
}

extension AXUIElement {
    var selectedText: String? {
        getValue(for: kAXSelectedTextAttribute) as? String
    }

    func getValue(for attribute: String) -> CFTypeRef? {
        var result: CFTypeRef?
        guard AXUIElementCopyAttributeValue(self, attribute as CFString, &result) == .success else { return nil }
        return result
    }

    var children: [AXUIElement]? {
        guard let ref = getValue(for: kAXChildrenAttribute) else { return nil }
        return ref as? [AXUIElement]
    }

    var parent: AXUIElement? {
        guard let ref = getValue(for: kAXParentAttribute) else { return nil }
        // swiftlint:disable force_cast
        return (ref as! 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
        return (ref as! String)
        // swiftlint:enable force_cast
    }

    var rawTitle: String? {
        guard let ref = getValue(for: kAXTitleAttribute) else { return nil }
        // swiftlint:disable force_cast
        return (ref as! String)
        // swiftlint:enable force_cast
    }

    var role: String? {
        guard let ref = getValue(for: kAXRoleAttribute) else { return nil }
        // swiftlint:disable force_cast
        return (ref as! String)
        // swiftlint:enable force_cast
    }

    var subrole: String? {
        guard let ref = getValue(for: kAXSubroleAttribute) else { return nil }
        // swiftlint:disable force_cast
        return (ref as! String)
        // swiftlint:enable force_cast
    }

    var document: String? {
        guard let ref = getValue(for: kAXDocumentAttribute) else { return nil }
        // swiftlint:disable force_cast
        return (ref as! String)
        // swiftlint:enable force_cast
    }

    var value: String? {
        guard let ref = getValue(for: kAXValueAttribute) else { return nil }
        return (ref as? String)
    }

    var activeWindow: AXUIElement? {
        // swiftlint:disable force_cast
        if let window = getValue(for: kAXFocusedWindowAttribute) {
            return (window as! AXUIElement)
        }
        if let window = getValue(for: kAXMainWindowAttribute) {
            return (window as! AXUIElement)
        }
        if let window = getValue(for: kAXWindowAttribute) {
            return (window as! AXUIElement)
        }
        // swiftlint:enable force_cast
        return nil
    }

    var currentPath: URL? {
        if let window = activeWindow {
            if let path = window.document {
                if path.hasPrefix("file://") {
                    return URL(string: path.dropFirst(7).description)
                }
                return URL(string: path)
            }
        }
        if let path = document {
            if path.hasPrefix("file://") {
                return URL(string: path.dropFirst(7).description)
            }
            return URL(string: path)
        }
        return nil
    }

    // Traverses the element's children (breadth-first) until visitor() returns false or traversal is completed
    func traverseDown(visitor: (AXUIElement) -> Bool) {
        var queue: [AXUIElement] = [self]
        while !queue.isEmpty {
            let currentElement = queue.removeFirst()
            if let children = currentElement.children {
                for child in children {
                    if !visitor(child) { return }
                    queue.append(child)
                }
            }
        }
    }

    func traverseDownDFS(
        visitor: (AXUIElement) -> Bool,
        skipDescendantsWhere: ((AXUIElement) -> Bool)? = nil
    ) {
        var stack: [AXUIElement] = [self]
        while !stack.isEmpty {
            let currentElement = stack.removeLast()
            if !visitor(currentElement) { return }
            if skipDescendantsWhere?(currentElement) == true { continue }
            if let children = currentElement.children {
                stack.append(contentsOf: children.reversed())
            }
        }
    }

    // Traverses the element's parents until visitor() returns false or traversal is completed
    func traverseUp(visitor: (AXUIElement) -> Bool, element: AXUIElement? = nil) {
        let element = element ?? self
        if let parent = element.parent {
            if !visitor(parent) { return }
            traverseUp(visitor: visitor, element: parent)
        }
    }

    func firstDescendantWhere(
        _ condition: (AXUIElement) -> Bool,
        skipDescendantsWhere: ((AXUIElement) -> Bool)? = nil
    ) -> AXUIElement? {
        var matchingDescendant: AXUIElement?
        traverseDownDFS(visitor: { element in
            if condition(element) {
                matchingDescendant = element
                return false // stop traversal
            }
            return true // continue traversal
        }, skipDescendantsWhere: skipDescendantsWhere)
        return matchingDescendant
    }

    // Find the first descendant whose identifier matches the given identifier
    func elementById(identifier: String) -> AXUIElement? {
        firstDescendantWhere { $0.id == identifier }
    }

    func firstAncestorWhere(_ condition: (AXUIElement) -> Bool) -> AXUIElement? {
        var matchingAncestor: AXUIElement?
        traverseUp { element in
            if condition(element) {
                matchingAncestor = element
                return false
            }
            return true
        }
        return matchingAncestor
    }

    // Index path of `element` relative to self
    func indexPath(for element: AXUIElement) -> [Int] {
        var path = [Int]()
        var currentElement: AXUIElement? = element

        while let current = currentElement, current != self {
            if let parent = current.parent {
                if let index = parent.children?.firstIndex(where: { $0 == current }) {
                    path.insert(index, at: 0)
                }
                currentElement = parent
            } else {
                // No parent found, stop the loop
                break
            }
        }

        return path
    }

    // Finds the element at the given `indexPath`. `indexPath` must be relative to self.
    // If no element with the given index path exists, returns nil.
    func elementAtIndexPath(_ indexPath: [Int]) -> AXUIElement? {
        var currentElement: AXUIElement = self
        for index in indexPath {
            // currentElement.debugPrint()
            guard let children = currentElement.children, index < children.count else {
                // Index is out of bounds for the current element's children
                return nil
            }
            currentElement = children[index]
        }
        return currentElement
    }

    func findByPattern(_ pattern: AXPatternElement, within element: AXUIElement? = nil) -> AXUIElement? {
        let rootElement = element ?? self

        func matchesPattern(element: AXUIElement, pattern: AXPatternElement) -> Bool {
            let roleMatches = pattern.role == nil || element.role == pattern.role
            let subroleMatches = pattern.subrole == nil || element.subrole == pattern.subrole
            let titleMatches = pattern.title == nil || element.rawTitle == pattern.title
            let valueMatches = pattern.value == nil || element.selectedText == pattern.value
            let idMatches = pattern.id == nil || element.id == pattern.id

            return roleMatches && subroleMatches && titleMatches && valueMatches && idMatches
        }

        func search(element: AXUIElement, pattern: AXPatternElement) -> AXUIElement? {
            if matchesPattern(element: element, pattern: pattern) {
                var currentElement = element
                for childPattern in pattern.children {
                    guard let children = currentElement.children else { return nil }

                    var foundMatch = false
                    for child in children {
                        if let match = search(element: child, pattern: childPattern) {
                            currentElement = match
                            foundMatch = true
                            break
                        }
                    }
                    if !foundMatch {
                        return nil
                    }
                }
                return currentElement
            } else {
                guard let children = element.children else { return nil }

                for child in children {
                    if let match = search(element: child, pattern: pattern) {
                        return match
                    }
                }
            }
            return nil
        }

        return search(element: rootElement, pattern: pattern)
    }

    // Finds the first text area element whose value looks like a URL. Note that Chrome
    // cuts off the URL scheme, so this only scans for a domain with an optional path.
    func findAddressField() -> AXUIElement? {
        firstDescendantWhere { descendant in
            if descendant.role == kAXTextFieldRole, let value = descendant.value {
                let pattern = "(([^:\\/\\s]+)\\.([^:\\/\\s\\.]+))(\\/\\w+)*(\\/([\\w\\-\\.]+[^#?\\s]+))?(.*)?(#[\\w\\-]+)?$"
                do {
                    let regex = try NSRegularExpression(pattern: pattern)
                    let range = NSRange(value.startIndex..<value.endIndex, in: value)
                    let matches = regex.numberOfMatches(in: value, options: [], range: range)
                    return matches > 0
                } catch {
                    // print("Regex error: \(error.localizedDescription)")
                    return false
                }
            }
            return  false
        }
    }

    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 {
            for (index, child) in children.enumerated() {
                let indentation = String(repeating: " ", count: depth)
                let isMultiline = child.value?.contains("\n") ?? false
                let displayValue = isMultiline ? "[multiple lines]" : (child.value?.components(separatedBy: .newlines).first ?? "?")
                let ellipsedValue = displayValue.count > 50 ? String(displayValue.prefix(47)) + "..." : displayValue

                // Check if the current path matches the ancestry path
                let isOnIndexPath = currentPath + [index] == indexPath.prefix(currentPath.count + 1)
                let highlightIndicator = isOnIndexPath ? "→ " : "  "

                print(
                    "\(indentation)\(highlightIndicator)Role: \"\(child.role ?? "[undefined]")\", " +
                    "Subrole: \(child.subrole ?? "<nil>"), " +
                    "Id: \(id ?? "<nil>"), " +
                    "Title: \(child.rawTitle ?? "<nil>"), " +
                    "Value: \"\(ellipsedValue)\""
                )

                debugPrintSubtree(element: child, depth: depth + 1, highlight: indexPath, currentPath: currentPath + [index])
            }
        }
    }

    func debugPrintAncestors() {
        traverseUp { element in
            let title = element.rawTitle ?? "<nil>"
            let role = element.role ?? "<nil>"
            let subrole = element.subrole ?? "<nil>"
            print("Title: \(title), Role: \(role), Subrole: \(subrole)")
            return true // Continue traversing up
        }
    }

    func debugPrint() {
        let isMultiline = value?.contains("\n") ?? false
        let displayValue = isMultiline ? "[multiple lines]" : (value?.components(separatedBy: .newlines).first ?? "?")
        let ellipsedValue = displayValue.count > 50 ? String(displayValue.prefix(47)) + "..." : displayValue
        print(
            "Role: \(role ?? "<nil>"), " +
            "Subrole: \(subrole ?? "<nil>"), " +
            "Id: \(id ?? "<nil>"), " +
            "Title: \(rawTitle ?? "<nil>"), " +
            "Value: \"\(ellipsedValue)\""
        )
    }
}

enum AXUIElementNotification {
    case selectedTextChanged
    case focusedUIElementChanged
    case focusedWindowChanged
    case valueChanged
    case uknown

    static func notificationFrom(string notification: String) -> AXUIElementNotification {
        switch notification {
            case "AXSelectedTextChanged":
                return .selectedTextChanged
            case "AXFocusedUIElementChanged":
                return .focusedUIElementChanged
            case "AXFocusedWindowChanged":
                return .focusedWindowChanged
            case "AXValueChanged":
                return .valueChanged
            default:
                return .uknown
        }
    }
}


================================================
FILE: WakaTime/Extensions/BundleExtension.swift
================================================
import Foundation

extension Bundle {
    var displayName: String {
        readFromInfoDict(key: "CFBundleDisplayName") ?? "unknown"
    }

    var version: String {
        readFromInfoDict(key: "CFBundleShortVersionString") ?? "unknown"
    }

    var build: String {
        readFromInfoDict(key: "CFBundleVersion") ?? "unknown"
    }

    private func readFromInfoDict(key: String) -> String? {
        infoDictionary?[key] as? String
    }
}


================================================
FILE: WakaTime/Extensions/NSRunningApplicationExtension.swift
================================================
import Cocoa

extension NSRunningApplication {
    var monitoredApp: MonitoredApp? {
        guard let bundleId = bundleIdentifier else { return nil }

        return .init(from: bundleId)
    }
}


================================================
FILE: WakaTime/Extensions/OptionalExtension.swift
================================================
import Foundation

extension Optional where Wrapped: Collection {
    var isEmpty: Bool {
        self?.isEmpty ?? true
    }
}


================================================
FILE: WakaTime/Extensions/ProcessExtension.swift
================================================
import Foundation

extension Process {
    // Runs process.launch() prior to macOS 13 or process.run() on macOS 13 or newer.
    // Adds Swift exception handling to process.launch().
    func execute() throws {
        if #available(macOS 13.0, *) {
            // Use Process.run() on macOS 13 or newer. Process.run() throws Swift exceptions.
            try self.run()
        } else {
            // Note: Process.launch() can throw ObjC exceptions. For further reference, see
            // https://developer.apple.com/documentation/foundation/process/1414189-launch?changes=_3
            try ObjC.catchException { self.launch() }
        }
    }
}


================================================
FILE: WakaTime/Extensions/StringExtension.swift
================================================
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)
    }
}


================================================
FILE: WakaTime/Extensions/URLExtension.swift
================================================
import Foundation

extension URL {
    init?(stringWithoutScheme string: String) {
        if string.starts(with: "https?://") {
            self.init(string: string)
        } else {
            self.init(string: "https://\(string)")
        }
    }
}


================================================
FILE: WakaTime/Helpers/Accessibility.swift
================================================
import AppKit

class Accessibility {
    public static func requestA11yPermission() -> Bool {
        let prompt = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String
        let options: NSDictionary = [prompt: true]
        let appHasPermission = AXIsProcessTrustedWithOptions(options)
        return appHasPermission
    }
}


================================================
FILE: WakaTime/Helpers/AppInfo.swift
================================================
import Foundation
import Cocoa

class AppInfo {
    static func getAppName(bundleId: String) -> String? {
        let workspace = NSWorkspace.shared

        guard
            let appUrl = workspace.urlForApplication(withBundleIdentifier: bundleId),
            let appBundle = Bundle(url: appUrl)
        else { return nil }

        return appBundle.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
            ?? appBundle.object(forInfoDictionaryKey: "CFBundleName") as? String
    }

    static func getAppName(_ app: NSRunningApplication) -> String? {
        guard let bundleId = app.bundleIdentifier else { return nil }

        return getAppName(bundleId: bundleId)
    }

    static func getAppNameForHeartbeat(_ app: NSRunningApplication) -> String? {
        guard let appName = getAppName(app) else { return nil }
        return appName.filter { !$0.isWhitespace }
    }

    static func getIcon(file path: String) -> NSImage? {
        guard
            FileManager.default.fileExists(atPath: path)
        else { return nil }

        return NSWorkspace.shared.icon(forFile: path)
    }

    static func getIcon(bundleId: String) -> NSImage? {
        guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) else { return nil }

        return getIcon(file: url.absoluteURL.path)
    }
}


================================================
FILE: WakaTime/Helpers/Dependencies.swift
================================================
import Foundation

// swiftlint:disable force_unwrapping
// swiftlint:disable force_try
class Dependencies {
    public static var twelveHours = 43200

    public static func installDependencies() {
        Task {
            if !(await isCLILatest()) {
                downloadCLI()
            }
        }
    }

    public static var isLocalDevBuild: Bool {
        Bundle.main.version == "local-build"
    }

    public static func recentBrowserExtension() async -> String? {
        guard
            let apiKey = ConfigFile.getSetting(section: "settings", key: "api_key"),
            !apiKey.isEmpty
        else { return nil }
        let url = "https://api.wakatime.com/api/v1/users/current/user_agents?api_key=\(apiKey)"
        let request = URLRequest(url: URL(string: url)!, cachePolicy: .reloadIgnoringCacheData)
        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            guard
                let httpResponse = response as? HTTPURLResponse,
                httpResponse.statusCode == 200
            else { return nil }

            struct Resp: Decodable {
                let data: [UserAgent]
            }
            struct UserAgent: Decodable {
                let isBrowserExtension: Bool
                let editor: String?
                let lastSeenAt: String?
                enum CodingKeys: String, CodingKey {
                    case isBrowserExtension = "is_browser_extension"
                    case editor
                    case lastSeenAt = "last_seen_at"
                }
            }

            let release = try JSONDecoder().decode(Resp.self, from: data)
            let now = Date()
            for agent in release.data {
                guard
                    agent.isBrowserExtension,
                    let editor = agent.editor,
                    !editor.isEmpty,
                    let lastSeenAt = agent.lastSeenAt
                else { continue }

                let isoDateFormatter = ISO8601DateFormatter()
                isoDateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
                isoDateFormatter.formatOptions = [.withInternetDateTime]
                if let lastSeen = isoDateFormatter.date(from: lastSeenAt) {
                    if Int(now.timeIntervalSince(lastSeen)) > twelveHours {
                        break
                    }
                }

                return agent.editor
            }
        } catch {
            Logging.default.log("Request error checking for conflicting browser extension: \(error)")
            return nil
        }
        return nil
    }

    private static func getLatestVersion() async throws -> String? {
        struct Release: Decodable {
            let tagName: String
            private enum CodingKeys: String, CodingKey {
                case tagName = "tag_name"
            }
        }

        let apiUrl = "https://api.github.com/repos/wakatime/wakatime-cli/releases/latest"
        var request = URLRequest(url: URL(string: apiUrl)!, cachePolicy: .reloadIgnoringCacheData)
        let lastModified = ConfigFile.getSetting(section: "internal", key: "cli_version_last_modified", internalConfig: true)
        let currentVersion = ConfigFile.getSetting(section: "internal", key: "cli_version", internalConfig: true)
        if let lastModified, currentVersion != nil {
            request.setValue(lastModified, forHTTPHeaderField: "If-Modified-Since")
        }
        let (data, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse else { return nil }

        let now = Int(NSDate().timeIntervalSince1970)
        ConfigFile.setSetting(section: "internal", key: "cli_version_last_accessed", val: String(now), internalConfig: true)

        if httpResponse.statusCode == 304 {
            // Current version is still the latest version available
            return currentVersion
        } else if let lastModified = httpResponse.value(forHTTPHeaderField: "Last-Modified"),
                  let release = try? JSONDecoder().decode(Release.self, from: data) {
            // Remote version successfully decoded
            ConfigFile.setSetting(section: "internal", key: "cli_version_last_modified", val: lastModified, internalConfig: true)
            ConfigFile.setSetting(section: "internal", key: "cli_version", val: release.tagName, internalConfig: true)
            return release.tagName
        } else {
            // Unexpected response
            return nil
        }
    }

    private static func isCLILatest() async -> Bool {
        let cli = NSString.path(
            withComponents: ConfigFile.resourcesFolder + ["wakatime-cli"]
        )
        guard FileManager.default.fileExists(atPath: cli) else { return false }

        let outputPipe = Pipe()
        let process = Process()
        process.launchPath = cli
        process.arguments = ["--version"]
        process.standardOutput = outputPipe
        process.standardError = FileHandle.nullDevice
        do {
            try process.run()
        } catch {
            // Error running CLI process
            return false
        }
        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)),
           let range = Range(match.range, in: output) {
            version = String(output[range])
        } else {
            version = nil
        }

        let accessed = ConfigFile.getSetting(section: "internal", key: "cli_version_last_accessed", internalConfig: true)
        if let accessed, let accessed = Int(accessed) {
            let now = Int(NSDate().timeIntervalSince1970)
            let fourHours = 4 * 3600
            if accessed + fourHours > now {
                Logging.default.log("Skip checking for wakatime-cli updates because recently checked \(now - accessed) seconds ago")
                return true
            }
        }

        let remoteVersion = try? await getLatestVersion()
        guard let remoteVersion else {
            // Could not retrieve remote version
            return true
        }
        if let version, "v" + version == remoteVersion {
            // Local version up to date
            return true
        } else {
            // Newer version available
            return false
        }
    }

    private static func downloadCLI() {
        let dir = NSString.path(withComponents: ConfigFile.resourcesFolder)
        if !FileManager.default.fileExists(atPath: dir) {
            do {
                try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true, attributes: nil)
            } catch {
                Logging.default.log(error.localizedDescription)
            }
        }

        let url = "https://github.com/wakatime/wakatime-cli/releases/latest/download/wakatime-cli-darwin-\(architecture()).zip"
        let zipFile = NSString.path(withComponents: ConfigFile.resourcesFolder + ["wakatime-cli.zip"])
        let cli = NSString.path(withComponents: ConfigFile.resourcesFolder + ["wakatime-cli"])
        let cliReal = NSString.path(withComponents: ConfigFile.resourcesFolder + ["wakatime-cli-darwin-\(architecture())"])

        if FileManager.default.fileExists(atPath: zipFile) {
            do {
                try FileManager.default.removeItem(atPath: zipFile)
            } catch {
                Logging.default.log(error.localizedDescription)
                return
            }
        }

        URLSession.shared.downloadTask(with: URLRequest(url: URL(string: url)!)) { fileUrl, _, _ in
            guard let fileUrl else { return }

            do {
                // download wakatime-cli.zip
                try FileManager.default.moveItem(at: fileUrl, to: URL(fileURLWithPath: zipFile))

                if FileManager.default.fileExists(atPath: cliReal) {
                    do {
                        try FileManager.default.removeItem(atPath: cliReal)
                    } catch {
                        Logging.default.log(error.localizedDescription)
                        return
                    }
                }

                // unzip wakatime-cli.zip
                let process = Process()
                process.launchPath = "/usr/bin/unzip"
                process.arguments = [zipFile, "-d", dir]
                process.standardOutput = FileHandle.nullDevice
                process.standardError = FileHandle.nullDevice
                process.launch()
                process.waitUntilExit()

                // cleanup wakatime-cli.zip
                try! FileManager.default.removeItem(atPath: zipFile)

                // create ~/.wakatime/wakatime-cli symlink
                do {
                    try FileManager.default.removeItem(atPath: cli)
                } catch { }
                try! FileManager.default.createSymbolicLink(atPath: cli, withDestinationPath: cliReal)

            } catch {
                Logging.default.log(error.localizedDescription)
            }
        }.resume()
    }

    private static func architecture() -> String {
        var systeminfo = utsname()
        uname(&systeminfo)
        let machine = withUnsafeBytes(of: &systeminfo.machine) {bufPtr -> String in
            let data = Data(bufPtr)
            if let lastIndex = data.lastIndex(where: { $0 != 0 }) {
                return String(data: data[0...lastIndex], encoding: .isoLatin1)!
            } else {
                return String(data: data, encoding: .isoLatin1)!
            }
        }
        if machine == "x86_64" {
            return "amd64"
        }
        return "arm64"
    }
}
// swiftlint:enable force_unwrapping
// swiftlint:enable force_try


================================================
FILE: WakaTime/Helpers/EventSourceObserver.swift
================================================
import CoreGraphics

class EventSourceObserver {
    let pollIntervalInSeconds: CFTimeInterval
    var timer: Timer = Timer(timeInterval: 1, repeats: false) { _ in }

    init(pollIntervalInSeconds: CFTimeInterval) {
        self.pollIntervalInSeconds = pollIntervalInSeconds
        timer.invalidate()
    }

    func start(activityDetected: @escaping () -> Void) {
        stop()
        timer = Timer.scheduledTimer(withTimeInterval: pollIntervalInSeconds, repeats: true) { [self] _ in
            let secondsSinceLastKeyPress = Self.checkForKeyPresses()
            let secondsSinceLastMouseMoved = Self.checkForMouseActivity()
            if secondsSinceLastKeyPress < pollIntervalInSeconds || secondsSinceLastMouseMoved < pollIntervalInSeconds {
                activityDetected()
            }
        }
    }

    func stop() {
        timer.invalidate()
    }

    static private func checkForKeyPresses() -> CFTimeInterval {
        CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: .keyDown)
    }

    static private func checkForMouseActivity() -> CFTimeInterval {
        CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: .mouseMoved)
    }
}


================================================
FILE: WakaTime/Helpers/FilterManager.swift
================================================
import Cocoa

class FilterManager {
    static func filterBrowsedSites(_ url: String) -> Bool {
        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

        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
        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) }
    }
}


================================================
FILE: WakaTime/Helpers/MonitoringManager.swift
================================================
import Cocoa
import Foundation

class MonitoringManager {
    enum MonitoringState {
        case on
        case off
    }

    static func isAppMonitored(for bundleId: String) -> Bool {
        allMonitoredApps.contains(bundleId)
    }

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

        return isAppMonitored(for: bundleId)
    }

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

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

        return isAppElectron(for: bundleId)
    }

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

        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

        guard
            let activeWindow = AXUIElementCreateApplication(pid).activeWindow,
            let entity = entity(for: app, activeWindow),
            let entityUnwrapped = entity.0
        else { return nil }

        let project = project(for: app, activeWindow)
        var language = language(for: app, activeWindow)
        if project != nil && language == nil {
            language = "<<LAST_LANGUAGE>>"
        }

        let heartbeat = HeartbeatData(
            entity: entityUnwrapped,
            entityType: entity.1,
            project: project,
            language: language,
            category: category(for: app, activeWindow)
        )
        return heartbeat
    }

    static var isMonitoringBrowsing: Bool {
        for bundleId in MonitoredApp.browserAppIds {
            guard
                AppInfo.getAppName(bundleId: bundleId) != nil,
                isAppMonitored(for: bundleId)
            else { continue }

            return true
        }
        return false
    }

    static var allMonitoredApps: [String] {
        if let bundleIds = UserDefaults.standard.stringArray(forKey: monitoringKey) {
            return bundleIds.filter { MonitoredApp.pluginAppIds[$0] == nil }
        } else {
            var bundleIds: [String] = []
            let defaults = UserDefaults.standard.dictionaryRepresentation()
            for key in defaults.keys {
                if key.starts(with: "is_") && key.contains("_monitored") {
                    if UserDefaults.standard.bool(forKey: key) {
                        let bundleId = key.replacingOccurrences(of: "is_", with: "").replacingOccurrences(of: "_monitored", with: "")
                        bundleIds.append(bundleId)
                    }
                    UserDefaults.standard.removeObject(forKey: key)
                }
            }
            UserDefaults.standard.set(bundleIds, forKey: monitoringKey)
            UserDefaults.standard.synchronize()
            return bundleIds.filter { MonitoredApp.pluginAppIds[$0] == nil }
        }
    }

    static func set(monitoringState: MonitoringState, for bundleId: String) {
        if monitoringState == .on {
            UserDefaults.standard.set(Array(Set(allMonitoredApps + [bundleId])), forKey: monitoringKey)
        } else {
            let apps = allMonitoredApps.filter { $0 != bundleId }
            UserDefaults.standard.set(apps, forKey: monitoringKey)
        }
        UserDefaults.standard.synchronize()
    }

    static func enableByDefault(_ bundleId: String) {
        if AppInfo.getIcon(bundleId: bundleId) != nil && AppInfo.getAppName(bundleId: bundleId) != nil {
            MonitoringManager.set(monitoringState: .on, for: bundleId)
        }
        let setAppId = bundleId.appending("-setapp")
        if AppInfo.getIcon(bundleId: setAppId) != nil && AppInfo.getAppName(bundleId: setAppId) != nil {
            MonitoringManager.set(monitoringState: .on, for: setAppId)
        }
    }

    static var monitoringKey = "wakatime_monitored_apps"

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

            guard PropertiesManager.domainPreference == .domain else { return (url, .url) }

            return (domainFromUrl(url), .domain)
        }

        guard let monitoredApp = app.monitoredApp else { return (title(for: app, element), .app) }

        switch monitoredApp {
            case .canva:
                // Canva obviously implements tabs in a different way than the tab content UI.
                // Due to this circumstance, it's possible to just sample an element from the
                // Canva window which is positioned underneath the tab bar and trace to the
                // web area root which appears to be properly titled. All the UI zoom settings
                // in Canva only change the tab content or sub content of the tab content, hence
                // this should be relatively safe. In cases where this fails, nil should be
                // returned as a consequence of the web area not being found.
                let someElem = element.elementAtPositionRelativeToWindow(x: 10, y: 60)
                let webArea = someElem?.firstAncestorWhere { $0.role == "AXWebArea" }
                return (webArea?.rawTitle, .app)
            case .notes:
                let skipHighCostElements: (AXUIElement) -> Bool = { element in
                    guard let role = element.role else { return false }

                    // If we don't skip them, Notes app itself costs a lot of CPU time to create AXUIElement for us to traverse.
                    // AXOutline -> Folder Sidebar of Notes
                    // AXTable   -> List View of items of selected folder. It may contain thousands of rows, and
                    // each row may have text and image element.
                    if role == "AXOutline" || role == "AXTable" {
                        return true
                    }

                    return false
                }
                // 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 = element.firstDescendantWhere({ $0.role == "AXWebArea" }, skipDescendantsWhere: skipHighCostElements )
                if let webAreaElement {
                    // WebView-based implementation
                    let titleElement = webAreaElement.firstDescendantWhere { $0.role == kAXStaticTextRole }
                    return (titleElement?.value, .app)
                } else {
                    // ICTK2MacTextView
                    let textAreaElement = element.firstDescendantWhere({
                        $0.role == kAXTextAreaRole
                    }, skipDescendantsWhere: skipHighCostElements)
                    if let value = textAreaElement?.value {
                        let title = extractPrefix(value, separator: "\n")
                        return (title, .app)
                    }
                    return nil
                }
            default:
                return (title(for: app, element), .app)
        }
    }

    // swiftlint:disable cyclomatic_complexity
    static func title(for app: NSRunningApplication, _ element: AXUIElement) -> String? {
        guard let monitoredApp = app.monitoredApp else {
            return extractPrefix(element.rawTitle)
        }

        switch monitoredApp {
            case .adobeaftereffect:
                return extractPrefix(element.rawTitle)
            case .adobebridge:
                return extractPrefix(element.rawTitle)
            case .adobeillustrator:
                return extractPrefix(element.rawTitle)
            case .adobemediaencoder:
                return extractPrefix(element.rawTitle)
            case .adobephotoshop:
                return extractPrefix(element.rawTitle)
            case .adobepremierepro:
                return extractPrefix(element.rawTitle)
            case .arcbrowser:
                fatalError("\(monitoredApp.rawValue) should never use window title as entity")
            case .beeper:
                return extractPrefix(element.rawTitle)
            case .brave:
                fatalError("\(monitoredApp.rawValue) should never use window title as entity")
            case .canva:
                fatalError("\(monitoredApp.rawValue) should never use window title as entity")
            case .chrome, .chromebeta, .chromecanary:
                fatalError("\(monitoredApp.rawValue) should never use window title as entity")
            case .figma:
                guard
                    let title = extractPrefix(element.rawTitle, separator: " – "),
                    title != "Figma",
                    title != "Drafts"
                else { return nil }
                return title
            case .firefox:
                fatalError("\(monitoredApp.rawValue) should never use window title as entity")
            case .github:
                return extractPrefix(element.rawTitle, separator: " - ")
            case .imessage:
                return extractPrefix(element.rawTitle, separator: " - ")
            case .inkscape:
                return extractPrefix(element.rawTitle)
            case .iterm2:
                return extractPrefix(element.rawTitle, separator: " - ")
            case .linear:
                return extractPrefix(element.rawTitle, separator: " - ")
            case .miro:
                return extractSuffix(element.rawTitle)
            case .notes:
                fatalError("\(monitoredApp.rawValue) should never use window title as entity")
            case .notion:
                return extractPrefix(element.rawTitle, separator: " - ")
            case .postman:
                guard
                    let title = extractPrefix(element.rawTitle, separator: " - ", fullTitle: true),
                    title != "Postman"
                else { return nil }
                return title
            case .rocketchat:
                return extractPrefix(element.rawTitle)
            case .slack:
                return extractPrefix(element.rawTitle, separator: " - ")
            case .safari:
                fatalError("\(monitoredApp.rawValue) should never use window title as entity")
            case .safaripreview:
                fatalError("\(monitoredApp.rawValue) should never use window title as entity")
            case .tableplus:
                return extractPrefix(element.rawTitle, separator: " - ")
            case .terminal:
                return extractPrefix(element.rawTitle, separator: " - ")
            case .warp:
                guard
                    let title = extractPrefix(element.rawTitle, separator: " - "),
                    title != "Warp"
                else { return nil }
                return title
            case .wecom:
                return extractPrefix(element.rawTitle, separator: " - ")
            case .whatsapp:
                return extractPrefix(element.rawTitle, separator: " - ")
            case .xcode:
                fatalError("\(monitoredApp.rawValue) should never use window title as entity")
            case .zoom:
                return extractPrefix(element.rawTitle, separator: " - ")
            case .zed:
                return extractPrefix(element.rawTitle, separator: " — ")
        }
    }

    static func category(for app: NSRunningApplication, _ element: AXUIElement) -> Category {
        guard let monitoredApp = app.monitoredApp else { return .coding }

        if isAppBrowser(app) {
            guard let url = currentBrowserUrl(for: app, element) else { return .browsing }
            return category(from: url)
        }

        switch monitoredApp {
            case .adobeaftereffect:
                return .designing
            case .adobebridge:
                return .designing
            case .adobeillustrator:
                return .designing
            case .adobemediaencoder:
                return .designing
            case .adobephotoshop:
                return .designing
            case .adobepremierepro:
                return .designing
            case .arcbrowser:
                return .browsing
            case .beeper:
                return .communicating
            case .brave:
                return .browsing
            case .canva:
                return .designing
            case .chrome, .chromebeta, .chromecanary:
                return .browsing
            case .figma:
                return .designing
            case .firefox:
                return .browsing
            case .github:
                return .codereviewing
            case .imessage:
                return .communicating
            case .inkscape:
                return .designing
            case .iterm2:
                return .coding
            case .linear:
                return .planning
            case .miro:
                return .planning
            case .notes:
                return .writingdocs
            case .notion:
                return .writingdocs
            case .postman:
                return .debugging
            case .rocketchat:
                return .communicating
            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("\(monitoredApp.rawValue) should never use window title")
            case .zoom:
                return .meeting
            case .zed:
                return .coding
        }
    }
    // swiftlint:enable cyclomatic_complexity

    static func category(from url: String) -> Category {
        let patterns = [
            "github.com/[^/]+/[^/]+/pull/.*$",
            "gitlab.com/[^/]+/[^/]+/[^/]+/merge_requests/.*$",
            "bitbucket.org/[^/]+/[^/]+/pull-requests/.*$",
        ]

        for pattern in patterns {
            do {
                let regex = try NSRegularExpression(pattern: pattern)
                let nsrange = NSRange(url.startIndex..<url.endIndex, in: url)
                if regex.firstMatch(in: url, options: [], range: nsrange) != nil {
                    return .codereviewing
                }
            } catch {
                Logging.default.log("Regex error: \(error)")
                continue
            }
        }

        return .coding
    }

    static func project(for app: NSRunningApplication, _ element: AXUIElement) -> String? {
        guard let monitoredApp = app.monitoredApp else {
            guard let url = currentBrowserUrl(for: app, element) else { return nil }
            return project(from: url)
        }

        // TODO: detect repo from GitHub Desktop Client if possible
        switch monitoredApp {
            case .slack:
                return extractSuffix(element.rawTitle, separator: " - ", offset: 1)
            case .zed:
                return extractSuffix(element.rawTitle, separator: " — ")
            default:
                guard let url = currentBrowserUrl(for: app, element) else { return nil }
                return project(from: url)
        }
    }

    struct Pattern {
        var expression: String
        var group: Int
    }

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

        for pattern in patterns {
            do {
                let regex = try NSRegularExpression(pattern: pattern.expression)
                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 range = match.range(at: pattern.group)

                    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
    }

    static func language(for app: NSRunningApplication, _ element: AXUIElement) -> String? {
        guard let monitoredApp = app.monitoredApp else { return nil }

        switch monitoredApp {
            case .canva:
                return "Image (svg)"
            case .chrome, .chromebeta, .chromecanary:
                do {
                    guard let url = currentBrowserUrl(for: app, element) else { return nil }

                    let regex = try NSRegularExpression(pattern: "github.com/[^/]+/[^/]+/?$")
                    let nsrange = NSRange(url.startIndex..<url.endIndex, in: url)
                    if regex.firstMatch(in: url, options: [], range: nsrange) != nil {
                        let languages = element.firstDescendantWhere { $0.role == "AXStaticText" && $0.value == "Languages" }
                        guard let languages = languages else { return nil }

                        guard let wrapper = languages.parent?.parent else { return nil }

                        let langList = wrapper.firstDescendantWhere { $0.role == "AXList" }
                        guard let langList = langList else { return nil }

                        let link = langList.firstDescendantWhere { $0.role == "AXLink" }
                        guard let link = link else { return nil }

                        let lang = link.firstDescendantWhere { $0.role == "AXStaticText" }
                        guard let lang = lang else { return nil }

                        return lang.value
                    }

                    return nil
                } catch {
                    Logging.default.log("Error parsing language from browser: \(error)")
                    return nil
                }
            case .figma:
                return "Image (svg)"
            case .inkscape:
                return "Image (svg)"
            case .postman:
                return "HTTP Request"
            default:
                return nil
        }
    }

    static func currentBrowserUrl(for app: NSRunningApplication, _ element: AXUIElement) -> String? {
        guard let monitoredApp = app.monitoredApp else { return nil }

        var address: String?
        switch monitoredApp {
            case .arcbrowser:
                let addressField = element.findAddressField()
                address = addressField?.value
            case .brave:
                let addressField = element.findAddressField()
                address = addressField?.value
            case .chrome, .chromebeta, .chromecanary:
                let addressField = element.findAddressField()
                address = addressField?.value
            case .firefox:
                let addressField = element.findAddressField()
                address = addressField?.value
            case .linear:
                let projectLabel = element.firstDescendantWhere { $0.value == "Project" }
                let projectButton = projectLabel?.nextSibling?.firstDescendantWhere { $0.role == kAXButtonRole }
                return projectButton?.rawTitle
            case .safari:
                let addressField = element.elementById(identifier: "WEB_BROWSER_ADDRESS_AND_SEARCH_FIELD")
                address = addressField?.value
            case .safaripreview:
                let addressField = element.elementById(identifier: "WEB_BROWSER_ADDRESS_AND_SEARCH_FIELD")
                address = addressField?.value
            default: return nil
        }
        return address
    }

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

        guard let separator = separator else {
            return getFirstPrefixMatch(str)
        }

        let parts = str.components(separatedBy: separator)
        guard !parts.isEmpty else { return nil }
        guard let item = parts.first else { return nil }

        if let minCount = minCount, minCount > 0, parts.count < minCount {
            return nil
        }

        if item.trimmingCharacters(in: .whitespacesAndNewlines) != "" {
            if fullTitle {
                return str.trimmingCharacters(in: .whitespacesAndNewlines)
            }
            return item.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        return nil
    }

    static func extractSuffix(_ str: String?, separator: String? = nil, offset: Int = 0) -> String? {
        guard let str = str else { return nil }

        guard let separator = separator else {
            return getFirstSuffixMatch(str)
        }

        var parts = str.components(separatedBy: separator)
        guard !parts.isEmpty else { return nil }
        guard parts.count > 1 else { return nil }

        var i = offset
        while i > 0 {
            guard parts.count > 1 else { return nil }

            parts.removeLast()
            i += 1
        }
        guard let item = parts.last else { return nil }

        if item.trimmingCharacters(in: .whitespacesAndNewlines) != "" {
            return item.trimmingCharacters(in: .whitespacesAndNewlines)
        }

        return nil
    }

    static 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)"
    }

    static let separators = [
        "-",
        "᠆",
        "‐",
        "‑",
        "‒",
        "–",
        "—",
        "―",
        "⸺",
        "⸻",
        "︱",
        "︲",
        "﹘",
        "﹣",
        "-",
    ]

    static func getFirstPrefixMatch(_ str: String) -> String {
        guard !str.isEmpty else { return str.trimmingCharacters(in: .whitespacesAndNewlines) }

        for separator in separators {
            let parts = str.components(separatedBy: separator)
            guard parts.count > 1 else { continue }
            guard let item = parts.first else { continue }

            let trimmed = item.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !trimmed.isEmpty else { continue }

            return trimmed
        }

        return str.trimmingCharacters(in: .whitespacesAndNewlines)
    }

    static func getFirstSuffixMatch(_ str: String) -> String {
        guard !str.isEmpty else { return str.trimmingCharacters(in: .whitespacesAndNewlines) }

        for separator in separators {
            let parts = str.components(separatedBy: separator)
            guard parts.count > 1 else { continue }
            guard let item = parts.last else { continue }

            let trimmed = item.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !trimmed.isEmpty else { continue }

            return trimmed
        }

        return str.trimmingCharacters(in: .whitespacesAndNewlines)
    }
}

struct HeartbeatData {
    var entity: String
    var entityType: EntityType
    var project: String?
    var language: String?
    var category: Category?
}


================================================
FILE: WakaTime/Helpers/PropertiesManager.swift
================================================
import Foundation

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

    enum FilterType: String {
        case denylist
        case allowlist
    }

    enum Keys: String {
        case shouldLaunchOnLogin = "launch_on_login"
        case shouldLogToFile = "log_to_file"
        case shouldRequestA11y = "request_a11y"
        case shouldAutomaticallyDownloadUpdates = "should_automatically_download_updates"
        case hasLaunchedBefore = "has_launched_before"
        case shouldDisplayTodayInStatusBar = "status_bar_text"
        case domainPreference = "domain_preference"
        case filterType = "filter_type"
        case denylist = "denylist"
        case allowlist = "allowlist"
    }

    static var shouldLaunchOnLogin: Bool {
        get {
            guard UserDefaults.standard.string(forKey: Keys.shouldLaunchOnLogin.rawValue) != nil else {
                UserDefaults.standard.set(true, forKey: Keys.shouldLaunchOnLogin.rawValue)
                return true
            }

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

    static var shouldLogToFile: Bool {
        get {
            guard UserDefaults.standard.string(forKey: Keys.shouldLogToFile.rawValue) != nil else {
                UserDefaults.standard.set(false, forKey: Keys.shouldLogToFile.rawValue)
                return false
            }

            return UserDefaults.standard.bool(forKey: Keys.shouldLogToFile.rawValue)
        }
        set {
            UserDefaults.standard.set(newValue, forKey: Keys.shouldLogToFile.rawValue)
            UserDefaults.standard.synchronize()
            if newValue {
                Logging.default.activateLoggingToFile()
            } else {
                Logging.default.deactivateLoggingToFile()
            }
        }
    }

    static var shouldAutomaticallyDownloadUpdates: Bool {
        get {
            guard UserDefaults.standard.string(forKey: Keys.shouldAutomaticallyDownloadUpdates.rawValue) != nil else {
                UserDefaults.standard.set(true, forKey: Keys.shouldAutomaticallyDownloadUpdates.rawValue)
                return true
            }

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

    static var shouldRequestA11yPermission: Bool {
        get {
            guard UserDefaults.standard.string(forKey: Keys.shouldRequestA11y.rawValue) != nil else {
                UserDefaults.standard.set(true, forKey: Keys.shouldRequestA11y.rawValue)
                return true
            }

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

    static var shouldDisplayTodayInStatusBar: Bool {
        get {
            guard UserDefaults.standard.string(forKey: Keys.shouldDisplayTodayInStatusBar.rawValue) != nil else {
                UserDefaults.standard.set(true, forKey: Keys.shouldDisplayTodayInStatusBar.rawValue)
                return true
            }

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

    static var hasLaunchedBefore: Bool {
        get {
            guard UserDefaults.standard.string(forKey: Keys.hasLaunchedBefore.rawValue) != nil else {
                return false
            }

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

    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 {
                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
        }
    }
}


================================================
FILE: WakaTime/Helpers/SettingsManager.swift
================================================
import Foundation
import ServiceManagement

class SettingsManager {
#if !SIMULATE_OLD_MACOS
    static let simulateOldMacOS = false
#else
    static let simulateOldMacOS = true
#endif

    static func loginItemRegistered() -> Bool {
        if #available(macOS 13.0, *) {
            return SMAppService.mainApp.status != .notFound
        } else {
            return false
        }
    }

    static func shouldRegisterAsLoginItem() -> Bool {
        guard
            !loginItemRegistered(),
            PropertiesManager.shouldLaunchOnLogin
        else { return false }

        return !Dependencies.isLocalDevBuild
    }

    static func registerAsLoginItem() {
        PropertiesManager.shouldLaunchOnLogin = true

        // Use SMAppService on macOS 13 or newer to add WakaTime to the "Open at Login" list and SMLoginItemSetEnabled
        // for older versions of macOS to add WakaTime to the "Allow in Background" list
        if #available(macOS 13.0, *), !simulateOldMacOS {
            do {
                try SMAppService.mainApp.register()
                Logging.default.log("Registered for login")
            } catch let error {
                Logging.default.log(error.localizedDescription)
            }
        } else {
            if SMLoginItemSetEnabled("macos-wakatime.WakaTimeHelper" as CFString, true) {
                Logging.default.log("Login item enabled successfully.")
            } else {
                Logging.default.log("Failed to enable login item.")
            }
        }
    }

    static func unregisterAsLoginItem() {
        PropertiesManager.shouldLaunchOnLogin = false

        if #available(macOS 13.0, *), !simulateOldMacOS {
            do {
                try SMAppService.mainApp.unregister()
                Logging.default.log("Unregistered for login")
            } catch let error {
                Logging.default.log(error.localizedDescription)
            }
        } else {
            if SMLoginItemSetEnabled("macos-wakatime.WakaTimeHelper" as CFString, false) {
                Logging.default.log("Login item disabled successfully.")
            } else {
                Logging.default.log("Failed to disable login item.")
            }
        }
    }
}


================================================
FILE: WakaTime/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "16.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "16x16"
    },
    {
      "filename" : "32.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "16x16"
    },
    {
      "filename" : "32.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "32x32"
    },
    {
      "filename" : "64.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "32x32"
    },
    {
      "filename" : "128.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "128x128"
    },
    {
      "filename" : "256.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "128x128"
    },
    {
      "filename" : "256.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "256x256"
    },
    {
      "filename" : "512.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "256x256"
    },
    {
      "filename" : "512.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "512x512"
    },
    {
      "filename" : "1024.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "512x512"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: WakaTime/Resources/Assets.xcassets/Contents.json
================================================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: WakaTime/Resources/Assets.xcassets/WakaTime.imageset/Contents.json
================================================
{
  "images" : [
    {
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "filename" : "32.png",
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "filename" : "64.png",
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  },
  "properties" : {
    "template-rendering-intent" : "template"
  }
}


================================================
FILE: WakaTime/Resources/Assets.xcassets/WakaTimeDisabled.imageset/Contents.json
================================================
{
  "images" : [
    {
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "filename" : "32.png",
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "filename" : "64.png",
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  },
  "properties" : {
    "template-rendering-intent" : "template"
  }
}


================================================
FILE: WakaTime/Resources/GoogleService-Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CLIENT_ID</key>
	<string>473173879994-q78fldrplnkhrr4oa5h10mkv15g1ng2g.apps.googleusercontent.com</string>
	<key>REVERSED_CLIENT_ID</key>
	<string>com.googleusercontent.apps.473173879994-q78fldrplnkhrr4oa5h10mkv15g1ng2g</string>
	<key>API_KEY</key>
	<string>AIzaSyDBdPD7ZIMm7XtDueuhOBd-rx7kF3jIc0U</string>
	<key>GCM_SENDER_ID</key>
	<string>473173879994</string>
	<key>PLIST_VERSION</key>
	<string>1</string>
	<key>BUNDLE_ID</key>
	<string>macos-wakatime.WakaTime</string>
	<key>PROJECT_ID</key>
	<string>wakatime-macos-desktop-app</string>
	<key>STORAGE_BUCKET</key>
	<string>wakatime-macos-desktop-app.appspot.com</string>
	<key>IS_ADS_ENABLED</key>
	<false></false>
	<key>IS_ANALYTICS_ENABLED</key>
	<false></false>
	<key>IS_APPINVITE_ENABLED</key>
	<true></true>
	<key>IS_GCM_ENABLED</key>
	<true></true>
	<key>IS_SIGNIN_ENABLED</key>
	<true></true>
	<key>GOOGLE_APP_ID</key>
	<string>1:473173879994:ios:c9a7680a9e365351282683</string>
</dict>
</plist>

================================================
FILE: WakaTime/Utils/Atomic.swift
================================================
import Foundation

@propertyWrapper
struct Atomic<Value> {
    private var value: Value
    private let lock = NSLock()

    init(wrappedValue value: Value) {
        self.value = value
    }

    var wrappedValue: Value {
      get { getValue() }
      set { setValue(newValue) }
    }

    func getValue() -> Value {
        lock.lock()
        defer { lock.unlock() }
        return value
    }

    mutating func setValue(_ newValue: Value) {
        lock.lock()
        defer { lock.unlock() }
        value = newValue
    }
}


================================================
FILE: WakaTime/Utils/Logging.swift
================================================
import Foundation
import os.log

class Logging {
    static let `default` = Logging()
    private var filePath: String?

    private init() {}

    // Configures logging to also write to a file at the given path.
    func configure(filePath: String) {
        self.filePath = filePath
    }

    func activateLoggingToFile() {
        let userHome = FileManager.default.homeDirectoryForCurrentUser.pathComponents
        let logFilePath = NSString.path(withComponents: userHome + [".wakatime", "macos-wakatime.log"])
        configure(filePath: logFilePath)
    }

    func deactivateLoggingToFile() {
        filePath = nil
    }

    func log(_ message: String, type: OSLogType = .default) {
        os_log("%{public}@", log: .default, type: type, message)

        if let filePath {
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
            let timestamp = dateFormatter.string(from: Date())
            let logMessage = "\(timestamp): \(message)\n"

            // Attempt to append the log message to the log file
            if let fileHandle = FileHandle(forWritingAtPath: filePath) {
                fileHandle.seekToEndOfFile()
                if let data = logMessage.data(using: .utf8) {
                    fileHandle.write(data)
                }
                fileHandle.closeFile()
            } else {
                // If the file does not exist, create it
                try? logMessage.write(toFile: filePath, atomically: true, encoding: .utf8)
            }
        }
    }
}


================================================
FILE: WakaTime/Utils/ObjC.h
================================================
#ifndef ObjC_h
#define ObjC_h

#import <Foundation/Foundation.h>

@interface ObjC : NSObject

+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error;

@end

#endif /* ObjC_h */


================================================
FILE: WakaTime/Utils/ObjC.m
================================================
#import <Foundation/Foundation.h>

#import "ObjC.h"

@implementation ObjC

+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
    @try {
        tryBlock();
        return YES;
    }
    @catch (NSException *exception) {
        *error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:exception.userInfo];
        return NO;
    }
}

@end


================================================
FILE: WakaTime/Views/MonitoredAppsView.swift
================================================
import AppKit

class MonitoredAppsView: NSView, NSOutlineViewDataSource, NSOutlineViewDelegate {
    struct AppData: Equatable {
        let bundleId: String
        let icon: NSImage
        let name: String
        let tag: Int
    }

    private var outlineView: NSOutlineView!
    private var runningApps: [AppData] = []

    private func refreshRunningApps() {
        var apps = [AppData]()
        let bundleIds = sort(Array(Set(MonitoredApp.allBundleIds + getRunningApps() + MonitoringManager.allMonitoredApps)))
        var index = 0
        for bundleId in bundleIds {
            if let icon = AppInfo.getIcon(bundleId: bundleId),
               let name = AppInfo.getAppName(bundleId: bundleId) {
                apps.append(AppData(bundleId: bundleId, icon: icon, name: name, tag: index))
                index += 1
            }

            let setAppBundleId = bundleId.appending("-setapp")
            if let icon = AppInfo.getIcon(bundleId: setAppBundleId),
               let name = AppInfo.getAppName(bundleId: setAppBundleId) {
                apps.append(AppData(bundleId: setAppBundleId, icon: icon, name: name, tag: index))
                index += 1
            }
        }
        runningApps = apps
    }

    private func getRunningApps() -> [String] {
        var ids: [String] = []
        for runningApp in NSWorkspace.shared.runningApplications where runningApp.activationPolicy == .regular {
            guard let id = runningApp.bundleIdentifier else { continue }

            let bundleId = id.replacingOccurrences(of: "-setapp$", with: "", options: .regularExpression)

            guard
                !MonitoredApp.unsupportedAppIds.contains(where: { $0 == bundleId }),
                !MonitoredApp.allBundleIds.contains(where: { $0 == bundleId })
            else { continue }

            ids.append(bundleId)
        }
        return ids
    }

    private func sort(_ bundleIds: [String]) -> [String] {
        bundleIds.sorted {
            let left = AppInfo.getAppName(bundleId: $0) ?? $0
            let right = AppInfo.getAppName(bundleId: $1) ?? $1
            return left.localizedCaseInsensitiveCompare(right) == ComparisonResult.orderedAscending
        }
    }

    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)

        setupOutlineView()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupOutlineView() {
        let scrollView = NSScrollView()
        scrollView.hasVerticalScroller = true
        outlineView = NSOutlineView()
        outlineView.dataSource = self
        outlineView.delegate = self

        scrollView.documentView = outlineView
        addSubview(scrollView)

        scrollView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
            scrollView.topAnchor.constraint(equalTo: topAnchor),
            scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])

        let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("AppColumn"))
        outlineView.addTableColumn(column)
        outlineView.headerView = nil
        outlineView.outlineTableColumn = column
        outlineView.indentationPerLevel = 0.0
    }

    func reloadData() {
        refreshRunningApps()
        outlineView.reloadData()
    }

    // MARK: NSOutlineViewDataSource

    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        runningApps.count
    }

    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        false
    }

    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        runningApps[index]
    }

    // MARK: NSOutlineViewDelegate

    func outlineView(_ outlineView: NSOutlineView, shouldSelectItem item: Any) -> Bool {
        false
    }

    func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
        50
    }

    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
        guard let appData = item as? AppData else { return nil }

        let cellView = outlineView.makeView(
          withIdentifier: NSUserInterfaceItemIdentifier("AppCell"),
          owner: self
        ) as? NSTableCellView ?? NSTableCellView()

        // Clear existing subviews to prevent duplication
        cellView.subviews.forEach { $0.removeFromSuperview() }

        let imageView = NSImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.image = appData.icon
        imageView.image?.size = NSSize(width: 20, height: 20)

        let nameLabel = NSTextField(labelWithString: appData.name)
        nameLabel.translatesAutoresizingMaskIntoConstraints = false

        let action = switchOrLink(appData)

        cellView.addSubview(imageView)
        cellView.addSubview(nameLabel)
        cellView.addSubview(action)

        // Determine if the current item is the last in the list
        let isLastItem = runningApps.last == appData

        if !isLastItem {
            let divider = NSView()
            divider.translatesAutoresizingMaskIntoConstraints = false
            divider.wantsLayer = true
            divider.layer?.backgroundColor = NSColor.separatorColor.cgColor

            cellView.addSubview(divider)

            NSLayoutConstraint.activate([
                divider.heightAnchor.constraint(equalToConstant: 1),
                divider.leadingAnchor.constraint(equalTo: cellView.leadingAnchor),
                divider.trailingAnchor.constraint(equalTo: cellView.trailingAnchor),
                divider.bottomAnchor.constraint(equalTo: cellView.bottomAnchor)
            ])
        }

        NSLayoutConstraint.activate([
            imageView.leadingAnchor.constraint(equalTo: cellView.leadingAnchor, constant: 5),
            imageView.centerYAnchor.constraint(equalTo: cellView.centerYAnchor),
            imageView.widthAnchor.constraint(equalToConstant: 20),
            imageView.heightAnchor.constraint(equalToConstant: 20),

            nameLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10),
            nameLabel.centerYAnchor.constraint(equalTo: cellView.centerYAnchor),
            nameLabel.trailingAnchor.constraint(equalTo: action.leadingAnchor, constant: -5),

            action.trailingAnchor.constraint(equalTo: cellView.trailingAnchor, constant: -10),
            action.centerYAnchor.constraint(equalTo: cellView.centerYAnchor),
        ])

        return cellView
    }

    func switchOrLink(_ appData: AppData) -> NSView {
        if MonitoredApp.pluginAppIds[appData.bundleId] != nil {
            let button = NSButton()
            button.translatesAutoresizingMaskIntoConstraints = false
            button.bezelStyle = NSButton.BezelStyle.rounded
            button.title = "Install plugin"
            button.action = #selector(clickInstallPlugin(_:))
            button.widthAnchor.constraint(equalToConstant: 100).isActive = true
            button.tag = appData.tag
            return button
        }

        let isMonitored = MonitoringManager.isAppMonitored(for: appData.bundleId)
        let switchControl = NSSwitch()
        switchControl.translatesAutoresizingMaskIntoConstraints = false
        switchControl.state = isMonitored ? .on : .off
        switchControl.target = self
        switchControl.action = #selector(switchToggled(_:))
        switchControl.tag = appData.tag
        return switchControl
    }

    @objc func switchToggled(_ sender: NSSwitch) {
        let appData = runningApps[sender.tag]
        MonitoringManager.set(monitoringState: sender.state == .on ? .on : .off, for: appData.bundleId)
    }

    @objc func clickInstallPlugin(_ sender: NSButton) {
        let appData = runningApps[sender.tag]
        guard
            let path = MonitoredApp.pluginAppIds[appData.bundleId],
            let url = URL(string: "https://wakatime.com/\(path)")
        else { return }

        NSWorkspace.shared.open(url)
    }
}


================================================
FILE: WakaTime/Views/SettingsView.swift
================================================
import AppKit

class SettingsView: NSView, NSTextFieldDelegate, NSTextViewDelegate {
    var delegate: StatusBarDelegate?

    // MARK: API Key

    lazy var apiKeyLabel: NSTextField = {
        NSTextField(labelWithString: "WakaTime API Key:")
    }()

    lazy var apiKeyTextField: WKTextField = {
        let textField = WKTextField(frame: .zero)
        textField.stringValue = ConfigFile.getSetting(section: "settings", key: "api_key") ?? ""
        textField.delegate = self
        return textField
    }()

    lazy var apiKeyStackView: NSStackView = {
        let stack = NSStackView(views: [apiKeyLabel, apiKeyTextField])
        stack.alignment = .leading
        stack.orientation = .vertical
        stack.spacing = 5
        return stack
    }()

    // MARK: Checkboxes

    lazy var launchAtLoginCheckbox: NSButton = {
        let checkbox = NSButton(
            checkboxWithTitle: "Launch at login",
            target: self,
            action: #selector(launchAtLoginCheckboxClicked)
        )
        checkbox.state = PropertiesManager.shouldLaunchOnLogin ? .on : .off
        return checkbox
    }()

    lazy var enableLoggingCheckbox: NSButton = {
        let checkbox = NSButton(
            checkboxWithTitle: "Enable logging to ~/.wakatime/macos-wakatime.log",
            target: self,
            action: #selector(enableLoggingCheckboxClicked)
        )
        checkbox.state = PropertiesManager.shouldLogToFile ? .on : .off
        return checkbox
    }()

    lazy var statusBarTextCheckbox: NSButton = {
        let checkbox = NSButton(
            checkboxWithTitle: "Show today’s time in status bar",
            target: self,
            action: #selector(enableStatusBarTextCheckboxClicked)
        )
        checkbox.state = PropertiesManager.shouldDisplayTodayInStatusBar ? .on : .off
        return checkbox
    }()

    lazy var requestA11yCheckbox: NSButton = {
        let checkbox = NSButton(
            checkboxWithTitle: "Enable stats from Xcode by requesting accessibility permission",
            target: self,
            action: #selector(enableA11yCheckboxClicked)
        )
        checkbox.state = PropertiesManager.shouldRequestA11yPermission ? .on : .off
        return checkbox
    }()

    lazy var checkboxesStackView: NSStackView = {
        let stack = NSStackView(views: [launchAtLoginCheckbox, statusBarTextCheckbox, requestA11yCheckbox, enableLoggingCheckbox])
        stack.alignment = .leading
        stack.orientation = .vertical
        stack.spacing = 10
        return stack
    }()

    // MARK: Domain Preference

    lazy var browserLabel: NSTextField = {
        var label = NSTextField(labelWithString: "The settings below are only applicable because you’ve enabled " +
            "monitoring a browser in the Monitored Apps menu.")
        label.lineBreakMode = .byWordWrapping // Enable word wrapping
        label.maximumNumberOfLines = 0 // Set to 0 to allow unlimited lines
        label.preferredMaxLayoutWidth = 380
        return label
    }()

    lazy var domainPreferenceLabel: NSTextField = {
        NSTextField(labelWithString: "Browser Tracking:")
    }()

    lazy var domainPreferenceControl: NSSegmentedControl = {
        let control = NSSegmentedControl()
        control.segmentStyle = .texturedRounded
        control.segmentCount = 2
        control.setLabel("Domain only", forSegment: 0)
        control.setLabel("Full url", forSegment: 1)
        control.trackingMode = .selectOne // Ensure only one option can be selected at a time
        control.action = #selector(domainPreferenceDidChange(_:))
        return control
    }()

    // MARK: Denylist/Allowlist

    lazy var filterTypeLabel: NSTextField = {
        NSTextField(labelWithString: "Browser Filter:")
    }()

    lazy var filterSegmentedControl: NSSegmentedControl = {
        let control = NSSegmentedControl()
        control.segmentStyle = .texturedRounded
        control.segmentCount = 2
        control.setLabel("All except denied sites", forSegment: 0)
        control.setLabel("Only allowed sites", forSegment: 1)
        control.trackingMode = .selectOne // Ensure only one option can be selected at a time
        control.action = #selector(segmentedControlDidChange(_:))
        return control
    }()

    lazy var filterListLabel: NSTextField = {
        NSTextField(labelWithString: "")
    }()

    lazy var filterTextView: NSTextView = {
        let textView = NSTextView()
        textView.isEditable = true
        textView.isRichText = false
        textView.isSelectable = true
        textView.autoresizingMask = [.width]
        textView.isVerticallyResizable = true
        textView.isHorizontallyResizable = false
        textView.textContainer?.containerSize = NSSize(width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude)
        textView.textContainer?.widthTracksTextView = true
        textView.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
        textView.delegate = self
        return textView
    }()

    lazy var filterScrollView: NSScrollView = {
        let scrollView = NSScrollView()
        scrollView.hasVerticalScroller = true
        scrollView.documentView = filterTextView
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.heightAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive = true
        return scrollView
    }()

    lazy var filterRemarksLabel: NSTextField = {
        var label = NSTextField(labelWithString: "")
        label.lineBreakMode = .byWordWrapping // Enable word wrapping
        label.maximumNumberOfLines = 0 // Set to 0 to allow unlimited lines
        label.preferredMaxLayoutWidth = 380
        return label
    }()

    lazy var domainStackView: NSStackView = {
        let stack = NSStackView(views: [
            domainPreferenceLabel,
            domainPreferenceControl
        ])
        stack.alignment = .leading
        stack.orientation = .vertical
        stack.spacing = 10
        stack.translatesAutoresizingMaskIntoConstraints = false
        return stack
    }()

    lazy var filterStackView: NSStackView = {
        let stack = NSStackView(views: [
            filterTypeLabel,
            filterSegmentedControl,
            filterListLabel,
            filterScrollView,
            filterRemarksLabel
        ])
        stack.alignment = .leading
        stack.orientation = .vertical
        stack.spacing = 10
        stack.translatesAutoresizingMaskIntoConstraints = false
        return stack
    }()

    // MARK: Version Label

    lazy var versionLabel: NSTextField = {
        let versionString = "Version: \(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "")"
        let versionLabel = NSTextField(labelWithString: versionString)
        return versionLabel
    }()

    lazy var stackView: NSStackView = {
        let stackView = NSStackView(views: [
            apiKeyStackView,
            checkboxesStackView,
            browserLabel,
            domainStackView,
            filterStackView,
            versionLabel
        ])
        stackView.alignment = .leading
        stackView.orientation = .vertical
        stackView.spacing = 25
        stackView.distribution = .equalSpacing
        stackView.edgeInsets = NSEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        stackView.translatesAutoresizingMaskIntoConstraints = false

        stackView.addConstraint(
            NSLayoutConstraint(
                item: filterStackView,
                attribute: .width,
                relatedBy: .equal,
                toItem: stackView,
                attribute: .width,
                multiplier: 1,
                constant: -(stackView.edgeInsets.left + stackView.edgeInsets.right)
            )
        )

        return stackView
    }()

    // MARK: Lifecycle

    init() {
        super.init(frame: .zero)

        addSubview(stackView)
        setupConstraints()
        setBrowserVisibility()
        updateDomainPreference(animate: false)
        updateFilterControls(animate: false)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: Callbacks

    @objc func launchAtLoginCheckboxClicked() {
        PropertiesManager.shouldLaunchOnLogin = launchAtLoginCheckbox.state == .on
        if launchAtLoginCheckbox.state == .on {
            SettingsManager.registerAsLoginItem()
        } else {
            SettingsManager.unregisterAsLoginItem()
        }
    }

    @objc func enableLoggingCheckboxClicked() {
        PropertiesManager.shouldLogToFile = enableLoggingCheckbox.state == .on
        if enableLoggingCheckbox.state == .on {
            PropertiesManager.shouldLogToFile = true
        } else {
            PropertiesManager.shouldLogToFile = false
        }
    }

    @objc func enableStatusBarTextCheckboxClicked() {
        PropertiesManager.shouldDisplayTodayInStatusBar = statusBarTextCheckbox.state == .on
        if statusBarTextCheckbox.state == .on {
            PropertiesManager.shouldDisplayTodayInStatusBar = true
        } else {
            PropertiesManager.shouldDisplayTodayInStatusBar = false
        }
        delegate?.fetchToday()
    }

    @objc func enableA11yCheckboxClicked() {
        PropertiesManager.shouldRequestA11yPermission = requestA11yCheckbox.state == .on
        if requestA11yCheckbox.state == .on {
            PropertiesManager.shouldRequestA11yPermission = true
        } else {
            PropertiesManager.shouldRequestA11yPermission = false
        }
    }

    @objc func domainPreferenceDidChange(_ sender: NSSegmentedControl) {
        PropertiesManager.domainPreference = sender.selectedSegment == 0 ? .domain : .url
        updateDomainPreference(animate: true)
    }

    @objc func segmentedControlDidChange(_ sender: NSSegmentedControl) {
        PropertiesManager.filterType = sender.selectedSegment == 0 ? .denylist : .allowlist
        updateFilterControls(animate: true)
    }

    // MARK: NSTextFieldDelegate

    func controlTextDidChange(_ obj: Notification) {
        ConfigFile.setSetting(section: "settings", key: "api_key", val: apiKeyTextField.stringValue)
    }

    // MARK: NSTextViewDelegate

    func textDidChange(_ notification: Notification) {
        guard let textView = notification.object as? NSTextView else { return }

        switch PropertiesManager.filterType {
            case .denylist:
                PropertiesManager.denylist = textView.string
            case .allowlist:
                PropertiesManager.allowlist = textView.string
        }
    }

    // MARK: Constraints

    private func setupConstraints() {
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: topAnchor, constant: 20),
            stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
            stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
        ])
    }

    func setBrowserVisibility() {
        if MonitoringManager.isMonitoringBrowsing {
            browserLabel.isHidden = false
            domainStackView.isHidden = false
            filterStackView.isHidden = false
        } else {
            browserLabel.isHidden = true
            domainStackView.isHidden = true
            filterStackView.isHidden = true
        }
        adjustWindowSize(animate: false)
    }

    // MARK: State Helpers

    private func updateDomainPreference(animate: Bool) {
        var selectedSegment: Int
        switch PropertiesManager.domainPreference {
            case .domain:
                selectedSegment = 0
            case .url:
                selectedSegment = 1
        }
        domainPreferenceControl.setSelected(true, forSegment: selectedSegment)
        adjustWindowSize(animate: animate)
    }

    private func updateFilterControls(animate: Bool) {
        let denylistTitle = "Denylist:"
        let denylistRemarks =
            "Sites that you don't want to show in your reports. " +
            "Only applicable to browsing activity. One regex per line."
        let allowlistTitle = "Allowlist:"
        let allowlistRemarks =
            "Sites that you want to show in your reports. " +
            "Only applicable to browsing activity. One regex per line."

        var title: String
        var remarks: String
        var list: String
        var selectedSegment: Int
        switch PropertiesManager.filterType {
            case .denylist:
                title = denylistTitle
                remarks = denylistRemarks
                list = PropertiesManager.denylist
                selectedSegment = 0
            case .allowlist:
                title = allowlistTitle
                remarks = allowlistRemarks
                list = PropertiesManager.allowlist
                selectedSegment = 1
        }

        filterListLabel.stringValue = title
        filterRemarksLabel.stringValue = remarks
        filterTextView.string = list
        filterSegmentedControl.setSelected(true, forSegment: selectedSegment)

        adjustWindowSize(animate: animate)
    }

    func adjustWindowSize(animate: Bool) {
        guard let window = self.window else { return }

        let newHeight = stackView.fittingSize.height + 70

        var newWindowFrame = window.frame
        newWindowFrame.size.height = newHeight
        newWindowFrame.origin.y += window.frame.height - newWindowFrame.height // Adjust origin to keep the top-left corner stationary

        window.setFrame(newWindowFrame, display: true, animate: animate)
    }
}


================================================
FILE: WakaTime/WakaTime-Bridging-Header.h
================================================
//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "Utils/ObjC.h"


================================================
FILE: WakaTime/WakaTime-Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleIdentifier</key>
	<string></string>
	<key>LSUIElement</key>
	<true/>
	<key>CFBundleURLTypes</key>
	<array>
		<dict>
			<key>CFBundleTypeRole</key>
			<string>Editor</string>
			<key>CFBundleURLSchemes</key>
			<array>
				<string>wakatime</string>
			</array>
		</dict>
	</array>
</dict>
</plist>


================================================
FILE: WakaTime/WakaTime.entitlements
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.app-sandbox</key>
    <false/>
    <key>com.apple.security.notifications</key>
    <true/>
</dict>
</plist>


================================================
FILE: WakaTime/WakaTime.swift
================================================
import AppKit
import Firebase
import Foundation

class WakaTime: HeartbeatEventHandler {
    // MARK: Watcher

    let watcher = Watcher()
    let delegate: StatusBarDelegate

    // MARK: Watcher State

    // Note: The lastEntity and lastTime member vars are read and written on a worker thread.
    // To ensure that they can be accessed concurrently from other threads without issues,
    // they are declared atomic here
    @Atomic var lastEntity = ""
    @Atomic var lastTime = 0
    @Atomic var lastCategory = Category.coding

    // MARK: Initialization and Setup

    init(_ delegate: StatusBarDelegate) {
        self.delegate = delegate

        Dependencies.installDependencies()
        if SettingsManager.shouldRegisterAsLoginItem() { SettingsManager.registerAsLoginItem() }
        if PropertiesManager.shouldRequestA11yPermission && !Accessibility.requestA11yPermission() {
            delegate.a11yStatusChanged(false)
        }

        configureFirebase()
        checkForApiKey()
        watcher.heartbeatEventHandler = self
        watcher.statusBarDelegate = delegate

        if !PropertiesManager.hasLaunchedBefore {
            for bundleId in MonitoredApp.defaultEnabledApps {
                MonitoringManager.enableByDefault(bundleId)
            }
            PropertiesManager.hasLaunchedBefore = true
        }
    }

    private func configureFirebase() {
        // Needed for uncaught exception reporting
        UserDefaults.standard.register(
          defaults: ["NSApplicationCrashOnExceptions": true]
        )
        FirebaseApp.configure()
    }

    private func checkForApiKey() {
        let apiKey = ConfigFile.getSetting(section: "settings", key: "api_key")
        if apiKey.isEmpty {
            openSettingsDeeplink()
        }
    }

    private func openSettingsDeeplink() {
        guard let url = DeepLink.settings.url else { return }

        NSWorkspace.shared.open(url)
    }

    private func openMonitoredAppsDeeplink() {
        guard let url = DeepLink.monitoredApps.url else { return }

        NSWorkspace.shared.open(url)
    }

    // MARK: Watcher Event Handling

    private func shouldSendHeartbeat(entity: String, time: Int, isWrite: Bool, category: Category) -> Bool {
        if isWrite { return true }
        if category != lastCategory { return true }
        if !entity.isEmpty && entity != lastEntity { return true }
        if lastTime + 120 < time { return true }

        return false
    }

    public func handleHeartbeatEvent(
        app: NSRunningApplication,
        entity: String,
        entityType: EntityType,
        project: String?,
        language: String?,
        category: Category?,
        isWrite: Bool) {
        let time = Int(NSDate().timeIntervalSince1970)
        let category = category ?? Category.coding
        guard shouldSendHeartbeat(entity: entity, time: time, isWrite: isWrite, category: category) else { return }

        // make sure we should be tracking this app to avoid race condition bugs
        // do this after shouldSendHeartbeat for better performance because handleEvent may
        // be called frequently
        guard MonitoringManager.isAppMonitored(app) else { return }

        guard
            let appName = AppInfo.getAppNameForHeartbeat(app),
            let appVersion = watcher.getAppVersion(app)
        else { return }

        let cli = NSString.path(
            withComponents: ConfigFile.resourcesFolder + ["wakatime-cli"]
        )
        let process = Process()
        process.launchPath = cli
        var args = [
            "--entity",
            entity,
            "--entity-type",
            entityType.rawValue,
            "--category",
            category.rawValue.replacingOccurrences(of: "_", with: " "),
            "--plugin",
            "\(appName)/\(appVersion) macos-wakatime/" + Bundle.main.version,
            "--alternate-branch",
            "<<LAST_BRANCH>>",
        ]
        if let project = project {
            args.append("--project")
            args.append(project)
        } else {
            args.append("--alternate-project")
            args.append("<<LAST_PROJECT>>")
        }
        if let language = language {
            args.append("--language")
            args.append(language)
        }
        if isWrite {
            args.append("--write")
        }

        Logging.default.log("Sending heartbeat with: \(args)")

        lastEntity = entity
        lastTime = time
        lastCategory = category

        process.arguments = args
        process.standardOutput = FileHandle.nullDevice
        process.standardError = FileHandle.nullDevice
        do {
            // Use WakaTime's custom execute() method to run the process. This will call Process.launch()
            // with ObjC exception bridging on macOS 12 or earlier and Process.run() on macOS 13 or newer.
            try process.execute()
        } catch {
            Logging.default.log("Failed to run wakatime-cli: \(error)")
        }

        delegate.fetchToday()
    }
}

enum DeepLink: String {
    case settings
    case monitoredApps

    var url: URL? { URL(string: "wakatime://\(self)") }
}

enum EntityType: String {
    case file
    case app
    case domain
    case url
}

enum Category: String {
    case browsing
    case building
    case codereviewing = "code reviewing"
    case coding
    case communicating
    case debugging
    case designing
    case indexing
    case learning
    case manualtesting = "manual testing"
    case meeting
    case planning
    case researching
    case runningtests = "running tests"
    case translating
    case writingdocs = "writing docs"
    case writingtests = "writing tests"
}

protocol StatusBarDelegate: AnyObject {
    func a11yStatusChanged(_ hasPermission: Bool)
    func toastNotification(_ title: String)
    func fetchToday()
}

protocol HeartbeatEventHandler {
    func handleHeartbeatEvent(
        app: NSRunningApplication,
        entity: String,
        entityType: EntityType,
        project: String?,
        language: String?,
        category: Category?,
        isWrite: Bool)
}


================================================
FILE: WakaTime/Watchers/FileSavedWatcher.swift
================================================
class FileMonitor {
    private let fileURL: URL
    private var dispatchObject: DispatchSourceFileSystemObject?

    public var fileChangedEventHandler: (() -> Void)?

    init?(filePath: URL, queue: DispatchQueue) {
        self.fileURL = filePath
        let folderURL = fileURL.deletingLastPathComponent() // monitor enclosing folder to track changes by Xcode
        let descriptor = open(folderURL.path, O_EVTONLY)
        guard descriptor >= -1 else { Logging.default.log("open failed: \(descriptor)"); return nil }
        dispatchObject = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: .write, queue: queue)
        dispatchObject?.setEventHandler { [weak self] in
            self?.fileChangedEventHandler?()
        }
        dispatchObject?.setCancelHandler {
            close(descriptor)
        }
        dispatchObject?.activate()
    }

    deinit {
        dispatchObject?.cancel()
    }
}


================================================
FILE: WakaTime/Watchers/MonitoredApp.swift
================================================
import AppKit

enum MonitoredApp: String, CaseIterable {
    case adobeaftereffect = "com.adobe.AfterEffects"
    case adobebridge = "com.adobe.bridge14"
    case adobeillustrator = "com.adobe.illustrator"
    case adobemediaencoder = "com.adobe.ame.application.24"
    case adobephotoshop = "com.adobe.Photoshop"
    case adobepremierepro = "com.adobe.PremierePro.24"
    case arcbrowser = "company.thebrowser.Browser"
    case beeper = "im.beeper"
    case brave = "com.brave.Browser"
    case canva = "com.canva.CanvaDesktop"
    case chrome = "com.google.Chrome"
    case chromebeta = "com.google.Chrome.beta"
    case chromecanary = "com.google.Chrome.canary"
    case figma = "com.figma.Desktop"
    case firefox = "org.mozilla.firefox"
    case github = "com.github.GitHubClient"
    case imessage = "com.apple.MobileSMS"
    case inkscape = "org.inkscape.Inkscape"
    case iterm2 = "com.googlecode.iterm2"
    case linear = "com.linear"
    case miro = "com.electron.realtimeboard"
    case notes = "com.apple.Notes"
    case notion = "notion.id"
    case postman = "com.postmanlabs.mac"
    case rocketchat = "chat.rocket"
    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 zed = "dev.zed.Zed"
    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
        }
    }

    // Hide these from the Monitored Apps menu
    static let unsupportedAppIds = [
        "com.apple.finder",
        "macos-wakatime.WakaTime",
    ]

    // link to plugin install pages with wakatime.com domain prepended for apps with plugins available
    static let pluginAppIds: [String: String] = [
        "aptana.studio": "aptana",
        "com.google.android.studio": "android-studio",
        "com.jetbrains.CLion": "clion",
        "com.jetbrains.DataSpell": "dataspell",
        "com.jetbrains.PhpStorm": "phpstorm",
        "com.jetbrains.PyCharm": "pycharm",
        "com.jetbrains.pycharm.ce": "pycharm",
        "com.jetbrains.RubyMine": "rubymine",
        "com.jetbrains.RustRover": "rustrover",
        "com.jetbrains.WebStorm": "webstorm",
        "com.jetbrains.goland": "goland",
        "com.jetbrains.intellij": "intellij-idea",
        "com.jetbrains.intellij.ce": "intellij-idea",
        "com.jetbrains.rider": "rider",
        "com.microsoft.VSCode": "vs-code",
        "com.microsoft.VSCodeInsiders": "vs-code",
        "com.Roblox.RobloxStudio": "roblox-studio",
        "com.sublimetext.2": "sublime",
        "com.sublimetext.3": "sublime",
        "com.sublimetext.4": "sublime",
        "com.todesktop.230313mzl4w4u92": "cursor",
        "com.visualstudio.code.oss": "vs-code",
        "com.vscodium": "vs-code",
        "epp.package.committers": "eclipse",
        "epp.package.cpp": "eclipse",
        "epp.package.dsl": "eclipse",
        "epp.package.embedcpp": "eclipse",
        "epp.package.java": "eclipse",
        "epp.package.jee": "eclipse",
        "epp.package.modeling": "eclipse",
        "epp.package.parallel": "eclipse",
        "epp.package.php": "eclipse",
        "epp.package.rcp": "eclipse",
        "epp.package.scout": "eclipse",
        "org.vim.MacVim": "vim",
    ]

    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.chromebeta.rawValue,
        MonitoredApp.chromecanary.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.github.rawValue,
        MonitoredApp.linear.rawValue,
        MonitoredApp.notes.rawValue,
        MonitoredApp.notion.rawValue,
        MonitoredApp.postman.rawValue,
        MonitoredApp.tableplus.rawValue,
        MonitoredApp.xcode.rawValue,
        MonitoredApp.zoom.rawValue,
        MonitoredApp.zed.rawValue,
    ]
}


================================================
FILE: WakaTime/Watchers/Watcher.swift
================================================
import Cocoa
import Foundation
import AppKit

class Watcher: NSObject {
    private let callbackQueue = DispatchQueue(label: "com.WakaTime.Watcher.callbackQueue", qos: .utility)
    private let monitorQueue = DispatchQueue(label: "com.WakaTime.Watcher.monitorQueue", qos: .utility)

    var appVersions: [String: String] = [:]
    var eventSourceObserver: EventSourceObserver?
    var heartbeatEventHandler: HeartbeatEventHandler?
    var statusBarDelegate: StatusBarDelegate?
    var lastCheckedA11y = Date()
    var isBuilding = false
    var activeApp: NSRunningApplication?

    private var observer: AXObserver?
    private var observingElement: AXUIElement?
    private var observingActivityTextElement: AXUIElement?
    private var fileMonitor: FileMonitor?
    private var selectedText: String?
    private var lastValidHeartbeatForApp = [String: HeartbeatData]()

    override init() {
        super.init()

        eventSourceObserver = EventSourceObserver(pollIntervalInSeconds: 1)

        NSWorkspace.shared.notificationCenter.addObserver(
            self,
            selector: #selector(appChanged),
            name: NSWorkspace.didActivateApplicationNotification,
            object: nil
        )

        if let app = NSWorkspace.shared.frontmostApplication {
            handleAppChanged(app)
        }
    }

    deinit {
        NSWorkspace.shared.notificationCenter.removeObserver(self) // needed prior macOS 11 only
    }

    @objc private func appChanged(_ notification: Notification) {
        guard let newApp = notification.userInfo?["NSWorkspaceApplicationKey"] as? NSRunningApplication else { return }

        handleAppChanged(newApp)
    }

    private func handleAppChanged(_ app: NSRunningApplication) {
        if app != activeApp {
            // swiftlint:disable line_length
            Logging.default.log("App changed from \(activeApp?.localizedName ?? "nil") to \(app.localizedName ?? "nil") (\(app.bundleIdentifier ?? "nil"))")
            eventSourceObserver?.stop()
            // swiftlint:enable line_length
            if let oldApp = activeApp { unwatch(app: oldApp) }
            activeApp = app
            self.statusBarDelegate?.fetchToday()
            if let bundleId = app.bundleIdentifier, MonitoringManager.isAppMonitored(for: bundleId) {
                watch(app: app)
            }
        }

        setAppVersion(app)
    }

    private func setAppVersion(_ app: NSRunningApplication) {
        guard
            let id = app.bundleIdentifier,
            appVersions[id] == nil,
            let url = app.bundleURL,
            let bundle = Bundle(url: url)
        else { return }

        appVersions[id] = "\(bundle.version)-\(bundle.build)".filter { !$0.isWhitespace }
    }

    public func getAppVersion(_ app: NSRunningApplication) -> String? {
        guard let id = app.bundleIdentifier else { return nil }
        return appVersions[id]
    }

    private func watch(app: NSRunningApplication) {
        setAppVersion(app)

        do {
            if MonitoringManager.isAppElectron(app) {
                let pid = app.processIdentifier
                let axApp = AXUIElementCreateApplication(pid)
                let result = AXUIElementSetAttributeValue(axApp, "AXManualAccessibility" as CFString, true as CFTypeRef)
                if result.rawValue != 0 {
                    let appName = app.localizedName ?? "UnknownApp"
                    Logging.default.log("Setting AXManualAccessibility on \(appName) failed (\(result.rawValue))")
                }
            }

            let observer = try AXObserver.create(appID: app.processIdentifier, callback: observerCallback)
            let this = Unmanaged.passUnretained(self).toOpaque()
            let axApp = AXUIElementCreateApplication(app.processIdentifier)

            try observer.add(notification: kAXFocusedUIElementChangedNotification, element: axApp, refcon: this)
            try observer.add(notification: kAXFocusedWindowChangedNotification, element: axApp, refcon: this)
            try observer.add(notification: kAXSelectedTextChangedNotification, element: axApp, refcon: this)
            if MonitoringManager.isAppElectron(app) {
                try observer.add(notification: kAXValueChangedNotification, element: axApp, refcon: this)
            }

            observer.addToRunLoop()
            self.observer = observer
            self.observingElement = axApp
            self.statusBarDelegate?.a11yStatusChanged(true)

            if MonitoringManager.isAppXcode(app), let activeWindow = axApp.activeWindow {
                if let currentPath = activeWindow.currentPath {
                    self.documentPath = currentPath
                }
                observeActivityText(activeWindow: activeWindow)
            } else {
                eventSourceObserver?.start { [weak self] in
                    self?.callbackQueue.async {
                        guard
                            let app = self?.activeApp, !MonitoringManager.isAppXcode(app),
                            let bundleId = app.bundleIdentifier
                        else { return }

                        var heartbeat = MonitoringManager.heartbeatData(app)

                        if let heartbeat {
                            self?.lastValidHeartbeatForApp[bundleId] = heartbeat
                        } else {
                            heartbeat = self?.lastValidHeartbeatForApp[bundleId]
                        }

                        if let heartbeat {
                            self?.heartbeatEventHandler?.handleHeartbeatEvent(
                                app: app,
                                entity: heartbeat.entity,
                                entityType: heartbeat.entityType,
                                project: heartbeat.project,
                                language: heartbeat.language,
                                category: heartbeat.category,
                                isWrite: false
                            )
                        }
                    }
                }
            }
        } catch {
            Logging.default.log("Failed to setup AXObserver: \(error.localizedDescription)")

            guard PropertiesManager.shouldRequestA11yPermission else {
                return
            }

            // TODO: App could be still launching, retry setting AXObserver in 20 seconds for this app

            if lastCheckedA11y.timeIntervalSinceNow > 60 {
                lastCheckedA11y = Date()
                self.statusBarDelegate?.a11yStatusChanged(Accessibility.requestA11yPermission())
            }
        }
    }

    private func unwatch(app: NSRunningApplication) {
        if let observer {
            observer.removeFromRunLoop()
            guard let observingElement else { fatalError("observingElement should not be nil here") }

            try? observer.remove(notification: kAXFocusedUIElementChangedNotification, element: observingElement)
            try? observer.remove(notification: kAXFocusedWindowChangedNotification, element: observingElement)
            try? observer.remove(notification: kAXSelectedTextChangedNotification, element: observingElement)
            if MonitoringManager.isAppElectron(app) {
                try? observer.remove(notification: kAXValueChangedNotification, element: observingElement)
            }

            self.observingElement = nil
            self.observer = nil
        }
    }

    func observeActivityText(activeWindow: AXUIElement) {
        let this = Unmanaged.passUnretained(self).toOpaque()
        activeWindow.traverseDown { element in
            if let id = element.id, id == "Activity Text" {
                // Remove previously observed "Activity Text" value observer, if any
                if let observingActivityTextElement {
                    try? self.observer?.remove(notification: kAXValueChangedNotification, element: observingActivityTextElement)
                }

                do {
                    // Update the current isBuilding state when the observed "Activity Text" UI element changes
                    self.isBuilding = checkIsBuilding(activityText: element.value)

                    if let path = self.documentPath {
                        self.handleNotificationEvent(path: path, isWrite: false)
                    }

                    // Try to add observer to the current "Activity Text" UI element
                    try self.observer?.add(notification: kAXValueChangedNotification, element: element, refcon: this)
                    observingActivityTextElement = element
                } catch {
                    observingActivityTextElement = nil
                }
                return false // "Activity Text" element found, abort traversal
            }
            return true // continue traversal
        }
    }

    func checkIsBuilding(activityText: String?) -> Bool {
        activityText == "Build" || (activityText?.contains("Building") == true)
    }

    var documentPath: URL? {
        didSet {
            if documentPath != oldValue {
                guard let newPath = documentPath else { return }

                Logging.default.log("Document changed: \(newPath)")

                handleNotificationEvent(path: newPath, isWrite: false)
                fileMonitor = nil
                fileMonitor = FileMonitor(filePath: newPath, queue: monitorQueue)
                fileMonitor?.fileChangedEventHandler = { [weak self] in
                    self?.handleNotificationEvent(path: newPath, isWrite: true)
                }
            }
        }
    }

    public func handleNotificationEvent(path: URL, isWrite: Bool) {
        callbackQueue.async {
            guard let app = self.activeApp else { return }

            self.heartbeatEventHandler?.handleHeartbeatEvent(
                app: app,
                entity: path.path,
                entityType: .file,
                project: nil,
                language: nil,
                category: self.isBuilding ? Category.building : Category.coding,
                isWrite: isWrite
            )
        }
    }
}

private func observerCallback(
    _ observer: AXObserver,
    _ element: AXUIElement,
    _ notification: CFString,
    _ refcon: UnsafeMutableRawPointer?
) {
    guard let refcon = refcon else { return }

    let this = Unmanaged<Watcher>.fromOpaque(refcon).takeUnretainedValue()

    guard let app = this.activeApp else { return }

    let axNotification = AXUIElementNotification.notificationFrom(string: notification as String)
    switch axNotification {
        case .selectedTextChanged:
            if MonitoringManager.isAppXcode(app) {
                guard
                    !element.selectedText.isEmpty,
                    let currentPath = element.currentPath
                else { return }
                this.heartbeatEventHandler?.handleHeartbeatEvent(
                    app: app,
                    entity: currentPath.path,
                    entityType: EntityType.file,
                    project: nil,
                    language: nil,
                    category: this.isBuilding ? Category.building : Category.coding,
                    isWrite: false)
            }
        case .focusedUIElementChanged:
            if MonitoringManager.isAppXcode(app) {
                guard let currentPath = element.currentPath else { return }

                this.documentPath = currentPath
            }
        case .focusedWindowChanged:
            if MonitoringManager.isAppXcode(app) {
                this.observeActivityText(activeWindow: element)
            }
        case .valueChanged:
            if MonitoringManager.isAppXcode(app) {
                if let id = element.id, id == "Activity Text" {
                    this.isBuilding = this.checkIsBuilding(activityText: element.value)
                    if let path = this.documentPath {
                        this.handleNotificationEvent(path: path, isWrite: false)
                    }
                }
            }
        default:
            break
    }
}


================================================
FILE: WakaTime/WindowControllers/MonitoredAppsWindowController.swift
================================================
import AppKit

class MonitoredAppsWindowController: NSWindowController {
    let monitoredAppsView = MonitoredAppsView()

    convenience init() {
        self.init(window: nil)

        let window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 400, height: 450),
            styleMask: [.titled, .closable, .resizable],
            backing: .buffered,
            defer: false
        )
        window.center()
        window.title = "Monitored Apps"
        window.contentView = monitoredAppsView
        self.window = window
    }

    override func showWindow(_ sender: Any?) {
        monitoredAppsView.reloadData()
        super.showWindow(sender)
    }
}


================================================
FILE: WakaTime/WindowControllers/SettingsWindowController.swift
================================================
import AppKit

class SettingsWindowController: NSWindowController, NSTextFieldDelegate {
    public let settingsView = SettingsView()

    convenience init() {
        self.init(window: nil)

        let window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 440, height: 470),
            styleMask: [.titled, .closable],
            backing: .buffered,
            defer: false
        )
        window.center()
        window.title = "Settings"
        window.contentView = settingsView
        self.window = window
        settingsView.adjustWindowSize(animate: false)
    }
}


================================================
FILE: WakaTime/main.swift
================================================
import Cocoa

let delegate = AppDelegate()
let application = NSApplication.shared
application.delegate = delegate
application.run()


================================================
FILE: WakaTime Helper/AppDelegate.swift
================================================
import Cocoa

class AppDelegate: NSObject, NSApplicationDelegate {
    struct Constants {
        static let mainAppBundleID = "macos-wakatime.WakaTime"
    }

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let userHome = FileManager.default.homeDirectoryForCurrentUser.pathComponents
        let logFilePath = NSString.path(withComponents: userHome + [".wakatime", "macos-wakatime-helper.log"])
        Logging.default.configure(filePath: logFilePath)

        Logging.default.log("Starting WakaTime Helper")

        let runningApps = NSWorkspace.shared.runningApplications
        let isRunning = runningApps.contains {
            $0.bundleIdentifier == Constants.mainAppBundleID
        }

        if !isRunning {
            Logging.default.log("WakaTime is not running")
            var path = Bundle.main.bundlePath as NSString
            for _ in 1...4 {
                path = path.deletingLastPathComponent as NSString
            }
            let fileURL = URL(fileURLWithPath: path as String)
            Logging.default.log("Attempting to open WakaTime at \"\(fileURL.absoluteString)\"")
            NSWorkspace.shared.openApplication(
                at: fileURL,
                configuration: NSWorkspace.OpenConfiguration()
            ) { _, error in
                if let error {
                    Logging.default.log(error.localizedDescription)
                }
            }
        } else {
            Logging.default.log("WakaTime is already running")
        }
    }
}


================================================
FILE: WakaTime Helper/Logging.swift
================================================
import Foundation
import os.log

class Logging {
    static let `default` = Logging()
    private var filePath: String?

    private init() {}

    // Configures logging to also write to a file at the given path.
    func configure(filePath: String) {
        self.filePath = filePath
    }

    func log(_ message: String, type: OSLogType = .default) {
        os_log("%{public}@", log: .default, type: type, message)

        if let filePath = self.filePath {
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
            let timestamp = dateFormatter.string(from: Date())
            let logMessage = "\(timestamp): \(message)\n"

            // Attempt to append the log message to the log file
            if let fileHandle = FileHandle(forWritingAtPath: filePath) {
                fileHandle.seekToEndOfFile()
                if let data = logMessage.data(using: .utf8) {
                    fileHandle.write(data)
                }
                fileHandle.closeFile()
            } else {
                // If the file does not exist, create it
                try? logMessage.write(toFile: filePath, atomically: true, encoding: .utf8)
            }
        }
    }
}


================================================
FILE: WakaTime Helper/WakaTime Helper-Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>LSBackgroundOnly</key>
	<true/>
</dict>
</plist>


================================================
FILE: WakaTime Helper/main.swift
================================================
import Cocoa

let delegate = AppDelegate()
let application = NSApplication.shared
application.delegate = delegate
application.run()


================================================
FILE: bin/prepare_changelog.sh
================================================
#!/bin/bash

set -e

if [[ $# -ne 2 ]]; then
    echo 'incorrect number of arguments'
    exit 1
fi

# Read arguments
branch=$1
changelog=$2
slack=

clean_up() {
    changelog="${changelog//\`/}"
    changelog="${changelog//\'/}"
    changelog="${changelog//\"/}"
}

replace_for_release() {
    changelog="${changelog//'%'/'%25'}"
    changelog="${changelog//$'\n'/'%0A'}"
    changelog="${changelog//$'\r'/'%0D'}"
}

replace_for_slack() {
    slack="${slack//'%'/'%25'}"
    slack="${slack//$'\n'/'%0A'}"
    slack="${slack//$'\r'/'%0D'}"
}

slack_output_for_main() {
    local IFS=$'\n' # make newlines the only separator
    local temp=
    for j in ${changelog}
    do
        temp="${temp}$(echo "$j" | awk '{printf "<https://github.com/wakatime/macos-wakatime/commit/"$1"|"$1">";$1=""; print $0 }')\n"
    done

    slack="*Changelog*\n${temp}"
}

slack_output_for_release() {
    local IFS=$'\n' # make newlines the only separator
    local temp=
    for j in ${changelog}
    do
        temp="${temp}${j}\n"
    done

    slack="*Changelog*\n${temp}"
}

parse_for_main() {
    changelog=$(awk 'f;/## Changelog/{f=1}' <<< "$changelog")
}

parse_for_release() {
    changelog=$(awk 'f;/Changelog:/{f=1}' <<< "$changelog")
}

case $branch in
    main) 
        parse_for_main
        clean_up
        slack_output_for_main
        replace_for_release
        ;;
    release)
        parse_for_release
        [ -z "$changelog" ] && exit 1
        clean_up
        slack_output_for_release
        replace_for_release
        replace_for_slack
        ;;
    *) exit 1 ;;
esac

echo "::set-output name=changelog::${changelog}"
echo "::set-output name=slack::${slack}"


================================================
FILE: project.yml
================================================
name: WakaTime

options:
  bundleIdPrefix: macos-wakatime
  createIntermediateGroups: true

packages:
  AppUpdater:
    url: https://github.com/alanhamlett/AppUpdater
    branch: master
  Firebase:
    url: https://github.com/firebase/firebase-ios-sdk
    from: 11.11.0

targets:
  WakaTime:
    type: application
    platform: macOS
    deploymentTarget: 10.15
    sources: [WakaTime]
    settings:
      CURRENT_PROJECT_VERSION: local-build
      MARKETING_VERSION: local-build
      INFOPLIST_FILE: WakaTime/WakaTime-Info.plist
      GENERATE_INFOPLIST_FILE: YES
      CODE_SIGN_STYLE: Automatic
      DEVELOPMENT_TEAM: ${SV_DEVELOPMENT_TEAM}
      ENABLE_HARDENED_RUNTIME: YES
      DEAD_CODE_STRIPPING: YES
      SWIFT_OBJC_BRIDGING_HEADER: WakaTime/WakaTime-Bridging-Header.h
    postCompileScripts:
      - script: ./Scripts/Lint/swiftlint lint --quiet
        name: Swiftlint
    dependencies:
      - target: WakaTime Helper
      - package: AppUpdater
      - package: Firebase
        product: FirebaseCrashlytics
    postBuildScripts:
      - script: |
          LOGIN_ITEMS_DIR="$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/Contents/Library/LoginItems"
          rm -rf "$LOGIN_ITEMS_DIR"
          mkdir -p "$LOGIN_ITEMS_DIR"
          mv "$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/Contents/Resources/WakaTime Helper.app" "$LOGIN_ITEMS_DIR/"
        name: Move "WakaTime Helper.app" to LoginItems
      - script: Scripts/Firebase/upload-dSYM.sh
        name: Firebase
        inputFiles:
          - ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}
          - $(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)
  WakaTime Helper:
    type: application
    platform: macOS
    deploymentTarget: 10.15
    sources: [WakaTime Helper]
    settings:
      CURRENT_PROJECT_VERSION: local-build
      MARKETING_VERSION: local-build
      INFOPLIST_FILE: WakaTime Helper/WakaTime Helper-Info.plist
      GENERATE_INFOPLIST_FILE: YES
      CODE_SIGN_STYLE: Automatic
      DEVELOPMENT_TEAM: ${SV_DEVELOPMENT_TEAM}
      ENABLE_HARDENED_RUNTIME: YES
      DEAD_CODE_STRIPPING: YES
      SKIP_INSTALL: YES
    postCompileScripts:
      - script: ./Scripts/Lint/swiftlint lint --quiet
        name: Swiftlint
Download .txt
gitextract_1hnk5wnk/

├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── support-app-request.yaml
│   └── workflows/
│       ├── on_pull_request_linter.yml
│       └── on_push.yml
├── .gitignore
├── .swiftlint.yml
├── .vscode/
│   └── settings.json
├── AUTHORS
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── Scripts/
│   ├── Firebase/
│   │   └── upload-dSYM.sh
│   └── Lint/
│       └── swiftlint
├── WakaTime/
│   ├── AppDelegate.swift
│   ├── ConfigFile.swift
│   ├── Controls/
│   │   └── WKTextField.swift
│   ├── Extensions/
│   │   ├── AXObserverExtension.swift
│   │   ├── AXUIElementExtension.swift
│   │   ├── BundleExtension.swift
│   │   ├── NSRunningApplicationExtension.swift
│   │   ├── OptionalExtension.swift
│   │   ├── ProcessExtension.swift
│   │   ├── StringExtension.swift
│   │   └── URLExtension.swift
│   ├── Helpers/
│   │   ├── Accessibility.swift
│   │   ├── AppInfo.swift
│   │   ├── Dependencies.swift
│   │   ├── EventSourceObserver.swift
│   │   ├── FilterManager.swift
│   │   ├── MonitoringManager.swift
│   │   ├── PropertiesManager.swift
│   │   └── SettingsManager.swift
│   ├── Resources/
│   │   ├── Assets.xcassets/
│   │   │   ├── AppIcon.appiconset/
│   │   │   │   └── Contents.json
│   │   │   ├── Contents.json
│   │   │   ├── WakaTime.imageset/
│   │   │   │   └── Contents.json
│   │   │   └── WakaTimeDisabled.imageset/
│   │   │       └── Contents.json
│   │   └── GoogleService-Info.plist
│   ├── Utils/
│   │   ├── Atomic.swift
│   │   ├── Logging.swift
│   │   ├── ObjC.h
│   │   └── ObjC.m
│   ├── Views/
│   │   ├── MonitoredAppsView.swift
│   │   └── SettingsView.swift
│   ├── WakaTime-Bridging-Header.h
│   ├── WakaTime-Info.plist
│   ├── WakaTime.entitlements
│   ├── WakaTime.swift
│   ├── Watchers/
│   │   ├── FileSavedWatcher.swift
│   │   ├── MonitoredApp.swift
│   │   └── Watcher.swift
│   ├── WindowControllers/
│   │   ├── MonitoredAppsWindowController.swift
│   │   └── SettingsWindowController.swift
│   └── main.swift
├── WakaTime Helper/
│   ├── AppDelegate.swift
│   ├── Logging.swift
│   ├── WakaTime Helper-Info.plist
│   └── main.swift
├── bin/
│   └── prepare_changelog.sh
└── project.yml
Condensed preview — 59 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (178K chars).
[
  {
    "path": ".gitattributes",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/support-app-request.yaml",
    "chars": 595,
    "preview": "name: Support app request\ndescription: Suggest a new app for tracking\ntitle: \"Support new app: XXX\"\nlabels: enhancement\n"
  },
  {
    "path": ".github/workflows/on_pull_request_linter.yml",
    "chars": 977,
    "preview": "name: Tests\n\non: pull_request\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Lint allowed br"
  },
  {
    "path": ".github/workflows/on_push.yml",
    "chars": 9523,
    "preview": "name: Release\n\non:\n  pull_request:\n    types: [opened, reopened, ready_for_review, synchronize]\n  push:\n    branches: [m"
  },
  {
    "path": ".gitignore",
    "chars": 72,
    "preview": ".DS_Store\n*.xcodeproj\nxcuserdata/\nMint/\n.build/\nbuild/\nPackage.resolved\n"
  },
  {
    "path": ".swiftlint.yml",
    "chars": 2838,
    "preview": "#\n# .swiftlint.yml\n#\n#\n\ndisabled_rules:\n  - inclusive_language\n  - nesting\n  - redundant_string_enum_value\n  - todo\n  - "
  },
  {
    "path": ".vscode/settings.json",
    "chars": 183,
    "preview": "{\n    \"lldb.library\": \"/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB\",\n    \"githubPul"
  },
  {
    "path": "AUTHORS",
    "chars": 268,
    "preview": "WakaTime is written and maintained by Alan Hamlett and various contributors:\n\n- Alan Hamlett <alan.hamlett@gmail.com>\n- "
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 4272,
    "preview": "# Contributing\n\n## Setup\n\nThis project depends on the [xcodegen](https://github.com/yonaskolb/XcodeGen?tab=readme-ov-fil"
  },
  {
    "path": "LICENSE",
    "chars": 1501,
    "preview": "BSD 3-Clause License\n\nCopyright (c) 2023 Alan Hamlett.\n\nRedistribution and use in source and binary forms, with or witho"
  },
  {
    "path": "README.md",
    "chars": 1978,
    "preview": "# macos-wakatime\n\nMac system tray app for automatic time tracking and metrics generated from your Xcode activity.\n\n## In"
  },
  {
    "path": "Scripts/Firebase/upload-dSYM.sh",
    "chars": 81,
    "preview": "\"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\""
  },
  {
    "path": "WakaTime/AppDelegate.swift",
    "chars": 11437,
    "preview": "import AppUpdater\nimport Cocoa\nimport UserNotifications\n\nclass AppDelegate: NSObject, NSApplicationDelegate, StatusBarDe"
  },
  {
    "path": "WakaTime/ConfigFile.swift",
    "chars": 3667,
    "preview": "import Foundation\n\nstruct ConfigFile {\n    private static var userHome: [String] {\n        FileManager.default.homeDirec"
  },
  {
    "path": "WakaTime/Controls/WKTextField.swift",
    "chars": 1501,
    "preview": "import AppKit\n\nclass WKTextField: NSTextField {\n    override func performKeyEquivalent(with event: NSEvent) -> Bool {\n  "
  },
  {
    "path": "WakaTime/Extensions/AXObserverExtension.swift",
    "chars": 2029,
    "preview": "import AppKit\n\nextension AXObserver {\n    static func create(appID: pid_t, callback: AXObserverCallback) throws -> AXObs"
  },
  {
    "path": "WakaTime/Extensions/AXUIElementExtension.swift",
    "chars": 15216,
    "preview": "import AppKit\n\nstruct AXPatternElement {\n    var role: String?\n    var subrole: String?\n    var id: String?\n    var titl"
  },
  {
    "path": "WakaTime/Extensions/BundleExtension.swift",
    "chars": 448,
    "preview": "import Foundation\n\nextension Bundle {\n    var displayName: String {\n        readFromInfoDict(key: \"CFBundleDisplayName\")"
  },
  {
    "path": "WakaTime/Extensions/NSRunningApplicationExtension.swift",
    "chars": 197,
    "preview": "import Cocoa\n\nextension NSRunningApplication {\n    var monitoredApp: MonitoredApp? {\n        guard let bundleId = bundle"
  },
  {
    "path": "WakaTime/Extensions/OptionalExtension.swift",
    "chars": 128,
    "preview": "import Foundation\n\nextension Optional where Wrapped: Collection {\n    var isEmpty: Bool {\n        self?.isEmpty ?? true\n"
  },
  {
    "path": "WakaTime/Extensions/ProcessExtension.swift",
    "chars": 654,
    "preview": "import Foundation\n\nextension Process {\n    // Runs process.launch() prior to macOS 13 or process.run() on macOS 13 or ne"
  },
  {
    "path": "WakaTime/Extensions/StringExtension.swift",
    "chars": 455,
    "preview": "import Foundation\n\nextension String {\n    func matchesRegex(_ pattern: String) -> Bool {\n        if let regex = try? NSR"
  },
  {
    "path": "WakaTime/Extensions/URLExtension.swift",
    "chars": 253,
    "preview": "import Foundation\n\nextension URL {\n    init?(stringWithoutScheme string: String) {\n        if string.starts(with: \"https"
  },
  {
    "path": "WakaTime/Helpers/Accessibility.swift",
    "chars": 336,
    "preview": "import AppKit\n\nclass Accessibility {\n    public static func requestA11yPermission() -> Bool {\n        let prompt = kAXTr"
  },
  {
    "path": "WakaTime/Helpers/AppInfo.swift",
    "chars": 1346,
    "preview": "import Foundation\nimport Cocoa\n\nclass AppInfo {\n    static func getAppName(bundleId: String) -> String? {\n        let wo"
  },
  {
    "path": "WakaTime/Helpers/Dependencies.swift",
    "chars": 10127,
    "preview": "import Foundation\n\n// swiftlint:disable force_unwrapping\n// swiftlint:disable force_try\nclass Dependencies {\n    public "
  },
  {
    "path": "WakaTime/Helpers/EventSourceObserver.swift",
    "chars": 1205,
    "preview": "import CoreGraphics\n\nclass EventSourceObserver {\n    let pollIntervalInSeconds: CFTimeInterval\n    var timer: Timer = Ti"
  },
  {
    "path": "WakaTime/Helpers/FilterManager.swift",
    "chars": 1664,
    "preview": "import Cocoa\n\nclass FilterManager {\n    static func filterBrowsedSites(_ url: String) -> Bool {\n        let patterns = S"
  },
  {
    "path": "WakaTime/Helpers/MonitoringManager.swift",
    "chars": 25058,
    "preview": "import Cocoa\nimport Foundation\n\nclass MonitoringManager {\n    enum MonitoringState {\n        case on\n        case off\n  "
  },
  {
    "path": "WakaTime/Helpers/PropertiesManager.swift",
    "chars": 6703,
    "preview": "import Foundation\n\nclass PropertiesManager {\n    enum DomainPreferenceType: String {\n        case domain\n        case ur"
  },
  {
    "path": "WakaTime/Helpers/SettingsManager.swift",
    "chars": 2227,
    "preview": "import Foundation\nimport ServiceManagement\n\nclass SettingsManager {\n#if !SIMULATE_OLD_MACOS\n    static let simulateOldMa"
  },
  {
    "path": "WakaTime/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "chars": 1201,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"16.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"16x16\"\n"
  },
  {
    "path": "WakaTime/Resources/Assets.xcassets/Contents.json",
    "chars": 63,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "WakaTime/Resources/Assets.xcassets/WakaTime.imageset/Contents.json",
    "chars": 398,
    "preview": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"32.png\",\n     "
  },
  {
    "path": "WakaTime/Resources/Assets.xcassets/WakaTimeDisabled.imageset/Contents.json",
    "chars": 398,
    "preview": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"32.png\",\n     "
  },
  {
    "path": "WakaTime/Resources/GoogleService-Info.plist",
    "chars": 1134,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "WakaTime/Utils/Atomic.swift",
    "chars": 532,
    "preview": "import Foundation\n\n@propertyWrapper\nstruct Atomic<Value> {\n    private var value: Value\n    private let lock = NSLock()\n"
  },
  {
    "path": "WakaTime/Utils/Logging.swift",
    "chars": 1567,
    "preview": "import Foundation\nimport os.log\n\nclass Logging {\n    static let `default` = Logging()\n    private var filePath: String?\n"
  },
  {
    "path": "WakaTime/Utils/ObjC.h",
    "chars": 209,
    "preview": "#ifndef ObjC_h\n#define ObjC_h\n\n#import <Foundation/Foundation.h>\n\n@interface ObjC : NSObject\n\n+ (BOOL)catchException:(vo"
  },
  {
    "path": "WakaTime/Utils/ObjC.m",
    "chars": 393,
    "preview": "#import <Foundation/Foundation.h>\n\n#import \"ObjC.h\"\n\n@implementation ObjC\n\n+ (BOOL)catchException:(void(^)(void))tryBloc"
  },
  {
    "path": "WakaTime/Views/MonitoredAppsView.swift",
    "chars": 8294,
    "preview": "import AppKit\n\nclass MonitoredAppsView: NSView, NSOutlineViewDataSource, NSOutlineViewDelegate {\n    struct AppData: Equ"
  },
  {
    "path": "WakaTime/Views/SettingsView.swift",
    "chars": 13635,
    "preview": "import AppKit\n\nclass SettingsView: NSView, NSTextFieldDelegate, NSTextViewDelegate {\n    var delegate: StatusBarDelegate"
  },
  {
    "path": "WakaTime/WakaTime-Bridging-Header.h",
    "chars": 127,
    "preview": "//\n//  Use this file to import your target's public headers that you would like to expose to Swift.\n//\n\n#import \"Utils/O"
  },
  {
    "path": "WakaTime/WakaTime-Info.plist",
    "chars": 482,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "WakaTime/WakaTime.entitlements",
    "chars": 307,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "WakaTime/WakaTime.swift",
    "chars": 6133,
    "preview": "import AppKit\nimport Firebase\nimport Foundation\n\nclass WakaTime: HeartbeatEventHandler {\n    // MARK: Watcher\n\n    let w"
  },
  {
    "path": "WakaTime/Watchers/FileSavedWatcher.swift",
    "chars": 942,
    "preview": "class FileMonitor {\n    private let fileURL: URL\n    private var dispatchObject: DispatchSourceFileSystemObject?\n\n    pu"
  },
  {
    "path": "WakaTime/Watchers/MonitoredApp.swift",
    "chars": 4844,
    "preview": "import AppKit\n\nenum MonitoredApp: String, CaseIterable {\n    case adobeaftereffect = \"com.adobe.AfterEffects\"\n    case a"
  },
  {
    "path": "WakaTime/Watchers/Watcher.swift",
    "chars": 12126,
    "preview": "import Cocoa\nimport Foundation\nimport AppKit\n\nclass Watcher: NSObject {\n    private let callbackQueue = DispatchQueue(la"
  },
  {
    "path": "WakaTime/WindowControllers/MonitoredAppsWindowController.swift",
    "chars": 678,
    "preview": "import AppKit\n\nclass MonitoredAppsWindowController: NSWindowController {\n    let monitoredAppsView = MonitoredAppsView()"
  },
  {
    "path": "WakaTime/WindowControllers/SettingsWindowController.swift",
    "chars": 596,
    "preview": "import AppKit\n\nclass SettingsWindowController: NSWindowController, NSTextFieldDelegate {\n    public let settingsView = S"
  },
  {
    "path": "WakaTime/main.swift",
    "chars": 132,
    "preview": "import Cocoa\n\nlet delegate = AppDelegate()\nlet application = NSApplication.shared\napplication.delegate = delegate\napplic"
  },
  {
    "path": "WakaTime Helper/AppDelegate.swift",
    "chars": 1534,
    "preview": "import Cocoa\n\nclass AppDelegate: NSObject, NSApplicationDelegate {\n    struct Constants {\n        static let mainAppBund"
  },
  {
    "path": "WakaTime Helper/Logging.swift",
    "chars": 1243,
    "preview": "import Foundation\nimport os.log\n\nclass Logging {\n    static let `default` = Logging()\n    private var filePath: String?\n"
  },
  {
    "path": "WakaTime Helper/WakaTime Helper-Info.plist",
    "chars": 226,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "WakaTime Helper/main.swift",
    "chars": 132,
    "preview": "import Cocoa\n\nlet delegate = AppDelegate()\nlet application = NSApplication.shared\napplication.delegate = delegate\napplic"
  },
  {
    "path": "bin/prepare_changelog.sh",
    "chars": 1672,
    "preview": "#!/bin/bash\n\nset -e\n\nif [[ $# -ne 2 ]]; then\n    echo 'incorrect number of arguments'\n    exit 1\nfi\n\n# Read arguments\nbr"
  },
  {
    "path": "project.yml",
    "chars": 2242,
    "preview": "name: WakaTime\n\noptions:\n  bundleIdPrefix: macos-wakatime\n  createIntermediateGroups: true\n\npackages:\n  AppUpdater:\n    "
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the wakatime/macos-wakatime GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 59 files (27.1 MB), approximately 39.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!