mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 22:34:02 +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
118 lines
3.6 KiB
Python
118 lines
3.6 KiB
Python
"""Style 3: a year of habits as four stacked day grids, colour only."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
from dataclasses import dataclass
|
|
from datetime import date, timedelta
|
|
import json
|
|
from typing import TYPE_CHECKING, cast
|
|
|
|
from PIL import Image, ImageDraw
|
|
|
|
from python_pkg.wallpaper_rotator import _palette
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
from python_pkg.wallpaper_rotator._context import RenderContext
|
|
|
|
# Ledger directory under ~/.local/share -> the colour of its grid.
|
|
HABITS: tuple[tuple[str, _palette.Rgb], ...] = (
|
|
("workout_locker", _palette.GREEN),
|
|
("leetcode_guard", _palette.ORANGE),
|
|
("anki_guard", _palette.CYAN),
|
|
("book_guard", _palette.PURPLE),
|
|
)
|
|
WEEKS = 52
|
|
_EMPTY = _palette.mix(_palette.BG, _palette.LINE, 0.5)
|
|
_FULL_AT = 3 # credits in a day that light a cell fully
|
|
|
|
|
|
def credit_days(ledger: Path) -> Counter[date]:
|
|
"""Count the non-zero ``credit`` rows per day in one guard ledger.
|
|
|
|
A missing or unreadable ledger yields no days: the grid stays empty
|
|
rather than the background failing.
|
|
"""
|
|
try:
|
|
data = json.loads(ledger.read_text(encoding="utf-8"))
|
|
except OSError, ValueError:
|
|
return Counter()
|
|
rows = data.get("entries", []) if isinstance(data, dict) else []
|
|
days: Counter[date] = Counter()
|
|
for row in cast("list[object]", rows):
|
|
if not isinstance(row, dict) or row.get("kind") != "credit":
|
|
continue
|
|
if row.get("amount", 1) == 0:
|
|
continue
|
|
try:
|
|
days[date.fromisoformat(str(row.get("day")))] += 1
|
|
except ValueError:
|
|
continue
|
|
return days
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _Grid:
|
|
"""Where the day cells sit: first day, left edge, cell pitch and size."""
|
|
|
|
today: date
|
|
start: date
|
|
left: float
|
|
pitch: float
|
|
cell: float
|
|
|
|
|
|
def _cell_colour(count: int, colour: _palette.Rgb) -> _palette.Rgb:
|
|
if count <= 0:
|
|
return _EMPTY
|
|
return _palette.mix(_EMPTY, colour, 0.35 + 0.45 * min(count, _FULL_AT) / _FULL_AT)
|
|
|
|
|
|
def _draw_band(
|
|
draw: ImageDraw.ImageDraw,
|
|
grid: _Grid,
|
|
band_top: float,
|
|
days: Counter[date],
|
|
colour: _palette.Rgb,
|
|
) -> None:
|
|
"""Fill one habit's 7-row grid, oldest day at the left."""
|
|
for offset in range((grid.today - grid.start).days + 1):
|
|
day = grid.start + timedelta(days=offset)
|
|
x = grid.left + (offset // 7) * grid.pitch
|
|
y = band_top + day.weekday() * grid.pitch
|
|
draw.rectangle(
|
|
(x, y, x + grid.cell, y + grid.cell),
|
|
fill=_cell_colour(days[day], colour),
|
|
)
|
|
|
|
|
|
def render(
|
|
ctx: RenderContext, size: tuple[int, int], _rng: np.random.Generator
|
|
) -> Image.Image:
|
|
"""Draw one 7-row x 53-column grid per habit, today at the bottom right."""
|
|
width, height = size
|
|
image = Image.new("RGB", size, _palette.DEEP)
|
|
draw = ImageDraw.Draw(image)
|
|
today = ctx.now.date()
|
|
columns = WEEKS + 1
|
|
band_gap = 2 # cells of space between habits
|
|
rows = 7 * len(HABITS) + band_gap * (len(HABITS) - 1)
|
|
pitch = min(width * 0.86 / columns, height * 0.8 / rows)
|
|
grid = _Grid(
|
|
today=today,
|
|
start=today - timedelta(days=today.weekday() + 7 * WEEKS),
|
|
left=(width - pitch * columns) / 2,
|
|
pitch=pitch,
|
|
cell=max(pitch * 0.82, 1.0),
|
|
)
|
|
top = (height - pitch * rows) / 2
|
|
for band, (folder, colour) in enumerate(HABITS):
|
|
days = credit_days(ctx.data_home / folder / "ledger.json")
|
|
band_top = top + band * (7 + band_gap) * pitch
|
|
_draw_band(draw, grid, band_top, days, colour)
|
|
return image
|