mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
107 lines
3.8 KiB
Python
107 lines
3.8 KiB
Python
"""The ``earner-bonus.path`` unit: apply a flat bonus the moment it is earned.
|
|
|
|
The live bonus pass used to run only on the 15-minute ``workout-sync`` timer,
|
|
so a LeetCode solve could wait a quarter of an hour for its shutdown hour
|
|
while the gaming hour landed in seconds (2026-10-06). The path unit watches
|
|
every flat earner's ledger and starts ``earner-bonus.service``
|
|
(``--apply-bonuses``) when a gate writes one.
|
|
|
|
The watch list is rendered from the ``earned_time`` registry, not kept by
|
|
hand. Re-pinning the registry is a pip install that never re-runs the
|
|
installer, so :func:`sync_path_unit` -- called from every ``--sync-only``
|
|
pass -- rewrites an installed unit that has drifted from the registry. A new
|
|
earner is watched within 15 minutes with no step to remember.
|
|
|
|
``PathChanged=`` and not ``PathModified=``: the gates write a temp file and
|
|
rename it into place, which ``PathChanged`` sees, including the first write
|
|
into a directory that does not exist yet. ``PathModified`` would also fire
|
|
mid-write on a gate that writes in place, and a half-written ledger reads as
|
|
"could not check".
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
from typing import Final
|
|
|
|
from screen_locker._earned import ledger_file, watched_earners
|
|
|
|
__all__ = ["PATH_UNIT_NAME", "USER_UNIT_DIR", "render_path_unit", "sync_path_unit"]
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
PATH_UNIT_NAME: Final = "earner-bonus.path"
|
|
USER_UNIT_DIR: Final = Path.home() / ".config" / "systemd" / "user"
|
|
|
|
_HEADER: Final = """\
|
|
# Generated by screen_locker._earner_units from the earned_time registry.
|
|
# Do not edit: the next --sync-only pass rewrites a unit that has drifted.
|
|
[Unit]
|
|
Description=Apply a flat earner's shutdown bonus as soon as its ledger changes
|
|
|
|
[Path]
|
|
"""
|
|
_FOOTER: Final = """
|
|
[Install]
|
|
WantedBy=default.target
|
|
"""
|
|
|
|
|
|
def render_path_unit() -> str:
|
|
"""The unit text: one ``PathChanged=`` per gate earner's ledger.
|
|
|
|
Every registry's gates, not just today's (``watched_earners``), so a
|
|
dated cutover (the Automation tutor's ``TUTOR_FROM``) needs no re-render
|
|
on the day. The header keeps saying "flat": changing it would rewrite the
|
|
installed unit for no behavioural change.
|
|
"""
|
|
ledgers = dict.fromkeys(ledger_file(e) for e in watched_earners())
|
|
watches = "".join(f"PathChanged={path}\n" for path in ledgers)
|
|
return _HEADER + watches + _FOOTER
|
|
|
|
|
|
def sync_path_unit(unit_dir: Path | None = None) -> bool:
|
|
"""Rewrite an installed path unit that no longer matches the registry.
|
|
|
|
An absent unit is left absent: it was never installed, or it was removed
|
|
on purpose, and installing units is the installer's job. *unit_dir*
|
|
defaults to :data:`USER_UNIT_DIR`, read at call time.
|
|
|
|
Returns True if the unit was rewritten.
|
|
"""
|
|
unit = (unit_dir or USER_UNIT_DIR) / PATH_UNIT_NAME
|
|
if not unit.is_file():
|
|
return False
|
|
wanted = render_path_unit()
|
|
try:
|
|
if unit.read_text(encoding="utf-8") == wanted:
|
|
return False
|
|
unit.write_text(wanted, encoding="utf-8")
|
|
subprocess.run(
|
|
["/usr/bin/systemctl", "--user", "daemon-reload"],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
except (OSError, subprocess.CalledProcessError) as exc:
|
|
_logger.warning(
|
|
"%s differs from the earner registry and could not be refreshed: %s"
|
|
" -- a newly registered earner waits for the 15-minute sync",
|
|
unit,
|
|
exc,
|
|
)
|
|
return False
|
|
_logger.warning(
|
|
"%s was out of date with the earner registry; rewritten and reloaded",
|
|
unit,
|
|
)
|
|
return True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# install_systemd.sh / arm.sh: write the unit into the given directory.
|
|
target = Path(sys.argv[1]) / PATH_UNIT_NAME
|
|
target.write_text(render_path_unit(), encoding="utf-8")
|