snapshot of https://github.com/ActivityWatch/activitywatch.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Research feed publisher tests / test (push) Failing after 3s
Release / Pre-flight checks (push) Failing after 0s
Release / Research Edition — patch and packaging smoke tests (push) Has been skipped
Release / Build Qt artifacts (false, 22, ubuntu-22.04, 3.9, ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) }}, false, false) (push) Has been skipped
Release / Build Qt artifacts (false, 22, ubuntu-22.04, 3.9, false, false, false) (push) Has been skipped
Release / Build Qt artifacts (manylinux_2_28 — glibc 2.28 ABI floor) (22, 3.9) (push) Has been skipped
Release / Generate release notes (push) Has been skipped
Release / Create dev release tag (push) Has been skipped
CodeQL / Analyze (javascript) (push) Failing after 1s
CodeQL / Analyze (python) (push) Failing after 1s
Release / Build Qt artifacts (false, 22, windows-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, macos-15-intel, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, macos-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04, 3.9, ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) }}, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04-arm, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, windows-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Qt artifacts (false, 22, macos-15-intel, 3.9, false, false, false) (push) Has been cancelled
Release / Build Qt artifacts (false, 22, macos-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Publish draft release (push) Has been cancelled
Some checks failed
Research feed publisher tests / test (push) Failing after 3s
Release / Pre-flight checks (push) Failing after 0s
Release / Research Edition — patch and packaging smoke tests (push) Has been skipped
Release / Build Qt artifacts (false, 22, ubuntu-22.04, 3.9, ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) }}, false, false) (push) Has been skipped
Release / Build Qt artifacts (false, 22, ubuntu-22.04, 3.9, false, false, false) (push) Has been skipped
Release / Build Qt artifacts (manylinux_2_28 — glibc 2.28 ABI floor) (22, 3.9) (push) Has been skipped
Release / Generate release notes (push) Has been skipped
Release / Create dev release tag (push) Has been skipped
CodeQL / Analyze (javascript) (push) Failing after 1s
CodeQL / Analyze (python) (push) Failing after 1s
Release / Build Qt artifacts (false, 22, windows-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, macos-15-intel, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, macos-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04, 3.9, ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) }}, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04-arm, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, windows-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Qt artifacts (false, 22, macos-15-intel, 3.9, false, false, false) (push) Has been cancelled
Release / Build Qt artifacts (false, 22, macos-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Publish draft release (push) Has been cancelled
This commit is contained in:
164
scripts/emit_research_category_preset.py
Normal file
164
scripts/emit_research_category_preset.py
Normal file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Emit the Research Edition category preset consumed by aw-webui at build time.
|
||||
|
||||
The approved study contract keeps application names while the watcher replaces
|
||||
browser titles with study categories and removes every URL. aw-webui categorises
|
||||
client-side, so this preset matches both stored browser category labels and the
|
||||
known raw application-name aliases. Top Applications can therefore show Word,
|
||||
Spotify, and Teams while Top Categories still uses the study taxonomy.
|
||||
|
||||
aw-webui (ActivityWatch/aw-webui#936) reads a preset category set from the
|
||||
`AW_PRESET_CATEGORY_SETS` env var at build time. This script derives that
|
||||
preset from the same taxonomy source as the watcher build patch, so the two can
|
||||
never drift:
|
||||
|
||||
python3 scripts/emit_research_category_preset.py > preset.json
|
||||
|
||||
Rules are exact, case-insensitive matches. aw-webui applies every category rule
|
||||
to `app` and `title`, and the oldest web UI pinned by the release carriers drops
|
||||
unknown per-rule metadata, so the preset cannot rely on field or priority keys.
|
||||
Each category carries a `data.color` so the Activity view is not unstyled
|
||||
(ActivityWatch/activitywatch#1439). Explicitly excluded app aliases map to
|
||||
`Excluded`; unknown applications remain `Uncategorized` instead of overlapping
|
||||
every specific rule with a catch-all.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
# Characters that are special to BOTH Python's `re` and JavaScript's RegExp
|
||||
# outside a character class. Escaping is deliberately restricted to these.
|
||||
#
|
||||
# `re.escape()` is not usable here: it escapes space as `\ ` and `&` as `\&`,
|
||||
# which are *invalid identity escapes* in JavaScript unicode-mode regex. Names
|
||||
# like "AI Chatbots & Assistants" then throw `Invalid escape` under `new
|
||||
# RegExp(r, "u")` -- 14 of the 18 study categories do. They happen to compile
|
||||
# today only because aw-webui builds the regex without the `u` flag, and the
|
||||
# failure would present as "categories don't show", i.e. indistinguishable from
|
||||
# the bug this preset exists to fix. Escape only what both engines agree on.
|
||||
_REGEX_METACHARACTERS = set(r"\^$.|?*+()[]{}")
|
||||
|
||||
PRESET_ID = "research-study"
|
||||
PRESET_NAME = "Research Edition study categories"
|
||||
|
||||
# Qualitative palette for the study taxonomy. aw-webui only colors a category
|
||||
# when `data.color` is set — there is no name-hash fallback for categories —
|
||||
# so omitting this is what made the research-study set render grey.
|
||||
# Keys must stay in lockstep with CATEGORY_MAP ∪ APP_CATEGORY_MAP; build_preset
|
||||
# raises if a category is missing.
|
||||
CATEGORY_COLORS: dict[str, str] = {
|
||||
"AI Chatbots & Assistants": "#7B1FA2",
|
||||
"Banking & Finance": "#1B5E20",
|
||||
"Education & Learning": "#1565C0",
|
||||
"Email": "#00838F",
|
||||
"Excluded": "#BDBDBD",
|
||||
"Games": "#EF6C00",
|
||||
"Messaging": "#00897B",
|
||||
"Music & Audio": "#7CB342",
|
||||
"News & Current Affairs": "#C62828",
|
||||
"Public Services": "#455A64",
|
||||
"Search & Navigation": "#5C6BC0",
|
||||
"Sensitive / Excluded": "#757575",
|
||||
"Shopping - Goods": "#6D4C41",
|
||||
"Shopping - Groceries & Food": "#F9A825",
|
||||
"Social Networking": "#AD1457",
|
||||
"Travel & Mobility": "#0277BD",
|
||||
"Video Streaming": "#E53935",
|
||||
"Work & Productivity": "#2E7D32",
|
||||
}
|
||||
|
||||
_PATCHER = pathlib.Path(__file__).with_name("patch_research_edition_config.py")
|
||||
|
||||
|
||||
def _load_category_source():
|
||||
"""Import the patch script without executing its CLI entry point."""
|
||||
spec = importlib.util.spec_from_file_location("_re_patcher", _PATCHER)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"could not load {_PATCHER}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["_re_patcher"] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def escape_portable(value: str) -> str:
|
||||
"""Escape `value` so the result is a literal in both Python and JS regex.
|
||||
|
||||
Portable across engines *and* across JS flag modes, unlike `re.escape()`.
|
||||
"""
|
||||
return "".join(
|
||||
"\\" + char if char in _REGEX_METACHARACTERS else char for char in value
|
||||
)
|
||||
|
||||
|
||||
def exact_alternation(values: set[str]) -> str:
|
||||
"""Build a stable whole-value alternation portable across Python and JS."""
|
||||
escaped = [escape_portable(value) for value in sorted(values)]
|
||||
return f"^(?:{'|'.join(escaped)})$"
|
||||
|
||||
|
||||
def color_for(category: str) -> str:
|
||||
"""Look up the Activity-view color for a study category.
|
||||
|
||||
Missing keys fail the build rather than ship an unstyled taxonomy.
|
||||
"""
|
||||
try:
|
||||
return CATEGORY_COLORS[category]
|
||||
except KeyError as exc:
|
||||
raise RuntimeError(
|
||||
f"study category {category!r} has no color in CATEGORY_COLORS"
|
||||
) from exc
|
||||
|
||||
|
||||
def build_preset() -> dict:
|
||||
source = _load_category_source()
|
||||
|
||||
categories = {category for _, category in source.CATEGORY_MAP}
|
||||
categories |= set(source.APP_CATEGORY_MAP.values())
|
||||
if not categories:
|
||||
raise RuntimeError("no categories found -- refusing to emit an empty preset")
|
||||
|
||||
extra_colors = set(CATEGORY_COLORS) - categories
|
||||
if extra_colors:
|
||||
raise RuntimeError(
|
||||
"CATEGORY_COLORS has entries not in the study taxonomy: "
|
||||
+ ", ".join(sorted(extra_colors))
|
||||
)
|
||||
|
||||
return {
|
||||
"id": PRESET_ID,
|
||||
"name": PRESET_NAME,
|
||||
# Sorted so the same taxonomy always produces a byte-identical preset.
|
||||
"categories": [
|
||||
{
|
||||
"name": [category],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": exact_alternation(
|
||||
{category}
|
||||
| {
|
||||
app
|
||||
for app, app_category in source.APP_CATEGORY_MAP.items()
|
||||
if app_category == category
|
||||
}
|
||||
),
|
||||
"ignore_case": True,
|
||||
},
|
||||
"data": {"color": color_for(category)},
|
||||
}
|
||||
for category in sorted(categories)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
preset = build_preset()
|
||||
# Compact and newline-free: this is written straight into $GITHUB_ENV,
|
||||
# which treats a newline as the end of the value.
|
||||
print(json.dumps([preset], separators=(",", ":")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user