Personal noctalia plugins collection
1#!/usr/bin/env python3
2"""List VTODO items from Evolution Data Server task lists."""
3
4import argparse
5import json
6import sys
7from datetime import datetime, timezone
8
9import gi
10gi.require_version("ECal", "2.0")
11gi.require_version("EDataServer", "1.2")
12gi.require_version("ICalGLib", "3.0")
13from gi.repository import ECal, EDataServer, ICalGLib
14
15
16def ical_time_to_iso(ical_time):
17 """Convert ICalTime to ISO 8601 string, or None if null/invalid."""
18 if not ical_time or ical_time.is_null_time():
19 return None
20 y = ical_time.get_year()
21 m = ical_time.get_month()
22 d = ical_time.get_day()
23 h = ical_time.get_hour()
24 mi = ical_time.get_minute()
25 s = ical_time.get_second()
26 try:
27 dt = datetime(y, m, d, h, mi, s)
28 return dt.isoformat()
29 except (ValueError, OverflowError):
30 return None
31
32
33def get_status_string(status):
34 mapping = {
35 ICalGLib.PropertyStatus.NEEDSACTION: "NEEDS-ACTION",
36 ICalGLib.PropertyStatus.COMPLETED: "COMPLETED",
37 ICalGLib.PropertyStatus.INPROCESS: "IN-PROCESS",
38 ICalGLib.PropertyStatus.CANCELLED: "CANCELLED",
39 }
40 return mapping.get(status, "NEEDS-ACTION")
41
42
43def main():
44 parser = argparse.ArgumentParser(description="List VTODO items from EDS")
45 parser.add_argument("--include-completed", action="store_true",
46 help="Include completed tasks")
47 args = parser.parse_args()
48
49 try:
50 registry = EDataServer.SourceRegistry.new_sync(None)
51 todos = []
52
53 for source in registry.list_sources(EDataServer.SOURCE_EXTENSION_TASK_LIST):
54 if not source.get_enabled():
55 continue
56
57 try:
58 client = ECal.Client.connect_sync(
59 source, ECal.ClientSourceType.TASKS, 1, None
60 )
61 except Exception:
62 continue
63
64 # #t matches all objects
65 success, result = client.get_object_list_as_comps_sync("#t", None)
66 if not success:
67 continue
68
69 cal_name = source.get_display_name()
70 cal_uid = source.get_uid()
71
72 for comp in result:
73 if comp.get_vtype() != ECal.ComponentVType.TODO:
74 continue
75
76 ical = comp.get_icalcomponent()
77 status = get_status_string(ical.get_status())
78
79 if not args.include_completed and status == "COMPLETED":
80 continue
81
82 due = ical_time_to_iso(ical.get_due())
83 dtstart = ical_time_to_iso(ical.get_dtstart())
84
85 # Get percent-complete
86 percent = 0
87 prop = ical.get_first_property(ICalGLib.PropertyKind.PERCENTCOMPLETE_PROPERTY)
88 if prop:
89 percent = prop.get_percentcomplete()
90
91 # Get priority
92 priority = ical.get_priority() if hasattr(ical, 'get_priority') else 0
93 # Fallback: read priority property directly
94 if priority == 0:
95 prop = ical.get_first_property(ICalGLib.PropertyKind.PRIORITY_PROPERTY)
96 if prop:
97 priority = prop.get_priority()
98
99 todos.append({
100 "uid": ical.get_uid(),
101 "summary": ical.get_summary() or "",
102 "description": ical.get_description() or "",
103 "due": due,
104 "dtstart": dtstart,
105 "status": status,
106 "priority": priority,
107 "percentComplete": percent,
108 "calendarName": cal_name,
109 "calendarUid": cal_uid,
110 })
111
112 # Sort: non-null due dates first (ascending), then null-due items
113 todos.sort(key=lambda t: (t["due"] is None, t["due"] or ""))
114
115 print(json.dumps(todos))
116
117 except Exception as e:
118 print(json.dumps({"error": str(e)}), file=sys.stderr)
119 sys.exit(1)
120
121
122if __name__ == "__main__":
123 main()