Add dates, gratitude, calc, timer - dates.py: Important dates and anniversaries - gratitude.py: Gratitude log - calc.py: Calculator and unit converter - timer.py: Countdown and stopwatch - 31 tools total!
This commit is contained in:
10
data/dates.json
Normal file
10
data/dates.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
17
data/gratitude.json
Normal file
17
data/gratitude.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
@ -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"
|
||||
|
||||
140
tools/calc.py
Executable file
140
tools/calc.py
Executable file
@ -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 <expression> - Math calculation")
|
||||
print(" calc <value> <conversion> - Unit conversion")
|
||||
print(" calc until <YYYY-MM-DD> - Time until date")
|
||||
print(" calc pct <part> <whole> - 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()
|
||||
164
tools/dates.py
Executable file
164
tools/dates.py
Executable file
@ -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 <name> <MM-DD>")
|
||||
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 <name> <MM-DD>")
|
||||
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 <name> <MM-DD> [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()
|
||||
158
tools/gratitude.py
Executable file
158
tools/gratitude.py
Executable file
@ -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()
|
||||
114
tools/timer.py
Executable file
114
tools/timer.py
Executable file
@ -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 <time> [message] - Countdown timer")
|
||||
print(" timer stop - Stopwatch")
|
||||
print("")
|
||||
print("Time formats:")
|
||||
print(" 30 - 30 seconds")
|
||||
print(" 30s - 30 seconds")
|
||||
print(" 5m - 5 minutes")
|
||||
print(" 1h - 1 hour")
|
||||
print(" 1:30 - 1 min 30 sec")
|
||||
print(" 1:30:00 - 1 hour 30 min")
|
||||
print("")
|
||||
print("Examples:")
|
||||
print(" timer 5m 'Break time'")
|
||||
print(" timer 25m 'Pomodoro done'")
|
||||
print(" timer stop")
|
||||
return
|
||||
|
||||
cmd = sys.argv[1]
|
||||
|
||||
if cmd in ['stop', 'stopwatch', 'sw']:
|
||||
stopwatch()
|
||||
else:
|
||||
seconds = parse_time(cmd)
|
||||
message = ' '.join(sys.argv[2:]) if len(sys.argv) > 2 else None
|
||||
countdown(seconds, message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
4
ws
4
ws
@ -43,6 +43,10 @@ COMMANDS = {
|
||||
'ideas': ('tools/ideas.py', 'Idea incubator'),
|
||||
'weather': ('tools/weather.py', 'Weather check'),
|
||||
'standup': ('tools/standup.py', 'Daily standup generator'),
|
||||
'dates': ('tools/dates.py', 'Important dates tracker'),
|
||||
'gratitude': ('tools/gratitude.py', 'Gratitude log'),
|
||||
'calc': ('tools/calc.py', 'Calculator and converter'),
|
||||
'timer': ('tools/timer.py', 'Countdown and stopwatch'),
|
||||
|
||||
# Projects
|
||||
'news': ('projects/news-feed/main.py', 'RSS news reader'),
|
||||
|
||||
Reference in New Issue
Block a user