Full Code of ActivityWatch/activitywatch for AI

master 5548a0b2c4e3 cached
61 files
154.4 KB
42.1k tokens
27 symbols
1 requests
Download .txt
Repository: ActivityWatch/activitywatch
Branch: master
Commit: 5548a0b2c4e3
Files: 61
Total size: 154.4 KB

Directory structure:
gitextract_zuvhj3r3/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.md
│   │   ├── config.yml
│   │   └── everything-else.md
│   ├── dependabot.yml
│   ├── stale.yml
│   └── workflows/
│       ├── build-tauri.yml
│       ├── build.yml
│       ├── codeql.yml
│       ├── dependabot-automerge.yml
│       ├── diagram.yml
│       ├── greetings.yml
│       ├── test.yml
│       └── winget.yml
├── .gitignore
├── .gitmodules
├── .tool-versions
├── CITATION.cff
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.txt
├── Makefile
├── README.md
├── SECURITY.md
├── aw.spec
├── gptme.toml
├── pyproject.toml
└── scripts/
    ├── build_changelog.py
    ├── changelog_contributors.csv
    ├── changelog_contributors_twitter.csv
    ├── checkout-latest-tag.sh
    ├── chores/
    │   └── make-release.sh
    ├── ci/
    │   ├── enable_long_paths.bat
    │   ├── import-macos-p12.sh
    │   ├── install_node.ps1
    │   ├── install_pyhook.ps1
    │   ├── install_python.ps1
    │   └── run_with_env.cmd
    ├── count_lines.sh
    ├── get_latest_release.sh
    ├── logcrawler.py
    ├── nop.sh
    ├── notarize.sh
    ├── package/
    │   ├── README.txt
    │   ├── activitywatch-setup.iss
    │   ├── aw-tauri.iss
    │   ├── build_app_tauri.sh
    │   ├── deb/
    │   │   └── control
    │   ├── dmgbuild-settings.py
    │   ├── entitlements.plist
    │   ├── getversion.sh
    │   ├── move-to-aw-modules.sh
    │   ├── package-all.sh
    │   ├── package-appimage.sh
    │   └── package-deb.sh
    ├── submodule-branch.sh
    ├── symlink-systemd.sh
    ├── tests/
    │   └── integration_tests.py
    ├── uninstall.sh
    └── update-deps.sh

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

================================================
FILE: .gitattributes
================================================
# See https://github.com/github/linguist for details

# Trick to remove some build tools from language overview
Makefile linguist-vendored
*.sh linguist-vendored
*.cmd linguist-vendored
*.ps1 linguist-vendored


================================================
FILE: .github/FUNDING.yml
================================================
# Docs for this file can be found here:
# https://docs.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository

github: ["ActivityWatch", "ErikBjare", "johan-bjareholt"]
patreon: "erikbjare"
open_collective: "activitywatch"
liberapay: "ActivityWatch"
custom: ["https://activitywatch.net/donate/"]


================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: "\U0001F41E Bug report"
about: Did you find a bug?
title: ''
labels: 'type: bug'
assignees: ''

---

<!--
  Hi there! Thank you for discovering and submitting an issue.

  Before you submit this; let's make sure of a few things.
  Please make sure the following boxes are ticked if they are correct.
  If not, please try and complete them first.
-->

