capsule.py
python · 216 lines
1#!/usr/bin/env python32"""Kindness Time Capsule — leave gentle notes for your future self."""34from __future__ import annotations56import argparse7import json8import random9import sys10import time11from datetime import datetime, timezone12from pathlib import Path13from typing import Any1415DATA_FILE: Path = Path("capsule_data.json")1617# --- Data persistence ---181920def load_data() -> list[dict[str, Any]]:21 """Read capsule messages from disk, returning an empty list on any failure."""22 if not DATA_FILE.exists():23 return []24 try:25 text = DATA_FILE.read_text(encoding="utf-8")26 data = json.loads(text)27 if isinstance(data, list):28 return data # type: ignore[no-any-return]29 except (json.JSONDecodeError, OSError):30 pass31 return []323334def save_data(capsules: list[dict[str, Any]]) -> None:35 """Write capsule messages to disk as pretty-printed JSON."""36 DATA_FILE.write_text(37 json.dumps(capsules, ensure_ascii=False, indent=2) + "\n",38 encoding="utf-8",39 )404142# --- Time helpers ---434445def _relative_time(iso_stamp: str) -> str:46 """Turn an ISO timestamp into a warm, human-readable relative string."""47 then = datetime.fromisoformat(iso_stamp)48 now = datetime.now(timezone.utc)49 seconds = int((now - then).total_seconds())5051 if seconds < 60:52 return "just now"53 minutes = seconds // 6054 if minutes < 60:55 unit = "minute" if minutes == 1 else "minutes"56 return f"{minutes} {unit} ago"57 hours = minutes // 6058 if hours < 24:59 unit = "hour" if hours == 1 else "hours"60 return f"{hours} {unit} ago"61 days = hours // 2462 if days < 30:63 unit = "day" if days == 1 else "days"64 return f"{days} {unit} ago"65 months = days // 3066 if months < 12:67 unit = "month" if months == 1 else "months"68 return f"{months} {unit} ago"69 years = days // 36570 unit = "year" if years == 1 else "years"71 return f"{years} {unit} ago"727374def _gentle_print(text: str, delay: float = 0.02) -> None:75 """Print text character-by-character for a calm, typewriter feel."""76 for ch in text:77 sys.stdout.write(ch)78 sys.stdout.flush()79 time.sleep(delay)80 sys.stdout.write("\n")818283# --- Commands ---848586def add_message(text: str) -> None:87 """Bury a new message in the capsule for your future self."""88 capsules = load_data()89 capsules.append(90 {91 "message": text,92 "created_at": datetime.now(timezone.utc).isoformat(),93 "tags": [],94 }95 )96 save_data(capsules)9798 print()99 _gentle_print(" \U0001f331 Your words have been buried safely.")100 _gentle_print(" Someday, you\u2019ll find them again.")101 print()102103104def open_random() -> None:105 """Open the capsule and rediscover a random past message."""106 capsules = load_data()107 if not capsules:108 print()109 _gentle_print(" The capsule is empty.")110 _gentle_print(" Maybe leave something for future-you?")111 print()112 return113114 entry = random.choice(capsules)115 ago = _relative_time(entry["created_at"])116117 print()118 print(" \u2500" * 30)119 print()120 _gentle_print(f" \U0001f4dc You left this for yourself {ago}:")121 print()122 _gentle_print(f" \u201c{entry['message']}\u201d")123 print()124 print(" \u2500" * 30)125 print()126127128def list_messages() -> None:129 """Show every message in the capsule, most recent first."""130 capsules = load_data()131 if not capsules:132 print()133 _gentle_print(" Nothing here yet. The capsule is waiting.")134 print()135 return136137 print()138 _gentle_print(" \U0001f30d Everything you\u2019ve buried so far:\n")139140 for i, entry in enumerate(capsules, start=1):141 ago = _relative_time(entry["created_at"])142 preview = entry["message"]143 if len(preview) > 50:144 preview = preview[:47] + "\u2026"145 print(f" {i:>3}. \u201c{preview}\u201d")146 print(f" \u2014 {ago}")147 print()148149 print(f" {len(capsules)} little note{'s' if len(capsules) != 1 else ''}, "150 "waiting to be remembered.\n")151152153def open_oldest() -> None:154 """Revisit the very first message you ever buried."""155 capsules = load_data()156 if not capsules:157 print()158 _gentle_print(" No memories yet. Write one today?")159 print()160 return161162 oldest = min(capsules, key=lambda c: c["created_at"])163 ago = _relative_time(oldest["created_at"])164165 print()166 _gentle_print(" \U0001f30c Here\u2019s where it all began\u2026")167 print()168 _gentle_print(f" You wrote this {ago}:")169 print()170 _gentle_print(f" \u201c{oldest['message']}\u201d")171 print()172 _gentle_print(" Look how far you\u2019ve come.")173 print()174175176# --- CLI ---177178179def parse_args(argv: list[str] | None = None) -> argparse.Namespace:180 """Parse command-line arguments for the capsule."""181 parser = argparse.ArgumentParser(182 description="Kindness Time Capsule \u2014 leave gentle notes for future-you.",183 )184 sub = parser.add_subparsers(dest="command")185186 add_parser = sub.add_parser("add", help="Bury a new message")187 add_parser.add_argument("message", help="The words you want to save")188189 sub.add_parser("open", help="Open the capsule and find a surprise")190 sub.add_parser("list", help="See everything you\u2019ve buried")191 sub.add_parser("oldest", help="Revisit your very first message")192193 args = parser.parse_args(argv)194 if args.command is None:195 parser.print_help()196 raise SystemExit(0)197 return args198199200def main() -> None:201 """Entry point for the Kindness Time Capsule."""202 args = parse_args()203204 if args.command == "add":205 add_message(args.message)206 elif args.command == "open":207 open_random()208 elif args.command == "list":209 list_messages()210 elif args.command == "oldest":211 open_oldest()212213214if __name__ == "__main__":215 main()216