Commit 7eecfc31868f

Vincent Demeester <vincent@sbr.pm>
2026-01-15 09:48:09
feat(gcal-to-org): add event categorization and automatic sync timer
- Add CATEGORY property to events based on source calendar (work/personal) - Filter declined events using --nodeclined flag - Create systemd service and timer for automatic hourly sync - Update README with categorization details and NixOS integration Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent b766ab5
Changed files (4)
home
common
systems
kyushu
tools
home/common/services/gcal-to-org.nix
@@ -0,0 +1,48 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+
+with lib;
+
+let
+  cfg = config.services.gcal-to-org;
+in
+{
+  options.services.gcal-to-org = {
+    enable = mkEnableOption "automatic Google Calendar sync to org-mode";
+
+    interval = mkOption {
+      type = types.str;
+      default = "hourly";
+      description = "How often to sync the calendar (systemd timer format)";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.gcal-to-org = {
+      Unit = {
+        Description = "Sync Google Calendar to org-mode file";
+      };
+      Service = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.gcal-to-org}/bin/gcal-to-org";
+      };
+    };
+
+    systemd.user.timers.gcal-to-org = {
+      Unit = {
+        Description = "Timer for syncing Google Calendar to org-mode";
+      };
+      Timer = {
+        OnCalendar = cfg.interval;
+        Persistent = true;
+      };
+      Install = {
+        WantedBy = [ "timers.target" ];
+      };
+    };
+  };
+}
systems/kyushu/home.nix
@@ -11,6 +11,7 @@ in
     ../../home/common/dev/containers.nix
     ../../home/common/dev/tektoncd.nix
     ../../home/common/services/color-scheme-timer.nix
+    ../../home/common/services/gcal-to-org.nix
     ../../home/common/services/goimapnotify.nix
     ../../home/common/services/mail-monitor.nix
     ../../home/common/services/redhat.nix
@@ -83,6 +84,12 @@ in
     darkTime = "19:00"; # Switch to dark mode at 7pm
   };
 
