Emacs calendar

Upcoming events

Introduction

This calendar is maintained by sacha@sachachua.com. You can find it at https://emacslife.com/calendar/

You can find a list of upcoming events and other meet-ups at https://www.emacswiki.org/emacs/Usergroups.

Or you can add this iCal to your calendar program:

https://emacslife.com/calendar/emacs-calendar.ics

Or you can view the following HTML calendars:

(America/Vancouver America/Chicago America/Toronto Etc/GMT Europe/Berlin Asia/Kolkata Asia/Singapore)

(Let me know if you want me to add your timezone! - mailto:sacha@sachachua.com)

Or you can periodically download and include one of these files in your Org agenda files:

Enjoy!

Code I use to run it

Timezones

  • America/Vancouver
  • America/Chicago
  • America/Toronto
  • Etc/GMT
  • Europe/Berlin
  • Asia/Kolkata
  • Asia/Singapore

Download and parse the iCal file with Python

pip3 install icalevents recurring_ical_events pypandoc requests pytz
import requests
import http.cookiejar as cookielib
import os
cj = cookielib.MozillaCookieJar(os.path.expanduser('~/.cache/meetup-cookies.txt'))
cj.load()
data = requests.get('https://www.meetup.com/emacs-sf/events/ical', cookies=cj)
print(data)

<Response [200]>

import requests
import http.cookiejar as cookielib
import os
from urllib.request import urlopen
import icalendar
import datetime
from dateutil.relativedelta import *
import recurring_ical_events
import pytz
import re
import pypandoc
import subprocess
import sys
import csv
import requests

INCLUDE_MEETUPS = True
INCLUDE_OTHER_ICALS = True

cj = cookielib.MozillaCookieJar(os.path.expanduser('~/.cache/meetup-cookies.txt'))
cj.load()

#                 'Singapore': 'Emacs-SG',
other_meetups = {'EmacsNYC': 'New-York-Emacs-Meetup',
                 'EmacsSF (in person)': 'Emacs-SF',
                 'EmacsATX': 'EmacsATX',
                 'Pelotas, Brazil': 'Pelotas-Emacs-Meetup',
                 'Emacs FFM': 'emacs-ffm',
                 'London Emacs (in person)': 'London-Emacs-Hacking',
                 'London Emacs Lisp': 'London-Emacs-Lisp-Meetup',
                 # 'Finland': 'Finland-Emacs-User-Group',
                 'Delhi': 'Emacs-Delhi'}
other_icals = [ {'name': 'Atelier Emacs (in French)',
                 'source': 'https://mobilizon.fr/@communaute_emacs_francophone/feed/ics'},
                {'name': 'Emacs APAC',
                 'source': 'https://emacs-apac.gitlab.io/announcements/index.ics'},
                {'name': 'Emacs Paris',
                 'source': 'https://emacs-doctor.com/emacs-paris-meetups.ics',
                 'url':'https://emacs-doctor.com/'},
                {'name': 'M-x Research',
                'url': 'https://m-x-research.github.io/',
                'source': 'https://calendar.google.com/calendar/ical/o0tiadljp5dq7lkb51mnvnrh04%40group.calendar.google.com/public/basic.ics',
                'summary_re': r'^M-x Research - '},
                {'name': 'Emacs.si (in person)',
                'url': 'https://dogodki.kompot.si/@emacssi',
                'source': 'https://dogodki.kompot.si/@emacssi/feed/ics'}
               ]
# https://www.meetup.com/Emacs-SF/events/ical/',
all_timezones = []

def summarized_event(e, timezones):
  s = ''
  times = None
  if re.search('in[ -]person|v #živo|au Bib', e['SUMMARY']):
    m = re.search("Event time:(.*?)([<\n]|$)", e.get('description') or '')
    if m:
      s += '- ' + m.group(1).strip()
    elif e['DTSTART'].dt.tzinfo:
      tz = e['DTSTART'].dt.tzinfo
      times = [[e['DTSTART'].dt.astimezone(tz), tz.zone, e['DTSTART'].dt.astimezone(tz).utcoffset()]]
    else:
      s += '- see event for details'
  else:
    times = [[e['DTSTART'].dt.astimezone(pytz.timezone(t)), t, e['DTSTART'].dt.astimezone(pytz.timezone(t)).utcoffset()] for t in timezones]
    times.sort(key=lambda x: x[2])
  if times:
    for i, t in enumerate(times):
      if i == 0 or t[0].day != times[i - 1][0].day:
         if i > 0:
           s += " -- "
         s += t[0].strftime('%a %b %-d %H%M') + " " + t[1]
      else:
         s += " - " + t[0].strftime('%H%M') + " " + t[1]
  if e['LOCATION']:
    return "- %s %s %s" % (e['SUMMARY'], e['LOCATION'], s)
  else:
    return "- %s %s" % (e['SUMMARY'], s)

link = "https://calendar.google.com/calendar/ical/c_rkq3fc6u8k1nem23qegqc90l6c%40group.calendar.google.com/public/basic.ics"
f = urlopen(link)
cal = icalendar.Calendar.from_ical(f.read())
start_date = datetime.date(datetime.date.today().year, datetime.date.today().month, 1)

end_date = datetime.date(datetime.date.today().year + 1, datetime.date.today().month, 1)

