86 lines
2.4 KiB
Python
86 lines
2.4 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from uuid import uuid4
|
|
|
|
from PIL import Image, ImageDraw
|
|
|
|
from app.persistence import atomic_write_bytes
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
STARTUP_BRIGHTNESS = 50
|
|
OPEN_EYE_SECONDS = 1.3
|
|
CLOSED_EYE_SECONDS = 0.2
|
|
BACKGROUND = (255, 255, 255)
|
|
PINK = (255, 79, 163)
|
|
_PROCESS_BOOT_ID = f"process-{uuid4()}"
|
|
|
|
|
|
def render_startup_smile(*, eyes_closed: bool = False) -> Image.Image:
|
|
image = Image.new("RGB", (64, 64), BACKGROUND)
|
|
draw = ImageDraw.Draw(image)
|
|
|
|
if eyes_closed:
|
|
draw.rectangle((14, 23, 25, 26), fill=PINK)
|
|
draw.rectangle((38, 23, 49, 26), fill=PINK)
|
|
else:
|
|
draw.rectangle((16, 18, 23, 29), fill=PINK)
|
|
draw.rectangle((40, 18, 47, 29), fill=PINK)
|
|
|
|
for box in (
|
|
(14, 36, 17, 39),
|
|
(18, 40, 21, 43),
|
|
(22, 44, 25, 47),
|
|
(26, 48, 37, 51),
|
|
(38, 44, 41, 47),
|
|
(42, 40, 45, 43),
|
|
(46, 36, 49, 39),
|
|
):
|
|
draw.rectangle(box, fill=PINK)
|
|
|
|
return image
|
|
|
|
|
|
def read_boot_id() -> str:
|
|
try:
|
|
value = Path("/proc/sys/kernel/random/boot_id").read_text(encoding="ascii").strip()
|
|
if value:
|
|
return value
|
|
except (OSError, UnicodeError):
|
|
logger.info("Linux boot ID is unavailable; using the current process session")
|
|
return _PROCESS_BOOT_ID
|
|
|
|
|
|
class StartupSessionStore:
|
|
def __init__(self, data_dir: Path, boot_id: str | None = None) -> None:
|
|
self.path = data_dir / "startup_indicator.json"
|
|
self.boot_id = boot_id or read_boot_id()
|
|
|
|
def should_show(self) -> bool:
|
|
if not self.path.exists():
|
|
return True
|
|
try:
|
|
raw = json.loads(self.path.read_text(encoding="utf-8"))
|
|
if not isinstance(raw, dict):
|
|
raise ValueError("startup indicator state must be an object")
|
|
return raw.get("dismissed_boot_id") != self.boot_id
|
|
except (OSError, ValueError, TypeError):
|
|
logger.exception("Startup indicator state is invalid; showing the indicator")
|
|
return True
|
|
|
|
def mark_dismissed(self) -> None:
|
|
atomic_write_bytes(
|
|
self.path,
|
|
(
|
|
json.dumps(
|
|
{"dismissed_boot_id": self.boot_id},
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
)
|
|
+ "\n"
|
|
).encode("utf-8"),
|
|
)
|