<!-- Checked checkbox should look like this: [x] -->
 - [ ] I am on the [latest](https://github.com/ActivityWatch/activitywatch/releases/latest) ActivityWatch version.
 - [ ] I have searched the issues of this repo and believe that this is not a duplicate.


<!--
  Once those are done, if you're able to fill in the following list with your information,
  it'd be very helpful to whoever handles the issue.
-->

- **OS name and version**: <!-- Replace this comment with OS name + version -->
- **ActivityWatch version**: <!-- Replace this comment with the ActivityWatch version (found at the bottom of the Web UI) -->

## Describe the bug
<!-- A clear and concise description of what the bug is. -->

## To Reproduce
<!--
  Steps to reproduce the behavior, for example:
    1. Go to '...'
    2. Click on '...'
    3. Scroll down to '...'
    4. See error
-->

## Expected behavior
<!-- A clear and concise description of what you expected to happen. -->

## Documentation
<!--
  If applicable, add screenshots or logs to help explain your problem.

  Logs can be found in different places depending on platform:
   - Windows: `C:\Users\<USER>\AppData\Local\ActivityWatch\Logs`
   - macOS: `/Users/<USER>/Library/Logs/activitywatch`
   - Linux: `/home/<USER>/.cache/activitywatch/log`
  They can be opened with any plain text editor.
-->

## Additional context
<!-- Add any other context about the problem here. -->


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
# Issue templates are based on templates for poetry:
# https://github.com/python-poetry/poetry/tree/master/.github/ISSUE_TEMPLATE
blank_issues_enabled: false
contact_links:
  - name: "\U0001F381 Feature requests"
    url: https://forum.activitywatch.net/c/features
    about: Request and vote on features on the forum.
  - name: "\u2753 Support"
    url: https://forum.activitywatch.net/
    about: Need help with something? Ask for help on the forum!
  - name: "\U0001F4AD Discussion (on our forum)"
    url: https://forum.activitywatch.net/
    about: The preferred place for general discussion about ActivityWatch
  - name: "\U0001F4AD Discussion (on GitHub Discussions)"
    url: https://github.com/ActivityWatch/activitywatch/discussions
    about: We're testing it out (but the forum is still the preferred place).
  - name: "\U0001F4AC Chat with us on Discord"
    url: https://discord.gg/dctJK6USjK
    about: We love to see people who are active in issues on our Discord, come join us!


================================================
FILE: .github/ISSUE_TEMPLATE/everything-else.md
================================================
---
name: "\U0001F5C3 Everything Else"
about: For questions and issues that do not fall in any of the other categories.
title: ''
labels: ''
assignees: ''

---

<!-- 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/ -->


<!-- Checked checkbox should look like this: [x] -->
- [ ] I have searched the [issues](https://github.com/ActivityWatch/activitywatch/issues) of this repo and believe that this is not a duplicate.
- [ ] I have searched the [documentation](https://docs.activitywatch.net/en/latest/) and believe that my question is not covered.

## Issue
<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->


================================================
FILE: .github/dependabot.yml
================================================
# Set update schedule for GitHub Actions
version: 2
updates:
  # Maintain dependencies for GitHub Actions
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "monthly"

  # Maintain submodule versions
  # NOTE: too noisy, easier to update by hand
  #- package-ecosystem: "gitsubmodule"
  #  directory: "/"
  #  schedule:
  #    interval: "monthly"

  # Maintain dependencies for pip/poetry
  # NOTE: too noisy, easier to update by hand
  #- package-ecosystem: "pip"
  #  directory: "/"
  #  schedule:
  #    interval: "monthly"


================================================
FILE: .github/stale.yml
================================================
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 365
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 14
# Issues with these labels will never be considered stale
exemptLabels:
  - '!pinned'
  - 'priority: high'
  - 'improves: security'
  - 'improves: sustainability'
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
  This issue has been automatically marked as stale because it has not had
  recent activity. It will be closed if no further activity occurs. Thank you
  for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false


================================================
FILE: .github/workflows/build-tauri.yml
================================================
name: Build Tauri

on:
  push:
    branches: [master]
    tags:
      - v*
  pull_request:
    branches: [master]

jobs:
  build:
    name: ${{ matrix.os }}, py-${{ matrix.python_version }}, node-${{ matrix.node_version }}
    runs-on: ${{ matrix.os }}
    continue-on-error: ${{ matrix.experimental }}
    env:
      # Whether to build and include extras (like aw-notify and aw-watcher-input)
      AW_EXTRAS: true
      TAURI_BUILD: true
      # sets the macOS version target, see: https://users.rust-lang.org/t/compile-rust-binary-for-older-versions-of-mac-osx/38695
      MACOSX_DEPLOYMENT_TARGET: 10.9
    defaults:
      run:
        shell: bash
    strategy:
      fail-fast: false
      max-parallel: 5
      matrix:
        os:
          [
            ubuntu-24.04,
            ubuntu-24.04-arm,
            windows-latest,
            macos-14,
            macos-latest,
          ]
        python_version: [3.9]
        node_version: [22]
        skip_rust: [false]
        skip_webui: [false]
        experimental: [false]

    steps:
      - uses: actions/checkout@v4
        with:
          submodules: "recursive"
          fetch-depth: 0

      - name: Set environment variables
        run: |
          echo "RELEASE=${{ startsWith(github.ref_name, 'v') || github.ref_name == 'master' }}" >> $GITHUB_ENV
          echo "TAURI_BUILD=true" >> $GITHUB_ENV

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python_version }}

      - name: Set up Node
        if: ${{ !matrix.skip_webui }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node_version }}

      - name: Set up Rust
        if: ${{ !matrix.skip_rust }}
        uses: dtolnay/rust-toolchain@master
        id: toolchain
        with:
          toolchain: stable

      - name: Cache node_modules
        uses: actions/cache@v4
        if: ${{ !matrix.skip_webui }}
        with:
          path: |
            aw-server-rust/aw-webui/node_modules
            aw-tauri/node_modules
          key: ${{ matrix.os }}-node_modules-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ matrix.os }}-node_modules-

      - name: Cache cargo build
        uses: actions/cache@v4
        env:
          cache-name: cargo-build-target
        with:
          path: |
            aw-server-rust/target
            aw-tauri/src-tauri/target
          key: ${{ matrix.os }}-${{ env.cache-name }}-${{ steps.toolchain.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ matrix.os }}-${{ env.cache-name }}-${{ steps.toolchain.outputs.rustc_hash }}-

      - name: Install APT dependencies
        if: runner.os == 'Linux'
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libgtk-3-dev \
            libwebkit2gtk-4.1-dev \
            libayatana-appindicator3-dev \
            librsvg2-dev \
            libjavascriptcoregtk-4.1-dev \
            libsoup-3.0-dev \
            xdg-utils

      - name: Install dependencies
        run: |
          if [ "$RUNNER_OS" == "Windows" ]; then
            choco install innosetup
          fi
          pip3 install poetry==1.4.2

      - name: Build
        uses: nick-fields/retry@v3
        with:
          timeout_minutes: 60
          max_attempts: 3
          shell: bash
          command: |
            python3 -m venv venv
            source venv/bin/activate || source venv/Scripts/activate
            poetry install
            make build SKIP_WEBUI=${{ matrix.skip_webui }} SKIP_SERVER_RUST=${{ matrix.skip_rust }}
            pip freeze

      - name: Run tests
        uses: nick-fields/retry@v3
        with:
          timeout_minutes: 60
          max_attempts: 3
          shell: bash
          command: |
            source venv/bin/activate || source venv/Scripts/activate
            make test SKIP_SERVER_RUST=${{ matrix.skip_rust }}

      - name: Package
        run: |
          source venv/bin/activate || source venv/Scripts/activate
          poetry install
          make package SKIP_SERVER_RUST=${{ matrix.skip_rust }}

      - name: Package dmg
        if: runner.os == 'macOS'
        run: |
          if [ -n "$APPLE_EMAIL" ]; then
            ./scripts/ci/import-macos-p12.sh
          fi

          source venv/bin/activate
          make dist/ActivityWatch.dmg

          if [ -n "$APPLE_EMAIL" ]; then
            codesign --verbose -s ${APPLE_PERSONALID} dist/ActivityWatch.dmg

            brew install akeru-inc/tap/xcnotary
            xcnotary precheck dist/ActivityWatch.app
            xcnotary precheck dist/ActivityWatch.dmg

            make dist/notarize
          fi
          mv dist/ActivityWatch.dmg dist/activitywatch-$(scripts/package/getversion.sh)-macos-x86_64.dmg
        env:
          APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }}
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
          APPLE_PERSONALID: ${{ secrets.APPLE_TEAMID }}
          APPLE_TEAMID: ${{ secrets.APPLE_TEAMID }}
          CERTIFICATE_MACOS_P12_BASE64: ${{ secrets.CERTIFICATE_MACOS_P12_BASE64 }}
          CERTIFICATE_MACOS_P12_PASSWORD: ${{ secrets.CERTIFICATE_MACOS_P12_PASSWORD }}

      - name: Upload packages
        uses: actions/upload-artifact@v4
        with:
          name: builds-tauri-${{ matrix.os }}-py${{ matrix.python_version }}
          path: dist/activitywatch-*.*

  release-notes:
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: "recursive"
          fetch-depth: 0

      - uses: ActivityWatch/check-version-format-action@v2
        id: version
        with:
          prefix: "v"

      - name: Echo version
        run: |
          echo "${{ steps.version.outputs.full }} (stable: ${{ steps.version.outputs.is_stable }})"

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install deps
        run: |
          pip install requests

      - name: Generate release notes
        run: |
          LAST_RELEASE=`STABLE_ONLY=${{ steps.version.output.is_stable }} ./scripts/get_latest_release.sh`
          ./scripts/build_changelog.py --range "$LAST_RELEASE...${{ steps.version.outputs.full }}"

      - name: Rename
        run: |
          mv changelog.md release_notes.md

      - name: Upload release notes
        uses: actions/upload-artifact@v4
        with:
          name: release_notes_tauri
          path: release_notes.md

  release:
    needs: [build, release-notes]
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          path: dist

      - name: Display structure of downloaded files
        run: ls -R
        working-directory: dist

      - uses: ActivityWatch/check-version-format-action@v2
        id: version
        with:
          prefix: "v"

      - name: Release
        uses: softprops/action-gh-release@v1
        with:
          draft: true
          files: dist/*/activitywatch-*.*
          body_path: dist/release_notes_tauri/release_notes.md
          prerelease: ${{ !(steps.version.outputs.is_stable == 'true') }}


================================================
FILE: .github/workflows/build.yml
================================================
name: Build

on:
  push:
    branches: [ master ]
    tags:
      - v*
  pull_request:
    branches: [ master ]
  #release:
  #  types: [published]

jobs:
  build:
    name: ${{ matrix.os }}, py-${{ matrix.python_version }}, node-${{ matrix.node_version }}
    runs-on: ${{ matrix.os }}
    continue-on-error: ${{ matrix.experimental }}
    env:
      # Whether to build and include extras (like aw-notify and aw-watcher-input)
      AW_EXTRAS: true
      # sets the macOS version target, see: https://users.rust-lang.org/t/compile-rust-binary-for-older-versions-of-mac-osx/38695
      MACOSX_DEPLOYMENT_TARGET: 10.9
    defaults:
      run:
        shell: bash
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-24.04, windows-latest, macos-14, macos-latest]
        python_version: [3.9]
        node_version: [22]
        skip_rust: [false]
        skip_webui: [false]
        experimental: [false]

        #include:
        #  - os: ubuntu-latest
        #    python_version: 3.9
        #    node_version: 20
        #    experimental: true

    steps:
    - uses: actions/checkout@v4
      with:
        submodules: 'recursive'
        fetch-depth: 0  # fetch all branches and tags

    # Build in release mode if: (longer build times)
    #  - on a tag (release)
    #  - on the master branch (nightly)
    - name: Set RELEASE
      run: |
        echo "RELEASE=${{ startsWith(github.ref_name, 'v') || github.ref_name == 'master' }}" >> $GITHUB_ENV

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python_version }}

    - name: Set up Node
      if: ${{ !matrix.skip_webui }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node_version }}

    - name: Set up Rust
      if: ${{ !matrix.skip_rust }}
      uses: dtolnay/rust-toolchain@master
      id: toolchain
      with:
        toolchain: stable

    - name: Cache node_modules
      uses: actions/cache@v4
      if: ${{ !matrix.skip_webui }}
      with:
        path: aw-server-rust/aw-webui/node_modules
        key: ${{ matrix.os }}-node_modules-${{ hashFiles('**/package-lock.json') }}
        restore-keys: |
          ${{ matrix.os }}-node_modules-

    - name: Cache cargo build
      uses: actions/cache@v4
      # 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
      env:
        cache-name: cargo-build-target
      with:
        path: aw-server-rust/target
        # key needs to contain rustc_hash due to https://github.com/ActivityWatch/aw-server-rust/issues/180
        key: ${{ matrix.os }}-${{ env.cache-name }}-${{ steps.toolchain.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }}
        restore-keys: |
          ${{ matrix.os }}-${{ env.cache-name }}-${{ steps.toolchain.outputs.rustc_hash }}-

    - name: Install APT dependencies
      if: runner.os == 'Linux'
      run: |
          sudo apt-get update
          # Unsure which of these are actually necessary...
          sudo apt-get install -y \
            appstream \
            qt5-qmake \
            qtbase5-dev \
            qtwayland5 \
            libqt5x11extras5 \
            libfontconfig1 \
            libxcb1 \
            libfontconfig1-dev \
            libfreetype6-dev \
            libx11-dev \
            libxcursor-dev \
            libxext-dev \
            libxfixes-dev \
            libxft-dev \
            libxi-dev \
            libxrandr-dev \
            libxrender-dev

    - name: Install dependencies
      run: |
        if [ "$RUNNER_OS" == "Windows" ]; then
          choco install innosetup
        fi
        pip3 install poetry==1.4.2

    - name: Build
      run: |
        python3 -m venv venv
        source venv/bin/activate || source venv/Scripts/activate
        poetry install
        make build SKIP_WEBUI=${{ matrix.skip_webui }} SKIP_SERVER_RUST=${{ matrix.skip_rust }}
        pip freeze  # output Python packages, useful for debugging dependency versions

    - name: Run tests
      run: |
        source venv/bin/activate || source venv/Scripts/activate
        make test SKIP_SERVER_RUST=${{ matrix.skip_rust }}

    # Don't run integration tests on Windows, doesn't work for some reason
    - name: Run integration tests
      if: runner.os != 'Windows'
      run: |
        source venv/bin/activate || source venv/Scripts/activate
        make test-integration

    - name: Package
      run: |
        source venv/bin/activate || source venv/Scripts/activate
        poetry install  # run again to ensure we have the correct version of PyInstaller
        make package SKIP_SERVER_RUST=${{ matrix.skip_rust }}

    - name: Package dmg
      if: runner.os == 'macOS'
      run: |
        # Load certificates
        # Only load key & sign if env vars for signing exists
        if [ -n "$APPLE_EMAIL" ]; then
          ./scripts/ci/import-macos-p12.sh
        fi

        # Build .app and .dmg
        source venv/bin/activate
        make dist/ActivityWatch.dmg

        # codesign and notarize
        if [ -n "$APPLE_EMAIL" ]; then
          codesign --verbose -s ${APPLE_PERSONALID} dist/ActivityWatch.dmg

          # Run prechecks
          brew install akeru-inc/tap/xcnotary
          xcnotary precheck dist/ActivityWatch.app
          xcnotary precheck dist/ActivityWatch.dmg

          # Notarize
          make dist/notarize
        fi
        mv dist/ActivityWatch.dmg dist/activitywatch-$(scripts/package/getversion.sh)-macos-x86_64.dmg
      env:
        APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }}
        APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
        APPLE_PERSONALID: ${{ secrets.APPLE_TEAMID }} # APPLE_PERSONAL_ID == APPLE_TEAM_ID for personal accounts
        APPLE_TEAMID: ${{ secrets.APPLE_TEAMID }}
        CERTIFICATE_MACOS_P12_BASE64: ${{ secrets.CERTIFICATE_MACOS_P12_BASE64 }}
        CERTIFICATE_MACOS_P12_PASSWORD: ${{ secrets.CERTIFICATE_MACOS_P12_PASSWORD }}

    - name: Package AppImage
      if: startsWith(runner.os, 'linux')
      run: |
        ./scripts/package/package-appimage.sh

    - name: Package deb
      if: startsWith(runner.os, 'linux')
      run: |
        # The entire process is deferred to a shell file for consistency.
        ./scripts/package/package-deb.sh

    - name: Upload packages
      uses: actions/upload-artifact@v4
      with:
        name: builds-${{ matrix.os }}-py${{ matrix.python_version }}
        path: dist/activitywatch-*.*

  release-notes:
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')  # only on runs triggered from tag
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: 'recursive'
          fetch-depth: 0  # fetch all branches and tags

      - uses: ActivityWatch/check-version-format-action@v2
        id: version
        with:
          prefix: 'v'

      - name: Echo version
        run: |
          echo "${{ steps.version.outputs.full }} (stable: ${{ steps.version.outputs.is_stable }})"

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install deps
        run: |
          pip install requests

      - name: Generate release notes
        run: |
          LAST_RELEASE=`STABLE_ONLY=${{ steps.version.output.is_stable }} ./scripts/get_latest_release.sh`
          ./scripts/build_changelog.py --range "$LAST_RELEASE...${{ steps.version.outputs.full }}"

      # TODO: Move rename build_changelog and move into there
      - name: Rename
        run: |
          mv changelog.md release_notes.md

      - name: Upload release notes
        uses: actions/upload-artifact@v4
        with:
          name: release_notes
          path: release_notes.md

  release:
    needs: [build, release-notes]
    if: startsWith(github.ref, 'refs/tags/v')  # only run on tag
    runs-on: ubuntu-latest
    steps:
      # Will download all artifacts to path
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          path: dist

      - name: Display structure of downloaded files
        run: ls -R
        working-directory: dist

      # detect if version tag is stable/beta
      - uses: ActivityWatch/check-version-format-action@v2
        id: version
        with:
          prefix: 'v'

      # create a release
      - name: Release
        uses: softprops/action-gh-release@v1
        with:
          draft: true
          files: dist/*/activitywatch-*.*
          body_path: dist/release_notes/release_notes.md
          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


================================================
FILE: .github/workflows/codeql.yml
================================================
name: "CodeQL"

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]
  schedule:
    - cron: "57 14 * * 4"

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: [ python, javascript ]

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
            submodules: recursive

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v2
        with:
          languages: ${{ matrix.language }}
          queries: +security-and-quality

      - name: Autobuild
        uses: github/codeql-action/autobuild@v2

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v2
        with:
          category: "/language:${{ matrix.language }}"


================================================
FILE: .github/workflows/dependabot-automerge.yml
================================================
name: Dependabot Auto-merge

# NOTE: This workflow relies on a Personal Access Token from the @ActivityWatchBot user
#       See this issue for details: https://github.com/ridedott/merge-me-action/issues/1581

on:
  workflow_run:
    types:
      - completed
    workflows:
      # List all required workflow names here.
      - Build

permissions:
  contents: write
  pull-requests: read

jobs:
  auto_merge:
    name: Auto-merge
    runs-on: ubuntu-latest
    if: github.event.workflow_run.conclusion == 'success' && github.actor == 'dependabot[bot]'

    steps:
      - uses: ridedott/merge-me-action@v2
        with:
          GITHUB_TOKEN: ${{ secrets.AWBOT_GH_TOKEN }}


================================================
FILE: .github/workflows/diagram.yml
================================================
name: Diagram

on:
  workflow_dispatch: {}
  push:
    branches:
      - diagram
     #- master  # protected branch, can't push updated diagram to

jobs:
  update-diagram:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          submodules: recursive
      - name: Also checkout docs & website
        run: |
          git clone https://github.com/ActivityWatch/docs
          git clone https://github.com/ActivityWatch/activitywatch.github.io
      - name: Update diagram
        uses: githubocto/repo-visualizer@main
        with:
          commit_message: 'chore: update diagram [skip ci]'
          file_colors: '{"rs": "#b7410e", "py": "#229922", "rst": "pink", "txt": "pink", "md": "pink", "css": "purple", "scss": "purple"}'
          excluded_globs: "**/.github;**/.git;**/*.builds;**/*.bat;**/*.iss;**/*.ps1;**/*.pyi;**/*.plist;**/*.cmd"
          #excluded_paths: '.github'


================================================
FILE: .github/workflows/greetings.yml
================================================
name: Greetings

on: [issues, pull_request]

jobs:
  greeting:
    runs-on: ubuntu-latest
    if: github.repository_owner == 'ActivityWatch'  # don't run on forks
    steps:
    - uses: actions/first-interaction@v1
      with:
        repo-token: ${{ secrets.GITHUB_TOKEN }}
        issue-message: >
          Hi there!

          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!).
          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).

          Thanks a bunch for opening your first issue! 🙏
        pr-message: >
          Congratulations on opening your first pull request to this repo!

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

          Thanks for contributing!


================================================
FILE: .github/workflows/test.yml
================================================
name: Test

on:
  #push:
  #  branches: [ master ]
  #pull_request:
  #  branches: [ master ]
  workflow_dispatch:



jobs:
  # an integration test designed to catch bugs triggered by updating (database migrations and such)
  upgrades:
    name: upgrade from ${{ matrix.aw_server_old }} ${{ matrix.aw_server_old_args }} to ${{ matrix.aw_server_new }} ${{ matrix.aw_server_new_args }}
    # needs: [build]
    #if: startsWith(github.ref, 'refs/tags/v')  # only run on tag
    runs-on: ubuntu-latest
    env:
      old_version: 'v0.12.2'
      new_version: 'v0.13.1'
    strategy:
      fail-fast: false
      matrix:
        aw_server_old: ['aw-server', 'aw-server-rust']
        aw_server_new: ['aw-server', 'aw-server-rust']
        aw_server_old_args: ['']
        aw_server_new_args: ['']
        include:
          # python, peewee (default)
          - aw_server_old: 'aw-server'
            aw_server_new: 'aw-server'
          # python, sqlite
          # FIXME: sqlite broken since aw-server enabled flask multithreading (new default)
          - aw_server_old: "aw-server"
            aw_server_new: "aw-server"
            aw_server_old_args: "--storage sqlite"
            aw_server_new_args: "--storage sqlite"
            old_version: 'v0.12.2'
            new_version: 'v0.13.1'
          # python, peewee to sqlite
          # FIXME: broken, same thing with sqlite as above
          - aw_server_old: "aw-server"
            aw_server_new: "aw-server"
            aw_server_old_args: "--storage peewee"
            aw_server_new_args: "--storage sqlite"
            old_version: 'v0.12.2'
            new_version: 'v0.13.1'
        exclude:
          # rust to python, not supported
          - aw_server_old: 'aw-server-rust'
            aw_server_new: 'aw-server'

    steps:
      # Will download all artifacts to path
      - name: Download build artifacts
        if: ${{ env.new_version == 'this' }}
        uses: actions/download-artifact@v4
        with:
          name: builds-Linux-py3.9
          path: dist

      # Only used during testing, so we don't have to wait for the main build job
      - name: Download new ActivityWatch
        if: ${{ env.new_version != 'this' }}
        run: |
          mkdir dist
          pushd dist
          wget -q https://github.com/ActivityWatch/activitywatch/releases/download/${{ env.new_version }}/activitywatch-${{ env.new_version }}-linux-x86_64.zip

      - name: Install new & old ActivityWatch
        run: |
          pushd dist

          # New version
          unzip activitywatch-*-linux-x86_64.zip
          mv activitywatch/ aw-new

          # Old version
          wget -q -O aw-old.zip https://github.com/ActivityWatch/activitywatch/releases/download/${{ env.old_version }}/activitywatch-${{ env.old_version }}-linux-x86_64.zip
          unzip aw-old.zip
          mv activitywatch/ aw-old

      - name: Display structure of downloaded files
        run: ls -R
        working-directory: dist

      - name: Run and test old server
        run: |
          bin=dist/aw-old/${{ matrix.aw_server_old }}/${{ matrix.aw_server_old }}
          url="http://localhost:5600"

          # Check version
          $bin --version || true  # due to bug in old aw-server

          # Run server and log output
          $bin ${{ matrix.aw_server_old_args }} >> log-old.txt 2>&1 &
          sleep 5  # wait for startup

          # Set server URL

          # Get server info
          curl "$url/api/0/info" --fail-with-body

          # Create bucket
          curl -X 'POST' --fail-with-body \
            "$url/api/0/buckets/aw-test" \
            -H 'accept: application/json' \
            -H 'Content-Type: application/json' \
            -d '{
            "client": "test",
            "type": "test",
            "hostname": "test"
          }'

          # Get buckets
          curl "$url/api/0/buckets/" -H 'accept: application/json'

          # Send a heartbeat
          timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
          curl -X 'POST' \
            "$url/api/0/buckets/aw-test/heartbeat?pulsetime=0" \
            -H 'accept: application/json' \
            -H 'Content-Type: application/json' \
            -d '{
            "timestamp": "'$timestamp'",
            "duration": 0,
            "data": {"key": "test value"}
          }'

          # Give a sec, then kill server process
          sleep 1
          kill $!

      - name: Run and test new server
        run: |
          bin=dist/aw-new/${{ matrix.aw_server_new }}/${{ matrix.aw_server_new }}
          url="http://localhost:5600"

          # Check version
          $bin --version

          # Run server and log output
          $bin ${{ matrix.aw_server_new_args }} >> log-new.txt 2>&1 &
          sleep 5  # wait for startup

          # Get server info
          curl "$url/api/0/info"

          # Get buckets
          curl "$url/api/0/buckets/" -H 'accept: application/json'

          # Send a heartbeat
          timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
          curl -X 'POST' --fail-with-body \
            "$url/api/0/buckets/aw-test/heartbeat?pulsetime=60" \
            -H 'accept: application/json' \
            -H 'Content-Type: application/json' \
            -d '{
            "timestamp": "'$timestamp'",
            "duration": 0,
            "data": {"key": "test value"}
          }'

          # Give a sec, then kill server process
          sleep 1
          kill $!

      - name: Output logs
        if: always()
        run: |
          cat log-old.txt || true
          echo "\n---\n"
          cat log-new.txt || true



================================================
FILE: .github/workflows/winget.yml
================================================
name: Publish to WinGet
on:
  release:
    types: [released]
jobs:
  publish:
    runs-on: windows-latest # action can only be run on windows
    steps:
      - uses: vedantmgoyal2009/winget-releaser@v2
        with:
          identifier: ActivityWatch.ActivityWatch
          token: ${{ secrets.GH_TOKEN_WINGET_AUTOUPDATE }}
          fork-user: ActivityWatchBot


================================================
FILE: .gitignore
================================================
build
dist
docs
other
old

# Coverage
*coverage*
htmlcov

# Editor/IDEs
.idea
*.swp

# Python
*venv*
__pycache__
.python-version

# Misc
.*cache
.DS_Store


================================================
FILE: .gitmodules
================================================
[submodule "aw-core"]
	path = aw-core
	url = https://github.com/ActivityWatch/aw-core.git
[submodule "aw-client"]
	path = aw-client
	url = https://github.com/ActivityWatch/aw-client.git
[submodule "aw-server"]
	path = aw-server
	url = https://github.com/ActivityWatch/aw-server.git
[submodule "aw-watcher-afk"]
	path = aw-watcher-afk
	url = https://github.com/ActivityWatch/aw-watcher-afk.git
[submodule "aw-qt"]
	path = aw-qt
	url = https://github.com/ActivityWatch/aw-qt.git
[submodule "aw-watcher-window"]
	path = aw-watcher-window
	url = https://github.com/ActivityWatch/aw-watcher-window.git
[submodule "aw-server-rust"]
	path = aw-server-rust
	url = https://github.com/ActivityWatch/aw-server-rust.git
[submodule "aw-watcher-input"]
	path = aw-watcher-input
	url = https://github.com/ActivityWatch/aw-watcher-input.git
[submodule "aw-tauri"]
	path = aw-tauri
	url = https://github.com/activitywatch/aw-tauri
[submodule "awatcher"]
	path = awatcher
	url = https://github.com/2e3s/awatcher
[submodule "aw-notify"]
	path = aw-notify
	url = https://github.com/ActivityWatch/aw-notify-rs.git


================================================
FILE: .tool-versions
================================================
poetry 1.5.1
nodejs 16.20.2
rust nightly
python 3.9.13


================================================
FILE: CITATION.cff
================================================
cff-version: 1.2.0
message: "If you use or refer to this software in your research, please cite it."
authors:
- family-names: "Bjäreholt"
  given-names: "Erik"
  orcid: "https://orcid.org/0000-0003-1350-9677"
- family-names: "Bjäreholt"
  given-names: "Johan"
  orcid: "https://orcid.org/0000-0003-4789-3160"
title: "ActivityWatch"
version: 0.13.1
doi: 10.5281/zenodo.4957165
date-released: 2024-06-10
url: "https://github.com/ActivityWatch/activitywatch"


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In 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.

## Our Standards

Examples of behavior that contributes to creating a positive environment include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting

## Our Responsibilities

Project 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.

Project 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.

## Scope

This 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.

## Enforcement

Instances 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.

Project 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.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/


================================================
FILE: CONTRIBUTING.md
================================================
How to Contribute
=================

<!-- This guide could be improved by following the advice at https://mozillascience.github.io/working-open-workshop/contributing/ -->

**Table of Contents**

 - [Getting started](#getting-started)
 - [How you can help](#how-you-can-help)
 - [Filing an issue](#filing-an-issue)
 - [Code of Conduct](#code-of-conduct)
 - [Commit message guidelines](#commit-message-guidelines)
 - [Getting paid](#getting-paid)
 - [Claiming GitPOAP](#claiming-gitpoap)
 - [Questions?](#questions)


## Getting started

To 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).

You 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).

If 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).


## How you can help

There are many ways to contribute to ActivityWatch:

 - Work on issues labeled [`good first issue`][good first issue] or [`help wanted`][help wanted], these are especially suited for new contributors.
 - Fix [`bugs`][bugs].
 - Implement new features.
   - Look among the [requested features][requested features] on the forum.
   - Talk to us in the issues or on [our Discord server][discord] to get help on how to proceed.
 - Write [documentation](https://github.com/ActivityWatch/docs).
 - Build the ecosystem.
   - Examples: New watchers, tools to analyze data, tools to import data from other sources, etc.

If you're interested in what's next for ActivityWatch, have a look at our [roadmap][roadmap] and [milestones][milestones].

Most of the above will get you up on our [contributor stats page][contributors] as thanks!

[good first issue]: https://github.com/ActivityWatch/activitywatch/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22
[help wanted]: https://github.com/ActivityWatch/activitywatch/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22
[bugs]: https://github.com/ActivityWatch/activitywatch/issues?q=is%3Aissue+is%3Aopen+label%3A%22type%3A+bug%22
[milestones]: https://github.com/ActivityWatch/activitywatch/milestones
[roadmap]: https://github.com/orgs/ActivityWatch/projects/2
[requested features]: https://forum.activitywatch.net/c/features
[contributors]: http://activitywatch.net/contributors/


## Filing an issue

Thanks for wanting to help out with squashing bugs and more by filing an issue.

When 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!


## Code of Conduct

We 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).


## Commit message guidelines

When 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.

The format is: 

```
<type>[optional scope]: <description>

[optional body]

[optional footer]
```

Where `type` can be one of: `feat, fix, chore, ci, docs, style, refactor, perf, test`

Examples:

```
 - feat: added ability to sort by duration
 - fix: fixes incorrect week number (#407)
 - docs: improved query documentation 
```

This guideline was adopted in [issue #391](https://github.com/ActivityWatch/activitywatch/issues/391).


## Getting paid

We're experimenting with paying our contributors using funds we've raised from donations and grants. 

The 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.

If you've contributed to ActivityWatch (for a minimum of 10h) and want to get paid for your time, contact us!

You 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).


## Claiming GitPOAP

If 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

The one for 2022 looks like this:

<a href="https://www.gitpoap.io/gh/ActivityWatch/activitywatch">
  <img src="https://assets.poap.xyz/gitpoap-2022-activitywatch-contributor-2022-logo-1663695908409.png" width="256px">
</a>


## Questions?

If you have any questions, you can:

 - Talk to us on our [Discord server][discord]
 - Post on [the forum][forum] or [GitHub Discussions][github discussions].
 - (as a last resort/if needed) Email one of the maintainers at: [erik@bjareho.lt](mailto:erik@bjareho.lt)

[forum]: https://forum.activitywatch.net
[github discussions]: https://github.com/ActivityWatch/activitywatch/discussions
[discord]: https://discord.gg/vDskV9q


================================================
FILE: LICENSE.txt
================================================
Mozilla Public License Version 2.0
==================================

1. Definitions
--------------

1.1. "Contributor"
    means each individual or legal entity that creates, contributes to
    the creation of, or owns Covered Software.

1.2. "Contributor Version"
    means the combination of the Contributions of others (if any) used
    by a Contributor and that particular Contributor's Contribution.

1.3. "Contribution"
    means Covered Software of a particular Contributor.

1.4. "Covered Software"
    means Source Code Form to which the initial Contributor has attached
    the notice in Exhibit A, the Executable Form of such Source Code
    Form, and Modifications of such Source Code Form, in each case
    including portions thereof.

1.5. "Incompatible With Secondary Licenses"
    means

    (a) that the initial Contributor has attached the notice described
        in Exhibit B to the Covered Software; or

    (b) that the Covered Software was made available under the terms of
        version 1.1 or earlier of the License, but not also under the
        terms of a Secondary License.

1.6. "Executable Form"
    means any form of the work other than Source Code Form.

1.7. "Larger Work"
    means a work that combines Covered Software with other material, in
    a separate file or files, that is not Covered Software.

1.8. "License"
    means this document.

1.9. "Licensable"
    means having the right to grant, to the maximum extent possible,
    whether at the time of the initial grant or subsequently, any and
    all of the rights conveyed by this License.

1.10. "Modifications"
    means any of the following:

    (a) any file in Source Code Form that results from an addition to,
        deletion from, or modification of the contents of Covered
        Software; or

    (b) any new file in Source Code Form that contains any Covered
        Software.

1.11. "Patent Claims" of a Contributor
    means any patent claim(s), including without limitation, method,
    process, and apparatus claims, in any patent Licensable by such
    Contributor that would be infringed, but for the grant of the
    License, by the making, using, selling, offering for sale, having
    made, import, or transfer of either its Contributions or its
    Contributor Version.

1.12. "Secondary License"
    means either the GNU General Public License, Version 2.0, the GNU
    Lesser General Public License, Version 2.1, the GNU Affero General
    Public License, Version 3.0, or any later versions of those
    licenses.

1.13. "Source Code Form"
    means the form of the work preferred for making modifications.

1.14. "You" (or "Your")
    means an individual or a legal entity exercising rights under this
    License. For legal entities, "You" includes any entity that
    controls, is controlled by, or is under common control with You. For
    purposes of this definition, "control" means (a) the power, direct
    or indirect, to cause the direction or management of such entity,
    whether by contract or otherwise, or (b) ownership of more than
    fifty percent (50%) of the outstanding shares or beneficial
    ownership of such entity.

2. License Grants and Conditions
--------------------------------

2.1. Grants

Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:

(a) under intellectual property rights (other than patent or trademark)
    Licensable by such Contributor to use, reproduce, make available,
    modify, display, perform, distribute, and otherwise exploit its
    Contributions, either on an unmodified basis, with Modifications, or
    as part of a Larger Work; and

(b) under Patent Claims of such Contributor to make, use, sell, offer
    for sale, have made, import, and otherwise transfer either its
    Contributions or its Contributor Version.

2.2. Effective Date

The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.

2.3. Limitations on Grant Scope

The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:

(a) for any code that a Contributor has removed from Covered Software;
    or

(b) for infringements caused by: (i) Your and any other third party's
    modifications of Covered Software, or (ii) the combination of its
    Contributions with other software (except as part of its Contributor
    Version); or

(c) under Patent Claims infringed by Covered Software in the absence of
    its Contributions.

This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).

2.4. Subsequent Licenses

No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).

2.5. Representation

Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.

2.6. Fair Use

This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.

2.7. Conditions

Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.

3. Responsibilities
-------------------

3.1. Distribution of Source Form

All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.

3.2. Distribution of Executable Form

If You distribute Covered Software in Executable Form then:

(a) such Covered Software must also be made available in Source Code
    Form, as described in Section 3.1, and You must inform recipients of
    the Executable Form how they can obtain a copy of such Source Code
    Form by reasonable means in a timely manner, at a charge no more
    than the cost of distribution to the recipient; and

(b) You may distribute such Executable Form under the terms of this
    License, or sublicense it under different terms, provided that the
    license for the Executable Form does not attempt to limit or alter
    the recipients' rights in the Source Code Form under this License.

3.3. Distribution of a Larger Work

You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).

3.4. Notices

You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.

3.5. Application of Additional Terms

You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.

4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------

If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.

5. Termination
--------------

5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.

5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.

5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.

************************************************************************
*                                                                      *
*  6. Disclaimer of Warranty                                           *
*  -------------------------                                           *
*                                                                      *
*  Covered Software is provided under this License on an "as is"       *
*  basis, without warranty of any kind, either expressed, implied, or  *
*  statutory, including, without limitation, warranties that the       *
*  Covered Software is free of defects, merchantable, fit for a        *
*  particular purpose or non-infringing. The entire risk as to the     *
*  quality and performance of the Covered Software is with You.        *
*  Should any Covered Software prove defective in any respect, You     *
*  (not any Contributor) assume the cost of any necessary servicing,   *
*  repair, or correction. This disclaimer of warranty constitutes an   *
*  essential part of this License. No use of any Covered Software is   *
*  authorized under this License except under this disclaimer.         *
*                                                                      *
************************************************************************

************************************************************************
*                                                                      *
*  7. Limitation of Liability                                          *
*  --------------------------                                          *
*                                                                      *
*  Under no circumstances and under no legal theory, whether tort      *
*  (including negligence), contract, or otherwise, shall any           *
*  Contributor, or anyone who distributes Covered Software as          *
*  permitted above, be liable to You for any direct, indirect,         *
*  special, incidental, or consequential damages of any character      *
*  including, without limitation, damages for lost profits, loss of    *
*  goodwill, work stoppage, computer failure or malfunction, or any    *
*  and all other commercial damages or losses, even if such party      *
*  shall have been informed of the possibility of such damages. This   *
*  limitation of liability shall not apply to liability for death or   *
*  personal injury resulting from such party's negligence to the       *
*  extent applicable law prohibits such limitation. Some               *
*  jurisdictions do not allow the exclusion or limitation of           *
*  incidental or consequential damages, so this exclusion and          *
*  limitation may not apply to You.                                    *
*                                                                      *
************************************************************************

8. Litigation
-------------

Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.

9. Miscellaneous
----------------

This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.

10. Versions of the License
---------------------------

10.1. New Versions

Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.

10.2. Effect of New Versions

You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.

10.3. Modified Versions

If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).

10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses

If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.

Exhibit A - Source Code Form License Notice
-------------------------------------------

  This Source Code Form is subject to the terms of the Mozilla Public
  License, v. 2.0. If a copy of the MPL was not distributed with this
  file, You can obtain one at https://mozilla.org/MPL/2.0/.

If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.

You may add additional accurate notices of copyright ownership.

Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------

  This Source Code Form is "Incompatible With Secondary Licenses", as
  defined by the Mozilla Public License, v. 2.0.


================================================
FILE: Makefile
================================================
# =====================================
# Makefile for the ActivityWatch bundle
# =====================================
#
# [GUIDE] How to install from source:
#  - https://activitywatch.readthedocs.io/en/latest/installing-from-source.html
#
# We recommend creating and activating a Python virtualenv before building.
# Instructions on how to do this can be found in the guide linked above.
.PHONY: build install test clean clean_all

SHELL := /usr/bin/env bash

OS := $(shell uname -s)

ifeq ($(TAURI_BUILD),true)
	SUBMODULES := aw-core aw-client aw-server aw-server-rust aw-watcher-afk aw-watcher-window aw-tauri
	# Include awatcher on Linux (Wayland-compatible window watcher)
	ifeq ($(OS),Linux)
		SUBMODULES := $(SUBMODULES) awatcher
	endif
else
	SUBMODULES := aw-core aw-client aw-qt aw-server aw-server-rust aw-watcher-afk aw-watcher-window
endif

# Exclude aw-server-rust if SKIP_SERVER_RUST is true
ifeq ($(SKIP_SERVER_RUST),true)
	SUBMODULES := $(filter-out aw-server-rust,$(SUBMODULES))
endif
# Include extras if AW_EXTRAS is true
ifeq ($(AW_EXTRAS),true)
	SUBMODULES := $(SUBMODULES) aw-notify aw-watcher-input
endif

# A function that checks if a target exists in a Makefile
# Usage: $(call has_target,<dir>,<target>)
define has_target
$(shell make -q -C $1 $2 >/dev/null 2>&1; if [ $$? -eq 0 -o $$? -eq 1 ]; then echo $1; fi)
endef

# Submodules with test/package/lint/typecheck targets
TESTABLES := $(foreach dir,$(SUBMODULES),$(call has_target,$(dir),test))
PACKAGEABLES := $(foreach dir,$(SUBMODULES),$(call has_target,$(dir),package))
LINTABLES := $(foreach dir,$(SUBMODULES),$(call has_target,$(dir),lint))
TYPECHECKABLES := $(foreach dir,$(SUBMODULES),$(call has_target,$(dir),typecheck))

# When building with Tauri, aw-server-rust is built as aw-sync only (not full server),
# so exclude it from the standard package target
ifeq ($(TAURI_BUILD),true)
	PACKAGEABLES := $(filter-out aw-server-rust aw-server, $(PACKAGEABLES))
endif

# Build mode: release vs debug
ifeq ($(RELEASE), false)
	targetdir := debug
else
	targetdir := release
endif

# The `build` target
# ------------------
#
# What it does:
#  - Installs all the Python modules
#  - Builds the web UI and bundles it with aw-server
build: aw-core/.git
#	needed due to https://github.com/pypa/setuptools/issues/1963
#	would ordinarily be specified in pyproject.toml, but is not respected due to https://github.com/pypa/setuptools/issues/1963
	pip install 'setuptools>49.1.1'
	for module in $(SUBMODULES); do \
		echo "Building $$module"; \
		if [ "$$module" = "aw-server-rust" ] && [ "$(TAURI_BUILD)" = "true" ]; then \
			make --directory=$$module aw-sync SKIP_WEBUI=$(SKIP_WEBUI) || { echo "Error in $$module aw-sync"; exit 2; }; \
		else \
			make --directory=$$module build SKIP_WEBUI=$(SKIP_WEBUI) || { echo "Error in $$module build"; exit 2; }; \
		fi; \
	done
#   The below is needed due to: https://github.com/ActivityWatch/activitywatch/issues/173
	make --directory=aw-client build
	make --directory=aw-core build
#	Needed to ensure that the server has the correct version set
	python -c "import aw_server; print(aw_server.__version__)"


# Install
# -------
#
# Installs things like desktop/menu shortcuts.
# Might in the future configure autostart on the system.
ifneq ($(TAURI_BUILD),true)
install:
	make --directory=aw-qt install
# Installation is already happening in the `make build` step currently.
# We might want to change this.
# We should also add some option to install as user (pip3 install --user)
endif

# Update
# ------
#
# Pulls the latest version, updates all the submodules, then runs `make build`.
update:
	git pull
	git submodule update --init --recursive
	make build


lint:
	@for module in $(LINTABLES); do \
		echo "Linting $$module"; \
		make --directory=$$module lint || { echo "Error in $$module lint"; exit 2; }; \
	done

typecheck:
	@for module in $(TYPECHECKABLES); do \
		echo "Typechecking $$module"; \
		make --directory=$$module typecheck || { echo "Error in $$module typecheck"; exit 2; }; \
	done

# Uninstall
# ---------
#
# Uninstalls all the Python modules.
uninstall:
	modules=$$(pip3 list --format=legacy | grep 'aw-' | grep -o '^aw-[^ ]*'); \
	for module in $$modules; do \
		echo "Uninstalling $$module"; \
		pip3 uninstall -y $$module; \
	done

test:
	@for module in $(TESTABLES); do \
		echo "Running tests for $$module"; \
		poetry run make -C $$module test || { echo "Error in $$module tests"; exit 2; }; \
    done

test-integration:
	# TODO: Move "integration tests" to aw-client
	# FIXME: For whatever reason the script stalls on Appveyor
	#        Example: https://ci.appveyor.com/project/ErikBjare/activitywatch/build/1.0.167/job/k1ulexsc5ar5uv4v
	# aw-server-python
	@echo "== Integration testing aw-server =="
	@pytest ./scripts/tests/integration_tests.py ./aw-server/tests/ -v

%/.git:
	git submodule update --init --recursive

ifeq ($(TAURI_BUILD),true)
	ICON := "aw-tauri/src-tauri/icons/icon.png"
else
	ICON := "aw-qt/media/logo/logo.png"
endif

aw-qt/media/logo/logo.icns:
	mkdir -p build/MyIcon.iconset
	sips -z 16 16     $(ICON) --out build/MyIcon.iconset/icon_16x16.png
	sips -z 32 32     $(ICON) --out build/MyIcon.iconset/icon_16x16@2x.png
	sips -z 32 32     $(ICON) --out build/MyIcon.iconset/icon_32x32.png
	sips -z 64 64     $(ICON) --out build/MyIcon.iconset/icon_32x32@2x.png
	sips -z 128 128   $(ICON) --out build/MyIcon.iconset/icon_128x128.png
	sips -z 256 256   $(ICON) --out build/MyIcon.iconset/icon_128x128@2x.png
	sips -z 256 256   $(ICON) --out build/MyIcon.iconset/icon_256x256.png
	sips -z 512 512   $(ICON) --out build/MyIcon.iconset/icon_256x256@2x.png
	sips -z 512 512   $(ICON) --out build/MyIcon.iconset/icon_512x512.png
	cp				  $(ICON)       build/MyIcon.iconset/icon_512x512@2x.png
	iconutil -c icns build/MyIcon.iconset
	rm -R build/MyIcon.iconset
	mv build/MyIcon.icns aw-qt/media/logo/logo.icns

dist/ActivityWatch.app: aw-qt/media/logo/logo.icns
ifeq ($(TAURI_BUILD),true)
	scripts/package/build_app_tauri.sh
else
	pyinstaller --clean --noconfirm aw.spec
endif

dist/ActivityWatch.dmg: dist/ActivityWatch.app
	# NOTE: This does not codesign the dmg, that is done in the CI config
	pip install dmgbuild
	dmgbuild -s scripts/package/dmgbuild-settings.py -D app=dist/ActivityWatch.app "ActivityWatch" dist/ActivityWatch.dmg

dist/notarize:
	./scripts/notarize.sh

package:
	rm -rf dist
	mkdir -p dist/activitywatch
	for dir in $(PACKAGEABLES); do \
		make --directory=$$dir package; \
		cp -r $$dir/dist/$$dir dist/activitywatch; \
	done
ifeq ($(TAURI_BUILD),true)
# Copy aw-sync binary for Tauri builds
	mkdir -p dist/activitywatch/aw-server-rust
	cp aw-server-rust/target/$(targetdir)/aw-sync dist/activitywatch/aw-server-rust/aw-sync
else
# Move aw-qt to the root of the dist folder
	mv dist/activitywatch/aw-qt aw-qt-tmp
	mv aw-qt-tmp/* dist/activitywatch
	rmdir aw-qt-tmp
endif
# Remove problem-causing binaries
	rm -f dist/activitywatch/libdrm.so.2       # see: https://github.com/ActivityWatch/activitywatch/issues/161
	rm -f dist/activitywatch/libharfbuzz.so.0  # see: https://github.com/ActivityWatch/activitywatch/issues/660#issuecomment-959889230
# These should be provided by the distro itself
# Had to be removed due to otherwise causing the error:
#   aw-qt: symbol lookup error: /opt/activitywatch/libQt5XcbQpa.so.5: undefined symbol: FT_Get_Font_Format
	rm -f dist/activitywatch/libfontconfig.so.1
	rm -f dist/activitywatch/libfreetype.so.6
# Remove unnecessary files
	rm -rf dist/activitywatch/pytz
# Builds zips and setups
	bash scripts/package/package-all.sh

clean:
	rm -rf build dist

# Clean all subprojects
clean_all: clean
	for dir in $(SUBMODULES); do \
		make --directory=$$dir clean; \
	done

clean-auto:
	rm -rIv **/aw-server-rust/target
	rm -rIv **/aw-android/mobile/build
	rm -rIfv **/node_modules