for event in cal.walk():
  if event.name == 'VEVENT' and not '(ignore)' in event.name:
    if isinstance(event['DTSTART'].dt, datetime.datetime):
      event['UID'] = str(event['DTSTART'].dt.timestamp()) + '-' + event['UID']
    elif isinstance(event['DTSTART'].dt, datetime.date):
      event['UID'] = str(datetime.datetime.combine(event['DTSTART'].dt, datetime.time.min)) + '-' + event['UID']
    if event.get('location') == '':
      match = re.search(r'href="([^"]+)"', event.get('description'))
      if not match:
        match = re.search('^(http.*?)(&nbsp;|<br>|\n)', event.get('description'))
      if match:
        event['location'] = match.group(1)
      else:
        print(event.get('description'))
  if event.name == 'VTIMEZONE' and event['TZID'] not in all_timezones:
    all_timezones.append(event['TZID'])

def merge_cal(main_cal, name, url, start_date, end_date, info=None):
  # print('Requesting cal ', url)
  try:
    if 'meetup.com' in url:
      data = requests.get(url, cookies=cj)
    else:
      data = requests.get(url)
    meetup_cal = icalendar.Calendar.from_ical(data.content)
  except Exception as e:
    print("Error with url: %s" % url)
    print(repr(e))
    return
  # Copy the timezone components
  for tz in meetup_cal.walk():
    if tz.name == 'VTIMEZONE' and tz['TZID'] not in all_timezones:
      all_timezones.append(tz['TZID'])
      main_cal.add_component(tz)
  meetup_events = recurring_ical_events.of(meetup_cal).between(start_date, end_date)
  for event in meetup_events:
    low = event['SUMMARY'].lower()
    if info and 'summary_re' in info:
      event['SUMMARY'] = re.sub(info['summary_re'], '', event['SUMMARY'])
    if re.search(r'virtual|hybrid|online', low):
      name = re.sub(r' \(in person\)', '', name)
    event['SUMMARY'] = name + ': ' + event['SUMMARY']
    event['UID'] = str(event['DTSTART'].dt.timestamp()) + '-' + event['UID']
    event['LOCATION'] = ('URL' in event and event['URL']) or (info and ('url' in info) and info['url'])
    main_cal.add_component(event)

def merge_meetup_events(cal, start_date, end_date):
  global other_meetups
  for name, identifier in other_meetups.items():
    url = "https://www.meetup.com/%s/events/ical/" % (identifier)
    merge_cal(cal, name, url, start_date, end_date)

if INCLUDE_MEETUPS:
  merge_meetup_events(cal, start_date, end_date)
if INCLUDE_OTHER_ICALS:
  for item in other_icals:
    merge_cal(cal, item['name'], item['source'], start_date, end_date, item)

def convert_events_to_utc(cal):
  # Convert everything to UTC?
  utc = pytz.timezone('UTC')
  for event in cal.walk():
    if event.name == 'VEVENT' in event.name:
      for attr in ['DTSTART', 'DTEND', 'DTSTAMP', 'RECURRENCE-ID']:
        if attr in event:
          event[attr].dt = event[attr].dt.astimezone(utc)
          event[attr].params.clear()



f = open('emacs-calendar.ics', 'wb')

f.write(cal.to_ical())
f.close()

def get_aware_dt(dt_val):
    if isinstance(dt_val, datetime.date) and not isinstance(dt_val, datetime.datetime):
        dt_val = datetime.datetime.combine(dt_val, datetime.time.min)
    if dt_val.tzinfo is not None and dt_val.tzinfo.utcoffset(dt_val) is not None:
        return dt_val.astimezone(datetime.timezone.utc)
    else:
        return dt_val.replace(tzinfo=datetime.timezone.utc)

events = recurring_ical_events.of(cal).between(start_date, end_date)
events.sort(key=lambda x: get_aware_dt(x['DTSTART'].dt))

files = {}
org_date = "%Y-%m-%d %a %H:%M" # 2006-11-01 Wed 19:15
# Prepare string for copying
highlight_start = datetime.datetime.now(pytz.UTC)
highlight_end = datetime.datetime.now(pytz.UTC) + relativedelta(weeks=+6)
all_end = datetime.datetime.now(pytz.UTC) + relativedelta(weeks=+1)

org_timezones = [x for x in timezones]
for offset in range(-12, 14):
  if offset < 0:
    org_timezones.append('Etc/GMT+%s' % abs(offset))
  else:
    org_timezones.append('Etc/GMT-%s' % abs(offset))

for t in org_timezones:
  stub = "emacs-calendar-" + re.sub('^.*?/', '', t).lower()
  if 'gmt-' in stub:
    stub = re.sub(r'gmt-', 'gmt+', stub)
  elif 'gmt+' in stub:
    stub = re.sub(r'gmt\+', 'gmt-', stub)
  ical_args = ["ical2html", "-l", "-f", "Times are in " + t, "-z", t, datetime.datetime.today().strftime("%Y%m01"), "P8W", "emacs-calendar.ics"]
  output = subprocess.check_output(ical_args).decode(sys.stdout.encoding)
  changed = re.sub(r'<span class=summary>([^<]+)</span>\n<pre><b class=location>([^<]+)</b></pre>',
                   r'<span class="summary"><a href="\2">\1</a></span>', output)
  f = open(stub + '.html', 'wb')
  f.write(changed.encode(sys.stdout.encoding))
  f.close()
  files[t] = open(stub + '.org', "w")

