[
  {
    "path": ".gitattributes",
    "content": "\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/support-app-request.yaml",
    "content": "name: Support app request\ndescription: Suggest a new app for tracking\ntitle: \"Support new app: XXX\"\nlabels: enhancement\nbody:\n  - type: input\n    id: bundleid\n    attributes:\n      label: BundleId of the app\n      description: Build this app in Xcode and it prints bundleIds of all running apps in the output window\n    validations:\n      required: true\n  - type: textarea\n    id: titles\n    attributes:\n      label: Example window titles\n      description: List some example window titles of the app, which is used to parse the filename for your dashboard\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/workflows/on_pull_request_linter.yml",
    "content": "name: Tests\n\non: pull_request\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Lint allowed branch names\n        uses: lekterable/branchlint-action@1.2.0\n        with:\n          allowed: |\n            /^(.+:)?bugfix/.+/i\n            /^(.+:)?docs?/.+/i\n            /^(.+:)?feature/.+/i\n            /^(.+:)?hotfix/.+/i\n            /^(.+:)?major/.+/i\n            /^(.+:)?misc/.+/i\n            /^(.+:)?main$/i\n      -\n        name: Block fixup/squash commits\n        uses: xt0rted/block-autosquash-commits-action@v2\n        with:\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n      -\n        # Run only for release branch\n        if: ${{ github.base_ref == 'release' }}\n        name: Check for changelog pattern\n        uses: gandarez/check-pr-body-action@v1.0.3\n        with:\n          pr_number: ${{ github.event.number }}\n          contains: 'Changelog:'\n          not_contains: '`'\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/on_push.yml",
    "content": "name: Release\n\non:\n  pull_request:\n    types: [opened, reopened, ready_for_review, synchronize]\n  push:\n    branches: [main, release]\n    tags-ignore: \"**\"\n\njobs:\n  test:\n    name: Tests and Build\n    runs-on: macos-15\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v3\n      -\n        name: Install xcodegen via Homebrew for linting and building xcode project\n        run: brew install xcodegen\n      -\n        name: Generate project\n        run: xcodegen\n      -\n        name: Build app to run linters\n        run: xcodebuild -scheme WakaTime -configuration Debug -destination 'generic/platform=macOS' ONLY_ACTIVE_ARCH=NO ARCHS='arm64 x86_64' build\n\n  version:\n    name: Version\n    concurrency: tagging\n    if: ${{ github.ref == 'refs/heads/release' || github.ref == 'refs/heads/main' }}\n    runs-on: ubuntu-latest\n    needs: [test]\n    outputs:\n      semver: ${{ steps.format.outputs.semver }}\n      semver_tag: ${{ steps.semver-tag.outputs.semver_tag }}\n      ancestor_tag: ${{ steps.semver-tag.outputs.ancestor_tag }}\n      is_prerelease: ${{ steps.semver-tag.outputs.is_prerelease }}\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n      -\n        name: Calculate semver tag\n        id: semver-tag\n        uses: gandarez/semver-action@master\n        with:\n          prefix: v\n          prerelease_id: alpha\n          develop_branch_name: main\n          main_branch_name: release\n          major_pattern: \"(?i)^(.+:)?(major/.+)\"\n      -\n        name: Format\n        id: format\n        run: |\n          echo \"${{ steps.semver-tag.outputs.semver_tag }}\"\n          ver=`echo \"${{ steps.semver-tag.outputs.semver_tag }}\" | sed 's/^v//'`\n          echo \"$ver\"\n          echo \"semver=$ver\" >> $GITHUB_OUTPUT\n      -\n        name: Create tag\n        uses: actions/github-script@v6\n        with:\n          github-token: ${{ github.token }}\n          script: |\n            github.rest.git.createRef({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              ref: \"refs/tags/${{ steps.semver-tag.outputs.semver_tag }}\",\n              sha: context.sha\n            })\n\n  sign:\n    name: Sign Apple app\n    needs: [version]\n    runs-on: macos-15\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v3\n      -\n        name: Update project.yml\n        uses: fjogeleit/yaml-update-action@main\n        with:\n          valueFile: 'project.yml'\n          changes:  |\n            {\n              \"targets.WakaTime.settings.CURRENT_PROJECT_VERSION\": \"${{ needs.version.outputs.semver }}\",\n              \"targets.WakaTime.settings.MARKETING_VERSION\": \"${{ needs.version.outputs.semver }}\"\n            }\n          commitChange: false\n      -\n        name: Install xcodegen via Homebrew for linting and building xcode project\n        run: brew install xcodegen\n      -\n        name: Generate project\n        run: xcodegen\n      -\n        name: Build app\n        id: build\n        run: |\n          xcodebuild -scheme WakaTime -configuration Release -destination 'generic/platform=macOS' ONLY_ACTIVE_ARCH=NO ARCHS='arm64 x86_64' build\n          app=`find /Users/runner/Library/Developer/Xcode/DerivedData/ -name WakaTime.app`\n          echo \"$app\"\n          lipo -info \"$app/Contents/MacOS/WakaTime\"\n          directory=`dirname $app`\n          echo \"$directory\"\n          echo \"directory=$directory\" >> $GITHUB_OUTPUT\n      -\n        name: Verify universal platform\n        run: |\n          app=`find /Users/runner/Library/Developer/Xcode/DerivedData/ -name WakaTime.app`\n          echo \"$app\"\n          lipo -info \"$app/Contents/MacOS/WakaTime\"\n          BIN=\"$app/Contents/MacOS/WakaTime\"\n          archs=\"$(lipo -archs \"$BIN\" 2>/dev/null || true)\"\n          echo \"Reported architectures: $archs\"\n          for required in arm64 x86_64; do\n            echo \"$archs\" | grep -qw \"$required\" || {\n              echo \"❌ Missing required architecture: $required\"\n              exit 1\n            }\n          done\n      -\n        name: Import Code-Signing Certificates\n        uses: Apple-Actions/import-codesign-certs@v1\n        with:\n          # The certificates in a PKCS12 file encoded as a base64 string\n          p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}\n          # The password used to import the PKCS12 file.\n          p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}\n      - name: Codesign\n        env:\n          APP_SIGNING_IDENTITY: ${{ secrets.APPLE_DEVELOPER_IDENTITY }}\n        run: |\n          codesign --force --deep --timestamp --options runtime --sign \"$APP_SIGNING_IDENTITY\" \"${{ steps.build.outputs.directory }}/WakaTime.app\"\n      -\n        name: Store Credentials\n        env:\n          NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}\n          NOTARIZATION_APPLE_ID: ${{ secrets.AC_USERNAME }}\n          NOTARIZATION_PWD: ${{ secrets.AC_PASSWORD }}\n        run: xcrun notarytool store-credentials \"notarytool-profile\" --apple-id \"$NOTARIZATION_APPLE_ID\" --team-id \"$NOTARIZATION_TEAM_ID\" --password \"$NOTARIZATION_PWD\"\n      -\n        name: Notarize Helper\n        run: |\n          ditto -c -k --keepParent \"${{ steps.build.outputs.directory }}/WakaTime.app/Contents/Library/LoginItems/WakaTime Helper.app\" helper.zip\n          xcrun notarytool submit helper.zip --keychain-profile \"notarytool-profile\" --wait\n          xcrun stapler staple \"${{ steps.build.outputs.directory }}/WakaTime.app/Contents/Library/LoginItems/WakaTime Helper.app\"\n      -\n        name: Notarize App\n        run: |\n          ditto -c -k --keepParent ${{ steps.build.outputs.directory }}/WakaTime.app main.zip\n          xcrun notarytool submit main.zip --keychain-profile \"notarytool-profile\" --wait\n          xcrun stapler staple ${{ steps.build.outputs.directory }}/WakaTime.app\n      -\n        name: Zip\n        run: ditto -c -k --sequesterRsrc --keepParent ${{ steps.build.outputs.directory }}/WakaTime.app WakaTime.zip\n      -\n        name: Upload artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: app\n          path: ./WakaTime.zip\n      -\n        name: Remove tag if failure\n        if: ${{ failure() }}\n        uses: actions/github-script@v6\n        with:\n          github-token: ${{ github.token }}\n          script: |\n            github.rest.git.deleteRef({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              ref: \"tags/${{ needs.version.outputs.semver_tag }}\"\n            })\n\n  changelog:\n    name: Changelog\n    runs-on: ubuntu-latest\n    needs: [version, sign]\n    outputs:\n      changelog: ${{ steps.changelog.outputs.changelog }}\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n      -\n        if: ${{ github.ref == 'refs/heads/main' }}\n        name: Changelog for main\n        uses: gandarez/changelog-action@v1.2.0\n        id: changelog-main\n        with:\n          current_tag: ${{ github.sha }}\n          previous_tag: ${{ needs.version.outputs.ancestor_tag }}\n          exclude: |\n            ^Merge pull request .*\n      -\n        if: ${{ github.ref == 'refs/heads/release' }}\n        name: Get related pull request\n        uses: 8BitJonny/gh-get-current-pr@2.2.0\n        id: changelog-release\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n      -\n        name: Prepare changelog\n        id: changelog\n        run: |\n          echo \"${{ steps.changelog-main.outputs.changelog || steps.changelog-release.outputs.pr_body }}\" > changelog.txt\n          ./bin/prepare_changelog.sh $(echo ${GITHUB_REF#refs/heads/}) \"$(cat changelog.txt)\"\n      -\n        name: Remove tag if failure\n        if: ${{ failure() }}\n        uses: actions/github-script@v6\n        with:\n          github-token: ${{ github.token }}\n          script: |\n            github.rest.git.deleteRef({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              ref: \"tags/${{ needs.version.outputs.semver_tag }}\"\n            })\n\n  release:\n    name: Release\n    runs-on: macos-15\n    needs: [version, sign, changelog]\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n      -\n        name: Download artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: app\n          path: ./\n      -\n        name: Prepare release folder\n        id: prepare\n        run: |\n          mkdir release\n          mv ./WakaTime.zip release/macos-wakatime.zip\n      -\n        name: \"Create release\"\n        uses: softprops/action-gh-release@master\n        with:\n          name: ${{ needs.version.outputs.semver_tag }}\n          tag_name: ${{ needs.version.outputs.semver_tag }}\n          body: \"## Changelog\\n${{ needs.changelog.outputs.changelog }}\"\n          prerelease: ${{ needs.version.outputs.is_prerelease }}\n          target_commitish: ${{ github.sha }}\n          draft: false\n          files: |\n            ./release/macos-wakatime.zip\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      -\n        name: Remove tag if failure\n        if: ${{ failure() }}\n        uses: actions/github-script@v6\n        with:\n          github-token: ${{ github.token }}\n          script: |\n            github.rest.git.deleteRef({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              ref: \"tags/${{ needs.version.outputs.semver_tag }}\"\n            })\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n*.xcodeproj\nxcuserdata/\nMint/\n.build/\nbuild/\nPackage.resolved\n"
  },
  {
    "path": ".swiftlint.yml",
    "content": "#\n# .swiftlint.yml\n#\n#\n\ndisabled_rules:\n  - inclusive_language\n  - nesting\n  - redundant_string_enum_value\n  - todo\n  - trailing_comma\n  - type_body_length\n  - vertical_parameter_alignment\n  # 36 false positives. Will disable weak_delegate rule for now\n  - weak_delegate\n\nopt_in_rules:\n  - array_init\n  - closure_end_indentation\n  - closure_spacing\n  - contains_over_first_not_nil\n  - empty_count\n  - explicit_init\n  - fatal_error_message\n  - first_where\n  - force_unwrapping\n  - implicit_return\n  - literal_expression_end_indentation\n  - operator_usage_whitespace\n  - overridden_super_call\n  - override_in_extension\n  - private_outlet\n  - redundant_nil_coalescing\n  - sorted_first_last\n  - strict_fileprivate\n  - trailing_closure\n  - unneeded_parentheses_in_closure_argument\n\nincluded:\n  - WakaTime\n\nforce_cast: error\nforce_try: error\nforce_unwrapping: error\n\ntrailing_whitespace:\n  ignores_empty_lines: false\n  severity: warning\ntrailing_newline: error\ntrailing_semicolon: error\n\nvertical_whitespace:\n  max_empty_lines: 1\n  severity: warning\n\ncomma: error\ncolon:\n  severity: error\nopening_brace: error\nempty_count: error\nlegacy_constructor: error\nstatement_position:\n  statement_mode: default\n  severity: error\nlegacy_constant: error\n\ntype_name:\n  min_length: 3\n  max_length:\n    warning: 45\n    error: 50\n  excluded:\n    - T\n\nidentifier_name:\n  max_length:\n    warning: 40\n    error: 50\n  min_length:\n    error: 3\n  excluded:\n    - x\n    - y\n    - z\n    - i\n    - j\n    - at\n    - on\n    - id\n    - db\n    - rs\n    - to\n    - in\n    - me\n    - up\n    - dx\n    - dy\n    - preferredInterfaceOrientationForPresentation\n\nfunction_parameter_count:\n  warning: 10\n  error: 10\n\nline_length:\n  warning: 140\n  error: 140\n\nfunction_body_length:\n  warning: 150\n  error: 200\n\nfile_length:\n  warning: 1000\n  error: 1000\n\ncyclomatic_complexity:\n  warning: 30\n  error: 30\n\nlarge_tuple:\n  warning: 4\n  error: 5\n\nswitch_case_alignment:\n  indented_cases: true\n\nreporter: 'xcode'\n\ncustom_rules:\n  comments_space:\n    name: 'Space After Comment'\n    regex: '(^ *//\\w+)'\n    message: 'There should be a space after //'\n    severity: warning\n\n  empty_first_line:\n    name: 'Empty First Line'\n    regex: '(^[ a-zA-Z ]*(?:protocol|extension|class|struct|func) [ a-zA-Z0-9:,<>\\.\\(\\)\\\"-=`]*\\{\\n( *)?\\n)'\n    message: 'There should not be an empty line after a declaration'\n    severity: error\n\n  empty_line_after_guard:\n    name: 'Empty Line After Guard'\n    regex: '(^ *guard[ a-zA-Z0-9=?.\\(\\),><!`]*\\{[ a-zA-Z0-9=?.\\(\\),><!`\\\"]*\\}\\n *(?!(?:return|guard))\\S+)'\n    message: 'There should be an empty line after a guard'\n    severity: error\n\n  empty_line_after_super:\n    name: 'Empty Line After Super'\n    regex: '(^ *super\\.[ a-zA-Z0-9=?.\\(\\)\\{\\}:,><!`\\\"]*\\n *(?!(?:\\}|return))\\S+)'\n    message: 'There should be an empty line after super'\n    severity: error\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"lldb.library\": \"/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB\",\n    \"githubPullRequests.ignoredPullRequestBranches\": [\n        \"main\"\n    ]\n}"
  },
  {
    "path": "AUTHORS",
    "content": "WakaTime is written and maintained by Alan Hamlett and various contributors:\n\n- Alan Hamlett <alan.hamlett@gmail.com>\n- Carlos Henrique Gandarez <gandarez@gmail.com>\n- Michael Mavris <@MMavrisPaleBlue>\n- Tobias Lensing <@starbugs>\n- Chris Pastl <chris@crispybits.app>\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n## Setup\n\nThis project depends on the [xcodegen](https://github.com/yonaskolb/XcodeGen?tab=readme-ov-file#installing) command line tool.\n\n```bash\ngit clone git@github.com:wakatime/macos-wakatime.git\ncd macos-wakatime\nxcodegen\n```\n\nThen 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).\nCurrently there’s a bug in new Swift compiler versions, so the largest Xcode version working with this app is 15.2.\n\n## Branches\n\nThis project currently has two branches\n\n- `main` - Default branch for every new `feature` or `fix`\n- `release` - Branch for production releases and hotfixes\n\n## Testing and Linting\n\nBuild with `Xcode` before creating any pull requests, or your PR won’t pass the automated checks.\n\n## SwiftLint\n\nTo fix linter warning(s), run `swiftlint --fix`.\n\n## Branching Strategy\n\nWe require specific branch name prefixes for PRs:\n\n- `^major/.+` - `major`\n- `^feature/.+` - `minor`\n- `^bugfix/.+` - `patch`\n- `^docs?/.+` - `build`\n- `^misc/.+` - `build`\n\nMore info at [wakatime/semver-action](https://github.com/wakatime/semver-action#branch-names).\n\n## Pull Requests\n\n- Big changes, changes to the API, or changes with backward compatibility trade-offs should be first discussed in the Slack.\n- 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.\n- Code formatting should be consistent with the style used in the existing code.\n- Don't leave commented out code. A record of this code is already preserved in the commit history.\n- 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>.\n- 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.\n- Commit messages:\n  - Use the [imperative mood](http://chris.beams.io/posts/git-commit/#imperative) in the title. For example: \"Apply editor.indent preference\"\n  - Capitalize the title.\n  - Do not end the title with a period.\n  - Separate title from the body with a blank line. If you're committing via GitHub or GitHub Desktop this will be done automatically.\n  - Wrap body at 72 characters.\n  - Completely explain the purpose of the commit. Include a rationale for the change, any caveats, side-effects, etc.\n  - 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.\n  - See <http://chris.beams.io/posts/git-commit> for more tips on writing good commit messages.\n- Pull request title and description should follow the same guidelines as commit messages.\n- 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.\n\n## Troubleshooting\n\nIf you have trouble building off `main` branch, try:\n\n* close Xcode\n* `rm -rf ~/Library/Developer/Xcode/DerivedData/WakaTime*`\n* `rm -rf ./WakaTime.xcodeproj`\n* `xcodegen`\n* Open the project in Xcode\n* Under `Signing & Capabilities`, set your `Team`\n\nTo read local user preferences, run:\n\n    defaults read macos-wakatime.WakaTime\n\nAny question join us on [Slack](https://wakaslack.herokuapp.com/).\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2023 Alan Hamlett.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright\n  notice, this list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright\n  notice, this list of conditions and the following disclaimer\n  in the documentation and/or other materials provided\n  with the distribution.\n\n* Neither the names of WakaTime, nor the names of its\n  contributors may be used to endorse or promote products derived\n  from this software without specific prior written permission.\n\nTHIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND\nCONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT\nNOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER\nOR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# macos-wakatime\n\nMac system tray app for automatic time tracking and metrics generated from your Xcode activity.\n\n## Install\n\n1. Download the [latest release](https://github.com/wakatime/macos-wakatime/releases/latest/download/macos-wakatime.zip).\n2. Move `WakaTime.app` into your `Applications` folder, and run `WakaTime.app`.\n3. Enter your [WakaTime API Key][api key], then press `Save`.\n4. Use Xcode like normal and your coding activity will be displayed on your [WakaTime dashboard][dashboard]\n\n## Usage\n\nKeep the app running in your system tray, and your Xcode usage will show on your [WakaTime dashboard][dashboard].\n\n## Building from Source\n\n1. Run `xcodegen` to generate the project.\n2. Open the project with Xcode.\n3. Click Run (⌘+R).\n\nIf you run into Accessibility problems, try running `sudo tccutil reset Accessibility`.\n\n## Uninstall\n\nTo uninstall, move `WakaTime.app` into your mac Trash.\n\nIf you don’t use any other WakaTime plugins, run `rm -r ~/.wakatime*`.\n\n## Supported Apps\n\nWakaTime 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.\n\nWe add support for specific apps when a custom category, project, or entity type is necessary.\nFor example, when Slack needs the `communicating` category or Figma needs the `designing` category.\nOnly request support for a new app when it needs a custom category, or we can detect the project from the window title.\n\nBefore requesting support for a new app, first check the [list of supported apps][supported apps].\n\n## Contributing\n\nPull requests and issues are welcome!\nSee [Contributing][contributing] for more details.\nMany thanks to all [contributors][authors]!\n\nMade with :heart: by the WakaTime Team.\n\n[api key]: https://wakatime.com/api-key\n[dashboard]: https://wakatime.com/\n[contributing]: CONTRIBUTING.md\n[authors]: AUTHORS\n[supported apps]: https://github.com/wakatime/macos-wakatime/blob/main/WakaTime/Watchers/MonitoredApp.swift#L3\n"
  },
  {
    "path": "Scripts/Firebase/upload-dSYM.sh",
    "content": "\"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\""
  },
  {
    "path": "WakaTime/AppDelegate.swift",
    "content": "import AppUpdater\nimport Cocoa\nimport UserNotifications\n\nclass AppDelegate: NSObject, NSApplicationDelegate, StatusBarDelegate, UNUserNotificationCenterDelegate {\n    var window: NSWindow!\n    var statusBarItem: NSStatusItem!\n    let menu = NSMenu()\n    var statusBarA11yItem: NSMenuItem!\n    var statusBarA11ySeparator: NSMenuItem!\n    var statusBarA11yStatus: Bool = true\n    var settingsWindowController = SettingsWindowController()\n    var monitoredAppsWindowController = MonitoredAppsWindowController()\n    var wakaTime: WakaTime?\n\n    @Atomic var lastTodayTime = 0\n    @Atomic var lastTodayText = \"\"\n    @Atomic var lastBrowserWarningTime = 0\n\n    let updater = AppUpdater(owner: \"wakatime\", repo: \"macos-wakatime\")\n\n    func applicationDidFinishLaunching(_ aNotification: Notification) {\n        // Configure logging to a log file if activated by the user\n        if PropertiesManager.shouldLogToFile {\n            Logging.default.activateLoggingToFile()\n        }\n\n        Logging.default.log(\"Starting WakaTime\")\n\n        // Handle deep links\n        let eventManager = NSAppleEventManager.shared()\n        eventManager.setEventHandler(\n            self,\n            andSelector: #selector(handleGetURL(_:withReplyEvent:)),\n            forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL)\n        )\n\n        let statusBar = NSStatusBar.system\n        statusBarItem = statusBar.statusItem(withLength: NSStatusItem.variableLength)\n        statusBarItem.button?.image = NSImage(named: NSImage.Name(\"WakaTime\"))\n\n        // refresh code time text when status bar icon clicked\n        statusBarItem.button?.target = self\n        statusBarItem.button?.action = #selector(AppDelegate.onClick(_:))\n        statusBarItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])\n\n        statusBarA11yItem = NSMenuItem(\n            title: \"* A11y permission needed *\",\n            action: #selector(AppDelegate.a11yClicked(_:)),\n            keyEquivalent: \"\")\n        statusBarA11yItem.isHidden = true\n        menu.addItem(statusBarA11yItem)\n        statusBarA11ySeparator = NSMenuItem.separator()\n        menu.addItem(statusBarA11ySeparator)\n        statusBarA11ySeparator.isHidden = true\n        menu.addItem(withTitle: \"Dashboard\", action: #selector(AppDelegate.dashboardClicked(_:)), keyEquivalent: \"\")\n        menu.addItem(withTitle: \"Settings\", action: #selector(AppDelegate.settingsClicked(_:)), keyEquivalent: \"\")\n        menu.addItem(\n            withTitle: \"Monitored Apps\",\n            action: #selector(AppDelegate.monitoredAppsClicked(_:)),\n            keyEquivalent: \"\")\n        menu.addItem(NSMenuItem.separator())\n        menu.addItem(\n            withTitle: \"Check for Updates\",\n            action: #selector(AppDelegate.checkForUpdatesClicked(_:)),\n            keyEquivalent: \"\")\n        menu.addItem(NSMenuItem.separator())\n        menu.addItem(withTitle: \"Quit\", action: #selector(AppDelegate.quitClicked(_:)), keyEquivalent: \"\")\n\n        wakaTime = WakaTime(self)\n\n        settingsWindowController.settingsView.delegate = self\n\n        Task.detached(priority: .background) {\n            self.fetchToday()\n        }\n\n        // request notifications permission\n        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in\n            guard granted else {\n                if let msg = error?.localizedDescription {\n                    Logging.default.log(msg)\n                }\n                return\n            }\n        }\n    }\n\n    func applicationWillTerminate(_ notification: Notification) {\n        Logging.default.log(\"WakaTime will terminate\")\n    }\n\n    @objc func handleGetURL(_ event: NSAppleEventDescriptor, withReplyEvent replyEvent: NSAppleEventDescriptor) {\n        // Handle deep links\n        guard let urlString = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue,\n              let url = URL(string: urlString),\n              url.scheme == \"wakatime\",\n              let link = DeepLink(rawValue: url.host ?? \"\")\n        else { return }\n\n        switch link {\n            case .settings:\n                showSettings()\n            case .monitoredApps:\n                showMonitoredApps()\n        }\n    }\n\n    @objc func dashboardClicked(_ sender: AnyObject) {\n        let defaultUrl = \"https://wakatime.com/\"\n\n        var url = ConfigFile.getSetting(section: \"settings\", key: \"api_url\") ?? defaultUrl\n        if url.isEmpty {\n            url = defaultUrl\n        }\n\n        url = url\n            .replacingOccurrences(of: \"://api.\", with: \"://\")\n            .replacingOccurrences(of: \"/api/v1\", with: \"\")\n            .replacingOccurrences(of: \"^api\\\\.\", with: \"\", options: .regularExpression)\n            .replacingOccurrences(of: \"/api\", with: \"\")\n\n        if let url = URL(string: url) {\n            NSWorkspace.shared.open(url)\n        } else {\n            if url != defaultUrl {\n                if let url = URL(string: defaultUrl) {\n                    NSWorkspace.shared.open(url)\n                }\n            }\n        }\n    }\n\n    @objc func settingsClicked(_ sender: AnyObject) {\n        showSettings()\n    }\n\n    @objc func monitoredAppsClicked(_ sender: AnyObject) {\n        showMonitoredApps()\n    }\n\n    @objc func checkForUpdatesClicked(_ sender: AnyObject) {\n        updater.check {\n            self.toastNotification(\"Updating to latest release\")\n        }.catch(policy: .allErrors) { error in\n            if error.isCancelled {\n                let alert = NSAlert()\n                alert.messageText = \"Up to date\"\n                alert.informativeText = \"You have the latest version (\\(Bundle.main.version)).\"\n                alert.alertStyle = NSAlert.Style.warning\n                alert.addButton(withTitle: \"OK\")\n                alert.runModal()\n            } else {\n                Logging.default.log(String(describing: error))\n                let alert = NSAlert()\n                alert.messageText = \"Error\"\n                let max = 200\n                if error.localizedDescription.count <= max {\n                    alert.informativeText = error.localizedDescription\n                } else {\n                    alert.informativeText = String(error.localizedDescription.prefix(max).appending(\"…\"))\n                }\n                alert.alertStyle = NSAlert.Style.warning\n                alert.addButton(withTitle: \"OK\")\n                alert.runModal()\n            }\n        }\n    }\n\n    @objc func a11yClicked(_ sender: AnyObject) {\n        a11yStatusChanged(Accessibility.requestA11yPermission())\n    }\n\n    @objc func quitClicked(_ sender: AnyObject) {\n        NSApplication.shared.terminate(self)\n    }\n\n    @objc func onClick(_ sender: NSStatusItem) {\n        Task.detached(priority: .background) {\n            self.fetchToday()\n        }\n        // statusBarItem.popUpMenu(menu)\n        statusBarItem.menu = menu\n    }\n\n    func a11yStatusChanged(_ hasPermission: Bool) {\n        guard statusBarA11yStatus != hasPermission else { return }\n\n        statusBarA11yStatus = hasPermission\n        if hasPermission {\n            statusBarItem.button?.image = NSImage(named: NSImage.Name(\"WakaTime\"))\n        } else {\n            statusBarItem.button?.image = NSImage(named: NSImage.Name(\"WakaTimeDisabled\"))\n        }\n        statusBarA11yItem.isHidden = hasPermission\n        statusBarA11ySeparator.isHidden = hasPermission\n    }\n\n    private func checkBrowserDuplicateTracking() {\n        // Warn about using both Browser extension and Mac app tracking a browser at same time, once per 12 hrs\n        let time = Int(NSDate().timeIntervalSince1970)\n        if time - lastBrowserWarningTime > Dependencies.twelveHours && MonitoringManager.isMonitoringBrowsing {\n            Task {\n                if let browser = await Dependencies.recentBrowserExtension() {\n                    lastBrowserWarningTime = time\n                    delegate.toastNotification(\"Warning: WakaTime \\(browser) extension detected. \" +\n                        \"It’s recommended to only track browsing activity with the \\(browser) \" +\n                        \"extension or Mac Desktop app, but not both.\")\n                }\n            }\n        }\n    }\n\n    private func showSettings() {\n        NSApp.activate(ignoringOtherApps: true)\n        settingsWindowController.settingsView.setBrowserVisibility()\n        settingsWindowController.showWindow(self)\n    }\n\n    private func showMonitoredApps() {\n        NSApp.activate(ignoringOtherApps: true)\n        monitoredAppsWindowController.showWindow(self)\n    }\n\n    internal func toastNotification(_ title: String) {\n        let content = UNMutableNotificationContent()\n        content.title = title\n        content.body = \" \"\n\n        let uuidString = UUID().uuidString\n        let request = UNNotificationRequest(\n        identifier: uuidString,\n        content: content, trigger: nil)\n\n        let notificationCenter = UNUserNotificationCenter.current()\n        notificationCenter.delegate = self\n\n        notificationCenter.requestAuthorization(options: [.alert, .sound]) { granted, _ in\n            guard granted else { return }\n\n            DispatchQueue.main.async {\n                notificationCenter.add(request)\n            }\n        }\n    }\n\n    func userNotificationCenter(_ center: UNUserNotificationCenter,\n                                willPresent notification: UNNotification,\n                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {\n        if #available(macOS 11.0, *) {\n            completionHandler([.banner, .sound])\n        } else {\n            completionHandler([.alert, .sound]) // Fallback for older macOS versions\n        }\n    }\n\n    private func setText(_ text: String) {\n        DispatchQueue.main.async {\n            Logging.default.log(\"Set status bar text: \\(text)\")\n            self.statusBarItem.button?.title = text.isEmpty ? text : \" \" + text\n        }\n    }\n\n    internal func fetchToday() {\n        guard PropertiesManager.shouldDisplayTodayInStatusBar else {\n            setText(\"\")\n            return\n        }\n\n        let time = Int(NSDate().timeIntervalSince1970)\n        guard lastTodayTime + 120 < time else {\n            setText(lastTodayText)\n            return\n        }\n\n        lastTodayTime = time\n\n        let cli = NSString.path(\n            withComponents: ConfigFile.resourcesFolder + [\"wakatime-cli\"]\n        )\n        let process = Process()\n        process.launchPath = cli\n        let args = [\n            \"--today\",\n            \"--today-hide-categories\",\n            \"true\",\n            \"--plugin\",\n            \"macos-wakatime/\" + Bundle.main.version,\n        ]\n\n        Logging.default.log(\"Fetching coding activity for Today from api: \\(args)\")\n\n        process.arguments = args\n        let pipe = Pipe()\n        process.standardOutput = pipe\n        process.standardError = FileHandle.nullDevice\n\n        do {\n            try process.execute()\n        } catch {\n            Logging.default.log(\"Failed to run wakatime-cli fetching Today coding activity: \\(error)\")\n            return\n        }\n\n        let handle = pipe.fileHandleForReading\n        let data = handle.readDataToEndOfFile()\n        let text = (String(data: data, encoding: String.Encoding.utf8) ?? \"\").trimmingCharacters(in: .whitespacesAndNewlines)\n        lastTodayText = text\n        setText(text)\n\n        checkBrowserDuplicateTracking()\n    }\n}\n"
  },
  {
    "path": "WakaTime/ConfigFile.swift",
    "content": "import Foundation\n\nstruct ConfigFile {\n    private static var userHome: [String] {\n        FileManager.default.homeDirectoryForCurrentUser.pathComponents\n    }\n\n    public static var resourcesFolder: [String] {\n        userHome + [\".wakatime\"]\n    }\n\n    private static var filePath: String {\n        NSString.path(withComponents: userHome + [\".wakatime.cfg\"])\n    }\n\n    private static var filePathInternal: String {\n        NSString.path(withComponents: resourcesFolder + [\"wakatime-internal.cfg\"])\n    }\n\n    static func getSetting(section: String, key: String, internalConfig: Bool = false) -> String? {\n        let file = internalConfig ? filePathInternal : filePath\n        let contents: String\n        do {\n            contents = try String(contentsOfFile: file)\n        } catch {\n            Logging.default.log(\"Failed reading \\(file): \" + error.localizedDescription)\n            return nil\n        }\n        let lines = contents.split(separator: \"\\n\")\n\n        var currentSection = \"\"\n        for line in lines {\n            if line.hasPrefix(\"[\") && line.hasSuffix(\"]\") {\n                currentSection = String(line.dropFirst().dropLast())\n            } else if currentSection == section {\n                let parts = line.split(separator: \"=\", maxSplits: 2)\n                if parts.count == 2 && parts[0].trimmingCharacters(in: .whitespacesAndNewlines) == key {\n                    return String(parts[1].trimmingCharacters(in: .whitespacesAndNewlines))\n                }\n            }\n        }\n        return nil\n    }\n\n    static func setSetting(section: String, key: String, val: String, internalConfig: Bool = false) {\n        let file = internalConfig ? filePathInternal : filePath\n        let contents: String\n        do {\n            contents = try String(contentsOfFile: file)\n        } catch {\n            contents = \"[\" + section + \"]\\n\" + key + \" = \" + val\n            do {\n                try contents.write(to: URL(fileURLWithPath: file), atomically: true, encoding: .utf8)\n            } catch {\n                assertionFailure(\"Failed writing to URL: \\(file), Error: \" + error.localizedDescription)\n            }\n        }\n\n        let lines = contents.split(separator: \"\\n\")\n        var output: [String] = []\n        var currentSection = \"\"\n        var found = false\n        for line in lines {\n            if line.hasPrefix(\"[\") && line.hasSuffix(\"]\") {\n                if currentSection == section && !found {\n                    output.append(key + \" = \" + val)\n                    found = true\n                }\n                output.append(String(line))\n                currentSection = String(line.dropFirst().dropLast())\n            } else if currentSection == section {\n                let parts = line.split(separator: \"=\", maxSplits: 2)\n                if parts.count == 2 && parts[0].trimmingCharacters(in: .whitespacesAndNewlines) == key {\n                    if !found {\n                        output.append(key + \" = \" + val)\n                        found = true\n                    }\n                } else {\n                    output.append(String(line))\n                }\n            } else {\n                output.append(String(line))\n            }\n        }\n\n        if !found {\n            if currentSection != section {\n                output.append(\"[\" + section + \"]\")\n            }\n            output.append(key + \" = \" + val)\n        }\n\n        do {\n            try output.joined(separator: \"\\n\").write(to: URL(fileURLWithPath: file), atomically: true, encoding: .utf8)\n        } catch {\n            assertionFailure(\"Failed writing to URL: \\(file), Error: \" + error.localizedDescription)\n        }\n    }\n}\n"
  },
  {
    "path": "WakaTime/Controls/WKTextField.swift",
    "content": "import AppKit\n\nclass WKTextField: NSTextField {\n    override func performKeyEquivalent(with event: NSEvent) -> Bool {\n        if event.type == NSEvent.EventType.keyDown {\n            let modifierFlags = event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue\n            if modifierFlags == NSEvent.ModifierFlags.command.rawValue {\n                switch event.charactersIgnoringModifiers?.first {\n                    case \"x\":\n                        if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) { return true }\n                    case \"c\":\n                        if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) { return true }\n                    case \"v\":\n                        if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) { return true }\n                    case \"a\":\n                        if NSApp.sendAction(#selector(NSText.selectAll(_:)), to: nil, from: self) { return true }\n                    case \"z\":\n                        if NSApp.sendAction(Selector((\"undo:\")), to: nil, from: self) { return true }\n                    default:\n                        break\n                }\n            } else if modifierFlags == NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue {\n                if NSApp.sendAction(Selector((\"redo:\")), to: nil, from: self) { return true }\n            }\n        }\n        return super.performKeyEquivalent(with: event)\n    }\n}\n"
  },
  {
    "path": "WakaTime/Extensions/AXObserverExtension.swift",
    "content": "import AppKit\n\nextension AXObserver {\n    static func create(appID: pid_t, callback: AXObserverCallback) throws -> AXObserver {\n        var observer: AXObserver?\n        let error = AXObserverCreate(appID, callback, &observer)\n\n        guard error == .success else { throw AXObserverError.createFailed(error) }\n        guard let observer else { throw AXObserverError.createFailed(error) }\n\n        return observer\n    }\n\n    func add(notification: String, element: AXUIElement, refcon: UnsafeMutableRawPointer?) throws {\n        let error = AXObserverAddNotification(self, element, notification as CFString, refcon)\n        guard error == .success else {\n            Logging.default.log(\"Add notification \\(notification) failed: \\(error.rawValue)\")\n            throw AXObserverError.addNotificationFailed(error)\n        }\n\n        // Logging.default.log(\"Added notification \\(notification) to observer \\(self)\")\n    }\n\n    func remove(notification: String, element: AXUIElement) throws {\n        let error = AXObserverRemoveNotification(self, element, notification as CFString)\n        guard error == .success else {\n            Logging.default.log(\"Remove notification \\(notification) failed: \\(error.rawValue)\")\n            throw AXObserverError.removeNotificationFailed(error)\n        }\n\n        // Logging.default.log(\"Removed notification \\(notification) from observer \\(self)\")\n    }\n\n    func addToRunLoop(mode: CFRunLoopMode = .defaultMode) {\n        CFRunLoopAddSource(RunLoop.current.getCFRunLoop(), AXObserverGetRunLoopSource(self), mode)\n        // Logging.default.log(\"Added observer \\(self) to run loop\")\n    }\n\n    func removeFromRunLoop(mode: CFRunLoopMode = .defaultMode) {\n        CFRunLoopRemoveSource(RunLoop.current.getCFRunLoop(), AXObserverGetRunLoopSource(self), mode)\n        // Logging.default.log(\"Removed observer \\(self) from run loop\")\n    }\n}\n\nprivate enum AXObserverError: Error {\n    case createFailed(AXError)\n    case addNotificationFailed(AXError)\n    case removeNotificationFailed(AXError)\n}\n"
  },
  {
    "path": "WakaTime/Extensions/AXUIElementExtension.swift",
    "content": "import AppKit\n\nstruct AXPatternElement {\n    var role: String?\n    var subrole: String?\n    var id: String?\n    var title: String?\n    var value: String?\n    var children: [AXPatternElement] = []\n}\n\nextension AXUIElement {\n    var selectedText: String? {\n        getValue(for: kAXSelectedTextAttribute) as? String\n    }\n\n    func getValue(for attribute: String) -> CFTypeRef? {\n        var result: CFTypeRef?\n        guard AXUIElementCopyAttributeValue(self, attribute as CFString, &result) == .success else { return nil }\n        return result\n    }\n\n    var children: [AXUIElement]? {\n        guard let ref = getValue(for: kAXChildrenAttribute) else { return nil }\n        return ref as? [AXUIElement]\n    }\n\n    var parent: AXUIElement? {\n        guard let ref = getValue(for: kAXParentAttribute) else { return nil }\n        // swiftlint:disable force_cast\n        return (ref as! AXUIElement)\n        // swiftlint:enable force_cast\n    }\n\n    var nextSibling: AXUIElement? {\n        guard let parentChildren = self.parent?.children, let currentIndex = parentChildren.firstIndex(of: self) else { return nil }\n        let nextIndex = currentIndex + 1\n        guard parentChildren.indices.contains(nextIndex) else { return nil }\n        return parentChildren[nextIndex]\n    }\n\n    var previousSibling: AXUIElement? {\n        guard let parentChildren = self.parent?.children, let currentIndex = parentChildren.firstIndex(of: self) else { return nil }\n        let previousIndex = currentIndex - 1\n        guard parentChildren.indices.contains(previousIndex) else { return nil }\n        return parentChildren[previousIndex]\n    }\n\n    var id: String? {\n        guard let ref = getValue(for: kAXIdentifierAttribute) else { return nil }\n        // swiftlint:disable force_cast\n        return (ref as! String)\n        // swiftlint:enable force_cast\n    }\n\n    var rawTitle: String? {\n        guard let ref = getValue(for: kAXTitleAttribute) else { return nil }\n        // swiftlint:disable force_cast\n        return (ref as! String)\n        // swiftlint:enable force_cast\n    }\n\n    var role: String? {\n        guard let ref = getValue(for: kAXRoleAttribute) else { return nil }\n        // swiftlint:disable force_cast\n        return (ref as! String)\n        // swiftlint:enable force_cast\n    }\n\n    var subrole: String? {\n        guard let ref = getValue(for: kAXSubroleAttribute) else { return nil }\n        // swiftlint:disable force_cast\n        return (ref as! String)\n        // swiftlint:enable force_cast\n    }\n\n    var document: String? {\n        guard let ref = getValue(for: kAXDocumentAttribute) else { return nil }\n        // swiftlint:disable force_cast\n        return (ref as! String)\n        // swiftlint:enable force_cast\n    }\n\n    var value: String? {\n        guard let ref = getValue(for: kAXValueAttribute) else { return nil }\n        return (ref as? String)\n    }\n\n    var activeWindow: AXUIElement? {\n        // swiftlint:disable force_cast\n        if let window = getValue(for: kAXFocusedWindowAttribute) {\n            return (window as! AXUIElement)\n        }\n        if let window = getValue(for: kAXMainWindowAttribute) {\n            return (window as! AXUIElement)\n        }\n        if let window = getValue(for: kAXWindowAttribute) {\n            return (window as! AXUIElement)\n        }\n        // swiftlint:enable force_cast\n        return nil\n    }\n\n    var currentPath: URL? {\n        if let window = activeWindow {\n            if let path = window.document {\n                if path.hasPrefix(\"file://\") {\n                    return URL(string: path.dropFirst(7).description)\n                }\n                return URL(string: path)\n            }\n        }\n        if let path = document {\n            if path.hasPrefix(\"file://\") {\n                return URL(string: path.dropFirst(7).description)\n            }\n            return URL(string: path)\n        }\n        return nil\n    }\n\n    // Traverses the element's children (breadth-first) until visitor() returns false or traversal is completed\n    func traverseDown(visitor: (AXUIElement) -> Bool) {\n        var queue: [AXUIElement] = [self]\n        while !queue.isEmpty {\n            let currentElement = queue.removeFirst()\n            if let children = currentElement.children {\n                for child in children {\n                    if !visitor(child) { return }\n                    queue.append(child)\n                }\n            }\n        }\n    }\n\n    func traverseDownDFS(\n        visitor: (AXUIElement) -> Bool,\n        skipDescendantsWhere: ((AXUIElement) -> Bool)? = nil\n    ) {\n        var stack: [AXUIElement] = [self]\n        while !stack.isEmpty {\n            let currentElement = stack.removeLast()\n            if !visitor(currentElement) { return }\n            if skipDescendantsWhere?(currentElement) == true { continue }\n            if let children = currentElement.children {\n                stack.append(contentsOf: children.reversed())\n            }\n        }\n    }\n\n    // Traverses the element's parents until visitor() returns false or traversal is completed\n    func traverseUp(visitor: (AXUIElement) -> Bool, element: AXUIElement? = nil) {\n        let element = element ?? self\n        if let parent = element.parent {\n            if !visitor(parent) { return }\n            traverseUp(visitor: visitor, element: parent)\n        }\n    }\n\n    func firstDescendantWhere(\n        _ condition: (AXUIElement) -> Bool,\n        skipDescendantsWhere: ((AXUIElement) -> Bool)? = nil\n    ) -> AXUIElement? {\n        var matchingDescendant: AXUIElement?\n        traverseDownDFS(visitor: { element in\n            if condition(element) {\n                matchingDescendant = element\n                return false // stop traversal\n            }\n            return true // continue traversal\n        }, skipDescendantsWhere: skipDescendantsWhere)\n        return matchingDescendant\n    }\n\n    // Find the first descendant whose identifier matches the given identifier\n    func elementById(identifier: String) -> AXUIElement? {\n        firstDescendantWhere { $0.id == identifier }\n    }\n\n    func firstAncestorWhere(_ condition: (AXUIElement) -> Bool) -> AXUIElement? {\n        var matchingAncestor: AXUIElement?\n        traverseUp { element in\n            if condition(element) {\n                matchingAncestor = element\n                return false\n            }\n            return true\n        }\n        return matchingAncestor\n    }\n\n    // Index path of `element` relative to self\n    func indexPath(for element: AXUIElement) -> [Int] {\n        var path = [Int]()\n        var currentElement: AXUIElement? = element\n\n        while let current = currentElement, current != self {\n            if let parent = current.parent {\n                if let index = parent.children?.firstIndex(where: { $0 == current }) {\n                    path.insert(index, at: 0)\n                }\n                currentElement = parent\n            } else {\n                // No parent found, stop the loop\n                break\n            }\n        }\n\n        return path\n    }\n\n    // Finds the element at the given `indexPath`. `indexPath` must be relative to self.\n    // If no element with the given index path exists, returns nil.\n    func elementAtIndexPath(_ indexPath: [Int]) -> AXUIElement? {\n        var currentElement: AXUIElement = self\n        for index in indexPath {\n            // currentElement.debugPrint()\n            guard let children = currentElement.children, index < children.count else {\n                // Index is out of bounds for the current element's children\n                return nil\n            }\n            currentElement = children[index]\n        }\n        return currentElement\n    }\n\n    func findByPattern(_ pattern: AXPatternElement, within element: AXUIElement? = nil) -> AXUIElement? {\n        let rootElement = element ?? self\n\n        func matchesPattern(element: AXUIElement, pattern: AXPatternElement) -> Bool {\n            let roleMatches = pattern.role == nil || element.role == pattern.role\n            let subroleMatches = pattern.subrole == nil || element.subrole == pattern.subrole\n            let titleMatches = pattern.title == nil || element.rawTitle == pattern.title\n            let valueMatches = pattern.value == nil || element.selectedText == pattern.value\n            let idMatches = pattern.id == nil || element.id == pattern.id\n\n            return roleMatches && subroleMatches && titleMatches && valueMatches && idMatches\n        }\n\n        func search(element: AXUIElement, pattern: AXPatternElement) -> AXUIElement? {\n            if matchesPattern(element: element, pattern: pattern) {\n                var currentElement = element\n                for childPattern in pattern.children {\n                    guard let children = currentElement.children else { return nil }\n\n                    var foundMatch = false\n                    for child in children {\n                        if let match = search(element: child, pattern: childPattern) {\n                            currentElement = match\n                            foundMatch = true\n                            break\n                        }\n                    }\n                    if !foundMatch {\n                        return nil\n                    }\n                }\n                return currentElement\n            } else {\n                guard let children = element.children else { return nil }\n\n                for child in children {\n                    if let match = search(element: child, pattern: pattern) {\n                        return match\n                    }\n                }\n            }\n            return nil\n        }\n\n        return search(element: rootElement, pattern: pattern)\n    }\n\n    // Finds the first text area element whose value looks like a URL. Note that Chrome\n    // cuts off the URL scheme, so this only scans for a domain with an optional path.\n    func findAddressField() -> AXUIElement? {\n        firstDescendantWhere { descendant in\n            if descendant.role == kAXTextFieldRole, let value = descendant.value {\n                let pattern = \"(([^:\\\\/\\\\s]+)\\\\.([^:\\\\/\\\\s\\\\.]+))(\\\\/\\\\w+)*(\\\\/([\\\\w\\\\-\\\\.]+[^#?\\\\s]+))?(.*)?(#[\\\\w\\\\-]+)?$\"\n                do {\n                    let regex = try NSRegularExpression(pattern: pattern)\n                    let range = NSRange(value.startIndex..<value.endIndex, in: value)\n                    let matches = regex.numberOfMatches(in: value, options: [], range: range)\n                    return matches > 0\n                } catch {\n                    // print(\"Regex error: \\(error.localizedDescription)\")\n                    return false\n                }\n            }\n            return  false\n        }\n    }\n\n    func elementAtPosition(x: Float, y: Float) -> AXUIElement? {\n        var element: AXUIElement?\n        AXUIElementCopyElementAtPosition(self, x, y, &element)\n        return element\n    }\n\n    func elementAtPositionRelativeToWindow(x: CGFloat, y: CGFloat) -> AXUIElement? {\n        // swiftlint:disable force_unwrapping\n        let windowPositionData = getValue(for: kAXPositionAttribute)!\n        let windowSizeData = getValue(for: kAXSizeAttribute)!\n        // swiftlint:enable force_unwrapping\n\n        var windowPosition = CGPoint()\n        var windowSize = CGSize()\n\n        // swiftlint:disable force_cast\n        if !AXValueGetValue(windowPositionData as! AXValue, .cgPoint, &windowPosition) ||\n           !AXValueGetValue(windowSizeData as! AXValue, .cgSize, &windowSize) {\n            return nil\n        }\n        // swiftlint:enable force_cast\n\n        let globalX = windowPosition.x + x\n        let globalY = windowPosition.y + y\n\n        if globalX < windowPosition.x || globalX > windowPosition.x + windowSize.width ||\n           globalY < windowPosition.y || globalY > windowPosition.y + windowSize.height {\n            // Point is outside the window bounds\n            return nil\n        }\n\n        var element: AXUIElement?\n        let systemWideElement = AXUIElementCreateSystemWide()\n        AXUIElementCopyElementAtPosition(systemWideElement, Float(globalX), Float(globalY), &element)\n        return element\n    }\n\n    func debugPrintSubtree(element: AXUIElement? = nil, depth: Int = 0, highlight indexPath: [Int] = [], currentPath: [Int] = []) {\n        let element = element ?? self\n        if let children = element.children {\n            for (index, child) in children.enumerated() {\n                let indentation = String(repeating: \" \", count: depth)\n                let isMultiline = child.value?.contains(\"\\n\") ?? false\n                let displayValue = isMultiline ? \"[multiple lines]\" : (child.value?.components(separatedBy: .newlines).first ?? \"?\")\n                let ellipsedValue = displayValue.count > 50 ? String(displayValue.prefix(47)) + \"...\" : displayValue\n\n                // Check if the current path matches the ancestry path\n                let isOnIndexPath = currentPath + [index] == indexPath.prefix(currentPath.count + 1)\n                let highlightIndicator = isOnIndexPath ? \"→ \" : \"  \"\n\n                print(\n                    \"\\(indentation)\\(highlightIndicator)Role: \\\"\\(child.role ?? \"[undefined]\")\\\", \" +\n                    \"Subrole: \\(child.subrole ?? \"<nil>\"), \" +\n                    \"Id: \\(id ?? \"<nil>\"), \" +\n                    \"Title: \\(child.rawTitle ?? \"<nil>\"), \" +\n                    \"Value: \\\"\\(ellipsedValue)\\\"\"\n                )\n\n                debugPrintSubtree(element: child, depth: depth + 1, highlight: indexPath, currentPath: currentPath + [index])\n            }\n        }\n    }\n\n    func debugPrintAncestors() {\n        traverseUp { element in\n            let title = element.rawTitle ?? \"<nil>\"\n            let role = element.role ?? \"<nil>\"\n            let subrole = element.subrole ?? \"<nil>\"\n            print(\"Title: \\(title), Role: \\(role), Subrole: \\(subrole)\")\n            return true // Continue traversing up\n        }\n    }\n\n    func debugPrint() {\n        let isMultiline = value?.contains(\"\\n\") ?? false\n        let displayValue = isMultiline ? \"[multiple lines]\" : (value?.components(separatedBy: .newlines).first ?? \"?\")\n        let ellipsedValue = displayValue.count > 50 ? String(displayValue.prefix(47)) + \"...\" : displayValue\n        print(\n            \"Role: \\(role ?? \"<nil>\"), \" +\n            \"Subrole: \\(subrole ?? \"<nil>\"), \" +\n            \"Id: \\(id ?? \"<nil>\"), \" +\n            \"Title: \\(rawTitle ?? \"<nil>\"), \" +\n            \"Value: \\\"\\(ellipsedValue)\\\"\"\n        )\n    }\n}\n\nenum AXUIElementNotification {\n    case selectedTextChanged\n    case focusedUIElementChanged\n    case focusedWindowChanged\n    case valueChanged\n    case uknown\n\n    static func notificationFrom(string notification: String) -> AXUIElementNotification {\n        switch notification {\n            case \"AXSelectedTextChanged\":\n                return .selectedTextChanged\n            case \"AXFocusedUIElementChanged\":\n                return .focusedUIElementChanged\n            case \"AXFocusedWindowChanged\":\n                return .focusedWindowChanged\n            case \"AXValueChanged\":\n                return .valueChanged\n            default:\n                return .uknown\n        }\n    }\n}\n"
  },
  {
    "path": "WakaTime/Extensions/BundleExtension.swift",
    "content": "import Foundation\n\nextension Bundle {\n    var displayName: String {\n        readFromInfoDict(key: \"CFBundleDisplayName\") ?? \"unknown\"\n    }\n\n    var version: String {\n        readFromInfoDict(key: \"CFBundleShortVersionString\") ?? \"unknown\"\n    }\n\n    var build: String {\n        readFromInfoDict(key: \"CFBundleVersion\") ?? \"unknown\"\n    }\n\n    private func readFromInfoDict(key: String) -> String? {\n        infoDictionary?[key] as? String\n    }\n}\n"
  },
  {
    "path": "WakaTime/Extensions/NSRunningApplicationExtension.swift",
    "content": "import Cocoa\n\nextension NSRunningApplication {\n    var monitoredApp: MonitoredApp? {\n        guard let bundleId = bundleIdentifier else { return nil }\n\n        return .init(from: bundleId)\n    }\n}\n"
  },
  {
    "path": "WakaTime/Extensions/OptionalExtension.swift",
    "content": "import Foundation\n\nextension Optional where Wrapped: Collection {\n    var isEmpty: Bool {\n        self?.isEmpty ?? true\n    }\n}\n"
  },
  {
    "path": "WakaTime/Extensions/ProcessExtension.swift",
    "content": "import Foundation\n\nextension Process {\n    // Runs process.launch() prior to macOS 13 or process.run() on macOS 13 or newer.\n    // Adds Swift exception handling to process.launch().\n    func execute() throws {\n        if #available(macOS 13.0, *) {\n            // Use Process.run() on macOS 13 or newer. Process.run() throws Swift exceptions.\n            try self.run()\n        } else {\n            // Note: Process.launch() can throw ObjC exceptions. For further reference, see\n            // https://developer.apple.com/documentation/foundation/process/1414189-launch?changes=_3\n            try ObjC.catchException { self.launch() }\n        }\n    }\n}\n"
  },
  {
    "path": "WakaTime/Extensions/StringExtension.swift",
    "content": "import Foundation\n\nextension String {\n    func matchesRegex(_ pattern: String) -> Bool {\n        if let regex = try? NSRegularExpression(pattern: pattern) {\n            let range = NSRange(location: 0, length: self.utf16.count)\n            return regex.firstMatch(in: self, options: [], range: range) != nil\n        }\n        return false\n    }\n\n    func trim() -> String {\n        self.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)\n    }\n}\n"
  },
  {
    "path": "WakaTime/Extensions/URLExtension.swift",
    "content": "import Foundation\n\nextension URL {\n    init?(stringWithoutScheme string: String) {\n        if string.starts(with: \"https?://\") {\n            self.init(string: string)\n        } else {\n            self.init(string: \"https://\\(string)\")\n        }\n    }\n}\n"
  },
  {
    "path": "WakaTime/Helpers/Accessibility.swift",
    "content": "import AppKit\n\nclass Accessibility {\n    public static func requestA11yPermission() -> Bool {\n        let prompt = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String\n        let options: NSDictionary = [prompt: true]\n        let appHasPermission = AXIsProcessTrustedWithOptions(options)\n        return appHasPermission\n    }\n}\n"
  },
  {
    "path": "WakaTime/Helpers/AppInfo.swift",
    "content": "import Foundation\nimport Cocoa\n\nclass AppInfo {\n    static func getAppName(bundleId: String) -> String? {\n        let workspace = NSWorkspace.shared\n\n        guard\n            let appUrl = workspace.urlForApplication(withBundleIdentifier: bundleId),\n            let appBundle = Bundle(url: appUrl)\n        else { return nil }\n\n        return appBundle.object(forInfoDictionaryKey: \"CFBundleDisplayName\") as? String\n            ?? appBundle.object(forInfoDictionaryKey: \"CFBundleName\") as? String\n    }\n\n    static func getAppName(_ app: NSRunningApplication) -> String? {\n        guard let bundleId = app.bundleIdentifier else { return nil }\n\n        return getAppName(bundleId: bundleId)\n    }\n\n    static func getAppNameForHeartbeat(_ app: NSRunningApplication) -> String? {\n        guard let appName = getAppName(app) else { return nil }\n        return appName.filter { !$0.isWhitespace }\n    }\n\n    static func getIcon(file path: String) -> NSImage? {\n        guard\n            FileManager.default.fileExists(atPath: path)\n        else { return nil }\n\n        return NSWorkspace.shared.icon(forFile: path)\n    }\n\n    static func getIcon(bundleId: String) -> NSImage? {\n        guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) else { return nil }\n\n        return getIcon(file: url.absoluteURL.path)\n    }\n}\n"
  },
  {
    "path": "WakaTime/Helpers/Dependencies.swift",
    "content": "import Foundation\n\n// swiftlint:disable force_unwrapping\n// swiftlint:disable force_try\nclass Dependencies {\n    public static var twelveHours = 43200\n\n    public static func installDependencies() {\n        Task {\n            if !(await isCLILatest()) {\n                downloadCLI()\n            }\n        }\n    }\n\n    public static var isLocalDevBuild: Bool {\n        Bundle.main.version == \"local-build\"\n    }\n\n    public static func recentBrowserExtension() async -> String? {\n        guard\n            let apiKey = ConfigFile.getSetting(section: \"settings\", key: \"api_key\"),\n            !apiKey.isEmpty\n        else { return nil }\n        let url = \"https://api.wakatime.com/api/v1/users/current/user_agents?api_key=\\(apiKey)\"\n        let request = URLRequest(url: URL(string: url)!, cachePolicy: .reloadIgnoringCacheData)\n        do {\n            let (data, response) = try await URLSession.shared.data(for: request)\n            guard\n                let httpResponse = response as? HTTPURLResponse,\n                httpResponse.statusCode == 200\n            else { return nil }\n\n            struct Resp: Decodable {\n                let data: [UserAgent]\n            }\n            struct UserAgent: Decodable {\n                let isBrowserExtension: Bool\n                let editor: String?\n                let lastSeenAt: String?\n                enum CodingKeys: String, CodingKey {\n                    case isBrowserExtension = \"is_browser_extension\"\n                    case editor\n                    case lastSeenAt = \"last_seen_at\"\n                }\n            }\n\n            let release = try JSONDecoder().decode(Resp.self, from: data)\n            let now = Date()\n            for agent in release.data {\n                guard\n                    agent.isBrowserExtension,\n                    let editor = agent.editor,\n                    !editor.isEmpty,\n                    let lastSeenAt = agent.lastSeenAt\n                else { continue }\n\n                let isoDateFormatter = ISO8601DateFormatter()\n                isoDateFormatter.timeZone = TimeZone(secondsFromGMT: 0)\n                isoDateFormatter.formatOptions = [.withInternetDateTime]\n                if let lastSeen = isoDateFormatter.date(from: lastSeenAt) {\n                    if Int(now.timeIntervalSince(lastSeen)) > twelveHours {\n                        break\n                    }\n                }\n\n                return agent.editor\n            }\n        } catch {\n            Logging.default.log(\"Request error checking for conflicting browser extension: \\(error)\")\n            return nil\n        }\n        return nil\n    }\n\n    private static func getLatestVersion() async throws -> String? {\n        struct Release: Decodable {\n            let tagName: String\n            private enum CodingKeys: String, CodingKey {\n                case tagName = \"tag_name\"\n            }\n        }\n\n        let apiUrl = \"https://api.github.com/repos/wakatime/wakatime-cli/releases/latest\"\n        var request = URLRequest(url: URL(string: apiUrl)!, cachePolicy: .reloadIgnoringCacheData)\n        let lastModified = ConfigFile.getSetting(section: \"internal\", key: \"cli_version_last_modified\", internalConfig: true)\n        let currentVersion = ConfigFile.getSetting(section: \"internal\", key: \"cli_version\", internalConfig: true)\n        if let lastModified, currentVersion != nil {\n            request.setValue(lastModified, forHTTPHeaderField: \"If-Modified-Since\")\n        }\n        let (data, response) = try await URLSession.shared.data(for: request)\n        guard let httpResponse = response as? HTTPURLResponse else { return nil }\n\n        let now = Int(NSDate().timeIntervalSince1970)\n        ConfigFile.setSetting(section: \"internal\", key: \"cli_version_last_accessed\", val: String(now), internalConfig: true)\n\n        if httpResponse.statusCode == 304 {\n            // Current version is still the latest version available\n            return currentVersion\n        } else if let lastModified = httpResponse.value(forHTTPHeaderField: \"Last-Modified\"),\n                  let release = try? JSONDecoder().decode(Release.self, from: data) {\n            // Remote version successfully decoded\n            ConfigFile.setSetting(section: \"internal\", key: \"cli_version_last_modified\", val: lastModified, internalConfig: true)\n            ConfigFile.setSetting(section: \"internal\", key: \"cli_version\", val: release.tagName, internalConfig: true)\n            return release.tagName\n        } else {\n            // Unexpected response\n            return nil\n        }\n    }\n\n    private static func isCLILatest() async -> Bool {\n        let cli = NSString.path(\n            withComponents: ConfigFile.resourcesFolder + [\"wakatime-cli\"]\n        )\n        guard FileManager.default.fileExists(atPath: cli) else { return false }\n\n        let outputPipe = Pipe()\n        let process = Process()\n        process.launchPath = cli\n        process.arguments = [\"--version\"]\n        process.standardOutput = outputPipe\n        process.standardError = FileHandle.nullDevice\n        do {\n            try process.run()\n        } catch {\n            // Error running CLI process\n            return false\n        }\n        let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()\n        let output = String(decoding: outputData, as: UTF8.self)\n\n        // disable updating wakatime-cli when it was built from source\n        if output.trim() == \"<local-build>\" {\n            return true\n        }\n\n        let version: String?\n        if let regex = try? NSRegularExpression(pattern: \"([0-9]+\\\\.[0-9]+\\\\.[0-9]+)\"),\n           let match = regex.firstMatch(in: output, range: NSRange(output.startIndex..., in: output)),\n           let range = Range(match.range, in: output) {\n            version = String(output[range])\n        } else {\n            version = nil\n        }\n\n        let accessed = ConfigFile.getSetting(section: \"internal\", key: \"cli_version_last_accessed\", internalConfig: true)\n        if let accessed, let accessed = Int(accessed) {\n            let now = Int(NSDate().timeIntervalSince1970)\n            let fourHours = 4 * 3600\n            if accessed + fourHours > now {\n                Logging.default.log(\"Skip checking for wakatime-cli updates because recently checked \\(now - accessed) seconds ago\")\n                return true\n            }\n        }\n\n        let remoteVersion = try? await getLatestVersion()\n        guard let remoteVersion else {\n            // Could not retrieve remote version\n            return true\n        }\n        if let version, \"v\" + version == remoteVersion {\n            // Local version up to date\n            return true\n        } else {\n            // Newer version available\n            return false\n        }\n    }\n\n    private static func downloadCLI() {\n        let dir = NSString.path(withComponents: ConfigFile.resourcesFolder)\n        if !FileManager.default.fileExists(atPath: dir) {\n            do {\n                try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true, attributes: nil)\n            } catch {\n                Logging.default.log(error.localizedDescription)\n            }\n        }\n\n        let url = \"https://github.com/wakatime/wakatime-cli/releases/latest/download/wakatime-cli-darwin-\\(architecture()).zip\"\n        let zipFile = NSString.path(withComponents: ConfigFile.resourcesFolder + [\"wakatime-cli.zip\"])\n        let cli = NSString.path(withComponents: ConfigFile.resourcesFolder + [\"wakatime-cli\"])\n        let cliReal = NSString.path(withComponents: ConfigFile.resourcesFolder + [\"wakatime-cli-darwin-\\(architecture())\"])\n\n        if FileManager.default.fileExists(atPath: zipFile) {\n            do {\n                try FileManager.default.removeItem(atPath: zipFile)\n            } catch {\n                Logging.default.log(error.localizedDescription)\n                return\n            }\n        }\n\n        URLSession.shared.downloadTask(with: URLRequest(url: URL(string: url)!)) { fileUrl, _, _ in\n            guard let fileUrl else { return }\n\n            do {\n                // download wakatime-cli.zip\n                try FileManager.default.moveItem(at: fileUrl, to: URL(fileURLWithPath: zipFile))\n\n                if FileManager.default.fileExists(atPath: cliReal) {\n                    do {\n                        try FileManager.default.removeItem(atPath: cliReal)\n                    } catch {\n                        Logging.default.log(error.localizedDescription)\n                        return\n                    }\n                }\n\n                // unzip wakatime-cli.zip\n                let process = Process()\n                process.launchPath = \"/usr/bin/unzip\"\n                process.arguments = [zipFile, \"-d\", dir]\n                process.standardOutput = FileHandle.nullDevice\n                process.standardError = FileHandle.nullDevice\n                process.launch()\n                process.waitUntilExit()\n\n                // cleanup wakatime-cli.zip\n                try! FileManager.default.removeItem(atPath: zipFile)\n\n                // create ~/.wakatime/wakatime-cli symlink\n                do {\n                    try FileManager.default.removeItem(atPath: cli)\n                } catch { }\n                try! FileManager.default.createSymbolicLink(atPath: cli, withDestinationPath: cliReal)\n\n            } catch {\n                Logging.default.log(error.localizedDescription)\n            }\n        }.resume()\n    }\n\n    private static func architecture() -> String {\n        var systeminfo = utsname()\n        uname(&systeminfo)\n        let machine = withUnsafeBytes(of: &systeminfo.machine) {bufPtr -> String in\n            let data = Data(bufPtr)\n            if let lastIndex = data.lastIndex(where: { $0 != 0 }) {\n                return String(data: data[0...lastIndex], encoding: .isoLatin1)!\n            } else {\n                return String(data: data, encoding: .isoLatin1)!\n            }\n        }\n        if machine == \"x86_64\" {\n            return \"amd64\"\n        }\n        return \"arm64\"\n    }\n}\n// swiftlint:enable force_unwrapping\n// swiftlint:enable force_try\n"
  },
  {
    "path": "WakaTime/Helpers/EventSourceObserver.swift",
    "content": "import CoreGraphics\n\nclass EventSourceObserver {\n    let pollIntervalInSeconds: CFTimeInterval\n    var timer: Timer = Timer(timeInterval: 1, repeats: false) { _ in }\n\n    init(pollIntervalInSeconds: CFTimeInterval) {\n        self.pollIntervalInSeconds = pollIntervalInSeconds\n        timer.invalidate()\n    }\n\n    func start(activityDetected: @escaping () -> Void) {\n        stop()\n        timer = Timer.scheduledTimer(withTimeInterval: pollIntervalInSeconds, repeats: true) { [self] _ in\n            let secondsSinceLastKeyPress = Self.checkForKeyPresses()\n            let secondsSinceLastMouseMoved = Self.checkForMouseActivity()\n            if secondsSinceLastKeyPress < pollIntervalInSeconds || secondsSinceLastMouseMoved < pollIntervalInSeconds {\n                activityDetected()\n            }\n        }\n    }\n\n    func stop() {\n        timer.invalidate()\n    }\n\n    static private func checkForKeyPresses() -> CFTimeInterval {\n        CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: .keyDown)\n    }\n\n    static private func checkForMouseActivity() -> CFTimeInterval {\n        CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: .mouseMoved)\n    }\n}\n"
  },
  {
    "path": "WakaTime/Helpers/FilterManager.swift",
    "content": "import Cocoa\n\nclass FilterManager {\n    static func filterBrowsedSites(_ url: String) -> Bool {\n        let patterns = Self.parseList(PropertiesManager.currentFilterList)\n        if patterns.isEmpty { return true }\n\n        // Create scheme-prefixed address versions to allow regular expressions\n        // that incorporate a scheme to match\n        let httpUrl = \"http://\" + url\n        let httpsUrl = \"https://\" + url\n\n        switch PropertiesManager.filterType {\n            case .denylist:\n                for pattern in patterns {\n                    if url.matchesRegex(pattern) || httpUrl.matchesRegex(pattern) || httpsUrl.matchesRegex(pattern) {\n                        // Address matches a pattern on the denylist. Filter the site out.\n                        return false\n                    }\n                }\n            case .allowlist:\n                let addressMatchesAllowlist = patterns.contains { pattern in\n                    url.matchesRegex(pattern) || httpUrl.matchesRegex(pattern) || httpsUrl.matchesRegex(pattern)\n                }\n                // If none of the patterns on the allowlist match the given address, filter the site out\n                if !addressMatchesAllowlist {\n                    return false\n                }\n        }\n\n        // The given address passed all filters and will be included\n        return true\n    }\n\n    private static func parseList(_ listString: String) -> [String] {\n        Self.sanitizeList(listString.components(separatedBy: \"\\n\"))\n    }\n\n    private static func sanitizeList(_ urls: [String]) -> [String] {\n        urls.map { $0.trimmingCharacters(in: CharacterSet.whitespaces) }\n    }\n}\n"
  },
  {
    "path": "WakaTime/Helpers/MonitoringManager.swift",
    "content": "import Cocoa\nimport Foundation\n\nclass MonitoringManager {\n    enum MonitoringState {\n        case on\n        case off\n    }\n\n    static func isAppMonitored(for bundleId: String) -> Bool {\n        allMonitoredApps.contains(bundleId)\n    }\n\n    static func isAppMonitored(_ app: NSRunningApplication) -> Bool {\n        guard let bundleId = app.bundleIdentifier else { return false }\n\n        return isAppMonitored(for: bundleId)\n    }\n\n    static func isAppElectron(for bundleId: String) -> Bool {\n        MonitoredApp.electronAppIds.contains(bundleId)\n    }\n\n    static func isAppElectron(_ app: NSRunningApplication) -> Bool {\n        guard let bundleId = app.bundleIdentifier else { return false }\n\n        return isAppElectron(for: bundleId)\n    }\n\n    static func isAppXcode(_ app: NSRunningApplication) -> Bool {\n        guard let bundleId = app.bundleIdentifier else { return false }\n\n        return bundleId == MonitoredApp.xcode.rawValue\n    }\n\n    static func isAppBrowser(for bundleId: String) -> Bool {\n        MonitoredApp.browserAppIds.contains(bundleId)\n    }\n\n    static func isAppBrowser(_ app: NSRunningApplication) -> Bool {\n        guard let bundleId = app.bundleIdentifier else { return false }\n\n        return isAppBrowser(for: bundleId)\n    }\n\n    static func heartbeatData(_ app: NSRunningApplication) -> HeartbeatData? {\n        let pid = app.processIdentifier\n\n        guard\n            let activeWindow = AXUIElementCreateApplication(pid).activeWindow,\n            let entity = entity(for: app, activeWindow),\n            let entityUnwrapped = entity.0\n        else { return nil }\n\n        let project = project(for: app, activeWindow)\n        var language = language(for: app, activeWindow)\n        if project != nil && language == nil {\n            language = \"<<LAST_LANGUAGE>>\"\n        }\n\n        let heartbeat = HeartbeatData(\n            entity: entityUnwrapped,\n            entityType: entity.1,\n            project: project,\n            language: language,\n            category: category(for: app, activeWindow)\n        )\n        return heartbeat\n    }\n\n    static var isMonitoringBrowsing: Bool {\n        for bundleId in MonitoredApp.browserAppIds {\n            guard\n                AppInfo.getAppName(bundleId: bundleId) != nil,\n                isAppMonitored(for: bundleId)\n            else { continue }\n\n            return true\n        }\n        return false\n    }\n\n    static var allMonitoredApps: [String] {\n        if let bundleIds = UserDefaults.standard.stringArray(forKey: monitoringKey) {\n            return bundleIds.filter { MonitoredApp.pluginAppIds[$0] == nil }\n        } else {\n            var bundleIds: [String] = []\n            let defaults = UserDefaults.standard.dictionaryRepresentation()\n            for key in defaults.keys {\n                if key.starts(with: \"is_\") && key.contains(\"_monitored\") {\n                    if UserDefaults.standard.bool(forKey: key) {\n                        let bundleId = key.replacingOccurrences(of: \"is_\", with: \"\").replacingOccurrences(of: \"_monitored\", with: \"\")\n                        bundleIds.append(bundleId)\n                    }\n                    UserDefaults.standard.removeObject(forKey: key)\n                }\n            }\n            UserDefaults.standard.set(bundleIds, forKey: monitoringKey)\n            UserDefaults.standard.synchronize()\n            return bundleIds.filter { MonitoredApp.pluginAppIds[$0] == nil }\n        }\n    }\n\n    static func set(monitoringState: MonitoringState, for bundleId: String) {\n        if monitoringState == .on {\n            UserDefaults.standard.set(Array(Set(allMonitoredApps + [bundleId])), forKey: monitoringKey)\n        } else {\n            let apps = allMonitoredApps.filter { $0 != bundleId }\n            UserDefaults.standard.set(apps, forKey: monitoringKey)\n        }\n        UserDefaults.standard.synchronize()\n    }\n\n    static func enableByDefault(_ bundleId: String) {\n        if AppInfo.getIcon(bundleId: bundleId) != nil && AppInfo.getAppName(bundleId: bundleId) != nil {\n            MonitoringManager.set(monitoringState: .on, for: bundleId)\n        }\n        let setAppId = bundleId.appending(\"-setapp\")\n        if AppInfo.getIcon(bundleId: setAppId) != nil && AppInfo.getAppName(bundleId: setAppId) != nil {\n            MonitoringManager.set(monitoringState: .on, for: setAppId)\n        }\n    }\n\n    static var monitoringKey = \"wakatime_monitored_apps\"\n\n    static func entity(for app: NSRunningApplication, _ element: AXUIElement) -> (String?, EntityType)? {\n        if MonitoringManager.isAppBrowser(app) {\n            guard\n                let url = currentBrowserUrl(for: app, element),\n                FilterManager.filterBrowsedSites(url)\n            else { return nil }\n\n            guard PropertiesManager.domainPreference == .domain else { return (url, .url) }\n\n            return (domainFromUrl(url), .domain)\n        }\n\n        guard let monitoredApp = app.monitoredApp else { return (title(for: app, element), .app) }\n\n        switch monitoredApp {\n            case .canva:\n                // Canva obviously implements tabs in a different way than the tab content UI.\n                // Due to this circumstance, it's possible to just sample an element from the\n                // Canva window which is positioned underneath the tab bar and trace to the\n                // web area root which appears to be properly titled. All the UI zoom settings\n                // in Canva only change the tab content or sub content of the tab content, hence\n                // this should be relatively safe. In cases where this fails, nil should be\n                // returned as a consequence of the web area not being found.\n                let someElem = element.elementAtPositionRelativeToWindow(x: 10, y: 60)\n                let webArea = someElem?.firstAncestorWhere { $0.role == \"AXWebArea\" }\n                return (webArea?.rawTitle, .app)\n            case .notes:\n                let skipHighCostElements: (AXUIElement) -> Bool = { element in\n                    guard let role = element.role else { return false }\n\n                    // If we don't skip them, Notes app itself costs a lot of CPU time to create AXUIElement for us to traverse.\n                    // AXOutline -> Folder Sidebar of Notes\n                    // AXTable   -> List View of items of selected folder. It may contain thousands of rows, and\n                    // each row may have text and image element.\n                    if role == \"AXOutline\" || role == \"AXTable\" {\n                        return true\n                    }\n\n                    return false\n                }\n                // There's apparently two text editor implementations in Apple Notes. One uses a web view,\n                // the other appears to be a native implementation based on the `ICTK2MacTextView` class.\n                let webAreaElement = element.firstDescendantWhere({ $0.role == \"AXWebArea\" }, skipDescendantsWhere: skipHighCostElements )\n                if let webAreaElement {\n                    // WebView-based implementation\n                    let titleElement = webAreaElement.firstDescendantWhere { $0.role == kAXStaticTextRole }\n                    return (titleElement?.value, .app)\n                } else {\n                    // ICTK2MacTextView\n                    let textAreaElement = element.firstDescendantWhere({\n                        $0.role == kAXTextAreaRole\n                    }, skipDescendantsWhere: skipHighCostElements)\n                    if let value = textAreaElement?.value {\n                        let title = extractPrefix(value, separator: \"\\n\")\n                        return (title, .app)\n                    }\n                    return nil\n                }\n            default:\n                return (title(for: app, element), .app)\n        }\n    }\n\n    // swiftlint:disable cyclomatic_complexity\n    static func title(for app: NSRunningApplication, _ element: AXUIElement) -> String? {\n        guard let monitoredApp = app.monitoredApp else {\n            return extractPrefix(element.rawTitle)\n        }\n\n        switch monitoredApp {\n            case .adobeaftereffect:\n                return extractPrefix(element.rawTitle)\n            case .adobebridge:\n                return extractPrefix(element.rawTitle)\n            case .adobeillustrator:\n                return extractPrefix(element.rawTitle)\n            case .adobemediaencoder:\n                return extractPrefix(element.rawTitle)\n            case .adobephotoshop:\n                return extractPrefix(element.rawTitle)\n            case .adobepremierepro:\n                return extractPrefix(element.rawTitle)\n            case .arcbrowser:\n                fatalError(\"\\(monitoredApp.rawValue) should never use window title as entity\")\n            case .beeper:\n                return extractPrefix(element.rawTitle)\n            case .brave:\n                fatalError(\"\\(monitoredApp.rawValue) should never use window title as entity\")\n            case .canva:\n                fatalError(\"\\(monitoredApp.rawValue) should never use window title as entity\")\n            case .chrome, .chromebeta, .chromecanary:\n                fatalError(\"\\(monitoredApp.rawValue) should never use window title as entity\")\n            case .figma:\n                guard\n                    let title = extractPrefix(element.rawTitle, separator: \" – \"),\n                    title != \"Figma\",\n                    title != \"Drafts\"\n                else { return nil }\n                return title\n            case .firefox:\n                fatalError(\"\\(monitoredApp.rawValue) should never use window title as entity\")\n            case .github:\n                return extractPrefix(element.rawTitle, separator: \" - \")\n            case .imessage:\n                return extractPrefix(element.rawTitle, separator: \" - \")\n            case .inkscape:\n                return extractPrefix(element.rawTitle)\n            case .iterm2:\n                return extractPrefix(element.rawTitle, separator: \" - \")\n            case .linear:\n                return extractPrefix(element.rawTitle, separator: \" - \")\n            case .miro:\n                return extractSuffix(element.rawTitle)\n            case .notes:\n                fatalError(\"\\(monitoredApp.rawValue) should never use window title as entity\")\n            case .notion:\n                return extractPrefix(element.rawTitle, separator: \" - \")\n            case .postman:\n                guard\n                    let title = extractPrefix(element.rawTitle, separator: \" - \", fullTitle: true),\n                    title != \"Postman\"\n                else { return nil }\n                return title\n            case .rocketchat:\n                return extractPrefix(element.rawTitle)\n            case .slack:\n                return extractPrefix(element.rawTitle, separator: \" - \")\n            case .safari:\n                fatalError(\"\\(monitoredApp.rawValue) should never use window title as entity\")\n            case .safaripreview:\n                fatalError(\"\\(monitoredApp.rawValue) should never use window title as entity\")\n            case .tableplus:\n                return extractPrefix(element.rawTitle, separator: \" - \")\n            case .terminal:\n                return extractPrefix(element.rawTitle, separator: \" - \")\n            case .warp:\n                guard\n                    let title = extractPrefix(element.rawTitle, separator: \" - \"),\n                    title != \"Warp\"\n                else { return nil }\n                return title\n            case .wecom:\n                return extractPrefix(element.rawTitle, separator: \" - \")\n            case .whatsapp:\n                return extractPrefix(element.rawTitle, separator: \" - \")\n            case .xcode:\n                fatalError(\"\\(monitoredApp.rawValue) should never use window title as entity\")\n            case .zoom:\n                return extractPrefix(element.rawTitle, separator: \" - \")\n            case .zed:\n                return extractPrefix(element.rawTitle, separator: \" — \")\n        }\n    }\n\n    static func category(for app: NSRunningApplication, _ element: AXUIElement) -> Category {\n        guard let monitoredApp = app.monitoredApp else { return .coding }\n\n        if isAppBrowser(app) {\n            guard let url = currentBrowserUrl(for: app, element) else { return .browsing }\n            return category(from: url)\n        }\n\n        switch monitoredApp {\n            case .adobeaftereffect:\n                return .designing\n            case .adobebridge:\n                return .designing\n            case .adobeillustrator:\n                return .designing\n            case .adobemediaencoder:\n                return .designing\n            case .adobephotoshop:\n                return .designing\n            case .adobepremierepro:\n                return .designing\n            case .arcbrowser:\n                return .browsing\n            case .beeper:\n                return .communicating\n            case .brave:\n                return .browsing\n            case .canva:\n                return .designing\n            case .chrome, .chromebeta, .chromecanary:\n                return .browsing\n            case .figma:\n                return .designing\n            case .firefox:\n                return .browsing\n            case .github:\n                return .codereviewing\n            case .imessage:\n                return .communicating\n            case .inkscape:\n                return .designing\n            case .iterm2:\n                return .coding\n            case .linear:\n                return .planning\n            case .miro:\n                return .planning\n            case .notes:\n                return .writingdocs\n            case .notion:\n                return .writingdocs\n            case .postman:\n                return .debugging\n            case .rocketchat:\n                return .communicating\n            case .slack:\n                return .communicating\n            case .safari:\n                return .browsing\n            case .safaripreview:\n                return .browsing\n            case .tableplus:\n                return .debugging\n            case .terminal:\n                return .coding\n            case .warp:\n                return .coding\n            case .wecom:\n                return .communicating\n            case .whatsapp:\n                return .meeting\n            case .xcode:\n                fatalError(\"\\(monitoredApp.rawValue) should never use window title\")\n            case .zoom:\n                return .meeting\n            case .zed:\n                return .coding\n        }\n    }\n    // swiftlint:enable cyclomatic_complexity\n\n    static func category(from url: String) -> Category {\n        let patterns = [\n            \"github.com/[^/]+/[^/]+/pull/.*$\",\n            \"gitlab.com/[^/]+/[^/]+/[^/]+/merge_requests/.*$\",\n            \"bitbucket.org/[^/]+/[^/]+/pull-requests/.*$\",\n        ]\n\n        for pattern in patterns {\n            do {\n                let regex = try NSRegularExpression(pattern: pattern)\n                let nsrange = NSRange(url.startIndex..<url.endIndex, in: url)\n                if regex.firstMatch(in: url, options: [], range: nsrange) != nil {\n                    return .codereviewing\n                }\n            } catch {\n                Logging.default.log(\"Regex error: \\(error)\")\n                continue\n            }\n        }\n\n        return .coding\n    }\n\n    static func project(for app: NSRunningApplication, _ element: AXUIElement) -> String? {\n        guard let monitoredApp = app.monitoredApp else {\n            guard let url = currentBrowserUrl(for: app, element) else { return nil }\n            return project(from: url)\n        }\n\n        // TODO: detect repo from GitHub Desktop Client if possible\n        switch monitoredApp {\n            case .slack:\n                return extractSuffix(element.rawTitle, separator: \" - \", offset: 1)\n            case .zed:\n                return extractSuffix(element.rawTitle, separator: \" — \")\n            default:\n                guard let url = currentBrowserUrl(for: app, element) else { return nil }\n                return project(from: url)\n        }\n    }\n\n    struct Pattern {\n        var expression: String\n        var group: Int\n    }\n\n    static func project(from url: String) -> String? {\n        let patterns: [Pattern] = [\n            Pattern(expression: \"github.com/[^/]+/([^/]+)/?.*$\", group: 1),\n            Pattern(expression: \"gitlab.com/[^/]+/([^/]+)/?.*$\", group: 1),\n            Pattern(expression: \"bitbucket.org/[^/]+/([^/]+)/?.*$\", group: 1),\n            Pattern(expression: \"app.circleci.com/.*/?(github|bitbucket|gitlab)/[^/]+/([^/]+)/?.*$\", group: 2),\n            Pattern(expression: \"app.travis-ci.com/(github|bitbucket|gitlab)/[^/]+/([^/]+)/?.*$\", group: 2),\n            Pattern(expression: \"app.travis-ci.org/(github|bitbucket|gitlab)/[^/]+/([^/]+)/?.*$\", group: 2)\n        ]\n\n        for pattern in patterns {\n            do {\n                let regex = try NSRegularExpression(pattern: pattern.expression)\n                let nsrange = NSRange(url.startIndex..<url.endIndex, in: url)\n                if let match = regex.firstMatch(in: url, options: [], range: nsrange) {\n                    // Adjusted to capture the right group based on the pattern.\n                    // The group index might be 2 if the pattern includes a platform prefix before the project name.\n                    let range = match.range(at: pattern.group)\n\n                    if range.location != NSNotFound, let range = Range(range, in: url) {\n                        return String(url[range])\n                    }\n                }\n            } catch {\n                Logging.default.log(\"Regex error: \\(error)\")\n                continue\n            }\n        }\n\n        // Return nil if no pattern matches\n        return nil\n    }\n\n    static func language(for app: NSRunningApplication, _ element: AXUIElement) -> String? {\n        guard let monitoredApp = app.monitoredApp else { return nil }\n\n        switch monitoredApp {\n            case .canva:\n                return \"Image (svg)\"\n            case .chrome, .chromebeta, .chromecanary:\n                do {\n                    guard let url = currentBrowserUrl(for: app, element) else { return nil }\n\n                    let regex = try NSRegularExpression(pattern: \"github.com/[^/]+/[^/]+/?$\")\n                    let nsrange = NSRange(url.startIndex..<url.endIndex, in: url)\n                    if regex.firstMatch(in: url, options: [], range: nsrange) != nil {\n                        let languages = element.firstDescendantWhere { $0.role == \"AXStaticText\" && $0.value == \"Languages\" }\n                        guard let languages = languages else { return nil }\n\n                        guard let wrapper = languages.parent?.parent else { return nil }\n\n                        let langList = wrapper.firstDescendantWhere { $0.role == \"AXList\" }\n                        guard let langList = langList else { return nil }\n\n                        let link = langList.firstDescendantWhere { $0.role == \"AXLink\" }\n                        guard let link = link else { return nil }\n\n                        let lang = link.firstDescendantWhere { $0.role == \"AXStaticText\" }\n                        guard let lang = lang else { return nil }\n\n                        return lang.value\n                    }\n\n                    return nil\n                } catch {\n                    Logging.default.log(\"Error parsing language from browser: \\(error)\")\n                    return nil\n                }\n            case .figma:\n                return \"Image (svg)\"\n            case .inkscape:\n                return \"Image (svg)\"\n            case .postman:\n                return \"HTTP Request\"\n            default:\n                return nil\n        }\n    }\n\n    static func currentBrowserUrl(for app: NSRunningApplication, _ element: AXUIElement) -> String? {\n        guard let monitoredApp = app.monitoredApp else { return nil }\n\n        var address: String?\n        switch monitoredApp {\n            case .arcbrowser:\n                let addressField = element.findAddressField()\n                address = addressField?.value\n            case .brave:\n                let addressField = element.findAddressField()\n                address = addressField?.value\n            case .chrome, .chromebeta, .chromecanary:\n                let addressField = element.findAddressField()\n                address = addressField?.value\n            case .firefox:\n                let addressField = element.findAddressField()\n                address = addressField?.value\n            case .linear:\n                let projectLabel = element.firstDescendantWhere { $0.value == \"Project\" }\n                let projectButton = projectLabel?.nextSibling?.firstDescendantWhere { $0.role == kAXButtonRole }\n                return projectButton?.rawTitle\n            case .safari:\n                let addressField = element.elementById(identifier: \"WEB_BROWSER_ADDRESS_AND_SEARCH_FIELD\")\n                address = addressField?.value\n            case .safaripreview:\n                let addressField = element.elementById(identifier: \"WEB_BROWSER_ADDRESS_AND_SEARCH_FIELD\")\n                address = addressField?.value\n            default: return nil\n        }\n        return address\n    }\n\n    static func extractPrefix(_ str: String?, separator: String? = nil, minCount: Int? = nil, fullTitle: Bool = false) -> String? {\n        guard let str = str else { return nil }\n\n        guard let separator = separator else {\n            return getFirstPrefixMatch(str)\n        }\n\n        let parts = str.components(separatedBy: separator)\n        guard !parts.isEmpty else { return nil }\n        guard let item = parts.first else { return nil }\n\n        if let minCount = minCount, minCount > 0, parts.count < minCount {\n            return nil\n        }\n\n        if item.trimmingCharacters(in: .whitespacesAndNewlines) != \"\" {\n            if fullTitle {\n                return str.trimmingCharacters(in: .whitespacesAndNewlines)\n            }\n            return item.trimmingCharacters(in: .whitespacesAndNewlines)\n        }\n        return nil\n    }\n\n    static func extractSuffix(_ str: String?, separator: String? = nil, offset: Int = 0) -> String? {\n        guard let str = str else { return nil }\n\n        guard let separator = separator else {\n            return getFirstSuffixMatch(str)\n        }\n\n        var parts = str.components(separatedBy: separator)\n        guard !parts.isEmpty else { return nil }\n        guard parts.count > 1 else { return nil }\n\n        var i = offset\n        while i > 0 {\n            guard parts.count > 1 else { return nil }\n\n            parts.removeLast()\n            i += 1\n        }\n        guard let item = parts.last else { return nil }\n\n        if item.trimmingCharacters(in: .whitespacesAndNewlines) != \"\" {\n            return item.trimmingCharacters(in: .whitespacesAndNewlines)\n        }\n\n        return nil\n    }\n\n    static func domainFromUrl(_ url: String) -> String? {\n        guard let host = URL(stringWithoutScheme: url)?.host else { return nil }\n        let domain = host.replacingOccurrences(of: \"^www.\", with: \"\", options: .regularExpression)\n        guard let port = URL(stringWithoutScheme: url)?.port else { return domain }\n        return \"\\(domain):\\(port)\"\n    }\n\n    static let separators = [\n        \"-\",\n        \"᠆\",\n        \"‐\",\n        \"‑\",\n        \"‒\",\n        \"–\",\n        \"—\",\n        \"―\",\n        \"⸺\",\n        \"⸻\",\n        \"︱\",\n        \"︲\",\n        \"﹘\",\n        \"﹣\",\n        \"－\",\n    ]\n\n    static func getFirstPrefixMatch(_ str: String) -> String {\n        guard !str.isEmpty else { return str.trimmingCharacters(in: .whitespacesAndNewlines) }\n\n        for separator in separators {\n            let parts = str.components(separatedBy: separator)\n            guard parts.count > 1 else { continue }\n            guard let item = parts.first else { continue }\n\n            let trimmed = item.trimmingCharacters(in: .whitespacesAndNewlines)\n            guard !trimmed.isEmpty else { continue }\n\n            return trimmed\n        }\n\n        return str.trimmingCharacters(in: .whitespacesAndNewlines)\n    }\n\n    static func getFirstSuffixMatch(_ str: String) -> String {\n        guard !str.isEmpty else { return str.trimmingCharacters(in: .whitespacesAndNewlines) }\n\n        for separator in separators {\n            let parts = str.components(separatedBy: separator)\n            guard parts.count > 1 else { continue }\n            guard let item = parts.last else { continue }\n\n            let trimmed = item.trimmingCharacters(in: .whitespacesAndNewlines)\n            guard !trimmed.isEmpty else { continue }\n\n            return trimmed\n        }\n\n        return str.trimmingCharacters(in: .whitespacesAndNewlines)\n    }\n}\n\nstruct HeartbeatData {\n    var entity: String\n    var entityType: EntityType\n    var project: String?\n    var language: String?\n    var category: Category?\n}\n"
  },
  {
    "path": "WakaTime/Helpers/PropertiesManager.swift",
    "content": "import Foundation\n\nclass PropertiesManager {\n    enum DomainPreferenceType: String {\n        case domain\n        case url\n    }\n\n    enum FilterType: String {\n        case denylist\n        case allowlist\n    }\n\n    enum Keys: String {\n        case shouldLaunchOnLogin = \"launch_on_login\"\n        case shouldLogToFile = \"log_to_file\"\n        case shouldRequestA11y = \"request_a11y\"\n        case shouldAutomaticallyDownloadUpdates = \"should_automatically_download_updates\"\n        case hasLaunchedBefore = \"has_launched_before\"\n        case shouldDisplayTodayInStatusBar = \"status_bar_text\"\n        case domainPreference = \"domain_preference\"\n        case filterType = \"filter_type\"\n        case denylist = \"denylist\"\n        case allowlist = \"allowlist\"\n    }\n\n    static var shouldLaunchOnLogin: Bool {\n        get {\n            guard UserDefaults.standard.string(forKey: Keys.shouldLaunchOnLogin.rawValue) != nil else {\n                UserDefaults.standard.set(true, forKey: Keys.shouldLaunchOnLogin.rawValue)\n                return true\n            }\n\n            return UserDefaults.standard.bool(forKey: Keys.shouldLaunchOnLogin.rawValue)\n        }\n        set {\n            UserDefaults.standard.set(newValue, forKey: Keys.shouldLaunchOnLogin.rawValue)\n            UserDefaults.standard.synchronize()\n        }\n    }\n\n    static var shouldLogToFile: Bool {\n        get {\n            guard UserDefaults.standard.string(forKey: Keys.shouldLogToFile.rawValue) != nil else {\n                UserDefaults.standard.set(false, forKey: Keys.shouldLogToFile.rawValue)\n                return false\n            }\n\n            return UserDefaults.standard.bool(forKey: Keys.shouldLogToFile.rawValue)\n        }\n        set {\n            UserDefaults.standard.set(newValue, forKey: Keys.shouldLogToFile.rawValue)\n            UserDefaults.standard.synchronize()\n            if newValue {\n                Logging.default.activateLoggingToFile()\n            } else {\n                Logging.default.deactivateLoggingToFile()\n            }\n        }\n    }\n\n    static var shouldAutomaticallyDownloadUpdates: Bool {\n        get {\n            guard UserDefaults.standard.string(forKey: Keys.shouldAutomaticallyDownloadUpdates.rawValue) != nil else {\n                UserDefaults.standard.set(true, forKey: Keys.shouldAutomaticallyDownloadUpdates.rawValue)\n                return true\n            }\n\n            return UserDefaults.standard.bool(forKey: Keys.shouldAutomaticallyDownloadUpdates.rawValue)\n        }\n        set {\n            UserDefaults.standard.set(newValue, forKey: Keys.shouldAutomaticallyDownloadUpdates.rawValue)\n            UserDefaults.standard.synchronize()\n        }\n    }\n\n    static var shouldRequestA11yPermission: Bool {\n        get {\n            guard UserDefaults.standard.string(forKey: Keys.shouldRequestA11y.rawValue) != nil else {\n                UserDefaults.standard.set(true, forKey: Keys.shouldRequestA11y.rawValue)\n                return true\n            }\n\n            return UserDefaults.standard.bool(forKey: Keys.shouldRequestA11y.rawValue)\n        }\n        set {\n            UserDefaults.standard.set(newValue, forKey: Keys.shouldRequestA11y.rawValue)\n            UserDefaults.standard.synchronize()\n        }\n    }\n\n    static var shouldDisplayTodayInStatusBar: Bool {\n        get {\n            guard UserDefaults.standard.string(forKey: Keys.shouldDisplayTodayInStatusBar.rawValue) != nil else {\n                UserDefaults.standard.set(true, forKey: Keys.shouldDisplayTodayInStatusBar.rawValue)\n                return true\n            }\n\n            return UserDefaults.standard.bool(forKey: Keys.shouldDisplayTodayInStatusBar.rawValue)\n        }\n        set {\n            UserDefaults.standard.set(newValue, forKey: Keys.shouldDisplayTodayInStatusBar.rawValue)\n            UserDefaults.standard.synchronize()\n        }\n    }\n\n    static var hasLaunchedBefore: Bool {\n        get {\n            guard UserDefaults.standard.string(forKey: Keys.hasLaunchedBefore.rawValue) != nil else {\n                return false\n            }\n\n            return UserDefaults.standard.bool(forKey: Keys.hasLaunchedBefore.rawValue)\n        }\n        set {\n            UserDefaults.standard.set(newValue, forKey: Keys.hasLaunchedBefore.rawValue)\n            UserDefaults.standard.synchronize()\n        }\n    }\n\n    static var domainPreference: DomainPreferenceType {\n        get {\n            guard let domainPreferenceString = UserDefaults.standard.string(forKey: Keys.domainPreference.rawValue) else {\n                return .domain\n            }\n\n            return DomainPreferenceType(rawValue: domainPreferenceString) ?? .domain\n        }\n        set {\n            UserDefaults.standard.set(newValue.rawValue, forKey: Keys.domainPreference.rawValue)\n            UserDefaults.standard.synchronize()\n        }\n    }\n\n    static var filterType: FilterType {\n        get {\n            guard let filterTypeString = UserDefaults.standard.string(forKey: Keys.filterType.rawValue) else {\n                return .allowlist\n            }\n\n            return FilterType(rawValue: filterTypeString) ?? .denylist\n        }\n        set {\n            UserDefaults.standard.set(newValue.rawValue, forKey: Keys.filterType.rawValue)\n            UserDefaults.standard.synchronize()\n        }\n    }\n\n    static var denylist: String {\n        get {\n            guard let denylist = UserDefaults.standard.string(forKey: Keys.denylist.rawValue) else {\n                return \"\"\n            }\n\n            return denylist\n        }\n        set {\n            UserDefaults.standard.set(newValue, forKey: Keys.denylist.rawValue)\n            UserDefaults.standard.synchronize()\n        }\n    }\n\n    static var allowlist: String {\n        get {\n            guard let allowlist = UserDefaults.standard.string(forKey: Keys.allowlist.rawValue) else {\n                return\n                    \"https?://(\\\\w\\\\.)*github\\\\.com/\\n\" +\n                    \"https?://(\\\\w\\\\.)*gitlab\\\\.com/\\n\" +\n                    \"^stackoverflow\\\\.com/\\n\" +\n                    \"^docs\\\\.python\\\\.org/\\n\" +\n                    \"https?://(\\\\w\\\\.)*golang\\\\.org/\\n\" +\n                    \"https?://(\\\\w\\\\.)*go\\\\.dev/\\n\" +\n                    \"https?://(\\\\w\\\\.)*npmjs\\\\.com/\\n\" +\n                    \"https?//localhost[:\\\\d+]?/\"\n            }\n\n            return allowlist\n        }\n        set {\n            UserDefaults.standard.set(newValue, forKey: Keys.allowlist.rawValue)\n            UserDefaults.standard.synchronize()\n        }\n    }\n\n    static var currentFilterList: String {\n        switch Self.filterType {\n            case .denylist: return Self.denylist\n            case .allowlist: return Self.allowlist\n        }\n    }\n}\n"
  },
  {
    "path": "WakaTime/Helpers/SettingsManager.swift",
    "content": "import Foundation\nimport ServiceManagement\n\nclass SettingsManager {\n#if !SIMULATE_OLD_MACOS\n    static let simulateOldMacOS = false\n#else\n    static let simulateOldMacOS = true\n#endif\n\n    static func loginItemRegistered() -> Bool {\n        if #available(macOS 13.0, *) {\n            return SMAppService.mainApp.status != .notFound\n        } else {\n            return false\n        }\n    }\n\n    static func shouldRegisterAsLoginItem() -> Bool {\n        guard\n            !loginItemRegistered(),\n            PropertiesManager.shouldLaunchOnLogin\n        else { return false }\n\n        return !Dependencies.isLocalDevBuild\n    }\n\n    static func registerAsLoginItem() {\n        PropertiesManager.shouldLaunchOnLogin = true\n\n        // Use SMAppService on macOS 13 or newer to add WakaTime to the \"Open at Login\" list and SMLoginItemSetEnabled\n        // for older versions of macOS to add WakaTime to the \"Allow in Background\" list\n        if #available(macOS 13.0, *), !simulateOldMacOS {\n            do {\n                try SMAppService.mainApp.register()\n                Logging.default.log(\"Registered for login\")\n            } catch let error {\n                Logging.default.log(error.localizedDescription)\n            }\n        } else {\n            if SMLoginItemSetEnabled(\"macos-wakatime.WakaTimeHelper\" as CFString, true) {\n                Logging.default.log(\"Login item enabled successfully.\")\n            } else {\n                Logging.default.log(\"Failed to enable login item.\")\n            }\n        }\n    }\n\n    static func unregisterAsLoginItem() {\n        PropertiesManager.shouldLaunchOnLogin = false\n\n        if #available(macOS 13.0, *), !simulateOldMacOS {\n            do {\n                try SMAppService.mainApp.unregister()\n                Logging.default.log(\"Unregistered for login\")\n            } catch let error {\n                Logging.default.log(error.localizedDescription)\n            }\n        } else {\n            if SMLoginItemSetEnabled(\"macos-wakatime.WakaTimeHelper\" as CFString, false) {\n                Logging.default.log(\"Login item disabled successfully.\")\n            } else {\n                Logging.default.log(\"Failed to disable login item.\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "WakaTime/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"16.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"16x16\"\n    },\n    {\n      \"filename\" : \"32.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"16x16\"\n    },\n    {\n      \"filename\" : \"32.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"32x32\"\n    },\n    {\n      \"filename\" : \"64.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"32x32\"\n    },\n    {\n      \"filename\" : \"128.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"128x128\"\n    },\n    {\n      \"filename\" : \"256.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"128x128\"\n    },\n    {\n      \"filename\" : \"256.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"256x256\"\n    },\n    {\n      \"filename\" : \"512.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"256x256\"\n    },\n    {\n      \"filename\" : \"512.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"512x512\"\n    },\n    {\n      \"filename\" : \"1024.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"512x512\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "WakaTime/Resources/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "WakaTime/Resources/Assets.xcassets/WakaTime.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"32.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"64.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "WakaTime/Resources/Assets.xcassets/WakaTimeDisabled.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"32.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"64.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "WakaTime/Resources/GoogleService-Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CLIENT_ID</key>\n\t<string>473173879994-q78fldrplnkhrr4oa5h10mkv15g1ng2g.apps.googleusercontent.com</string>\n\t<key>REVERSED_CLIENT_ID</key>\n\t<string>com.googleusercontent.apps.473173879994-q78fldrplnkhrr4oa5h10mkv15g1ng2g</string>\n\t<key>API_KEY</key>\n\t<string>AIzaSyDBdPD7ZIMm7XtDueuhOBd-rx7kF3jIc0U</string>\n\t<key>GCM_SENDER_ID</key>\n\t<string>473173879994</string>\n\t<key>PLIST_VERSION</key>\n\t<string>1</string>\n\t<key>BUNDLE_ID</key>\n\t<string>macos-wakatime.WakaTime</string>\n\t<key>PROJECT_ID</key>\n\t<string>wakatime-macos-desktop-app</string>\n\t<key>STORAGE_BUCKET</key>\n\t<string>wakatime-macos-desktop-app.appspot.com</string>\n\t<key>IS_ADS_ENABLED</key>\n\t<false></false>\n\t<key>IS_ANALYTICS_ENABLED</key>\n\t<false></false>\n\t<key>IS_APPINVITE_ENABLED</key>\n\t<true></true>\n\t<key>IS_GCM_ENABLED</key>\n\t<true></true>\n\t<key>IS_SIGNIN_ENABLED</key>\n\t<true></true>\n\t<key>GOOGLE_APP_ID</key>\n\t<string>1:473173879994:ios:c9a7680a9e365351282683</string>\n</dict>\n</plist>"
  },
  {
    "path": "WakaTime/Utils/Atomic.swift",
    "content": "import Foundation\n\n@propertyWrapper\nstruct Atomic<Value> {\n    private var value: Value\n    private let lock = NSLock()\n\n    init(wrappedValue value: Value) {\n        self.value = value\n    }\n\n    var wrappedValue: Value {\n      get { getValue() }\n      set { setValue(newValue) }\n    }\n\n    func getValue() -> Value {\n        lock.lock()\n        defer { lock.unlock() }\n        return value\n    }\n\n    mutating func setValue(_ newValue: Value) {\n        lock.lock()\n        defer { lock.unlock() }\n        value = newValue\n    }\n}\n"
  },
  {
    "path": "WakaTime/Utils/Logging.swift",
    "content": "import Foundation\nimport os.log\n\nclass Logging {\n    static let `default` = Logging()\n    private var filePath: String?\n\n    private init() {}\n\n    // Configures logging to also write to a file at the given path.\n    func configure(filePath: String) {\n        self.filePath = filePath\n    }\n\n    func activateLoggingToFile() {\n        let userHome = FileManager.default.homeDirectoryForCurrentUser.pathComponents\n        let logFilePath = NSString.path(withComponents: userHome + [\".wakatime\", \"macos-wakatime.log\"])\n        configure(filePath: logFilePath)\n    }\n\n    func deactivateLoggingToFile() {\n        filePath = nil\n    }\n\n    func log(_ message: String, type: OSLogType = .default) {\n        os_log(\"%{public}@\", log: .default, type: type, message)\n\n        if let filePath {\n            let dateFormatter = DateFormatter()\n            dateFormatter.dateFormat = \"yyyy-MM-dd HH:mm:ss.SSS\"\n            let timestamp = dateFormatter.string(from: Date())\n            let logMessage = \"\\(timestamp): \\(message)\\n\"\n\n            // Attempt to append the log message to the log file\n            if let fileHandle = FileHandle(forWritingAtPath: filePath) {\n                fileHandle.seekToEndOfFile()\n                if let data = logMessage.data(using: .utf8) {\n                    fileHandle.write(data)\n                }\n                fileHandle.closeFile()\n            } else {\n                // If the file does not exist, create it\n                try? logMessage.write(toFile: filePath, atomically: true, encoding: .utf8)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "WakaTime/Utils/ObjC.h",
    "content": "#ifndef ObjC_h\n#define ObjC_h\n\n#import <Foundation/Foundation.h>\n\n@interface ObjC : NSObject\n\n+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error;\n\n@end\n\n#endif /* ObjC_h */\n"
  },
  {
    "path": "WakaTime/Utils/ObjC.m",
    "content": "#import <Foundation/Foundation.h>\n\n#import \"ObjC.h\"\n\n@implementation ObjC\n\n+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error {\n    @try {\n        tryBlock();\n        return YES;\n    }\n    @catch (NSException *exception) {\n        *error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:exception.userInfo];\n        return NO;\n    }\n}\n\n@end\n"
  },
  {
    "path": "WakaTime/Views/MonitoredAppsView.swift",
    "content": "import AppKit\n\nclass MonitoredAppsView: NSView, NSOutlineViewDataSource, NSOutlineViewDelegate {\n    struct AppData: Equatable {\n        let bundleId: String\n        let icon: NSImage\n        let name: String\n        let tag: Int\n    }\n\n    private var outlineView: NSOutlineView!\n    private var runningApps: [AppData] = []\n\n    private func refreshRunningApps() {\n        var apps = [AppData]()\n        let bundleIds = sort(Array(Set(MonitoredApp.allBundleIds + getRunningApps() + MonitoringManager.allMonitoredApps)))\n        var index = 0\n        for bundleId in bundleIds {\n            if let icon = AppInfo.getIcon(bundleId: bundleId),\n               let name = AppInfo.getAppName(bundleId: bundleId) {\n                apps.append(AppData(bundleId: bundleId, icon: icon, name: name, tag: index))\n                index += 1\n            }\n\n            let setAppBundleId = bundleId.appending(\"-setapp\")\n            if let icon = AppInfo.getIcon(bundleId: setAppBundleId),\n               let name = AppInfo.getAppName(bundleId: setAppBundleId) {\n                apps.append(AppData(bundleId: setAppBundleId, icon: icon, name: name, tag: index))\n                index += 1\n            }\n        }\n        runningApps = apps\n    }\n\n    private func getRunningApps() -> [String] {\n        var ids: [String] = []\n        for runningApp in NSWorkspace.shared.runningApplications where runningApp.activationPolicy == .regular {\n            guard let id = runningApp.bundleIdentifier else { continue }\n\n            let bundleId = id.replacingOccurrences(of: \"-setapp$\", with: \"\", options: .regularExpression)\n\n            guard\n                !MonitoredApp.unsupportedAppIds.contains(where: { $0 == bundleId }),\n                !MonitoredApp.allBundleIds.contains(where: { $0 == bundleId })\n            else { continue }\n\n            ids.append(bundleId)\n        }\n        return ids\n    }\n\n    private func sort(_ bundleIds: [String]) -> [String] {\n        bundleIds.sorted {\n            let left = AppInfo.getAppName(bundleId: $0) ?? $0\n            let right = AppInfo.getAppName(bundleId: $1) ?? $1\n            return left.localizedCaseInsensitiveCompare(right) == ComparisonResult.orderedAscending\n        }\n    }\n\n    override init(frame frameRect: NSRect) {\n        super.init(frame: frameRect)\n\n        setupOutlineView()\n    }\n\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n\n    private func setupOutlineView() {\n        let scrollView = NSScrollView()\n        scrollView.hasVerticalScroller = true\n        outlineView = NSOutlineView()\n        outlineView.dataSource = self\n        outlineView.delegate = self\n\n        scrollView.documentView = outlineView\n        addSubview(scrollView)\n\n        scrollView.translatesAutoresizingMaskIntoConstraints = false\n        NSLayoutConstraint.activate([\n            scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),\n            scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),\n            scrollView.topAnchor.constraint(equalTo: topAnchor),\n            scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),\n        ])\n\n        let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(\"AppColumn\"))\n        outlineView.addTableColumn(column)\n        outlineView.headerView = nil\n        outlineView.outlineTableColumn = column\n        outlineView.indentationPerLevel = 0.0\n    }\n\n    func reloadData() {\n        refreshRunningApps()\n        outlineView.reloadData()\n    }\n\n    // MARK: NSOutlineViewDataSource\n\n    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {\n        runningApps.count\n    }\n\n    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {\n        false\n    }\n\n    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {\n        runningApps[index]\n    }\n\n    // MARK: NSOutlineViewDelegate\n\n    func outlineView(_ outlineView: NSOutlineView, shouldSelectItem item: Any) -> Bool {\n        false\n    }\n\n    func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {\n        50\n    }\n\n    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {\n        guard let appData = item as? AppData else { return nil }\n\n        let cellView = outlineView.makeView(\n          withIdentifier: NSUserInterfaceItemIdentifier(\"AppCell\"),\n          owner: self\n        ) as? NSTableCellView ?? NSTableCellView()\n\n        // Clear existing subviews to prevent duplication\n        cellView.subviews.forEach { $0.removeFromSuperview() }\n\n        let imageView = NSImageView()\n        imageView.translatesAutoresizingMaskIntoConstraints = false\n        imageView.image = appData.icon\n        imageView.image?.size = NSSize(width: 20, height: 20)\n\n        let nameLabel = NSTextField(labelWithString: appData.name)\n        nameLabel.translatesAutoresizingMaskIntoConstraints = false\n\n        let action = switchOrLink(appData)\n\n        cellView.addSubview(imageView)\n        cellView.addSubview(nameLabel)\n        cellView.addSubview(action)\n\n        // Determine if the current item is the last in the list\n        let isLastItem = runningApps.last == appData\n\n        if !isLastItem {\n            let divider = NSView()\n            divider.translatesAutoresizingMaskIntoConstraints = false\n            divider.wantsLayer = true\n            divider.layer?.backgroundColor = NSColor.separatorColor.cgColor\n\n            cellView.addSubview(divider)\n\n            NSLayoutConstraint.activate([\n                divider.heightAnchor.constraint(equalToConstant: 1),\n                divider.leadingAnchor.constraint(equalTo: cellView.leadingAnchor),\n                divider.trailingAnchor.constraint(equalTo: cellView.trailingAnchor),\n                divider.bottomAnchor.constraint(equalTo: cellView.bottomAnchor)\n            ])\n        }\n\n        NSLayoutConstraint.activate([\n            imageView.leadingAnchor.constraint(equalTo: cellView.leadingAnchor, constant: 5),\n            imageView.centerYAnchor.constraint(equalTo: cellView.centerYAnchor),\n            imageView.widthAnchor.constraint(equalToConstant: 20),\n            imageView.heightAnchor.constraint(equalToConstant: 20),\n\n            nameLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10),\n            nameLabel.centerYAnchor.constraint(equalTo: cellView.centerYAnchor),\n            nameLabel.trailingAnchor.constraint(equalTo: action.leadingAnchor, constant: -5),\n\n            action.trailingAnchor.constraint(equalTo: cellView.trailingAnchor, constant: -10),\n            action.centerYAnchor.constraint(equalTo: cellView.centerYAnchor),\n        ])\n\n        return cellView\n    }\n\n    func switchOrLink(_ appData: AppData) -> NSView {\n        if MonitoredApp.pluginAppIds[appData.bundleId] != nil {\n            let button = NSButton()\n            button.translatesAutoresizingMaskIntoConstraints = false\n            button.bezelStyle = NSButton.BezelStyle.rounded\n            button.title = \"Install plugin\"\n            button.action = #selector(clickInstallPlugin(_:))\n            button.widthAnchor.constraint(equalToConstant: 100).isActive = true\n            button.tag = appData.tag\n            return button\n        }\n\n        let isMonitored = MonitoringManager.isAppMonitored(for: appData.bundleId)\n        let switchControl = NSSwitch()\n        switchControl.translatesAutoresizingMaskIntoConstraints = false\n        switchControl.state = isMonitored ? .on : .off\n        switchControl.target = self\n        switchControl.action = #selector(switchToggled(_:))\n        switchControl.tag = appData.tag\n        return switchControl\n    }\n\n    @objc func switchToggled(_ sender: NSSwitch) {\n        let appData = runningApps[sender.tag]\n        MonitoringManager.set(monitoringState: sender.state == .on ? .on : .off, for: appData.bundleId)\n    }\n\n    @objc func clickInstallPlugin(_ sender: NSButton) {\n        let appData = runningApps[sender.tag]\n        guard\n            let path = MonitoredApp.pluginAppIds[appData.bundleId],\n            let url = URL(string: \"https://wakatime.com/\\(path)\")\n        else { return }\n\n        NSWorkspace.shared.open(url)\n    }\n}\n"
  },
  {
    "path": "WakaTime/Views/SettingsView.swift",
    "content": "import AppKit\n\nclass SettingsView: NSView, NSTextFieldDelegate, NSTextViewDelegate {\n    var delegate: StatusBarDelegate?\n\n    // MARK: API Key\n\n    lazy var apiKeyLabel: NSTextField = {\n        NSTextField(labelWithString: \"WakaTime API Key:\")\n    }()\n\n    lazy var apiKeyTextField: WKTextField = {\n        let textField = WKTextField(frame: .zero)\n        textField.stringValue = ConfigFile.getSetting(section: \"settings\", key: \"api_key\") ?? \"\"\n        textField.delegate = self\n        return textField\n    }()\n\n    lazy var apiKeyStackView: NSStackView = {\n        let stack = NSStackView(views: [apiKeyLabel, apiKeyTextField])\n        stack.alignment = .leading\n        stack.orientation = .vertical\n        stack.spacing = 5\n        return stack\n    }()\n\n    // MARK: Checkboxes\n\n    lazy var launchAtLoginCheckbox: NSButton = {\n        let checkbox = NSButton(\n            checkboxWithTitle: \"Launch at login\",\n            target: self,\n            action: #selector(launchAtLoginCheckboxClicked)\n        )\n        checkbox.state = PropertiesManager.shouldLaunchOnLogin ? .on : .off\n        return checkbox\n    }()\n\n    lazy var enableLoggingCheckbox: NSButton = {\n        let checkbox = NSButton(\n            checkboxWithTitle: \"Enable logging to ~/.wakatime/macos-wakatime.log\",\n            target: self,\n            action: #selector(enableLoggingCheckboxClicked)\n        )\n        checkbox.state = PropertiesManager.shouldLogToFile ? .on : .off\n        return checkbox\n    }()\n\n    lazy var statusBarTextCheckbox: NSButton = {\n        let checkbox = NSButton(\n            checkboxWithTitle: \"Show today’s time in status bar\",\n            target: self,\n            action: #selector(enableStatusBarTextCheckboxClicked)\n        )\n        checkbox.state = PropertiesManager.shouldDisplayTodayInStatusBar ? .on : .off\n        return checkbox\n    }()\n\n    lazy var requestA11yCheckbox: NSButton = {\n        let checkbox = NSButton(\n            checkboxWithTitle: \"Enable stats from Xcode by requesting accessibility permission\",\n            target: self,\n            action: #selector(enableA11yCheckboxClicked)\n        )\n        checkbox.state = PropertiesManager.shouldRequestA11yPermission ? .on : .off\n        return checkbox\n    }()\n\n    lazy var checkboxesStackView: NSStackView = {\n        let stack = NSStackView(views: [launchAtLoginCheckbox, statusBarTextCheckbox, requestA11yCheckbox, enableLoggingCheckbox])\n        stack.alignment = .leading\n        stack.orientation = .vertical\n        stack.spacing = 10\n        return stack\n    }()\n\n    // MARK: Domain Preference\n\n    lazy var browserLabel: NSTextField = {\n        var label = NSTextField(labelWithString: \"The settings below are only applicable because you’ve enabled \" +\n            \"monitoring a browser in the Monitored Apps menu.\")\n        label.lineBreakMode = .byWordWrapping // Enable word wrapping\n        label.maximumNumberOfLines = 0 // Set to 0 to allow unlimited lines\n        label.preferredMaxLayoutWidth = 380\n        return label\n    }()\n\n    lazy var domainPreferenceLabel: NSTextField = {\n        NSTextField(labelWithString: \"Browser Tracking:\")\n    }()\n\n    lazy var domainPreferenceControl: NSSegmentedControl = {\n        let control = NSSegmentedControl()\n        control.segmentStyle = .texturedRounded\n        control.segmentCount = 2\n        control.setLabel(\"Domain only\", forSegment: 0)\n        control.setLabel(\"Full url\", forSegment: 1)\n        control.trackingMode = .selectOne // Ensure only one option can be selected at a time\n        control.action = #selector(domainPreferenceDidChange(_:))\n        return control\n    }()\n\n    // MARK: Denylist/Allowlist\n\n    lazy var filterTypeLabel: NSTextField = {\n        NSTextField(labelWithString: \"Browser Filter:\")\n    }()\n\n    lazy var filterSegmentedControl: NSSegmentedControl = {\n        let control = NSSegmentedControl()\n        control.segmentStyle = .texturedRounded\n        control.segmentCount = 2\n        control.setLabel(\"All except denied sites\", forSegment: 0)\n        control.setLabel(\"Only allowed sites\", forSegment: 1)\n        control.trackingMode = .selectOne // Ensure only one option can be selected at a time\n        control.action = #selector(segmentedControlDidChange(_:))\n        return control\n    }()\n\n    lazy var filterListLabel: NSTextField = {\n        NSTextField(labelWithString: \"\")\n    }()\n\n    lazy var filterTextView: NSTextView = {\n        let textView = NSTextView()\n        textView.isEditable = true\n        textView.isRichText = false\n        textView.isSelectable = true\n        textView.autoresizingMask = [.width]\n        textView.isVerticallyResizable = true\n        textView.isHorizontallyResizable = false\n        textView.textContainer?.containerSize = NSSize(width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude)\n        textView.textContainer?.widthTracksTextView = true\n        textView.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)\n        textView.delegate = self\n        return textView\n    }()\n\n    lazy var filterScrollView: NSScrollView = {\n        let scrollView = NSScrollView()\n        scrollView.hasVerticalScroller = true\n        scrollView.documentView = filterTextView\n        scrollView.translatesAutoresizingMaskIntoConstraints = false\n        scrollView.heightAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive = true\n        return scrollView\n    }()\n\n    lazy var filterRemarksLabel: NSTextField = {\n        var label = NSTextField(labelWithString: \"\")\n        label.lineBreakMode = .byWordWrapping // Enable word wrapping\n        label.maximumNumberOfLines = 0 // Set to 0 to allow unlimited lines\n        label.preferredMaxLayoutWidth = 380\n        return label\n    }()\n\n    lazy var domainStackView: NSStackView = {\n        let stack = NSStackView(views: [\n            domainPreferenceLabel,\n            domainPreferenceControl\n        ])\n        stack.alignment = .leading\n        stack.orientation = .vertical\n        stack.spacing = 10\n        stack.translatesAutoresizingMaskIntoConstraints = false\n        return stack\n    }()\n\n    lazy var filterStackView: NSStackView = {\n        let stack = NSStackView(views: [\n            filterTypeLabel,\n            filterSegmentedControl,\n            filterListLabel,\n            filterScrollView,\n            filterRemarksLabel\n        ])\n        stack.alignment = .leading\n        stack.orientation = .vertical\n        stack.spacing = 10\n        stack.translatesAutoresizingMaskIntoConstraints = false\n        return stack\n    }()\n\n    // MARK: Version Label\n\n    lazy var versionLabel: NSTextField = {\n        let versionString = \"Version: \\(Bundle.main.object(forInfoDictionaryKey: \"CFBundleShortVersionString\") as? String ?? \"\")\"\n        let versionLabel = NSTextField(labelWithString: versionString)\n        return versionLabel\n    }()\n\n    lazy var stackView: NSStackView = {\n        let stackView = NSStackView(views: [\n            apiKeyStackView,\n            checkboxesStackView,\n            browserLabel,\n            domainStackView,\n            filterStackView,\n            versionLabel\n        ])\n        stackView.alignment = .leading\n        stackView.orientation = .vertical\n        stackView.spacing = 25\n        stackView.distribution = .equalSpacing\n        stackView.edgeInsets = NSEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)\n        stackView.translatesAutoresizingMaskIntoConstraints = false\n\n        stackView.addConstraint(\n            NSLayoutConstraint(\n                item: filterStackView,\n                attribute: .width,\n                relatedBy: .equal,\n                toItem: stackView,\n                attribute: .width,\n                multiplier: 1,\n                constant: -(stackView.edgeInsets.left + stackView.edgeInsets.right)\n            )\n        )\n\n        return stackView\n    }()\n\n    // MARK: Lifecycle\n\n    init() {\n        super.init(frame: .zero)\n\n        addSubview(stackView)\n        setupConstraints()\n        setBrowserVisibility()\n        updateDomainPreference(animate: false)\n        updateFilterControls(animate: false)\n    }\n\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n\n    // MARK: Callbacks\n\n    @objc func launchAtLoginCheckboxClicked() {\n        PropertiesManager.shouldLaunchOnLogin = launchAtLoginCheckbox.state == .on\n        if launchAtLoginCheckbox.state == .on {\n            SettingsManager.registerAsLoginItem()\n        } else {\n            SettingsManager.unregisterAsLoginItem()\n        }\n    }\n\n    @objc func enableLoggingCheckboxClicked() {\n        PropertiesManager.shouldLogToFile = enableLoggingCheckbox.state == .on\n        if enableLoggingCheckbox.state == .on {\n            PropertiesManager.shouldLogToFile = true\n        } else {\n            PropertiesManager.shouldLogToFile = false\n        }\n    }\n\n    @objc func enableStatusBarTextCheckboxClicked() {\n        PropertiesManager.shouldDisplayTodayInStatusBar = statusBarTextCheckbox.state == .on\n        if statusBarTextCheckbox.state == .on {\n            PropertiesManager.shouldDisplayTodayInStatusBar = true\n        } else {\n            PropertiesManager.shouldDisplayTodayInStatusBar = false\n        }\n        delegate?.fetchToday()\n    }\n\n    @objc func enableA11yCheckboxClicked() {\n        PropertiesManager.shouldRequestA11yPermission = requestA11yCheckbox.state == .on\n        if requestA11yCheckbox.state == .on {\n            PropertiesManager.shouldRequestA11yPermission = true\n        } else {\n            PropertiesManager.shouldRequestA11yPermission = false\n        }\n    }\n\n    @objc func domainPreferenceDidChange(_ sender: NSSegmentedControl) {\n        PropertiesManager.domainPreference = sender.selectedSegment == 0 ? .domain : .url\n        updateDomainPreference(animate: true)\n    }\n\n    @objc func segmentedControlDidChange(_ sender: NSSegmentedControl) {\n        PropertiesManager.filterType = sender.selectedSegment == 0 ? .denylist : .allowlist\n        updateFilterControls(animate: true)\n    }\n\n    // MARK: NSTextFieldDelegate\n\n    func controlTextDidChange(_ obj: Notification) {\n        ConfigFile.setSetting(section: \"settings\", key: \"api_key\", val: apiKeyTextField.stringValue)\n    }\n\n    // MARK: NSTextViewDelegate\n\n    func textDidChange(_ notification: Notification) {\n        guard let textView = notification.object as? NSTextView else { return }\n\n        switch PropertiesManager.filterType {\n            case .denylist:\n                PropertiesManager.denylist = textView.string\n            case .allowlist:\n                PropertiesManager.allowlist = textView.string\n        }\n    }\n\n    // MARK: Constraints\n\n    private func setupConstraints() {\n        NSLayoutConstraint.activate([\n            stackView.topAnchor.constraint(equalTo: topAnchor, constant: 20),\n            stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),\n            stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),\n        ])\n    }\n\n    func setBrowserVisibility() {\n        if MonitoringManager.isMonitoringBrowsing {\n            browserLabel.isHidden = false\n            domainStackView.isHidden = false\n            filterStackView.isHidden = false\n        } else {\n            browserLabel.isHidden = true\n            domainStackView.isHidden = true\n            filterStackView.isHidden = true\n        }\n        adjustWindowSize(animate: false)\n    }\n\n    // MARK: State Helpers\n\n    private func updateDomainPreference(animate: Bool) {\n        var selectedSegment: Int\n        switch PropertiesManager.domainPreference {\n            case .domain:\n                selectedSegment = 0\n            case .url:\n                selectedSegment = 1\n        }\n        domainPreferenceControl.setSelected(true, forSegment: selectedSegment)\n        adjustWindowSize(animate: animate)\n    }\n\n    private func updateFilterControls(animate: Bool) {\n        let denylistTitle = \"Denylist:\"\n        let denylistRemarks =\n            \"Sites that you don't want to show in your reports. \" +\n            \"Only applicable to browsing activity. One regex per line.\"\n        let allowlistTitle = \"Allowlist:\"\n        let allowlistRemarks =\n            \"Sites that you want to show in your reports. \" +\n            \"Only applicable to browsing activity. One regex per line.\"\n\n        var title: String\n        var remarks: String\n        var list: String\n        var selectedSegment: Int\n        switch PropertiesManager.filterType {\n            case .denylist:\n                title = denylistTitle\n                remarks = denylistRemarks\n                list = PropertiesManager.denylist\n                selectedSegment = 0\n            case .allowlist:\n                title = allowlistTitle\n                remarks = allowlistRemarks\n                list = PropertiesManager.allowlist\n                selectedSegment = 1\n        }\n\n        filterListLabel.stringValue = title\n        filterRemarksLabel.stringValue = remarks\n        filterTextView.string = list\n        filterSegmentedControl.setSelected(true, forSegment: selectedSegment)\n\n        adjustWindowSize(animate: animate)\n    }\n\n    func adjustWindowSize(animate: Bool) {\n        guard let window = self.window else { return }\n\n        let newHeight = stackView.fittingSize.height + 70\n\n        var newWindowFrame = window.frame\n        newWindowFrame.size.height = newHeight\n        newWindowFrame.origin.y += window.frame.height - newWindowFrame.height // Adjust origin to keep the top-left corner stationary\n\n        window.setFrame(newWindowFrame, display: true, animate: animate)\n    }\n}\n"
  },
  {
    "path": "WakaTime/WakaTime-Bridging-Header.h",
    "content": "//\n//  Use this file to import your target's public headers that you would like to expose to Swift.\n//\n\n#import \"Utils/ObjC.h\"\n"
  },
  {
    "path": "WakaTime/WakaTime-Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleIdentifier</key>\n\t<string></string>\n\t<key>LSUIElement</key>\n\t<true/>\n\t<key>CFBundleURLTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleTypeRole</key>\n\t\t\t<string>Editor</string>\n\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t<array>\n\t\t\t\t<string>wakatime</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "WakaTime/WakaTime.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>com.apple.security.app-sandbox</key>\n    <false/>\n    <key>com.apple.security.notifications</key>\n    <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "WakaTime/WakaTime.swift",
    "content": "import AppKit\nimport Firebase\nimport Foundation\n\nclass WakaTime: HeartbeatEventHandler {\n    // MARK: Watcher\n\n    let watcher = Watcher()\n    let delegate: StatusBarDelegate\n\n    // MARK: Watcher State\n\n    // Note: The lastEntity and lastTime member vars are read and written on a worker thread.\n    // To ensure that they can be accessed concurrently from other threads without issues,\n    // they are declared atomic here\n    @Atomic var lastEntity = \"\"\n    @Atomic var lastTime = 0\n    @Atomic var lastCategory = Category.coding\n\n    // MARK: Initialization and Setup\n\n    init(_ delegate: StatusBarDelegate) {\n        self.delegate = delegate\n\n        Dependencies.installDependencies()\n        if SettingsManager.shouldRegisterAsLoginItem() { SettingsManager.registerAsLoginItem() }\n        if PropertiesManager.shouldRequestA11yPermission && !Accessibility.requestA11yPermission() {\n            delegate.a11yStatusChanged(false)\n        }\n\n        configureFirebase()\n        checkForApiKey()\n        watcher.heartbeatEventHandler = self\n        watcher.statusBarDelegate = delegate\n\n        if !PropertiesManager.hasLaunchedBefore {\n            for bundleId in MonitoredApp.defaultEnabledApps {\n                MonitoringManager.enableByDefault(bundleId)\n            }\n            PropertiesManager.hasLaunchedBefore = true\n        }\n    }\n\n    private func configureFirebase() {\n        // Needed for uncaught exception reporting\n        UserDefaults.standard.register(\n          defaults: [\"NSApplicationCrashOnExceptions\": true]\n        )\n        FirebaseApp.configure()\n    }\n\n    private func checkForApiKey() {\n        let apiKey = ConfigFile.getSetting(section: \"settings\", key: \"api_key\")\n        if apiKey.isEmpty {\n            openSettingsDeeplink()\n        }\n    }\n\n    private func openSettingsDeeplink() {\n        guard let url = DeepLink.settings.url else { return }\n\n        NSWorkspace.shared.open(url)\n    }\n\n    private func openMonitoredAppsDeeplink() {\n        guard let url = DeepLink.monitoredApps.url else { return }\n\n        NSWorkspace.shared.open(url)\n    }\n\n    // MARK: Watcher Event Handling\n\n    private func shouldSendHeartbeat(entity: String, time: Int, isWrite: Bool, category: Category) -> Bool {\n        if isWrite { return true }\n        if category != lastCategory { return true }\n        if !entity.isEmpty && entity != lastEntity { return true }\n        if lastTime + 120 < time { return true }\n\n        return false\n    }\n\n    public func handleHeartbeatEvent(\n        app: NSRunningApplication,\n        entity: String,\n        entityType: EntityType,\n        project: String?,\n        language: String?,\n        category: Category?,\n        isWrite: Bool) {\n        let time = Int(NSDate().timeIntervalSince1970)\n        let category = category ?? Category.coding\n        guard shouldSendHeartbeat(entity: entity, time: time, isWrite: isWrite, category: category) else { return }\n\n        // make sure we should be tracking this app to avoid race condition bugs\n        // do this after shouldSendHeartbeat for better performance because handleEvent may\n        // be called frequently\n        guard MonitoringManager.isAppMonitored(app) else { return }\n\n        guard\n            let appName = AppInfo.getAppNameForHeartbeat(app),\n            let appVersion = watcher.getAppVersion(app)\n        else { return }\n\n        let cli = NSString.path(\n            withComponents: ConfigFile.resourcesFolder + [\"wakatime-cli\"]\n        )\n        let process = Process()\n        process.launchPath = cli\n        var args = [\n            \"--entity\",\n            entity,\n            \"--entity-type\",\n            entityType.rawValue,\n            \"--category\",\n            category.rawValue.replacingOccurrences(of: \"_\", with: \" \"),\n            \"--plugin\",\n            \"\\(appName)/\\(appVersion) macos-wakatime/\" + Bundle.main.version,\n            \"--alternate-branch\",\n            \"<<LAST_BRANCH>>\",\n        ]\n        if let project = project {\n            args.append(\"--project\")\n            args.append(project)\n        } else {\n            args.append(\"--alternate-project\")\n            args.append(\"<<LAST_PROJECT>>\")\n        }\n        if let language = language {\n            args.append(\"--language\")\n            args.append(language)\n        }\n        if isWrite {\n            args.append(\"--write\")\n        }\n\n        Logging.default.log(\"Sending heartbeat with: \\(args)\")\n\n        lastEntity = entity\n        lastTime = time\n        lastCategory = category\n\n        process.arguments = args\n        process.standardOutput = FileHandle.nullDevice\n        process.standardError = FileHandle.nullDevice\n        do {\n            // Use WakaTime's custom execute() method to run the process. This will call Process.launch()\n            // with ObjC exception bridging on macOS 12 or earlier and Process.run() on macOS 13 or newer.\n            try process.execute()\n        } catch {\n            Logging.default.log(\"Failed to run wakatime-cli: \\(error)\")\n        }\n\n        delegate.fetchToday()\n    }\n}\n\nenum DeepLink: String {\n    case settings\n    case monitoredApps\n\n    var url: URL? { URL(string: \"wakatime://\\(self)\") }\n}\n\nenum EntityType: String {\n    case file\n    case app\n    case domain\n    case url\n}\n\nenum Category: String {\n    case browsing\n    case building\n    case codereviewing = \"code reviewing\"\n    case coding\n    case communicating\n    case debugging\n    case designing\n    case indexing\n    case learning\n    case manualtesting = \"manual testing\"\n    case meeting\n    case planning\n    case researching\n    case runningtests = \"running tests\"\n    case translating\n    case writingdocs = \"writing docs\"\n    case writingtests = \"writing tests\"\n}\n\nprotocol StatusBarDelegate: AnyObject {\n    func a11yStatusChanged(_ hasPermission: Bool)\n    func toastNotification(_ title: String)\n    func fetchToday()\n}\n\nprotocol HeartbeatEventHandler {\n    func handleHeartbeatEvent(\n        app: NSRunningApplication,\n        entity: String,\n        entityType: EntityType,\n        project: String?,\n        language: String?,\n        category: Category?,\n        isWrite: Bool)\n}\n"
  },
  {
    "path": "WakaTime/Watchers/FileSavedWatcher.swift",
    "content": "class FileMonitor {\n    private let fileURL: URL\n    private var dispatchObject: DispatchSourceFileSystemObject?\n\n    public var fileChangedEventHandler: (() -> Void)?\n\n    init?(filePath: URL, queue: DispatchQueue) {\n        self.fileURL = filePath\n        let folderURL = fileURL.deletingLastPathComponent() // monitor enclosing folder to track changes by Xcode\n        let descriptor = open(folderURL.path, O_EVTONLY)\n        guard descriptor >= -1 else { Logging.default.log(\"open failed: \\(descriptor)\"); return nil }\n        dispatchObject = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: .write, queue: queue)\n        dispatchObject?.setEventHandler { [weak self] in\n            self?.fileChangedEventHandler?()\n        }\n        dispatchObject?.setCancelHandler {\n            close(descriptor)\n        }\n        dispatchObject?.activate()\n    }\n\n    deinit {\n        dispatchObject?.cancel()\n    }\n}\n"
  },
  {
    "path": "WakaTime/Watchers/MonitoredApp.swift",
    "content": "import AppKit\n\nenum MonitoredApp: String, CaseIterable {\n    case adobeaftereffect = \"com.adobe.AfterEffects\"\n    case adobebridge = \"com.adobe.bridge14\"\n    case adobeillustrator = \"com.adobe.illustrator\"\n    case adobemediaencoder = \"com.adobe.ame.application.24\"\n    case adobephotoshop = \"com.adobe.Photoshop\"\n    case adobepremierepro = \"com.adobe.PremierePro.24\"\n    case arcbrowser = \"company.thebrowser.Browser\"\n    case beeper = \"im.beeper\"\n    case brave = \"com.brave.Browser\"\n    case canva = \"com.canva.CanvaDesktop\"\n    case chrome = \"com.google.Chrome\"\n    case chromebeta = \"com.google.Chrome.beta\"\n    case chromecanary = \"com.google.Chrome.canary\"\n    case figma = \"com.figma.Desktop\"\n    case firefox = \"org.mozilla.firefox\"\n    case github = \"com.github.GitHubClient\"\n    case imessage = \"com.apple.MobileSMS\"\n    case inkscape = \"org.inkscape.Inkscape\"\n    case iterm2 = \"com.googlecode.iterm2\"\n    case linear = \"com.linear\"\n    case miro = \"com.electron.realtimeboard\"\n    case notes = \"com.apple.Notes\"\n    case notion = \"notion.id\"\n    case postman = \"com.postmanlabs.mac\"\n    case rocketchat = \"chat.rocket\"\n    case safari = \"com.apple.Safari\"\n    case safaripreview = \"com.apple.SafariTechnologyPreview\"\n    case slack = \"com.tinyspeck.slackmacgap\"\n    case tableplus = \"com.tinyapp.TablePlus\"\n    case terminal = \"com.apple.Terminal\"\n    case warp = \"dev.warp.Warp-Stable\"\n    case wecom = \"com.tencent.WeWorkMac\"\n    case whatsapp = \"net.whatsapp.WhatsApp\"\n    case xcode = \"com.apple.dt.Xcode\"\n    case zed = \"dev.zed.Zed\"\n    case zoom = \"us.zoom.xos\"\n\n    init?(from bundleId: String) {\n        if let app = MonitoredApp(rawValue: bundleId) {\n            self = app\n        } else if let app = MonitoredApp(rawValue: bundleId.replacingOccurrences(of: \"-setapp$\", with: \"\", options: .regularExpression)) {\n            self = app\n        } else {\n            return nil\n        }\n    }\n\n    // Hide these from the Monitored Apps menu\n    static let unsupportedAppIds = [\n        \"com.apple.finder\",\n        \"macos-wakatime.WakaTime\",\n    ]\n\n    // link to plugin install pages with wakatime.com domain prepended for apps with plugins available\n    static let pluginAppIds: [String: String] = [\n        \"aptana.studio\": \"aptana\",\n        \"com.google.android.studio\": \"android-studio\",\n        \"com.jetbrains.CLion\": \"clion\",\n        \"com.jetbrains.DataSpell\": \"dataspell\",\n        \"com.jetbrains.PhpStorm\": \"phpstorm\",\n        \"com.jetbrains.PyCharm\": \"pycharm\",\n        \"com.jetbrains.pycharm.ce\": \"pycharm\",\n        \"com.jetbrains.RubyMine\": \"rubymine\",\n        \"com.jetbrains.RustRover\": \"rustrover\",\n        \"com.jetbrains.WebStorm\": \"webstorm\",\n        \"com.jetbrains.goland\": \"goland\",\n        \"com.jetbrains.intellij\": \"intellij-idea\",\n        \"com.jetbrains.intellij.ce\": \"intellij-idea\",\n        \"com.jetbrains.rider\": \"rider\",\n        \"com.microsoft.VSCode\": \"vs-code\",\n        \"com.microsoft.VSCodeInsiders\": \"vs-code\",\n        \"com.Roblox.RobloxStudio\": \"roblox-studio\",\n        \"com.sublimetext.2\": \"sublime\",\n        \"com.sublimetext.3\": \"sublime\",\n        \"com.sublimetext.4\": \"sublime\",\n        \"com.todesktop.230313mzl4w4u92\": \"cursor\",\n        \"com.visualstudio.code.oss\": \"vs-code\",\n        \"com.vscodium\": \"vs-code\",\n        \"epp.package.committers\": \"eclipse\",\n        \"epp.package.cpp\": \"eclipse\",\n        \"epp.package.dsl\": \"eclipse\",\n        \"epp.package.embedcpp\": \"eclipse\",\n        \"epp.package.java\": \"eclipse\",\n        \"epp.package.jee\": \"eclipse\",\n        \"epp.package.modeling\": \"eclipse\",\n        \"epp.package.parallel\": \"eclipse\",\n        \"epp.package.php\": \"eclipse\",\n        \"epp.package.rcp\": \"eclipse\",\n        \"epp.package.scout\": \"eclipse\",\n        \"org.vim.MacVim\": \"vim\",\n    ]\n\n    static var allBundleIds: [String] {\n        MonitoredApp.allCases.map { $0.rawValue }\n    }\n\n    static let electronAppIds = [\n        MonitoredApp.figma.rawValue,\n        MonitoredApp.slack.rawValue,\n    ]\n\n    static let browserAppIds = [\n        MonitoredApp.arcbrowser.rawValue,\n        MonitoredApp.brave.rawValue,\n        MonitoredApp.chrome.rawValue,\n        MonitoredApp.chromebeta.rawValue,\n        MonitoredApp.chromecanary.rawValue,\n        MonitoredApp.firefox.rawValue,\n        MonitoredApp.safari.rawValue,\n        MonitoredApp.safaripreview.rawValue,\n    ]\n\n    // list apps which are enabled by default on first run\n    static let defaultEnabledApps = [\n        MonitoredApp.canva.rawValue,\n        MonitoredApp.figma.rawValue,\n        MonitoredApp.github.rawValue,\n        MonitoredApp.linear.rawValue,\n        MonitoredApp.notes.rawValue,\n        MonitoredApp.notion.rawValue,\n        MonitoredApp.postman.rawValue,\n        MonitoredApp.tableplus.rawValue,\n        MonitoredApp.xcode.rawValue,\n        MonitoredApp.zoom.rawValue,\n        MonitoredApp.zed.rawValue,\n    ]\n}\n"
  },
  {
    "path": "WakaTime/Watchers/Watcher.swift",
    "content": "import Cocoa\nimport Foundation\nimport AppKit\n\nclass Watcher: NSObject {\n    private let callbackQueue = DispatchQueue(label: \"com.WakaTime.Watcher.callbackQueue\", qos: .utility)\n    private let monitorQueue = DispatchQueue(label: \"com.WakaTime.Watcher.monitorQueue\", qos: .utility)\n\n    var appVersions: [String: String] = [:]\n    var eventSourceObserver: EventSourceObserver?\n    var heartbeatEventHandler: HeartbeatEventHandler?\n    var statusBarDelegate: StatusBarDelegate?\n    var lastCheckedA11y = Date()\n    var isBuilding = false\n    var activeApp: NSRunningApplication?\n\n    private var observer: AXObserver?\n    private var observingElement: AXUIElement?\n    private var observingActivityTextElement: AXUIElement?\n    private var fileMonitor: FileMonitor?\n    private var selectedText: String?\n    private var lastValidHeartbeatForApp = [String: HeartbeatData]()\n\n    override init() {\n        super.init()\n\n        eventSourceObserver = EventSourceObserver(pollIntervalInSeconds: 1)\n\n        NSWorkspace.shared.notificationCenter.addObserver(\n            self,\n            selector: #selector(appChanged),\n            name: NSWorkspace.didActivateApplicationNotification,\n            object: nil\n        )\n\n        if let app = NSWorkspace.shared.frontmostApplication {\n            handleAppChanged(app)\n        }\n    }\n\n    deinit {\n        NSWorkspace.shared.notificationCenter.removeObserver(self) // needed prior macOS 11 only\n    }\n\n    @objc private func appChanged(_ notification: Notification) {\n        guard let newApp = notification.userInfo?[\"NSWorkspaceApplicationKey\"] as? NSRunningApplication else { return }\n\n        handleAppChanged(newApp)\n    }\n\n    private func handleAppChanged(_ app: NSRunningApplication) {\n        if app != activeApp {\n            // swiftlint:disable line_length\n            Logging.default.log(\"App changed from \\(activeApp?.localizedName ?? \"nil\") to \\(app.localizedName ?? \"nil\") (\\(app.bundleIdentifier ?? \"nil\"))\")\n            eventSourceObserver?.stop()\n            // swiftlint:enable line_length\n            if let oldApp = activeApp { unwatch(app: oldApp) }\n            activeApp = app\n            self.statusBarDelegate?.fetchToday()\n            if let bundleId = app.bundleIdentifier, MonitoringManager.isAppMonitored(for: bundleId) {\n                watch(app: app)\n            }\n        }\n\n        setAppVersion(app)\n    }\n\n    private func setAppVersion(_ app: NSRunningApplication) {\n        guard\n            let id = app.bundleIdentifier,\n            appVersions[id] == nil,\n            let url = app.bundleURL,\n            let bundle = Bundle(url: url)\n        else { return }\n\n        appVersions[id] = \"\\(bundle.version)-\\(bundle.build)\".filter { !$0.isWhitespace }\n    }\n\n    public func getAppVersion(_ app: NSRunningApplication) -> String? {\n        guard let id = app.bundleIdentifier else { return nil }\n        return appVersions[id]\n    }\n\n    private func watch(app: NSRunningApplication) {\n        setAppVersion(app)\n\n        do {\n            if MonitoringManager.isAppElectron(app) {\n                let pid = app.processIdentifier\n                let axApp = AXUIElementCreateApplication(pid)\n                let result = AXUIElementSetAttributeValue(axApp, \"AXManualAccessibility\" as CFString, true as CFTypeRef)\n                if result.rawValue != 0 {\n                    let appName = app.localizedName ?? \"UnknownApp\"\n                    Logging.default.log(\"Setting AXManualAccessibility on \\(appName) failed (\\(result.rawValue))\")\n                }\n            }\n\n            let observer = try AXObserver.create(appID: app.processIdentifier, callback: observerCallback)\n            let this = Unmanaged.passUnretained(self).toOpaque()\n            let axApp = AXUIElementCreateApplication(app.processIdentifier)\n\n            try observer.add(notification: kAXFocusedUIElementChangedNotification, element: axApp, refcon: this)\n            try observer.add(notification: kAXFocusedWindowChangedNotification, element: axApp, refcon: this)\n            try observer.add(notification: kAXSelectedTextChangedNotification, element: axApp, refcon: this)\n            if MonitoringManager.isAppElectron(app) {\n                try observer.add(notification: kAXValueChangedNotification, element: axApp, refcon: this)\n            }\n\n            observer.addToRunLoop()\n            self.observer = observer\n            self.observingElement = axApp\n            self.statusBarDelegate?.a11yStatusChanged(true)\n\n            if MonitoringManager.isAppXcode(app), let activeWindow = axApp.activeWindow {\n                if let currentPath = activeWindow.currentPath {\n                    self.documentPath = currentPath\n                }\n                observeActivityText(activeWindow: activeWindow)\n            } else {\n                eventSourceObserver?.start { [weak self] in\n                    self?.callbackQueue.async {\n                        guard\n                            let app = self?.activeApp, !MonitoringManager.isAppXcode(app),\n                            let bundleId = app.bundleIdentifier\n                        else { return }\n\n                        var heartbeat = MonitoringManager.heartbeatData(app)\n\n                        if let heartbeat {\n                            self?.lastValidHeartbeatForApp[bundleId] = heartbeat\n                        } else {\n                            heartbeat = self?.lastValidHeartbeatForApp[bundleId]\n                        }\n\n                        if let heartbeat {\n                            self?.heartbeatEventHandler?.handleHeartbeatEvent(\n                                app: app,\n                                entity: heartbeat.entity,\n                                entityType: heartbeat.entityType,\n                                project: heartbeat.project,\n                                language: heartbeat.language,\n                                category: heartbeat.category,\n                                isWrite: false\n                            )\n                        }\n                    }\n                }\n            }\n        } catch {\n            Logging.default.log(\"Failed to setup AXObserver: \\(error.localizedDescription)\")\n\n            guard PropertiesManager.shouldRequestA11yPermission else {\n                return\n            }\n\n            // TODO: App could be still launching, retry setting AXObserver in 20 seconds for this app\n\n            if lastCheckedA11y.timeIntervalSinceNow > 60 {\n                lastCheckedA11y = Date()\n                self.statusBarDelegate?.a11yStatusChanged(Accessibility.requestA11yPermission())\n            }\n        }\n    }\n\n    private func unwatch(app: NSRunningApplication) {\n        if let observer {\n            observer.removeFromRunLoop()\n            guard let observingElement else { fatalError(\"observingElement should not be nil here\") }\n\n            try? observer.remove(notification: kAXFocusedUIElementChangedNotification, element: observingElement)\n            try? observer.remove(notification: kAXFocusedWindowChangedNotification, element: observingElement)\n            try? observer.remove(notification: kAXSelectedTextChangedNotification, element: observingElement)\n            if MonitoringManager.isAppElectron(app) {\n                try? observer.remove(notification: kAXValueChangedNotification, element: observingElement)\n            }\n\n            self.observingElement = nil\n            self.observer = nil\n        }\n    }\n\n    func observeActivityText(activeWindow: AXUIElement) {\n        let this = Unmanaged.passUnretained(self).toOpaque()\n        activeWindow.traverseDown { element in\n            if let id = element.id, id == \"Activity Text\" {\n                // Remove previously observed \"Activity Text\" value observer, if any\n                if let observingActivityTextElement {\n                    try? self.observer?.remove(notification: kAXValueChangedNotification, element: observingActivityTextElement)\n                }\n\n                do {\n                    // Update the current isBuilding state when the observed \"Activity Text\" UI element changes\n                    self.isBuilding = checkIsBuilding(activityText: element.value)\n\n                    if let path = self.documentPath {\n                        self.handleNotificationEvent(path: path, isWrite: false)\n                    }\n\n                    // Try to add observer to the current \"Activity Text\" UI element\n                    try self.observer?.add(notification: kAXValueChangedNotification, element: element, refcon: this)\n                    observingActivityTextElement = element\n                } catch {\n                    observingActivityTextElement = nil\n                }\n                return false // \"Activity Text\" element found, abort traversal\n            }\n            return true // continue traversal\n        }\n    }\n\n    func checkIsBuilding(activityText: String?) -> Bool {\n        activityText == \"Build\" || (activityText?.contains(\"Building\") == true)\n    }\n\n    var documentPath: URL? {\n        didSet {\n            if documentPath != oldValue {\n                guard let newPath = documentPath else { return }\n\n                Logging.default.log(\"Document changed: \\(newPath)\")\n\n                handleNotificationEvent(path: newPath, isWrite: false)\n                fileMonitor = nil\n                fileMonitor = FileMonitor(filePath: newPath, queue: monitorQueue)\n                fileMonitor?.fileChangedEventHandler = { [weak self] in\n                    self?.handleNotificationEvent(path: newPath, isWrite: true)\n                }\n            }\n        }\n    }\n\n    public func handleNotificationEvent(path: URL, isWrite: Bool) {\n        callbackQueue.async {\n            guard let app = self.activeApp else { return }\n\n            self.heartbeatEventHandler?.handleHeartbeatEvent(\n                app: app,\n                entity: path.path,\n                entityType: .file,\n                project: nil,\n                language: nil,\n                category: self.isBuilding ? Category.building : Category.coding,\n                isWrite: isWrite\n            )\n        }\n    }\n}\n\nprivate func observerCallback(\n    _ observer: AXObserver,\n    _ element: AXUIElement,\n    _ notification: CFString,\n    _ refcon: UnsafeMutableRawPointer?\n) {\n    guard let refcon = refcon else { return }\n\n    let this = Unmanaged<Watcher>.fromOpaque(refcon).takeUnretainedValue()\n\n    guard let app = this.activeApp else { return }\n\n    let axNotification = AXUIElementNotification.notificationFrom(string: notification as String)\n    switch axNotification {\n        case .selectedTextChanged:\n            if MonitoringManager.isAppXcode(app) {\n                guard\n                    !element.selectedText.isEmpty,\n                    let currentPath = element.currentPath\n                else { return }\n                this.heartbeatEventHandler?.handleHeartbeatEvent(\n                    app: app,\n                    entity: currentPath.path,\n                    entityType: EntityType.file,\n                    project: nil,\n                    language: nil,\n                    category: this.isBuilding ? Category.building : Category.coding,\n                    isWrite: false)\n            }\n        case .focusedUIElementChanged:\n            if MonitoringManager.isAppXcode(app) {\n                guard let currentPath = element.currentPath else { return }\n\n                this.documentPath = currentPath\n            }\n        case .focusedWindowChanged:\n            if MonitoringManager.isAppXcode(app) {\n                this.observeActivityText(activeWindow: element)\n            }\n        case .valueChanged:\n            if MonitoringManager.isAppXcode(app) {\n                if let id = element.id, id == \"Activity Text\" {\n                    this.isBuilding = this.checkIsBuilding(activityText: element.value)\n                    if let path = this.documentPath {\n                        this.handleNotificationEvent(path: path, isWrite: false)\n                    }\n                }\n            }\n        default:\n            break\n    }\n}\n"
  },
  {
    "path": "WakaTime/WindowControllers/MonitoredAppsWindowController.swift",
    "content": "import AppKit\n\nclass MonitoredAppsWindowController: NSWindowController {\n    let monitoredAppsView = MonitoredAppsView()\n\n    convenience init() {\n        self.init(window: nil)\n\n        let window = NSWindow(\n            contentRect: NSRect(x: 0, y: 0, width: 400, height: 450),\n            styleMask: [.titled, .closable, .resizable],\n            backing: .buffered,\n            defer: false\n        )\n        window.center()\n        window.title = \"Monitored Apps\"\n        window.contentView = monitoredAppsView\n        self.window = window\n    }\n\n    override func showWindow(_ sender: Any?) {\n        monitoredAppsView.reloadData()\n        super.showWindow(sender)\n    }\n}\n"
  },
  {
    "path": "WakaTime/WindowControllers/SettingsWindowController.swift",
    "content": "import AppKit\n\nclass SettingsWindowController: NSWindowController, NSTextFieldDelegate {\n    public let settingsView = SettingsView()\n\n    convenience init() {\n        self.init(window: nil)\n\n        let window = NSWindow(\n            contentRect: NSRect(x: 0, y: 0, width: 440, height: 470),\n            styleMask: [.titled, .closable],\n            backing: .buffered,\n            defer: false\n        )\n        window.center()\n        window.title = \"Settings\"\n        window.contentView = settingsView\n        self.window = window\n        settingsView.adjustWindowSize(animate: false)\n    }\n}\n"
  },
  {
    "path": "WakaTime/main.swift",
    "content": "import Cocoa\n\nlet delegate = AppDelegate()\nlet application = NSApplication.shared\napplication.delegate = delegate\napplication.run()\n"
  },
  {
    "path": "WakaTime Helper/AppDelegate.swift",
    "content": "import Cocoa\n\nclass AppDelegate: NSObject, NSApplicationDelegate {\n    struct Constants {\n        static let mainAppBundleID = \"macos-wakatime.WakaTime\"\n    }\n\n    func applicationDidFinishLaunching(_ aNotification: Notification) {\n        let userHome = FileManager.default.homeDirectoryForCurrentUser.pathComponents\n        let logFilePath = NSString.path(withComponents: userHome + [\".wakatime\", \"macos-wakatime-helper.log\"])\n        Logging.default.configure(filePath: logFilePath)\n\n        Logging.default.log(\"Starting WakaTime Helper\")\n\n        let runningApps = NSWorkspace.shared.runningApplications\n        let isRunning = runningApps.contains {\n            $0.bundleIdentifier == Constants.mainAppBundleID\n        }\n\n        if !isRunning {\n            Logging.default.log(\"WakaTime is not running\")\n            var path = Bundle.main.bundlePath as NSString\n            for _ in 1...4 {\n                path = path.deletingLastPathComponent as NSString\n            }\n            let fileURL = URL(fileURLWithPath: path as String)\n            Logging.default.log(\"Attempting to open WakaTime at \\\"\\(fileURL.absoluteString)\\\"\")\n            NSWorkspace.shared.openApplication(\n                at: fileURL,\n                configuration: NSWorkspace.OpenConfiguration()\n            ) { _, error in\n                if let error {\n                    Logging.default.log(error.localizedDescription)\n                }\n            }\n        } else {\n            Logging.default.log(\"WakaTime is already running\")\n        }\n    }\n}\n"
  },
  {
    "path": "WakaTime Helper/Logging.swift",
    "content": "import Foundation\nimport os.log\n\nclass Logging {\n    static let `default` = Logging()\n    private var filePath: String?\n\n    private init() {}\n\n    // Configures logging to also write to a file at the given path.\n    func configure(filePath: String) {\n        self.filePath = filePath\n    }\n\n    func log(_ message: String, type: OSLogType = .default) {\n        os_log(\"%{public}@\", log: .default, type: type, message)\n\n        if let filePath = self.filePath {\n            let dateFormatter = DateFormatter()\n            dateFormatter.dateFormat = \"yyyy-MM-dd HH:mm:ss.SSS\"\n            let timestamp = dateFormatter.string(from: Date())\n            let logMessage = \"\\(timestamp): \\(message)\\n\"\n\n            // Attempt to append the log message to the log file\n            if let fileHandle = FileHandle(forWritingAtPath: filePath) {\n                fileHandle.seekToEndOfFile()\n                if let data = logMessage.data(using: .utf8) {\n                    fileHandle.write(data)\n                }\n                fileHandle.closeFile()\n            } else {\n                // If the file does not exist, create it\n                try? logMessage.write(toFile: filePath, atomically: true, encoding: .utf8)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "WakaTime Helper/WakaTime Helper-Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>LSBackgroundOnly</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "WakaTime Helper/main.swift",
    "content": "import Cocoa\n\nlet delegate = AppDelegate()\nlet application = NSApplication.shared\napplication.delegate = delegate\napplication.run()\n"
  },
  {
    "path": "bin/prepare_changelog.sh",
    "content": "#!/bin/bash\n\nset -e\n\nif [[ $# -ne 2 ]]; then\n    echo 'incorrect number of arguments'\n    exit 1\nfi\n\n# Read arguments\nbranch=$1\nchangelog=$2\nslack=\n\nclean_up() {\n    changelog=\"${changelog//\\`/}\"\n    changelog=\"${changelog//\\'/}\"\n    changelog=\"${changelog//\\\"/}\"\n}\n\nreplace_for_release() {\n    changelog=\"${changelog//'%'/'%25'}\"\n    changelog=\"${changelog//$'\\n'/'%0A'}\"\n    changelog=\"${changelog//$'\\r'/'%0D'}\"\n}\n\nreplace_for_slack() {\n    slack=\"${slack//'%'/'%25'}\"\n    slack=\"${slack//$'\\n'/'%0A'}\"\n    slack=\"${slack//$'\\r'/'%0D'}\"\n}\n\nslack_output_for_main() {\n    local IFS=$'\\n' # make newlines the only separator\n    local temp=\n    for j in ${changelog}\n    do\n        temp=\"${temp}$(echo \"$j\" | awk '{printf \"<https://github.com/wakatime/macos-wakatime/commit/\"$1\"|\"$1\">\";$1=\"\"; print $0 }')\\n\"\n    done\n\n    slack=\"*Changelog*\\n${temp}\"\n}\n\nslack_output_for_release() {\n    local IFS=$'\\n' # make newlines the only separator\n    local temp=\n    for j in ${changelog}\n    do\n        temp=\"${temp}${j}\\n\"\n    done\n\n    slack=\"*Changelog*\\n${temp}\"\n}\n\nparse_for_main() {\n    changelog=$(awk 'f;/## Changelog/{f=1}' <<< \"$changelog\")\n}\n\nparse_for_release() {\n    changelog=$(awk 'f;/Changelog:/{f=1}' <<< \"$changelog\")\n}\n\ncase $branch in\n    main) \n        parse_for_main\n        clean_up\n        slack_output_for_main\n        replace_for_release\n        ;;\n    release)\n        parse_for_release\n        [ -z \"$changelog\" ] && exit 1\n        clean_up\n        slack_output_for_release\n        replace_for_release\n        replace_for_slack\n        ;;\n    *) exit 1 ;;\nesac\n\necho \"::set-output name=changelog::${changelog}\"\necho \"::set-output name=slack::${slack}\"\n"
  },
  {
    "path": "project.yml",
    "content": "name: WakaTime\n\noptions:\n  bundleIdPrefix: macos-wakatime\n  createIntermediateGroups: true\n\npackages:\n  AppUpdater:\n    url: https://github.com/alanhamlett/AppUpdater\n    branch: master\n  Firebase:\n    url: https://github.com/firebase/firebase-ios-sdk\n    from: 11.11.0\n\ntargets:\n  WakaTime:\n    type: application\n    platform: macOS\n    deploymentTarget: 10.15\n    sources: [WakaTime]\n    settings:\n      CURRENT_PROJECT_VERSION: local-build\n      MARKETING_VERSION: local-build\n      INFOPLIST_FILE: WakaTime/WakaTime-Info.plist\n      GENERATE_INFOPLIST_FILE: YES\n      CODE_SIGN_STYLE: Automatic\n      DEVELOPMENT_TEAM: ${SV_DEVELOPMENT_TEAM}\n      ENABLE_HARDENED_RUNTIME: YES\n      DEAD_CODE_STRIPPING: YES\n      SWIFT_OBJC_BRIDGING_HEADER: WakaTime/WakaTime-Bridging-Header.h\n    postCompileScripts:\n      - script: ./Scripts/Lint/swiftlint lint --quiet\n        name: Swiftlint\n    dependencies:\n      - target: WakaTime Helper\n      - package: AppUpdater\n      - package: Firebase\n        product: FirebaseCrashlytics\n    postBuildScripts:\n      - script: |\n          LOGIN_ITEMS_DIR=\"$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/Contents/Library/LoginItems\"\n          rm -rf \"$LOGIN_ITEMS_DIR\"\n          mkdir -p \"$LOGIN_ITEMS_DIR\"\n          mv \"$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/Contents/Resources/WakaTime Helper.app\" \"$LOGIN_ITEMS_DIR/\"\n        name: Move \"WakaTime Helper.app\" to LoginItems\n      - script: Scripts/Firebase/upload-dSYM.sh\n        name: Firebase\n        inputFiles:\n          - ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}\n          - $(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)\n  WakaTime Helper:\n    type: application\n    platform: macOS\n    deploymentTarget: 10.15\n    sources: [WakaTime Helper]\n    settings:\n      CURRENT_PROJECT_VERSION: local-build\n      MARKETING_VERSION: local-build\n      INFOPLIST_FILE: WakaTime Helper/WakaTime Helper-Info.plist\n      GENERATE_INFOPLIST_FILE: YES\n      CODE_SIGN_STYLE: Automatic\n      DEVELOPMENT_TEAM: ${SV_DEVELOPMENT_TEAM}\n      ENABLE_HARDENED_RUNTIME: YES\n      DEAD_CODE_STRIPPING: YES\n      SKIP_INSTALL: YES\n    postCompileScripts:\n      - script: ./Scripts/Lint/swiftlint lint --quiet\n        name: Swiftlint\n"
  }
]