Emacs calendar

Upcoming events

  • Atelier Emacs Montpellier (in person) https://lebib.org/date/atelier-emacs Fri Apr 12 1800 Europe/Paris
  • M-x Research: TBA https://m-x-research.github.io/ Wed Apr 17 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1500 Etc/GMT - 1700 Europe/Berlin - 2030 Asia/Kolkata - 2300 Asia/Singapore
  • Emacs Berlin (virtual, in English) https://emacs-berlin.org/ Wed Apr 24 0930 America/Vancouver - 1130 America/Chicago - 1230 America/Toronto - 1630 Etc/GMT - 1830 Europe/Berlin - 2200 Asia/Kolkata – Thu Apr 25 0030 Asia/Singapore
  • Emacs APAC (virtual) https://emacs-apac.gitlab.io/ Sat Apr 27 0130 America/Vancouver - 0330 America/Chicago - 0430 America/Toronto - 0830 Etc/GMT - 1030 Europe/Berlin - 1400 Asia/Kolkata - 1630 Asia/Singapore
  • EmacsATX: Emacs Social https://www.meetup.com/emacsatx/events/299015530/ Wed May 1 1630 America/Vancouver - 1830 America/Chicago - 1930 America/Toronto - 2330 Etc/GMT – Thu May 2 0130 Europe/Berlin - 0500 Asia/Kolkata - 0730 Asia/Singapore
  • Emacs Paris: S: Emacs workshop in Paris (online) https://emacs-doctor.com/ Thu May 2 0830 America/Vancouver - 1030 America/Chicago - 1130 America/Toronto - 1530 Etc/GMT - 1730 Europe/Berlin - 2100 Asia/Kolkata - 2330 Asia/Singapore
  • M-x Research: TBA https://m-x-research.github.io/ Fri May 3 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1500 Etc/GMT - 1700 Europe/Berlin - 2030 Asia/Kolkata - 2300 Asia/Singapore
  • Emacs.si (in person): Emacs.si meetup #8 2024 (v #živo) https://dogodki.kompot.si/events/7350dff6-e688-4e84-8aad-4c734b4ed61f Thu May 9 2000 CET
  • Atelier Emacs Montpellier (in person) https://lebib.org/date/atelier-emacs Fri May 10 1800 Europe/Paris
  • M-x Research: TBA https://m-x-research.github.io/ Wed May 15 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1500 Etc/GMT - 1700 Europe/Berlin - 2030 Asia/Kolkata - 2300 Asia/Singapore


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:


Or you 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 yours! - mailto:sacha@sachachua.com)

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


Code I use to run it


  • 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'))
data = requests.get('https://www.meetup.com/emacs-sf/events/ical', cookies=cj)
import requests
import http.cookiejar as cookielib
import os
from urllib.request import urlopen
import icalendar
from datetime import date, datetime
from dateutil.relativedelta import *
import recurring_ical_events
import pytz
import re
import pypandoc
import subprocess
import sys
import csv
import requests


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

#                 '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',
                 'Stockholm': 'Stockholm-Emacs-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 Paris',
                 'source': 'https://emacs-doctor.com/emacs-paris-meetups.ics',
                {'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)
    elif e['DTSTART'].dt.tzinfo:
      tz = e['DTSTART'].dt.tzinfo
      times = [[e['DTSTART'].dt.astimezone(tz), tz.zone, e['DTSTART'].dt.astimezone(tz).utcoffset()]]
      s += '- see event for details'
    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]
         s += " - " + t[0].strftime('%H%M') + " " + t[1]
  if e['LOCATION']:
    return "- %s %s %s" % (e['SUMMARY'], e['LOCATION'], s)
    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 = date(date.today().year, date.today().month, 1)

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

for event in cal.walk():
  if event.name == 'VEVENT' and not '(ignore)' in event.name:
    event['UID'] = str(event['DTSTART'].dt.timestamp()) + '-' + 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)
  if event.name == 'VTIMEZONE' and event['TZID'] not in all_timezones:

def merge_cal(main_cal, name, url, start_date, end_date, info=None):
    if 'meetup.com' in url:
      data = requests.get(url, cookies=cj)
      data = requests.get(url)
    meetup_cal = icalendar.Calendar.from_ical(data.content)
  except Exception as e:
    print("Error with url: %s" % url)
  # Copy the timezone components
  for tz in meetup_cal.walk():
    if tz.name == 'VTIMEZONE' and tz['TZID'] not in all_timezones:
  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(' \(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'])

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)

  merge_meetup_events(cal, start_date, end_date)
  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)

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

events = recurring_ical_events.of(cal).between(start_date, end_date)
events.sort(key=lambda x: 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.utcnow()
highlight_end = datetime.utcnow() + relativedelta(weeks=+6)
all_end = datetime.utcnow() + relativedelta(weeks=+1)

for t in timezones:
  stub = "emacs-calendar-" + re.sub('^.*?/', '', t).lower()
  ical_args = ["ical2html", "-l", "-f", "Times are in " + t, "-z", t, 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')
  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')
  for e in events:
                     '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('\\\\', '')
  utc = datetime.utcfromtimestamp(e['DTSTART'].dt.timestamp())
  if utc >= highlight_start and utc <= highlight_end:
    if 'Mastering Emacs' in e['DESCRIPTION'] and utc > all_end:
      pass # do nothing
      print(summarized_event(e, timezones))
  for t in timezones:
    zone = pytz.timezone(t)
    start = e['DTSTART'].dt.astimezone(zone)
    end = e['DTEND'].dt.astimezone(zone)
    files[t].write("""* %s


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


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

Convert timezones

(defun my-summarize-times (time timezones)
  (let (prev-day)
     (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)
           (setq prev-day cur-day)
           (concat cur-day " " cur-time))))
     " / ")))

(defun my-org-summarize-event-in-timezones (timezones)
  (interactive (list (or timezones my-timezones)))
      (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)))
               (msg (format "%s - %s - %s"
                            (org-get-heading t t t t)
                            (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)
                (message "%s" msg)
                (kill-new msg))

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))
       (lambda ()
           (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))
    (if (interactive-p)
        (insert result)

Announcing Emacs events

(defun my-announce-on-irc (channels message host port)
    (insert "PASS " erc-password "\n"
            "USER " erc-nick "\n"
            "NICK " erc-nick "\n"
            (mapconcat (lambda (o)
                         (format "PRIVMSG %s :%s\n" o message))
                       channels "")
    (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 ()
  (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)))))))

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)))))))


Back to top | E-mail me