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