[
  {
    "path": ".gitattributes",
    "content": "# See https://github.com/github/linguist for details\n\n# Trick to remove some build tools from language overview\nMakefile linguist-vendored\n*.sh linguist-vendored\n*.cmd linguist-vendored\n*.ps1 linguist-vendored\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# Docs for this file can be found here:\n# https://docs.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository\n\ngithub: [\"ActivityWatch\", \"ErikBjare\", \"johan-bjareholt\"]\npatreon: \"erikbjare\"\nopen_collective: \"activitywatch\"\nliberapay: \"ActivityWatch\"\ncustom: [\"https://activitywatch.net/donate/\"]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: \"\\U0001F41E Bug report\"\nabout: Did you find a bug?\ntitle: ''\nlabels: 'type: bug'\nassignees: ''\n\n---\n\n<!--\n  Hi there! Thank you for discovering and submitting an issue.\n\n  Before you submit this; let's make sure of a few things.\n  Please make sure the following boxes are ticked if they are correct.\n  If not, please try and complete them first.\n-->\n\n<!-- Checked checkbox should look like this: [x] -->\n - [ ] I am on the [latest](https://github.com/ActivityWatch/activitywatch/releases/latest) ActivityWatch version.\n - [ ] I have searched the issues of this repo and believe that this is not a duplicate.\n\n\n<!--\n  Once those are done, if you're able to fill in the following list with your information,\n  it'd be very helpful to whoever handles the issue.\n-->\n\n- **OS name and version**: <!-- Replace this comment with OS name + version -->\n- **ActivityWatch version**: <!-- Replace this comment with the ActivityWatch version (found at the bottom of the Web UI) -->\n\n## Describe the bug\n<!-- A clear and concise description of what the bug is. -->\n\n## To Reproduce\n<!--\n  Steps to reproduce the behavior, for example:\n    1. Go to '...'\n    2. Click on '...'\n    3. Scroll down to '...'\n    4. See error\n-->\n\n## Expected behavior\n<!-- A clear and concise description of what you expected to happen. -->\n\n## Documentation\n<!--\n  If applicable, add screenshots or logs to help explain your problem.\n\n  Logs can be found in different places depending on platform:\n   - Windows: `C:\\Users\\<USER>\\AppData\\Local\\ActivityWatch\\Logs`\n   - macOS: `/Users/<USER>/Library/Logs/activitywatch`\n   - Linux: `/home/<USER>/.cache/activitywatch/log`\n  They can be opened with any plain text editor.\n-->\n\n## Additional context\n<!-- Add any other context about the problem here. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "# Issue templates are based on templates for poetry:\n# https://github.com/python-poetry/poetry/tree/master/.github/ISSUE_TEMPLATE\nblank_issues_enabled: false\ncontact_links:\n  - name: \"\\U0001F381 Feature requests\"\n    url: https://forum.activitywatch.net/c/features\n    about: Request and vote on features on the forum.\n  - name: \"\\u2753 Support\"\n    url: https://forum.activitywatch.net/\n    about: Need help with something? Ask for help on the forum!\n  - name: \"\\U0001F4AD Discussion (on our forum)\"\n    url: https://forum.activitywatch.net/\n    about: The preferred place for general discussion about ActivityWatch\n  - name: \"\\U0001F4AD Discussion (on GitHub Discussions)\"\n    url: https://github.com/ActivityWatch/activitywatch/discussions\n    about: We're testing it out (but the forum is still the preferred place).\n  - name: \"\\U0001F4AC Chat with us on Discord\"\n    url: https://discord.gg/dctJK6USjK\n    about: We love to see people who are active in issues on our Discord, come join us!\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/everything-else.md",
    "content": "---\nname: \"\\U0001F5C3 Everything Else\"\nabout: For questions and issues that do not fall in any of the other categories.\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n<!-- Describe your question and issue here. This space is meant to be used for general questions that are neither bugs nor feature requests. If you're looking for help or support, please post on the forum instead: https://forum.activitywatch.net/ -->\n\n\n<!-- Checked checkbox should look like this: [x] -->\n- [ ] I have searched the [issues](https://github.com/ActivityWatch/activitywatch/issues) of this repo and believe that this is not a duplicate.\n- [ ] I have searched the [documentation](https://docs.activitywatch.net/en/latest/) and believe that my question is not covered.\n\n## Issue\n<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# Set update schedule for GitHub Actions\nversion: 2\nupdates:\n  # Maintain dependencies for GitHub Actions\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n\n  # Maintain submodule versions\n  # NOTE: too noisy, easier to update by hand\n  #- package-ecosystem: \"gitsubmodule\"\n  #  directory: \"/\"\n  #  schedule:\n  #    interval: \"monthly\"\n\n  # Maintain dependencies for pip/poetry\n  # NOTE: too noisy, easier to update by hand\n  #- package-ecosystem: \"pip\"\n  #  directory: \"/\"\n  #  schedule:\n  #    interval: \"monthly\"\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 365\n# Number of days of inactivity before a stale issue is closed\ndaysUntilClose: 14\n# Issues with these labels will never be considered stale\nexemptLabels:\n  - '!pinned'\n  - 'priority: high'\n  - 'improves: security'\n  - 'improves: sustainability'\n# Label to use when marking an issue as stale\nstaleLabel: stale\n# Comment to post when marking an issue as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed if no further activity occurs. Thank you\n  for your contributions.\n# Comment to post when closing a stale issue. Set to `false` to disable\ncloseComment: false\n"
  },
  {
    "path": ".github/workflows/build-tauri.yml",
    "content": "name: Build Tauri\n\non:\n  push:\n    branches: [master]\n    tags:\n      - v*\n  pull_request:\n    branches: [master]\n\njobs:\n  build:\n    name: ${{ matrix.os }}, py-${{ matrix.python_version }}, node-${{ matrix.node_version }}\n    runs-on: ${{ matrix.os }}\n    continue-on-error: ${{ matrix.experimental }}\n    env:\n      # Whether to build and include extras (like aw-notify and aw-watcher-input)\n      AW_EXTRAS: true\n      TAURI_BUILD: true\n      # sets the macOS version target, see: https://users.rust-lang.org/t/compile-rust-binary-for-older-versions-of-mac-osx/38695\n      MACOSX_DEPLOYMENT_TARGET: 10.9\n    defaults:\n      run:\n        shell: bash\n    strategy:\n      fail-fast: false\n      max-parallel: 5\n      matrix:\n        os:\n          [\n            ubuntu-24.04,\n            ubuntu-24.04-arm,\n            windows-latest,\n            macos-14,\n            macos-latest,\n          ]\n        python_version: [3.9]\n        node_version: [22]\n        skip_rust: [false]\n        skip_webui: [false]\n        experimental: [false]\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: \"recursive\"\n          fetch-depth: 0\n\n      - name: Set environment variables\n        run: |\n          echo \"RELEASE=${{ startsWith(github.ref_name, 'v') || github.ref_name == 'master' }}\" >> $GITHUB_ENV\n          echo \"TAURI_BUILD=true\" >> $GITHUB_ENV\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python_version }}\n\n      - name: Set up Node\n        if: ${{ !matrix.skip_webui }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node_version }}\n\n      - name: Set up Rust\n        if: ${{ !matrix.skip_rust }}\n        uses: dtolnay/rust-toolchain@master\n        id: toolchain\n        with:\n          toolchain: stable\n\n      - name: Cache node_modules\n        uses: actions/cache@v4\n        if: ${{ !matrix.skip_webui }}\n        with:\n          path: |\n            aw-server-rust/aw-webui/node_modules\n            aw-tauri/node_modules\n          key: ${{ matrix.os }}-node_modules-${{ hashFiles('**/package-lock.json') }}\n          restore-keys: |\n            ${{ matrix.os }}-node_modules-\n\n      - name: Cache cargo build\n        uses: actions/cache@v4\n        env:\n          cache-name: cargo-build-target\n        with:\n          path: |\n            aw-server-rust/target\n            aw-tauri/src-tauri/target\n          key: ${{ matrix.os }}-${{ env.cache-name }}-${{ steps.toolchain.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }}\n          restore-keys: |\n            ${{ matrix.os }}-${{ env.cache-name }}-${{ steps.toolchain.outputs.rustc_hash }}-\n\n      - name: Install APT dependencies\n        if: runner.os == 'Linux'\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y \\\n            libgtk-3-dev \\\n            libwebkit2gtk-4.1-dev \\\n            libayatana-appindicator3-dev \\\n            librsvg2-dev \\\n            libjavascriptcoregtk-4.1-dev \\\n            libsoup-3.0-dev \\\n            xdg-utils\n\n      - name: Install dependencies\n        run: |\n          if [ \"$RUNNER_OS\" == \"Windows\" ]; then\n            choco install innosetup\n          fi\n          pip3 install poetry==1.4.2\n\n      - name: Build\n        uses: nick-fields/retry@v3\n        with:\n          timeout_minutes: 60\n          max_attempts: 3\n          shell: bash\n          command: |\n            python3 -m venv venv\n            source venv/bin/activate || source venv/Scripts/activate\n            poetry install\n            make build SKIP_WEBUI=${{ matrix.skip_webui }} SKIP_SERVER_RUST=${{ matrix.skip_rust }}\n            pip freeze\n\n      - name: Run tests\n        uses: nick-fields/retry@v3\n        with:\n          timeout_minutes: 60\n          max_attempts: 3\n          shell: bash\n          command: |\n            source venv/bin/activate || source venv/Scripts/activate\n            make test SKIP_SERVER_RUST=${{ matrix.skip_rust }}\n\n      - name: Package\n        run: |\n          source venv/bin/activate || source venv/Scripts/activate\n          poetry install\n          make package SKIP_SERVER_RUST=${{ matrix.skip_rust }}\n\n      - name: Package dmg\n        if: runner.os == 'macOS'\n        run: |\n          if [ -n \"$APPLE_EMAIL\" ]; then\n            ./scripts/ci/import-macos-p12.sh\n          fi\n\n          source venv/bin/activate\n          make dist/ActivityWatch.dmg\n\n          if [ -n \"$APPLE_EMAIL\" ]; then\n            codesign --verbose -s ${APPLE_PERSONALID} dist/ActivityWatch.dmg\n\n            brew install akeru-inc/tap/xcnotary\n            xcnotary precheck dist/ActivityWatch.app\n            xcnotary precheck dist/ActivityWatch.dmg\n\n            make dist/notarize\n          fi\n          mv dist/ActivityWatch.dmg dist/activitywatch-$(scripts/package/getversion.sh)-macos-x86_64.dmg\n        env:\n          APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }}\n          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}\n          APPLE_PERSONALID: ${{ secrets.APPLE_TEAMID }}\n          APPLE_TEAMID: ${{ secrets.APPLE_TEAMID }}\n          CERTIFICATE_MACOS_P12_BASE64: ${{ secrets.CERTIFICATE_MACOS_P12_BASE64 }}\n          CERTIFICATE_MACOS_P12_PASSWORD: ${{ secrets.CERTIFICATE_MACOS_P12_PASSWORD }}\n\n      - name: Upload packages\n        uses: actions/upload-artifact@v4\n        with:\n          name: builds-tauri-${{ matrix.os }}-py${{ matrix.python_version }}\n          path: dist/activitywatch-*.*\n\n  release-notes:\n    runs-on: ubuntu-latest\n    if: startsWith(github.ref, 'refs/tags/v')\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: \"recursive\"\n          fetch-depth: 0\n\n      - uses: ActivityWatch/check-version-format-action@v2\n        id: version\n        with:\n          prefix: \"v\"\n\n      - name: Echo version\n        run: |\n          echo \"${{ steps.version.outputs.full }} (stable: ${{ steps.version.outputs.is_stable }})\"\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n\n      - name: Install deps\n        run: |\n          pip install requests\n\n      - name: Generate release notes\n        run: |\n          LAST_RELEASE=`STABLE_ONLY=${{ steps.version.output.is_stable }} ./scripts/get_latest_release.sh`\n          ./scripts/build_changelog.py --range \"$LAST_RELEASE...${{ steps.version.outputs.full }}\"\n\n      - name: Rename\n        run: |\n          mv changelog.md release_notes.md\n\n      - name: Upload release notes\n        uses: actions/upload-artifact@v4\n        with:\n          name: release_notes_tauri\n          path: release_notes.md\n\n  release:\n    needs: [build, release-notes]\n    if: startsWith(github.ref, 'refs/tags/v')\n    runs-on: ubuntu-latest\n    steps:\n      - name: Download build artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: dist\n\n      - name: Display structure of downloaded files\n        run: ls -R\n        working-directory: dist\n\n      - uses: ActivityWatch/check-version-format-action@v2\n        id: version\n        with:\n          prefix: \"v\"\n\n      - name: Release\n        uses: softprops/action-gh-release@v1\n        with:\n          draft: true\n          files: dist/*/activitywatch-*.*\n          body_path: dist/release_notes_tauri/release_notes.md\n          prerelease: ${{ !(steps.version.outputs.is_stable == 'true') }}\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  push:\n    branches: [ master ]\n    tags:\n      - v*\n  pull_request:\n    branches: [ master ]\n  #release:\n  #  types: [published]\n\njobs:\n  build:\n    name: ${{ matrix.os }}, py-${{ matrix.python_version }}, node-${{ matrix.node_version }}\n    runs-on: ${{ matrix.os }}\n    continue-on-error: ${{ matrix.experimental }}\n    env:\n      # Whether to build and include extras (like aw-notify and aw-watcher-input)\n      AW_EXTRAS: true\n      # sets the macOS version target, see: https://users.rust-lang.org/t/compile-rust-binary-for-older-versions-of-mac-osx/38695\n      MACOSX_DEPLOYMENT_TARGET: 10.9\n    defaults:\n      run:\n        shell: bash\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-24.04, windows-latest, macos-14, macos-latest]\n        python_version: [3.9]\n        node_version: [22]\n        skip_rust: [false]\n        skip_webui: [false]\n        experimental: [false]\n\n        #include:\n        #  - os: ubuntu-latest\n        #    python_version: 3.9\n        #    node_version: 20\n        #    experimental: true\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        submodules: 'recursive'\n        fetch-depth: 0  # fetch all branches and tags\n\n    # Build in release mode if: (longer build times)\n    #  - on a tag (release)\n    #  - on the master branch (nightly)\n    - name: Set RELEASE\n      run: |\n        echo \"RELEASE=${{ startsWith(github.ref_name, 'v') || github.ref_name == 'master' }}\" >> $GITHUB_ENV\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      with:\n        python-version: ${{ matrix.python_version }}\n\n    - name: Set up Node\n      if: ${{ !matrix.skip_webui }}\n      uses: actions/setup-node@v4\n      with:\n        node-version: ${{ matrix.node_version }}\n\n    - name: Set up Rust\n      if: ${{ !matrix.skip_rust }}\n      uses: dtolnay/rust-toolchain@master\n      id: toolchain\n      with:\n        toolchain: stable\n\n    - name: Cache node_modules\n      uses: actions/cache@v4\n      if: ${{ !matrix.skip_webui }}\n      with:\n        path: aw-server-rust/aw-webui/node_modules\n        key: ${{ matrix.os }}-node_modules-${{ hashFiles('**/package-lock.json') }}\n        restore-keys: |\n          ${{ matrix.os }}-node_modules-\n\n    - name: Cache cargo build\n      uses: actions/cache@v4\n      # if: ${{ !matrix.skip_rust && (runner.os != 'macOS') }}  # cache doesn't seem to behave nicely on macOS, see: https://github.com/ActivityWatch/aw-server-rust/issues/180\n      env:\n        cache-name: cargo-build-target\n      with:\n        path: aw-server-rust/target\n        # key needs to contain rustc_hash due to https://github.com/ActivityWatch/aw-server-rust/issues/180\n        key: ${{ matrix.os }}-${{ env.cache-name }}-${{ steps.toolchain.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }}\n        restore-keys: |\n          ${{ matrix.os }}-${{ env.cache-name }}-${{ steps.toolchain.outputs.rustc_hash }}-\n\n    - name: Install APT dependencies\n      if: runner.os == 'Linux'\n      run: |\n          sudo apt-get update\n          # Unsure which of these are actually necessary...\n          sudo apt-get install -y \\\n            appstream \\\n            qt5-qmake \\\n            qtbase5-dev \\\n            qtwayland5 \\\n            libqt5x11extras5 \\\n            libfontconfig1 \\\n            libxcb1 \\\n            libfontconfig1-dev \\\n            libfreetype6-dev \\\n            libx11-dev \\\n            libxcursor-dev \\\n            libxext-dev \\\n            libxfixes-dev \\\n            libxft-dev \\\n            libxi-dev \\\n            libxrandr-dev \\\n            libxrender-dev\n\n    - name: Install dependencies\n      run: |\n        if [ \"$RUNNER_OS\" == \"Windows\" ]; then\n          choco install innosetup\n        fi\n        pip3 install poetry==1.4.2\n\n    - name: Build\n      run: |\n        python3 -m venv venv\n        source venv/bin/activate || source venv/Scripts/activate\n        poetry install\n        make build SKIP_WEBUI=${{ matrix.skip_webui }} SKIP_SERVER_RUST=${{ matrix.skip_rust }}\n        pip freeze  # output Python packages, useful for debugging dependency versions\n\n    - name: Run tests\n      run: |\n        source venv/bin/activate || source venv/Scripts/activate\n        make test SKIP_SERVER_RUST=${{ matrix.skip_rust }}\n\n    # Don't run integration tests on Windows, doesn't work for some reason\n    - name: Run integration tests\n      if: runner.os != 'Windows'\n      run: |\n        source venv/bin/activate || source venv/Scripts/activate\n        make test-integration\n\n    - name: Package\n      run: |\n        source venv/bin/activate || source venv/Scripts/activate\n        poetry install  # run again to ensure we have the correct version of PyInstaller\n        make package SKIP_SERVER_RUST=${{ matrix.skip_rust }}\n\n    - name: Package dmg\n      if: runner.os == 'macOS'\n      run: |\n        # Load certificates\n        # Only load key & sign if env vars for signing exists\n        if [ -n \"$APPLE_EMAIL\" ]; then\n          ./scripts/ci/import-macos-p12.sh\n        fi\n\n        # Build .app and .dmg\n        source venv/bin/activate\n        make dist/ActivityWatch.dmg\n\n        # codesign and notarize\n        if [ -n \"$APPLE_EMAIL\" ]; then\n          codesign --verbose -s ${APPLE_PERSONALID} dist/ActivityWatch.dmg\n\n          # Run prechecks\n          brew install akeru-inc/tap/xcnotary\n          xcnotary precheck dist/ActivityWatch.app\n          xcnotary precheck dist/ActivityWatch.dmg\n\n          # Notarize\n          make dist/notarize\n        fi\n        mv dist/ActivityWatch.dmg dist/activitywatch-$(scripts/package/getversion.sh)-macos-x86_64.dmg\n      env:\n        APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }}\n        APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}\n        APPLE_PERSONALID: ${{ secrets.APPLE_TEAMID }} # APPLE_PERSONAL_ID == APPLE_TEAM_ID for personal accounts\n        APPLE_TEAMID: ${{ secrets.APPLE_TEAMID }}\n        CERTIFICATE_MACOS_P12_BASE64: ${{ secrets.CERTIFICATE_MACOS_P12_BASE64 }}\n        CERTIFICATE_MACOS_P12_PASSWORD: ${{ secrets.CERTIFICATE_MACOS_P12_PASSWORD }}\n\n    - name: Package AppImage\n      if: startsWith(runner.os, 'linux')\n      run: |\n        ./scripts/package/package-appimage.sh\n\n    - name: Package deb\n      if: startsWith(runner.os, 'linux')\n      run: |\n        # The entire process is deferred to a shell file for consistency.\n        ./scripts/package/package-deb.sh\n\n    - name: Upload packages\n      uses: actions/upload-artifact@v4\n      with:\n        name: builds-${{ matrix.os }}-py${{ matrix.python_version }}\n        path: dist/activitywatch-*.*\n\n  release-notes:\n    runs-on: ubuntu-latest\n    if: startsWith(github.ref, 'refs/tags/v')  # only on runs triggered from tag\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: 'recursive'\n          fetch-depth: 0  # fetch all branches and tags\n\n      - uses: ActivityWatch/check-version-format-action@v2\n        id: version\n        with:\n          prefix: 'v'\n\n      - name: Echo version\n        run: |\n          echo \"${{ steps.version.outputs.full }} (stable: ${{ steps.version.outputs.is_stable }})\"\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n\n      - name: Install deps\n        run: |\n          pip install requests\n\n      - name: Generate release notes\n        run: |\n          LAST_RELEASE=`STABLE_ONLY=${{ steps.version.output.is_stable }} ./scripts/get_latest_release.sh`\n          ./scripts/build_changelog.py --range \"$LAST_RELEASE...${{ steps.version.outputs.full }}\"\n\n      # TODO: Move rename build_changelog and move into there\n      - name: Rename\n        run: |\n          mv changelog.md release_notes.md\n\n      - name: Upload release notes\n        uses: actions/upload-artifact@v4\n        with:\n          name: release_notes\n          path: release_notes.md\n\n  release:\n    needs: [build, release-notes]\n    if: startsWith(github.ref, 'refs/tags/v')  # only run on tag\n    runs-on: ubuntu-latest\n    steps:\n      # Will download all artifacts to path\n      - name: Download build artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: dist\n\n      - name: Display structure of downloaded files\n        run: ls -R\n        working-directory: dist\n\n      # detect if version tag is stable/beta\n      - uses: ActivityWatch/check-version-format-action@v2\n        id: version\n        with:\n          prefix: 'v'\n\n      # create a release\n      - name: Release\n        uses: softprops/action-gh-release@v1\n        with:\n          draft: true\n          files: dist/*/activitywatch-*.*\n          body_path: dist/release_notes/release_notes.md\n          prerelease: ${{ !(steps.version.outputs.is_stable == 'true') }}  # must compare to true, since boolean outputs are actually just strings, and \"false\" is truthy since it's not empty: https://github.com/actions/runner/issues/1483#issuecomment-994986996\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "name: \"CodeQL\"\n\non:\n  push:\n    branches: [ \"master\" ]\n  pull_request:\n    branches: [ \"master\" ]\n  schedule:\n    - cron: \"57 14 * * 4\"\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ python, javascript ]\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n            submodules: recursive\n\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@v2\n        with:\n          languages: ${{ matrix.language }}\n          queries: +security-and-quality\n\n      - name: Autobuild\n        uses: github/codeql-action/autobuild@v2\n\n      - name: Perform CodeQL Analysis\n        uses: github/codeql-action/analyze@v2\n        with:\n          category: \"/language:${{ matrix.language }}\"\n"
  },
  {
    "path": ".github/workflows/dependabot-automerge.yml",
    "content": "name: Dependabot Auto-merge\n\n# NOTE: This workflow relies on a Personal Access Token from the @ActivityWatchBot user\n#       See this issue for details: https://github.com/ridedott/merge-me-action/issues/1581\n\non:\n  workflow_run:\n    types:\n      - completed\n    workflows:\n      # List all required workflow names here.\n      - Build\n\npermissions:\n  contents: write\n  pull-requests: read\n\njobs:\n  auto_merge:\n    name: Auto-merge\n    runs-on: ubuntu-latest\n    if: github.event.workflow_run.conclusion == 'success' && github.actor == 'dependabot[bot]'\n\n    steps:\n      - uses: ridedott/merge-me-action@v2\n        with:\n          GITHUB_TOKEN: ${{ secrets.AWBOT_GH_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/diagram.yml",
    "content": "name: Diagram\n\non:\n  workflow_dispatch: {}\n  push:\n    branches:\n      - diagram\n     #- master  # protected branch, can't push updated diagram to\n\njobs:\n  update-diagram:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          submodules: recursive\n      - name: Also checkout docs & website\n        run: |\n          git clone https://github.com/ActivityWatch/docs\n          git clone https://github.com/ActivityWatch/activitywatch.github.io\n      - name: Update diagram\n        uses: githubocto/repo-visualizer@main\n        with:\n          commit_message: 'chore: update diagram [skip ci]'\n          file_colors: '{\"rs\": \"#b7410e\", \"py\": \"#229922\", \"rst\": \"pink\", \"txt\": \"pink\", \"md\": \"pink\", \"css\": \"purple\", \"scss\": \"purple\"}'\n          excluded_globs: \"**/.github;**/.git;**/*.builds;**/*.bat;**/*.iss;**/*.ps1;**/*.pyi;**/*.plist;**/*.cmd\"\n          #excluded_paths: '.github'\n"
  },
  {
    "path": ".github/workflows/greetings.yml",
    "content": "name: Greetings\n\non: [issues, pull_request]\n\njobs:\n  greeting:\n    runs-on: ubuntu-latest\n    if: github.repository_owner == 'ActivityWatch'  # don't run on forks\n    steps:\n    - uses: actions/first-interaction@v1\n      with:\n        repo-token: ${{ secrets.GITHUB_TOKEN }}\n        issue-message: >\n          Hi there!\n\n          As you're new to this repo, please make sure you've used an appropriate [issue template](https://github.com/ActivityWatch/activitywatch/issues/new/choose) and searched for duplicates (it helps us focus on actual development!).\n          We'd also like to suggest that you read our [contribution guidelines](https://github.com/ActivityWatch/activitywatch/blob/master/CONTRIBUTING.md) and our [code of conduct](https://github.com/ActivityWatch/activitywatch/blob/master/CODE_OF_CONDUCT.md).\n\n          Thanks a bunch for opening your first issue! 🙏\n        pr-message: >\n          Congratulations on opening your first pull request to this repo!\n\n          We'll get back to you as soon as possible. In the meantime, please make sure you've read our [contribution guidelines](https://github.com/ActivityWatch/activitywatch/blob/master/CONTRIBUTING.md).\n\n          Thanks for contributing!\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non:\n  #push:\n  #  branches: [ master ]\n  #pull_request:\n  #  branches: [ master ]\n  workflow_dispatch:\n\n\n\njobs:\n  # an integration test designed to catch bugs triggered by updating (database migrations and such)\n  upgrades:\n    name: upgrade from ${{ matrix.aw_server_old }} ${{ matrix.aw_server_old_args }} to ${{ matrix.aw_server_new }} ${{ matrix.aw_server_new_args }}\n    # needs: [build]\n    #if: startsWith(github.ref, 'refs/tags/v')  # only run on tag\n    runs-on: ubuntu-latest\n    env:\n      old_version: 'v0.12.2'\n      new_version: 'v0.13.1'\n    strategy:\n      fail-fast: false\n      matrix:\n        aw_server_old: ['aw-server', 'aw-server-rust']\n        aw_server_new: ['aw-server', 'aw-server-rust']\n        aw_server_old_args: ['']\n        aw_server_new_args: ['']\n        include:\n          # python, peewee (default)\n          - aw_server_old: 'aw-server'\n            aw_server_new: 'aw-server'\n          # python, sqlite\n          # FIXME: sqlite broken since aw-server enabled flask multithreading (new default)\n          - aw_server_old: \"aw-server\"\n            aw_server_new: \"aw-server\"\n            aw_server_old_args: \"--storage sqlite\"\n            aw_server_new_args: \"--storage sqlite\"\n            old_version: 'v0.12.2'\n            new_version: 'v0.13.1'\n          # python, peewee to sqlite\n          # FIXME: broken, same thing with sqlite as above\n          - aw_server_old: \"aw-server\"\n            aw_server_new: \"aw-server\"\n            aw_server_old_args: \"--storage peewee\"\n            aw_server_new_args: \"--storage sqlite\"\n            old_version: 'v0.12.2'\n            new_version: 'v0.13.1'\n        exclude:\n          # rust to python, not supported\n          - aw_server_old: 'aw-server-rust'\n            aw_server_new: 'aw-server'\n\n    steps:\n      # Will download all artifacts to path\n      - name: Download build artifacts\n        if: ${{ env.new_version == 'this' }}\n        uses: actions/download-artifact@v4\n        with:\n          name: builds-Linux-py3.9\n          path: dist\n\n      # Only used during testing, so we don't have to wait for the main build job\n      - name: Download new ActivityWatch\n        if: ${{ env.new_version != 'this' }}\n        run: |\n          mkdir dist\n          pushd dist\n          wget -q https://github.com/ActivityWatch/activitywatch/releases/download/${{ env.new_version }}/activitywatch-${{ env.new_version }}-linux-x86_64.zip\n\n      - name: Install new & old ActivityWatch\n        run: |\n          pushd dist\n\n          # New version\n          unzip activitywatch-*-linux-x86_64.zip\n          mv activitywatch/ aw-new\n\n          # Old version\n          wget -q -O aw-old.zip https://github.com/ActivityWatch/activitywatch/releases/download/${{ env.old_version }}/activitywatch-${{ env.old_version }}-linux-x86_64.zip\n          unzip aw-old.zip\n          mv activitywatch/ aw-old\n\n      - name: Display structure of downloaded files\n        run: ls -R\n        working-directory: dist\n\n      - name: Run and test old server\n        run: |\n          bin=dist/aw-old/${{ matrix.aw_server_old }}/${{ matrix.aw_server_old }}\n          url=\"http://localhost:5600\"\n\n          # Check version\n          $bin --version || true  # due to bug in old aw-server\n\n          # Run server and log output\n          $bin ${{ matrix.aw_server_old_args }} >> log-old.txt 2>&1 &\n          sleep 5  # wait for startup\n\n          # Set server URL\n\n          # Get server info\n          curl \"$url/api/0/info\" --fail-with-body\n\n          # Create bucket\n          curl -X 'POST' --fail-with-body \\\n            \"$url/api/0/buckets/aw-test\" \\\n            -H 'accept: application/json' \\\n            -H 'Content-Type: application/json' \\\n            -d '{\n            \"client\": \"test\",\n            \"type\": \"test\",\n            \"hostname\": \"test\"\n          }'\n\n          # Get buckets\n          curl \"$url/api/0/buckets/\" -H 'accept: application/json'\n\n          # Send a heartbeat\n          timestamp=$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\n          curl -X 'POST' \\\n            \"$url/api/0/buckets/aw-test/heartbeat?pulsetime=0\" \\\n            -H 'accept: application/json' \\\n            -H 'Content-Type: application/json' \\\n            -d '{\n            \"timestamp\": \"'$timestamp'\",\n            \"duration\": 0,\n            \"data\": {\"key\": \"test value\"}\n          }'\n\n          # Give a sec, then kill server process\n          sleep 1\n          kill $!\n\n      - name: Run and test new server\n        run: |\n          bin=dist/aw-new/${{ matrix.aw_server_new }}/${{ matrix.aw_server_new }}\n          url=\"http://localhost:5600\"\n\n          # Check version\n          $bin --version\n\n          # Run server and log output\n          $bin ${{ matrix.aw_server_new_args }} >> log-new.txt 2>&1 &\n          sleep 5  # wait for startup\n\n          # Get server info\n          curl \"$url/api/0/info\"\n\n          # Get buckets\n          curl \"$url/api/0/buckets/\" -H 'accept: application/json'\n\n          # Send a heartbeat\n          timestamp=$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\n          curl -X 'POST' --fail-with-body \\\n            \"$url/api/0/buckets/aw-test/heartbeat?pulsetime=60\" \\\n            -H 'accept: application/json' \\\n            -H 'Content-Type: application/json' \\\n            -d '{\n            \"timestamp\": \"'$timestamp'\",\n            \"duration\": 0,\n            \"data\": {\"key\": \"test value\"}\n          }'\n\n          # Give a sec, then kill server process\n          sleep 1\n          kill $!\n\n      - name: Output logs\n        if: always()\n        run: |\n          cat log-old.txt || true\n          echo \"\\n---\\n\"\n          cat log-new.txt || true\n\n"
  },
  {
    "path": ".github/workflows/winget.yml",
    "content": "name: Publish to WinGet\non:\n  release:\n    types: [released]\njobs:\n  publish:\n    runs-on: windows-latest # action can only be run on windows\n    steps:\n      - uses: vedantmgoyal2009/winget-releaser@v2\n        with:\n          identifier: ActivityWatch.ActivityWatch\n          token: ${{ secrets.GH_TOKEN_WINGET_AUTOUPDATE }}\n          fork-user: ActivityWatchBot\n"
  },
  {
    "path": ".gitignore",
    "content": "build\ndist\ndocs\nother\nold\n\n# Coverage\n*coverage*\nhtmlcov\n\n# Editor/IDEs\n.idea\n*.swp\n\n# Python\n*venv*\n__pycache__\n.python-version\n\n# Misc\n.*cache\n.DS_Store\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"aw-core\"]\n\tpath = aw-core\n\turl = https://github.com/ActivityWatch/aw-core.git\n[submodule \"aw-client\"]\n\tpath = aw-client\n\turl = https://github.com/ActivityWatch/aw-client.git\n[submodule \"aw-server\"]\n\tpath = aw-server\n\turl = https://github.com/ActivityWatch/aw-server.git\n[submodule \"aw-watcher-afk\"]\n\tpath = aw-watcher-afk\n\turl = https://github.com/ActivityWatch/aw-watcher-afk.git\n[submodule \"aw-qt\"]\n\tpath = aw-qt\n\turl = https://github.com/ActivityWatch/aw-qt.git\n[submodule \"aw-watcher-window\"]\n\tpath = aw-watcher-window\n\turl = https://github.com/ActivityWatch/aw-watcher-window.git\n[submodule \"aw-server-rust\"]\n\tpath = aw-server-rust\n\turl = https://github.com/ActivityWatch/aw-server-rust.git\n[submodule \"aw-watcher-input\"]\n\tpath = aw-watcher-input\n\turl = https://github.com/ActivityWatch/aw-watcher-input.git\n[submodule \"aw-tauri\"]\n\tpath = aw-tauri\n\turl = https://github.com/activitywatch/aw-tauri\n[submodule \"awatcher\"]\n\tpath = awatcher\n\turl = https://github.com/2e3s/awatcher\n[submodule \"aw-notify\"]\n\tpath = aw-notify\n\turl = https://github.com/ActivityWatch/aw-notify-rs.git\n"
  },
  {
    "path": ".tool-versions",
    "content": "poetry 1.5.1\nnodejs 16.20.2\nrust nightly\npython 3.9.13\n"
  },
  {
    "path": "CITATION.cff",
    "content": "cff-version: 1.2.0\nmessage: \"If you use or refer to this software in your research, please cite it.\"\nauthors:\n- family-names: \"Bjäreholt\"\n  given-names: \"Erik\"\n  orcid: \"https://orcid.org/0000-0003-1350-9677\"\n- family-names: \"Bjäreholt\"\n  given-names: \"Johan\"\n  orcid: \"https://orcid.org/0000-0003-4789-3160\"\ntitle: \"ActivityWatch\"\nversion: 0.13.1\ndoi: 10.5281/zenodo.4957165\ndate-released: 2024-06-10\nurl: \"https://github.com/ActivityWatch/activitywatch\"\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at erik@bjareho.lt. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "How to Contribute\n=================\n\n<!-- This guide could be improved by following the advice at https://mozillascience.github.io/working-open-workshop/contributing/ -->\n\n**Table of Contents**\n\n - [Getting started](#getting-started)\n - [How you can help](#how-you-can-help)\n - [Filing an issue](#filing-an-issue)\n - [Code of Conduct](#code-of-conduct)\n - [Commit message guidelines](#commit-message-guidelines)\n - [Getting paid](#getting-paid)\n - [Claiming GitPOAP](#claiming-gitpoap)\n - [Questions?](#questions)\n\n\n## Getting started\n\nTo develop on ActivityWatch you'll first want to install from source. To do so, follow [the guide in the documentation](https://activitywatch.readthedocs.io/en/latest/installing-from-source.html).\n\nYou might then want to read about the [architecture](https://activitywatch.readthedocs.io/en/latest/architecture.html) and the [data model](https://activitywatch.readthedocs.io/en/latest/buckets-and-events.html).\n\nIf you want some code examples for how to write watchers or other types of clients, see the [documentation for writing watchers](https://docs.activitywatch.net/en/latest/examples/writing-watchers.html).\n\n\n## How you can help\n\nThere are many ways to contribute to ActivityWatch:\n\n - Work on issues labeled [`good first issue`][good first issue] or [`help wanted`][help wanted], these are especially suited for new contributors.\n - Fix [`bugs`][bugs].\n - Implement new features.\n   - Look among the [requested features][requested features] on the forum.\n   - Talk to us in the issues or on [our Discord server][discord] to get help on how to proceed.\n - Write [documentation](https://github.com/ActivityWatch/docs).\n - Build the ecosystem.\n   - Examples: New watchers, tools to analyze data, tools to import data from other sources, etc.\n\nIf you're interested in what's next for ActivityWatch, have a look at our [roadmap][roadmap] and [milestones][milestones].\n\nMost of the above will get you up on our [contributor stats page][contributors] as thanks!\n\n[good first issue]: https://github.com/ActivityWatch/activitywatch/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22\n[help wanted]: https://github.com/ActivityWatch/activitywatch/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22\n[bugs]: https://github.com/ActivityWatch/activitywatch/issues?q=is%3Aissue+is%3Aopen+label%3A%22type%3A+bug%22\n[milestones]: https://github.com/ActivityWatch/activitywatch/milestones\n[roadmap]: https://github.com/orgs/ActivityWatch/projects/2\n[requested features]: https://forum.activitywatch.net/c/features\n[contributors]: http://activitywatch.net/contributors/\n\n\n## Filing an issue\n\nThanks for wanting to help out with squashing bugs and more by filing an issue.\n\nWhen filing an issue, it's important to use an [issue template](https://github.com/ActivityWatch/activitywatch/issues/new/choose). This ensures that we have the information we need to understand the issue, so we don't have to ask for tons of follow-up questions, so we can fix the issue faster!\n\n\n## Code of Conduct\n\nWe have a Code of Conduct that we expect all contributors to follow, you can find it in [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md).\n\n\n## Commit message guidelines\n\nWhen writing commit messages try to follow [Conventional Commits](https://www.conventionalcommits.org/). It is not a strict requirement (to minimize overhead for new contributors) but it is encouraged.\n\nThe format is: \n\n```\n<type>[optional scope]: <description>\n\n[optional body]\n\n[optional footer]\n```\n\nWhere `type` can be one of: `feat, fix, chore, ci, docs, style, refactor, perf, test`\n\nExamples:\n\n```\n - feat: added ability to sort by duration\n - fix: fixes incorrect week number (#407)\n - docs: improved query documentation \n```\n\nThis guideline was adopted in [issue #391](https://github.com/ActivityWatch/activitywatch/issues/391).\n\n\n## Getting paid\n\nWe're experimenting with paying our contributors using funds we've raised from donations and grants. \n\nThe idea is you track your work with ActivityWatch (and ensure it gets categorized correctly), then you modify the [working_hours.py](https://github.com/ActivityWatch/aw-client/blob/master/examples/working_hours.py) script to use your category rule and generate a report of time worked per day and the matching events.\n\nIf you've contributed to ActivityWatch (for a minimum of 10h) and want to get paid for your time, contact us!\n\nYou can read more about this experiment on [the forum](https://forum.activitywatch.net/t/getting-paid-with-activitywatch/986) and in [the issues](https://github.com/ActivityWatch/activitywatch/issues/458).\n\n\n## Claiming GitPOAP\n\nIf you've contributed a commit to ActivityWatch, you are eligible to claim a GitPOAP on Ethereum. You can read about it here: https://twitter.com/ActivityWatchIt/status/1584454595467612160\n\nThe one for 2022 looks like this:\n\n<a href=\"https://www.gitpoap.io/gh/ActivityWatch/activitywatch\">\n  <img src=\"https://assets.poap.xyz/gitpoap-2022-activitywatch-contributor-2022-logo-1663695908409.png\" width=\"256px\">\n</a>\n\n\n## Questions?\n\nIf you have any questions, you can:\n\n - Talk to us on our [Discord server][discord]\n - Post on [the forum][forum] or [GitHub Discussions][github discussions].\n - (as a last resort/if needed) Email one of the maintainers at: [erik@bjareho.lt](mailto:erik@bjareho.lt)\n\n[forum]: https://forum.activitywatch.net\n[github discussions]: https://github.com/ActivityWatch/activitywatch/discussions\n[discord]: https://discord.gg/vDskV9q\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at https://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "Makefile",
    "content": "# =====================================\n# Makefile for the ActivityWatch bundle\n# =====================================\n#\n# [GUIDE] How to install from source:\n#  - https://activitywatch.readthedocs.io/en/latest/installing-from-source.html\n#\n# We recommend creating and activating a Python virtualenv before building.\n# Instructions on how to do this can be found in the guide linked above.\n.PHONY: build install test clean clean_all\n\nSHELL := /usr/bin/env bash\n\nOS := $(shell uname -s)\n\nifeq ($(TAURI_BUILD),true)\n\tSUBMODULES := aw-core aw-client aw-server aw-server-rust aw-watcher-afk aw-watcher-window aw-tauri\n\t# Include awatcher on Linux (Wayland-compatible window watcher)\n\tifeq ($(OS),Linux)\n\t\tSUBMODULES := $(SUBMODULES) awatcher\n\tendif\nelse\n\tSUBMODULES := aw-core aw-client aw-qt aw-server aw-server-rust aw-watcher-afk aw-watcher-window\nendif\n\n# Exclude aw-server-rust if SKIP_SERVER_RUST is true\nifeq ($(SKIP_SERVER_RUST),true)\n\tSUBMODULES := $(filter-out aw-server-rust,$(SUBMODULES))\nendif\n# Include extras if AW_EXTRAS is true\nifeq ($(AW_EXTRAS),true)\n\tSUBMODULES := $(SUBMODULES) aw-notify aw-watcher-input\nendif\n\n# A function that checks if a target exists in a Makefile\n# Usage: $(call has_target,<dir>,<target>)\ndefine has_target\n$(shell make -q -C $1 $2 >/dev/null 2>&1; if [ $$? -eq 0 -o $$? -eq 1 ]; then echo $1; fi)\nendef\n\n# Submodules with test/package/lint/typecheck targets\nTESTABLES := $(foreach dir,$(SUBMODULES),$(call has_target,$(dir),test))\nPACKAGEABLES := $(foreach dir,$(SUBMODULES),$(call has_target,$(dir),package))\nLINTABLES := $(foreach dir,$(SUBMODULES),$(call has_target,$(dir),lint))\nTYPECHECKABLES := $(foreach dir,$(SUBMODULES),$(call has_target,$(dir),typecheck))\n\n# When building with Tauri, aw-server-rust is built as aw-sync only (not full server),\n# so exclude it from the standard package target\nifeq ($(TAURI_BUILD),true)\n\tPACKAGEABLES := $(filter-out aw-server-rust aw-server, $(PACKAGEABLES))\nendif\n\n# Build mode: release vs debug\nifeq ($(RELEASE), false)\n\ttargetdir := debug\nelse\n\ttargetdir := release\nendif\n\n# The `build` target\n# ------------------\n#\n# What it does:\n#  - Installs all the Python modules\n#  - Builds the web UI and bundles it with aw-server\nbuild: aw-core/.git\n#\tneeded due to https://github.com/pypa/setuptools/issues/1963\n#\twould ordinarily be specified in pyproject.toml, but is not respected due to https://github.com/pypa/setuptools/issues/1963\n\tpip install 'setuptools>49.1.1'\n\tfor module in $(SUBMODULES); do \\\n\t\techo \"Building $$module\"; \\\n\t\tif [ \"$$module\" = \"aw-server-rust\" ] && [ \"$(TAURI_BUILD)\" = \"true\" ]; then \\\n\t\t\tmake --directory=$$module aw-sync SKIP_WEBUI=$(SKIP_WEBUI) || { echo \"Error in $$module aw-sync\"; exit 2; }; \\\n\t\telse \\\n\t\t\tmake --directory=$$module build SKIP_WEBUI=$(SKIP_WEBUI) || { echo \"Error in $$module build\"; exit 2; }; \\\n\t\tfi; \\\n\tdone\n#   The below is needed due to: https://github.com/ActivityWatch/activitywatch/issues/173\n\tmake --directory=aw-client build\n\tmake --directory=aw-core build\n#\tNeeded to ensure that the server has the correct version set\n\tpython -c \"import aw_server; print(aw_server.__version__)\"\n\n\n# Install\n# -------\n#\n# Installs things like desktop/menu shortcuts.\n# Might in the future configure autostart on the system.\nifneq ($(TAURI_BUILD),true)\ninstall:\n\tmake --directory=aw-qt install\n# Installation is already happening in the `make build` step currently.\n# We might want to change this.\n# We should also add some option to install as user (pip3 install --user)\nendif\n\n# Update\n# ------\n#\n# Pulls the latest version, updates all the submodules, then runs `make build`.\nupdate:\n\tgit pull\n\tgit submodule update --init --recursive\n\tmake build\n\n\nlint:\n\t@for module in $(LINTABLES); do \\\n\t\techo \"Linting $$module\"; \\\n\t\tmake --directory=$$module lint || { echo \"Error in $$module lint\"; exit 2; }; \\\n\tdone\n\ntypecheck:\n\t@for module in $(TYPECHECKABLES); do \\\n\t\techo \"Typechecking $$module\"; \\\n\t\tmake --directory=$$module typecheck || { echo \"Error in $$module typecheck\"; exit 2; }; \\\n\tdone\n\n# Uninstall\n# ---------\n#\n# Uninstalls all the Python modules.\nuninstall:\n\tmodules=$$(pip3 list --format=legacy | grep 'aw-' | grep -o '^aw-[^ ]*'); \\\n\tfor module in $$modules; do \\\n\t\techo \"Uninstalling $$module\"; \\\n\t\tpip3 uninstall -y $$module; \\\n\tdone\n\ntest:\n\t@for module in $(TESTABLES); do \\\n\t\techo \"Running tests for $$module\"; \\\n\t\tpoetry run make -C $$module test || { echo \"Error in $$module tests\"; exit 2; }; \\\n    done\n\ntest-integration:\n\t# TODO: Move \"integration tests\" to aw-client\n\t# FIXME: For whatever reason the script stalls on Appveyor\n\t#        Example: https://ci.appveyor.com/project/ErikBjare/activitywatch/build/1.0.167/job/k1ulexsc5ar5uv4v\n\t# aw-server-python\n\t@echo \"== Integration testing aw-server ==\"\n\t@pytest ./scripts/tests/integration_tests.py ./aw-server/tests/ -v\n\n%/.git:\n\tgit submodule update --init --recursive\n\nifeq ($(TAURI_BUILD),true)\n\tICON := \"aw-tauri/src-tauri/icons/icon.png\"\nelse\n\tICON := \"aw-qt/media/logo/logo.png\"\nendif\n\naw-qt/media/logo/logo.icns:\n\tmkdir -p build/MyIcon.iconset\n\tsips -z 16 16     $(ICON) --out build/MyIcon.iconset/icon_16x16.png\n\tsips -z 32 32     $(ICON) --out build/MyIcon.iconset/icon_16x16@2x.png\n\tsips -z 32 32     $(ICON) --out build/MyIcon.iconset/icon_32x32.png\n\tsips -z 64 64     $(ICON) --out build/MyIcon.iconset/icon_32x32@2x.png\n\tsips -z 128 128   $(ICON) --out build/MyIcon.iconset/icon_128x128.png\n\tsips -z 256 256   $(ICON) --out build/MyIcon.iconset/icon_128x128@2x.png\n\tsips -z 256 256   $(ICON) --out build/MyIcon.iconset/icon_256x256.png\n\tsips -z 512 512   $(ICON) --out build/MyIcon.iconset/icon_256x256@2x.png\n\tsips -z 512 512   $(ICON) --out build/MyIcon.iconset/icon_512x512.png\n\tcp\t\t\t\t  $(ICON)       build/MyIcon.iconset/icon_512x512@2x.png\n\ticonutil -c icns build/MyIcon.iconset\n\trm -R build/MyIcon.iconset\n\tmv build/MyIcon.icns aw-qt/media/logo/logo.icns\n\ndist/ActivityWatch.app: aw-qt/media/logo/logo.icns\nifeq ($(TAURI_BUILD),true)\n\tscripts/package/build_app_tauri.sh\nelse\n\tpyinstaller --clean --noconfirm aw.spec\nendif\n\ndist/ActivityWatch.dmg: dist/ActivityWatch.app\n\t# NOTE: This does not codesign the dmg, that is done in the CI config\n\tpip install dmgbuild\n\tdmgbuild -s scripts/package/dmgbuild-settings.py -D app=dist/ActivityWatch.app \"ActivityWatch\" dist/ActivityWatch.dmg\n\ndist/notarize:\n\t./scripts/notarize.sh\n\npackage:\n\trm -rf dist\n\tmkdir -p dist/activitywatch\n\tfor dir in $(PACKAGEABLES); do \\\n\t\tmake --directory=$$dir package; \\\n\t\tcp -r $$dir/dist/$$dir dist/activitywatch; \\\n\tdone\nifeq ($(TAURI_BUILD),true)\n# Copy aw-sync binary for Tauri builds\n\tmkdir -p dist/activitywatch/aw-server-rust\n\tcp aw-server-rust/target/$(targetdir)/aw-sync dist/activitywatch/aw-server-rust/aw-sync\nelse\n# Move aw-qt to the root of the dist folder\n\tmv dist/activitywatch/aw-qt aw-qt-tmp\n\tmv aw-qt-tmp/* dist/activitywatch\n\trmdir aw-qt-tmp\nendif\n# Remove problem-causing binaries\n\trm -f dist/activitywatch/libdrm.so.2       # see: https://github.com/ActivityWatch/activitywatch/issues/161\n\trm -f dist/activitywatch/libharfbuzz.so.0  # see: https://github.com/ActivityWatch/activitywatch/issues/660#issuecomment-959889230\n# These should be provided by the distro itself\n# Had to be removed due to otherwise causing the error:\n#   aw-qt: symbol lookup error: /opt/activitywatch/libQt5XcbQpa.so.5: undefined symbol: FT_Get_Font_Format\n\trm -f dist/activitywatch/libfontconfig.so.1\n\trm -f dist/activitywatch/libfreetype.so.6\n# Remove unnecessary files\n\trm -rf dist/activitywatch/pytz\n# Builds zips and setups\n\tbash scripts/package/package-all.sh\n\nclean:\n\trm -rf build dist\n\n# Clean all subprojects\nclean_all: clean\n\tfor dir in $(SUBMODULES); do \\\n\t\tmake --directory=$$dir clean; \\\n\tdone\n\nclean-auto:\n\trm -rIv **/aw-server-rust/target\n\trm -rIv **/aw-android/mobile/build\n\trm -rIfv **/node_modules\n"
  },
  {
    "path": "README.md",
    "content": "<img title=\"ActivityWatch\" src=\"https://activitywatch.net/img/banner.png\" align=\"center\">\n\n<p align=\"center\">\n  <b>Records what you do</b> so that you can <i>know how you've spent your time</i>.\n  <br>\n  All in a secure way where <i>you control the data</i>.\n</p>\n\n<p align=\"center\">\n  <a href=\"https://twitter.com/ActivityWatchIt\">\n    <img title=\"Twitter follow\" src=\"https://img.shields.io/twitter/follow/ActivityWatchIt.svg?style=social&label=Follow\"/>\n  </a>\n  <a href=\"https://github.com/ActivityWatch/activitywatch\">\n    <img title=\"Star on GitHub\" src=\"https://img.shields.io/github/stars/ActivityWatch/activitywatch.svg?style=social&label=Star\">\n  </a>\n\n  <br>\n\n  <b>\n    <a href=\"https://activitywatch.net/\">Website</a>\n    — <a href=\"https://forum.activitywatch.net/\">Forum</a>\n    — <a href=\"https://docs.activitywatch.net\">Documentation</a>\n    — <a href=\"https://github.com/ActivityWatch/activitywatch/releases\">Releases</a>\n  </b>\n\n  <br>\n\n  <b>\n    <a href=\"https://activitywatch.net/contributors/\">Contributor stats</a>\n    — <a href=\"https://activitywatch.net/ci/\">CI overview</a>\n  </b>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/ActivityWatch/activitywatch/actions?query=branch%3Amaster\">\n    <img title=\"Build Status GitHub\" src=\"https://github.com/ActivityWatch/activitywatch/workflows/Build/badge.svg?branch=master\" />\n  </a>\n  <a href=\"https://ci.appveyor.com/project/ErikBjare/activitywatch\">\n    <img title=\"Build Status Appveyor\" src=\"https://ci.appveyor.com/api/projects/status/vm7g9sdfi2vgix6n?svg=true\" />\n  </a>\n  <a href=\"https://docs.activitywatch.net\">\n    <img title=\"Documentation\" src=\"https://readthedocs.org/projects/activitywatch/badge/?version=latest\" />\n  </a>\n\n  <br>\n\n  <a href=\"https://github.com/ActivityWatch/activitywatch/releases\">\n    <img title=\"Latest release\" src=\"https://img.shields.io/github/release-pre/ActivityWatch/activitywatch.svg\">\n  </a>\n  <a href=\"https://github.com/ActivityWatch/activitywatch/releases\">\n    <img title=\"Total downloads (GitHub Releases)\" src=\"https://img.shields.io/github/downloads/ActivityWatch/activitywatch/total.svg\" />\n  </a>\n  <a href=\"https://discord.gg/vDskV9q\">\n    <img title=\"Discord\" src=\"https://img.shields.io/discord/755040852727955476\" />\n  </a>\n\n  <br>\n\n  <a href=\"https://activitywatch.net/donate/\">\n    <img title=\"Donated\" src=\"https://img.shields.io/badge/budget-%24201%2Fmo%20from%2040%20supporters-orange.svg\" />\n  </a>\n  <a href=\"https://doi.org/10.5281/zenodo.4957165\">\n    <img src=\"https://zenodo.org/badge/DOI/10.5281/zenodo.4957165.svg\" />\n  </a>\n</p>\n\n<!--\n# TODO: Best practices badge that we should work towards, see issue #42.\n[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/873/badge)](https://bestpractices.coreinfrastructure.org/projects/873)\n[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FActivityWatch%2Factivitywatch.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FActivityWatch%2Factivitywatch?ref=badge_shield)\n-->\n\n\n*Do you want to receive email updates on major announcements?*<br>\n***[Signup for the newsletter](http://eepurl.com/cTU6QX)!***\n\n<details>\n <summary>Table of Contents</summary>\n\n * [About](#about)\n    * [Screenshots](#screenshots)\n    * [Is this yet another time tracker?](#is-this-yet-another-time-tracker)\n       * [Feature comparison](#feature-comparison)\n    * [Installation &amp; Usage](#installation--usage)\n * [About this repository](#about-this-repository)\n    * [Server](#server)\n    * [Watchers](#watchers)\n    * [Libraries](#libraries)\n * [Contributing](#contributing)\n</details>\n\n## About\n\nThe goal of ActivityWatch is simple: *Enable the collection of as much valuable lifedata as possible without compromising user privacy.*\n\nWe've worked towards this goal by creating an application for safe storage of the data on the user's local machine and as well as a set of watchers which record data such as:\n\n - Currently active application and the title of its window\n - Currently active browser tab and its title and URL\n - Keyboard and mouse activity, to detect if you are AFK (\"away from keyboard\") or not\n\nIt is up to you as user to collect as much as you want, or as little as you want (and we hope some of you will help write watchers so we can collect more).\n\n### Screenshots\n\n<span><img src=\"https://activitywatch.net/img/screenshots/screenshot-v0.9.3-activity.png\"   width=\"45%\"></span>\n<span><img src=\"https://activitywatch.net/img/screenshots/screenshot-v0.8.0b9-timeline.png\" width=\"50%\"></span>\n\nYou can find more (and newer) screenshots on [the website](https://activitywatch.net/screenshots/).\n\n\n## Installation & Usage\n\nDownloads are available on the [releases page](https://github.com/ActivityWatch/activitywatch/releases).\n\nFor instructions on how to get started, please see the [guide in the documentation](https://docs.activitywatch.net/en/latest/getting-started.html).\n\nInterested in building from source? [There's a guide for that too](https://docs.activitywatch.net/en/latest/installing-from-source.html).\n\n## Is this yet another time tracker?\n\nYes, but we found that most time trackers lack one or more important features.\n\n**Common dealbreakers:**\n\n - Not open source\n - The user does not own the data (common with non-open source options)\n - Lack of synchronization (and when available: it's centralized and the sync server knows everything)\n - Difficult to setup/use (most open source options tend to target programmers)\n - Low data resolution (low level of detail, does not store raw data, long intervals between entries)\n - Hard or impossible to extend (collecting more data is not as simple as it could be)\n\n**To sum it up:**\n\n - Closed source solutions suffer from privacy issues and limited features.\n - Open source solutions aren't developed with end-users in mind and are usually not written to be easily extended (they lack a proper API). They also lack synchronization.\n\nWe have a plan to address all of these and we're well on our way. See the table below for our progress.\n\n\n### Feature comparison\n\n##### Basics\n\n|               | User owns data     | GUI                | Sync                       | Open Source        |\n| ------------- |:------------------:|:------------------:|:--------------------------:|:------------------:|\n| ActivityWatch | :white_check_mark: | :white_check_mark: | [WIP][sync], decentralized | :white_check_mark: |\n| [Selfspy]       | :white_check_mark: | :x:                | :x:                        | :white_check_mark: |\n| [ulogme]        | :white_check_mark: | :white_check_mark: | :x:                        | :white_check_mark: |\n| [RescueTime]    | :x:                | :white_check_mark: | Centralized                | :x:                |\n| [WakaTime]      | :x:                | :white_check_mark: | Centralized                | Clients            |\n\n[sync]: https://github.com/ActivityWatch/activitywatch/issues/35\n[Selfspy]: https://github.com/selfspy/selfspy\n[ulogme]: https://github.com/karpathy/ulogme\n[RescueTime]: https://www.rescuetime.com/\n[WakaTime]: https://wakatime.com/\n\n##### Platforms\n<!-- TODO: Replace Platform names with icons  -->\n\n|               | Windows            | macOS              | Linux              | Android            | iOS                 |\n| ------------- |:------------------:|:------------------:|:------------------:|:------------------:|:-------------------:|\n| ActivityWatch | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |:x:                  |\n| Selfspy       | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x:                |:x:                  |\n| ulogme        | :x:                | :white_check_mark: | :white_check_mark: | :x:                |:x:                  |\n| RescueTime    | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |Limited functionality|\n\n##### Tracking\n\n|               | App & Window Title | AFK                | Browser Extensions | Editor Plugins     | Extensible            |\n| ------------- |:------------------:|:------------------:|:------------------:|:------------------:|:---------------------:|\n| ActivityWatch | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark:    |\n| Selfspy       | :white_check_mark: | :white_check_mark: | :x:                | :x:                | :x:                   |\n| ulogme        | :white_check_mark: | :white_check_mark: | :x:                | :x:                | :x:                   |\n| RescueTime    | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x:                | :x:                   |\n| WakaTime      | :x:                | :white_check_mark: | :white_check_mark: | :white_check_mark: | Only for text editors |\n\nFor a complete list of the things ActivityWatch can track, [see the page on *watchers* in the documentation](https://docs.activitywatch.net/en/latest/watchers.html).\n\n\n## Architecture\n\n```mermaid\ngraph TD;\n  aw-qt[<a href='https://github.com/ActivityWatch/aw-qt'>aw-qt</a>];\n  aw-notify[<a href='https://github.com/ActivityWatch/aw-notify-rs'>aw-notify</a>];\n  aw-server[<a href='https://github.com/ActivityWatch/aw-server'>aw-server</a>];\n  aw-webui[<a href='https://github.com/ActivityWatch/aw-webui'>aw-webui</a>];\n  aw-watcher-window[<a href='https://github.com/ActivityWatch/aw-watcher-window'>aw-watcher-window</a>];\n  aw-watcher-afk[<a href='https://github.com/ActivityWatch/aw-watcher-afk'>aw-watcher-afk</a>];\n  aw-watcher-web[<a href='https://github.com/ActivityWatch/aw-watcher-web'>aw-watcher-web</a>];\n  aw-sync[<a href='https://github.com/ActivityWatch/aw-server-rust/tree/master/aw-sync'>aw-sync</a>];\n\n  aw-qt -- Manages --> aw-server;\n  aw-qt -- Manages --> aw-notify -- Queries --> aw-server;\n  aw-qt -- Manages --> aw-watcher-window -- Watches --> S1[Active window] -- Heartbeats --> aw-server;\n  aw-qt -- Manages --> aw-watcher-afk -- Watches --> S2[AFK status] -- Heartbeats --> aw-server;\n  Browser -- Manages --> aw-watcher-web -- Watches --> S3[Active tab] -- Heartbeats --> aw-server;\n  SF -- Dropbox/Syncthing/etc --> SF;\n  aw-server <-- Push/Pull --> aw-sync <-- Read/Write --> SF[Sync folder];\n  aw-server -- Serves --> aw-webui -- Queries --> aw-server;\n\n  %% User -- Interacts --> aw-webui;\n  %% User -- Observes --> aw-notify;\n  %% User -- Interacts --> aw-qt;\n\nclassDef lightMode fill:#FFFFFF, stroke:#333333, color:#333333;\nclassDef darkMode fill:#333333, stroke:#FFFFFF, color:#FFFFFF;\n\nclassDef lightModeLinks stroke:#333333;\nclassDef darkModeLinks stroke:#FFFFFF;\n\nclass A,B,C,D,E,G lightMode;\nclass A,B,C,D,E,G darkMode;\n\n%% linkStyle 0 stroke:#FF4136, stroke-width:2px;\n%% linkStyle 1 stroke:#1ABC9C, stroke-width:2px;\n```\n\n## About this repository\n\nThis repo is a bundle of the core components and official modules of ActivityWatch (managed with `git submodule`). Its primary use is as a meta-package providing all the components in one repo; enabling easier packaging and installation. It is also where releases of the full suite are published (see [releases](https://github.com/ActivityWatch/activitywatch/releases)).\n\n### Server\n\nActivityWatch has two server implementations:\n\n- `aw-server` (Python) - The current default implementation\n- `aw-server-rust` - A Rust implementation that is the planned future default\n\nBoth provide a REST API to a datastore and query engine, and serve the web interface developed in the `aw-webui` project (which provides the frontend).\n\nThe REST API includes:\n\n - Access to a datastore suitable for timeseries/timeperiod-data organized in \"buckets\" (containers grouping related activity data by metadata like client type or hostname)\n - **Buckets API:** Create, retrieve, and delete data buckets\n - **Events API:** Read and write timestamped events within buckets\n - **Heartbeat API:** Watchers use heartbeat signals to update the current state of activity (e.g., active application, AFK status)\n - **Query API:** simple query scripting language for filtering, merging, grouping, and transforming events\n - **Client libraries:** Language-specific libraries like `aw-client` (Python), `aw-client-js`, and `aw-client-rust` that wrap REST endpoints for programmatic access\n\nThe frontend (`aw-webui`) includes:\n\n - **Data visualization:** Dashboard and timeline views showing activity summaries with detailed breakdowns of app usage, web browsing, and user-defined categories\n - **Query explorer:** Browser-based interface for writing, executing, and debugging queries with real-time results\n - **Activity browser:** Navigate through historical data with filtering by date ranges, applications, websites, and custom categories\n - **Raw data access:** View and browse individual events from all tracking buckets with detailed metadata\n - **Export functionality:** Export activity data in JSON format (individual buckets or complete datasets) via web interface or REST API\n\n### Watchers\n\nActivityWatch comes pre-installed with two watchers:\n\n - `aw-watcher-afk` tracks the user active/inactive state from keyboard and mouse input\n - `aw-watcher-window` tracks the currently active application and its window title.\n\nThere are lots of other watchers for ActivityWatch which can track more types of activity. Like `aw-watcher-web` which tracks time spent on websites, multiple editor watchers which track spent time coding, and many more! A full list of watchers can be found in [the documentation](https://docs.activitywatch.net/en/latest/watchers.html).\n\n### Libraries\n\n - `aw-core` - core library, provides no runnable modules\n - `aw-client` - client library, useful when writing watchers\n\n### Folder structure\n\n<span><img src=\"https://raw.githubusercontent.com/ActivityWatch/activitywatch/master/diagram.svg\" width=\"60%\"></span>\n\n## Contributing\n\nWant to help? Great! Check out the [CONTRIBUTING.md file](./CONTRIBUTING.md)!\n\n## Questions and support\n\nHave a question, suggestion, problem, or just want to say hi? Post on [the forum](https://forum.activitywatch.net/)!\n\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n<!--\n## Supported Versions\n\nUse this section to tell people about which versions of your project are\ncurrently being supported with security updates.\n\n| Version    | Supported          |\n| ---------- | ------------------ |\n| 0.11.0     | :white_check_mark: |\n| <= 0.10.0  | :x:                |\n-->\n\n## Reporting a Vulnerability\n\n<!--\nUse this section to tell people how to report a vulnerability.\n\nTell them where to go, how often they can expect to get an update on a\nreported vulnerability, what to expect if the vulnerability is accepted or\ndeclined, etc.\n-->\n\nIf you discover a vulnerability, please send a PGP encrypted email with details to [erik@bjareho.lt](mailto:erik@bjareho.lt) (preferably PGP encrypted using [this key](https://erik.bjareholt.com/erikbjare.asc)).\n"
  },
  {
    "path": "aw.spec",
    "content": "# -*- mode: python -*-\n# vi: set ft=python :\n\nimport os\nimport platform\nimport shlex\nimport subprocess\nfrom pathlib import Path\n\nimport aw_core\nimport flask_restx\n\n\ndef build_analysis(name, location, binaries=[], datas=[], hiddenimports=[]):\n    name_py = name.replace(\"-\", \"_\")\n    location_candidates = [\n        location / f\"{name_py}/__main__.py\",\n        location / f\"src/{name_py}/__main__.py\",\n    ]\n    try:\n        location = next(p for p in location_candidates if p.exists())\n    except StopIteration:\n        raise Exception(f\"Could not find {name} location from {location_candidates}\")\n\n    return Analysis(\n        [location],\n        pathex=[],\n        binaries=binaries,\n        datas=datas,\n        hiddenimports=hiddenimports,\n        hookspath=[],\n        runtime_hooks=[],\n        excludes=[],\n        win_no_prefer_redirects=False,\n        win_private_assemblies=False,\n    )\n\n\ndef build_collect(analysis, name, console=True):\n    \"\"\"Used to build the COLLECT statements for each module\"\"\"\n    pyz = PYZ(analysis.pure, analysis.zipped_data)\n    exe = EXE(\n        pyz,\n        analysis.scripts,\n        exclude_binaries=True,\n        name=name,\n        debug=False,\n        strip=False,\n        upx=True,\n        console=console,\n        contents_directory=\".\",\n        entitlements_file=entitlements_file,\n        codesign_identity=codesign_identity,\n    )\n    return COLLECT(\n        exe,\n        analysis.binaries,\n        analysis.zipfiles,\n        analysis.datas,\n        strip=False,\n        upx=True,\n        name=name,\n    )\n\n\n# Get the current release version\ncurrent_release = subprocess.run(\n    shlex.split(\"git describe --tags --abbrev=0\"),\n    stdout=subprocess.PIPE,\n    stderr=subprocess.STDOUT,\n    encoding=\"utf8\",\n).stdout.strip()\nprint(\"bundling activitywatch version \" + current_release)\n\n# Get entitlements and codesign identity\nentitlements_file = Path(\".\") / \"scripts\" / \"package\" / \"entitlements.plist\"\ncodesign_identity = os.environ.get(\"APPLE_PERSONALID\", \"\").strip()\nif not codesign_identity:\n    print(\"Environment variable APPLE_PERSONALID not set. Releases won't be signed.\")\n\naw_core_path = Path(os.path.dirname(aw_core.__file__))\nrestx_path = Path(os.path.dirname(flask_restx.__file__))\n\naws_location = Path(\"aw-server\")\naw_server_rust_location = Path(\"aw-server-rust\")\naw_server_rust_bin = aw_server_rust_location / \"target/package/aw-server-rust\"\naw_sync_bin = aw_server_rust_location / \"target/package/aw-sync\"\naw_qt_location = Path(\"aw-qt\")\nawa_location = Path(\"aw-watcher-afk\")\naww_location = Path(\"aw-watcher-window\")\nawi_location = Path(\"aw-watcher-input\")\naw_notify_location = Path(\"aw-notify\")\n\nif platform.system() == \"Darwin\":\n    icon = aw_qt_location / \"media/logo/logo.icns\"\nelse:\n    icon = aw_qt_location / \"media/logo/logo.ico\"\n\nskip_rust = False\nif not aw_server_rust_bin.exists():\n    skip_rust = True\n    print(\"Skipping Rust build because aw-server-rust binary not found.\")\n\n\naw_qt_a = build_analysis(\n    \"aw-qt\",\n    aw_qt_location,\n    binaries=[(aw_server_rust_bin, \".\"), (aw_sync_bin, \".\")] if not skip_rust else [],\n    datas=[\n        (aw_qt_location / \"resources/aw-qt.desktop\", \"aw_qt/resources\"),\n        (aw_qt_location / \"media\", \"aw_qt/media\"),\n    ],\n)\naw_server_a = build_analysis(\n    \"aw-server\",\n    aws_location,\n    datas=[\n        (aws_location / \"aw_server/static\", \"aw_server/static\"),\n        (restx_path / \"templates\", \"flask_restx/templates\"),\n        (restx_path / \"static\", \"flask_restx/static\"),\n        (aw_core_path / \"schemas\", \"aw_core/schemas\"),\n    ],\n)\naw_watcher_afk_a = build_analysis(\n    \"aw_watcher_afk\",\n    awa_location,\n    hiddenimports=[\n        \"Xlib.keysymdef.miscellany\",\n        \"Xlib.keysymdef.latin1\",\n        \"Xlib.keysymdef.latin2\",\n        \"Xlib.keysymdef.latin3\",\n        \"Xlib.keysymdef.latin4\",\n        \"Xlib.keysymdef.greek\",\n        \"Xlib.support.unix_connect\",\n        \"Xlib.ext.shape\",\n        \"Xlib.ext.xinerama\",\n        \"Xlib.ext.composite\",\n        \"Xlib.ext.randr\",\n        \"Xlib.ext.xfixes\",\n        \"Xlib.ext.security\",\n        \"Xlib.ext.xinput\",\n        \"pynput.keyboard._xorg\",\n        \"pynput.mouse._xorg\",\n        \"pynput.keyboard._win32\",\n        \"pynput.mouse._win32\",\n        \"pynput.keyboard._darwin\",\n        \"pynput.mouse._darwin\",\n    ],\n)\naw_watcher_input_a = build_analysis(\"aw_watcher_input\", awi_location)\naw_watcher_window_a = build_analysis(\n    \"aw_watcher_window\",\n    aww_location,\n    binaries=(\n        [\n            (\n                aww_location / \"aw_watcher_window/aw-watcher-window-macos\",\n                \"aw_watcher_window\",\n            )\n        ]\n        if platform.system() == \"Darwin\"\n        else []\n    ),\n    datas=[\n        (aww_location / \"aw_watcher_window/printAppStatus.jxa\", \"aw_watcher_window\")\n    ],\n)\n# Check if aw-notify is a Python package\n_notify_candidates = [\n    aw_notify_location / \"aw_notify/__main__.py\",\n    aw_notify_location / \"src/aw_notify/__main__.py\",\n]\nskip_aw_notify = not any(p.exists() for p in _notify_candidates)\nif skip_aw_notify:\n    print(\"Skipping aw-notify Python packaging (Rust-based implementation detected)\")\n\naw_notify_a = None if skip_aw_notify else build_analysis(\n    \"aw_notify\", aw_notify_location, hiddenimports=[\"desktop_notifier.resources\"]\n)\n\n# https://pythonhosted.org/PyInstaller/spec-files.html#multipackage-bundles\n# MERGE takes a bit weird arguments, it wants tuples which consists of\n# the analysis paired with the script name and the bin name\nmerge_args = [\n    (aw_server_a, \"aw-server\", \"aw-server\"),\n    (aw_qt_a, \"aw-qt\", \"aw-qt\"),\n    (aw_watcher_afk_a, \"aw-watcher-afk\", \"aw-watcher-afk\"),\n    (aw_watcher_window_a, \"aw-watcher-window\", \"aw-watcher-window\"),\n    (aw_watcher_input_a, \"aw-watcher-input\", \"aw-watcher-input\"),\n]\nif aw_notify_a is not None:\n    merge_args.append((aw_notify_a, \"aw-notify\", \"aw-notify\"))\n\nMERGE(*merge_args)\n\n\n# aw-server\naws_coll = build_collect(aw_server_a, \"aw-server\")\n\n# aw-watcher-window\naww_coll = build_collect(aw_watcher_window_a, \"aw-watcher-window\")\n\n# aw-watcher-afk\nawa_coll = build_collect(aw_watcher_afk_a, \"aw-watcher-afk\")\n\n# aw-qt\nawq_coll = build_collect(\n    aw_qt_a,\n    \"aw-qt\",\n    console=False if platform.system() == \"Windows\" else True,\n)\n\n# aw-watcher-input\nawi_coll = build_collect(aw_watcher_input_a, \"aw-watcher-input\")\n\n# aw-notify (only if Python package exists)\naw_notify_coll = build_collect(aw_notify_a, \"aw-notify\") if aw_notify_a is not None else None\n\nif platform.system() == \"Darwin\":\n    bundle_args = [\n        awq_coll,\n        aws_coll,\n        aww_coll,\n        awa_coll,\n        awi_coll,\n    ]\n    if aw_notify_coll is not None:\n        bundle_args.append(aw_notify_coll)\n    \n    app = BUNDLE(\n        *bundle_args,\n        name=\"ActivityWatch.app\",\n        icon=icon,\n        bundle_identifier=\"net.activitywatch.ActivityWatch\",\n        version=current_release.lstrip(\"v\"),\n        info_plist={\n            \"NSPrincipalClass\": \"NSApplication\",\n            \"CFBundleExecutable\": \"MacOS/aw-qt\",\n            \"CFBundleIconFile\": \"logo.icns\",\n            \"NSAppleEventsUsageDescription\": \"Please grant access to use Apple Events\",\n            # This could be set to a more specific version string (including the commit id, for example)\n            \"CFBundleVersion\": current_release.lstrip(\"v\"),\n            # Replaced by the 'version' kwarg above\n            # \"CFBundleShortVersionString\": current_release.lstrip('v'),\n        },\n    )\n"
  },
  {
    "path": "gptme.toml",
    "content": "files = [\n    \"README.md\",\n    \"Makefile\",\n    \"aw-server/README.md\",\n    \"aw-server/aw-webui/README.md\",\n    \"aw-server-rust/README.md\",\n    \"aw-server-rust/aw-sync/README.md\",\n    \"aw-client/README.md\",\n    # ideally we'd also include some of the docs here, but they are not a submodule\n]\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"activitywatch\"\nversion = \"0.13.2\"\ndescription = \"The free and open-source automated time tracker. Cross-platform, extensible, privacy-focused.\"\nauthors = [\"Erik Bjäreholt <erik@bjareho.lt>\", \"Johan Bjäreholt <johan@bjareho.lt>\"]\nlicense = \"MPL-2.0\"\n\n[tool.poetry.dependencies]\npython = \"^3.8\"\n# Installing them from here won't work\n#aw-core = {path = \"aw-core\"}\n#aw-client = {path = \"aw-client\"}\n#aw-watcher-afk = {path = \"aw-watcher-afk\"}\n#aw-watcher-window = {path = \"aw-watcher-window\"}\n#aw-server = {path = \"aw-server\"}\n#aw-qt = {path = \"aw-qt\"}\n\n# https://github.com/ionrock/cachecontrol/issues/292\nurllib3 = \"<2\"\n\n[tool.poetry.dev-dependencies]\nmypy = \"*\"\npytest = \"*\"\npytest-cov = \"*\"\npytest-benchmark = \"*\"\npsutil = \"*\"\npywin32-ctypes = {version = \"*\", platform = \"win32\"}\npefile = {version = \"*\", platform = \"win32\"}\n\npyinstaller = {version = \"*\", python = \"^3.8,<3.14\"}\n# releases are very infrequent, so good idea to use the master branch\n# we need this unreleased commit: https://github.com/pyinstaller/pyinstaller-hooks-contrib/commit/0f40dc6e74086e5472aee75070b9077b4c17ab18\npyinstaller-hooks-contrib = {git = \"https://github.com/pyinstaller/pyinstaller-hooks-contrib.git\", branch=\"master\"}\n\n# Won't be respected due to https://github.com/python-poetry/poetry/issues/1584\n#setuptools = \">49.1.1\"  # needed due to https://github.com/pypa/setuptools/issues/1963\n\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "scripts/build_changelog.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nScript that generates a changelog for the repository and its submodules, and outputs it in the current directory.\n\nNOTE: This script can be downloaded as-is and run from your repository.\n\nRepos using this script:\n - ActivityWatch/activitywatch\n - ErikBjare/gptme\n\nManual actions needed to clean up for changelog:\n - Reorder modules in a logical order (aw-webui, aw-server, aw-server-rust, aw-watcher-window, aw-watcher-afk, ...)\n - Remove duplicate aw-webui entries\n\"\"\"\n\nimport argparse\nimport logging\nimport os\nimport re\nimport shlex\nfrom collections import defaultdict\nfrom collections.abc import Collection\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom subprocess import PIPE, STDOUT\nfrom subprocess import run as _run\nfrom time import sleep\nfrom typing import (\n    Dict,\n    List,\n    Optional,\n    Tuple,\n)\n\nimport requests\n\nlogging.basicConfig(level=logging.DEBUG)\nlogger = logging.getLogger(__name__)\n\n\nscript_dir = Path(__file__).parent.resolve()\n\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"Generate changelog from git history\")\n\n    # repo info\n    parser.add_argument(\"--org\", default=\"ActivityWatch\", help=\"GitHub organization\")\n    parser.add_argument(\"--repo\", default=\"activitywatch\", help=\"GitHub repository\")\n    parser.add_argument(\n        \"--project-title\", default=\"ActivityWatch\", help=\"Project title\"\n    )\n\n    # settings\n    last_tag = run(\"git describe --tags --abbrev=0\").strip()  # get latest tag\n    branch = run(\"git rev-parse --abbrev-ref HEAD\").strip()  # get current branch name\n    parser.add_argument(\n        \"--range\", default=f\"{last_tag}...{branch}\", help=\"Git commit range\"\n    )\n    parser.add_argument(\"--path\", default=\".\", help=\"Path to git repo\")\n\n    # output\n    parser.add_argument(\n        \"--output\", default=\"changelog.md\", help=\"Path to output changelog\"\n    )\n    parser.add_argument(\n        \"--add-version-header\",\n        action=\"store_true\",\n        help=\"Add version header and adjust heading levels for docs\",\n    )\n\n    # parse args\n    args = parser.parse_args()\n    since, until = args.range.split(\"...\", 1)\n\n    # preferred output order for submodules\n    repo_order = [\n        \"activitywatch\",\n        \"aw-server\",\n        \"aw-server-rust\",\n        \"aw-webui\",\n        \"aw-watcher-afk\",\n        \"aw-watcher-window\",\n        \"aw-qt\",\n        \"aw-core\",\n        \"aw-client\",\n    ]\n\n    build(\n        args.org,\n        args.repo,\n        args.project_title,\n        commit_range=(since, until),\n        output_path=args.output,\n        repo_order=repo_order,\n        add_version_header=args.add_version_header,\n    )\n\n\nclass CommitMsg:\n    type: str\n    subtype: str\n    msg: str\n\n\n@dataclass\nclass Commit:\n    id: str\n    msg: str\n    org: str\n    repo: str\n\n    @property\n    def msg_processed(self) -> str:\n        \"\"\"Generates links from commit and issue references (like 0c14d77, #123) to correct repo and such\"\"\"\n        s = self.msg\n        s = re.sub(\n            rf\"[^(-]https://github.com/{self.org}/([\\-\\w\\d]+)/(issues|pulls)/(\\d+)\",\n            rf\"[#\\3](https://github.com/{self.org}/\\1/issues/\\3)\",\n            s,\n        )\n        s = re.sub(\n            r\"#(\\d+)\",\n            rf\"[#\\1](https://github.com/{self.org}/{self.repo}/issues/\\1)\",\n            s,\n        )\n        s = re.sub(\n            r\"[\\s\\(][0-9a-f]{7}[\\s\\)]\",\n            rf\"[`\\0`](https://github.com/{self.org}/{self.repo}/issues/\\0)\",\n            s,\n        )\n        # wrap html elements in backticks, if not already wrapped\n        s = re.sub(r\"(?<!`)<([^>]+)>(?!`)\", r\"`<\\1>`\", s)\n        return s\n\n    def parse_type(self) -> Optional[Tuple[str, str]]:\n        # Needs to handle '!' indicating breaking change\n        match = re.search(r\"^(\\w+)(\\((.+)\\))?[!]?:\", self.msg)\n        if match:\n            type = match.group(1)\n            subtype = match.group(3)\n            if type in [\"build\", \"ci\", \"fix\", \"feat\"]:\n                return type, subtype\n        return None\n\n    @property\n    def type(self) -> Optional[str]:\n        _type, _ = self.parse_type() or (None, None)\n        return _type\n\n    @property\n    def subtype(self) -> Optional[str]:\n        _, subtype = self.parse_type() or (None, None)\n        return subtype\n\n    def type_str(self) -> str:\n        _type, subtype = self.parse_type() or (None, None)\n        return f\"{_type}\" + (f\"({subtype})\" if subtype else \"\")\n\n    def format(self) -> str:\n        commit_link = commit_linkify(self.id, self.org, self.repo) if self.id else \"\"\n\n        return f\"{self.msg_processed}\" + (f\" ({commit_link})\" if commit_link else \"\")\n\n\ndef run(cmd, cwd=\".\") -> str:\n    logger.debug(f\"Running in {cwd}: {cmd}\")\n    p = _run(shlex.split(cmd), stdout=PIPE, stderr=STDOUT, encoding=\"utf8\", cwd=cwd)\n    if p.returncode != 0:\n        print(p.stdout)\n        print(p.stderr)\n        raise Exception\n    return p.stdout\n\n\ndef pr_linkify(prid: str, org: str, repo: str) -> str:\n    return f\"[#{prid}](https://github.com/{org}/{repo}/pulls/{prid})\"\n\n\ndef commit_linkify(commitid: str, org: str, repo: str) -> str:\n    return f\"[`{commitid}`](https://github.com/{org}/{repo}/commit/{commitid})\"\n\n\ndef wrap_details(title, body, wraplines=5):\n    \"\"\"Wrap lines into a <details> element if body is longer than `wraplines`\"\"\"\n    out = f\"\\n\\n### {title}\"\n    wrap = body.strip().count(\"\\n\") > wraplines\n    if wrap:\n        out += \"\\n<details><summary>Click to expand</summary>\\n<p>\"\n    out += f\"\\n{body.rstrip()}\"\n    if wrap:\n        out += \"\\n\\n</p>\\n</details>\"\n    return out\n\n\ncontributor_emails = set()\n\n\ndef summary_repo(\n    org: str,\n    repo: str,\n    path: str,\n    commit_range: Tuple[str, str],\n    filter_types: List[str],\n    repo_order: List[str],\n) -> str:\n    if commit_range[1] == \"0000000\":\n        # Happens when a submodule has been removed\n        return \"\"\n    if commit_range[0] == \"0000000\":\n        # Happens when a submodule has been added\n        commit_range = (\"\", \"\")  # no range = all commits for new submodule\n\n    out = f\"\\n## 📦 {repo}\"\n\n    feats = \"\"\n    fixes = \"\"\n    misc = \"\"\n    hidden = 0\n\n    # pretty format is modified version of: https://stackoverflow.com/a/1441062/965332\n    summary_bundle = run(\n        f\"git log {'...'.join(commit_range) if any(commit_range) else ''} --no-decorate --pretty=format:'%h%x09%an%x09%ae%x09%s'\",\n        cwd=path,\n    )\n    print(f\"Found {len(summary_bundle.splitlines())} commits in {repo}\")\n    for line in summary_bundle.split(\"\\n\"):\n        if line:\n            _id, _author, email, msg = line.split(\"\\t\")\n            # will add author email to contributor list\n            # the `contributor_emails` is global and collected later\n            contributor_emails.add(email)\n            commit = Commit(id=_id, msg=msg, org=org, repo=repo)\n\n            entry = f\"\\n - {commit.format()}\"\n            if commit.type == \"feat\":\n                feats += entry\n            elif commit.type == \"fix\":\n                fixes += entry\n            elif commit.type not in filter_types:\n                misc += entry\n            else:\n                hidden += 1\n\n    for name, entries in (\n        (\"✨ Features\", feats),\n        (\"🐛 Fixes\", fixes),\n        (\"🔨 Misc\", misc),\n    ):\n        if entries:\n            _count = len(entries.strip().split(\"\\n\"))\n            title = f\"{name} ({_count})\"\n            if \"Misc\" in name or \"Fixes\" in name:\n                out += wrap_details(title, entries)\n            else:\n                out += f\"\\n\\n### {title}\\n\"\n                out += entries\n    if hidden > 1:\n        full_history_url = f\"https://github.com/{org}/{repo}/compare/{commit_range[0]}...{commit_range[1]}\"\n        out += f\"\\n\\n*(excluded {hidden} less relevant [commits]({full_history_url}))*\"\n\n    # NOTE: For now, these TODOs can be manually fixed for each changelog.\n    # TODO: Fix issue where subsubmodules can appear twice (like aw-webui)\n    # TODO: Use specific order (aw-webui should be one of the first, for example)\n    summary_subrepos = run(\n        f\"git submodule summary --cached {commit_range[0]}\", cwd=path\n    )\n    subrepos = {}\n    for header, *_ in [s.split(\"\\n\") for s in summary_subrepos.split(\"\\n\\n\")]:\n        if header.startswith(\"fatal: not a git repository\"):\n            # Happens when a submodule has been removed\n            continue\n        if header.strip():\n            if len(header.split(\" \")) < 4:\n                # Submodule may have been deleted\n                continue\n\n            _, name, crange, count = header.split(\" \")\n            commit_range = tuple(crange.split(\"...\", 1))  # type: ignore\n            count = count.strip().lstrip(\"(\").rstrip(\"):\")\n            logger.info(\n                f\"Found {name}, looking up range: {commit_range} ({count} commits)\"\n            )\n            name = name.strip(\".\").strip(\"/\")\n\n            subrepos[name] = summary_repo(\n                org,\n                name,\n                f\"{path}/{name}\",\n                commit_range,\n                filter_types=filter_types,\n                repo_order=repo_order,\n            )\n\n    # filter out subrepos with no commits (single line after stripping whitespace)\n    subrepos = {\n        name: output\n        for name, output in subrepos.items()\n        if len(output.strip().splitlines()) > 1\n    }\n\n    # pick subrepos in repo_order, and remove from dict\n    for name in repo_order:\n        if name in subrepos:\n            out += \"\\n\"\n            out += subrepos[name]\n            logger.info(f\"{name:12} length: \\t{len(subrepos[name])}\")\n            del subrepos[name]\n\n    # add remaining repos\n    for output in subrepos.values():\n        out += \"\\n\"\n        out += output\n\n    return out\n\n\n# FIXME: Doesn't work, messy af, just gonna have to remove the aw-webui section by hand\ndef remove_duplicates(s: List[str], minlen=10, only_sections=True) -> List[str]:\n    \"\"\"\n    Removes the longest sequence of repeated elements (they don't have to be adjacent), if sequence if longer than `minlen`.\n    Preserves order of elements.\n    \"\"\"\n    if len(s) < minlen:\n        return s\n    out = []\n    longest: List[str] = []\n    for i in range(len(s)):\n        if i == 0 or s[i] not in out:\n            # Not matching any previous line,\n            # so add longest and new line to output, and reset longest\n            if len(longest) < minlen:\n                out.extend(longest)\n            else:\n                duplicate = \"\\n\".join(longest)\n                print(f\"Removing duplicate '{duplicate[:80]}...'\")\n            out.append(s[i])\n            longest = []\n        else:\n            # Matches a previous line, so add to longest\n            # If longest is empty and only_sections is True, check that the line is a section start\n            if only_sections:\n                if not longest and s[i].startswith(\"#\"):\n                    longest.append(s[i])\n                else:\n                    out.append(s[i])\n            else:\n                longest.append(s[i])\n\n    return out\n\n\ndef build(\n    org: str,\n    repo: str,\n    project_name: str,\n    commit_range: Tuple[str, str],\n    output_path: str,\n    repo_order: List[str],\n    filter_types: Optional[List[str]] = None,\n    add_version_header: bool = False,\n):\n    # provides a commit summary for the repo and subrepos, recursively looking up subrepos\n    # NOTE: this must be done *before* `get_all_contributors` is called,\n    #       as the latter relies on summary_repo looking up all users and storing in a global.\n    if not filter_types:\n        filter_types = [\"build\", \"ci\", \"tests\", \"test\"]\n\n    logger.info(\"Generating commit summary\")\n    since, tag = commit_range\n    output_changelog = summary_repo(\n        org,\n        repo,\n        \".\",\n        commit_range=commit_range,\n        filter_types=filter_types,\n        repo_order=repo_order,\n    )\n\n    output_changelog = f\"\"\"\n# Changelog\n\nChanges since {since}:\n\n{output_changelog}\n    \"\"\".strip()\n\n    # Would ideally sort by number of commits or something, but that's tricky\n    usernames = sorted(get_all_contributors(), key=str.casefold)\n    usernames = [u for u in usernames if not u.endswith(\"[bot]\")]\n    twitter_handles = get_twitter_of_ghusers(usernames)\n    print(\n        \"Twitter handles: \"\n        + \", \".join(\"@\" + handle for handle in twitter_handles.values() if handle),\n    )\n\n    output_contributors = f\"\"\"# Contributors\n\nThanks to everyone who contributed to this release:\n\n{', '.join(('@' + username for username in usernames))}\"\"\"\n\n    # Header starts here\n    logger.info(\"Building final output\")\n    output = f\"These are the release notes for {project_name} version {tag}.\".strip()\n    output += \"\\n\\n\"\n\n    # hardcoded for now\n    if repo == \"activitywatch\":\n        output += \"**New to ActivityWatch?** Check out the [website](https://activitywatch.net) and the [README](https://github.com/ActivityWatch/activitywatch/blob/master/README.md).\"\n        output += \"\\n\\n\"\n        output += \"\"\"# Installation\n\nSee the [getting started guide in the documentation](https://docs.activitywatch.net/en/latest/getting-started.html).\n        \"\"\".strip()\n        output += \"\\n\\n\"\n        output += f\"\"\"# Downloads\n\n - [**Windows**](https://github.com/ActivityWatch/activitywatch/releases/download/{tag}/activitywatch-{tag}-windows-x86_64-setup.exe) (.exe, installer)\n - [**macOS**](https://github.com/ActivityWatch/activitywatch/releases/download/{tag}/activitywatch-{tag}-macos-x86_64.dmg) (.dmg)\n - [**Linux**](https://github.com/ActivityWatch/activitywatch/releases/download/{tag}/activitywatch-{tag}-linux-x86_64.zip) (.zip)\n     \"\"\".strip()\n        output += \"\\n\\n\"\n\n    output += output_contributors.strip() + \"\\n\\n\"\n    output += output_changelog.strip() + \"\\n\\n\"\n    output += (\n        f\"**Full Changelog**: https://github.com/{org}/{repo}/compare/{since}...{tag}\"\n    )\n\n    if repo == \"activitywatch\":\n        output = output.replace(\"# activitywatch\", \"# activitywatch (bundle repo)\")\n\n    if add_version_header:\n        output = f\"# {tag}\\n\\n\" + output\n        output = output.replace(\"\\n# Contributors\\n\", \"\\n## Contributors\\n\")\n        output = output.replace(\"\\n# Changelog\\n\", \"\\n## Changelog\\n\")\n\n    with open(output_path, \"w\") as f:\n        f.write(output)\n    print(f\"Wrote {len(output.splitlines())} lines to {output_path}\")\n\n\ndef _resolve_email(email: str) -> Optional[str]:\n    if \"users.noreply.github.com\" in email:\n        username = email.split(\"@\")[0]\n        if \"+\" in username:\n            username = username.split(\"+\")[1]\n        # TODO: Verify username is valid using the GitHub API\n        print(f\"Contributor: @{username}\")\n        return username\n    else:\n        resp = None\n        backoff = 0\n        max_backoff = 2\n        while resp is None:\n            if backoff >= max_backoff:\n                logger.warning(f\"Backed off {max_backoff} times, giving up\")\n                break\n            try:\n                logger.info(f\"Sending request for {email}\")\n                _resp = requests.get(\n                    f\"https://api.github.com/search/users?q={email}+in%3Aemail\"\n                )\n                _resp.raise_for_status()\n                resp = _resp\n                backoff = 0\n            # if rate limit exceeded, back off\n            except requests.exceptions.RequestException as e:\n                if isinstance(e, requests.exceptions.HTTPError):\n                    if e.response.status_code == 403:\n                        logger.warning(\"Rate limit exceeded, backing off...\")\n                        backoff += 1\n                        sleep(3)\n                        continue\n                else:\n                    raise e\n            finally:\n                # Just to respect API limits...\n                sleep(1)\n\n        if resp:\n            data = resp.json()\n            if data[\"total_count\"] == 0:\n                logger.info(f\"No match for email: {email}\")\n            if data[\"total_count\"] > 1:\n                logger.warning(f\"Multiple matches for email: {email}\")\n            if data[\"total_count\"] >= 1:\n                username = data[\"items\"][0][\"login\"]\n                logger.info(f\"Contributor: @{username}  (by email: {email})\")\n                return username\n    return None\n\n\ndef get_all_contributors() -> set[str]:\n    # TODO: Merge with contributor-stats?\n    logger.info(\"Getting all contributors\")\n\n    # We will commit this file, to act as a cache (preventing us from querying GitHub API every time)\n    filename = script_dir / \"changelog_contributors.csv\"\n\n    # mapping from username to one or more emails\n    usernames: Dict[str, set] = defaultdict(set)\n\n    # some hardcoded ones, some that don't resolve...\n    usernames[\"erikbjare\"] |= {\"erik.bjareholt@gmail.com\", \"erik@bjareho.lt\"}\n    usernames[\"iloveitaly\"] |= {\"iloveitaly@gmail.com\"}\n    usernames[\"kewde\"] |= {\"kewde@particl.io\"}\n    usernames[\"victorwinberg\"] |= {\"victor.m.winberg@gmail.com\"}\n    usernames[\"NicoWeio\"] |= {\"nico.weio@gmail.com\"}\n    usernames[\"2e3s\"] |= {\"2e3s19@gmail.com\"}\n    usernames[\"alwinator\"] |= {\"accounts@alwinschuster.at\"}\n\n    # read existing contributors, to avoid extra calls to the GitHub API\n    if os.path.exists(filename):\n        with open(filename, \"r\") as f:\n            s = f.read()\n        for line in s.split(\"\\n\"):\n            if not line:\n                continue\n            username, *emails = line.split(\"\\t\")\n            for email in emails:\n                usernames[username].add(email)\n        logger.info(f\"Read {len(usernames)} contributors from {filename}\")\n\n    resolved_emails = set(\n        email for email_set in usernames.values() for email in email_set\n    )\n    unresolved_emails = contributor_emails - resolved_emails\n    for email in unresolved_emails:\n        username_opt = _resolve_email(email)\n        if username_opt:\n            usernames[username_opt].add(email)\n\n    with open(filename, \"w\") as f:\n        for username, email_set in sorted(usernames.items()):\n            emails_str = \"\\t\".join(sorted(email_set))\n            f.write(f\"{username}\\t{emails_str}\")\n            f.write(\"\\n\")\n\n    logger.info(f\"Wrote {len(usernames)} contributors to {filename}\")\n\n    email_to_username = {\n        email: username for username, emails in usernames.items() for email in emails\n    }\n\n    return set(\n        email_to_username[email]\n        for email in contributor_emails\n        if email in email_to_username\n    )\n\n\ndef get_twitter_of_ghusers(ghusers: Collection[str]):\n    logger.info(\"Getting twitter of GitHub usernames\")\n\n    # We will commit this file, to act as a cache (preventing us from querying GitHub API every time)\n    filename = script_dir / \"changelog_contributors_twitter.csv\"\n\n    twitter = {}\n\n    # read existing contributors, to avoid extra calls to the GitHub API\n    if os.path.exists(filename):\n        with open(filename, \"r\") as f:\n            s = f.read()\n        for line in s.split(\"\\n\"):\n            if not line:\n                continue\n            gh_username, twitter_username = line.split(\"\\t\")\n            twitter[gh_username] = twitter_username\n        logger.info(f\"Read {len(twitter)} Twitter handles from {filename}\")\n\n    for username in ghusers:\n        if username in twitter:\n            continue\n        try:\n            resp = requests.get(f\"https://api.github.com/users/{username}\")\n            resp.raise_for_status()\n            data = resp.json()\n        except Exception as e:\n            logger.warning(f\"Failed to get twitter of {username}: {e}\")\n            continue\n\n        twitter_username = data[\"twitter_username\"]\n        if twitter_username:\n            twitter[username] = twitter_username\n\n    with open(filename, \"w\") as f:\n        for username, twitter_username in sorted(twitter.items()):\n            f.write(f\"{username}\\t{twitter_username}\")\n            f.write(\"\\n\")\n\n    return twitter\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/changelog_contributors.csv",
    "content": "2e3s\t2e3s19@gmail.com\n750\t37119951+750@users.noreply.github.com\nAlwinator\t39517491+Alwinator@users.noreply.github.com\nBasileusErwin\t67933444+BasileusErwin@users.noreply.github.com\nBelKed\t66956532+BelKed@users.noreply.github.com\nCrazyPython\tJamtlu@gmail.com\nDrarig29\tcorentingirard.dev@gmail.com\nElijah-Bodden\t106613755+Elijah-Bodden@users.noreply.github.com\nFurffico\t43836984+Furffico@users.noreply.github.com\nGabLeRoux\tlebreton.gabriel@gmail.com\nGame4Move78\t83040764+Game4Move78@users.noreply.github.com\nJulianoe\tJulianoe@users.noreply.github.com\nLockBlock-dev\t68129141+LockBlock-dev@users.noreply.github.com\nLunarWatcher\tzoe.i2k1@gmail.com\nNicoWeio\tkontakt@nicolaiweitkemper.de\tnico.weio@gmail.com\nOrganoidus\t150709464+Organoidus@users.noreply.github.com\nShi-Soul\t86898048+Shi-Soul@users.noreply.github.com\nShootingKing-AM\tnarnindi.raghu@gmail.com\nShubham0324\t53115519+Shubham0324@users.noreply.github.com\nStefanoChiodino\tStefanoChiodino@users.noreply.github.com\nTSRBerry\t20988865+TSRBerry@users.noreply.github.com\nValentin-N\t1926716+Valentin-N@users.noreply.github.com\nY7n05h\tY7n05h@protonmail.com\naaayushsingh\tayush-_-singh@live.com\nalclary\t9044153+alclary@users.noreply.github.com\nalialamine\tali@towbe.com\nalwinator\taccounts@alwinschuster.at\n0xbrayo\tvukubrian@gmail.com\nchaoky\tlevimanga@gmail.com\nchengyuhui\tchengyuhui1@gmail.com\ndavidfraser\tdavidfraser@users.noreply.github.com\ndependabot-preview[bot]\t27856297+dependabot-preview[bot]@users.noreply.github.com\ndependabot[bot]\t49699333+dependabot[bot]@users.noreply.github.com\nerikbjare\terik.bjareholt@gmail.com\terik@bjareho.lt\nhooger\thooger@users.noreply.github.com\niloveitaly\tiloveitaly@gmail.com\ninfokiller\tinfokiller@users.noreply.github.com\nishitatsuyuki\tishitatsuyuki@gmail.com\njkbh\t33606327+jkbh@users.noreply.github.com\njohan-bjareholt\tjohan@bjareho.lt\njtojnar\tjtojnar@gmail.com\nkewde\tkewde@particl.io\nlgtm-com[bot]\t43144390+lgtm-com[bot]@users.noreply.github.com\nliutiming\t39947942+liutiming@users.noreply.github.com\nluzpaz\tluzpaz@users.noreply.github.com\nmaciekstosio\tmaciekstosio@users.noreply.github.com\nmichaeljelly\t53475252+michaeljelly@users.noreply.github.com\nmodderme123\tmodderme123@users.noreply.github.com\nnathanmerrill\tnathanmerrill@users.noreply.github.com\nnoisersup\tpatryk@kwiatek.xyz\nochen1\to.chen1@share.epsb.ca\nomahs\t73983677+omahs@users.noreply.github.com\noscar-king\toscar-king@users.noreply.github.com\npktiuk\tkotiuk@zohomail.eu\npkvach\tpavel.kvach@gmail.com\nrakleed\t19418601+rakleed@users.noreply.github.com\nrepo-visualizer\trepo-visualizer@users.noreply.github.com\nsalahineo\tsalahineo.personal@gmail.com\nskaparis\t43264989+skaparis@users.noreply.github.com\nskewballfox\tjoshua.ferguson.273@gmail.com\nsoxofaan\tsoxofaan@users.noreply.github.com\nsunrosa\t79175772+sunrosa@users.noreply.github.com\nvedantmgoyal2009\t83997633+vedantmgoyal2009@users.noreply.github.com\nvictorlin\t13424970+victorlin@users.noreply.github.com\nvictorwinberg\tvictor.m.winberg@gmail.com\nvieteh\tviet.tran@employmenthero.com\nxylix\tkerk.pelt@gmail.com\nyuhldr\tyuhldr@qq.com\nyumemio\t59369226+yumemio@users.noreply.github.com\n"
  },
  {
    "path": "scripts/changelog_contributors_twitter.csv",
    "content": "0xbrayo\tsubrupt\nchaoky\tchaokyer\nerikbjare\terikbjare\niloveitaly\tmike_bianco\nvedantmgoyal2009\tvedantmgoyal\nvictorlin\tvictorlin_\n"
  },
  {
    "path": "scripts/checkout-latest-tag.sh",
    "content": "#!/bin/bash\n\nlatest_version_tag=$(git tag -l | grep \"^v[0-9]\\..*\" | sort --version-sort | tail -n1 )\ncurrent_version_tag=$(git describe --tags)\necho \"Latest version: $latest_version_tag\"\necho \"Current version: $current_version_tag\"\n"
  },
  {
    "path": "scripts/chores/make-release.sh",
    "content": "#!/bin/bash\n\n#\n# We should create a release checklist to ensure releases are consistent.\n#\n\n# Create an annotated tag\n#git tag -a $\n"
  },
  {
    "path": "scripts/ci/enable_long_paths.bat",
    "content": ":: Enable long paths on Windows (needed when building since node_modules can create deep hierarchies)\n\nREG ADD \"HKLM\\SYSTEM\\CurrentControlSet\\Control\\FileSystem\" /v LongPathsEnabled /t REG_DWORD /d 1 /f\n"
  },
  {
    "path": "scripts/ci/import-macos-p12.sh",
    "content": "#!/bin/sh\n\nset -e\n\n# Source: https://www.update.rocks/blog/osx-signing-with-travis/\nexport KEY_CHAIN=build.keychain\nexport CERTIFICATE_P12=aw_certificate.p12\n\n# Recreate the certificate from the secure environment variable\necho $CERTIFICATE_MACOS_P12_BASE64 | base64 --decode > $CERTIFICATE_P12\n\n#create a keychain\nsecurity -v create-keychain -p travis $KEY_CHAIN\n# Make the keychain the default so identities are found\nsecurity -v default-keychain -s $KEY_CHAIN\n# Unlock the keychain\nsecurity -v unlock-keychain -p travis $KEY_CHAIN\n\nsecurity -v import $CERTIFICATE_P12 -k $KEY_CHAIN -P $CERTIFICATE_MACOS_P12_PASSWORD -A\nsecurity -v set-key-partition-list -S apple-tool:,apple: -s -k travis $KEY_CHAIN\n\n# remove certs\nrm -rf *.p12\n"
  },
  {
    "path": "scripts/ci/install_node.ps1",
    "content": "$msipath = \"$PSScriptRoot\\node-installer.msi\"\n\nfunction RunCommand ($command, $command_args) {\n    Write-Host $command $command_args\n    Start-Process -FilePath $command -ArgumentList $command_args -Wait -Passthru\n}\n\nfunction InstallNode () {\n    DownloadNodeMSI\n    InstallNodeMSI\n}\n\nfunction DownloadNodeMSI () {\n    $url = \"https://nodejs.org/dist/v12.18.4/node-v12.18.4-x64.msi\"\n    $start_time = Get-Date\n\n    Write-Output \"Downloading node msi\"\n    Invoke-WebRequest -Uri $url -OutFile $msipath\n    Write-Output \"Time taken: $((Get-Date).Subtract($start_time).Seconds) second(s)\"\n}\n\nfunction InstallNodeMSI () {\n    $install_args = \"/qn /log node_install.log /i $msipath\"\n    $uninstall_args = \"/qn /x $msipath\"\n    RunCommand \"msiexec.exe\" $install_args\n\n    #if (-not(Test-Path $python_home)) {\n    #    Write-Host \"Python seems to be installed else-where, reinstalling.\"\n    #    RunCommand \"msiexec.exe\" $uninstall_args\n    #    RunCommand \"msiexec.exe\" $install_args\n    #}\n}\n\n\nfunction main () {\n    InstallNode\n    rm $msipath\n}\n\nmain\n"
  },
  {
    "path": "scripts/ci/install_pyhook.ps1",
    "content": "function main ($arch) {\n    If ( $arch -eq \"64\" ) {\n        $url=\"https://github.com/ActivityWatch/wheels/raw/master/pyHook-1.5.1-cp36-cp36m-win_amd64.whl\"\n    } ElseIf ( $arch -eq \"32\" ) {\n        $url=\"https://github.com/ActivityWatch/wheels/raw/master/pyHook-1.5.1-cp36-cp36m-win32.whl\"\n    } Else {\n        Write-Output \"Invalid architecture\"\n        return -1\n    }\n    pip install --user $url\n}\n\nmain $env:PYTHON_ARCH\n"
  },
  {
    "path": "scripts/ci/install_python.ps1",
    "content": "# Sample script to install Python and pip under Windows\n# Authors: Olivier Grisel, Jonathan Helmus, Kyle Kastner, and Alex Willmer\n# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/\n#\n# Find the latest version of this script at:\n# https://github.com/ogrisel/python-appveyor-demo/blob/master/appveyor/install.ps1\n\n\n$MINICONDA_URL = \"http://repo.continuum.io/miniconda/\"\n$BASE_URL = \"https://www.python.org/ftp/python/\"\n$GET_PIP_URL = \"https://bootstrap.pypa.io/get-pip.py\"\n$GET_PIP_PATH = \"C:\\get-pip.py\"\n\n$PYTHON_PRERELEASE_REGEX = @\"\n(?x)\n(?<major>\\d+)\n\\.\n(?<minor>\\d+)\n\\.\n(?<micro>\\d+)\n(?<prerelease>[a-z]{1,2}\\d+)\n\"@\n\n\nfunction Download ($filename, $url) {\n    $webclient = New-Object System.Net.WebClient\n\n    $basedir = $pwd.Path + \"\\\"\n    $filepath = $basedir + $filename\n    if (Test-Path $filename) {\n        Write-Host \"Reusing\" $filepath\n        return $filepath\n    }\n\n    # Download and retry up to 3 times in case of network transient errors.\n    Write-Host \"Downloading\" $filename \"from\" $url\n    $retry_attempts = 2\n    for ($i = 0; $i -lt $retry_attempts; $i++) {\n        try {\n            $webclient.DownloadFile($url, $filepath)\n            break\n        }\n        Catch [Exception]{\n            Start-Sleep 1\n        }\n    }\n    if (Test-Path $filepath) {\n        Write-Host \"File saved at\" $filepath\n    } else {\n        # Retry once to get the error message if any at the last try\n        $webclient.DownloadFile($url, $filepath)\n    }\n    return $filepath\n}\n\n\nfunction ParsePythonVersion ($python_version) {\n    if ($python_version -match $PYTHON_PRERELEASE_REGEX) {\n        return ([int]$matches.major, [int]$matches.minor, [int]$matches.micro,\n                $matches.prerelease)\n    }\n    $version_obj = [version]$python_version\n    return ($version_obj.major, $version_obj.minor, $version_obj.build, \"\")\n}\n\n\nfunction DownloadPython ($python_version, $platform_suffix) {\n    $major, $minor, $micro, $prerelease = ParsePythonVersion $python_version\n\n    if (($major -le 2 -and $micro -eq 0) `\n        -or ($major -eq 3 -and $minor -le 2 -and $micro -eq 0) `\n        ) {\n        $dir = \"$major.$minor\"\n        $python_version = \"$major.$minor$prerelease\"\n    } else {\n        $dir = \"$major.$minor.$micro\"\n    }\n\n    if ($prerelease) {\n        if (($major -le 2) `\n            -or ($major -eq 3 -and $minor -eq 1) `\n            -or ($major -eq 3 -and $minor -eq 2) `\n            -or ($major -eq 3 -and $minor -eq 3) `\n            ) {\n            $dir = \"$dir/prev\"\n        }\n    }\n\n    if (($major -le 2) -or ($major -le 3 -and $minor -le 4)) {\n        $ext = \"msi\"\n        if ($platform_suffix) {\n            $platform_suffix = \".$platform_suffix\"\n        }\n    } else {\n        $ext = \"exe\"\n        if ($platform_suffix) {\n            $platform_suffix = \"-$platform_suffix\"\n        }\n    }\n\n    $filename = \"python-$python_version$platform_suffix.$ext\"\n    $url = \"$BASE_URL$dir/$filename\"\n    $filepath = Download $filename $url\n    return $filepath\n}\n\n\nfunction InstallPython ($python_version, $architecture, $python_home) {\n    Write-Host \"Installing Python\" $python_version \"for\" $architecture \"bit architecture to\" $python_home\n    if (Test-Path $python_home) {\n        Write-Host $python_home \"already exists, skipping.\"\n        return $false\n    }\n    if ($architecture -eq \"32\") {\n        $platform_suffix = \"\"\n    } else {\n        $platform_suffix = \"amd64\"\n    }\n    $installer_path = DownloadPython $python_version $platform_suffix\n    $installer_ext = [System.IO.Path]::GetExtension($installer_path)\n    Write-Host \"Installing $installer_path to $python_home\"\n    $install_log = $python_home + \".log\"\n    if ($installer_ext -eq '.msi') {\n        InstallPythonMSI $installer_path $python_home $install_log\n    } else {\n        InstallPythonEXE $installer_path $python_home $install_log\n    }\n    if (Test-Path $python_home) {\n        Write-Host \"Python $python_version ($architecture) installation complete\"\n    } else {\n        Write-Host \"Failed to install Python in $python_home\"\n        Get-Content -Path $install_log\n        Exit 1\n    }\n}\n\n\nfunction InstallPythonEXE ($exepath, $python_home, $install_log) {\n    $install_args = \"/quiet InstallAllUsers=1 TargetDir=$python_home\"\n    RunCommand $exepath $install_args\n}\n\n\nfunction InstallPythonMSI ($msipath, $python_home, $install_log) {\n    $install_args = \"/qn /log $install_log /i $msipath TARGETDIR=$python_home\"\n    $uninstall_args = \"/qn /x $msipath\"\n    RunCommand \"msiexec.exe\" $install_args\n    if (-not(Test-Path $python_home)) {\n        Write-Host \"Python seems to be installed else-where, reinstalling.\"\n        RunCommand \"msiexec.exe\" $uninstall_args\n        RunCommand \"msiexec.exe\" $install_args\n    }\n}\n\nfunction RunCommand ($command, $command_args) {\n    Write-Host $command $command_args\n    Start-Process -FilePath $command -ArgumentList $command_args -Wait -Passthru\n}\n\n\nfunction InstallPip ($python_home) {\n    $pip_path = $python_home + \"\\Scripts\\pip.exe\"\n    $python_path = $python_home + \"\\python.exe\"\n    if (-not(Test-Path $pip_path)) {\n        Write-Host \"Installing pip...\"\n        $webclient = New-Object System.Net.WebClient\n        $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH)\n        Write-Host \"Executing:\" $python_path $GET_PIP_PATH\n        & $python_path $GET_PIP_PATH\n    } else {\n        Write-Host \"pip already installed.\"\n    }\n}\n\n\nfunction DownloadMiniconda ($python_version, $platform_suffix) {\n    if ($python_version -eq \"3.4\") {\n        $filename = \"Miniconda3-3.5.5-Windows-\" + $platform_suffix + \".exe\"\n    } else {\n        $filename = \"Miniconda-3.5.5-Windows-\" + $platform_suffix + \".exe\"\n    }\n    $url = $MINICONDA_URL + $filename\n    $filepath = Download $filename $url\n    return $filepath\n}\n\n\nfunction InstallMiniconda ($python_version, $architecture, $python_home) {\n    Write-Host \"Installing Python\" $python_version \"for\" $architecture \"bit architecture to\" $python_home\n    if (Test-Path $python_home) {\n        Write-Host $python_home \"already exists, skipping.\"\n        return $false\n    }\n    if ($architecture -eq \"32\") {\n        $platform_suffix = \"x86\"\n    } else {\n        $platform_suffix = \"x86_64\"\n    }\n    $filepath = DownloadMiniconda $python_version $platform_suffix\n    Write-Host \"Installing\" $filepath \"to\" $python_home\n    $install_log = $python_home + \".log\"\n    $args = \"/S /D=$python_home\"\n    Write-Host $filepath $args\n    Start-Process -FilePath $filepath -ArgumentList $args -Wait -Passthru\n    if (Test-Path $python_home) {\n        Write-Host \"Python $python_version ($architecture) installation complete\"\n    } else {\n        Write-Host \"Failed to install Python in $python_home\"\n        Get-Content -Path $install_log\n        Exit 1\n    }\n}\n\n\nfunction InstallMinicondaPip ($python_home) {\n    $pip_path = $python_home + \"\\Scripts\\pip.exe\"\n    $conda_path = $python_home + \"\\Scripts\\conda.exe\"\n    if (-not(Test-Path $pip_path)) {\n        Write-Host \"Installing pip...\"\n        $args = \"install --yes pip\"\n        Write-Host $conda_path $args\n        Start-Process -FilePath \"$conda_path\" -ArgumentList $args -Wait -Passthru\n    } else {\n        Write-Host \"pip already installed.\"\n    }\n}\n\nfunction main () {\n    InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON\n    InstallPip $env:PYTHON\n}\n\nmain\n"
  },
  {
    "path": "scripts/ci/run_with_env.cmd",
    "content": ":: To build extensions for 64 bit Python 3, we need to configure environment\n:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of:\n:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1)\n::\n:: To build extensions for 64 bit Python 2, we need to configure environment\n:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of:\n:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0)\n::\n:: 32 bit builds, and 64-bit builds for 3.5 and beyond, do not require specific\n:: environment configurations.\n::\n:: Note: this script needs to be run with the /E:ON and /V:ON flags for the\n:: cmd interpreter, at least for (SDK v7.0)\n::\n:: More details at:\n:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows\n:: http://stackoverflow.com/a/13751649/163740\n::\n:: Author: Olivier Grisel\n:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/\n::\n:: Notes about batch files for Python people:\n::\n:: Quotes in values are literally part of the values:\n::      SET FOO=\"bar\"\n:: FOO is now five characters long: \" b a r \"\n:: If you don't want quotes, don't include them on the right-hand side.\n::\n:: The CALL lines at the end of this file look redundant, but if you move them\n:: outside of the IF clauses, they do not run properly in the SET_SDK_64==Y\n:: case, I don't know why.\n@ECHO OFF\nSET COMMAND_TO_RUN=%*\nSET WIN_SDK_ROOT=C:\\Program Files\\Microsoft SDKs\\Windows\nSET WIN_WDK=c:\\Program Files (x86)\\Windows Kits\\10\\Include\\wdf\n\n:: Extract the major and minor versions, and allow for the minor version to be\n:: more than 9.  This requires the version number to have two dots in it.\nSET MAJOR_PYTHON_VERSION=%PYTHON_VERSION:~0,1%\nIF \"%PYTHON_VERSION:~3,1%\" == \".\" (\n    SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,1%\n) ELSE (\n    SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,2%\n)\n\n:: Based on the Python version, determine what SDK version to use, and whether\n:: to set the SDK for 64-bit.\nIF %MAJOR_PYTHON_VERSION% == 2 (\n    SET WINDOWS_SDK_VERSION=\"v7.0\"\n    SET SET_SDK_64=Y\n) ELSE (\n    IF %MAJOR_PYTHON_VERSION% == 3 (\n        SET WINDOWS_SDK_VERSION=\"v7.1\"\n        IF %MINOR_PYTHON_VERSION% LEQ 4 (\n            SET SET_SDK_64=Y\n        ) ELSE (\n            SET SET_SDK_64=N\n            IF EXIST \"%WIN_WDK%\" (\n                :: See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/\n                REN \"%WIN_WDK%\" 0wdf\n            )\n        )\n    ) ELSE (\n        ECHO Unsupported Python version: \"%MAJOR_PYTHON_VERSION%\"\n        EXIT 1\n    )\n)\n\nIF %PYTHON_ARCH% == 64 (\n    IF %SET_SDK_64% == Y (\n        ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture\n        SET DISTUTILS_USE_SDK=1\n        SET MSSdk=1\n        \"%WIN_SDK_ROOT%\\%WINDOWS_SDK_VERSION%\\Setup\\WindowsSdkVer.exe\" -q -version:%WINDOWS_SDK_VERSION%\n        \"%WIN_SDK_ROOT%\\%WINDOWS_SDK_VERSION%\\Bin\\SetEnv.cmd\" /x64 /release\n        ECHO Executing: %COMMAND_TO_RUN%\n        call %COMMAND_TO_RUN% || EXIT 1\n    ) ELSE (\n        ECHO Using default MSVC build environment for 64 bit architecture\n        ECHO Executing: %COMMAND_TO_RUN%\n        call %COMMAND_TO_RUN% || EXIT 1\n    )\n) ELSE (\n    ECHO Using default MSVC build environment for 32 bit architecture\n    ECHO Executing: %COMMAND_TO_RUN%\n    call %COMMAND_TO_RUN% || EXIT 1\n)\n"
  },
  {
    "path": "scripts/count_lines.sh",
    "content": "#!/usr/bin/env bash\n\nre_ignore='.*(build|dist|venv|old|other|scripts|node|static).*'\n\necho -n \"Lines of code (excluding test): \"\nfiles=$(find | egrep '\\.(py|js|ts|rs|vue)$' | egrep -v $re_ignore | grep -v 'test')\necho $files | xargs cat | wc -l\n\n#echo \"Files:\"\n#for file in $files; do\n#    echo \" - $file\"\n#done\n\necho -n \" - of which Python code: \"\nfiles=$(find | egrep '\\.(py)$' | egrep -v $re_ignore | grep -v 'test')\necho $files | xargs cat | wc -l\n\necho -n \" - of which Rust code: \"\nfiles=$(find | egrep '\\.(rs)$' | egrep -v  $re_ignore | grep -v 'test')\necho $files | xargs cat | wc -l\n\necho -n \" - of which JS/TS code: \"\nfiles=$(find | egrep '\\.(js|ts)$' | egrep -v $re_ignore | grep -v 'test')\necho $files | xargs cat | wc -l\n\necho -n \" - of which Vue code: \"\nfiles=$(find | egrep '\\.(vue)$' | egrep -v  $re_ignore | grep -v 'test')\necho $files | xargs cat | wc -l\n\necho -ne \"\\nLines of test: \"\nfiles=$(find | egrep '\\.(py|js|vue)$' | egrep -v $re_ignore | grep 'test')\necho $files | xargs cat | wc -l\n"
  },
  {
    "path": "scripts/get_latest_release.sh",
    "content": "#!/bin/bash\n\n# TODO: Merge with scripts/package/getversion.sh\n\n# Script that fetches the previous release (if current commit is a tag),\n# or the latest release, if current commit is not a tag.\n\n# If stable only, then we return the latest stable release, \n# else, we will return the latest release, either stable or prerelease.\nRE_STABLE='(?<=[/])v[0-9\\.]+$'\nRE_INCL_PRERELEASE='(?<=[/])v[0-9\\.]+(a|b|rc)?[0-9]+$'\n\n# Get tag for this commit, if any\nTAG=$(git describe --tags --exact-match 2>/dev/null)\n\nRE=$RE_INCL_PRERELEASE\nif [ -n \"$STABLE_ONLY\" ]; then\n    if [ \"$STABLE_ONLY\" = \"true\" ]; then\n        RE=$RE_STABLE\n    fi\nfi\nALL_TAGS=`git for-each-ref --sort=creatordate --format '%(refname)' refs/tags`\n\n# If current commit is a tag, we filter it out\nif [ -n \"$TAG\" ]; then\n    ALL_TAGS=`echo \"$ALL_TAGS\" | grep -v \"^refs/tags/$TAG$\"`\nfi\n\necho \"$ALL_TAGS\" | grep -P \"$RE\" --only-matching | tail -n1\n"
  },
  {
    "path": "scripts/logcrawler.py",
    "content": "import os\nimport re\nfrom datetime import datetime\nfrom collections import defaultdict\nimport logging\n\nimport aw_core\n\nlogging.basicConfig()\n\nlog_dir = aw_core.dirs.get_log_dir(\"\")\n\n\ndef get_filepaths():\n    filepaths = []\n    for folder, dirs, files in os.walk(log_dir):\n        print(\"Crawling folder: \" + folder)\n        filepaths.extend([os.path.join(folder, filename) for filename in files])\n    return filepaths\n\n\ndef collect():\n    matched_lines = defaultdict(lambda: [])\n    for filepath in sorted(get_filepaths()):\n        with open(filepath, \"r\") as f:\n            log = f.read()\n            for line in log.split(\"\\n\"):\n                s = re.search(\"(ERR|WARN)\", line)\n                ignored = re.search(\"(CORS|Deleted bucket)\", line)\n                if s and not ignored:\n                    matched_lines[filepath].append(line)\n    return matched_lines\n\n\n_date_reg_exp = re.compile('\\d{4}-\\d{2}-\\d{2}')\n\n\ntoday = datetime.now()\n\n\ndef line_age(line):\n    \"\"\"Returns line age in days\"\"\"\n    match = _date_reg_exp.search(line)\n    if not match:\n        logging.warning(\"Line had no date, avoid multiple line messages in logs. Line will have its age set to zero.\")\n        return 0\n    else:\n        dt = datetime.strptime(match.group(), '%Y-%m-%d')\n        td = today - dt\n        return td.days\n\n\ndef main(exclude_testing: bool = False, limit_days: int = 10, limit_lines: int = 10):\n    file_lines = collect()\n\n    if exclude_testing:\n        keys = filter(lambda k: \"testing\" not in k, file_lines.keys())\n        file_lines = {key: file_lines[key] for key in keys}\n\n    for filename, lines in sorted(file_lines.items()):\n        lines = sorted(file_lines[filename], reverse=True)\n\n        # Filter lines older than x days\n        if limit_days:\n            lines = [line for line in lines if line_age(line) <= limit_days]\n\n        if lines:\n            print(\"-\" * 50)\n            print(\"File: {}\".format(filename))\n\n            # Print lines up to the limit\n            for line in lines[:limit_lines]:\n                print(\"  \" + line)\n\n            if limit_lines < len(lines):\n                print(\"Showing {} out of {} lines\".format(limit_lines, len(lines)))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/nop.sh",
    "content": "#!/bin/bash\n\necho \"nop.bat was executed as a workaround for something\"\n"
  },
  {
    "path": "scripts/notarize.sh",
    "content": "#!/bin/bash\n\napplemail=$APPLE_EMAIL # Email address used for Apple ID\npassword=$APPLE_PASSWORD # See apps-specific password https://support.apple.com/en-us/HT204397\nteamid=$APPLE_TEAMID # Team idenitifer (if single developer, then set to developer identifier)\nkeychain_profile=\"activitywatch-$APPLE_PERSONALID\"  # name of the keychain profile to use\nbundleid=net.activitywatch.ActivityWatch # Match aw.spec\napp=dist/ActivityWatch.app\ndmg=dist/ActivityWatch.dmg\n\n# XCode >= 13 \nrun_notarytool() {\n    dist=$1\n    # Setup the credentials for notarization\n    xcrun notarytool store-credentials $keychain_profile --apple-id $applemail --team-id $teamid --password $password\n    # Notarize and wait\n    echo \"Notarization: starting for $dist\"\n    echo \"Notarization: in progress for $dist\"\n    xcrun notarytool submit $dist --keychain-profile $keychain_profile --wait\n}\n\n# XCode < 13 \nrun_altool() {\n    dist=$1\n    # Setup the credentials for notarization\n    xcrun altool --store-password-in-keychain-item $keychain_profile -u $applemail -p $password\n    # Notarize and wait\n    echo \"Notarization: starting for $dist\"\n    upload=$(xcrun altool --notarize-app -t osx -f $dist --primary-bundle-id $bundleid -u $applemail --password \"@keychain:$keychain_profile\")\n    uuid = $(/usr/libexec/PlistBuddy -c \"Print :notarization-upload:RequestUUID\" $upload)\n    while true; do \n        req=$(xcrun altool --notarization-info $uuid -u $applemail -p $password --output-format xml)\n        status=$(/usr/libexec/PlistBuddy -c \"Print :notarization-info:Status\" $req)\n        if [ $status != \"in progress\" ]; then \n            break\n        else\n            echo \"Notarization: in progress for $dist\"\n        fi\n        sleep 10\n    done\n}\n\n# Staples the notarization certificate to the executable/bunldle\nrun_stapler() {\n    dist=$1\n    xcrun stapler staple $dist\n}\n\necho 'Detecting availability of notarization tools'\nnotarization_method=exit\n# Detect if notarytool is available\nxcrun notarytool >/dev/null 2>&1\nif [ $? -eq 0 ]; then\n    echo \"+ Found notarytool\"\n    notarization_method=run_notarytool\nfi\n# Fallbqck to altool\noutput=xcrun altool >/dev/null 2>&1\nif [ $? -eq 0 ]; then\n    echo \"+ Found altool\"\n    notarization_method=run_altool\nfi\n\nif [ $notarization_method = \"exit\" ]; then\n    echo \"- Found no tools, exiting\"\n    $notarization_method\nfi\n\nif test -f \"$app\"; then\n    echo \"Notarizing: $app\"\n    zip=$app.zip\n    # Turn the app into a zip file that notarization will accept\n    ditto -c -k --keepParent $app $zip\n    $notarization_method $zip\n    run_stapler $app\nelse\n    echo \"Skipping: $app\"\nfi\n\nif test -f \"$dmg\"; then\n    echo \"Notarizing: $dmg\"\n    $notarization_method $dmg\n    run_stapler $dmg\nelse\n    echo \"Skipping: $dmg\"\nfi\n"
  },
  {
    "path": "scripts/package/README.txt",
    "content": "Run move-to-aw-modules.sh to copy all modules except aw-tauri to ~/aw-modules/.\naw-tauri (replaces aw-qt) will use this directory to discover new modules.\nYou can add your own modules and scripts to this directory. The modules should\nstart with the aw- prefix and should not have an extension (e.g. no .sh).\n\nIn the aw-tauri folder there are AppImage, RPM, and DEB binaries. Choose the\nappropriate one for your Linux distribution. If in doubt, use the AppImage as\nit works on most Linux systems. If you use the AppImage, copy it to a permanent\nfolder like ~/bin or /usr/local/bin, since autostart relies on the AppImage\nbeing in the same location each time.\n"
  },
  {
    "path": "scripts/package/activitywatch-setup.iss",
    "content": "; Script generated by the Inno Setup Script Wizard.\n; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!\n\n#define MyAppName \"ActivityWatch\"\n#define MyAppVersion GetEnv('AW_VERSION')\n#define MyAppPublisher \"ActivityWatch Contributors\"\n#define MyAppURL \"https://activitywatch.net/\"\n#define MyAppExeName \"aw-qt.exe\"\n#define RootDir \"..\\..\"\n#define DistDir \"..\\..\\dist\"\n\n#pragma verboselevel 9\n\n[Setup]\n; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.\n; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)\n; NOTE: the double {{ are used to escape the { character (needed for the AppId)\nAppId={{F226B8F4-3244-46E6-901D-0CE8035423E4}\nAppName={#MyAppName}\nAppVersion={#MyAppVersion}\n;AppVerName={#MyAppName} {#MyAppVersion}\nAppPublisher={#MyAppPublisher}\nAppPublisherURL={#MyAppURL}\nAppSupportURL=\"https://github.com/ActivityWatch/activitywatch/issues\"\nAppUpdatesURL=\"https://github.com/ActivityWatch/activitywatch/releases\"\nDefaultDirName={autopf}\\{#MyAppName}\nDisableProgramGroupPage=yes\n; Uncomment the following line to run in non administrative install mode (install for current user only.)\nPrivilegesRequired=lowest\nPrivilegesRequiredOverridesAllowed=dialog\nOutputDir={#DistDir}\nOutputBaseFilename=activitywatch-setup\nSetupIconFile=\"{#RootDir}\\aw-qt\\media\\logo\\logo.ico\"\nUninstallDisplayName={#MyAppName}\nUninstallDisplayIcon={app}\\{#MyAppExeName}\nCompression=lzma\nSolidCompression=yes\nWizardStyle=modern\n\n[Languages]\nName: \"english\"; MessagesFile: \"compiler:Default.isl\"\n\n[Tasks]\nName: \"desktopicon\"; Description: \"{cm:CreateDesktopIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\"; Flags: unchecked\nName: \"StartMenuEntry\" ; Description: \"Start ActivityWatch when Windows starts\"; GroupDescription: \"Windows Startup\"; MinVersion: 4,4;\n\n[Files]\nSource: \"{#DistDir}\\activitywatch\\aw-qt.exe\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \"{#DistDir}\\activitywatch\\*\"; DestDir: \"{app}\"; Flags: ignoreversion recursesubdirs createallsubdirs\n; NOTE: Don't use \"Flags: ignoreversion\" on any shared system files\n\n[Icons]\nName: \"{autoprograms}\\{#MyAppName}\"; Filename: \"{app}\\{#MyAppExeName}\"\nName: \"{autodesktop}\\{#MyAppName}\"; Filename: \"{app}\\{#MyAppExeName}\"; Tasks: desktopicon\nName: \"{userstartup}\\{#MyAppName}\"; Filename: \"{app}\\{#MyAppExeName}\"; Tasks: StartMenuEntry;\n\n[Run]\nFilename: \"{app}\\{#MyAppExeName}\"; Description: \"{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}\"; Flags: nowait postinstall skipifsilent\n\n; Removes the previously installed version before installing the new one\n; NOTE: Doesn't work? And also discouraged by the docs\n;[InstallDelete]\n;Type: filesandordirs; Name: \"{app}\\\"\n"
  },
  {
    "path": "scripts/package/aw-tauri.iss",
    "content": "; Inno Setup script for ActivityWatch (Tauri edition)\n;\n; This is separate from activitywatch-setup.iss (aw-qt) to avoid\n; installation collisions. Uses a different AppId, install directory,\n; and display name.\n\n#define MyAppName \"ActivityWatch (Tauri)\"\n#define MyAppVersion GetEnv('AW_VERSION')\n#define MyAppPublisher \"ActivityWatch Contributors\"\n#define MyAppURL \"https://activitywatch.net/\"\n#define MyAppExeName \"aw-tauri.exe\"\n#define RootDir \"..\\..\"\n#define DistDir \"..\\..\\dist\"\n\n#pragma verboselevel 9\n\n[Setup]\n; IMPORTANT: Different AppId from aw-qt to allow side-by-side installation\nAppId={{983D0855-08C8-46BD-AEFB-3924581C6703}\nAppName={#MyAppName}\nAppVersion={#MyAppVersion}\nAppVerName={#MyAppName} {#MyAppVersion}\nAppPublisher={#MyAppPublisher}\nAppPublisherURL={#MyAppURL}\nAppSupportURL=\"https://github.com/ActivityWatch/activitywatch/issues\"\nAppUpdatesURL=\"https://github.com/ActivityWatch/activitywatch/releases\"\nDefaultDirName={autopf}\\ActivityWatch-Tauri\nDisableProgramGroupPage=yes\nPrivilegesRequired=lowest\nPrivilegesRequiredOverridesAllowed=dialog\nOutputDir={#DistDir}\nOutputBaseFilename=activitywatch-setup\nSetupIconFile=\"{#RootDir}\\aw-tauri\\src-tauri\\icons\\icon.ico\"\nUninstallDisplayName={#MyAppName}\nUninstallDisplayIcon={app}\\{#MyAppExeName}\nCompression=lzma\nSolidCompression=yes\nWizardStyle=modern\n\n[Languages]\nName: \"english\"; MessagesFile: \"compiler:Default.isl\"\n\n[Tasks]\nName: \"desktopicon\"; Description: \"{cm:CreateDesktopIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\"; Flags: unchecked\nName: \"StartMenuEntry\" ; Description: \"Start ActivityWatch when Windows starts\"; GroupDescription: \"Windows Startup\"; MinVersion: 4,4;\n\n[Files]\nSource: \"{#DistDir}\\activitywatch\\aw-tauri.exe\"; DestDir: \"{app}\\aw-tauri\"; Flags: ignoreversion\nSource: \"{#DistDir}\\activitywatch\\*\"; DestDir: \"{app}\"; Flags: ignoreversion recursesubdirs createallsubdirs\n\n[Icons]\nName: \"{autoprograms}\\{#MyAppName}\"; Filename: \"{app}\\{#MyAppExeName}\"\nName: \"{autodesktop}\\{#MyAppName}\"; Filename: \"{app}\\{#MyAppExeName}\"; Tasks: desktopicon\nName: \"{userstartup}\\{#MyAppName}\"; Filename: \"{app}\\{#MyAppExeName}\"; Tasks: StartMenuEntry;\n\n[Run]\nFilename: \"{app}\\{#MyAppExeName}\"; Description: \"{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}\"; Flags: nowait postinstall skipifsilent\n"
  },
  {
    "path": "scripts/package/build_app_tauri.sh",
    "content": "#!/bin/bash\nset -e\n\n# Build a macOS .app bundle for the Tauri-based ActivityWatch.\n# This replaces the PyInstaller-based bundling used by aw-qt.\n\nAPP_NAME=\"ActivityWatch\"\nBUNDLE_ID=\"net.activitywatch.ActivityWatch\"\nVERSION=\"0.1.0\"\nICON_PATH=\"aw-tauri/src-tauri/icons/icon.icns\"\n\nif [[ \"$(uname)\" != \"Darwin\" ]]; then\n    echo \"This script is designed to run on macOS only.\"\n    exit 1\nfi\n\nif [ ! -d \"dist/activitywatch\" ]; then\n    echo \"Error: dist/activitywatch directory not found. Please build the project first.\"\n    exit 1\nfi\n\nif [ ! -f \"dist/activitywatch/aw-tauri\" ]; then\n    echo \"Error: aw-tauri binary not found in dist/activitywatch/\"\n    exit 1\nfi\n\necho \"Cleaning previous builds...\"\nrm -rf \"dist/${APP_NAME}.app\"\nmkdir -p \"dist\"\n\necho \"Creating app bundle structure...\"\nmkdir -p \"dist/${APP_NAME}.app/Contents/\"{MacOS,Resources}\n\necho \"Copying aw-tauri as main executable...\"\ncp \"dist/activitywatch/aw-tauri\" \"dist/${APP_NAME}.app/Contents/MacOS/aw-tauri\"\nchmod +x \"dist/${APP_NAME}.app/Contents/MacOS/aw-tauri\"\n\necho \"Copying components to Resources...\"\nfor component in dist/activitywatch/*/; do\n    if [ -d \"$component\" ]; then\n        component_name=$(basename \"$component\")\n        echo \"  Copying $component_name...\"\n        mkdir -p \"dist/${APP_NAME}.app/Contents/Resources/$component_name\"\n        cp -r \"$component\"/* \"dist/${APP_NAME}.app/Contents/Resources/$component_name/\"\n    fi\ndone\n\necho \"Setting executable permissions...\"\nfind \"dist/${APP_NAME}.app/Contents/Resources\" -type f -name \"aw-*\" -exec chmod +x {} \\;\n\necho \"Copying app icon...\"\nif [ -f \"$ICON_PATH\" ]; then\n    cp \"$ICON_PATH\" \"dist/${APP_NAME}.app/Contents/Resources/icon.icns\"\nelse\n    echo \"Warning: Icon file not found at $ICON_PATH\"\nfi\n\necho \"Creating Info.plist...\"\ncat > \"dist/${APP_NAME}.app/Contents/Info.plist\" << EOF\n<?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>CFBundleDevelopmentRegion</key>\n    <string>English</string>\n    <key>CFBundleExecutable</key>\n    <string>aw-tauri</string>\n    <key>CFBundleIconFile</key>\n    <string>icon.icns</string>\n    <key>CFBundleIdentifier</key>\n    <string>${BUNDLE_ID}</string>\n    <key>CFBundleInfoDictionaryVersion</key>\n    <string>6.0</string>\n    <key>CFBundleName</key>\n    <string>${APP_NAME}</string>\n    <key>CFBundlePackageType</key>\n    <string>APPL</string>\n    <key>CFBundleShortVersionString</key>\n    <string>${VERSION}</string>\n    <key>CFBundleVersion</key>\n    <string>${VERSION}</string>\n    <key>NSAppleEventsUsageDescription</key>\n    <string>ActivityWatch needs access to monitor application usage</string>\n    <key>NSHighResolutionCapable</key>\n    <true/>\n    <key>NSPrincipalClass</key>\n    <string>NSApplication</string>\n    <key>LSMinimumSystemVersion</key>\n    <string>10.14</string>\n</dict>\n</plist>\nEOF\n\necho \"Creating PkgInfo...\"\necho \"APPL????\" > \"dist/${APP_NAME}.app/Contents/PkgInfo\"\n\nif [ -n \"$APPLE_PERSONALID\" ]; then\n    echo \"Signing app with identity: $APPLE_PERSONALID\"\n    codesign --deep --force --sign \"$APPLE_PERSONALID\" \"dist/${APP_NAME}.app\"\n    echo \"App signing complete.\"\nelse\n    echo \"APPLE_PERSONALID not set. Skipping code signing.\"\nfi\n\necho \"App bundle created at: dist/${APP_NAME}.app\"\n"
  },
  {
    "path": "scripts/package/deb/control",
    "content": "Package: activitywatch\nArchitecture: amd64\nMaintainer: Erik Bjäreholt <erik@bjareho.lt>\nDepends:\nPriority: optional\nVersion: SCRIPT_VERSION_HERE\nDescription: Open source time tracker\n https://github.com/ActivityWatch/activitywatch\n"
  },
  {
    "path": "scripts/package/dmgbuild-settings.py",
    "content": "# -*- coding: utf-8 -*-\nfrom __future__ import unicode_literals\n\nimport plistlib\nimport os.path\n\n# Use like this: dmgbuild -s settings.py \"Test Volume\" test.dmg\n\n# You can actually use this file for your own application (not just TextEdit)\n# by doing e.g.\n#\n#   dmgbuild -s settings.py -D app=/path/to/My.app \"My Application\" MyApp.dmg\n\n# .. Useful stuff ..............................................................\n\napplication = defines.get('app', 'dist/ActivityWatch.app')\nappname = os.path.basename(application)\n\ndef icon_from_app(app_path):\n    plist_path = os.path.join(app_path, 'Contents', 'Info.plist')\n    with open(plist_path, \"rb\") as f:\n        plist = plistlib.load(f)\n    icon_name = plist['CFBundleIconFile']\n    icon_root,icon_ext = os.path.splitext(icon_name)\n    if not icon_ext:\n        icon_ext = '.icns'\n    icon_name = icon_root + icon_ext\n    return os.path.join(app_path, 'Contents', 'Resources', icon_name)\n\n# .. Basics ....................................................................\n\n# Uncomment to override the output filename\n# filename = 'test.dmg'\n\n# Uncomment to override the output volume name\n# volume_name = 'Test'\n\n# Volume format (see hdiutil create -help)\nformat = defines.get('format', 'UDBZ')\n\n# Volume size\nsize = defines.get('size', None)\n\n# Files to include\nfiles = [ application ]\n\n# Symlinks to create\nsymlinks = { 'Applications': '/Applications' }\n\n# Volume icon\n#\n# You can either define icon, in which case that icon file will be copied to the\n# image, *or* you can define badge_icon, in which case the icon file you specify\n# will be used to badge the system's Removable Disk icon\n#\n#icon = '/path/to/icon.icns'\nbadge_icon = icon_from_app(application)\n\n# Where to put the icons\nicon_locations = {\n    appname:        (140, 120),\n    'Applications': (500, 120)\n}\n\nshow_status_bar = False\nshow_tab_view = False\nshow_toolbar = False\nshow_pathbar = False\nshow_sidebar = False\nsidebar_width = 180\n\n# Window position in ((x, y), (w, h)) format\nwindow_rect = ((100, 100), (640, 280))\n\ndefault_view = 'icon-view'\n\nshow_icon_preview = False\n\n# Set these to True to force inclusion of icon/list view settings (otherwise\n# we only include settings for the default view)\ninclude_icon_view_settings = 'auto'\ninclude_list_view_settings = 'auto'\n\n# .. Icon view configuration ...................................................\n\narrange_by = None\ngrid_offset = (0, 0)\ngrid_spacing = 100\nscroll_position = (0, 0)\nlabel_pos = 'bottom' # or 'right'\ntext_size = 16\nicon_size = 128\n\n# .. List view configuration ...................................................\n\n# Column names are as follows:\n#\n#   name\n#   date-modified\n#   date-created\n#   date-added\n#   date-last-opened\n#   size\n#   kind\n#   label\n#   version\n#   comments\n#\nlist_icon_size = 16\nlist_text_size = 12\nlist_scroll_position = (0, 0)\nlist_sort_by = 'name'\nlist_use_relative_dates = True\nlist_calculate_all_sizes = False,\nlist_columns = ('name', 'date-modified', 'size', 'kind', 'date-added')\nlist_column_widths = {\n    'name': 300,\n    'date-modified': 181,\n    'date-created': 181,\n    'date-added': 181,\n    'date-last-opened': 181,\n    'size': 97,\n    'kind': 115,\n    'label': 100,\n    'version': 75,\n    'comments': 300,\n}\n\nlist_column_sort_directions = {\n    'name': 'ascending',\n    'date-modified': 'descending',\n    'date-created': 'descending',\n    'date-added': 'descending',\n    'date-last-opened': 'descending',\n    'size': 'descending',\n    'kind': 'ascending',\n    'label': 'ascending',\n    'version': 'ascending',\n    'comments': 'ascending',\n}\n\n"
  },
  {
    "path": "scripts/package/entitlements.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<!-- These are required for binaries built by PyInstaller -->\n\t<key>com.apple.security.cs.allow-jit</key>\n\t<true/>\n\t<key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "scripts/package/getversion.sh",
    "content": "#!/bin/bash\n\n# TODO: Merge with scripts/package/getversion.sh\n# set -e\n\nif [[ $TRAVIS_TAG ]]; then\n    _version=$TRAVIS_TAG;\nelif [[ $APPVEYOR_REPO_TAG_NAME ]]; then\n    _version=$APPVEYOR_REPO_TAG_NAME;\nelse\n    # Exact\n    _version=$(git describe --tags --abbrev=0 --exact-match 2>/dev/null)\n    if [[ -z $_version ]]; then\n        # Latest tag + commit ID\n        _version=\"$(git describe --tags --abbrev=0).dev-$(git rev-parse --short HEAD)\"\n    fi\nfi\n\necho $_version;\n"
  },
  {
    "path": "scripts/package/move-to-aw-modules.sh",
    "content": "#!/bin/bash\n# Copy all AW modules to ~/aw-modules/ for aw-tauri to discover.\n# aw-tauri uses this directory to find and launch AW components.\nset -e\n\nmkdir -p ~/aw-modules/\n\nif [[ -n \"$XDG_SESSION_TYPE\" && \"$XDG_SESSION_TYPE\" == \"wayland\" ]]; then\n    rsync -a . ~/aw-modules/ \\\n        --exclude=aw-tauri \\\n        --exclude=aw-server-rust \\\n        --exclude=awatcher \\\n        --exclude=move-to-aw-modules.sh \\\n        --exclude=README.txt\n    cp ./awatcher/aw-awatcher ~/aw-modules/\n    cp ./aw-server-rust/aw-sync ~/aw-modules/\nelse\n    rsync -a . ~/aw-modules/ \\\n        --exclude=aw-tauri \\\n        --exclude=awatcher \\\n        --exclude=aw-server-rust \\\n        --exclude=move-to-aw-modules.sh \\\n        --exclude=README.txt\n    cp ./aw-server-rust/aw-sync ~/aw-modules/\nfi\n\necho \"Modules copied to ~/aw-modules/\"\n"
  },
  {
    "path": "scripts/package/package-all.sh",
    "content": "#!/bin/bash\n\nset -e\n\nechoerr() { echo \"$@\" 1>&2; }\n\nfunction get_platform() {\n    # Will return \"linux\" for GNU/Linux\n    #   I'd just like to interject for a moment...\n    #   https://wiki.installgentoo.com/index.php/Interjection\n    # Will return \"macos\" for macOS/OS X\n    # Will return \"windows\" for Windows/MinGW/msys\n\n    _platform=$(uname | tr '[:upper:]' '[:lower:]')\n    if [[ $_platform == \"darwin\" ]]; then\n        _platform=\"macos\";\n    elif [[ $_platform == \"msys\"* ]]; then\n        _platform=\"windows\";\n    elif [[ $_platform == \"mingw\"* ]]; then\n        _platform=\"windows\";\n    elif [[ $_platform == \"linux\" ]]; then\n        # Nothing to do\n        true;\n    else\n        echoerr \"ERROR: $_platform is not a valid platform\";\n        exit 1;\n    fi\n\n    echo $_platform;\n}\n\nfunction get_version() {\n    $(dirname \"$0\")/getversion.sh;\n}\n\nfunction get_arch() {\n    _arch=\"$(uname -m)\"\n    echo $_arch;\n}\n\nplatform=$(get_platform)\nversion=$(get_version)\narch=$(get_arch)\necho \"Platform: $platform, arch: $arch, version: $version\"\n\n# For Tauri Linux builds, include helper scripts and README\nif [[ $platform == \"linux\" && $TAURI_BUILD == \"true\" ]]; then\n    cp scripts/package/README.txt scripts/package/move-to-aw-modules.sh dist/activitywatch/\nfi\n\nfunction build_zip() {\n    echo \"Zipping executables...\"\n    pushd dist;\n    filename=\"activitywatch-${version}-${platform}-${arch}.zip\"\n    echo \"Name of package will be: $filename\"\n\n    if [[ $platform == \"windows\"* ]]; then\n        7z a $filename activitywatch;\n    else\n        zip -r $filename activitywatch;\n    fi\n    popd;\n    echo \"Zip built!\"\n}\n\nfunction build_setup() {\n    filename=\"activitywatch-${version}-${platform}-${arch}-setup.exe\"\n    echo \"Name of package will be: $filename\"\n\n    innosetupdir=\"/c/Program Files (x86)/Inno Setup 6\"\n    if [ ! -d \"$innosetupdir\" ]; then\n        echo \"ERROR: Couldn't find innosetup which is needed to build the installer. We suggest you install it using chocolatey. Exiting.\"\n        exit 1\n    fi\n\n    # Windows installer version should not include 'v' prefix, see: https://github.com/microsoft/winget-pkgs/pull/17564\n    version_no_prefix=\"$(echo $version | sed -e 's/^v//')\"\n    if [[ $TAURI_BUILD == \"true\" ]]; then\n        env AW_VERSION=$version_no_prefix \"$innosetupdir/iscc.exe\" scripts/package/aw-tauri.iss\n    else\n        env AW_VERSION=$version_no_prefix \"$innosetupdir/iscc.exe\" scripts/package/activitywatch-setup.iss\n    fi\n    mv dist/activitywatch-setup.exe dist/$filename\n    echo \"Setup built!\"\n}\n\nbuild_zip\nif [[ $platform == \"windows\"* ]]; then\n    build_setup\nfi\n\necho\necho \"-------------------------------------\"\necho \"Contents of ./dist\"\nls -l dist\necho \"-------------------------------------\"\n\n"
  },
  {
    "path": "scripts/package/package-appimage.sh",
    "content": "#!/bin/bash\n\n# pick the latest zip\n# NOTE: this assumes that the latest built zip is the only zip in the directory\nZIP_FILE=`ls ./dist/ -1 | grep zip | sort -r | head -1`\nunzip ./dist/$ZIP_FILE\n\n# fetch deps\nwget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage\nchmod +x linuxdeploy-x86_64.AppImage\nwget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage\nchmod +x appimagetool-x86_64.AppImage\n\n# create AppRun\necho '#!/bin/sh\nDIR=\"$(dirname \"$(readlink -f \"${0}\")\")\"\n\"${DIR}\"/aw-qt \"$@\"' > activitywatch/AppRun\nchmod a+x ./activitywatch/AppRun\n\n# build appimage\n./linuxdeploy-x86_64.AppImage --appdir activitywatch --executable ./activitywatch/aw-qt --output appimage --desktop-file ./activitywatch/aw-qt.desktop --icon-file ./activitywatch/media/logo/logo.png --icon-filename activitywatch\nAPPIMAGE_FILE=`ls -1 | grep AppImage| grep -i ActivityWatch`\ncp -v $APPIMAGE_FILE ./dist/activitywatch-linux-x86_64.AppImage\n"
  },
  {
    "path": "scripts/package/package-deb.sh",
    "content": "#!/usr/bin/bash\n# Setting the shell is required, as `sh` doesn't support slicing.\n\n# Fail fast\nset -e\n# Verbose commands for CI verification\nset -x\n\nVERSION=$(scripts/package/getversion.sh)\n# Slice off the \"v\" from the tag, which is probably guaranteed\nVERSION_NUM=${VERSION:1}\necho $VERSION_NUM\nPKGDIR=\"activitywatch_$VERSION_NUM\"\n\n# Package tools\nsudo apt-get install sed jdupes wget\n\nif [ -d \"PKGDIR\" ]; then\n    sudo rm -rf $PKGDIR\nfi\n\n# .deb meta files\nmkdir -p $PKGDIR/DEBIAN\n# activitywatch's install location\nmkdir -p $PKGDIR/opt\n# Allows aw-qt to autostart.\nmkdir -p $PKGDIR/etc/xdg/autostart\n# Allows users to manually start aw-qt from their start menu.\nmkdir -p $PKGDIR/usr/share/applications\n\n# While storing the control file in a variable here, dumping it in a file is so unnecessarily\n# complicated that it's easier to just dump move and sed.\ncp ./scripts/package/deb/control $PKGDIR/DEBIAN/control\nsed -i \"s/SCRIPT_VERSION_HERE/${VERSION_NUM}/\" $PKGDIR/DEBIAN/control\n\n# Verify the file content\ncat $PKGDIR/DEBIAN/control\n# The entire opt directory (should) consist of dist/activitywatch/*\n\ncp -r dist/activitywatch/ $PKGDIR/opt/\n\n# Hard link duplicated libraries\n# (I have no idea what this is for)\njdupes -L -r -S -Xsize-:1K $PKGDIR/opt/\n\nsudo chown -R root:root $PKGDIR\n\n# Prepare the .desktop file\nsudo sed -i 's!Exec=aw-qt!Exec=/opt/activitywatch/aw-qt!' $PKGDIR/opt/activitywatch/aw-qt.desktop\nsudo cp $PKGDIR/opt/activitywatch/aw-qt.desktop $PKGDIR/etc/xdg/autostart/\nsudo cp $PKGDIR/opt/activitywatch/aw-qt.desktop $PKGDIR/usr/share/applications/\n\ndpkg-deb --build $PKGDIR\nsudo mv activitywatch_${VERSION_NUM}.deb dist/activitywatch-${VERSION}-linux-x86_64.deb\n"
  },
  {
    "path": "scripts/submodule-branch.sh",
    "content": "#!/bin/bash\n\n# Get current branch\n#   git rev-parse --abbrev-ref HEAD\n# Get branch for each submodule\n#   git submodule foreach \"git rev-parse --abbrev-ref HEAD\"\n\nSUBMODULES=$(git submodule | sed -r -e 's/^[ \\+][a-z0-9]+ //g' -e 's/ \\(.*\\)//g')\nfor module in $SUBMODULES; do\n    branch=$(git --git-dir=$module/.git rev-parse --abbrev-ref HEAD)\n    printf \"%-20s %-30s\\n\" \"$module\" \"$branch\"\ndone\n"
  },
  {
    "path": "scripts/symlink-systemd.sh",
    "content": "#!/bin/bash\nfor module in \"aw-server\" \"aw-watcher-afk\" \"aw-watcher-x11\"; do\n    ln -s $(pwd)/$module/misc/${module}.service ~/.config/systemd/user/${module}.service\ndone\n"
  },
  {
    "path": "scripts/tests/integration_tests.py",
    "content": "import os\nimport platform\nimport subprocess\nimport tempfile\nfrom time import sleep\n\nimport pytest\n\n\ndef _windows_kill_process(pid):\n    import ctypes\n\n    PROCESS_TERMINATE = 1\n    handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, False, pid)\n    ctypes.windll.kernel32.TerminateProcess(handle, -1)\n    ctypes.windll.kernel32.CloseHandle(handle)\n\n\n# NOTE: to run tests with a specific server binary,\n#       set the PATH such that it is the \"aw-server\" binary.\n@pytest.fixture(scope=\"session\")\ndef server_process():\n    logfile_stdout = tempfile.NamedTemporaryFile(delete=False)\n    logfile_stderr = tempfile.NamedTemporaryFile(delete=False)\n\n    # find the path of the \"aw-server\" binary and log it\n    which_server = subprocess.check_output([\"which\", \"aw-server\"], text=True)\n    print(f\"aw-server path: {which_server}\")\n\n    # if aw-server-rust in PATH, assert that we're picking up the aw-server-rust binary\n    if \"aw-server-rust\" in os.environ[\"PATH\"]:\n        assert \"aw-server-rust\" in which_server\n\n    server_proc = subprocess.Popen(\n        [\"aw-server\", \"--testing\"], stdout=logfile_stdout, stderr=logfile_stderr\n    )\n\n    # Wait for server to start up properly\n    # TODO: Ping the server until it's alive to remove this sleep\n    sleep(5)\n\n    yield server_proc\n\n    if platform.system() == \"Windows\":\n        # On Windows, for whatever reason, server_proc.kill() doesn't do the job.\n        _windows_kill_process(server_proc.pid)\n    else:\n        server_proc.kill()\n    server_proc.wait(5)\n    server_proc.communicate()\n\n    error_indicators = [\"ERROR\"]\n\n    with open(logfile_stdout.name, \"r+b\") as f:\n        stdout = str(f.read(), \"utf8\")\n        if any(e in stdout for e in error_indicators):\n            pytest.fail(f\"Found ERROR indicator in stdout from server: {stdout}\")\n\n    with open(logfile_stderr.name, \"r+b\") as f:\n        stderr = str(f.read(), \"utf8\")\n        # For some reason, this fails aw-server-rust, but not aw-server-python\n        # if not stderr:\n        #    pytest.fail(\"No output to stderr from server\")\n\n        # Will show in case pytest fails\n        print(stderr)\n\n        for s in error_indicators:\n            if s in stderr:\n                pytest.fail(f\"Found ERROR indicator in stderr from server: {s}\")\n\n    # NOTE: returncode was -9 for whatever reason\n    # if server_proc.returncode != 0:\n    #     pytest.fail(\"Exit code was non-zero ({})\".format(server_proc.returncode))\n\n\n# TODO: Use the fixture in the tests instead of this thing here\ndef test_integration(server_process):\n    # This is just here so that the server_process fixture is initialized\n    pass\n\n    # exit_code = pytest.main([\"./aw-server/tests\", \"-v\"])\n    # if exit_code != 0:\n    #     pytest.fail(\"Tests exited with non-zero code: \" + str(exit_code))\n"
  },
  {
    "path": "scripts/uninstall.sh",
    "content": "#!/bin/bash\n\nmodules=$(pip3 list --format=legacy | grep 'aw-' | grep -o '^aw-[^ ]*')\n\nfor module in $modules; do\n    pip3 uninstall -y $module\ndone\n\n"
  },
  {
    "path": "scripts/update-deps.sh",
    "content": "#!/bin/bash\n\n# Update dependency locks for each submodule in the activitywatch repo\n\nset -e\nset -x\n\n# For submodule in submodules:\nfor submodule in $(git submodule | sed 's/^[+ ]//' | cut -d' ' -f2); do\n    # Go to submodule\n    cd $submodule\n\n    # Check that we're on the master branch and latest commit\n    if [ $(git rev-parse --abbrev-ref HEAD) != \"master\" ]; then\n        echo \"Submodule $submodule is not on master branch, aborting\"\n        exit 1\n    fi\n\n    # Update dependency locks\n    # Use poetry if poetry.lock exists, or cargo if Cargo.toml exists\n    if [ -f \"poetry.lock\" ]; then\n        poetry update\n    elif [ -f \"Cargo.toml\" ]; then\n        cargo update\n    fi\n\n    # Go back to root\n    cd ..\ndone\n\n"
  }
]