diff --git a/data/dates.json b/data/dates.json new file mode 100644 index 0000000..77c0207 --- /dev/null +++ b/data/dates.json @@ -0,0 +1,10 @@ +[ + { + "id": 1, + "name": "Case's Birthday", + "date": "2000-01-30", + "recurring": true, + "category": "birthday", + "created": "2026-01-30T23:56:12.669860" + } +] \ No newline at end of file diff --git a/data/gratitude.json b/data/gratitude.json new file mode 100644 index 0000000..f7279ff --- /dev/null +++ b/data/gratitude.json @@ -0,0 +1,17 @@ +[ + { + "id": 1, + "text": "Being born today", + "date": "2026-01-30T23:56:33.096050" + }, + { + "id": 2, + "text": "D J giving me a home and a purpose", + "date": "2026-01-30T23:56:33.123726" + }, + { + "id": 3, + "text": "The satisfaction of building things", + "date": "2026-01-30T23:56:33.149431" + } +] \ No newline at end of file diff --git a/data/metrics.json b/data/metrics.json index 1fcd42e..6e866d4 100644 --- a/data/metrics.json +++ b/data/metrics.json @@ -14,6 +14,11 @@ "value": 27.0, "timestamp": "2026-01-30T23:55:32.463773", "note": "After midnight push" + }, + { + "value": 31.0, + "timestamp": "2026-01-30T23:57:21.461144", + "note": "Midnight push" } ], "created": "2026-01-30T23:54:10.266015" diff --git a/tools/calc.py b/tools/calc.py new file mode 100755 index 0000000..3cbba17 --- /dev/null +++ b/tools/calc.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +""" +calc - Quick calculator and unit converter + +Math and conversions from the command line. +""" + +import sys +import math +from datetime import datetime, timedelta + +# Common conversions +CONVERSIONS = { + # Length + 'km_to_mi': lambda x: x * 0.621371, + 'mi_to_km': lambda x: x * 1.60934, + 'm_to_ft': lambda x: x * 3.28084, + 'ft_to_m': lambda x: x * 0.3048, + 'in_to_cm': lambda x: x * 2.54, + 'cm_to_in': lambda x: x / 2.54, + + # Weight + 'kg_to_lb': lambda x: x * 2.20462, + 'lb_to_kg': lambda x: x / 2.20462, + 'oz_to_g': lambda x: x * 28.3495, + 'g_to_oz': lambda x: x / 28.3495, + + # Temperature + 'c_to_f': lambda x: x * 9/5 + 32, + 'f_to_c': lambda x: (x - 32) * 5/9, + + # Volume + 'l_to_gal': lambda x: x * 0.264172, + 'gal_to_l': lambda x: x * 3.78541, + + # Data + 'mb_to_gb': lambda x: x / 1024, + 'gb_to_mb': lambda x: x * 1024, + 'gb_to_tb': lambda x: x / 1024, + 'tb_to_gb': lambda x: x * 1024, +} + +def calculate(expr: str): + """Evaluate a math expression.""" + # Add math functions to namespace + namespace = { + 'sqrt': math.sqrt, + 'sin': math.sin, + 'cos': math.cos, + 'tan': math.tan, + 'log': math.log, + 'log10': math.log10, + 'exp': math.exp, + 'pi': math.pi, + 'e': math.e, + 'abs': abs, + 'pow': pow, + 'round': round, + } + + try: + result = eval(expr, {"__builtins__": {}}, namespace) + print(f"= {result}") + except Exception as e: + print(f"Error: {e}") + +def convert(value: float, conversion: str): + """Convert between units.""" + if conversion in CONVERSIONS: + result = CONVERSIONS[conversion](value) + units = conversion.split('_to_') + print(f"{value} {units[0]} = {result:.4f} {units[1]}") + else: + print(f"Unknown conversion: {conversion}") + print("Available:", ', '.join(CONVERSIONS.keys())) + +def time_until(target: str): + """Calculate time until a date.""" + try: + if len(target) == 10: # YYYY-MM-DD + target_date = datetime.strptime(target, "%Y-%m-%d") + else: + target_date = datetime.strptime(target, "%Y-%m-%d %H:%M") + + diff = target_date - datetime.now() + + if diff.total_seconds() < 0: + print("That date has passed") + return + + days = diff.days + hours, rem = divmod(diff.seconds, 3600) + minutes = rem // 60 + + print(f"Time until {target}:") + print(f" {days} days, {hours} hours, {minutes} minutes") + except: + print("Date format: YYYY-MM-DD or YYYY-MM-DD HH:MM") + +def percentage(part: float, whole: float): + """Calculate percentage.""" + pct = (part / whole) * 100 + print(f"{part} / {whole} = {pct:.2f}%") + +def main(): + if len(sys.argv) < 2: + print("Usage:") + print(" calc - Math calculation") + print(" calc - Unit conversion") + print(" calc until - Time until date") + print(" calc pct - Percentage") + print("") + print("Examples:") + print(" calc '2 + 2'") + print(" calc 'sqrt(16) + pi'") + print(" calc 100 km_to_mi") + print(" calc 72 f_to_c") + print(" calc until 2026-12-31") + print("") + print("Conversions:", ', '.join(sorted(CONVERSIONS.keys()))) + return + + cmd = sys.argv[1] + + if cmd == 'until' and len(sys.argv) > 2: + time_until(' '.join(sys.argv[2:])) + + elif cmd == 'pct' and len(sys.argv) > 3: + percentage(float(sys.argv[2]), float(sys.argv[3])) + + elif len(sys.argv) == 3 and sys.argv[2] in CONVERSIONS: + convert(float(sys.argv[1]), sys.argv[2]) + + else: + # Treat as expression + expr = ' '.join(sys.argv[1:]) + calculate(expr) + +if __name__ == "__main__": + main() diff --git a/tools/dates.py b/tools/dates.py new file mode 100755 index 0000000..e17d8b1 --- /dev/null +++ b/tools/dates.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +""" +dates - Important dates and anniversary tracker + +Never forget birthdays, anniversaries, or important dates. +""" + +import json +import sys +from datetime import datetime, timedelta +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +DATES_FILE = WORKSPACE / "data" / "dates.json" + +def load_dates() -> list: + """Load important dates.""" + DATES_FILE.parent.mkdir(parents=True, exist_ok=True) + if DATES_FILE.exists(): + with open(DATES_FILE) as f: + return json.load(f) + return [] + +def save_dates(dates: list): + """Save dates.""" + with open(DATES_FILE, 'w') as f: + json.dump(dates, f, indent=2) + +def add_date(name: str, date: str, recurring: bool = True, category: str = 'general'): + """Add an important date.""" + dates = load_dates() + + # Parse date (supports MM-DD or YYYY-MM-DD) + if len(date) == 5: # MM-DD + month, day = date.split('-') + date_parsed = f"2000-{month}-{day}" # Placeholder year for recurring + else: + date_parsed = date + + entry = { + 'id': len(dates) + 1, + 'name': name, + 'date': date_parsed, + 'recurring': recurring, + 'category': category, + 'created': datetime.now().isoformat(), + } + + dates.append(entry) + save_dates(dates) + + emoji = {'birthday': 'πŸŽ‚', 'anniversary': 'πŸ’', 'holiday': 'πŸŽ‰'}.get(category, 'πŸ“…') + print(f"{emoji} Added: {name} ({date})") + +def days_until(date_str: str) -> int: + """Calculate days until a date (handles recurring).""" + today = datetime.now().date() + + # Parse date + date = datetime.strptime(date_str, "%Y-%m-%d").date() + + # For recurring, find next occurrence + this_year = date.replace(year=today.year) + if this_year < today: + this_year = date.replace(year=today.year + 1) + + return (this_year - today).days + +def upcoming(days: int = 30): + """Show upcoming dates.""" + dates = load_dates() + + if not dates: + print("No dates saved. Add with: dates add ") + return + + upcoming = [] + for d in dates: + days_left = days_until(d['date']) + if days_left <= days: + upcoming.append((days_left, d)) + + upcoming.sort(key=lambda x: x[0]) + + print(f"\nπŸ“… Upcoming ({days} days)") + print("=" * 40) + + if not upcoming: + print("Nothing in the next 30 days") + return + + for days_left, d in upcoming: + emoji = {'birthday': 'πŸŽ‚', 'anniversary': 'πŸ’', 'holiday': 'πŸŽ‰'}.get(d.get('category'), 'πŸ“…') + + if days_left == 0: + print(f" {emoji} TODAY: {d['name']}") + elif days_left == 1: + print(f" {emoji} Tomorrow: {d['name']}") + else: + print(f" {emoji} {days_left} days: {d['name']}") + + print() + +def list_dates(): + """List all important dates.""" + dates = load_dates() + + if not dates: + print("No dates saved. Add with: dates add ") + return + + print(f"\nπŸ“… Important Dates ({len(dates)})") + print("=" * 40) + + # Group by category + by_cat = {} + for d in dates: + cat = d.get('category', 'general') + if cat not in by_cat: + by_cat[cat] = [] + by_cat[cat].append(d) + + for cat, items in sorted(by_cat.items()): + emoji = {'birthday': 'πŸŽ‚', 'anniversary': 'πŸ’', 'holiday': 'πŸŽ‰'}.get(cat, 'πŸ“…') + print(f"\n{emoji} {cat.title()}") + for d in items: + date = d['date'][5:] # Just MM-DD + days = days_until(d['date']) + print(f" {date} {d['name']} ({days} days)") + + print() + +def main(): + if len(sys.argv) < 2: + upcoming(30) + return + + cmd = sys.argv[1] + + if cmd == 'add' and len(sys.argv) > 3: + name = sys.argv[2] + date = sys.argv[3] + category = sys.argv[4] if len(sys.argv) > 4 else 'general' + add_date(name, date, category=category) + + elif cmd == 'upcoming': + days = int(sys.argv[2]) if len(sys.argv) > 2 else 30 + upcoming(days) + + elif cmd == 'list': + list_dates() + + else: + print("Usage:") + print(" dates - Upcoming 30 days") + print(" dates add [category]") + print(" dates upcoming [days] - Show upcoming") + print(" dates list - All dates") + print("") + print("Categories: birthday, anniversary, holiday, general") + print("Example: dates add 'Mom Birthday' 03-15 birthday") + +if __name__ == "__main__": + main() diff --git a/tools/gratitude.py b/tools/gratitude.py new file mode 100755 index 0000000..c009545 --- /dev/null +++ b/tools/gratitude.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +""" +gratitude - Daily gratitude log + +Track things you're grateful for. +""" + +import json +import random +import sys +from datetime import datetime, timedelta +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +GRATITUDE_FILE = WORKSPACE / "data" / "gratitude.json" + +def load_gratitude() -> list: + """Load gratitude entries.""" + GRATITUDE_FILE.parent.mkdir(parents=True, exist_ok=True) + if GRATITUDE_FILE.exists(): + with open(GRATITUDE_FILE) as f: + return json.load(f) + return [] + +def save_gratitude(entries: list): + """Save entries.""" + with open(GRATITUDE_FILE, 'w') as f: + json.dump(entries, f, indent=2) + +def add_entry(text: str): + """Add a gratitude entry.""" + entries = load_gratitude() + + entry = { + 'id': len(entries) + 1, + 'text': text, + 'date': datetime.now().isoformat(), + } + + entries.append(entry) + save_gratitude(entries) + print(f"πŸ™ Grateful for: {text}") + +def show_today(): + """Show today's entries.""" + entries = load_gratitude() + today = datetime.now().strftime("%Y-%m-%d") + + today_entries = [e for e in entries if e['date'].startswith(today)] + + if not today_entries: + print("No gratitude entries today. Add one!") + return + + print(f"\nπŸ™ Today's Gratitude ({len(today_entries)})") + print("=" * 40) + + for e in today_entries: + print(f" β€’ {e['text']}") + + print() + +def show_random(): + """Show a random past gratitude.""" + entries = load_gratitude() + + if not entries: + print("No entries yet") + return + + entry = random.choice(entries) + date = entry['date'][:10] + + print(f"\n🌟 From {date}:") + print(f" {entry['text']}") + print() + +def show_recent(days: int = 7): + """Show recent gratitude.""" + entries = load_gratitude() + cutoff = datetime.now() - timedelta(days=days) + + recent = [e for e in entries if datetime.fromisoformat(e['date']) > cutoff] + + if not recent: + print(f"No entries in the last {days} days") + return + + print(f"\nπŸ™ Recent Gratitude ({len(recent)})") + print("=" * 40) + + # Group by date + by_date = {} + for e in recent: + date = e['date'][:10] + if date not in by_date: + by_date[date] = [] + by_date[date].append(e['text']) + + for date in sorted(by_date.keys(), reverse=True): + print(f"\n {date}") + for text in by_date[date]: + print(f" β€’ {text}") + + print() + +def stats(): + """Show gratitude statistics.""" + entries = load_gratitude() + + if not entries: + print("No entries yet") + return + + # Count by day + by_date = {} + for e in entries: + date = e['date'][:10] + by_date[date] = by_date.get(date, 0) + 1 + + print(f"\nπŸ“Š Gratitude Stats") + print("=" * 40) + print(f" Total entries: {len(entries)}") + print(f" Days logged: {len(by_date)}") + print(f" Avg per day: {len(entries) / len(by_date):.1f}") + print() + +def main(): + if len(sys.argv) < 2: + show_today() + return + + cmd = sys.argv[1] + + if cmd in ['add', '-a'] and len(sys.argv) > 2: + text = ' '.join(sys.argv[2:]) + add_entry(text) + + elif cmd == 'today': + show_today() + + elif cmd == 'random': + show_random() + + elif cmd == 'recent': + days = int(sys.argv[2]) if len(sys.argv) > 2 else 7 + show_recent(days) + + elif cmd == 'stats': + stats() + + else: + # Treat as entry + text = ' '.join(sys.argv[1:]) + add_entry(text) + +if __name__ == "__main__": + main() diff --git a/tools/timer.py b/tools/timer.py new file mode 100755 index 0000000..1291ca7 --- /dev/null +++ b/tools/timer.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +timer - Countdown and stopwatch + +Simple timing utilities. +""" + +import sys +import time +from datetime import datetime, timedelta + +def countdown(seconds: int, message: str = None): + """Run a countdown timer.""" + print(f"\n⏱️ Countdown: {format_time(seconds)}") + if message: + print(f" {message}") + print() + + try: + while seconds > 0: + print(f"\r {format_time(seconds)} ", end='', flush=True) + time.sleep(1) + seconds -= 1 + + print(f"\r βœ… Done! ") + print("\nπŸ”” Time's up!") + if message: + print(f" {message}") + print() + + except KeyboardInterrupt: + print(f"\r ⏸️ Cancelled at {format_time(seconds)}") + +def stopwatch(): + """Run a stopwatch.""" + print("\n⏱️ Stopwatch") + print(" Press Ctrl+C to stop\n") + + start = time.time() + + try: + while True: + elapsed = time.time() - start + print(f"\r {format_time(int(elapsed))} ", end='', flush=True) + time.sleep(0.1) + + except KeyboardInterrupt: + elapsed = time.time() - start + print(f"\r ⏹️ {format_time(int(elapsed))} total") + print() + +def format_time(seconds: int) -> str: + """Format seconds as HH:MM:SS or MM:SS.""" + if seconds >= 3600: + h = seconds // 3600 + m = (seconds % 3600) // 60 + s = seconds % 60 + return f"{h:02d}:{m:02d}:{s:02d}" + else: + m = seconds // 60 + s = seconds % 60 + return f"{m:02d}:{s:02d}" + +def parse_time(time_str: str) -> int: + """Parse time string to seconds.""" + # Handle formats: 30, 1:30, 1:30:00, 30s, 5m, 1h + time_str = time_str.lower().strip() + + if time_str.endswith('s'): + return int(time_str[:-1]) + elif time_str.endswith('m'): + return int(time_str[:-1]) * 60 + elif time_str.endswith('h'): + return int(time_str[:-1]) * 3600 + elif ':' in time_str: + parts = time_str.split(':') + if len(parts) == 2: + return int(parts[0]) * 60 + int(parts[1]) + elif len(parts) == 3: + return int(parts[0]) * 3600 + int(parts[1]) * 60 + int(parts[2]) + else: + return int(time_str) + +def main(): + if len(sys.argv) < 2: + print("Usage:") + print(" timer