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