+  # Google Calendar sync to org-mode
+  services.gcal-to-org = {
+    enable = true;
+    interval = "hourly";
+  };
+
   # ntfy notification subscriber
   systemd.user.services.ntfy-subscriber = {
     Unit = {
tools/gcal-to-org/main.go
@@ -40,6 +40,7 @@ type Event struct {
 	EndDate   string
 	EndTime   string
 	Title     string
+	Calendar  string
 }
 
 // calendarsFlag implements flag.Value for string slice
@@ -119,7 +120,7 @@ func run(config *Config) error {
 
 func fetchCalendarData(calendars []string, startDate, endDate string) ([]Event, error) {
 	// Build gcalcli command
-	args := []string{"agenda", "--tsv"}
+	args := []string{"agenda", "--tsv", "--details", "calendar", "--nodeclined"}
 
 	// Add calendar filters
 	for _, cal := range calendars {
@@ -156,7 +157,7 @@ func fetchCalendarData(calendars []string, startDate, endDate string) ([]Event,
 
 	// First row is header
 	header := records[0]
-	var startDateIdx, startTimeIdx, endDateIdx, endTimeIdx, titleIdx int
+	var startDateIdx, startTimeIdx, endDateIdx, endTimeIdx, titleIdx, calendarIdx int
 	for i, h := range header {
 		switch h {
 		case "start_date":
@@ -169,6 +170,8 @@ func fetchCalendarData(calendars []string, startDate, endDate string) ([]Event,
 			endTimeIdx = i
 		case "title":
 			titleIdx = i
+		case "calendar":
+			calendarIdx = i
 		}
 	}
 
@@ -185,6 +188,7 @@ func fetchCalendarData(calendars []string, startDate, endDate string) ([]Event,
 			EndDate:   getField(record, endDateIdx),
 			EndTime:   getField(record, endTimeIdx),
 			Title:     getField(record, titleIdx),
+			Calendar:  getField(record, calendarIdx),
 		}
 
 		events = append(events, event)
@@ -200,6 +204,22 @@ func getField(record []string, idx int) string {
 	return record[idx]
 }
 
+// categorizeCalendar returns "work" or "personal" based on calendar name
+func categorizeCalendar(calendarName string) string {
+	lowerName := strings.ToLower(calendarName)
+
+	// Work calendar indicators
+	workKeywords := []string{"redhat", "work", "vdemeest@redhat"}
+	for _, keyword := range workKeywords {
+		if strings.Contains(lowerName, keyword) {
+			return "work"
+		}
+	}
+
+	// Default to personal
+	return "personal"
+}
+
 func generateOrgFile(events []Event, outputFile, startDate, endDate string) error {
 	// Group events by date
 	eventsByDate := make(map[string][]Event)
@@ -277,6 +297,14 @@ func eventToOrgMode(event Event) string {
 
 	fmt.Fprintf(&buf, "* %s\n", title)
 
+	// Add PROPERTIES drawer with CATEGORY
+	if event.Calendar != "" {
+		category := categorizeCalendar(event.Calendar)
+		fmt.Fprintf(&buf, ":PROPERTIES:\n")
+		fmt.Fprintf(&buf, ":CATEGORY: %s\n", category)
+		fmt.Fprintf(&buf, ":END:\n")
+	}
+
 	// Build timestamp
 	timestamp := formatTimestamp(event)
 	if timestamp != "" {
tools/gcal-to-org/README.md
@@ -8,6 +8,8 @@ One-way sync from Google Calendar to org-mode file.
 - **Separate file**: Generates `~/desktop/org/calendar.org` which is easily regenerated
 - **Automatic cleanup**: Keeps only events from 2 weeks ago onward (configurable)
 - **Works with gcalcli**: Uses your existing gcalcli setup and authentication
+- **Automatic categorization**: Events are automatically categorized as "personal" or "work" based on their source calendar
+- **Smart filtering**: Only includes events you've accepted or marked as "maybe" (declined events are excluded)
 
 ## Usage
 
@@ -72,7 +74,35 @@ const (
 
 ## Automation
 
-### Systemd timer (recommended)
+### NixOS/home-manager (recommended)
+
+If using NixOS with home-manager, add to your home configuration:
+
+```nix
+{
+  imports = [
+    ../../home/common/services/gcal-to-org.nix
+  ];
+
+  services.gcal-to-org = {
+    enable = true;
+    interval = "hourly"; # Can be "hourly", "daily", or any systemd timer format
+  };
+}
+```
+
+This will automatically:
+- Create a systemd service to run gcal-to-org
+- Create a systemd timer to run it on your chosen schedule
+- Start the timer on login
+
+Check timer status:
+```bash
+systemctl --user list-timers gcal-to-org
+systemctl --user status gcal-to-org.service
+```
+
+### Manual systemd timer
 
 Create `~/.config/systemd/user/gcal-sync.service`:
 
@@ -130,15 +160,36 @@ DO NOT EDIT MANUALLY - changes will be overwritten.
 * 2026-01-14 Wednesday
 
 * Meeting with team
+:PROPERTIES:
+:CATEGORY: work
+:END:
 <2026-01-14 Wed 09:00-10:00>
 
 * All-day event
+:PROPERTIES:
+:CATEGORY: personal
+:END:
 <2026-01-14 Wed>
 
 * Multi-day conference
+:PROPERTIES:
+:CATEGORY: work
+:END:
 <2026-01-14 Wed>--<2026-01-16 Fri>
 ```
 
+### Event Categorization
+
+Events are automatically categorized based on their source calendar:
+
+- **Work**: Calendars containing "redhat", "work", or "vdemeest@redhat" are tagged with `CATEGORY: work`
+- **Personal**: All other calendars are tagged with `CATEGORY: personal`
+
+This allows you to:
+- Filter events by category in org-agenda views
+- Color-code events based on category
+- Create separate agenda views for personal vs work events
+
 ## Building
 
 ```bash
@@ -191,6 +242,18 @@ To use the calendar in your agenda views, add to your Emacs config:
 ;; Optional: exclude from refile targets (read-only)
 (setq org-refile-targets
       '((org-agenda-files :maxlevel . 3)))
+
+;; Optional: Color-code categories in agenda
+(setq org-agenda-category-icon-alist
+      '(("work" "๐Ÿ’ผ" nil nil :ascent center)
+        ("personal" "๐Ÿ " nil nil :ascent center)))
+
+;; Optional: Create custom agenda views by category
+(setq org-agenda-custom-commands
+      '(("w" "Work events" agenda ""
+         ((org-agenda-category-filter-preset '("+work"))))
+        ("p" "Personal events" agenda ""
+         ((org-agenda-category-filter-preset '("+personal"))))))
 ```
 
 ## License