Personal noctalia plugins collection

Add inline event edit and delete functionality

Replace GNOME Calendar launch with an inline event detail popup
supporting view, edit, and delete operations. Events now carry
uid and calendarUid fields for EDS operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+512 -11
+70 -11
weekly-calendar/Main.qml
··· 296 296 var isDueAllDay = (dueDate.getHours() === 0 && dueDate.getMinutes() === 0) 297 297 // Render timed todos as short deadline markers (~8–10 px tall) 298 298 var endDate = isDueAllDay ? new Date(dueDate.getTime() + 86400000) 299 - : new Date(dueDate.getTime() + 5 * 60000) 299 + : new Date(dueDate.getTime() + 30 * 60000) 300 300 301 301 var todoEvent = { 302 302 id: "todo-" + todo.uid, ··· 364 364 id: id, title: event.summary || "Untitled Event", description: event.description || "", 365 365 location: event.location || "", startTime: start, endTime: end, allDay: allDay, multiDay: multiDay, 366 366 daySpan: daySpan, rawStart: event.start, rawEnd: event.end, duration: (event.end - event.start) / 3600, 367 - endsAtMidnight: endsMidnight, isTodo: false, todoUid: "", calendarUid: "", todoStatus: "", todoPriority: 0 367 + endsAtMidnight: endsMidnight, isTodo: false, todoUid: "", calendarUid: event.calendar_uid || "", 368 + eventUid: event.uid || "", todoStatus: "", todoPriority: 0 368 369 } 369 370 } 370 371 ··· 374 375 location: event.location, startTime: start, endTime: end, allDay: false, multiDay: true, 375 376 daySpan: 1, fullStartTime: event.startTime, fullEndTime: event.endTime, isPart: true, 376 377 partDay: new Date(day), partIndex: partNum, totalParts: total, 377 - isTodo: false, todoUid: "", calendarUid: "", todoStatus: "", todoPriority: 0 378 + isTodo: false, todoUid: "", calendarUid: event.calendarUid || "", eventUid: event.eventUid || "", 379 + todoStatus: "", todoPriority: 0 378 380 } 379 381 } 380 382 ··· 454 456 endsAtMidnight: event.endsAtMidnight, fullStartTime: event.fullStartTime, fullEndTime: event.fullEndTime, 455 457 startDay: startDay, spanDays: spanDays, lane: lane, isContinuation: isCont, 456 458 isTodo: event.isTodo || false, todoUid: event.todoUid || "", calendarUid: event.calendarUid || "", 457 - todoStatus: event.todoStatus || "", todoPriority: event.todoPriority || 0 459 + eventUid: event.eventUid || "", todoStatus: event.todoStatus || "", todoPriority: event.todoPriority || 0 458 460 } 459 461 } 460 462 ··· 631 633 currentDate = d 632 634 } 633 635 636 + // Event detail popup state 637 + property var selectedEvent: null 638 + property bool showEventDetail: false 639 + 634 640 function handleEventClick(event) { 635 - const date = event.startTime || new Date(); 636 - const month = date.getMonth() + 1; 637 - const day = date.getDate(); 638 - const year = date.getFullYear(); 639 - const dateWithSlashes = `${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}/${year.toString().substring(2)}`; 640 - if (ProgramCheckerService.gnomeCalendarAvailable) { 641 - Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes]); 641 + selectedEvent = { 642 + title: event.title || "", 643 + description: event.description || "", 644 + location: event.location || "", 645 + startTime: event.fullStartTime || event.startTime, 646 + endTime: event.fullEndTime || event.endTime, 647 + calendarUid: event.calendarUid || "", 648 + eventUid: event.eventUid || "", 649 + rawStart: event.rawStart || 0, 650 + rawEnd: event.rawEnd || 0 642 651 } 652 + showEventDetail = true 653 + } 654 + 655 + function deleteEvent(calendarUid, eventUid) { 656 + if (!pluginApi) return 657 + var scriptPath = pluginApi.pluginDir + "/scripts/update-event.py" 658 + updateEventProcess.command = ["python3", scriptPath, 659 + "--calendar", calendarUid, "--uid", eventUid, "--action", "delete"] 660 + updateEventProcess.running = true 661 + } 662 + 663 + function updateEvent(calendarUid, eventUid, summary, location, description, startTs, endTs) { 664 + if (!pluginApi) return 665 + var scriptPath = pluginApi.pluginDir + "/scripts/update-event.py" 666 + var args = ["python3", scriptPath, 667 + "--calendar", calendarUid, "--uid", eventUid, "--action", "update"] 668 + if (summary !== undefined && summary !== null) { args.push("--summary"); args.push(summary) } 669 + if (location !== undefined && location !== null) { args.push("--location"); args.push(location) } 670 + if (description !== undefined && description !== null) { args.push("--description"); args.push(description) } 671 + if (startTs > 0) { args.push("--start"); args.push(String(startTs)) } 672 + if (endTs > 0) { args.push("--end"); args.push(String(endTs)) } 673 + updateEventProcess.command = args 674 + updateEventProcess.running = true 643 675 } 644 676 645 677 function goToToday() { currentDate = new Date() } ··· 673 705 stderr: SplitParser { 674 706 onRead: data => createEventStderr += data 675 707 } 708 + } 709 + 710 + // Event update/delete process 711 + property string updateEventStdout: "" 712 + property string updateEventStderr: "" 713 + 714 + Process { 715 + id: updateEventProcess 716 + onExited: function(exitCode, exitStatus) { 717 + if (exitCode === 0) { 718 + try { 719 + var result = JSON.parse(updateEventStdout) 720 + if (result.success) { 721 + console.log("[weekly-calendar] Event updated/deleted") 722 + Qt.callLater(loadEvents) 723 + } 724 + } catch(e) { 725 + console.error("[weekly-calendar] Failed to parse update-event output: " + updateEventStdout) 726 + } 727 + } else { 728 + console.error("[weekly-calendar] update-event.py failed: " + updateEventStderr) 729 + } 730 + updateEventStdout = "" 731 + updateEventStderr = "" 732 + } 733 + stdout: SplitParser { onRead: data => updateEventStdout += data } 734 + stderr: SplitParser { onRead: data => updateEventStderr += data } 676 735 } 677 736 678 737 function createEvent(calendarUid, summary, startTimestamp, endTimestamp, location, description) {
+337
weekly-calendar/Panel.qml
··· 22 22 23 23 property bool showCreateDialog: false 24 24 property bool showCreateTaskDialog: false 25 + property bool showEventDetailDialog: false 26 + property bool eventDetailEditMode: false 27 + property bool showDeleteConfirmation: false 25 28 26 29 property real defaultHourHeight: 50 * Style.uiScaleRatio 27 30 property real minHourHeight: 32 * Style.uiScaleRatio ··· 380 383 } 381 384 } 382 385 386 + // Event detail/edit popup 387 + Rectangle { 388 + id: eventDetailOverlay 389 + anchors.fill: parent 390 + color: Qt.rgba(0, 0, 0, 0.5) 391 + visible: showEventDetailDialog 392 + z: 2000 393 + 394 + MouseArea { anchors.fill: parent; onClicked: { showEventDetailDialog = false; eventDetailEditMode = false; showDeleteConfirmation = false } } 395 + 396 + Rectangle { 397 + anchors.centerIn: parent 398 + width: 420 * Style.uiScaleRatio 399 + height: eventDetailColumn.implicitHeight + 2 * Style.marginM 400 + color: Color.mSurface 401 + radius: Style.radiusM 402 + 403 + MouseArea { anchors.fill: parent } 404 + 405 + ColumnLayout { 406 + id: eventDetailColumn 407 + anchors.fill: parent 408 + anchors.margins: Style.marginM 409 + spacing: Style.marginS 410 + 411 + property var evt: mainInstance?.selectedEvent || {} 412 + 413 + // View mode 414 + ColumnLayout { 415 + visible: !eventDetailEditMode && !showDeleteConfirmation 416 + spacing: Style.marginS 417 + Layout.fillWidth: true 418 + 419 + NText { 420 + text: eventDetailColumn.evt.title || "" 421 + font.pointSize: Style.fontSizeL; font.weight: Font.Bold 422 + color: Color.mOnSurface 423 + wrapMode: Text.Wrap; Layout.fillWidth: true 424 + } 425 + 426 + RowLayout { 427 + spacing: Style.marginS 428 + NIcon { icon: "clock"; pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant } 429 + NText { 430 + text: { 431 + var e = eventDetailColumn.evt 432 + if (!e.startTime) return "" 433 + return mainInstance?.formatDateTime(e.startTime) + " - " + mainInstance?.formatDateTime(e.endTime) 434 + } 435 + font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 436 + wrapMode: Text.Wrap; Layout.fillWidth: true 437 + } 438 + } 439 + 440 + RowLayout { 441 + visible: (eventDetailColumn.evt.location || "") !== "" 442 + spacing: Style.marginS 443 + NIcon { icon: "map-pin"; pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant } 444 + NText { 445 + text: eventDetailColumn.evt.location || "" 446 + font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 447 + wrapMode: Text.Wrap; Layout.fillWidth: true 448 + } 449 + } 450 + 451 + NText { 452 + visible: (eventDetailColumn.evt.description || "") !== "" 453 + text: eventDetailColumn.evt.description || "" 454 + font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 455 + wrapMode: Text.Wrap; Layout.fillWidth: true 456 + } 457 + 458 + RowLayout { 459 + Layout.fillWidth: true 460 + spacing: Style.marginS 461 + 462 + Item { Layout.fillWidth: true } 463 + 464 + Rectangle { 465 + Layout.preferredWidth: editEventBtn.implicitWidth + 2 * Style.marginM 466 + Layout.preferredHeight: editEventBtn.implicitHeight + Style.marginS 467 + color: Color.mPrimary; radius: Style.radiusS 468 + visible: (eventDetailColumn.evt.eventUid || "") !== "" 469 + NText { 470 + id: editEventBtn; anchors.centerIn: parent 471 + text: pluginApi.tr("panel.edit") || "Edit" 472 + color: Color.mOnPrimary; font.weight: Font.Bold 473 + } 474 + MouseArea { 475 + anchors.fill: parent; cursorShape: Qt.PointingHandCursor 476 + onClicked: { 477 + var e = eventDetailColumn.evt 478 + editEventSummary.text = e.title || "" 479 + editEventLocation.text = e.location || "" 480 + editEventDescription.text = e.description || "" 481 + if (e.startTime) { 482 + var s = new Date(e.startTime) 483 + editEventDate.text = s.getFullYear() + "-" + String(s.getMonth()+1).padStart(2,'0') + "-" + String(s.getDate()).padStart(2,'0') 484 + editEventStartTime.text = String(s.getHours()).padStart(2,'0') + ":" + String(s.getMinutes()).padStart(2,'0') 485 + } 486 + if (e.endTime) { 487 + var en = new Date(e.endTime) 488 + editEventEndTime.text = String(en.getHours()).padStart(2,'0') + ":" + String(en.getMinutes()).padStart(2,'0') 489 + } 490 + eventDetailEditMode = true 491 + } 492 + } 493 + } 494 + 495 + Rectangle { 496 + Layout.preferredWidth: deleteEventBtn.implicitWidth + 2 * Style.marginM 497 + Layout.preferredHeight: deleteEventBtn.implicitHeight + Style.marginS 498 + color: Color.mError; radius: Style.radiusS 499 + visible: (eventDetailColumn.evt.eventUid || "") !== "" 500 + NText { 501 + id: deleteEventBtn; anchors.centerIn: parent 502 + text: pluginApi.tr("panel.delete") || "Delete" 503 + color: Color.mOnError; font.weight: Font.Bold 504 + } 505 + MouseArea { 506 + anchors.fill: parent; cursorShape: Qt.PointingHandCursor 507 + onClicked: showDeleteConfirmation = true 508 + } 509 + } 510 + 511 + Rectangle { 512 + Layout.preferredWidth: closeEventBtn.implicitWidth + 2 * Style.marginM 513 + Layout.preferredHeight: closeEventBtn.implicitHeight + Style.marginS 514 + color: Color.mSurfaceVariant; radius: Style.radiusS 515 + NText { 516 + id: closeEventBtn; anchors.centerIn: parent 517 + text: pluginApi.tr("panel.close") || "Close" 518 + color: Color.mOnSurfaceVariant 519 + } 520 + MouseArea { 521 + anchors.fill: parent; cursorShape: Qt.PointingHandCursor 522 + onClicked: { showEventDetailDialog = false; eventDetailEditMode = false } 523 + } 524 + } 525 + } 526 + } 527 + 528 + // Delete confirmation mode 529 + ColumnLayout { 530 + visible: showDeleteConfirmation 531 + spacing: Style.marginS 532 + Layout.fillWidth: true 533 + 534 + NText { 535 + text: (pluginApi.tr("panel.delete_confirm") || "Delete this event?") 536 + font.pointSize: Style.fontSizeM; font.weight: Font.Bold 537 + color: Color.mOnSurface 538 + } 539 + NText { 540 + text: eventDetailColumn.evt.title || "" 541 + font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 542 + } 543 + 544 + RowLayout { 545 + Layout.fillWidth: true 546 + spacing: Style.marginS 547 + Item { Layout.fillWidth: true } 548 + 549 + Rectangle { 550 + Layout.preferredWidth: confirmDeleteBtn.implicitWidth + 2 * Style.marginM 551 + Layout.preferredHeight: confirmDeleteBtn.implicitHeight + Style.marginS 552 + color: Color.mError; radius: Style.radiusS 553 + NText { 554 + id: confirmDeleteBtn; anchors.centerIn: parent 555 + text: pluginApi.tr("panel.delete") || "Delete" 556 + color: Color.mOnError; font.weight: Font.Bold 557 + } 558 + MouseArea { 559 + anchors.fill: parent; cursorShape: Qt.PointingHandCursor 560 + onClicked: { 561 + var e = eventDetailColumn.evt 562 + mainInstance?.deleteEvent(e.calendarUid, e.eventUid) 563 + showEventDetailDialog = false 564 + showDeleteConfirmation = false 565 + eventDetailEditMode = false 566 + } 567 + } 568 + } 569 + 570 + Rectangle { 571 + Layout.preferredWidth: cancelDeleteBtn.implicitWidth + 2 * Style.marginM 572 + Layout.preferredHeight: cancelDeleteBtn.implicitHeight + Style.marginS 573 + color: Color.mSurfaceVariant; radius: Style.radiusS 574 + NText { 575 + id: cancelDeleteBtn; anchors.centerIn: parent 576 + text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant 577 + } 578 + MouseArea { 579 + anchors.fill: parent; cursorShape: Qt.PointingHandCursor 580 + onClicked: showDeleteConfirmation = false 581 + } 582 + } 583 + } 584 + } 585 + 586 + // Edit mode 587 + ColumnLayout { 588 + visible: eventDetailEditMode && !showDeleteConfirmation 589 + spacing: Style.marginS 590 + Layout.fillWidth: true 591 + 592 + NText { 593 + text: pluginApi.tr("panel.edit_event") || "Edit Event" 594 + font.pointSize: Style.fontSizeL; font.weight: Font.Bold 595 + color: Color.mOnSurface 596 + } 597 + 598 + NText { text: pluginApi.tr("panel.summary"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 599 + TextField { 600 + id: editEventSummary 601 + Layout.fillWidth: true 602 + color: Color.mOnSurface 603 + background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 604 + } 605 + 606 + NText { text: pluginApi.tr("panel.date"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 607 + TextField { 608 + id: editEventDate 609 + Layout.fillWidth: true 610 + placeholderText: "YYYY-MM-DD" 611 + color: Color.mOnSurface 612 + background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 613 + } 614 + 615 + RowLayout { 616 + spacing: Style.marginS 617 + ColumnLayout { 618 + Layout.fillWidth: true 619 + NText { text: pluginApi.tr("panel.start_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 620 + TextField { 621 + id: editEventStartTime 622 + Layout.fillWidth: true 623 + placeholderText: "HH:MM" 624 + color: Color.mOnSurface 625 + background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 626 + } 627 + } 628 + ColumnLayout { 629 + Layout.fillWidth: true 630 + NText { text: pluginApi.tr("panel.end_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 631 + TextField { 632 + id: editEventEndTime 633 + Layout.fillWidth: true 634 + placeholderText: "HH:MM" 635 + color: Color.mOnSurface 636 + background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 637 + } 638 + } 639 + } 640 + 641 + NText { text: pluginApi.tr("panel.location"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 642 + TextField { 643 + id: editEventLocation 644 + Layout.fillWidth: true 645 + color: Color.mOnSurface 646 + background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 647 + } 648 + 649 + NText { text: pluginApi.tr("panel.description"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 650 + TextField { 651 + id: editEventDescription 652 + Layout.fillWidth: true 653 + color: Color.mOnSurface 654 + background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 655 + } 656 + 657 + RowLayout { 658 + Layout.fillWidth: true 659 + spacing: Style.marginS 660 + Item { Layout.fillWidth: true } 661 + 662 + Rectangle { 663 + Layout.preferredWidth: saveEventBtn.implicitWidth + 2 * Style.marginM 664 + Layout.preferredHeight: saveEventBtn.implicitHeight + Style.marginS 665 + color: Color.mPrimary; radius: Style.radiusS 666 + NText { 667 + id: saveEventBtn; anchors.centerIn: parent 668 + text: pluginApi.tr("panel.save") || "Save" 669 + color: Color.mOnPrimary; font.weight: Font.Bold 670 + } 671 + MouseArea { 672 + anchors.fill: parent; cursorShape: Qt.PointingHandCursor 673 + onClicked: { 674 + var e = eventDetailColumn.evt 675 + var dateParts = editEventDate.text.split("-") 676 + var startParts = editEventStartTime.text.split(":") 677 + var endParts = editEventEndTime.text.split(":") 678 + var startDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]), 679 + parseInt(startParts[0]), parseInt(startParts[1]), 0) 680 + var endDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]), 681 + parseInt(endParts[0]), parseInt(endParts[1]), 0) 682 + mainInstance?.updateEvent( 683 + e.calendarUid, e.eventUid, 684 + editEventSummary.text.trim(), 685 + editEventLocation.text.trim(), 686 + editEventDescription.text.trim(), 687 + Math.floor(startDate.getTime()/1000), 688 + Math.floor(endDate.getTime()/1000)) 689 + showEventDetailDialog = false 690 + eventDetailEditMode = false 691 + } 692 + } 693 + } 694 + 695 + Rectangle { 696 + Layout.preferredWidth: editCancelBtn.implicitWidth + 2 * Style.marginM 697 + Layout.preferredHeight: editCancelBtn.implicitHeight + Style.marginS 698 + color: Color.mSurfaceVariant; radius: Style.radiusS 699 + NText { 700 + id: editCancelBtn; anchors.centerIn: parent 701 + text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant 702 + } 703 + MouseArea { 704 + anchors.fill: parent; cursorShape: Qt.PointingHandCursor 705 + onClicked: eventDetailEditMode = false 706 + } 707 + } 708 + } 709 + } 710 + } 711 + } 712 + } 713 + 383 714 // UI 384 715 Rectangle { 385 716 id: panelContainer ··· 625 956 mainInstance?.completeTodo(eventData.calendarUid, eventData.todoUid) 626 957 } else { 627 958 mainInstance?.handleEventClick(eventData) 959 + showEventDetailDialog = true 960 + eventDetailEditMode = false 961 + showDeleteConfirmation = false 628 962 } 629 963 } 630 964 onExited: TooltipService.hide() ··· 876 1210 mainInstance?.completeTodo(model.calendarUid, model.todoUid) 877 1211 } else { 878 1212 mainInstance?.handleEventClick(eventData) 1213 + showEventDetailDialog = true 1214 + eventDetailEditMode = false 1215 + showDeleteConfirmation = false 879 1216 } 880 1217 } 881 1218 onExited: TooltipService.hide()
+105
weekly-calendar/scripts/update-event.py
··· 1 + #!/usr/bin/env python3 2 + """Update or delete a VEVENT item via Evolution Data Server.""" 3 + 4 + import argparse 5 + import json 6 + import sys 7 + from datetime import datetime, timezone 8 + 9 + import gi 10 + gi.require_version("ECal", "2.0") 11 + gi.require_version("EDataServer", "1.2") 12 + gi.require_version("ICalGLib", "3.0") 13 + from gi.repository import ECal, EDataServer, ICalGLib 14 + 15 + 16 + def find_calendar_source(registry, calendar_uid): 17 + source = registry.ref_source(calendar_uid) 18 + if source and source.has_extension(EDataServer.SOURCE_EXTENSION_CALENDAR): 19 + return source 20 + for src in registry.list_sources(EDataServer.SOURCE_EXTENSION_CALENDAR): 21 + if src.get_display_name() == calendar_uid or src.get_uid() == calendar_uid: 22 + return src 23 + return None 24 + 25 + 26 + def 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 + 34 + def main(): 35 + parser = argparse.ArgumentParser(description="Update/delete EDS VEVENT item") 36 + parser.add_argument("--calendar", required=True, help="Calendar source UID") 37 + parser.add_argument("--uid", required=True, help="VEVENT UID") 38 + parser.add_argument("--action", required=True, choices=["delete", "update"], 39 + help="Action to perform") 40 + parser.add_argument("--summary", help="New event summary") 41 + parser.add_argument("--location", help="New event location") 42 + parser.add_argument("--description", help="New event description") 43 + parser.add_argument("--start", type=int, help="New start time (unix timestamp)") 44 + parser.add_argument("--end", type=int, help="New end time (unix timestamp)") 45 + args = parser.parse_args() 46 + 47 + try: 48 + registry = EDataServer.SourceRegistry.new_sync(None) 49 + source = find_calendar_source(registry, args.calendar) 50 + if not source: 51 + print(json.dumps({"success": False, "error": f"Calendar not found: {args.calendar}"})) 52 + sys.exit(1) 53 + 54 + client = ECal.Client.connect_sync( 55 + source, ECal.ClientSourceType.EVENTS, 1, None 56 + ) 57 + 58 + if args.action == "delete": 59 + client.remove_object_sync(args.uid, None, ECal.ObjModType.ALL, ECal.OperationFlags.NONE, None) 60 + print(json.dumps({"success": True})) 61 + return 62 + 63 + # For update, fetch the existing component first 64 + success, comp = client.get_object_sync(args.uid, None, None) 65 + if not success or not comp: 66 + print(json.dumps({"success": False, "error": "VEVENT not found"})) 67 + sys.exit(1) 68 + 69 + ical = comp.get_icalcomponent() 70 + 71 + if args.summary is not None: 72 + ical.set_summary(args.summary) 73 + 74 + if args.location is not None: 75 + ical.set_location(args.location) 76 + 77 + if args.description is not None: 78 + ical.set_description(args.description) 79 + 80 + if args.start is not None: 81 + dt = datetime.fromtimestamp(args.start, tz=timezone.utc) 82 + t = ICalGLib.Time.new_null_time() 83 + t.set_date(dt.year, dt.month, dt.day) 84 + t.set_time(dt.hour, dt.minute, dt.second) 85 + t.set_timezone(ICalGLib.Timezone.get_utc_timezone()) 86 + ical.set_dtstart(t) 87 + 88 + if args.end is not None: 89 + dt = datetime.fromtimestamp(args.end, tz=timezone.utc) 90 + t = ICalGLib.Time.new_null_time() 91 + t.set_date(dt.year, dt.month, dt.day) 92 + t.set_time(dt.hour, dt.minute, dt.second) 93 + t.set_timezone(ICalGLib.Timezone.get_utc_timezone()) 94 + ical.set_dtend(t) 95 + 96 + client.modify_object_sync(comp, ECal.ObjModType.ALL, ECal.OperationFlags.NONE, None) 97 + print(json.dumps({"success": True})) 98 + 99 + except Exception as e: 100 + print(json.dumps({"success": False, "error": str(e)})) 101 + sys.exit(1) 102 + 103 + 104 + if __name__ == "__main__": 105 + main()