Adding your google calendar agenda to your daily journal

If you want to skip ahead to today: I put this functionality together as a plugin: muness/obsidian-ics: Generate Daily Planner from ical feeds


I wanted to get my meetings into my Daily Notes. Here’s what I got working:


- Today's meetings
	 - [ ] 10:00-10:30 Chat about <...> https://xx.zoom.us/j/<id>?pwd=<pwd>
	 - [ ] 10:30-11:00 ❇️ Muness // ... 1:1 https://zoom.us/j/<id>?pwd=<pwd>
	 - [ ] 12:15-13:00 ❇️ ... / Muness Skip Level 
	 - [ ] 13:00-14:00 Team Weekly 
	 - [ ] 15:30-16:00 Appointment
	 - [ ] 16:30-17:00 ❇️ ... / Muness 1:1 

Along the day, I check off meetings as I wrap them up. Steps to get here follow.

Setup gcalcli

  1. insanum/gcalcli: Google Calendar Command Line Interface
  2. You’ll have to then setup your own API because Google doesn’t enable this out of the box. Follow the login instructions starting from “Go to the Google developer console”.
  3. Type gcalcli --client-id <clientid> --client-secret <secret> agenda from the command line replacing the client id and secret with the values you got from the preceding step. You should see your agenda after authenticating through your browser.
  4. Once authenticated, you no longer need to specify the client-id and secret. Test it with gcalcli agenda

Setup a script to pull the items you want to the command line and in the format you like. Test it and make sure it works on the command line. Here’s mine, replace “my work calendar” with the one you want to pull data from (you can get a list from gcalcli list:

IFS= read -r prefix

/usr/local/bin/gcalcli  --calendar "<my work calendar>"  agenda --details all --tsv --nodeclined  "`date '+%Y-%m-%d'`" "`date '+%Y-%m-%d 23:59'`" | awk -vprefix="$prefix" -F $'\t'  '{print prefix $2 "-" $4 " " $9 " " $8}' | grep -v "Busy (via Clockwise)" | grep -v "Travel Time (via Clockwise)" | grep -v "Lunch (via Clockwise)"

Bring into Obsidian in your daily notes

  1. Setup konodyuk/obsidian-text-expander: Text Expander plugin for Obsidian
  2. Move the script into ~/.obsidian/scripts/agenda.sh
  3. In the Text Expander settings, add a template for it:
		"regex": "^agenda:",
		"command": "echo <text> | cut -c 8- | sh <scripts_path>/agenda.sh"
	},
  1. And add it to your journal notes:
- Today's meetings
{{agenda:\t- [ ] }}

In the morning I hit tab after that template and Text Expander then uses the script to replace it with the list of meetings.

It’s a lot of steps. Let me know if you’re having trouble and I’ll try to help! Please include context and examples. I tested this on macos, and linux should be fine. I haven’t tried it on my Windows machine yet.

10 Likes

Here’s my approach using Templator. I have this integrated with the Day Planner plugin in Command mode. Omit the Day Planner bit if it doesn’t apply.

  1. I create a user command in Templater Settings:
    gcalcli : /usr/local/bin/gcalcli --calendar <work-calendar> --nocolor agenda 12am 11pm | tail -n +3

  2. I add this in my Daily Notes template below the ## Day Planner heading

