Personal noctalia plugins collection
1#!/usr/bin/env python3
2"""Update or delete a VTODO item via Evolution Data Server."""
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 find_task_source(registry, task_list_uid):
17 source = registry.ref_source(task_list_uid)
18 if source and source.has_extension(EDataServer.SOURCE_EXTENSION_TASK_LIST):
19 return source
20 for src in registry.list_sources(EDataServer.SOURCE_EXTENSION_TASK_LIST):
21 if src.get_display_name() == task_list_uid or src.get_uid() == task_list_uid:
22 return src
23 return None
24
25
26def remove_property(comp, kind):
27 """Remove all properties of the given kind from a component."""
28 prop = comp.get_first_property(kind)
29 while prop:
30 comp.remove_property(prop)
31 prop = comp.get_first_property(kind)
32
33
34def main():
35 parser = argparse.ArgumentParser(description="Update/delete EDS VTODO item")
36 parser.add_argument("--task-list", required=True, help="Task list UID")
37 parser.add_argument("--uid", required=True, help="VTODO UID")
38 parser.add_argument("--action", required=True, choices=["complete", "uncomplete", "delete", "update"],
39 help="Action to perform")
40 parser.add_argument("--summary", help="New task summary (for update)")
41 parser.add_argument("--description", help="New task description (for update)")
42 parser.add_argument("--due", type=int, help="New due date as unix timestamp (for update)")
43 parser.add_argument("--priority", type=int, help="New priority 0-9 (for update)")
44 args = parser.parse_args()
45
46 try:
47 registry = EDataServer.SourceRegistry.new_sync(None)
48 source = find_task_source(registry, args.task_list)
49 if not source:
50 print(json.dumps({"success": False, "error": f"Task list not found: {args.task_list}"}))
51 sys.exit(1)
52
53 client = ECal.Client.connect_sync(
54 source, ECal.ClientSourceType.TASKS, 1, None
55 )
56
57 if args.action == "delete":
58 client.remove_object_sync(args.uid, None, ECal.ObjModType.ALL, ECal.OperationFlags.NONE, None)
59 print(json.dumps({"success": True}))
60 return
61
62 # For complete/uncomplete, fetch the existing component first
63 success, comp = client.get_object_sync(args.uid, None, None)
64 if not success or not comp:
65 print(json.dumps({"success": False, "error": "VTODO not found"}))
66 sys.exit(1)
67
68 ical = comp.get_icalcomponent()
69
70 if args.action == "complete":
71 ical.set_status(ICalGLib.PropertyStatus.COMPLETED)
72
73 # Set PERCENT-COMPLETE to 100
74 remove_property(ical, ICalGLib.PropertyKind.PERCENTCOMPLETE_PROPERTY)
75 prop = ICalGLib.Property.new_percentcomplete(100)
76 ical.add_property(prop)
77
78 # Set COMPLETED timestamp
79 remove_property(ical, ICalGLib.PropertyKind.COMPLETED_PROPERTY)
80 now = datetime.now(timezone.utc)
81 completed_time = ICalGLib.Time.new_null_time()
82 completed_time.set_date(now.year, now.month, now.day)
83 completed_time.set_time(now.hour, now.minute, now.second)
84 completed_time.set_timezone(ICalGLib.Timezone.get_utc_timezone())
85 prop = ICalGLib.Property.new_completed(completed_time)
86 ical.add_property(prop)
87
88 elif args.action == "uncomplete":
89 ical.set_status(ICalGLib.PropertyStatus.NEEDSACTION)
90
91 # Set PERCENT-COMPLETE to 0
92 remove_property(ical, ICalGLib.PropertyKind.PERCENTCOMPLETE_PROPERTY)
93 prop = ICalGLib.Property.new_percentcomplete(0)
94 ical.add_property(prop)
95
96 # Remove COMPLETED timestamp
97 remove_property(ical, ICalGLib.PropertyKind.COMPLETED_PROPERTY)
98
99 elif args.action == "update":
100 if args.summary is not None:
101 ical.set_summary(args.summary)
102 if args.description is not None:
103 ical.set_description(args.description)
104 if args.due is not None:
105 dt = datetime.fromtimestamp(args.due, tz=timezone.utc)
106 t = ICalGLib.Time.new_null_time()
107 t.set_date(dt.year, dt.month, dt.day)
108 t.set_time(dt.hour, dt.minute, dt.second)
109 t.set_timezone(ICalGLib.Timezone.get_utc_timezone())
110 remove_property(ical, ICalGLib.PropertyKind.DUE_PROPERTY)
111 prop = ICalGLib.Property.new_due(t)
112 ical.add_property(prop)
113 if args.priority is not None:
114 remove_property(ical, ICalGLib.PropertyKind.PRIORITY_PROPERTY)
115 prop = ICalGLib.Property.new_priority(args.priority)
116 ical.add_property(prop)
117
118 client.modify_object_sync(comp, ECal.ObjModType.ALL, ECal.OperationFlags.NONE, None)
119 print(json.dumps({"success": True}))
120
121 except Exception as e:
122 print(json.dumps({"success": False, "error": str(e)}))
123 sys.exit(1)
124
125
126if __name__ == "__main__":
127 main()