Custom TRMNL Calendar Plugin
2025-05-24
If you haven't heard of the TRMNL yet, you're missing out. It's a little e-ink companion display that allows you to show pretty much anything you want on the screen--in a highly readable and distraction-free way.
Part of what makes it so great is its plugin ecosystem, a collection of first party and third party plugins to pull data in and display it in some useful way. As you'd expect they have a great calendar plugin however the data density when split vertically for me wasn't enough. I'd only see 2 or 3 events at most, often leaving a ton of empty space.
To fix this I started going down the custom plugin rabbit hole and one of the main things you need to do is get data into TRMNL so you can output it on device. While I started creating a little golang ICS parser for my calendar data I was pointed to this little helpful doc Fetch Plugin Data. Simply put, this allows me to grab the already parsed calendar data that TMRNL is getting and using it in my own custom plugin!
Making it all work
First you need to add your calendar data in to one of the native calendar plugins (I used the Apple calendar one but any should work). Once added, make sure the layout for it is set to the month option. You'll then need to get a user API key and follow the document from above about how to fetch plugin data and have it appear in your own custom plugin.
With that done you should hopefully see calendar data inside your custom plugin under the data.events
key under "Your Variables" on the markup edit page. Then paste in the below liquid template snippet into the appropriate markup size and you're done! I made it for the half vertical size but it can be adjusted to work with others.
Update 2025-05-26: Small update to the snippet below to also ignore future events since we don't care about them in this agenda style view but the month view where the data is sourced from does care.
🔥 Update 2025-06-05: Big update below as TRMNL updated the data structure for the calendar data solving a ton of issues (sorted data, better handling of recurring events). You're data show now live in data.events
as an array of events. I'm keeping things like sorting and filtering future events as I'm not certain if the TRMNL data will be sorted and it will still include past data due to being monthly data.
{% comment %}
Sort events by date_time and filter for future events
Handle timezone-aware comparisons
{% endcomment %}
{% assign sorted_events = data.events | sort: 'date_time' %}
{% assign now = 'now' | date: "%s" %}
{% assign user_offset_hours = trmnl.user.utc_offset | divided_by: 3600 %}
{% assign today_date = 'now' | date: "%B %e" %}
<div class="layout layout--top">
<div class="list" data-list-limit="true" data-list-hidden-count="true" data-list-max-columns="1">
{% assign current_day = "" %}
{% assign day_event_index = 0 %}
{% assign is_first_visible = true %}
{% for event in sorted_events %}
{% assign event_time = event.date_time | date: "%s" %}
{% if event_time >= now %}
{% assign event_day = event.date_time | date: "%A, %B %d" %}
{% if event_day != current_day %}
{% unless is_first_visible %}
</div>
{% endunless %}
{% assign current_day = event_day %}
{% assign day_event_index = 1 %}
<div class="content">
<span class="label mt--2 mb--1 text--gray-3">{{ current_day }}</span>
{% assign is_first_visible = false %}
{% else %}
{% assign day_event_index = day_event_index | plus: 1 %}
{% endif %}
<div class="item mb--2">
<div class="meta">
{% if event.all_day %}
<span class="index">#</span>
{% else %}
<span class="index">{{ day_event_index }}</span>
{% endif %}
</div>
<div class="content">
<span class="title title--small">{{ event.summary }}</span>
{% unless event.all_day %}
<span class="label label--small label--underline">{{ event.start }} - {{ event.end }}</span>
{% endunless %}
</div>
</div>
{% endif %}
{% endfor %}
{% unless is_first_visible %}
</div>
{% endunless %}
</div>
</div>
<div class="title_bar">
<img class="image" src="https://usetrmnl.com/images/plugins/trmnl--render.svg">
<span class="title">{{ trmnl.plugin_settings.instance_name }}</span>
<span class="instance">{{ today_date }}</span>
</div>
It's a little weird looking to handle the data coming in from the month TRMNL data (mostly around it not being sorted), but should work. There is a small bug in the hiding overflowing data as well at the moment where it doesn't handle nested data structures but hopefully that'll have a fix in the future to prevent things being cutoff at the bottom.
All in all a relatively painless way to add a semi-custom calendar plugin, and doesn't require running something externally!