calendar: dedup todo lookups, robust khal search, cleanup improvements
- Replace _find_todo_id and inline UID lookups with _find_todo in complete/delete/edit (removes ~40 lines of duplicated logic) - Handle khal search returning exit code 1 for no results - Sync calendar before event list for fresh data - Use shutil.rmtree for temp dir cleanup - Use datetime.fromisoformat instead of manual format strings
This commit is contained in:
@@ -19,6 +19,7 @@ Subcommands:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
@@ -98,14 +99,13 @@ def _strip_method(ics_bytes):
|
|||||||
|
|
||||||
|
|
||||||
def _parse_iso_datetime(dt_str):
|
def _parse_iso_datetime(dt_str):
|
||||||
"""Parse ISO 8601 datetime string to a datetime object."""
|
"""Parse ISO 8601 datetime string to a naive datetime object."""
|
||||||
# Handle both 2026-03-20T14:00:00 and 2026-03-20T14:00
|
try:
|
||||||
for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"):
|
dt = datetime.fromisoformat(dt_str)
|
||||||
try:
|
# Strip tzinfo if present — timezone is handled via --timezone / TZID param
|
||||||
return datetime.strptime(dt_str, fmt)
|
return dt.replace(tzinfo=None)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
raise ValueError(f"Cannot parse datetime: {dt_str}")
|
||||||
raise ValueError(f"Cannot parse datetime: {dt_str}")
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_date(date_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"))
|
ics_files = list(download_dir.glob("*.ics"))
|
||||||
if not ics_files:
|
if not ics_files:
|
||||||
print(f"Error: No .ics attachment found in envelope {envelope_id}", file=sys.stderr)
|
print(f"Error: No .ics attachment found in envelope {envelope_id}", file=sys.stderr)
|
||||||
# Cleanup
|
shutil.rmtree(download_dir, ignore_errors=True)
|
||||||
for f in download_dir.iterdir():
|
|
||||||
f.unlink()
|
|
||||||
download_dir.rmdir()
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return ics_files[0], download_dir
|
return ics_files[0], download_dir
|
||||||
@@ -391,9 +388,7 @@ def cmd_reply(args):
|
|||||||
print("=== Email Message ===")
|
print("=== Email Message ===")
|
||||||
print(email_str)
|
print(email_str)
|
||||||
if cleanup_dir:
|
if cleanup_dir:
|
||||||
for f in cleanup_dir.iterdir():
|
shutil.rmtree(cleanup_dir, ignore_errors=True)
|
||||||
f.unlink()
|
|
||||||
cleanup_dir.rmdir()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Send reply
|
# Send reply
|
||||||
@@ -414,9 +409,7 @@ def cmd_reply(args):
|
|||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
if cleanup_dir:
|
if cleanup_dir:
|
||||||
for f in cleanup_dir.iterdir():
|
shutil.rmtree(cleanup_dir, ignore_errors=True)
|
||||||
f.unlink()
|
|
||||||
cleanup_dir.rmdir()
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -485,19 +478,6 @@ def _urgency_label(days):
|
|||||||
return f"🟢 {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):
|
def cmd_todo_complete(args):
|
||||||
"""Mark a todo as done via todoman."""
|
"""Mark a todo as done via todoman."""
|
||||||
if args.uid:
|
todo_id, matched = _find_todo(uid=args.uid, match=args.match)
|
||||||
# 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)
|
|
||||||
|
|
||||||
_run_todoman("done", str(todo_id))
|
_run_todoman("done", str(todo_id))
|
||||||
print(f"Completed todo #{todo_id}")
|
print(f"Completed todo: {matched.get('summary')}")
|
||||||
_sync_calendar()
|
_sync_calendar()
|
||||||
|
|
||||||
|
|
||||||
@@ -710,21 +677,9 @@ def cmd_todo_edit(args):
|
|||||||
|
|
||||||
def cmd_todo_delete(args):
|
def cmd_todo_delete(args):
|
||||||
"""Delete a todo via todoman."""
|
"""Delete a todo via todoman."""
|
||||||
if args.uid:
|
todo_id, matched = _find_todo(uid=args.uid, match=args.match)
|
||||||
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)
|
|
||||||
|
|
||||||
_run_todoman("delete", "--yes", str(todo_id))
|
_run_todoman("delete", "--yes", str(todo_id))
|
||||||
print(f"Deleted todo #{todo_id}")
|
print(f"Deleted todo: {matched.get('summary')}")
|
||||||
_sync_calendar()
|
_sync_calendar()
|
||||||
|
|
||||||
|
|
||||||
@@ -746,6 +701,7 @@ def cmd_todo_check(args):
|
|||||||
|
|
||||||
def cmd_event_list(args):
|
def cmd_event_list(args):
|
||||||
"""List calendar events via khal."""
|
"""List calendar events via khal."""
|
||||||
|
_sync_calendar()
|
||||||
if args.search:
|
if args.search:
|
||||||
cmd = ["khal", "search"]
|
cmd = ["khal", "search"]
|
||||||
if args.format:
|
if args.format:
|
||||||
@@ -757,11 +713,16 @@ def cmd_event_list(args):
|
|||||||
cmd += ["--format", args.format]
|
cmd += ["--format", args.format]
|
||||||
cmd += [args.range_start, args.range_end]
|
cmd += [args.range_start, args.range_end]
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
print(result.stdout.rstrip())
|
if result.returncode != 0:
|
||||||
except subprocess.CalledProcessError as e:
|
# khal search returns 1 when no results found — not a real error
|
||||||
print(f"Error: khal failed: {e.stderr.strip()}", file=sys.stderr)
|
if args.search and not result.stderr.strip():
|
||||||
sys.exit(1)
|
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:
|
except FileNotFoundError:
|
||||||
print("Error: khal is not installed", file=sys.stderr)
|
print("Error: khal is not installed", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user