================================================
FILE: README.md
================================================
<img title="ActivityWatch" src="https://activitywatch.net/img/banner.png" align="center">

<p align="center">
  <b>Records what you do</b> so that you can <i>know how you've spent your time</i>.
  <br>
  All in a secure way where <i>you control the data</i>.
</p>

<p align="center">
  <a href="https://twitter.com/ActivityWatchIt">
    <img title="Twitter follow" src="https://img.shields.io/twitter/follow/ActivityWatchIt.svg?style=social&label=Follow"/>
  </a>
  <a href="https://github.com/ActivityWatch/activitywatch">
    <img title="Star on GitHub" src="https://img.shields.io/github/stars/ActivityWatch/activitywatch.svg?style=social&label=Star">
  </a>

  <br>

  <b>
    <a href="https://activitywatch.net/">Website</a>
    — <a href="https://forum.activitywatch.net/">Forum</a>
    — <a href="https://docs.activitywatch.net">Documentation</a>
    — <a href="https://github.com/ActivityWatch/activitywatch/releases">Releases</a>
  </b>

  <br>

  <b>
    <a href="https://activitywatch.net/contributors/">Contributor stats</a>
    — <a href="https://activitywatch.net/ci/">CI overview</a>
  </b>
</p>

<p align="center">
  <a href="https://github.com/ActivityWatch/activitywatch/actions?query=branch%3Amaster">
    <img title="Build Status GitHub" src="https://github.com/ActivityWatch/activitywatch/workflows/Build/badge.svg?branch=master" />
  </a>
  <a href="https://ci.appveyor.com/project/ErikBjare/activitywatch">
    <img title="Build Status Appveyor" src="https://ci.appveyor.com/api/projects/status/vm7g9sdfi2vgix6n?svg=true" />
  </a>
  <a href="https://docs.activitywatch.net">
    <img title="Documentation" src="https://readthedocs.org/projects/activitywatch/badge/?version=latest" />
  </a>

  <br>

  <a href="https://github.com/ActivityWatch/activitywatch/releases">
    <img title="Latest release" src="https://img.shields.io/github/release-pre/ActivityWatch/activitywatch.svg">
  </a>
  <a href="https://github.com/ActivityWatch/activitywatch/releases">
    <img title="Total downloads (GitHub Releases)" src="https://img.shields.io/github/downloads/ActivityWatch/activitywatch/total.svg" />
  </a>
  <a href="https://discord.gg/vDskV9q">
    <img title="Discord" src="https://img.shields.io/discord/755040852727955476" />
  </a>

  <br>

  <a href="https://activitywatch.net/donate/">
    <img title="Donated" src="https://img.shields.io/badge/budget-%24201%2Fmo%20from%2040%20supporters-orange.svg" />
  </a>
  <a href="https://doi.org/10.5281/zenodo.4957165">
    <img src="https://zenodo.org/badge/DOI/10.5281/zenodo.4957165.svg" />
  </a>
</p>