<%*
let agenda = await tp.user.gcalcli()
let split = agenda.split("\n")
tR += split.map(i => {
 const date = i.match(/([0-9]*\:[0-9]*)\s/gi)
 const event = i.match(/(\[\[)?[A-Z].*/gi)
if (date && event) {
return `- [ ] ${date} ${event}`
}
}).join('\n')
%>
<%* app.commands.executeCommandById("obsidian-day-planner:app:link-day-planner-to-note") %>

Output Looks like:

  • [ ] 09:30 Morning Centering
  • [ ] 11:00 Morning Dog Walk
  • [ ] 11:30 Fika! [T/Th]
  • [ ] 11:40 Book Club
11 Likes

Thanks for sharing! User functions in Templater look like a nice improvement to my current solution!

1 Like

Thanks to your tip, I switched to Templater. Here’s my user function which I named agenda:

/usr/local/bin/gcalcli  --calendar "$calendar" agenda --details all --tsv --nodeclined "$from" "$to" | \
 awk -vprefix="$prefix" -F $'\t'  '{print prefix $2 "-" $4 " " $9 " " $8}' | \
 grep -v "Busy (via Clockwise)" | grep -v "Travel Time (via Clockwise)" | grep -v "Lunch (via Clockwise)"

and the template definition in my daily template

- Today's meetings
<% tp.user.agenda({from: tp.file.title + " 00:00", 
                                  to: tp.file.title + " 23:59", 
								  prefix: "\t- [ ] ", 
								  calendar: "<calendar title>"}) %>

Simpler and handles opening future day notes.

3 Likes

Hi shabe, thank you for the tip!
I was able to fetch my calendars and here is the output

Wed Apr 14  8:00am             Check 
            5:00pm             Yorgo
            6:00pm             Check gmail emails
            7:00pm             Handle discord 

Thu Apr 15  8:00am             Check 
            11:00am            TDCS EA071098    
            12:30pm            TDCS - FJ040798 
            3:00pm             ? Briefing avec 

Fri Apr 16  8:00am             Check 
            9:30am             TDCS - JR130598 
            11:00am            TDCS - PA020496 - 
            12:30pm            TDCS - SH230995 
            2:30pm             TDCS - VN120599 
            4:00pm             TDCS MM160500

And the function I used

But I am unable to parse it correctly (for me, your code just returns empty lines). Could you help me out?

@y.h : I may say something really stupid here but maybe try with :

<%*
let agenda = await tp.user.gcalcli()
let split = agenda.split("\n")
tR += split.map(i => {
 const date = i.match(/([0-9]*\:[0-9]*)\s/gi)
 const event = i.match(/(\[\[)?[A-Z].*/gi)
if (date && event) {
return `- [ ] ${date} ${event}`
}
}).join('\n')
%>
<%* tR += app.commands.executeCommandById("obsidian-day-planner:app:link-day-planner-to-note") %>

It’s the same but with tR += added in the last line of the snippet :blush: .

Thank you for trying to help :blush:
Sadly that didn’t work as it seems it’s the first block that doesn’t work properly for me

<%*
let agenda = await tp.user.gcalcli()
let split = agenda.split("\n")
tR += split.map(i => {
 const date = i.match(/([0-9]*\:[0-9]*)\s/gi)
 const event = i.match(/(\[\[)?[A-Z].*/gi)
if (date && event) {
return `- [ ] ${date} ${event}`
}
}).join('\n')
%>

The above just returns some line breaks.

1 Like

To make this easier to configure, I added parsing of the TSV to the template. With this, the user function looks like:

/usr/local/bin/gcalcli  --calendar "$calendar" agenda --details all --tsv --nodeclined "$from" "$to"

and the template:

<%*
let agenda = await tp.user.agenda({from: tp.file.title + " 00:00", 
                                  to: tp.file.title + " 23:59", prefix: "", 
			                      calendar: "<calendar>"})
let rawEvents = agenda.split("\n")
let agendaEntries = rawEvents.map(event => {
	let props = event.split("\t")
	return ({startDate: props[0],
				startTime: props[1],
				endDate: props[2],
				endTime: props[3],
				calendarUrl: props[4],
				notSure5: props[5],
				conferenceType: props[6],
				conferenceUrl: props[7],
				title: props[8],
				location: props[9],
				description: props[10],
				meetingUrlOrLocation: props[6] == "video" ? props[7] : props[9],
				raw: event,
	})
})

tR += agendaEntries.map(e => { return `\t- [ ] ${e.startTime} ${e.title} ${e.meetingUrlOrLocation}`}).join('\n')
%>
3 Likes

That’s fricken brilliant. Thank you!

1 Like

Awww I tried this out, but I’m no programmer and I failed so hard :frowning:

Shoot me a message on the discord (same name) and I’ll help troubleshoot

2 Likes

And one more round of improvements…

Templater User function, agenda:

/usr/local/bin/gcalcli  agenda --details all --tsv --nodeclined "$from" "$to"

And a Day Planner friendly template for output in the daily journal:


## Day Planner

<%*
function parseAgendaTsv(agenda) {
return agenda.split("\n").map(event => {
	let props = event.split("\t")
	// [gcalcli handlers](https://github.com/insanum/gcalcli/blob/master/gcalcli/details.py#L251-L259)
	return ({startDate: props[0],
				startTime: props[1],
				endDate: props[2],
				endTime: props[3],
				calendarUrl: props[4],
				notSure5: props[5],
				conferenceType: props[6],
				conferenceUrl: props[7],
				title: props[8],
				location: props[9],
				description: props[10],
				calendar: props[11],
				meetingUrlOrLocation: props[6] == "video" ? props[7] : props[9],
				raw: event,
	})
})
}

let tsvAgenda = await tp.user.agenda({from: tp.file.title + " 00:00", 
                                  to: tp.file.title + " 23:59"})
								  
agenda = parseAgendaTsv(tsvAgenda)

// filter the agenda as needed, e.g. for only the work calendar events:
agenda = agenda.filter((e,i) => e.calendar == "<work calendar>" )

tR += agenda.map(e => { return `- [ ] ${e.startTime} ${e.title} ${e.meetingUrlOrLocation}`}).join('\n')

%>
1 Like

Hello,
I am getting this error with $from and $to.
image

However, when I enter manually the date in the command, it works (same if it’s hard coded in the user function)
image

So perhaps there is an issue in how the values are transferred?

I haven’t tried this on Windows yet. It looks like the variables aren’t getting evaluated when the user function is called.

What does a user function echo $from $to render?

It seems you are spot on.

This
image
image
Gives this
image

1 Like

Hmm, I would be tempted to create an issue with exactly that at the Templater plugin issues, Issues · SilentVoid13/Templater · GitHub

Done! custom variables don't work on windows: they aren't being evaluated · Issue #128 · SilentVoid13/Templater · GitHub
Let’s hope the issue can be fixed!

Hello @muness
For now, with the help on github, I got it working, but I had to remove all emojies from my calendar. And your code works :pray:

Template

<%*
function parseAgendaTsv(agenda) {
return agenda.split("\n").map(event => {
	let props = event.split("\t")
	// [gcalcli handlers](https://github.com/insanum/gcalcli/blob/master/gcalcli/details.py#L251-L259)
	return ({startDate: props[0],
				startTime: props[1],
				endDate: props[2],
				endTime: props[3],
				calendarUrl: props[4],
				notSure5: props[5],
				conferenceType: props[6],
				conferenceUrl: props[7],
				title: props[8],
				location: props[9],
				description: props[10],
				calendar: props[11],
				meetingUrlOrLocation: props[6] == "video" ? props[7] : props[9],
				raw: event,
	})
})
}

let tsvAgenda = await tp.user.agenda2({from:  tp.date.now("YYYY-MM-DD", 0, tp.file.title, "YYYY/MM/DD"),to: tp.date.now("YYYY-MM-DD", 1, tp.file.title, "YYYY/MM/DD")})
								  
agenda = parseAgendaTsv(tsvAgenda)

// filter the agenda as needed, e.g. for only the work calendar events:
//agenda = agenda.filter((e,i) => e.calendar == "<work calendar>" )

tR += agenda.map(e => { return `- [ ] ${e.startTime} ${e.title} ${e.meetingUrlOrLocation}`}).join('\n')

%>

User Function

powershell  (C:/Users/adminuser/AppData/Local/Programs/Python/Python37-32/Scripts/gcalcli --nocolor agenda --details all --tsv --nodeclined %from% %to%)

However, @muness my calendar names seem to give errors. If I include ${e.calendar}, this is the result I get:

- [ ]undefined -- 00:00 Read 30 minutes 
- [ ]undefined -- 00:00 Write 2x 30 minutes 
etc

Here are the names of my calendar :

  • 01 - ME meetings and events
  • 02 - ME - time-blocking
  • 03 - WORK - Meetings and events
  • 04 - WORK - Time-blocking for activities work-related

Could you help me out with the syntax?
Ideally, I would like to be able to select/display all four calendars.

2 Likes

I’ll take a look and see what I come up with. I have not tested calendar names much so hopefully it’ll be a simple bug in my tsv parse function.

All this makes me want to try a Javascript library we can use directly instead of this hack.

1 Like

If you are able to come up with a solution using a javascript library, I think you would make the day of many Obsidian users. This would perhaps even open the door to a calendar sync obsidian plugin.

I found this but apparently it requires your calendar to be public. From my understanding, to access a private calendar, it would require authentification and thus a server-side rendering.

Some other ressources I found in case a javascript magician wants to have a go at this :slight_smile: