Personal noctalia plugins collection
at main 123 lines 4.1 kB view raw
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()