diff --git a/skills/calendar/scripts/cal_tool.py b/skills/calendar/scripts/cal_tool.py index 0aa50f7..9c6fea5 100644 --- a/skills/calendar/scripts/cal_tool.py +++ b/skills/calendar/scripts/cal_tool.py @@ -19,6 +19,7 @@ Subcommands: """ import argparse +import shutil import subprocess import sys import uuid @@ -98,14 +99,13 @@ def _strip_method(ics_bytes): def _parse_iso_datetime(dt_str): - """Parse ISO 8601 datetime string to a datetime object.""" - # Handle both 2026-03-20T14:00:00 and 2026-03-20T14:00 - for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"): - try: - return datetime.strptime(dt_str, fmt) - except ValueError: - continue - raise ValueError(f"Cannot parse datetime: {dt_str}") + """Parse ISO 8601 datetime string to a naive datetime object.""" + try: + dt = datetime.fromisoformat(dt_str) + # Strip tzinfo if present — timezone is handled via --timezone / TZID param + return dt.replace(tzinfo=None) + except ValueError: + raise ValueError(f"Cannot parse datetime: {dt_str}") def _parse_date(date_str): @@ -286,10 +286,7 @@ def _extract_ics_from_email(envelope_id, folder, account): ics_files = list(download_dir.glob("*.ics")) if not ics_files: print(f"Error: No .ics attachment found in envelope {envelope_id}", file=sys.stderr) - # Cleanup - for f in download_dir.iterdir(): - f.unlink() - download_dir.rmdir() + shutil.rmtree(download_dir, ignore_errors=True) sys.exit(1) return ics_files[0], download_dir @@ -391,9 +388,7 @@ def cmd_reply(args): print("=== Email Message ===") print(email_str) if cleanup_dir: - for f in cleanup_dir.iterdir(): - f.unlink() - cleanup_dir.rmdir() + shutil.rmtree(cleanup_dir, ignore_errors=True) return # Send reply @@ -414,9 +409,7 @@ def cmd_reply(args): # Cleanup if cleanup_dir: - for f in cleanup_dir.iterdir(): - f.unlink() - cleanup_dir.rmdir() + shutil.rmtree(cleanup_dir, ignore_errors=True) # --------------------------------------------------------------------------- @@ -485,19 +478,6 @@ def _urgency_label(days): return f"🟢 {days} 天后" -def _find_todo_id(match_str): - """Find a todoman ID by matching summary text. Exits on 0 or >1 matches.""" - todos = _todoman_list_json("--sort", "due") - matches = [t for t in todos if match_str in (t.get("summary") or "")] - if not matches: - print(f"Error: No todo matching '{match_str}'", file=sys.stderr) - sys.exit(1) - if len(matches) > 1: - print(f"Error: Multiple todos match '{match_str}':", file=sys.stderr) - for t in matches: - print(f" - {t.get('summary')} (id: {t.get('id')})", file=sys.stderr) - sys.exit(1) - return matches[0]["id"] # --------------------------------------------------------------------------- @@ -638,22 +618,9 @@ def cmd_todo_list(args): def cmd_todo_complete(args): """Mark a todo as done via todoman.""" - if args.uid: - # todoman uses numeric IDs, not UIDs — search by UID in summary fallback - todos = _todoman_list_json("--sort", "due") - match = [t for t in todos if args.uid in (t.get("uid") or "")] - if not match: - print(f"Error: No todo with UID '{args.uid}'", file=sys.stderr) - sys.exit(1) - todo_id = match[0]["id"] - elif args.match: - todo_id = _find_todo_id(args.match) - else: - print("Error: --uid or --match is required", file=sys.stderr) - sys.exit(1) - + todo_id, matched = _find_todo(uid=args.uid, match=args.match) _run_todoman("done", str(todo_id)) - print(f"Completed todo #{todo_id}") + print(f"Completed todo: {matched.get('summary')}") _sync_calendar() @@ -710,21 +677,9 @@ def cmd_todo_edit(args): def cmd_todo_delete(args): """Delete a todo via todoman.""" - if args.uid: - todos = _todoman_list_json("--sort", "due") - match = [t for t in todos if args.uid in (t.get("uid") or "")] - if not match: - print(f"Error: No todo with UID '{args.uid}'", file=sys.stderr) - sys.exit(1) - todo_id = match[0]["id"] - elif args.match: - todo_id = _find_todo_id(args.match) - else: - print("Error: --uid or --match is required", file=sys.stderr) - sys.exit(1) - + todo_id, matched = _find_todo(uid=args.uid, match=args.match) _run_todoman("delete", "--yes", str(todo_id)) - print(f"Deleted todo #{todo_id}") + print(f"Deleted todo: {matched.get('summary')}") _sync_calendar() @@ -746,6 +701,7 @@ def cmd_todo_check(args): def cmd_event_list(args): """List calendar events via khal.""" + _sync_calendar() if args.search: cmd = ["khal", "search"] if args.format: @@ -757,11 +713,16 @@ def cmd_event_list(args): cmd += ["--format", args.format] cmd += [args.range_start, args.range_end] try: - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - print(result.stdout.rstrip()) - except subprocess.CalledProcessError as e: - print(f"Error: khal failed: {e.stderr.strip()}", file=sys.stderr) - sys.exit(1) + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + # khal search returns 1 when no results found — not a real error + if args.search and not result.stderr.strip(): + print(f"No events matching '{args.search}'.") + return + print(f"Error: khal failed: {result.stderr.strip()}", file=sys.stderr) + sys.exit(1) + output = result.stdout.rstrip() + print(output if output else "No events found.") except FileNotFoundError: print("Error: khal is not installed", file=sys.stderr) sys.exit(1)