with open('events.csv', 'w', newline='') as csvfile:
  fieldnames = ['DTSTART', 'DTEND', 'LOCATION', 'SUMMARY', 'TEXT', 'TZID']
  writer = csv.DictWriter(csvfile, fieldnames=fieldnames, extrasaction='ignore')
  writer.writeheader()
  for e in events:
    writer.writerow({**e,
                     'DTSTART': e['DTSTART'].dt.isoformat(),
                     'DTEND': e['DTEND'].dt.isoformat(),
                     'TEXT': summarized_event(e, timezones)
                     })


for e in events:
  if 'DESCRIPTION' not in e:
    e['DESCRIPTION'] = ''
  desc = pypandoc.convert_text(e['DESCRIPTION'], 'org', format='html').replace('\\\\', '')
  if isinstance(e['DTSTART'].dt, datetime.datetime):
    utc = datetime.datetime.fromtimestamp(e['DTSTART'].dt.timestamp(), datetime.UTC)
  else:
    utc = datetime.datetime.fromtimestamp(datetime.datetime.combine(e['DTSTART'].dt, datetime.time.min).timestamp(), datetime.UTC)
  if utc >= highlight_start and utc <= highlight_end:
    if 'Mastering Emacs' in e['DESCRIPTION'] and utc > all_end:
      pass # do nothing
    else:
      print(summarized_event(e, timezones))
  for t in org_timezones:
    zone = pytz.timezone(t)
    if isinstance(e['DTSTART'].dt, datetime.datetime):
      start = e['DTSTART'].dt.astimezone(zone).strftime(org_date)
      end = e['DTEND'].dt.strftime(org_date)
    else:
      start = e['DTSTART'].dt.strftime("%Y-%m-%d %a")
      end = (e['DTEND'].dt - datetime.timedelta(days=1)).strftime("%Y-%m-%d %a")
    files[t].write("""* %s
:PROPERTIES:
:LOCATION: %s
:END:
<%s>--<%s>

%s

""" % (e['SUMMARY'], e['LOCATION'], start, end, desc))

