docs: add Gitea workflows, issue/PR templates, README and licence notice
CI / ci (pull_request) Has been cancelled
CI / android (pull_request) Has been cancelled

ci.yml calls `scripts/ci.sh` rather than restating the checks: a workflow that
reimplements them becomes a second definition of "done", and the two drift.

Android runs as a separate job. It fails when the SDK is absent while ci.sh
skips it — a developer should not be blocked by a missing toolchain, but a
silently skipped Android build in CI means regressions ship unnoticed. Every
action is pinned to an exact version.

LICENSE.md records that no licence is chosen yet, and why it is not a formality:
the choice is upstream of the UI toolkit decision, since Slint is GPL-3.0.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-10-01 15:56:13 +03:00
parent 5073c7321d
commit af8b28f6c6
8 changed files with 784 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
name: Bug report
description: Something behaves differently from how CLAUDE.md section 1 describes it
title: ""
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Two failure modes matter most here, and they need different fixes.
- **A block that should have fired but did not.**
- **A block that fired but should not have.**
Please pick which one you are reporting in the first question.
- type: dropdown
id: mode
attributes:
label: Which kind of failure is this?
description: The two modes below need completely different investigations.
options:
- A blocked app was not blocked
- A block fired when it should not have
- Enforcement stopped working entirely
- The app crashed or would not start
- Something else
- type: dropdown
id: platform
attributes:
label: Platform
options:
- Linux (KDE Plasma / Wayland)
- Linux (KDE Plasma / X11)
- Linux (other compositor)
- Windows
- Android
- More than one
- Not yet implemented on any
- type: input
id: version
attributes:
label: Version or commit
description: Output of `blocker-daemon --version`, or the commit SHA for a build from source.
validations:
required: true
- type: textarea
id: repro
attributes:
label: Steps to reproduce
description: |
Concrete steps, starting from a known state. "Open Discord during Focus" is
reproducible; "it blocks sometimes" is not.
placeholder: |
1. Start a schedule named "Focus", 09:00-17:00, targeting Discord
2. Focus Discord
3. Nothing happens
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behaviour
description: What does CLAUDE.md section 1 say should happen? Quote the requirement if you can.
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behaviour
description: What happened instead, including any error output.
validations:
required: true
- type: textarea
id: backend
attributes:
label: Which enforcement backend is active?
description: |
The troubleshooting screen shows this. If you cannot find it, say so — that is
itself useful information, because it suggests the daemon started with no
backend and cannot block anything.
placeholder: "kwin: KWin script loaded successfully"
- type: textarea
id: logs
attributes:
label: Daemon log output
description: |
From `journalctl --user -u blocker -n 100`.
Redact anything private. Do not paste API tokens, keys, or credentials.
render: shell
- type: checkboxes
id: checks
attributes:
label: Before submitting
options:
- label: I searched existing issues for this.
- label: I removed secrets and personal paths from anything I pasted above.
- label: I am not asking to block ADB, safe mode, factory reset, or OS recovery.
description: |
Those are never blocked, by design (CLAUDE.md section 6.7 and ADR-11). Such a
request will be closed with a link to the ADR rather than implemented.
+102
View File
@@ -0,0 +1,102 @@
name: Platform limitation
description: A case where the OS will not permit what CLAUDE.md asks for
title: ""
labels: ["limitation"]
body:
- type: markdown
attributes:
value: |
CLAUDE.md section 6 lists the hard limits this project is known to have. This
template exists so a newly discovered one is recorded rather than quietly worked
around.
Section 0 rule 2: never fake a feature. If something cannot be enforced on a
platform, we implement the closest honest approximation and document the gap.
This form is how that documentation starts.
- type: dropdown
id: existing
attributes:
label: Is this already documented?
description: |
If it matches a numbered limitation in CLAUDE.md section 6, please comment on
that discussion instead of opening a new issue.
options:
- "No, this is new"
- "Yes, but our handling of it is wrong or incomplete"
- "Not sure"
- type: input
id: section
attributes:
label: Which section 6 limitation does this relate to?
description: "A number 1-8, or 'new'."
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
options:
- Linux (KDE Plasma / Wayland)
- Linux (KDE Plasma / X11)
- Linux (wlroots compositor)
- Linux (GNOME)
- Windows
- Android
- More than one
- type: textarea
id: reality
attributes:
label: What does the OS refuse to do?
description: |
The OS-level fact. Not "we could not figure it out" — what the platform makes
impossible or requires privileges we do not have.
placeholder: |
Wayland has no portable API for the focused window. Each compositor exposes its
own, and none is standard.
validations:
required: true
- type: textarea
id: workaround
attributes:
label: What do we do instead?
description: |
The closest honest approximation. If the answer is "nothing", say that. An
unimplemented limitation is a valid and common answer.
placeholder: "Process-based detection, which cannot show a block screen."
validations:
required: true
- type: textarea
id: privileges
attributes:
label: What privileges would a real fix need?
description: |
Root, CAP_SYS_ADMIN, an accessibility service, a browser extension, a Windows
service install? This determines whether the limitation is permanent or a
matter of permission design.
validations:
required: true
- type: checkboxes
id: docs
attributes:
label: Documentation
options:
- label: This gap is documented in docs/LIMITATIONS.md
description: |
Required. An undocumented limitation is worse than an unimplemented one,
because the docs will imply a capability that does not exist.
- type: checkboxes
id: notblocked
attributes:
label: Confirm
options:
- label: |
This is not a request to block ADB, root access, safe mode, factory reset, or
OS recovery. Those are never blocked, by design (section 6.7, ADR-11).
+69
View File
@@ -0,0 +1,69 @@
name: Question
description: An open decision for the project owner, or something blocking work
title: ""
labels: ["question"]
body:
- type: markdown
attributes:
value: |
CLAUDE.md section 10.3: any open question becomes an issue labelled
"question" instead of being buried in a chat thread where it gets lost.
Most questions here are ones the agent cannot answer alone — section 0 rule 3
requires stopping to ask before adding an unlisted dependency, changing the
architecture, or choosing between two non-obvious OS mechanisms.
- type: textarea
id: question
attributes:
label: The question
description: One question. If there are several, they need separate issues.
validations:
required: true
- type: dropdown
id: kind
attributes:
label: What kind of decision is this?
options:
- Dependency to add (section 0 rule 3)
- Architecture change (section 0 rule 3)
- Choice between OS mechanisms (section 0 rule 3)
- Licensing
- Scope or priority
- Something else
- type: input
id: blocks
attributes:
label: What does this block?
description: Which milestone, and what specifically cannot proceed.
placeholder: "M2-Linux: the KWin backend cannot be written until the block-window approach is chosen."
validations:
required: true
- type: textarea
id: options
attributes:
label: Options and trade-offs
description: |
What are the realistic choices, and what does each cost? Include binary size,
licence, and maintenance burden where they apply.
validations:
required: true
- type: textarea
id: recommendation
attributes:
label: Recommendation, if you have one
description: |
Optional. A default the agent has been proceeding with, so it can keep working
while you decide. Say here if you would rather it stop and wait.
- type: textarea
id: research
attributes:
label: Supporting research
description: |
Measurements, documentation links, or a comparison. Links are more useful than
conclusions.
+88
View File
@@ -0,0 +1,88 @@
<!--
Pull request template for Blocker.
CLAUDE.md section 10.2 requires each PR to state: what changed, which section 1
requirement it covers, test evidence, and perf numbers if it touches the hot path.
Test evidence means pasted output. A claim that tests pass is not evidence; the
output is. Section 10.6: if a check fails, say so in the PR — do not weaken a test
to make it pass.
-->
## What changed
<!-- What this PR does, in a paragraph. Not a file list — the diff is the file list. -->
Refs #
## Which requirement this covers
<!--
Quote the relevant part of CLAUDE.md section 1. If this covers none, say so plainly
and explain why the change exists anyway — refactors and CI changes legitimately
cover no user-facing requirement, but that should be visible rather than assumed.
-->
Section 1:
## Test evidence
<!--
Paste the actual output of `scripts/ci.sh`. Not "all tests pass" — the output.
Note any check that SKIPPED, and why. A skip is not a pass, and saying so here is
part of the evidence.
```
$ scripts/ci.sh
...
```
## Performance
<!--
Required if this touches the hot path — the evaluator, the event loop, the block
screen, or anything that runs on every focus change (section 9).
Otherwise write "not affected" and say why.
-->
Measured against section 5:
| Metric | Target | Before | After |
|---|---|---|---|
| | | | |
Command used:
```
## Checklist
- [ ] Each commit builds and passes tests on its own (section 10.2)
- [ ] Commits are conventional (`feat:`, `fix:`, `perf:`, `refactor:`, `test:`, `docs:`, `ci:`)
- [ ] `scripts/ci.sh` passes, or every skip is explained above
- [ ] New golden decision-table cases are **added**, and behaviour changes are
matched by a case change in the same commit (the add-before-change rule)
- [ ] Every fallible OS or I/O call is handled — no `unwrap()`/`expect()` outside
tests (section 0 rule 5)
- [ ] Perf budget re-measured if the hot path was touched (section 9)
- [ ] Any platform that cannot comply has its gap documented in `docs/LIMITATIONS.md`
- [ ] No credentials, keystores, `.env` files, or local databases in the diff
- [ ] No secret, API token, SSH key, or private path appears in the diff
- [ ] No dependency added without sign-off (section 0 rule 3) and an ADR entry
## Notes for the reviewer
<!--
Anything you would flag yourself: a place you are unsure about, a shortcut taken, a
decision that needs a second opinion.
If a check fails, say so here. A failing check stated plainly is fine. A failing
check hidden by a weakened test is not, and section 10.6 prohibits it.
-->
## Do not merge this yourself
The agent does not merge its own PR (section 10.2). Squash or rebase merge, keep
history linear. Tag the milestone when it lands.
+119
View File
@@ -0,0 +1,119 @@
# Gitea Actions setup
**The owner has not yet confirmed whether a runner is registered on this Gitea.**
Until one is, `scripts/ci.sh` is the only thing gating merges, and every check must
be run locally before opening a PR.
The workflows in this directory are written for **act_runner**, Gitea's
act-compatible runner, which executes jobs in Docker. They are also close enough to
GitHub Actions syntax that a GitHub-style runner would work with the labels
adjusted.
---
## Labels these workflows expect
| File | `runs-on` | Needs |
|---|---|---|
| `ci.yml` → `ci` | `ubuntu-latest` | Rust toolchain, no Android SDK |
| `ci.yml` → `android` | `android-build` | JDK 17 + Android SDK 35 |
If your runner is registered under different labels, change `runs-on` here. Both
must be changed together — a mismatch shows up as a job that queues forever, with
no error message, which is the worst failure mode of a mislabelled runner.
---
## Registering act_runner
On the runner host, with Docker present:
```sh
# 1. Generate a registration token, in Gitea's UI:
# Site Administration → Actions → Runners → Create new runner
# Copy the token.
# 2. Register. --instance is the URL Gitea is reachable at *from the runner*.
act_runner register \
--instance https://git.valiq.vu \
--token "<REGISTRATION_TOKEN>" \
--name blocker-ci \
--labels ubuntu-latest:docker://node:20-bookworm \
--labels android-build:docker://ghcr.io/cirruslabs/android-sdk:34
# 3. Start it.
./act_runner run --server http://localhost:8080
```
Each `--labels` entry is `name:docker://image`. The runner runs the job inside that
container, so the image must contain everything the job needs:
- `ubuntu-latest` — needs `bash`, `git`, `curl`, and Rust. The workflow installs
Rust via `dtolnay/rust-toolchain`, so a Rust-free base image is fine.
- `android-build` — needs JDK 17 and the Android SDK, hence the Cirrus Labs image.
### Running act_runner as a service
```ini
# /etc/systemd/system/act_runner.service
[Unit]
Description=Gitea Actions runner
After=docker.service
Requires=docker.service
[Service]
Type=simple
User=git
WorkingDirectory=/opt/act_runner
ExecStart=/opt/act_runner/act_runner run --server http://localhost:3000 --config /opt/act_runner/config.yml
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
---
## What the runner still needs from the owner
CLAUDE.md section 10.4 says to ask whether the runner has the Android SDK. **Not
yet answered.** The two behaviours are deliberately different until it is:
- `scripts/ci.sh` **skips** Android when no SDK is found, and prints SKIP with the
reason. A developer should not be blocked from running checks by a missing
toolchain.
- The `android` job in CI **fails** when the SDK is missing. A silently skipped
Android build in CI means Android regressions ship unnoticed.
Same check, opposite defaults, because the contexts are opposite. If you would
rather the CI job also skip, say so and it will be changed — but the failure mode
of the current setting is "you find out from a user", which is worse.
---
## Why the workflow calls scripts/ci.sh
`ci.yml` runs `bash scripts/ci.sh` rather than repeating the fmt/clippy/test/size
steps inline.
The reason is drift. A workflow that reimplements the checks is a second
definition of "done". The two diverge — someone adds a check locally, forgets CI,
and green ticks mean nothing. One script, two callers, no drift.
The workflow keeps separate steps for checkout, toolchain install, caching, and
artifact upload, because those are genuinely different between a runner and a
laptop.
---
## Action pinning
Every action is pinned to an exact version (`@v4.1.7`, not `@v4`, and never `@main`).
A floating tag means upstream can change what runs in your CI without a review, a
tag can be repointed, and `@main` can change arbitrarily. On a self-hosted runner
that is a supply chain problem: the code that decides whether a change is safe to
merge is code someone else controls.
Bumping a pin is its own commit, so the diff shows what changed.
+125
View File
@@ -0,0 +1,125 @@
name: CI
# Section 10.4 requires every PR to pass these checks. This workflow does not
# reimplement them: it runs `scripts/ci.sh`, which is the single source of truth
# for what must pass. Reimplementing the steps here is how CI and local checks
# drift apart, and then one of them is lying to you.
on:
push:
branches: [main]
pull_request:
# Superseded PR runs are cancelled rather than left to finish and report on a
# commit that no longer exists.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
# Matches the act_runner label in .gitea/workflows/README.md. Change both
# together if the runner is registered under a different label.
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Check out
uses: actions/checkout@v4.1.7
# Pinned to an exact version, never a floating tag. A floating tag lets
# upstream change what runs in your CI without review, which is a supply
# chain problem, not a convenience.
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
# rust-toolchain.toml pins the channel and components, so this action
# installs exactly what the project asks for and CI matches local.
- name: Cache cargo
uses: Swatinem/rust-cache@v2.7.5
- name: Cache cargo registry
uses: actions/cache@v4.1.2
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
- name: Run all checks
# scripts/ci.sh runs: fmt --check, clippy -D warnings, cargo test, the 5 MB
# size gate, Android lint (skipped loudly without an SDK), and gitleaks.
#
# A skip inside ci.sh prints SKIP with the reason and is listed in the
# summary as "NOT a pass", so an unverified Android build can never be
# mistaken for a verified one.
run: bash scripts/ci.sh
- name: Upload the release daemon
# Section 10.5 requires releases to carry SHA-256 checksums. Producing
# the checksum here means the release pipeline never has to trust an
# artifact it did not build itself.
if: always()
run: |
set -euo pipefail
BIN="${CARGO_TARGET_DIR:-$HOME/.blocker-target}/release/blocker-daemon"
mkdir -p dist
if [ -f "$BIN" ]; then
cp "$BIN" dist/blocker-daemon
sha256sum dist/blocker-daemon > dist/blocker-daemon.sha256
else
echo "release daemon not built; nothing to upload"
fi
- uses: actions/upload-artifact@v4.4.3
if: always()
with:
name: blocker-daemon-linux
path: dist/
if-no-files-found: warn
# A separate job, rather than a step, for two reasons: it needs a different
# runner label (a runner with the Android SDK), and its failure should be
# legible on its own rather than buried in the middle of the CI output.
#
# Note the asymmetry with ci.sh, which is deliberate. ci.sh SKIPS Android when
# no SDK is present, because a developer running checks locally should not be
# blocked by a missing toolchain. This job FAILS when the SDK is absent,
# because a silently skipped Android build in CI means Android regressions ship
# unnoticed. Same check, opposite defaults, for opposite contexts.
android:
runs-on: android-build
timeout-minutes: 60
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4.1.7
- uses: actions/setup-java@v4.4.0
with:
distribution: temurin
java-version: '17'
- uses: android-actions/setup-android@v3.2.2
- name: Accept SDK licences
run: yes | sdkmanager --licenses > /dev/null 2>&1 || true
- name: Cache Gradle
uses: actions/cache@v4.1.2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle.kts') }}
restore-keys: ${{ runner.os }}-gradle-
# Sideload flavour only. There is deliberately no Play Store signing path
# in CI: sections 6.8 and 4.1 record that the strongest friction
# (Stubborn level, Device Admin uninstall block) is hostile to Play Store
# policy and is intended for sideload or F-Droid distribution.
- name: Lint and assemble release
working-directory: android
run: ./gradlew lint assembleRelease
+50
View File
@@ -0,0 +1,50 @@
# Licence: not yet chosen
Blocker has **no licence yet**. This is deliberate, and it is recorded as ADR-17
rather than left as an oversight.
## Why nothing was applied
Applying a licence is a legal commitment, and it is the owner's to make. CLAUDE.md
section 0 rule 3 requires stopping to ask before decisions of that kind — and the
temptation to "just pick MIT" is exactly the kind of well-meant overreach that rule
exists to prevent.
There is a second reason specific to this project, and it is not a formality.
**The licence choice and the UI toolkit choice interact.** ADR-6 leaves the desktop
toolkit undecided between Slint and egui. Slint is GPL-3.0 or commercial; egui is
MIT or Apache-2.0. Choosing a permissive licence and then Slint would force the
project GPL, and discovering that after building against Slint would mean either
relicensing under copyleft or paying for a commercial licence — so the licence is
genuinely upstream of the toolkit decision, not beside it.
Until both are settled, the project stays unlicensed and unpublished.
## What this means in practice
- The code is not yet published anywhere. The repository is on a self-hosted Gitea.
- Nothing here is released, distributed, or offered to third parties.
- No licence header is asserted in any source file, because asserting one would be
asserting something untrue.
## What is needed to resolve this
1. **A licence choice.** The realistic candidates for a project intended to be
published: MIT, Apache-2.0, AGPL-3.0, or a dual licence.
2. **The UI toolkit decision** (ADR-6), which follows from it — or is decided on
measurement first, with the licence chosen to accommodate it.
## If you want to publish before this is settled
Don't. An unlicensed repository is, by default, not open source and not usable by
anyone else, and publishing it that way is a mess to unpick.
If you are in a hurry, the fastest honest path is: pick MIT or Apache-2.0 (both
permissive, both compatible with egui), then decide the toolkit with the licence
constraint known. That is a suggestion, not a decision — it is the owner's call.
---
*Recorded in `docs/DECISIONS.md` as ADR-17. Remove this file when a licence is
chosen, and add `LICENSE` with the actual terms.*
+122
View File
@@ -0,0 +1,122 @@
# Blocker
A cross-platform app and website blocker for **Android, Windows and Linux**.
One shared Rust core owns every decision. Platform code observes the OS and
enforces what the core decides. That is what makes the same schedule behave
identically on all three, and it is testable rather than aspirational — the golden
decision tables in `tests/` are the parity contract, so a platform disagreeing
with the core fails the build instead of producing a bug report.
## Status
**M0 — repository bootstrap.** The core engine exists and is tested; nothing
enforces anything yet. No platform has a working build.
| Milestone | State |
|---|---|
| M0 repo bootstrap | in progress |
| M1 core engine | engine, lock timers and safety guarantees written and tested; storage has no I/O yet |
| M2-Linux | not started — daemon is a placeholder |
| M3-Android | not started |
| M4 conditions | not started |
| M5 websites | not started |
| M6 Strict Mode + friction | not started |
| M7 statistics, settings, releases | not started |
Milestone order is Linux-first rather than Android-first. See ADR-15.
## What it will do
- Schedules of apps, websites and keywords, with time windows, usage limits,
launch counts, Wi-Fi and location conditions
- Blocklist and allowlist modes, with strictest-wins when schedules overlap
- QuickBlock: block a chosen set for a chosen duration
- Strict Mode and a configurable friction ladder, so turning blocking off takes
deliberate effort in a weak moment while remaining always possible
- Local usage statistics
## What it will not do
This is a friction tool, not a wall. A user with root, ADB, safe mode, a factory
reset, or desktop admin rights can always remove it, and that is intended. Factory
reset, safe mode and OS recovery are never interfered with. The goal is to outlast
an impulse, not to defeat a determined decision.
There is also a hardcoded list of things that can never be blocked — the phone and
dialler, emergency calls and alerts, and on desktop the OS shell and the Settings
basics needed to keep the machine usable. There is no setting that changes it.
See [`docs/LIMITATIONS.md`](docs/LIMITATIONS.md) for all eight platform limits,
stated plainly, plus the shortfalls in the current code.
## Build and test
Requires a stable Rust toolchain. The toolchain is pinned in `rust-toolchain.toml`.
```sh
scripts/ci.sh # everything, stops at the first failure
scripts/ci.sh --keep-going # everything, summarises all failures
cargo test --workspace # tests only
cargo build --workspace # build only
scripts/size-check.sh # the 5 MB stripped daemon gate
```
Build artifacts go to `$HOME/.blocker-target`, outside the repository, so `target/`
never shows up in `git status` and can be deleted in one command. Override with
`CARGO_TARGET_DIR`.
Two checks skip on a machine without the relevant toolchain — Android without an
SDK, the secret scan without gitleaks. Both **skip loudly** and are listed in the
summary as not-passes. A skip is never reported as a pass.
## Layout
```
core/ the decision engine — pure, no OS calls, no dependencies
tests/ golden decision tables, and the engine purity check
desktop/daemon/ long-lived observer and enforcer (5 MB budget)
desktop/ui/ block-screen window, launched on demand
desktop/platform-linux/ ForegroundWatcher backends, enforcement
desktop/platform-windows/ same, Windows
android/ not started
docs/ architecture, ADRs, limitations, permissions, perf, testing
scripts/ ci.sh, size-check.sh
```
## Documentation
| Document | What it is for |
|---|---|
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | The purity contract, `next_wakeup`, and why the engine has no dependencies |
| [`docs/DECISIONS.md`](docs/DECISIONS.md) | 17 ADRs, including the two still awaiting a decision |
| [`docs/LIMITATIONS.md`](docs/LIMITATIONS.md) | What the OS will not let us do, and what is not built yet |
| [`docs/PERMISSIONS.md`](docs/PERMISSIONS.md) | Every permission, and what degrades without it |
| [`docs/PERF.md`](docs/PERF.md) | Section 5 budgets, and the measurements taken so far |
| [`docs/TESTING.md`](docs/TESTING.md) | The golden-table contract and the manual checklists |
| [`CLAUDE.md`](CLAUDE.md) | The project specification. It governs. |
## Performance budgets
| Metric | Target |
|---|---|
| Idle CPU | ~0%, no wakeups without events |
| Idle RAM (daemon) | ≤ 15 MB RSS |
| Block latency | ≤ 150 ms |
| Daemon binary | ≤ 5 MB stripped |
| APK size | ≤ 10 MB |
Currently measured: **0.279 MB** stripped daemon — a placeholder that prints two
lines. The meaningful number comes in M2-Linux. Enforced in CI from the first
commit by `scripts/size-check.sh`.
## Licence
**Not yet chosen.** See ADR-17. Choosing a licence is the owner's decision, and it
interacts with the UI toolkit choice in ADR-6 (Slint is GPL-3.0 or commercial), so
it is not something to settle by accident.
## A note on origin
Blocker is a functional reimplementation of the behaviour of existing app blockers.
It reuses no third-party name, logo, icon, copy, or screenshot — only behaviour.