mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-12 20:10:53 +01:00
Six text-free styles (gradient, flow/Voronoi, habit heatmap, shutdown clock, Earth day/night, plasma) replace the white i3 root. A 15-minute systemd user timer picks a new random style per monitor every 3 hours, unlike its previous one and the other monitor's; live styles refresh in between. Also syncs i3/config with the live config and prunes the oldest evidence artifact for the 60-file cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWX51FMD58yKUC1eYWf
126 lines
4.5 KiB
Python
126 lines
4.5 KiB
Python
"""Branches of the data-driven styles: clock conf, habit ledgers, Earth maps."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, date, datetime
|
|
import json
|
|
from typing import TYPE_CHECKING
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
from python_pkg.wallpaper_rotator import _style_clock as _clock
|
|
from python_pkg.wallpaper_rotator import _style_earth as _earth
|
|
from python_pkg.wallpaper_rotator import _style_heatmap as _heatmap
|
|
from python_pkg.wallpaper_rotator._context import RenderContext
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
|
|
def _ctx(tmp_path: Path, now: datetime, conf: Path | None = None) -> RenderContext:
|
|
return RenderContext(
|
|
now=now,
|
|
data_home=tmp_path / "share",
|
|
earth_dir=tmp_path / "earth",
|
|
shutdown_conf=conf or tmp_path / "missing.conf",
|
|
)
|
|
|
|
|
|
def test_schedule_defaults_when_conf_is_missing(tmp_path: Path) -> None:
|
|
assert _clock.schedule(tmp_path / "nope.conf", 0) == (300, 1380)
|
|
|
|
|
|
def test_schedule_reads_numbers_and_skips_junk_lines(tmp_path: Path) -> None:
|
|
conf = tmp_path / "s.conf"
|
|
conf.write_text(
|
|
"no equals sign\n"
|
|
"MON_WED_MINUTES=1200\n"
|
|
"THU_SUN_MINUTES = 1300 \n"
|
|
"MORNING_END_MINUTES=abc\n",
|
|
encoding="utf-8",
|
|
)
|
|
assert _clock.schedule(conf, 2) == (300, 1200)
|
|
assert _clock.schedule(conf, 3) == (300, 1300)
|
|
|
|
|
|
def test_clock_after_shutdown_draws_only_the_dim_ring(tmp_path: Path) -> None:
|
|
late = _ctx(tmp_path, datetime(2026, 11, 11, 23, 30, tzinfo=UTC))
|
|
image = _clock.render(late, (200, 120), np.random.default_rng(0))
|
|
assert image.size == (200, 120)
|
|
|
|
|
|
def test_clock_tone_is_calmer_hours_before_shutdown(tmp_path: Path) -> None:
|
|
early = _ctx(tmp_path, datetime(2026, 11, 11, 8, 0, tzinfo=UTC))
|
|
late = _ctx(tmp_path, datetime(2026, 11, 11, 22, 30, tzinfo=UTC))
|
|
rng = np.random.default_rng(0)
|
|
corner_early = _clock.render(early, (200, 120), rng).getpixel((100, 5))
|
|
corner_late = _clock.render(late, (200, 120), rng).getpixel((100, 5))
|
|
assert corner_early != corner_late
|
|
|
|
|
|
def _ledger(tmp_path: Path, text: str) -> Path:
|
|
path = tmp_path / "ledger.json"
|
|
path.write_text(text, encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def test_credit_days_counts_real_credits_per_day(tmp_path: Path) -> None:
|
|
entries = [
|
|
{"kind": "credit", "amount": 5, "day": "2026-11-10"},
|
|
{"kind": "credit", "day": "2026-11-10"},
|
|
{"kind": "credit", "amount": 1, "day": "2026-11-09"},
|
|
]
|
|
days = _heatmap.credit_days(_ledger(tmp_path, json.dumps({"entries": entries})))
|
|
assert days == {date(2026, 11, 10): 2, date(2026, 11, 9): 1}
|
|
|
|
|
|
def test_credit_days_ignores_unusable_rows(tmp_path: Path) -> None:
|
|
entries = [
|
|
"not a dict",
|
|
{"kind": "debit", "amount": 3, "day": "2026-11-10"},
|
|
{"kind": "credit", "amount": 0, "day": "2026-11-10"},
|
|
{"kind": "credit", "amount": 1, "day": "yesterday"},
|
|
{"kind": "credit", "amount": 1},
|
|
]
|
|
days = _heatmap.credit_days(_ledger(tmp_path, json.dumps({"entries": entries})))
|
|
assert not days
|
|
|
|
|
|
def test_credit_days_survives_a_broken_ledger(tmp_path: Path) -> None:
|
|
assert not _heatmap.credit_days(tmp_path / "missing.json")
|
|
assert not _heatmap.credit_days(_ledger(tmp_path, "{not json"))
|
|
assert not _heatmap.credit_days(_ledger(tmp_path, "[1, 2]"))
|
|
assert not _heatmap.credit_days(_ledger(tmp_path, "{}"))
|
|
|
|
|
|
def test_heatmap_lights_cells_for_credit_days(tmp_path: Path) -> None:
|
|
now = datetime(2026, 11, 11, 12, 0, tzinfo=UTC)
|
|
ctx = _ctx(tmp_path, now)
|
|
folder = ctx.data_home / "workout_locker"
|
|
folder.mkdir(parents=True)
|
|
rows = [{"kind": "credit", "amount": 1, "day": "2026-11-11"}] * 3
|
|
(folder / "ledger.json").write_text(json.dumps({"entries": rows}), encoding="utf-8")
|
|
lit = _heatmap.render(ctx, (800, 480), np.random.default_rng(0))
|
|
blank = _heatmap.render(
|
|
_ctx(tmp_path / "x", now), (800, 480), np.random.default_rng(0)
|
|
)
|
|
assert lit.tobytes() != blank.tobytes()
|
|
|
|
|
|
def test_earth_available_needs_both_images(tmp_path: Path) -> None:
|
|
ctx = _ctx(tmp_path, datetime(2026, 11, 11, tzinfo=UTC))
|
|
assert not _earth.available(ctx)
|
|
ctx.earth_dir.mkdir()
|
|
Image.new("RGB", (4, 2)).save(ctx.earth_dir / "day.jpg")
|
|
assert not _earth.available(ctx)
|
|
Image.new("RGB", (4, 2)).save(ctx.earth_dir / "night.jpg")
|
|
assert _earth.available(ctx)
|
|
|
|
|
|
def test_sun_is_over_the_greenwich_meridian_at_noon_utc(tmp_path: Path) -> None:
|
|
ctx = _ctx(tmp_path, datetime(2026, 3, 20, 12, 0, tzinfo=UTC))
|
|
lat, lon = _earth.sun_position(ctx)
|
|
assert lon == 0.0
|
|
assert abs(lat) < np.radians(24)
|