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: '' --- - [ ] 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. - **OS name and version**: - **ActivityWatch version**: ## Describe the bug ## To Reproduce ## Expected behavior ## Documentation ## Additional context ================================================ 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: '' --- - [ ] 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 ================================================ 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 ================= **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: ``` [optional scope]: [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: ## 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,,) 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 ================================================

Records what you do so that you can know how you've spent your time.
All in a secure way where you control the data.


WebsiteForumDocumentationReleases
Contributor statsCI overview



*Do you want to receive email updates on major announcements?*
***[Signup for the newsletter](http://eepurl.com/cTU6QX)!***
Table of Contents * [About](#about) * [Screenshots](#screenshots) * [Is this yet another time tracker?](#is-this-yet-another-time-tracker) * [Feature comparison](#feature-comparison) * [Installation & Usage](#installation--usage) * [About this repository](#about-this-repository) * [Server](#server) * [Watchers](#watchers) * [Libraries](#libraries) * [Contributing](#contributing)
## 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 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 | | 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[aw-qt]; aw-notify[aw-notify]; aw-server[aw-server]; aw-webui[aw-webui]; aw-watcher-window[aw-watcher-window]; aw-watcher-afk[aw-watcher-afk]; aw-watcher-web[aw-watcher-web]; aw-sync[aw-sync]; 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 ## 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 ## Reporting a Vulnerability 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 ", "Johan Bjäreholt "] 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
element if body is longer than `wraplines`""" out = f"\n\n### {title}" wrap = body.strip().count("\n") > wraplines if wrap: out += "\n
Click to expand\n

" out += f"\n{body.rstrip()}" if wrap: out += "\n\n

\n
" 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) (?\d+) \. (?\d+) \. (?\d+) (?[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 CFBundleDevelopmentRegion English CFBundleExecutable aw-tauri CFBundleIconFile icon.icns CFBundleIdentifier ${BUNDLE_ID} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${APP_NAME} CFBundlePackageType APPL CFBundleShortVersionString ${VERSION} CFBundleVersion ${VERSION} NSAppleEventsUsageDescription ActivityWatch needs access to monitor application usage NSHighResolutionCapable NSPrincipalClass NSApplication LSMinimumSystemVersion 10.14 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 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 ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory ================================================ 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