<!--
# TODO: Best practices badge that we should work towards, see issue #42.
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/873/badge)](https://bestpractices.coreinfrastructure.org/projects/873)
[![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)
-->


*Do you want to receive email updates on major announcements?*<br>
***[Signup for the newsletter](http://eepurl.com/cTU6QX)!***

<details>
 <summary>Table of Contents</summary>

 * [About](#about)
    * [Screenshots](#screenshots)
    * [Is this yet another time tracker?](#is-this-yet-another-time-tracker)
       * [Feature comparison](#feature-comparison)
    * [Installation &amp; Usage](#installation--usage)
 * [About this repository](#about-this-repository)
    * [Server](#server)
    * [Watchers](#watchers)
    * [Libraries](#libraries)
 * [Contributing](#contributing)
</details>

## About

The goal of ActivityWatch is simple: *Enable the collection of as much valuable lifedata as possible without compromising user privacy.*

We'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:

 - Currently active application and the title of its window
 - Currently active browser tab and its title and URL
 - Keyboard and mouse activity, to detect if you are AFK ("away from keyboard") or not

It 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).

### Screenshots

<span><img src="https://activitywatch.net/img/screenshots/screenshot-v0.9.3-activity.png"   width="45%"></span>
<span><img src="https://activitywatch.net/img/screenshots/screenshot-v0.8.0b9-timeline.png" width="50%"></span>

You can find more (and newer) screenshots on [the website](https://activitywatch.net/screenshots/).


## Installation & Usage

Downloads are available on the [releases page](https://github.com/ActivityWatch/activitywatch/releases).

For instructions on how to get started, please see the [guide in the documentation](https://docs.activitywatch.net/en/latest/getting-started.html).

Interested in building from source? [There's a guide for that too](https://docs.activitywatch.net/en/latest/installing-from-source.html).

## Is this yet another time tracker?

Yes, but we found that most time trackers lack one or more important features.

**Common dealbreakers:**

 - Not open source
 - The user does not own the data (common with non-open source options)
 - Lack of synchronization (and when available: it's centralized and the sync server knows everything)
 - Difficult to setup/use (most open source options tend to target programmers)
 - Low data resolution (low level of detail, does not store raw data, long intervals between entries)
 - Hard or impossible to extend (collecting more data is not as simple as it could be)

**To sum it up:**

 - Closed source solutions suffer from privacy issues and limited features.
 - 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.

We have a plan to address all of these and we're well on our way. See the table below for our progress.


### Feature comparison

##### Basics

|               | User owns data     | GUI                | Sync                       | Open Source        |
| ------------- |:------------------:|:------------------:|:--------------------------:|:------------------:|
| ActivityWatch | :white_check_mark: | :white_check_mark: | [WIP][sync], decentralized | :white_check_mark: |
| [Selfspy]       | :white_check_mark: | :x:                | :x:                        | :white_check_mark: |
| [ulogme]        | :white_check_mark: | :white_check_mark: | :x:                        | :white_check_mark: |
| [RescueTime]    | :x:                | :white_check_mark: | Centralized                | :x:                |
| [WakaTime]      | :x:                | :white_check_mark: | Centralized                | Clients            |

[sync]: https://github.com/ActivityWatch/activitywatch/issues/35
[Selfspy]: https://github.com/selfspy/selfspy
[ulogme]: https://github.com/karpathy/ulogme
[RescueTime]: https://www.rescuetime.com/
[WakaTime]: https://wakatime.com/

##### Platforms
<!-- TODO: Replace Platform names with icons  -->

|               | Windows            | macOS              | Linux              | Android            | iOS                 |
| ------------- |:------------------:|:------------------:|:------------------:|:------------------:|:-------------------:|
| ActivityWatch | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |:x:                  |
| Selfspy       | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x:                |:x:                  |
| ulogme        | :x:                | :white_check_mark: | :white_check_mark: | :x:                |:x:                  |
| RescueTime    | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |Limited functionality|

##### Tracking

|               | App & Window Title | AFK                | Browser Extensions | Editor Plugins     | Extensible            |
| ------------- |:------------------:|:------------------:|:------------------:|:------------------:|:---------------------:|
| ActivityWatch | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark:    |
| Selfspy       | :white_check_mark: | :white_check_mark: | :x:                | :x:                | :x:                   |
| ulogme        | :white_check_mark: | :white_check_mark: | :x:                | :x:                | :x:                   |
| RescueTime    | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x:                | :x:                   |
| WakaTime      | :x:                | :white_check_mark: | :white_check_mark: | :white_check_mark: | Only for text editors |

For 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).


## Architecture

```mermaid
graph TD;
  aw-qt[<a href='https://github.com/ActivityWatch/aw-qt'>aw-qt</a>];
  aw-notify[<a href='https://github.com/ActivityWatch/aw-notify-rs'>aw-notify</a>];
  aw-server[<a href='https://github.com/ActivityWatch/aw-server'>aw-server</a>];
  aw-webui[<a href='https://github.com/ActivityWatch/aw-webui'>aw-webui</a>];
  aw-watcher-window[<a href='https://github.com/ActivityWatch/aw-watcher-window'>aw-watcher-window</a>];
  aw-watcher-afk[<a href='https://github.com/ActivityWatch/aw-watcher-afk'>aw-watcher-afk</a>];
  aw-watcher-web[<a href='https://github.com/ActivityWatch/aw-watcher-web'>aw-watcher-web</a>];
  aw-sync[<a href='https://github.com/ActivityWatch/aw-server-rust/tree/master/aw-sync'>aw-sync</a>];

  aw-qt -- Manages --> aw-server;
  aw-qt -- Manages --> aw-notify -- Queries --> aw-server;
  aw-qt -- Manages --> aw-watcher-window -- Watches --> S1[Active window] -- Heartbeats --> aw-server;
  aw-qt -- Manages --> aw-watcher-afk -- Watches --> S2[AFK status] -- Heartbeats --> aw-server;
  Browser -- Manages --> aw-watcher-web -- Watches --> S3[Active tab] -- Heartbeats --> aw-server;
  SF -- Dropbox/Syncthing/etc --> SF;
  aw-server <-- Push/Pull --> aw-sync <-- Read/Write --> SF[Sync folder];
  aw-server -- Serves --> aw-webui -- Queries --> aw-server;

  %% User -- Interacts --> aw-webui;
  %% User -- Observes --> aw-notify;
  %% User -- Interacts --> aw-qt;

classDef lightMode fill:#FFFFFF, stroke:#333333, color:#333333;
classDef darkMode fill:#333333, stroke:#FFFFFF, color:#FFFFFF;

classDef lightModeLinks stroke:#333333;
classDef darkModeLinks stroke:#FFFFFF;

class A,B,C,D,E,G lightMode;
class A,B,C,D,E,G darkMode;

%% linkStyle 0 stroke:#FF4136, stroke-width:2px;
%% linkStyle 1 stroke:#1ABC9C, stroke-width:2px;
```

## About this repository

This 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)).

### Server

ActivityWatch has two server implementations:

- `aw-server` (Python) - The current default implementation
- `aw-server-rust` - A Rust implementation that is the planned future default

Both 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).

The REST API includes:

 - Access to a datastore suitable for timeseries/timeperiod-data organized in "buckets" (containers grouping related activity data by metadata like client type or hostname)
 - **Buckets API:** Create, retrieve, and delete data buckets
 - **Events API:** Read and write timestamped events within buckets
 - **Heartbeat API:** Watchers use heartbeat signals to update the current state of activity (e.g., active application, AFK status)
 - **Query API:** simple query scripting language for filtering, merging, grouping, and transforming events
 - **Client libraries:** Language-specific libraries like `aw-client` (Python), `aw-client-js`, and `aw-client-rust` that wrap REST endpoints for programmatic access

The frontend (`aw-webui`) includes:

 - **Data visualization:** Dashboard and timeline views showing activity summaries with detailed breakdowns of app usage, web browsing, and user-defined categories
 - **Query explorer:** Browser-based interface for writing, executing, and debugging queries with real-time results
 - **Activity browser:** Navigate through historical data with filtering by date ranges, applications, websites, and custom categories
 - **Raw data access:** View and browse individual events from all tracking buckets with detailed metadata
 - **Export functionality:** Export activity data in JSON format (individual buckets or complete datasets) via web interface or REST API

### Watchers

ActivityWatch comes pre-installed with two watchers:

 - `aw-watcher-afk` tracks the user active/inactive state from keyboard and mouse input
 - `aw-watcher-window` tracks the currently active application and its window title.

There 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).

### Libraries

 - `aw-core` - core library, provides no runnable modules
 - `aw-client` - client library, useful when writing watchers

### Folder structure

<span><img src="https://raw.githubusercontent.com/ActivityWatch/activitywatch/master/diagram.svg" width="60%"></span>

## Contributing

Want to help? Great! Check out the [CONTRIBUTING.md file](./CONTRIBUTING.md)!

## Questions and support

Have a question, suggestion, problem, or just want to say hi? Post on [the forum](https://forum.activitywatch.net/)!



================================================
FILE: SECURITY.md
================================================
# Security Policy

<!--
## Supported Versions

Use this section to tell people about which versions of your project are
currently being supported with security updates.

| Version    | Supported          |
| ---------- | ------------------ |
| 0.11.0     | :white_check_mark: |
| <= 0.10.0  | :x:                |
-->

## Reporting a Vulnerability

<!--
Use this section to tell people how to report a vulnerability.

Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
-->

If 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)).


================================================
FILE: aw.spec
================================================
# -*- mode: python -*-
# vi: set ft=python :

import os
import platform
import shlex
import subprocess
from pathlib import Path

import aw_core
import flask_restx


def build_analysis(name, location, binaries=[], datas=[], hiddenimports=[]):
    name_py = name.replace("-", "_")
    location_candidates = [
        location / f"{name_py}/__main__.py",
        location / f"src/{name_py}/__main__.py",
    ]
    try:
        location = next(p for p in location_candidates if p.exists())
    except StopIteration:
        raise Exception(f"Could not find {name} location from {location_candidates}")

    return Analysis(
        [location],
        pathex=[],
        binaries=binaries,
        datas=datas,
        hiddenimports=hiddenimports,
        hookspath=[],
        runtime_hooks=[],
        excludes=[],
        win_no_prefer_redirects=False,
        win_private_assemblies=False,
    )


def build_collect(analysis, name, console=True):
    """Used to build the COLLECT statements for each module"""
    pyz = PYZ(analysis.pure, analysis.zipped_data)
    exe = EXE(
        pyz,
        analysis.scripts,
        exclude_binaries=True,
        name=name,
        debug=False,
        strip=False,
        upx=True,
        console=console,
        contents_directory=".",
        entitlements_file=entitlements_file,
        codesign_identity=codesign_identity,
    )
    return COLLECT(
        exe,
        analysis.binaries,
        analysis.zipfiles,
        analysis.datas,
        strip=False,
        upx=True,
        name=name,
    )


# Get the current release version
current_release = subprocess.run(
    shlex.split("git describe --tags --abbrev=0"),
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    encoding="utf8",
).stdout.strip()
print("bundling activitywatch version " + current_release)

# Get entitlements and codesign identity
entitlements_file = Path(".") / "scripts" / "package" / "entitlements.plist"
codesign_identity = os.environ.get("APPLE_PERSONALID", "").strip()
if not codesign_identity:
    print("Environment variable APPLE_PERSONALID not set. Releases won't be signed.")

aw_core_path = Path(os.path.dirname(aw_core.__file__))
restx_path = Path(os.path.dirname(flask_restx.__file__))

aws_location = Path("aw-server")
aw_server_rust_location = Path("aw-server-rust")
aw_server_rust_bin = aw_server_rust_location / "target/package/aw-server-rust"
aw_sync_bin = aw_server_rust_location / "target/package/aw-sync"
aw_qt_location = Path("aw-qt")
awa_location = Path("aw-watcher-afk")
aww_location = Path("aw-watcher-window")
awi_location = Path("aw-watcher-input")
aw_notify_location = Path("aw-notify")

if platform.system() == "Darwin":
    icon = aw_qt_location / "media/logo/logo.icns"
else:
    icon = aw_qt_location / "media/logo/logo.ico"

skip_rust = False
if not aw_server_rust_bin.exists():
    skip_rust = True
    print("Skipping Rust build because aw-server-rust binary not found.")


aw_qt_a = build_analysis(
    "aw-qt",
    aw_qt_location,
    binaries=[(aw_server_rust_bin, "."), (aw_sync_bin, ".")] if not skip_rust else [],
    datas=[
        (aw_qt_location / "resources/aw-qt.desktop", "aw_qt/resources"),
        (aw_qt_location / "media", "aw_qt/media"),
    ],
)
aw_server_a = build_analysis(
    "aw-server",
    aws_location,
    datas=[
        (aws_location / "aw_server/static", "aw_server/static"),
        (restx_path / "templates", "flask_restx/templates"),
        (restx_path / "static", "flask_restx/static"),
        (aw_core_path / "schemas", "aw_core/schemas"),
    ],
)
aw_watcher_afk_a = build_analysis(
    "aw_watcher_afk",
    awa_location,
    hiddenimports=[
        "Xlib.keysymdef.miscellany",
        "Xlib.keysymdef.latin1",
        "Xlib.keysymdef.latin2",
        "Xlib.keysymdef.latin3",
        "Xlib.keysymdef.latin4",
        "Xlib.keysymdef.greek",
        "Xlib.support.unix_connect",
        "Xlib.ext.shape",
        "Xlib.ext.xinerama",
        "Xlib.ext.composite",
        "Xlib.ext.randr",
        "Xlib.ext.xfixes",
        "Xlib.ext.security",
        "Xlib.ext.xinput",
        "pynput.keyboard._xorg",
        "pynput.mouse._xorg",
        "pynput.keyboard._win32",
        "pynput.mouse._win32",
        "pynput.keyboard._darwin",
        "pynput.mouse._darwin",
    ],
)
aw_watcher_input_a = build_analysis("aw_watcher_input", awi_location)
aw_watcher_window_a = build_analysis(
    "aw_watcher_window",
    aww_location,
    binaries=(
        [
            (
                aww_location / "aw_watcher_window/aw-watcher-window-macos",
                "aw_watcher_window",
            )
        ]
        if platform.system() == "Darwin"
        else []
    ),
    datas=[
        (aww_location / "aw_watcher_window/printAppStatus.jxa", "aw_watcher_window")
    ],
)
# Check if aw-notify is a Python package
_notify_candidates = [
    aw_notify_location / "aw_notify/__main__.py",
    aw_notify_location / "src/aw_notify/__main__.py",
]
skip_aw_notify = not any(p.exists() for p in _notify_candidates)
if skip_aw_notify:
    print("Skipping aw-notify Python packaging (Rust-based implementation detected)")

aw_notify_a = None if skip_aw_notify else build_analysis(
    "aw_notify", aw_notify_location, hiddenimports=["desktop_notifier.resources"]
)

# https://pythonhosted.org/PyInstaller/spec-files.html#multipackage-bundles
# MERGE takes a bit weird arguments, it wants tuples which consists of
# the analysis paired with the script name and the bin name
merge_args = [
    (aw_server_a, "aw-server", "aw-server"),
    (aw_qt_a, "aw-qt", "aw-qt"),
    (aw_watcher_afk_a, "aw-watcher-afk", "aw-watcher-afk"),
    (aw_watcher_window_a, "aw-watcher-window", "aw-watcher-window"),
    (aw_watcher_input_a, "aw-watcher-input", "aw-watcher-input"),
]
if aw_notify_a is not None:
    merge_args.append((aw_notify_a, "aw-notify", "aw-notify"))

MERGE(*merge_args)


# aw-server
aws_coll = build_collect(aw_server_a, "aw-server")

# aw-watcher-window
aww_coll = build_collect(aw_watcher_window_a, "aw-watcher-window")

# aw-watcher-afk
awa_coll = build_collect(aw_watcher_afk_a, "aw-watcher-afk")

# aw-qt
awq_coll = build_collect(
    aw_qt_a,
    "aw-qt",
    console=False if platform.system() == "Windows" else True,
)

# aw-watcher-input
awi_coll = build_collect(aw_watcher_input_a, "aw-watcher-input")

# aw-notify (only if Python package exists)
aw_notify_coll = build_collect(aw_notify_a, "aw-notify") if aw_notify_a is not None else None

if platform.system() == "Darwin":
    bundle_args = [
        awq_coll,
        aws_coll,
        aww_coll,
        awa_coll,
        awi_coll,
    ]
    if aw_notify_coll is not None:
        bundle_args.append(aw_notify_coll)
    
    app = BUNDLE(
        *bundle_args,
        name="ActivityWatch.app",
        icon=icon,
        bundle_identifier="net.activitywatch.ActivityWatch",
        version=current_release.lstrip("v"),
        info_plist={
            "NSPrincipalClass": "NSApplication",
            "CFBundleExecutable": "MacOS/aw-qt",
            "CFBundleIconFile": "logo.icns",
            "NSAppleEventsUsageDescription": "Please grant access to use Apple Events",
            # This could be set to a more specific version string (including the commit id, for example)
            "CFBundleVersion": current_release.lstrip("v"),
            # Replaced by the 'version' kwarg above
            # "CFBundleShortVersionString": current_release.lstrip('v'),
        },
    )


================================================
FILE: gptme.toml
================================================
files = [
    "README.md",
    "Makefile",
    "aw-server/README.md",
    "aw-server/aw-webui/README.md",
    "aw-server-rust/README.md",
    "aw-server-rust/aw-sync/README.md",
    "aw-client/README.md",
    # ideally we'd also include some of the docs here, but they are not a submodule
]


================================================
FILE: pyproject.toml
================================================
[tool.poetry]
name = "activitywatch"
version = "0.13.2"
description = "The free and open-source automated time tracker. Cross-platform, extensible, privacy-focused."
authors = ["Erik Bjäreholt <erik@bjareho.lt>", "Johan Bjäreholt <johan@bjareho.lt>"]
license = "MPL-2.0"

[tool.poetry.dependencies]
python = "^3.8"
# Installing them from here won't work
#aw-core = {path = "aw-core"}
#aw-client = {path = "aw-client"}
#aw-watcher-afk = {path = "aw-watcher-afk"}
#aw-watcher-window = {path = "aw-watcher-window"}
#aw-server = {path = "aw-server"}
#aw-qt = {path = "aw-qt"}

# https://github.com/ionrock/cachecontrol/issues/292
urllib3 = "<2"

[tool.poetry.dev-dependencies]
mypy = "*"
pytest = "*"
pytest-cov = "*"
pytest-benchmark = "*"
psutil = "*"
pywin32-ctypes = {version = "*", platform = "win32"}
pefile = {version = "*", platform = "win32"}

pyinstaller = {version = "*", python = "^3.8,<3.14"}
# releases are very infrequent, so good idea to use the master branch
# we need this unreleased commit: https://github.com/pyinstaller/pyinstaller-hooks-contrib/commit/0f40dc6e74086e5472aee75070b9077b4c17ab18
pyinstaller-hooks-contrib = {git = "https://github.com/pyinstaller/pyinstaller-hooks-contrib.git", branch="master"}

# Won't be respected due to https://github.com/python-poetry/poetry/issues/1584
#setuptools = ">49.1.1"  # needed due to https://github.com/pypa/setuptools/issues/1963

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"


================================================
FILE: scripts/build_changelog.py
================================================
#!/usr/bin/env python3
"""
Script that generates a changelog for the repository and its submodules, and outputs it in the current directory.

NOTE: This script can be downloaded as-is and run from your repository.

Repos using this script:
 - ActivityWatch/activitywatch
 - ErikBjare/gptme

Manual actions needed to clean up for changelog:
 - Reorder modules in a logical order (aw-webui, aw-server, aw-server-rust, aw-watcher-window, aw-watcher-afk, ...)
 - Remove duplicate aw-webui entries
"""

import argparse
import logging
import os
import re
import shlex
from collections import defaultdict
from collections.abc import Collection
from dataclasses import dataclass
from pathlib import Path
from subprocess import PIPE, STDOUT
from subprocess import run as _run
from time import sleep
from typing import (
    Dict,
    List,
    Optional,
    Tuple,
)

import requests

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)


script_dir = Path(__file__).parent.resolve()


def main():
    parser = argparse.ArgumentParser(description="Generate changelog from git history")

    # repo info
    parser.add_argument("--org", default="ActivityWatch", help="GitHub organization")
    parser.add_argument("--repo", default="activitywatch", help="GitHub repository")
    parser.add_argument(
        "--project-title", default="ActivityWatch", help="Project title"
    )

    # settings
    last_tag = run("git describe --tags --abbrev=0").strip()  # get latest tag
    branch = run("git rev-parse --abbrev-ref HEAD").strip()  # get current branch name
    parser.add_argument(
        "--range", default=f"{last_tag}...{branch}", help="Git commit range"
    )
    parser.add_argument("--path", default=".", help="Path to git repo")

    # output
    parser.add_argument(
        "--output", default="changelog.md", help="Path to output changelog"
    )
    parser.add_argument(
        "--add-version-header",
        action="store_true",
        help="Add version header and adjust heading levels for docs",
    )

    # parse args
    args = parser.parse_args()
    since, until = args.range.split("...", 1)

    # preferred output order for submodules
    repo_order = [
        "activitywatch",
        "aw-server",
        "aw-server-rust",
        "aw-webui",
        "aw-watcher-afk",
        "aw-watcher-window",
        "aw-qt",
        "aw-core",
        "aw-client",
    ]

    build(
        args.org,
        args.repo,
        args.project_title,
        commit_range=(since, until),
        output_path=args.output,
        repo_order=repo_order,
        add_version_header=args.add_version_header,
    )


class CommitMsg:
    type: str
    subtype: str
    msg: str


@dataclass
class Commit:
    id: str
    msg: str
    org: str
    repo: str

    @property
    def msg_processed(self) -> str:
        """Generates links from commit and issue references (like 0c14d77, #123) to correct repo and such"""
        s = self.msg
        s = re.sub(
            rf"[^(-]https://github.com/{self.org}/([\-\w\d]+)/(issues|pulls)/(\d+)",
            rf"[#\3](https://github.com/{self.org}/\1/issues/\3)",
            s,
        )
        s = re.sub(
            r"#(\d+)",
            rf"[#\1](https://github.com/{self.org}/{self.repo}/issues/\1)",
            s,
        )
        s = re.sub(
            r"[\s\(][0-9a-f]{7}[\s\)]",
            rf"[`\0`](https://github.com/{self.org}/{self.repo}/issues/\0)",
            s,
        )
        # wrap html elements in backticks, if not already wrapped
        s = re.sub(r"(?<!`)<([^>]+)>(?!`)", r"`<\1>`", s)
        return s

    def parse_type(self) -> Optional[Tuple[str, str]]:
        # Needs to handle '!' indicating breaking change
        match = re.search(r"^(\w+)(\((.+)\))?[!]?:", self.msg)
        if match:
            type = match.group(1)
            subtype = match.group(3)
            if type in ["build", "ci", "fix", "feat"]:
                return type, subtype
        return None

    @property
    def type(self) -> Optional[str]:
        _type, _ = self.parse_type() or (None, None)
        return _type

    @property
    def subtype(self) -> Optional[str]:
        _, subtype = self.parse_type() or (None, None)
        return subtype

    def type_str(self) -> str:
        _type, subtype = self.parse_type() or (None, None)
        return f"{_type}" + (f"({subtype})" if subtype else "")

    def format(self) -> str:
        commit_link = commit_linkify(self.id, self.org, self.repo) if self.id else ""

        return f"{self.msg_processed}" + (f" ({commit_link})" if commit_link else "")


def run(cmd, cwd=".") -> str:
    logger.debug(f"Running in {cwd}: {cmd}")
    p = _run(shlex.split(cmd), stdout=PIPE, stderr=STDOUT, encoding="utf8", cwd=cwd)
    if p.returncode != 0:
        print(p.stdout)
        print(p.stderr)
        raise Exception
    return p.stdout


def pr_linkify(prid: str, org: str, repo: str) -> str:
    return f"[#{prid}](https://github.com/{org}/{repo}/pulls/{prid})"


def commit_linkify(commitid: str, org: str, repo: str) -> str:
    return f"[`{commitid}`](https://github.com/{org}/{repo}/commit/{commitid})"


def wrap_details(title, body, wraplines=5):
    """Wrap lines into a <details> element if body is longer than `wraplines`"""
    out = f"\n\n### {title}"
    wrap = body.strip().count("\n") > wraplines
    if wrap:
        out += "\n<details><summary>Click to expand</summary>\n<p>"
    out += f"\n{body.rstrip()}"
    if wrap:
        out += "\n\n</p>\n</details>"
    return out


contributor_emails = set()


def summary_repo(
    org: str,
    repo: str,
    path: str,
    commit_range: Tuple[str, str],
    filter_types: List[str],
    repo_order: List[str],
) -> str:
    if commit_range[1] == "0000000":
        # Happens when a submodule has been removed
        return ""
    if commit_range[0] == "0000000":
        # Happens when a submodule has been added
        commit_range = ("", "")  # no range = all commits for new submodule

    out = f"\n## 📦 {repo}"

    feats = ""
    fixes = ""
    misc = ""
    hidden = 0

    # pretty format is modified version of: https://stackoverflow.com/a/1441062/965332
    summary_bundle = run(
        f"git log {'...'.join(commit_range) if any(commit_range) else ''} --no-decorate --pretty=format:'%h%x09%an%x09%ae%x09%s'",
        cwd=path,
    )
    print(f"Found {len(summary_bundle.splitlines())} commits in {repo}")
    for line in summary_bundle.split("\n"):
        if line:
            _id, _author, email, msg = line.split("\t")
            # will add author email to contributor list
            # the `contributor_emails` is global and collected later
            contributor_emails.add(email)
            commit = Commit(id=_id, msg=msg, org=org, repo=repo)

            entry = f"\n - {commit.format()}"
            if commit.type == "feat":
                feats += entry
            elif commit.type == "fix":
                fixes += entry
            elif commit.type not in filter_types:
                misc += entry
            else:
                hidden += 1

    for name, entries in (
        ("✨ Features", feats),
        ("🐛 Fixes", fixes),
        ("🔨 Misc", misc),
    ):
        if entries:
            _count = len(entries.strip().split("\n"))
            title = f"{name} ({_count})"
            if "Misc" in name or "Fixes" in name:
                out += wrap_details(title, entries)
            else:
                out += f"\n\n### {title}\n"
                out += entries
    if hidden > 1:
        full_history_url = f"https://github.com/{org}/{repo}/compare/{commit_range[0]}...{commit_range[1]}"
        out += f"\n\n*(excluded {hidden} less relevant [commits]({full_history_url}))*"

    # NOTE: For now, these TODOs can be manually fixed for each changelog.
    # TODO: Fix issue where subsubmodules can appear twice (like aw-webui)
    # TODO: Use specific order (aw-webui should be one of the first, for example)
    summary_subrepos = run(
        f"git submodule summary --cached {commit_range[0]}", cwd=path
    )
    subrepos = {}
    for header, *_ in [s.split("\n") for s in summary_subrepos.split("\n\n")]:
        if header.startswith("fatal: not a git repository"):
            # Happens when a submodule has been removed
            continue
        if header.strip():
            if len(header.split(" ")) < 4:
                # Submodule may have been deleted
                continue

            _, name, crange, count = header.split(" ")
            commit_range = tuple(crange.split("...", 1))  # type: ignore
            count = count.strip().lstrip("(").rstrip("):")
            logger.info(
                f"Found {name}, looking up range: {commit_range} ({count} commits)"
            )
            name = name.strip(".").strip("/")

            subrepos[name] = summary_repo(
                org,
                name,
                f"{path}/{name}",
                commit_range,
                filter_types=filter_types,
                repo_order=repo_order,
            )

    # filter out subrepos with no commits (single line after stripping whitespace)
    subrepos = {
        name: output
        for name, output in subrepos.items()
        if len(output.strip().splitlines()) > 1
    }

    # pick subrepos in repo_order, and remove from dict
    for name in repo_order:
        if name in subrepos:
            out += "\n"
            out += subrepos[name]
            logger.info(f"{name:12} length: \t{len(subrepos[name])}")
            del subrepos[name]

    # add remaining repos
    for output in subrepos.values():
        out += "\n"
        out += output

    return out


# FIXME: Doesn't work, messy af, just gonna have to remove the aw-webui section by hand
def remove_duplicates(s: List[str], minlen=10, only_sections=True) -> List[str]:
    """
    Removes the longest sequence of repeated elements (they don't have to be adjacent), if sequence if longer than `minlen`.
    Preserves order of elements.
    """
    if len(s) < minlen:
        return s
    out = []
    longest: List[str] = []
    for i in range(len(s)):
        if i == 0 or s[i] not in out:
            # Not matching any previous line,
            # so add longest and new line to output, and reset longest
            if len(longest) < minlen:
                out.extend(longest)
            else:
                duplicate = "\n".join(longest)
                print(f"Removing duplicate '{duplicate[:80]}...'")
            out.append(s[i])
            longest = []
        else:
            # Matches a previous line, so add to longest
            # If longest is empty and only_sections is True, check that the line is a section start
            if only_sections:
                if not longest and s[i].startswith("#"):
                    longest.append(s[i])
                else:
                    out.append(s[i])
            else:
                longest.append(s[i])

    return out


def build(
    org: str,
    repo: str,
    project_name: str,
    commit_range: Tuple[str, str],
    output_path: str,
    repo_order: List[str],
    filter_types: Optional[List[str]] = None,
    add_version_header: bool = False,
):
    # provides a commit summary for the repo and subrepos, recursively looking up subrepos
    # NOTE: this must be done *before* `get_all_contributors` is called,
    #       as the latter relies on summary_repo looking up all users and storing in a global.
    if not filter_types:
        filter_types = ["build", "ci", "tests", "test"]

    logger.info("Generating commit summary")
    since, tag = commit_range
    output_changelog = summary_repo(
        org,
        repo,
        ".",
        commit_range=commit_range,
        filter_types=filter_types,
        repo_order=repo_order,
    )

    output_changelog = f"""
# Changelog

Changes since {since}:

{output_changelog}
    """.strip()

    # Would ideally sort by number of commits or something, but that's tricky
    usernames = sorted(get_all_contributors(), key=str.casefold)
    usernames = [u for u in usernames if not u.endswith("[bot]")]
    twitter_handles = get_twitter_of_ghusers(usernames)
    print(
        "Twitter handles: "
        + ", ".join("@" + handle for handle in twitter_handles.values() if handle),
    )

    output_contributors = f"""# Contributors

Thanks to everyone who contributed to this release:

{', '.join(('@' + username for username in usernames))}"""

    # Header starts here
    logger.info("Building final output")
    output = f"These are the release notes for {project_name} version {tag}.".strip()
    output += "\n\n"

    # hardcoded for now
    if repo == "activitywatch":
        output += "**New to ActivityWatch?** Check out the [website](https://activitywatch.net) and the [README](https://github.com/ActivityWatch/activitywatch/blob/master/README.md)."
        output += "\n\n"
        output += """# Installation

See the [getting started guide in the documentation](https://docs.activitywatch.net/en/latest/getting-started.html).
        """.strip()
        output += "\n\n"
        output += f"""# Downloads

 - [**Windows**](https://github.com/ActivityWatch/activitywatch/releases/download/{tag}/activitywatch-{tag}-windows-x86_64-setup.exe) (.exe, installer)
 - [**macOS**](https://github.com/ActivityWatch/activitywatch/releases/download/{tag}/activitywatch-{tag}-macos-x86_64.dmg) (.dmg)
 - [**Linux**](https://github.com/ActivityWatch/activitywatch/releases/download/{tag}/activitywatch-{tag}-linux-x86_64.zip) (.zip)
     """.strip()
        output += "\n\n"

    output += output_contributors.strip() + "\n\n"
    output += output_changelog.strip() + "\n\n"
    output += (
        f"**Full Changelog**: https://github.com/{org}/{repo}/compare/{since}...{tag}"
    )

    if repo == "activitywatch":
        output = output.replace("# activitywatch", "# activitywatch (bundle repo)")

    if add_version_header:
        output = f"# {tag}\n\n" + output
        output = output.replace("\n# Contributors\n", "\n## Contributors\n")
        output = output.replace("\n# Changelog\n", "\n## Changelog\n")

    with open(output_path, "w") as f:
        f.write(output)
    print(f"Wrote {len(output.splitlines())} lines to {output_path}")


def _resolve_email(email: str) -> Optional[str]:
    if "users.noreply.github.com" in email:
        username = email.split("@")[0]
        if "+" in username:
            username = username.split("+")[1]
        # TODO: Verify username is valid using the GitHub API
        print(f"Contributor: @{username}")
        return username
    else:
        resp = None
        backoff = 0
        max_backoff = 2
        while resp is None:
            if backoff >= max_backoff:
                logger.warning(f"Backed off {max_backoff} times, giving up")
                break
            try:
                logger.info(f"Sending request for {email}")
                _resp = requests.get(
                    f"https://api.github.com/search/users?q={email}+in%3Aemail"
                )
                _resp.raise_for_status()
                resp = _resp
                backoff = 0
            # if rate limit exceeded, back off
            except requests.exceptions.RequestException as e:
                if isinstance(e, requests.exceptions.HTTPError):
                    if e.response.status_code == 403:
                        logger.warning("Rate limit exceeded, backing off...")
                        backoff += 1
                        sleep(3)
                        continue
                else:
                    raise e
            finally:
                # Just to respect API limits...
                sleep(1)

        if resp:
            data = resp.json()
            if data["total_count"] == 0:
                logger.info(f"No match for email: {email}")
            if data["total_count"] > 1:
                logger.warning(f"Multiple matches for email: {email}")
            if data["total_count"] >= 1:
                username = data["items"][0]["login"]
                logger.info(f"Contributor: @{username}  (by email: {email})")
                return username
    return None


def get_all_contributors() -> set[str]:
    # TODO: Merge with contributor-stats?
    logger.info("Getting all contributors")

    # We will commit this file, to act as a cache (preventing us from querying GitHub API every time)
    filename = script_dir / "changelog_contributors.csv"

    # mapping from username to one or more emails
    usernames: Dict[str, set] = defaultdict(set)

    # some hardcoded ones, some that don't resolve...
    usernames["erikbjare"] |= {"erik.bjareholt@gmail.com", "erik@bjareho.lt"}
    usernames["iloveitaly"] |= {"iloveitaly@gmail.com"}
    usernames["kewde"] |= {"kewde@particl.io"}
    usernames["victorwinberg"] |= {"victor.m.winberg@gmail.com"}
    usernames["NicoWeio"] |= {"nico.weio@gmail.com"}
    usernames["2e3s"] |= {"2e3s19@gmail.com"}
    usernames["alwinator"] |= {"accounts@alwinschuster.at"}

    # read existing contributors, to avoid extra calls to the GitHub API
    if os.path.exists(filename):
        with open(filename, "r") as f:
            s = f.read()
        for line in s.split("\n"):
            if not line:
                continue
            username, *emails = line.split("\t")
            for email in emails:
                usernames[username].add(email)
        logger.info(f"Read {len(usernames)} contributors from {filename}")

    resolved_emails = set(
        email for email_set in usernames.values() for email in email_set
    )
    unresolved_emails = contributor_emails - resolved_emails
    for email in unresolved_emails:
        username_opt = _resolve_email(email)
        if username_opt:
            usernames[username_opt].add(email)

    with open(filename, "w") as f:
        for username, email_set in sorted(usernames.items()):
            emails_str = "\t".join(sorted(email_set))
            f.write(f"{username}\t{emails_str}")
            f.write("\n")

    logger.info(f"Wrote {len(usernames)} contributors to {filename}")

    email_to_username = {
        email: username for username, emails in usernames.items() for email in emails
    }

    return set(
        email_to_username[email]
        for email in contributor_emails
        if email in email_to_username
    )


def get_twitter_of_ghusers(ghusers: Collection[str]):
    logger.info("Getting twitter of GitHub usernames")

    # We will commit this file, to act as a cache (preventing us from querying GitHub API every time)
    filename = script_dir / "changelog_contributors_twitter.csv"

    twitter = {}

    # read existing contributors, to avoid extra calls to the GitHub API
    if os.path.exists(filename):
        with open(filename, "r") as f:
            s = f.read()
        for line in s.split("\n"):
            if not line:
                continue
            gh_username, twitter_username = line.split("\t")
            twitter[gh_username] = twitter_username
        logger.info(f"Read {len(twitter)} Twitter handles from {filename}")

    for username in ghusers:
        if username in twitter:
            continue
        try:
            resp = requests.get(f"https://api.github.com/users/{username}")
            resp.raise_for_status()
            data = resp.json()
        except Exception as e:
            logger.warning(f"Failed to get twitter of {username}: {e}")
            continue

        twitter_username = data["twitter_username"]
        if twitter_username:
            twitter[username] = twitter_username

    with open(filename, "w") as f:
        for username, twitter_username in sorted(twitter.items()):
            f.write(f"{username}\t{twitter_username}")
            f.write("\n")

    return twitter


if __name__ == "__main__":
    main()


================================================
FILE: scripts/changelog_contributors.csv
================================================
2e3s	2e3s19@gmail.com
750	37119951+750@users.noreply.github.com
Alwinator	39517491+Alwinator@users.noreply.github.com
BasileusErwin	67933444+BasileusErwin@users.noreply.github.com
BelKed	66956532+BelKed@users.noreply.github.com
CrazyPython	Jamtlu@gmail.com
Drarig29	corentingirard.dev@gmail.com
Elijah-Bodden	106613755+Elijah-Bodden@users.noreply.github.com
Furffico	43836984+Furffico@users.noreply.github.com
GabLeRoux	lebreton.gabriel@gmail.com
Game4Move78	83040764+Game4Move78@users.noreply.github.com
Julianoe	Julianoe@users.noreply.github.com
LockBlock-dev	68129141+LockBlock-dev@users.noreply.github.com
LunarWatcher	zoe.i2k1@gmail.com
NicoWeio	kontakt@nicolaiweitkemper.de	nico.weio@gmail.com
Organoidus	150709464+Organoidus@users.noreply.github.com
Shi-Soul	86898048+Shi-Soul@users.noreply.github.com
ShootingKing-AM	narnindi.raghu@gmail.com
Shubham0324	53115519+Shubham0324@users.noreply.github.com
StefanoChiodino	StefanoChiodino@users.noreply.github.com
TSRBerry	20988865+TSRBerry@users.noreply.github.com
Valentin-N	1926716+Valentin-N@users.noreply.github.com
Y7n05h	Y7n05h@protonmail.com
aaayushsingh	ayush-_-singh@live.com
alclary	9044153+alclary@users.noreply.github.com
alialamine	ali@towbe.com
alwinator	accounts@alwinschuster.at
0xbrayo	vukubrian@gmail.com
chaoky	levimanga@gmail.com
chengyuhui	chengyuhui1@gmail.com
davidfraser	davidfraser@users.noreply.github.com
dependabot-preview[bot]	27856297+dependabot-preview[bot]@users.noreply.github.com
dependabot[bot]	49699333+dependabot[bot]@users.noreply.github.com
erikbjare	erik.bjareholt@gmail.com	erik@bjareho.lt
hooger	hooger@users.noreply.github.com
iloveitaly	iloveitaly@gmail.com
infokiller	infokiller@users.noreply.github.com
ishitatsuyuki	ishitatsuyuki@gmail.com
jkbh	33606327+jkbh@users.noreply.github.com
johan-bjareholt	johan@bjareho.lt
jtojnar	jtojnar@gmail.com
kewde	kewde@particl.io
lgtm-com[bot]	43144390+lgtm-com[bot]@users.noreply.github.com
liutiming	39947942+liutiming@users.noreply.github.com
luzpaz	luzpaz@users.noreply.github.com
maciekstosio	maciekstosio@users.noreply.github.com
michaeljelly	53475252+michaeljelly@users.noreply.github.com
modderme123	modderme123@users.noreply.github.com
nathanmerrill	nathanmerrill@users.noreply.github.com
noisersup	patryk@kwiatek.xyz
ochen1	o.chen1@share.epsb.ca
omahs	73983677+omahs@users.noreply.github.com
oscar-king	oscar-king@users.noreply.github.com
pktiuk	kotiuk@zohomail.eu
pkvach	pavel.kvach@gmail.com
rakleed	19418601+rakleed@users.noreply.github.com
repo-visualizer	repo-visualizer@users.noreply.github.com
salahineo	salahineo.personal@gmail.com
skaparis	43264989+skaparis@users.noreply.github.com
skewballfox	joshua.ferguson.273@gmail.com
soxofaan	soxofaan@users.noreply.github.com
sunrosa	79175772+sunrosa@users.noreply.github.com
vedantmgoyal2009	83997633+vedantmgoyal2009@users.noreply.github.com
victorlin	13424970+victorlin@users.noreply.github.com
victorwinberg	victor.m.winberg@gmail.com
vieteh	viet.tran@employmenthero.com
xylix	kerk.pelt@gmail.com
yuhldr	yuhldr@qq.com
yumemio	59369226+yumemio@users.noreply.github.com


================================================
FILE: scripts/changelog_contributors_twitter.csv
================================================
0xbrayo	subrupt
chaoky	chaokyer
erikbjare	erikbjare
iloveitaly	mike_bianco
vedantmgoyal2009	vedantmgoyal
victorlin	victorlin_


================================================
FILE: scripts/checkout-latest-tag.sh
================================================
#!/bin/bash

latest_version_tag=$(git tag -l | grep "^v[0-9]\..*" | sort --version-sort | tail -n1 )
current_version_tag=$(git describe --tags)
echo "Latest version: $latest_version_tag"
echo "Current version: $current_version_tag"


================================================
FILE: scripts/chores/make-release.sh
================================================
#!/bin/bash

#
# We should create a release checklist to ensure releases are consistent.
#

# Create an annotated tag
#git tag -a $


================================================
FILE: scripts/ci/enable_long_paths.bat
================================================
:: Enable long paths on Windows (needed when building since node_modules can create deep hierarchies)

REG ADD "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f


================================================
FILE: scripts/ci/import-macos-p12.sh
================================================
#!/bin/sh

set -e

# Source: https://www.update.rocks/blog/osx-signing-with-travis/
export KEY_CHAIN=build.keychain
export CERTIFICATE_P12=aw_certificate.p12

# Recreate the certificate from the secure environment variable
echo $CERTIFICATE_MACOS_P12_BASE64 | base64 --decode > $CERTIFICATE_P12

#create a keychain
security -v create-keychain -p travis $KEY_CHAIN
# Make the keychain the default so identities are found
security -v default-keychain -s $KEY_CHAIN
# Unlock the keychain
security -v unlock-keychain -p travis $KEY_CHAIN

security -v import $CERTIFICATE_P12 -k $KEY_CHAIN -P $CERTIFICATE_MACOS_P12_PASSWORD -A
security -v set-key-partition-list -S apple-tool:,apple: -s -k travis $KEY_CHAIN

# remove certs
rm -rf *.p12


================================================
FILE: scripts/ci/install_node.ps1
================================================
$msipath = "$PSScriptRoot\node-installer.msi"

function RunCommand ($command, $command_args) {
    Write-Host $command $command_args
    Start-Process -FilePath $command -ArgumentList $command_args -Wait -Passthru
}

function InstallNode () {
    DownloadNodeMSI
    InstallNodeMSI
}

function DownloadNodeMSI () {
    $url = "https://nodejs.org/dist/v12.18.4/node-v12.18.4-x64.msi"
    $start_time = Get-Date

    Write-Output "Downloading node msi"
    Invoke-WebRequest -Uri $url -OutFile $msipath
    Write-Output "Time taken: $((Get-Date).Subtract($start_time).Seconds) second(s)"
}

function InstallNodeMSI () {
    $install_args = "/qn /log node_install.log /i $msipath"
    $uninstall_args = "/qn /x $msipath"
    RunCommand "msiexec.exe" $install_args

    #if (-not(Test-Path $python_home)) {
    #    Write-Host "Python seems to be installed else-where, reinstalling."
    #    RunCommand "msiexec.exe" $uninstall_args
    #    RunCommand "msiexec.exe" $install_args
    #}
}


function main () {
    InstallNode
    rm $msipath
}

main


================================================
FILE: scripts/ci/install_pyhook.ps1
================================================
function main ($arch) {
    If ( $arch -eq "64" ) {
        $url="https://github.com/ActivityWatch/wheels/raw/master/pyHook-1.5.1-cp36-cp36m-win_amd64.whl"
    } ElseIf ( $arch -eq "32" ) {
        $url="https://github.com/ActivityWatch/wheels/raw/master/pyHook-1.5.1-cp36-cp36m-win32.whl"
    } Else {
        Write-Output "Invalid architecture"
        return -1
    }
    pip install --user $url
}

main $env:PYTHON_ARCH


================================================
FILE: scripts/ci/install_python.ps1
================================================
# Sample script to install Python and pip under Windows
# Authors: Olivier Grisel, Jonathan Helmus, Kyle Kastner, and Alex Willmer
# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/
#
# Find the latest version of this script at:
# https://github.com/ogrisel/python-appveyor-demo/blob/master/appveyor/install.ps1


$MINICONDA_URL = "http://repo.continuum.io/miniconda/"
$BASE_URL = "https://www.python.org/ftp/python/"
$GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py"
$GET_PIP_PATH = "C:\get-pip.py"

$PYTHON_PRERELEASE_REGEX = @"
(?x)
(?<major>\d+)
\.
(?<minor>\d+)
\.
(?<micro>\d+)
(?<prerelease>[a-z]{1,2}\d+)
"@


function Download ($filename, $url) {
    $webclient = New-Object System.Net.WebClient

    $basedir = $pwd.Path + "\"
    $filepath = $basedir + $filename
    if (Test-Path $filename) {
        Write-Host "Reusing" $filepath
        return $filepath
    }

    # Download and retry up to 3 times in case of network transient errors.
    Write-Host "Downloading" $filename "from" $url
    $retry_attempts = 2
    for ($i = 0; $i -lt $retry_attempts; $i++) {
        try {
            $webclient.DownloadFile($url, $filepath)
            break
        }
        Catch [Exception]{
            Start-Sleep 1
        }
    }
    if (Test-Path $filepath) {
        Write-Host "File saved at" $filepath
    } else {
        # Retry once to get the error message if any at the last try
        $webclient.DownloadFile($url, $filepath)
    }
    return $filepath
}


function ParsePythonVersion ($python_version) {
    if ($python_version -match $PYTHON_PRERELEASE_REGEX) {
        return ([int]$matches.major, [int]$matches.minor, [int]$matches.micro,
                $matches.prerelease)
    }
    $version_obj = [version]$python_version
    return ($version_obj.major, $version_obj.minor, $version_obj.build, "")
}


function DownloadPython ($python_version, $platform_suffix) {
    $major, $minor, $micro, $prerelease = ParsePythonVersion $python_version

    if (($major -le 2 -and $micro -eq 0) `
        -or ($major -eq 3 -and $minor -le 2 -and $micro -eq 0) `
        ) {
        $dir = "$major.$minor"
        $python_version = "$major.$minor$prerelease"
    } else {
        $dir = "$major.$minor.$micro"
    }

    if ($prerelease) {
        if (($major -le 2) `
            -or ($major -eq 3 -and $minor -eq 1) `
            -or ($major -eq 3 -and $minor -eq 2) `
            -or ($major -eq 3 -and $minor -eq 3) `
            ) {
            $dir = "$dir/prev"
        }
    }

    if (($major -le 2) -or ($major -le 3 -and $minor -le 4)) {
        $ext = "msi"
        if ($platform_suffix) {
            $platform_suffix = ".$platform_suffix"
        }
    } else {
        $ext = "exe"
        if ($platform_suffix) {
            $platform_suffix = "-$platform_suffix"
        }
    }

    $filename = "python-$python_version$platform_suffix.$ext"
    $url = "$BASE_URL$dir/$filename"
    $filepath = Download $filename $url
    return $filepath
}


function InstallPython ($python_version, $architecture, $python_home) {
    Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home
    if (Test-Path $python_home) {
        Write-Host $python_home "already exists, skipping."
        return $false
    }
    if ($architecture -eq "32") {
        $platform_suffix = ""
    } else {
        $platform_suffix = "amd64"
    }
    $installer_path = DownloadPython $python_version $platform_suffix
    $installer_ext = [System.IO.Path]::GetExtension($installer_path)
    Write-Host "Installing $installer_path to $python_home"
    $install_log = $python_home + ".log"
    if ($installer_ext -eq '.msi') {
        InstallPythonMSI $installer_path $python_home $install_log
    } else {
        InstallPythonEXE $installer_path $python_home $install_log
    }
    if (Test-Path $python_home) {
        Write-Host "Python $python_version ($architecture) installation complete"
    } else {
        Write-Host "Failed to install Python in $python_home"
        Get-Content -Path $install_log
        Exit 1
    }
}


function InstallPythonEXE ($exepath, $python_home, $install_log) {
    $install_args = "/quiet InstallAllUsers=1 TargetDir=$python_home"
    RunCommand $exepath $install_args
}


function InstallPythonMSI ($msipath, $python_home, $install_log) {
    $install_args = "/qn /log $install_log /i $msipath TARGETDIR=$python_home"
    $uninstall_args = "/qn /x $msipath"
    RunCommand "msiexec.exe" $install_args
    if (-not(Test-Path $python_home)) {
        Write-Host "Python seems to be installed else-where, reinstalling."
        RunCommand "msiexec.exe" $uninstall_args
        RunCommand "msiexec.exe" $install_args
    }
}

function RunCommand ($command, $command_args) {
    Write-Host $command $command_args
    Start-Process -FilePath $command -ArgumentList $command_args -Wait -Passthru
}


function InstallPip ($python_home) {
    $pip_path = $python_home + "\Scripts\pip.exe"
    $python_path = $python_home + "\python.exe"
    if (-not(Test-Path $pip_path)) {
        Write-Host "Installing pip..."
        $webclient = New-Object System.Net.WebClient
        $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH)
        Write-Host "Executing:" $python_path $GET_PIP_PATH
        & $python_path $GET_PIP_PATH
    } else {
        Write-Host "pip already installed."
    }
}


function DownloadMiniconda ($python_version, $platform_suffix) {
    if ($python_version -eq "3.4") {
        $filename = "Miniconda3-3.5.5-Windows-" + $platform_suffix + ".exe"
    } else {
        $filename = "Miniconda-3.5.5-Windows-" + $platform_suffix + ".exe"
    }
    $url = $MINICONDA_URL + $filename
    $filepath = Download $filename $url
    return $filepath
}


function InstallMiniconda ($python_version, $architecture, $python_home) {
    Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home
    if (Test-Path $python_home) {
        Write-Host $python_home "already exists, skipping."
        return $false
    }
    if ($architecture -eq "32") {
        $platform_suffix = "x86"
    } else {
        $platform_suffix = "x86_64"
    }
    $filepath = DownloadMiniconda $python_version $platform_suffix
    Write-Host "Installing" $filepath "to" $python_home
    $install_log = $python_home + ".log"
    $args = "/S /D=$python_home"
    Write-Host $filepath $args
    Start-Process -FilePath $filepath -ArgumentList $args -Wait -Passthru
    if (Test-Path $python_home) {
        Write-Host "Python $python_version ($architecture) installation complete"
    } else {
        Write-Host "Failed to install Python in $python_home"
        Get-Content -Path $install_log
        Exit 1
    }
}


function InstallMinicondaPip ($python_home) {
    $pip_path = $python_home + "\Scripts\pip.exe"
    $conda_path = $python_home + "\Scripts\conda.exe"
    if (-not(Test-Path $pip_path)) {
        Write-Host "Installing pip..."
        $args = "install --yes pip"
        Write-Host $conda_path $args
        Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru
    } else {
        Write-Host "pip already installed."
    }
}

function main () {
    InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON
    InstallPip $env:PYTHON
}

main


================================================
FILE: scripts/ci/run_with_env.cmd
================================================
:: To build extensions for 64 bit Python 3, we need to configure environment
:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of:
:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1)
::
:: To build extensions for 64 bit Python 2, we need to configure environment
:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of:
:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0)
::
:: 32 bit builds, and 64-bit builds for 3.5 and beyond, do not require specific
:: environment configurations.
::
:: Note: this script needs to be run with the /E:ON and /V:ON flags for the
:: cmd interpreter, at least for (SDK v7.0)
::
:: More details at:
:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows
:: http://stackoverflow.com/a/13751649/163740
::
:: Author: Olivier Grisel
:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/
::
:: Notes about batch files for Python people:
::
:: Quotes in values are literally part of the values:
::      SET FOO="bar"
:: FOO is now five characters long: " b a r "
:: If you don't want quotes, don't include them on the right-hand side.
::
:: The CALL lines at the end of this file look redundant, but if you move them
:: outside of the IF clauses, they do not run properly in the SET_SDK_64==Y
:: case, I don't know why.
@ECHO OFF
SET COMMAND_TO_RUN=%*
SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows
SET WIN_WDK=c:\Program Files (x86)\Windows Kits\10\Include\wdf

:: Extract the major and minor versions, and allow for the minor version to be
:: more than 9.  This requires the version number to have two dots in it.
SET MAJOR_PYTHON_VERSION=%PYTHON_VERSION:~0,1%
IF "%PYTHON_VERSION:~3,1%" == "." (
    SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,1%
) ELSE (
    SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,2%
)

:: Based on the Python version, determine what SDK version to use, and whether
:: to set the SDK for 64-bit.
IF %MAJOR_PYTHON_VERSION% == 2 (
    SET WINDOWS_SDK_VERSION="v7.0"
    SET SET_SDK_64=Y
) ELSE (
    IF %MAJOR_PYTHON_VERSION% == 3 (
        SET WINDOWS_SDK_VERSION="v7.1"
        IF %MINOR_PYTHON_VERSION% LEQ 4 (
            SET SET_SDK_64=Y
        ) ELSE (
            SET SET_SDK_64=N
            IF EXIST "%WIN_WDK%" (
                :: See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/
                REN "%WIN_WDK%" 0wdf
            )
        )
    ) ELSE (
        ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%"
        EXIT 1
    )
)

IF %PYTHON_ARCH% == 64 (
    IF %SET_SDK_64% == Y (
        ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture
        SET DISTUTILS_USE_SDK=1
        SET MSSdk=1
        "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION%
        "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release
        ECHO Executing: %COMMAND_TO_RUN%
        call %COMMAND_TO_RUN% || EXIT 1
    ) ELSE (
        ECHO Using default MSVC build environment for 64 bit architecture
        ECHO Executing: %COMMAND_TO_RUN%
        call %COMMAND_TO_RUN% || EXIT 1
    )
) ELSE (
    ECHO Using default MSVC build environment for 32 bit architecture
    ECHO Executing: %COMMAND_TO_RUN%
    call %COMMAND_TO_RUN% || EXIT 1
)


================================================
FILE: scripts/count_lines.sh
================================================
#!/usr/bin/env bash

re_ignore='.*(build|dist|venv|old|other|scripts|node|static).*'

echo -n "Lines of code (excluding test): "
files=$(find | egrep '\.(py|js|ts|rs|vue)$' | egrep -v $re_ignore | grep -v 'test')
echo $files | xargs cat | wc -l

#echo "Files:"
#for file in $files; do
#    echo " - $file"
#done

echo -n " - of which Python code: "
files=$(find | egrep '\.(py)$' | egrep -v $re_ignore | grep -v 'test')
echo $files | xargs cat | wc -l

echo -n " - of which Rust code: "
files=$(find | egrep '\.(rs)$' | egrep -v  $re_ignore | grep -v 'test')
echo $files | xargs cat | wc -l

echo -n " - of which JS/TS code: "
files=$(find | egrep '\.(js|ts)$' | egrep -v $re_ignore | grep -v 'test')
echo $files | xargs cat | wc -l

echo -n " - of which Vue code: "
files=$(find | egrep '\.(vue)$' | egrep -v  $re_ignore | grep -v 'test')
echo $files | xargs cat | wc -l

echo -ne "\nLines of test: "
files=$(find | egrep '\.(py|js|vue)$' | egrep -v $re_ignore | grep 'test')
echo $files | xargs cat | wc -l


================================================
FILE: scripts/get_latest_release.sh
================================================
#!/bin/bash

# TODO: Merge with scripts/package/getversion.sh

# Script that fetches the previous release (if current commit is a tag),
# or the latest release, if current commit is not a tag.

# If stable only, then we return the latest stable release, 
# else, we will return the latest release, either stable or prerelease.
RE_STABLE='(?<=[/])v[0-9\.]+$'
RE_INCL_PRERELEASE='(?<=[/])v[0-9\.]+(a|b|rc)?[0-9]+$'

# Get tag for this commit, if any
TAG=$(git describe --tags --exact-match 2>/dev/null)

RE=$RE_INCL_PRERELEASE
if [ -n "$STABLE_ONLY" ]; then
    if [ "$STABLE_ONLY" = "true" ]; then
        RE=$RE_STABLE
    fi
fi
ALL_TAGS=`git for-each-ref --sort=creatordate --format '%(refname)' refs/tags`

# If current commit is a tag, we filter it out
if [ -n "$TAG" ]; then
    ALL_TAGS=`echo "$ALL_TAGS" | grep -v "^refs/tags/$TAG$"`
fi

echo "$ALL_TAGS" | grep -P "$RE" --only-matching | tail -n1


================================================
FILE: scripts/logcrawler.py
================================================
import os
import re
from datetime import datetime
from collections import defaultdict
import logging

import aw_core

logging.basicConfig()

log_dir = aw_core.dirs.get_log_dir("")


def get_filepaths():
    filepaths = []
    for folder, dirs, files in os.walk(log_dir):
        print("Crawling folder: " + folder)
        filepaths.extend([os.path.join(folder, filename) for filename in files])
    return filepaths


def collect():
    matched_lines = defaultdict(lambda: [])
    for filepath in sorted(get_filepaths()):
        with open(filepath, "r") as f:
            log = f.read()
            for line in log.split("\n"):
                s = re.search("(ERR|WARN)", line)
                ignored = re.search("(CORS|Deleted bucket)", line)
                if s and not ignored:
                    matched_lines[filepath].append(line)
    return matched_lines


_date_reg_exp = re.compile('\d{4}-\d{2}-\d{2}')


today = datetime.now()


def line_age(line):
    """Returns line age in days"""
    match = _date_reg_exp.search(line)
    if not match:
        logging.warning("Line had no date, avoid multiple line messages in logs. Line will have its age set to zero.")
        return 0
    else:
        dt = datetime.strptime(match.group(), '%Y-%m-%d')
        td = today - dt
        return td.days


def main(exclude_testing: bool = False, limit_days: int = 10, limit_lines: int = 10):
    file_lines = collect()

    if exclude_testing:
        keys = filter(lambda k: "testing" not in k, file_lines.keys())
        file_lines = {key: file_lines[key] for key in keys}

    for filename, lines in sorted(file_lines.items()):
        lines = sorted(file_lines[filename], reverse=True)

        # Filter lines older than x days
        if limit_days:
            lines = [line for line in lines if line_age(line) <= limit_days]

        if lines:
            print("-" * 50)
            print("File: {}".format(filename))

            # Print lines up to the limit
            for line in lines[:limit_lines]:
                print("  " + line)

            if limit_lines < len(lines):
                print("Showing {} out of {} lines".format(limit_lines, len(lines)))


if __name__ == "__main__":
    main()


================================================
FILE: scripts/nop.sh
================================================
#!/bin/bash

echo "nop.bat was executed as a workaround for something"


================================================
FILE: scripts/notarize.sh
================================================
#!/bin/bash

applemail=$APPLE_EMAIL # Email address used for Apple ID
password=$APPLE_PASSWORD # See apps-specific password https://support.apple.com/en-us/HT204397
teamid=$APPLE_TEAMID # Team idenitifer (if single developer, then set to developer identifier)
keychain_profile="activitywatch-$APPLE_PERSONALID"  # name of the keychain profile to use
bundleid=net.activitywatch.ActivityWatch # Match aw.spec
app=dist/ActivityWatch.app
dmg=dist/ActivityWatch.dmg

# XCode >= 13 
run_notarytool() {
    dist=$1
    # Setup the credentials for notarization
    xcrun notarytool store-credentials $keychain_profile --apple-id $applemail --team-id $teamid --password $password
    # Notarize and wait
    echo "Notarization: starting for $dist"
    echo "Notarization: in progress for $dist"
    xcrun notarytool submit $dist --keychain-profile $keychain_profile --wait
}

# XCode < 13 
run_altool() {
    dist=$1
    # Setup the credentials for notarization
    xcrun altool --store-password-in-keychain-item $keychain_profile -u $applemail -p $password
    # Notarize and wait
    echo "Notarization: starting for $dist"
    upload=$(xcrun altool --notarize-app -t osx -f $dist --primary-bundle-id $bundleid -u $applemail --password "@keychain:$keychain_profile")
    uuid = $(/usr/libexec/PlistBuddy -c "Print :notarization-upload:RequestUUID" $upload)
    while true; do 
        req=$(xcrun altool --notarization-info $uuid -u $applemail -p $password --output-format xml)
        status=$(/usr/libexec/PlistBuddy -c "Print :notarization-info:Status" $req)
        if [ $status != "in progress" ]; then 
            break
        else
            echo "Notarization: in progress for $dist"
        fi
        sleep 10
    done
}

# Staples the notarization certificate to the executable/bunldle
run_stapler() {
    dist=$1
    xcrun stapler staple $dist
}

echo 'Detecting availability of notarization tools'
notarization_method=exit
# Detect if notarytool is available
xcrun notarytool >/dev/null 2>&1
if [ $? -eq 0 ]; then
    echo "+ Found notarytool"
    notarization_method=run_notarytool
fi
# Fallbqck to altool
output=xcrun altool >/dev/null 2>&1
if [ $? -eq 0 ]; then
    echo "+ Found altool"
    notarization_method=run_altool
fi

if [ $notarization_method = "exit" ]; then
    echo "- Found no tools, exiting"
    $notarization_method
fi

if test -f "$app"; then
    echo "Notarizing: $app"
    zip=$app.zip
    # Turn the app into a zip file that notarization will accept
    ditto -c -k --keepParent $app $zip
    $notarization_method $zip
    run_stapler $app
else
    echo "Skipping: $app"
fi

if test -f "$dmg"; then
    echo "Notarizing: $dmg"
    $notarization_method $dmg
    run_stapler $dmg
else
    echo "Skipping: $dmg"
fi


================================================
FILE: scripts/package/README.txt
================================================
Run move-to-aw-modules.sh to copy all modules except aw-tauri to ~/aw-modules/.
aw-tauri (replaces aw-qt) will use this directory to discover new modules.
You can add your own modules and scripts to this directory. The modules should
start with the aw- prefix and should not have an extension (e.g. no .sh).

In the aw-tauri folder there are AppImage, RPM, and DEB binaries. Choose the
appropriate one for your Linux distribution. If in doubt, use the AppImage as
it works on most Linux systems. If you use the AppImage, copy it to a permanent
folder like ~/bin or /usr/local/bin, since autostart relies on the AppImage
being in the same location each time.


================================================
FILE: scripts/package/activitywatch-setup.iss
================================================
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!

#define MyAppName "ActivityWatch"
#define MyAppVersion GetEnv('AW_VERSION')
#define MyAppPublisher "ActivityWatch Contributors"
#define MyAppURL "https://activitywatch.net/"
#define MyAppExeName "aw-qt.exe"
#define RootDir "..\.."
#define DistDir "..\..\dist"

#pragma verboselevel 9

[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
; NOTE: the double {{ are used to escape the { character (needed for the AppId)
AppId={{F226B8F4-3244-46E6-901D-0CE8035423E4}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL="https://github.com/ActivityWatch/activitywatch/issues"
AppUpdatesURL="https://github.com/ActivityWatch/activitywatch/releases"
DefaultDirName={autopf}\{#MyAppName}
DisableProgramGroupPage=yes
; Uncomment the following line to run in non administrative install mode (install for current user only.)
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
OutputDir={#DistDir}
OutputBaseFilename=activitywatch-setup
SetupIconFile="{#RootDir}\aw-qt\media\logo\logo.ico"
UninstallDisplayName={#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
Compression=lzma
SolidCompression=yes
WizardStyle=modern

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "StartMenuEntry" ; Description: "Start ActivityWatch when Windows starts"; GroupDescription: "Windows Startup"; MinVersion: 4,4;

[Files]
Source: "{#DistDir}\activitywatch\aw-qt.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#DistDir}\activitywatch\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files

[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: StartMenuEntry;

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent

; Removes the previously installed version before installing the new one
; NOTE: Doesn't work? And also discouraged by the docs
;[InstallDelete]
;Type: filesandordirs; Name: "{app}\"


================================================
FILE: scripts/package/aw-tauri.iss
================================================
; Inno Setup script for ActivityWatch (Tauri edition)
;
; This is separate from activitywatch-setup.iss (aw-qt) to avoid
; installation collisions. Uses a different AppId, install directory,
; and display name.

#define MyAppName "ActivityWatch (Tauri)"
#define MyAppVersion GetEnv('AW_VERSION')
#define MyAppPublisher "ActivityWatch Contributors"
#define MyAppURL "https://activitywatch.net/"
#define MyAppExeName "aw-tauri.exe"
#define RootDir "..\.."
#define DistDir "..\..\dist"

#pragma verboselevel 9

[Setup]
; IMPORTANT: Different AppId from aw-qt to allow side-by-side installation
AppId={{983D0855-08C8-46BD-AEFB-3924581C6703}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL="https://github.com/ActivityWatch/activitywatch/issues"
AppUpdatesURL="https://github.com/ActivityWatch/activitywatch/releases"
DefaultDirName={autopf}\ActivityWatch-Tauri
DisableProgramGroupPage=yes
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
OutputDir={#DistDir}
OutputBaseFilename=activitywatch-setup
SetupIconFile="{#RootDir}\aw-tauri\src-tauri\icons\icon.ico"
UninstallDisplayName={#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
Compression=lzma
SolidCompression=yes
WizardStyle=modern

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "StartMenuEntry" ; Description: "Start ActivityWatch when Windows starts"; GroupDescription: "Windows Startup"; MinVersion: 4,4;

[Files]
Source: "{#DistDir}\activitywatch\aw-tauri.exe"; DestDir: "{app}\aw-tauri"; Flags: ignoreversion
Source: "{#DistDir}\activitywatch\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs

[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: StartMenuEntry;

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent


================================================
FILE: scripts/package/build_app_tauri.sh
================================================
#!/bin/bash
set -e

# Build a macOS .app bundle for the Tauri-based ActivityWatch.
# This replaces the PyInstaller-based bundling used by aw-qt.

APP_NAME="ActivityWatch"
BUNDLE_ID="net.activitywatch.ActivityWatch"
VERSION="0.1.0"
ICON_PATH="aw-tauri/src-tauri/icons/icon.icns"

if [[ "$(uname)" != "Darwin" ]]; then
    echo "This script is designed to run on macOS only."
    exit 1
fi

if [ ! -d "dist/activitywatch" ]; then
    echo "Error: dist/activitywatch directory not found. Please build the project first."
    exit 1
fi

if [ ! -f "dist/activitywatch/aw-tauri" ]; then
    echo "Error: aw-tauri binary not found in dist/activitywatch/"
    exit 1
fi

echo "Cleaning previous builds..."
rm -rf "dist/${APP_NAME}.app"
mkdir -p "dist"

echo "Creating app bundle structure..."
mkdir -p "dist/${APP_NAME}.app/Contents/"{MacOS,Resources}

echo "Copying aw-tauri as main executable..."
cp "dist/activitywatch/aw-tauri" "dist/${APP_NAME}.app/Contents/MacOS/aw-tauri"
chmod +x "dist/${APP_NAME}.app/Contents/MacOS/aw-tauri"

echo "Copying components to Resources..."
for component in dist/activitywatch/*/; do
    if [ -d "$component" ]; then
        component_name=$(basename "$component")
        echo "  Copying $component_name..."
        mkdir -p "dist/${APP_NAME}.app/Contents/Resources/$component_name"
        cp -r "$component"/* "dist/${APP_NAME}.app/Contents/Resources/$component_name/"
    fi
done

echo "Setting executable permissions..."
find "dist/${APP_NAME}.app/Contents/Resources" -type f -name "aw-*" -exec chmod +x {} \;

echo "Copying app icon..."
if [ -f "$ICON_PATH" ]; then
    cp "$ICON_PATH" "dist/${APP_NAME}.app/Contents/Resources/icon.icns"
else
    echo "Warning: Icon file not found at $ICON_PATH"
fi

echo "Creating Info.plist..."
cat > "dist/${APP_NAME}.app/Contents/Info.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>English</string>
    <key>CFBundleExecutable</key>
    <string>aw-tauri</string>
    <key>CFBundleIconFile</key>
    <string>icon.icns</string>
    <key>CFBundleIdentifier</key>
    <string>${BUNDLE_ID}</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>${APP_NAME}</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>${VERSION}</string>
    <key>CFBundleVersion</key>
    <string>${VERSION}</string>
    <key>NSAppleEventsUsageDescription</key>
    <string>ActivityWatch needs access to monitor application usage</string>
    <key>NSHighResolutionCapable</key>
    <true/>
    <key>NSPrincipalClass</key>
    <string>NSApplication</string>
    <key>LSMinimumSystemVersion</key>
    <string>10.14</string>
</dict>
</plist>
EOF

echo "Creating PkgInfo..."
echo "APPL????" > "dist/${APP_NAME}.app/Contents/PkgInfo"

if [ -n "$APPLE_PERSONALID" ]; then
    echo "Signing app with identity: $APPLE_PERSONALID"
    codesign --deep --force --sign "$APPLE_PERSONALID" "dist/${APP_NAME}.app"
    echo "App signing complete."
else
    echo "APPLE_PERSONALID not set. Skipping code signing."
fi

echo "App bundle created at: dist/${APP_NAME}.app"


================================================
FILE: scripts/package/deb/control
================================================
Package: activitywatch
Architecture: amd64
Maintainer: Erik Bjäreholt <erik@bjareho.lt>
Depends:
Priority: optional
Version: SCRIPT_VERSION_HERE
Description: Open source time tracker
 https://github.com/ActivityWatch/activitywatch


================================================
FILE: scripts/package/dmgbuild-settings.py
================================================
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import plistlib
import os.path

# Use like this: dmgbuild -s settings.py "Test Volume" test.dmg

# You can actually use this file for your own application (not just TextEdit)
# by doing e.g.
#
#   dmgbuild -s settings.py -D app=/path/to/My.app "My Application" MyApp.dmg

# .. Useful stuff ..............................................................

application = defines.get('app', 'dist/ActivityWatch.app')
appname = os.path.basename(application)

def icon_from_app(app_path):
    plist_path = os.path.join(app_path, 'Contents', 'Info.plist')
    with open(plist_path, "rb") as f:
        plist = plistlib.load(f)
    icon_name = plist['CFBundleIconFile']
    icon_root,icon_ext = os.path.splitext(icon_name)
    if not icon_ext:
        icon_ext = '.icns'
    icon_name = icon_root + icon_ext
    return os.path.join(app_path, 'Contents', 'Resources', icon_name)

# .. Basics ....................................................................

# Uncomment to override the output filename
# filename = 'test.dmg'

# Uncomment to override the output volume name
# volume_name = 'Test'

# Volume format (see hdiutil create -help)
format = defines.get('format', 'UDBZ')

# Volume size
size = defines.get('size', None)

# Files to include
files = [ application ]

# Symlinks to create
symlinks = { 'Applications': '/Applications' }

# Volume icon
#
# You can either define icon, in which case that icon file will be copied to the
# image, *or* you can define badge_icon, in which case the icon file you specify
# will be used to badge the system's Removable Disk icon
#
#icon = '/path/to/icon.icns'
badge_icon = icon_from_app(application)

# Where to put the icons
icon_locations = {
    appname:        (140, 120),
    'Applications': (500, 120)
}

show_status_bar = False
show_tab_view = False
show_toolbar = False
show_pathbar = False
show_sidebar = False
sidebar_width = 180

# Window position in ((x, y), (w, h)) format
window_rect = ((100, 100), (640, 280))

default_view = 'icon-view'

show_icon_preview = False

# Set these to True to force inclusion of icon/list view settings (otherwise
# we only include settings for the default view)
include_icon_view_settings = 'auto'
include_list_view_settings = 'auto'

# .. Icon view configuration ...................................................

arrange_by = None
grid_offset = (0, 0)
grid_spacing = 100
scroll_position = (0, 0)
label_pos = 'bottom' # or 'right'
text_size = 16
icon_size = 128

# .. List view configuration ...................................................

# Column names are as follows:
#
#   name
#   date-modified
#   date-created
#   date-added
#   date-last-opened
#   size
#   kind
#   label
#   version
#   comments
#
list_icon_size = 16
list_text_size = 12
list_scroll_position = (0, 0)
list_sort_by = 'name'
list_use_relative_dates = True
list_calculate_all_sizes = False,
list_columns = ('name', 'date-modified', 'size', 'kind', 'date-added')
list_column_widths = {
    'name': 300,
    'date-modified': 181,
    'date-created': 181,
    'date-added': 181,
    'date-last-opened': 181,
    'size': 97,
    'kind': 115,
    'label': 100,
    'version': 75,
    'comments': 300,
}

list_column_sort_directions = {
    'name': 'ascending',
    'date-modified': 'descending',
    'date-created': 'descending',
    'date-added': 'descending',
    'date-last-opened': 'descending',
    'size': 'descending',
    'kind': 'ascending',
    'label': 'ascending',
    'version': 'ascending',
    'comments': 'ascending',
}



================================================
FILE: scripts/package/entitlements.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<!-- These are required for binaries built by PyInstaller -->
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
	<true/>
</dict>
</plist>


================================================
FILE: scripts/package/getversion.sh
================================================
#!/bin/bash

# TODO: Merge with scripts/package/getversion.sh
# set -e

if [[ $TRAVIS_TAG ]]; then
    _version=$TRAVIS_TAG;
elif [[ $APPVEYOR_REPO_TAG_NAME ]]; then
    _version=$APPVEYOR_REPO_TAG_NAME;
else
    # Exact
    _version=$(git describe --tags --abbrev=0 --exact-match 2>/dev/null)
    if [[ -z $_version ]]; then
        # Latest tag + commit ID
        _version="$(git describe --tags --abbrev=0).dev-$(git rev-parse --short HEAD)"
    fi
fi

echo $_version;


================================================
FILE: scripts/package/move-to-aw-modules.sh
================================================
#!/bin/bash
# Copy all AW modules to ~/aw-modules/ for aw-tauri to discover.
# aw-tauri uses this directory to find and launch AW components.
set -e

mkdir -p ~/aw-modules/

if [[ -n "$XDG_SESSION_TYPE" && "$XDG_SESSION_TYPE" == "wayland" ]]; then
    rsync -a . ~/aw-modules/ \
        --exclude=aw-tauri \
        --exclude=aw-server-rust \
        --exclude=awatcher \
        --exclude=move-to-aw-modules.sh \
        --exclude=README.txt
    cp ./awatcher/aw-awatcher ~/aw-modules/
    cp ./aw-server-rust/aw-sync ~/aw-modules/
else
    rsync -a . ~/aw-modules/ \
        --exclude=aw-tauri \
        --exclude=awatcher \
        --exclude=aw-server-rust \
        --exclude=move-to-aw-modules.sh \
        --exclude=README.txt
    cp ./aw-server-rust/aw-sync ~/aw-modules/
fi

echo "Modules copied to ~/aw-modules/"


================================================
FILE: scripts/package/package-all.sh
================================================
#!/bin/bash

set -e

echoerr() { echo "$@" 1>&2; }

function get_platform() {
    # Will return "linux" for GNU/Linux
    #   I'd just like to interject for a moment...
    #   https://wiki.installgentoo.com/index.php/Interjection
    # Will return "macos" for macOS/OS X
    # Will return "windows" for Windows/MinGW/msys

    _platform=$(uname | tr '[:upper:]' '[:lower:]')
    if [[ $_platform == "darwin" ]]; then
        _platform="macos";
    elif [[ $_platform == "msys"* ]]; then
        _platform="windows";
    elif [[ $_platform == "mingw"* ]]; then
        _platform="windows";
    elif [[ $_platform == "linux" ]]; then
        # Nothing to do
        true;
    else
        echoerr "ERROR: $_platform is not a valid platform";
        exit 1;
    fi

    echo $_platform;
}

function get_version() {
    $(dirname "$0")/getversion.sh;
}

function get_arch() {
    _arch="$(uname -m)"
    echo $_arch;
}

platform=$(get_platform)
version=$(get_version)
arch=$(get_arch)
echo "Platform: $platform, arch: $arch, version: $version"

# For Tauri Linux builds, include helper scripts and README
if [[ $platform == "linux" && $TAURI_BUILD == "true" ]]; then
    cp scripts/package/README.txt scripts/package/move-to-aw-modules.sh dist/activitywatch/
fi

function build_zip() {
    echo "Zipping executables..."
    pushd dist;
    filename="activitywatch-${version}-${platform}-${arch}.zip"
    echo "Name of package will be: $filename"

    if [[ $platform == "windows"* ]]; then
        7z a $filename activitywatch;
    else
        zip -r $filename activitywatch;
    fi
    popd;
    echo "Zip built!"
}

function build_setup() {
    filename="activitywatch-${version}-${platform}-${arch}-setup.exe"
    echo "Name of package will be: $filename"

    innosetupdir="/c/Program Files (x86)/Inno Setup 6"
    if [ ! -d "$innosetupdir" ]; then
        echo "ERROR: Couldn't find innosetup which is needed to build the installer. We suggest you install it using chocolatey. Exiting."
        exit 1
    fi

    # Windows installer version should not include 'v' prefix, see: https://github.com/microsoft/winget-pkgs/pull/17564
    version_no_prefix="$(echo $version | sed -e 's/^v//')"
    if [[ $TAURI_BUILD == "true" ]]; then
        env AW_VERSION=$version_no_prefix "$innosetupdir/iscc.exe" scripts/package/aw-tauri.iss
    else
        env AW_VERSION=$version_no_prefix "$innosetupdir/iscc.exe" scripts/package/activitywatch-setup.iss
    fi
    mv dist/activitywatch-setup.exe dist/$filename
    echo "Setup built!"
}

build_zip
if [[ $platform == "windows"* ]]; then
    build_setup
fi

echo
echo "-------------------------------------"
echo "Contents of ./dist"
ls -l dist
echo "-------------------------------------"



================================================
FILE: scripts/package/package-appimage.sh
================================================
#!/bin/bash

# pick the latest zip
# NOTE: this assumes that the latest built zip is the only zip in the directory
ZIP_FILE=`ls ./dist/ -1 | grep zip | sort -r | head -1`
unzip ./dist/$ZIP_FILE

# fetch deps
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage

# create AppRun
echo '#!/bin/sh
DIR="$(dirname "$(readlink -f "${0}")")"
"${DIR}"/aw-qt "$@"' > activitywatch/AppRun
chmod a+x ./activitywatch/AppRun

# build appimage
./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
APPIMAGE_FILE=`ls -1 | grep AppImage| grep -i ActivityWatch`
cp -v $APPIMAGE_FILE ./dist/activitywatch-linux-x86_64.AppImage


================================================
FILE: scripts/package/package-deb.sh
================================================
#!/usr/bin/bash
# Setting the shell is required, as `sh` doesn't support slicing.

# Fail fast
set -e
# Verbose commands for CI verification
set -x

VERSION=$(scripts/package/getversion.sh)
# Slice off the "v" from the tag, which is probably guaranteed
VERSION_NUM=${VERSION:1}
echo $VERSION_NUM
PKGDIR="activitywatch_$VERSION_NUM"

# Package tools
sudo apt-get install sed jdupes wget

if [ -d "PKGDIR" ]; then
    sudo rm -rf $PKGDIR
fi

# .deb meta files
mkdir -p $PKGDIR/DEBIAN
# activitywatch's install location
mkdir -p $PKGDIR/opt
# Allows aw-qt to autostart.
mkdir -p $PKGDIR/etc/xdg/autostart
# Allows users to manually start aw-qt from their start menu.
mkdir -p $PKGDIR/usr/share/applications

# While storing the control file in a variable here, dumping it in a file is so unnecessarily
# complicated that it's easier to just dump move and sed.
cp ./scripts/package/deb/control $PKGDIR/DEBIAN/control
sed -i "s/SCRIPT_VERSION_HERE/${VERSION_NUM}/" $PKGDIR/DEBIAN/control

# Verify the file content
cat $PKGDIR/DEBIAN/control
# The entire opt directory (should) consist of dist/activitywatch/*

cp -r dist/activitywatch/ $PKGDIR/opt/

# Hard link duplicated libraries
# (I have no idea what this is for)
jdupes -L -r -S -Xsize-:1K $PKGDIR/opt/

sudo chown -R root:root $PKGDIR

# Prepare the .desktop file
sudo sed -i 's!Exec=aw-qt!Exec=/opt/activitywatch/aw-qt!' $PKGDIR/opt/activitywatch/aw-qt.desktop
sudo cp $PKGDIR/opt/activitywatch/aw-qt.desktop $PKGDIR/etc/xdg/autostart/
sudo cp $PKGDIR/opt/activitywatch/aw-qt.desktop $PKGDIR/usr/share/applications/

dpkg-deb --build $PKGDIR
sudo mv activitywatch_${VERSION_NUM}.deb dist/activitywatch-${VERSION}-linux-x86_64.deb


================================================
FILE: scripts/submodule-branch.sh
================================================
#!/bin/bash

# Get current branch
#   git rev-parse --abbrev-ref HEAD
# Get branch for each submodule
#   git submodule foreach "git rev-parse --abbrev-ref HEAD"

SUBMODULES=$(git submodule | sed -r -e 's/^[ \+][a-z0-9]+ //g' -e 's/ \(.*\)//g')
for module in $SUBMODULES; do
    branch=$(git --git-dir=$module/.git rev-parse --abbrev-ref HEAD)
    printf "%-20s %-30s\n" "$module" "$branch"
done


================================================
FILE: scripts/symlink-systemd.sh
================================================
#!/bin/bash
for module in "aw-server" "aw-watcher-afk" "aw-watcher-x11"; do
    ln -s $(pwd)/$module/misc/${module}.service ~/.config/systemd/user/${module}.service
done


================================================
FILE: scripts/tests/integration_tests.py
================================================
import os
import platform
import subprocess
import tempfile
from time import sleep

import pytest


def _windows_kill_process(pid):
    import ctypes

    PROCESS_TERMINATE = 1
    handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, False, pid)
    ctypes.windll.kernel32.TerminateProcess(handle, -1)
    ctypes.windll.kernel32.CloseHandle(handle)


# NOTE: to run tests with a specific server binary,
#       set the PATH such that it is the "aw-server" binary.
@pytest.fixture(scope="session")
def server_process():
    logfile_stdout = tempfile.NamedTemporaryFile(delete=False)
    logfile_stderr = tempfile.NamedTemporaryFile(delete=False)

    # find the path of the "aw-server" binary and log it
    which_server = subprocess.check_output(["which", "aw-server"], text=True)
    print(f"aw-server path: {which_server}")

    # if aw-server-rust in PATH, assert that we're picking up the aw-server-rust binary
    if "aw-server-rust" in os.environ["PATH"]:
        assert "aw-server-rust" in which_server

    server_proc = subprocess.Popen(
        ["aw-server", "--testing"], stdout=logfile_stdout, stderr=logfile_stderr
    )

    # Wait for server to start up properly
    # TODO: Ping the server until it's alive to remove this sleep
    sleep(5)

    yield server_proc

    if platform.system() == "Windows":
        # On Windows, for whatever reason, server_proc.kill() doesn't do the job.
        _windows_kill_process(server_proc.pid)
    else:
        server_proc.kill()
    server_proc.wait(5)
    server_proc.communicate()

    error_indicators = ["ERROR"]

    with open(logfile_stdout.name, "r+b") as f:
        stdout = str(f.read(), "utf8")
        if any(e in stdout for e in error_indicators):
            pytest.fail(f"Found ERROR indicator in stdout from server: {stdout}")

    with open(logfile_stderr.name, "r+b") as f:
        stderr = str(f.read(), "utf8")
        # For some reason, this fails aw-server-rust, but not aw-server-python
        # if not stderr:
        #    pytest.fail("No output to stderr from server")

        # Will show in case pytest fails
        print(stderr)

        for s in error_indicators:
            if s in stderr:
                pytest.fail(f"Found ERROR indicator in stderr from server: {s}")

    # NOTE: returncode was -9 for whatever reason
    # if server_proc.returncode != 0:
    #     pytest.fail("Exit code was non-zero ({})".format(server_proc.returncode))


# TODO: Use the fixture in the tests instead of this thing here
def test_integration(server_process):
    # This is just here so that the server_process fixture is initialized
    pass

    # exit_code = pytest.main(["./aw-server/tests", "-v"])
    # if exit_code != 0:
    #     pytest.fail("Tests exited with non-zero code: " + str(exit_code))


================================================
FILE: scripts/uninstall.sh
================================================
#!/bin/bash

modules=$(pip3 list --format=legacy | grep 'aw-' | grep -o '^aw-[^ ]*')

for module in $modules; do
    pip3 uninstall -y $module
done



================================================
FILE: scripts/update-deps.sh
================================================
#!/bin/bash

# Update dependency locks for each submodule in the activitywatch repo

set -e
set -x

# For submodule in submodules:
for submodule in $(git submodule | sed 's/^[+ ]//' | cut -d' ' -f2); do
    # Go to submodule
    cd $submodule

    # Check that we're on the master branch and latest commit
    if [ $(git rev-parse --abbrev-ref HEAD) != "master" ]; then
        echo "Submodule $submodule is not on master branch, aborting"
        exit 1
    fi

    # Update dependency locks
    # Use poetry if poetry.lock exists, or cargo if Cargo.toml exists
    if [ -f "poetry.lock" ]; then
        poetry update
    elif [ -f "Cargo.toml" ]; then
        cargo update
    fi

    # Go back to root
    cd ..
done

Download .txt
gitextract_zuvhj3r3/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.md
│   │   ├── config.yml
│   │   └── everything-else.md
│   ├── dependabot.yml
│   ├── stale.yml
│   └── workflows/
│       ├── build-tauri.yml
│       ├── build.yml
│       ├── codeql.yml
│       ├── dependabot-automerge.yml
│       ├── diagram.yml
│       ├── greetings.yml
│       ├── test.yml
│       └── winget.yml
├── .gitignore
├── .gitmodules
├── .tool-versions
├── CITATION.cff
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.txt
├── Makefile
├── README.md
├── SECURITY.md
├── aw.spec
├── gptme.toml
├── pyproject.toml
└── scripts/
    ├── build_changelog.py
    ├── changelog_contributors.csv
    ├── changelog_contributors_twitter.csv
    ├── checkout-latest-tag.sh
    ├── chores/
    │   └── make-release.sh
    ├── ci/
    │   ├── enable_long_paths.bat
    │   ├── import-macos-p12.sh
    │   ├── install_node.ps1
    │   ├── install_pyhook.ps1
    │   ├── install_python.ps1
    │   └── run_with_env.cmd
    ├── count_lines.sh
    ├── get_latest_release.sh
    ├── logcrawler.py
    ├── nop.sh
    ├── notarize.sh
    ├── package/
    │   ├── README.txt
    │   ├── activitywatch-setup.iss
    │   ├── aw-tauri.iss
    │   ├── build_app_tauri.sh
    │   ├── deb/
    │   │   └── control
    │   ├── dmgbuild-settings.py
    │   ├── entitlements.plist
    │   ├── getversion.sh
    │   ├── move-to-aw-modules.sh
    │   ├── package-all.sh
    │   ├── package-appimage.sh
    │   └── package-deb.sh
    ├── submodule-branch.sh
    ├── symlink-systemd.sh
    ├── tests/
    │   └── integration_tests.py
    ├── uninstall.sh
    └── update-deps.sh
Download .txt
SYMBOL INDEX (27 symbols across 4 files)

FILE: scripts/build_changelog.py
  function main (line 44) | def main():
  class CommitMsg (line 100) | class CommitMsg:
  class Commit (line 107) | class Commit:
    method msg_processed (line 114) | def msg_processed(self) -> str:
    method parse_type (line 136) | def parse_type(self) -> Optional[Tuple[str, str]]:
    method type (line 147) | def type(self) -> Optional[str]:
    method subtype (line 152) | def subtype(self) -> Optional[str]:
    method type_str (line 156) | def type_str(self) -> str:
    method format (line 160) | def format(self) -> str:
  function run (line 166) | def run(cmd, cwd=".") -> str:
  function pr_linkify (line 176) | def pr_linkify(prid: str, org: str, repo: str) -> str:
  function commit_linkify (line 180) | def commit_linkify(commitid: str, org: str, repo: str) -> str:
  function wrap_details (line 184) | def wrap_details(title, body, wraplines=5):
  function summary_repo (line 199) | def summary_repo(
  function remove_duplicates (line 319) | def remove_duplicates(s: List[str], minlen=10, only_sections=True) -> Li...
  function build (line 353) | def build(
  function _resolve_email (line 444) | def _resolve_email(email: str) -> Optional[str]:
  function get_all_contributors (line 495) | def get_all_contributors() -> set[str]:
  function get_twitter_of_ghusers (line 554) | def get_twitter_of_ghusers(ghusers: Collection[str]):

FILE: scripts/logcrawler.py
  function get_filepaths (line 14) | def get_filepaths():
  function collect (line 22) | def collect():
  function line_age (line 41) | def line_age(line):
  function main (line 53) | def main(exclude_testing: bool = False, limit_days: int = 10, limit_line...

FILE: scripts/package/dmgbuild-settings.py
  function icon_from_app (line 19) | def icon_from_app(app_path):

FILE: scripts/tests/integration_tests.py
  function _windows_kill_process (line 10) | def _windows_kill_process(pid):
  function server_process (line 22) | def server_process():
  function test_integration (line 78) | def test_integration(server_process):
Condensed preview — 61 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (168K chars).
[
  {
    "path": ".gitattributes",
    "chars": 210,
    "preview": "# See https://github.com/github/linguist for details\n\n# Trick to remove some build tools from language overview\nMakefile"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 336,
    "preview": "# Docs for this file can be found here:\n# https://docs.github.com/en/github/administering-a-repository/displaying-a-spon"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "chars": 1778,
    "preview": "---\nname: \"\\U0001F41E Bug report\"\nabout: Did you find a bug?\ntitle: ''\nlabels: 'type: bug'\nassignees: ''\n\n---\n\n<!--\n  Hi"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 995,
    "preview": "# Issue templates are based on templates for poetry:\n# https://github.com/python-poetry/poetry/tree/master/.github/ISSUE"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/everything-else.md",
    "chars": 841,
    "preview": "---\nname: \"\\U0001F5C3 Everything Else\"\nabout: For questions and issues that do not fall in any of the other categories.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 565,
    "preview": "# Set update schedule for GitHub Actions\nversion: 2\nupdates:\n  # Maintain dependencies for GitHub Actions\n  - package-ec"
  },
  {
    "path": ".github/stale.yml",
    "chars": 751,
    "preview": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 365\n# Number of days of inactivity before a"
  },
  {
    "path": ".github/workflows/build-tauri.yml",
    "chars": 7353,
    "preview": "name: Build Tauri\n\non:\n  push:\n    branches: [master]\n    tags:\n      - v*\n  pull_request:\n    branches: [master]\n\njobs:"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 8880,
    "preview": "name: Build\n\non:\n  push:\n    branches: [ master ]\n    tags:\n      - v*\n  pull_request:\n    branches: [ master ]\n  #relea"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "chars": 898,
    "preview": "name: \"CodeQL\"\n\non:\n  push:\n    branches: [ \"master\" ]\n  pull_request:\n    branches: [ \"master\" ]\n  schedule:\n    - cron"
  },
  {
    "path": ".github/workflows/dependabot-automerge.yml",
    "chars": 675,
    "preview": "name: Dependabot Auto-merge\n\n# NOTE: This workflow relies on a Personal Access Token from the @ActivityWatchBot user\n#  "
  },
  {
    "path": ".github/workflows/diagram.yml",
    "chars": 951,
    "preview": "name: Diagram\n\non:\n  workflow_dispatch: {}\n  push:\n    branches:\n      - diagram\n     #- master  # protected branch, can"
  },
  {
    "path": ".github/workflows/greetings.yml",
    "chars": 1218,
    "preview": "name: Greetings\n\non: [issues, pull_request]\n\njobs:\n  greeting:\n    runs-on: ubuntu-latest\n    if: github.repository_owne"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 5610,
    "preview": "name: Test\n\non:\n  #push:\n  #  branches: [ master ]\n  #pull_request:\n  #  branches: [ master ]\n  workflow_dispatch:\n\n\n\njo"
  },
  {
    "path": ".github/workflows/winget.yml",
    "chars": 364,
    "preview": "name: Publish to WinGet\non:\n  release:\n    types: [released]\njobs:\n  publish:\n    runs-on: windows-latest # action can o"
  },
  {
    "path": ".gitignore",
    "chars": 155,
    "preview": "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"
  },
  {
    "path": ".gitmodules",
    "chars": 1093,
    "preview": "[submodule \"aw-core\"]\n\tpath = aw-core\n\turl = https://github.com/ActivityWatch/aw-core.git\n[submodule \"aw-client\"]\n\tpath "
  },
  {
    "path": ".tool-versions",
    "chars": 55,
    "preview": "poetry 1.5.1\nnodejs 16.20.2\nrust nightly\npython 3.9.13\n"
  },
  {
    "path": "CITATION.cff",
    "chars": 456,
    "preview": "cff-version: 1.2.0\nmessage: \"If you use or refer to this software in your research, please cite it.\"\nauthors:\n- family-n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3212,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 5474,
    "preview": "How to Contribute\n=================\n\n<!-- This guide could be improved by following the advice at https://mozillascience"
  },
  {
    "path": "LICENSE.txt",
    "chars": 16726,
    "preview": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\""
  },
  {
    "path": "Makefile",
    "chars": 7807,
    "preview": "# =====================================\n# Makefile for the ActivityWatch bundle\n# =====================================\n"
  },
  {
    "path": "README.md",
    "chars": 14075,
    "preview": "<img title=\"ActivityWatch\" src=\"https://activitywatch.net/img/banner.png\" align=\"center\">\n\n<p align=\"center\">\n  <b>Recor"
  },
  {
    "path": "SECURITY.md",
    "chars": 796,
    "preview": "# Security Policy\n\n<!--\n## Supported Versions\n\nUse this section to tell people about which versions of your project are\n"
  },
  {
    "path": "aw.spec",
    "chars": 7457,
    "preview": "# -*- mode: python -*-\n# vi: set ft=python :\n\nimport os\nimport platform\nimport shlex\nimport subprocess\nfrom pathlib impo"
  },
  {
    "path": "gptme.toml",
    "chars": 291,
    "preview": "files = [\n    \"README.md\",\n    \"Makefile\",\n    \"aw-server/README.md\",\n    \"aw-server/aw-webui/README.md\",\n    \"aw-server"
  },
  {
    "path": "pyproject.toml",
    "chars": 1481,
    "preview": "[tool.poetry]\nname = \"activitywatch\"\nversion = \"0.13.2\"\ndescription = \"The free and open-source automated time tracker. "
  },
  {
    "path": "scripts/build_changelog.py",
    "chars": 19857,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nScript that generates a changelog for the repository and its submodules, and outputs it in th"
  },
  {
    "path": "scripts/changelog_contributors.csv",
    "chars": 3068,
    "preview": "2e3s\t2e3s19@gmail.com\n750\t37119951+750@users.noreply.github.com\nAlwinator\t39517491+Alwinator@users.noreply.github.com\nBa"
  },
  {
    "path": "scripts/changelog_contributors_twitter.csv",
    "chars": 126,
    "preview": "0xbrayo\tsubrupt\nchaoky\tchaokyer\nerikbjare\terikbjare\niloveitaly\tmike_bianco\nvedantmgoyal2009\tvedantmgoyal\nvictorlin\tvicto"
  },
  {
    "path": "scripts/checkout-latest-tag.sh",
    "chars": 232,
    "preview": "#!/bin/bash\n\nlatest_version_tag=$(git tag -l | grep \"^v[0-9]\\..*\" | sort --version-sort | tail -n1 )\ncurrent_version_tag"
  },
  {
    "path": "scripts/chores/make-release.sh",
    "chars": 132,
    "preview": "#!/bin/bash\n\n#\n# We should create a release checklist to ensure releases are consistent.\n#\n\n# Create an annotated tag\n#g"
  },
  {
    "path": "scripts/ci/enable_long_paths.bat",
    "chars": 203,
    "preview": ":: Enable long paths on Windows (needed when building since node_modules can create deep hierarchies)\n\nREG ADD \"HKLM\\SYS"
  },
  {
    "path": "scripts/ci/import-macos-p12.sh",
    "chars": 733,
    "preview": "#!/bin/sh\n\nset -e\n\n# Source: https://www.update.rocks/blog/osx-signing-with-travis/\nexport KEY_CHAIN=build.keychain\nexpo"
  },
  {
    "path": "scripts/ci/install_node.ps1",
    "chars": 1048,
    "preview": "$msipath = \"$PSScriptRoot\\node-installer.msi\"\n\nfunction RunCommand ($command, $command_args) {\n    Write-Host $command $"
  },
  {
    "path": "scripts/ci/install_pyhook.ps1",
    "chars": 424,
    "preview": "function main ($arch) {\n    If ( $arch -eq \"64\" ) {\n        $url=\"https://github.com/ActivityWatch/wheels/raw/master/pyH"
  },
  {
    "path": "scripts/ci/install_python.ps1",
    "chars": 7326,
    "preview": "# Sample script to install Python and pip under Windows\n# Authors: Olivier Grisel, Jonathan Helmus, Kyle Kastner, and Al"
  },
  {
    "path": "scripts/ci/run_with_env.cmd",
    "chars": 3365,
    "preview": ":: To build extensions for 64 bit Python 3, we need to configure environment\n:: variables to use the MSVC 2010 C++ compi"
  },
  {
    "path": "scripts/count_lines.sh",
    "chars": 1009,
    "preview": "#!/usr/bin/env bash\n\nre_ignore='.*(build|dist|venv|old|other|scripts|node|static).*'\n\necho -n \"Lines of code (excluding "
  },
  {
    "path": "scripts/get_latest_release.sh",
    "chars": 904,
    "preview": "#!/bin/bash\n\n# TODO: Merge with scripts/package/getversion.sh\n\n# Script that fetches the previous release (if current co"
  },
  {
    "path": "scripts/logcrawler.py",
    "chars": 2218,
    "preview": "import os\nimport re\nfrom datetime import datetime\nfrom collections import defaultdict\nimport logging\n\nimport aw_core\n\nlo"
  },
  {
    "path": "scripts/nop.sh",
    "chars": 71,
    "preview": "#!/bin/bash\n\necho \"nop.bat was executed as a workaround for something\"\n"
  },
  {
    "path": "scripts/notarize.sh",
    "chars": 2744,
    "preview": "#!/bin/bash\n\napplemail=$APPLE_EMAIL # Email address used for Apple ID\npassword=$APPLE_PASSWORD # See apps-specific passw"
  },
  {
    "path": "scripts/package/README.txt",
    "chars": 658,
    "preview": "Run move-to-aw-modules.sh to copy all modules except aw-tauri to ~/aw-modules/.\naw-tauri (replaces aw-qt) will use this "
  },
  {
    "path": "scripts/package/activitywatch-setup.iss",
    "chars": 2731,
    "preview": "; Script generated by the Inno Setup Script Wizard.\n; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FI"
  },
  {
    "path": "scripts/package/aw-tauri.iss",
    "chars": 2284,
    "preview": "; Inno Setup script for ActivityWatch (Tauri edition)\n;\n; This is separate from activitywatch-setup.iss (aw-qt) to avoid"
  },
  {
    "path": "scripts/package/build_app_tauri.sh",
    "chars": 3323,
    "preview": "#!/bin/bash\nset -e\n\n# Build a macOS .app bundle for the Tauri-based ActivityWatch.\n# This replaces the PyInstaller-based"
  },
  {
    "path": "scripts/package/deb/control",
    "chars": 231,
    "preview": "Package: activitywatch\nArchitecture: amd64\nMaintainer: Erik Bjäreholt <erik@bjareho.lt>\nDepends:\nPriority: optional\nVers"
  },
  {
    "path": "scripts/package/dmgbuild-settings.py",
    "chars": 3567,
    "preview": "# -*- coding: utf-8 -*-\nfrom __future__ import unicode_literals\n\nimport plistlib\nimport os.path\n\n# Use like this: dmgbui"
  },
  {
    "path": "scripts/package/entitlements.plist",
    "chars": 380,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "scripts/package/getversion.sh",
    "chars": 473,
    "preview": "#!/bin/bash\n\n# TODO: Merge with scripts/package/getversion.sh\n# set -e\n\nif [[ $TRAVIS_TAG ]]; then\n    _version=$TRAVIS_"
  },
  {
    "path": "scripts/package/move-to-aw-modules.sh",
    "chars": 822,
    "preview": "#!/bin/bash\n# Copy all AW modules to ~/aw-modules/ for aw-tauri to discover.\n# aw-tauri uses this directory to find and "
  },
  {
    "path": "scripts/package/package-all.sh",
    "chars": 2734,
    "preview": "#!/bin/bash\n\nset -e\n\nechoerr() { echo \"$@\" 1>&2; }\n\nfunction get_platform() {\n    # Will return \"linux\" for GNU/Linux\n  "
  },
  {
    "path": "scripts/package/package-appimage.sh",
    "chars": 1014,
    "preview": "#!/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_F"
  },
  {
    "path": "scripts/package/package-deb.sh",
    "chars": 1684,
    "preview": "#!/usr/bin/bash\n# Setting the shell is required, as `sh` doesn't support slicing.\n\n# Fail fast\nset -e\n# Verbose commands"
  },
  {
    "path": "scripts/submodule-branch.sh",
    "chars": 396,
    "preview": "#!/bin/bash\n\n# Get current branch\n#   git rev-parse --abbrev-ref HEAD\n# Get branch for each submodule\n#   git submodule "
  },
  {
    "path": "scripts/symlink-systemd.sh",
    "chars": 170,
    "preview": "#!/bin/bash\nfor module in \"aw-server\" \"aw-watcher-afk\" \"aw-watcher-x11\"; do\n    ln -s $(pwd)/$module/misc/${module}.serv"
  },
  {
    "path": "scripts/tests/integration_tests.py",
    "chars": 2791,
    "preview": "import os\nimport platform\nimport subprocess\nimport tempfile\nfrom time import sleep\n\nimport pytest\n\n\ndef _windows_kill_pr"
  },
  {
    "path": "scripts/uninstall.sh",
    "chars": 149,
    "preview": "#!/bin/bash\n\nmodules=$(pip3 list --format=legacy | grep 'aw-' | grep -o '^aw-[^ ]*')\n\nfor module in $modules; do\n    pip"
  },
  {
    "path": "scripts/update-deps.sh",
    "chars": 721,
    "preview": "#!/bin/bash\n\n# Update dependency locks for each submodule in the activitywatch repo\n\nset -e\nset -x\n\n# For submodule in s"
  }
]

About this extraction

This page contains the full source code of the ActivityWatch/activitywatch GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 61 files (154.4 KB), approximately 42.1k tokens, and a symbol index with 27 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

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

Copied to clipboard!