2025-11-24 00:00:00-021s81ge91rgv4g51diu3fuf9v@google.com IS DATE VEVENT({'DTSTART': vDDDTypes(2025-11-24, Parameters({'VALUE': 'DATE'})), 'DTEND': vDDDTypes(2025-11-26, Parameters({'VALUE': 'DATE'})), 'DTSTAMP': vDDDTypes(2025-10-20 18:51:56+00:00, Parameters({})), 'UID': '2025-11-24 00:00:00-021s81ge91rgv4g51diu3fuf9v@google.com', 'CREATED': vDDDTypes(2025-10-20 18:17:12+00:00, Parameters({})), 'DESCRIPTION': vText('b'<a href="https://emacsboost.com/" target="_blank">https://emacsboost.com/%3C/a%3E%3Cbr%3EEvent time:\xc2\xa0 lundi 24 au mardi 25 novembre 2025 <br><h2>\xc3\x80 propos de la formation</h2><br><p>Vous passez des heures \xc3\xa0 travailler avec votre \xc3\xa9diteur de texte\\, \xc3\xa0 essayerd\xe2\x80\x99hacker Atom ou \xc3\xa0 explorer les forums de VSCode ? Il est temps de vous formersur Emacs !</p><p>Bien plus qu\xe2\x80\x99un simple \xc3\xa9diteur\\, Emacs est un environnement polyvalent\\, flexibleet extensible. Avec sa capacit\xc3\xa9 d\xe2\x80\x99extension et ses raccourcis clavier puissants\\,Emacs peut transformer votre fa\xc3\xa7on de travailler.</p><p>Rejoignez-moi dans cette formation pour explorer toutes ses fonctionnalit\xc3\xa9s\\,<b>booster votre productivit\xc3\xa9</b>. et d\xc3\xa9couvrir comment Emacs peut devenir votre outilultime pour accomplir des t\xc3\xa2ches avec rapidit\xc3\xa9\\, pr\xc3\xa9cision et facilit\xc3\xa9.</p><br><br><h2>Pourquoi choisir ma formation ?</h2><br><ul><li>Exp\xc3\xa9rience in\xc3\xa9gal\xc3\xa9e : B\xc3\xa9n\xc3\xa9ficiez de mes 26 ann\xc3\xa9es d\xe2\x80\x99expertise dansl\xe2\x80\x99utilisation d\xe2\x80\x99Emacs. J\xe2\x80\x99ai travaill\xc3\xa9 avec des professionnels de diverssecteurs et les ai aid\xc3\xa9s \xc3\xa0 ma\xc3\xaetriser Emacs pour accomplir leurs t\xc3\xa2ches plusrapidement et plus efficacement.</li><li>Packages de pointe : Apprenez \xc3\xa0 utiliser les meilleurs packages d\xe2\x80\x99Emacs pouroptimiser vos flux de travail. De la r\xc3\xa9daction d\xe2\x80\x99une th\xc3\xa8se \xc3\xa0 l\xe2\x80\x99\xc3\xa9criture decode ou d\xe2\x80\x99e-mails\\, Emacs offre une multitude de fonctionnalit\xc3\xa9s pour r\xc3\xa9pondre\xc3\xa0 vos besoins.</li><li>Formation intensive en 2 jours : Mon programme intensif vous permettra dema\xc3\xaetriser Emacs en un temps record. Vous apprendrez les astuces et lesraccourcis qui feront toute la diff\xc3\xa9rence dans votre productivit\xc3\xa9 quotidienne.De plus\\, vous aurez l\xe2\x80\x99occasion de mettre en pratique vos nouvelles comp\xc3\xa9tences\xc3\xa0 travers des exercices concrets.</li><li>Cr\xc3\xa9ation d\xe2\x80\x99un forum priv\xc3\xa9 exclusivement r\xc3\xa9serv\xc3\xa9 aux anciens participants : Cetespace offre une plateforme id\xc3\xa9ale pour poser toutes les questions qui peuventsurgir apr\xc3\xa8s avoir termin\xc3\xa9 la formation. Vous b\xc3\xa9n\xc3\xa9ficierez ainsi d\xe2\x80\x99un soutiencollaboratif\\, car les autres stagiaires auront \xc3\xa9galement la possibilit\xc3\xa9 departager leurs connaissances et de r\xc3\xa9pondre \xc3\xa0 vos interrogations.</li></ul><p>Ainsi\\, d\xc3\xa8s la fin de ma formation\\, vous serez pr\xc3\xaat(e) \xc3\xa0 exploiter tout lepotentiel de cet \xc3\xa9diteur de texte puissant et \xc3\xa0 l\xe2\x80\x99incorporer efficacement dansvotre flux de travail.</p><p><a href="https://emacsboost.com/contenu-et-prix.html" target="_blank">Contenu de la formation et tarifs</a></p><br><br><h2>Qui suis-je ?</h2><br><br><p>FNI-photo.png</p><p>Je suis Fabrice Niessen\\, consultant informatique et utilisateur compulsifd\xe2\x80\x99Emacs depuis plus de 26 ans. Je suis un contributeur actif au sein de lacommunaut\xc3\xa9 Emacs\\, et mon parcours est marqu\xc3\xa9 par diff\xc3\xa9rentes r\xc3\xa9alisations\\,notamment dans l\xe2\x80\x99am\xc3\xa9lioration d\xe2\x80\x99Org mode\\, une extension majeure d\xe2\x80\x99Emacs d\xc3\xa9di\xc3\xa9e\xc3\xa0 la gestion des t\xc3\xa2ches\\, \xc3\xa0 la prise de notes et \xc3\xa0 l\xe2\x80\x99organisation deprojets. Gr\xc3\xa2ce \xc3\xa0 mes contributions\\, j\xe2\x80\x99ai r\xc3\xa9ussi \xc3\xa0 attirer davantaged\xe2\x80\x99utilisateurs vers Emacs et Org mode en ajoutant des th\xc3\xa8mes pour les exportsHTML.</p><p>En tant que professionnel de l\xe2\x80\x99informatique\\, je suis passionn\xc3\xa9 parl\xe2\x80\x99enseignement d\xe2\x80\x99Emacs\\, et j\xe2\x80\x99interviens r\xc3\xa9guli\xc3\xa8rement lors de conf\xc3\xa9rencesprestigieuses telles que le FOSDEM \xc3\xa0 Bruxelles ou les stages LaTeX de Dunkerque\\,o\xc3\xb9 j\xe2\x80\x99ai l\xe2\x80\x99opportunit\xc3\xa9 de partager mes connaissances. Mes pr\xc3\xa9sentations mettentprincipalement en avant l\xe2\x80\x99utilisation d\xe2\x80\x99Emacs et d\xe2\x80\x99Org mode pour la r\xc3\xa9dactionscientifique\\, la gestion de projet et l\xe2\x80\x99organisation des connaissances.</p><p>Je trouve un grand plaisir \xc3\xa0 contribuer \xc3\xa0 l\xe2\x80\x99am\xc3\xa9lioration et \xc3\xa0 l\xe2\x80\x99expansion de cesoutils\\, qui sont utilis\xc3\xa9s par de nombreux d\xc3\xa9veloppeurs\\, chercheurs et \xc3\xa9crivains\xc3\xa0 travers le monde.</p><p>Vous pouvez retrouver mes projets et contributions sur mes d\xc3\xa9p\xc3\xb4ts <a href="https://github.com/fniessen" target="_blank">GitHub</a>.</p>''), 'LAST-MODIFIED': vDDDTypes(2025-10-20 18:27:18+00:00, Parameters({})), 'LOCATION': vText('b'https://emacsboost.com/''), 'SEQUENCE': 1, 'STATUS': vText('b'CONFIRMED''), 'SUMMARY': vText('b'Formation fondamentale (2 jours) - Paris\\, in-person''), 'TRANSP': vText('b'TRANSPARENT'')}) 2025-11-26 00:00:00-05bnl9evrt3p3l7ski0043342g@google.com IS DATE VEVENT({'DTSTART': vDDDTypes(2025-11-26, Parameters({'VALUE': 'DATE'})), 'DTEND': vDDDTypes(2025-11-28, Parameters({'VALUE': 'DATE'})), 'DTSTAMP': vDDDTypes(2025-10-20 18:51:56+00:00, Parameters({})), 'UID': '2025-11-26 00:00:00-05bnl9evrt3p3l7ski0043342g@google.com', 'CREATED': vDDDTypes(2025-10-20 18:18:05+00:00, Parameters({})), 'DESCRIPTION': vText('b'<a href="https://emacsboost.com/" target="_blank">https://emacsboost.com/%3C/a%3E%3Cbr%3EEvent time:\xc2\xa0lundi 27 au mardi 28 novembre 2025\xc2\xa0<br><br><h2>\xc3\x80 propos de la formation</h2><br><p>Vous passez des heures \xc3\xa0 travailler avec votre \xc3\xa9diteur de texte\\, \xc3\xa0 essayerd\xe2\x80\x99hacker Atom ou \xc3\xa0 explorer les forums de VSCode ? Il est temps de vous formersur Emacs !</p><p>Bien plus qu\xe2\x80\x99un simple \xc3\xa9diteur\\, Emacs est un environnement polyvalent\\, flexibleet extensible. Avec sa capacit\xc3\xa9 d\xe2\x80\x99extension et ses raccourcis clavier puissants\\,Emacs peut transformer votre fa\xc3\xa7on de travailler.</p><p>Rejoignez-moi dans cette formation pour explorer toutes ses fonctionnalit\xc3\xa9s\\,<b>booster votre productivit\xc3\xa9</b>. et d\xc3\xa9couvrir comment Emacs peut devenir votre outilultime pour accomplir des t\xc3\xa2ches avec rapidit\xc3\xa9\\, pr\xc3\xa9cision et facilit\xc3\xa9.</p><br><br><h2>Pourquoi choisir ma formation ?</h2><br><ul><li>Exp\xc3\xa9rience in\xc3\xa9gal\xc3\xa9e : B\xc3\xa9n\xc3\xa9ficiez de mes 26 ann\xc3\xa9es d\xe2\x80\x99expertise dansl\xe2\x80\x99utilisation d\xe2\x80\x99Emacs. J\xe2\x80\x99ai travaill\xc3\xa9 avec des professionnels de diverssecteurs et les ai aid\xc3\xa9s \xc3\xa0 ma\xc3\xaetriser Emacs pour accomplir leurs t\xc3\xa2ches plusrapidement et plus efficacement.</li><li>Packages de pointe : Apprenez \xc3\xa0 utiliser les meilleurs packages d\xe2\x80\x99Emacs pouroptimiser vos flux de travail. De la r\xc3\xa9daction d\xe2\x80\x99une th\xc3\xa8se \xc3\xa0 l\xe2\x80\x99\xc3\xa9criture decode ou d\xe2\x80\x99e-mails\\, Emacs offre une multitude de fonctionnalit\xc3\xa9s pour r\xc3\xa9pondre\xc3\xa0 vos besoins.</li><li>Formation intensive en 2 jours : Mon programme intensif vous permettra dema\xc3\xaetriser Emacs en un temps record. Vous apprendrez les astuces et lesraccourcis qui feront toute la diff\xc3\xa9rence dans votre productivit\xc3\xa9 quotidienne.De plus\\, vous aurez l\xe2\x80\x99occasion de mettre en pratique vos nouvelles comp\xc3\xa9tences\xc3\xa0 travers des exercices concrets.</li><li>Cr\xc3\xa9ation d\xe2\x80\x99un forum priv\xc3\xa9 exclusivement r\xc3\xa9serv\xc3\xa9 aux anciens participants : Cetespace offre une plateforme id\xc3\xa9ale pour poser toutes les questions qui peuventsurgir apr\xc3\xa8s avoir termin\xc3\xa9 la formation. Vous b\xc3\xa9n\xc3\xa9ficierez ainsi d\xe2\x80\x99un soutiencollaboratif\\, car les autres stagiaires auront \xc3\xa9galement la possibilit\xc3\xa9 departager leurs connaissances et de r\xc3\xa9pondre \xc3\xa0 vos interrogations.</li></ul><p>Ainsi\\, d\xc3\xa8s la fin de ma formation\\, vous serez pr\xc3\xaat(e) \xc3\xa0 exploiter tout lepotentiel de cet \xc3\xa9diteur de texte puissant et \xc3\xa0 l\xe2\x80\x99incorporer efficacement dansvotre flux de travail.</p><p><a href="https://emacsboost.com/contenu-et-prix.html" target="_blank">Contenu de la formation et tarifs</a></p><br><br><h2>Qui suis-je ?</h2><br><br><p>FNI-photo.png</p><p>Je suis Fabrice Niessen\\, consultant informatique et utilisateur compulsifd\xe2\x80\x99Emacs depuis plus de 26 ans. Je suis un contributeur actif au sein de lacommunaut\xc3\xa9 Emacs\\, et mon parcours est marqu\xc3\xa9 par diff\xc3\xa9rentes r\xc3\xa9alisations\\,notamment dans l\xe2\x80\x99am\xc3\xa9lioration d\xe2\x80\x99Org mode\\, une extension majeure d\xe2\x80\x99Emacs d\xc3\xa9di\xc3\xa9e\xc3\xa0 la gestion des t\xc3\xa2ches\\, \xc3\xa0 la prise de notes et \xc3\xa0 l\xe2\x80\x99organisation deprojets. Gr\xc3\xa2ce \xc3\xa0 mes contributions\\, j\xe2\x80\x99ai r\xc3\xa9ussi \xc3\xa0 attirer davantaged\xe2\x80\x99utilisateurs vers Emacs et Org mode en ajoutant des th\xc3\xa8mes pour les exportsHTML.</p><p>En tant que professionnel de l\xe2\x80\x99informatique\\, je suis passionn\xc3\xa9 parl\xe2\x80\x99enseignement d\xe2\x80\x99Emacs\\, et j\xe2\x80\x99interviens r\xc3\xa9guli\xc3\xa8rement lors de conf\xc3\xa9rencesprestigieuses telles que le FOSDEM \xc3\xa0 Bruxelles ou les stages LaTeX de Dunkerque\\,o\xc3\xb9 j\xe2\x80\x99ai l\xe2\x80\x99opportunit\xc3\xa9 de partager mes connaissances. Mes pr\xc3\xa9sentations mettentprincipalement en avant l\xe2\x80\x99utilisation d\xe2\x80\x99Emacs et d\xe2\x80\x99Org mode pour la r\xc3\xa9dactionscientifique\\, la gestion de projet et l\xe2\x80\x99organisation des connaissances.</p><p>Je trouve un grand plaisir \xc3\xa0 contribuer \xc3\xa0 l\xe2\x80\x99am\xc3\xa9lioration et \xc3\xa0 l\xe2\x80\x99expansion de cesoutils\\, qui sont utilis\xc3\xa9s par de nombreux d\xc3\xa9veloppeurs\\, chercheurs et \xc3\xa9crivains\xc3\xa0 travers le monde.</p><p>Vous pouvez retrouver mes projets et contributions sur mes d\xc3\xa9p\xc3\xb4ts <a href="https://github.com/fniessen" target="_blank">GitHub</a>.</p>''), 'LAST-MODIFIED': vDDDTypes(2025-10-20 18:27:38+00:00, Parameters({})), 'LOCATION': vText('b'https://emacsboost.com/''), 'SEQUENCE': 0, 'STATUS': vText('b'CONFIRMED''), 'SUMMARY': vText('b'Formation avanc\xc3\xa9e (2 jours) - Paris\\, in-person''), 'TRANSP': vText('b'TRANSPARENT'')})

Sync

rsync -avze ssh ./ web:/var/www/emacslife.com/calendar/ --exclude=.git

Convert timezones

(defun my-summarize-times (time timezones)
  (let (prev-day)
    (mapconcat
     (lambda (tz)
       (let ((cur-day (format-time-string "%a %b %-e" time tz))
             (cur-time (format-time-string "%H%MH %Z" time tz)))
         (if (equal prev-day cur-day)
             cur-time
           (setq prev-day cur-day)
           (concat cur-day " " cur-time))))
     timezones
     " / ")))

(defun my-org-summarize-event-in-timezones (timezones)
  (interactive (list (or timezones my-timezones)))
  (save-window-excursion
    (save-excursion
      (when (derived-mode-p 'org-agenda-mode) (org-agenda-goto))
      (when (re-search-forward org-element--timestamp-regexp nil (save-excursion (org-end-of-subtree) (point)))
        (goto-char (match-beginning 0))
        (let* ((times (org-element-timestamp-parser))
               (start-time (org-timestamp-to-time (org-timestamp-split-range times)))
               (event-time-from-body
                (save-excursion
                  (when  (re-search-forward "Event time: \\(.+\\)"
                                            (save-excursion (org-end-of-subtree) (point))
                                            t)
                    (match-string 1))))
               (msg (format "%s - %s - %s"
                            (org-get-heading t t t t)

                            (or event-time-from-body (my-summarize-times start-time timezones))
                            ;; (cond
                            ;;  ((time-less-p (org-timestamp-to-time (org-timestamp-split-range times t)) (current-time))
                            ;;   "(past)")
                            ;;  ((time-less-p (current-time) start-time)
                            ;;   (concat "in " (format-seconds "%D %H %M%Z" (time-subtract start-time (current-time)))))
                            ;;  (t "(ongoing)"))
                            (org-entry-get (point) "LOCATION"))))
          (if (called-interactively-p 'any)
              (progn
                (message "%s" msg)
                (kill-new msg))
            msg))))))

my-org-summarize-event-in-timezones

my-org-summarize-event-in-timezones

Summarize upcoming ones

(defun my-summarize-upcoming-events (limit timezones)
  (interactive (list (org-read-date nil t) my-timezones))
  (let (result)
    (with-current-buffer (find-file-noselect "~/sync/emacs-calendar/emacs-calendar-toronto.org")
      (goto-char (point-min))
      (org-map-entries
       (lambda ()
         (save-excursion
           (when (re-search-forward org-element--timestamp-regexp nil (save-excursion (org-end-of-subtree) (point)))
             (goto-char (match-beginning 0))
             (let ((time (org-timestamp-to-time (org-timestamp-split-range (org-element-timestamp-parser)))))
               (when (and (time-less-p (current-time) time)
                          (time-less-p time limit))
                 (setq result (cons
                               (cons time
                                     (my-org-summarize-event-in-timezones timezones)) result)))))))))
    (setq result (mapconcat
                  (lambda (o) (format "- %s" (cdr  o)))
                  (sort result (lambda (a b)
                                 (time-less-p (car a) (car b))
                                 ))
                  "\n"))
    (if (interactive-p)
        (insert result)
      result)))

my-summarize-upcoming-events

my-summarize-upcoming-events

Announcing Emacs events

(defun my-announce-on-irc (channels message host port)
  (with-temp-buffer
    (insert "PASS " erc-password "\n"
            "USER " erc-nick "\n"
            "NICK " erc-nick "\n"
            (mapconcat (lambda (o)
                         (format "PRIVMSG %s :%s\n" o message))
                       channels "")
            "QUIT\n")
    (call-process-region (point-min) (point-max) "ncat" nil 0 nil
                         "--ssl" host (number-to-string port))))

(defun my-announce-on-irc-and-twitter (time channels message host port)
  (when (< (time-to-seconds (subtract-time (current-time) time)) (* 5 60))
    (shell-command-to-string (format
                              (if my-laptop-p
                                  "zsh -l -c 'rvm use 2.4.1; t update %s'"
                                "bash -l -c 't update %s'")
                              (shell-quote-argument message)))
    (my-announce-on-irc channels message host port)))

(defun my-schedule-announcement (time message)
  (interactive (list (org-read-date t t) (read-string "Message: ")))
  (run-at-time time nil #'my-announce-on-irc-and-twitter time '("#emacs" "#emacsconf") message erc-server erc-port))

(defun my-org-table-as-alist (table)
  "Convert TABLE to an alist. Remember to set :colnames no."
  (let ((headers (seq-map 'intern (car table))))
    (cl-loop for x in (cdr table) collect (-zip headers x))))

(defun my-schedule-announcements-for-upcoming-emacs-meetups ()
  (interactive)
  (cancel-function-timers #'my-announce-on-irc-and-twitter)
  (let ((events (my-org-table-as-alist (pcsv-parse-file "events.csv")))
        (now (current-time))
        (before-limit (time-add (current-time) (seconds-to-time (* 14 24 60 60)))))
    (mapc (lambda (o)
            (unless (string-match "in person\\|workshop" (alist-get 'SUMMARY o))
              (let* ((start-time (encode-time (parse-time-string (alist-get 'DTSTART o))))
                     (fifteen-minutes-before (seconds-to-time (- (time-to-seconds start-time) (* 15 60)))))
                (when (and (time-less-p now fifteen-minutes-before)
                           (time-less-p fifteen-minutes-before before-limit))
                  (my-schedule-announcement fifteen-minutes-before
                                            (format "In 15 minutes: %s - see %s for details"
                                                    (alist-get 'SUMMARY o)
                                                    (alist-get 'LOCATION o))))
                (when (and (time-less-p now start-time)
                           (time-less-p start-time before-limit))
                  (my-schedule-announcement start-time
                                            (format "Starting now: %s - see %s for details"
                                                    (alist-get 'SUMMARY o)
                                                    (alist-get 'LOCATION o)))))))
          events)))

my-schedule-announcements-for-upcoming-emacs-meetups

Update EmacsWiki

(use-package oddmuse
:load-path "~/vendor/oddmuse-el"
:if my-laptop-p
:ensure nil
:config (oddmuse-mode-initialize)
:hook (oddmuse-mode-hook .
          (lambda ()
            (unless (string-match "question" oddmuse-post)
              (when (string-match "EmacsWiki" oddmuse-wiki)
                (setq oddmuse-post (concat "uihnscuskc=1;" oddmuse-post)))
              (when (string-match "OddmuseWiki" oddmuse-wiki)
                (setq oddmuse-post (concat "ham=1;" oddmuse-post)))))))

((lambda nil (unless (string-match question oddmuse-post) (when (string-match EmacsWiki oddmuse-wiki) (setq oddmuse-post (concat uihnscuskc=1; oddmuse-post))) (when (string-match OddmuseWiki oddmuse-wiki) (setq oddmuse-post (concat ham=1; oddmuse-post))))) (lambda nil (if (string-match question oddmuse-post) nil (if (string-match EmacsWiki oddmuse-wiki) (progn (setq oddmuse-post (concat uihnscuskc=1; oddmuse-post)))) (if (string-match OddmuseWiki oddmuse-wiki) (progn (setq oddmuse-post (concat ham=1; oddmuse-post)))))))

nil

TODO Using Org Mode, Emacs Lisp, and TRAMP to parse meetup calendar entries and generate a crontab   org emacs

Times and time zones trip me up. Even with calendar notifications, I still fumble scheduled events. Automation helps me avoid embarrassing hiccups.

We run BigBlueButton as a self-hosted web conferencing server for EmacsConf. It needs at least 8 GB of RAM when active. When it's dormant, it fits on a 1 GB RAM virtual private server. It's easy enough to scale the server up and down as needed. Using the server for Emacs meetups in between EmacsConfs gives people a way to get together, and it also means I can regularly test the infrastructure. That makes scaling it up for EmacsConf less nerve-wracking.

I have some code that processes various Emacs meetup iCalendar files (often with repeating entries) and combines them into one iCal file that people can subscribe to calendar, as well as Org files in different timezones that they can include in their org-agenda-files. The code I use to parse the iCal seems to handle time zones and daylight savings time just fine. I set it up so that the Org files have simple non-repeating entries, which makes them easy to parse. I can use the Org file to determine the scheduled jobs to run with cron on a home server (named xu4) that's up all the time.

This code parses the Org file for schedule information, then generates pairs of crontab entries. The first entry scales the BigBlueButton server up 1 hour before the event using my bbb-testing script, and the second entry scales the server down 6 hours after the event using my bbb-dormant script (more info). That gives organizers time to test it before the event starts, and it gives people plenty of time to chat. A shared CPU 8 GB RAM Linode costs USD 0.072 per hour, so that's USD 0.50 per meetup hosted.

Using #+begin_src emacs-lisp :file "/ssh:xu4:~/bbb.crontab" :results file as the header for my code block and using an SSH agent for authentication lets me use TRAMP to write the file directly to the server. (See Results of Evaluation (The Org Manual))

(let* ((file "/home/sacha/sync/emacs-calendar/emacs-calendar-toronto.org")
       (time-format "%M %H %d %m")
       (bbb-meetups "OrgMeetup\\|Emacs Berlin\\|Emacs APAC")
       (scale-up "/home/sacha/bin/bbb-testing")
       (scale-down "/home/sacha/bin/bbb-dormant"))
  (mapconcat
   (lambda (o)
     (let ((start-time (format-time-string time-format (- (car o) 3600 )))
           (end-time (format-time-string time-format (+ (car o) (* 6 3600)))))
       (format "# %s\n%s * %s\n%s * %s\n"
               (cdr o)
               start-time
               scale-up
               end-time
               scale-down)))
   (delq nil
         (with-temp-buffer
           (insert-file-contents file)
           (org-mode)
           (goto-char (point-min))
           (org-map-entries
            (lambda ()
              (when (and
                     (string-match bbb-meetups (org-entry-get (point) "ITEM"))
                     (re-search-forward org-tr-regexp (save-excursion (org-end-of-subtree)) t))
                (let ((time (match-string 0)))
                  (cons (org-time-string-to-seconds time)
                        (format "%s - %s" (org-entry-get (point) "ITEM") time)))))
            "LEVEL=1")))
   "\n"))

The code makes entries that look like this:

# OrgMeetup (virtual) - <2025-06-11 Wed 12:00>--<2025-06-11 Wed 14:00>
00 11 11 06 * /home/sacha/bin/bbb-testing
00 18 11 06 * /home/sacha/bin/bbb-dormant

# Emacs Berlin (hybrid, in English) - <2025-06-25 Wed 12:30>--<2025-06-25 Wed 14:30>
30 11 25 06 * /home/sacha/bin/bbb-testing
30 18 25 06 * /home/sacha/bin/bbb-dormant

# Emacs APAC: Emacs APAC meetup (virtual) - <2025-06-28 Sat 04:30>--<2025-06-28 Sat 06:00>
30 03 28 06 * /home/sacha/bin/bbb-testing
30 10 28 06 * /home/sacha/bin/bbb-dormant

This works because meetups don't currently overlap. If there were, I'll need to tweak the code so that the server isn't downscaled in the middle of a meetup. It'll be a good problem to have.

I need to load the crontab entries by using crontab bbb.crontab. Again, I can tell Org Mode to run this on the xu4 home server. This time I use the :dir argument to specify the default directory, like this:

#+begin_src sh :dir "/ssh:xu4:~" :results silent
crontab bbb.crontab
#+end_src

Then cron can take care of things automatically, and I'll just get the e-mail notifications from Linode telling me that the server has been resized. This has already come in handy, like when I thought of Emacs APAC as being on Saturday, but it was actually on Friday my time.

I have another Emacs Lisp block that I use to retrieve all the info and update the list of meetups. I can add (goto-char (org-find-property "CUSTOM_ID" "crontab")) to find this section and use org-babel-execute-subtree to execute all the code blocks. That makes it an automatic part of my process for updating the Emacs Calendar and Emacs News. Here's the code that does the calendar part (Org source):

(defun my-prepare-calendar-for-export ()
  (interactive)
  (with-current-buffer (find-file-noselect "~/sync/emacs-calendar/README.org")
    (save-restriction
      (widen)
      (goto-char (point-min))
      (re-search-forward "#\\+NAME: event-summary")
      (org-ctrl-c-ctrl-c)
      (org-export-to-file 'html "README.html")
      ;; (unless my-laptop-p (my-schedule-announcements-for-upcoming-emacs-meetups))
      ;; update the crontab
      (goto-char (org-find-property "CUSTOM_ID" "crontab"))
      (org-babel-execute-subtree)
      (when my-laptop-p
        (org-babel-goto-named-result "event-summary")
        (re-search-forward "^- ")
        (goto-char (match-beginning 0))
        (let ((events (org-babel-read-result)))
          (oddmuse-edit "EmacsWiki" "Usergroups")
          (goto-char (point-min))
          (delete-region (progn (re-search-forward "== Upcoming events ==\n\n") (match-end 0))
                         (progn (re-search-forward "^$") (match-beginning 0)))
          (save-excursion (insert (mapconcat (lambda (s) (concat "* " s "\n")) events ""))))))))
(my-prepare-calendar-for-export)

I used a similar technique to generate the EmacsConf crontabs for automatically switching to the next talk. For that one, I used Emacs Lisp to write the files directly instead of using the :file header argument for Org Mode source blocks. That made it easier to loop over multiple files.

Hmm. Come to think of it, the technique of "go to a specific subtree and then execute it" is pretty powerful. In the past, I've found it handy to execute source blocks by name. Executing a subtree by custom ID is even more useful because I can easily mix source blocks in different languages or include other information. I think that's worth adding a my-org-execute-subtree-by-custom-id function to my Emacs configuration. Combined with an elisp: link, I can make links that execute functional blocks that might even be in different files. That could be a good starting point for a dashboard.

I love the way Emacs can easily work with files and scripts in different languages on different computers, and how it can help me with times and time zones too. This code should help me avoid brain hiccups and calendar mixups so that people can just enjoy getting together. Now I don't have to worry about whether I remembered to set up cron entries and if I did the math right for the times. We'll see how it holds up!

Back to top | E-mail me