Emacs calendar
Upcoming events
- M-x Research: TBA https://m-x-research.github.io/ Wed Jun 18 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1500 Etc/GMT - 1700 Europe/Berlin - 2030 Asia/Kolkata - 2300 Asia/Singapore
- Emacs Berlin (hybrid, in English) https://emacs-berlin.org/ Wed Jun 25 0930 America/Vancouver - 1130 America/Chicago - 1230 America/Toronto - 1630 Etc/GMT - 1830 Europe/Berlin - 2200 Asia/Kolkata – Thu Jun 26 0030 Asia/Singapore
- Emacs APAC: Emacs APAC meetup (virtual) https://emacs-apac.gitlab.io/announcements/ Sat Jun 28 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/307923518/ Wed Jul 2 1600 America/Vancouver - 1800 America/Chicago - 1900 America/Toronto - 2300 Etc/GMT – Thu Jul 3 0100 Europe/Berlin - 0430 Asia/Kolkata - 0700 Asia/Singapore
- M-x Research: TBA https://m-x-research.github.io/ Fri Jul 4 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 #7 2025 (v #živo) https://dogodki.kompot.si/events/0fb2dc00-8cea-4365-a6ca-ab1a3e76f0ee Tue Jul 8 1900 CET
- OrgMeetup (virtual) https://orgmode.org/worg/orgmeetup.html Wed Jul 9 0900 America/Vancouver - 1100 America/Chicago - 1200 America/Toronto - 1600 Etc/GMT - 1800 Europe/Berlin - 2130 Asia/Kolkata – Thu Jul 10 0000 Asia/Singapore
- Atelier Emacs Montpellier (in person) https://lebib.org/date/atelier-emacs Fri Jul 11 1800 Europe/Paris
- M-x Research: TBA https://m-x-research.github.io/ Wed Jul 16 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1500 Etc/GMT - 1700 Europe/Berlin - 2030 Asia/Kolkata - 2300 Asia/Singapore
- Emacs APAC: Emacs APAC meetup (virtual) https://emacs-apac.gitlab.io/announcements/ Sat Jul 26 0130 America/Vancouver - 0330 America/Chicago - 0430 America/Toronto - 0830 Etc/GMT - 1030 Europe/Berlin - 1400 Asia/Kolkata - 1630 Asia/Singapore
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:
- emacs-calendar-berlin.org
- emacs-calendar-chicago.org
- emacs-calendar-gmt+1.org
- emacs-calendar-gmt+10.org
- emacs-calendar-gmt+11.org
- emacs-calendar-gmt+12.org
- emacs-calendar-gmt+2.org
- emacs-calendar-gmt+3.org
- emacs-calendar-gmt+4.org
- emacs-calendar-gmt+5.org
- emacs-calendar-gmt+6.org
- emacs-calendar-gmt+7.org
- emacs-calendar-gmt+8.org
- emacs-calendar-gmt+9.org
- emacs-calendar-gmt-0.org
- emacs-calendar-gmt-1.org
- emacs-calendar-gmt-10.org
- emacs-calendar-gmt-11.org
- emacs-calendar-gmt-12.org
- emacs-calendar-gmt-13.org
- emacs-calendar-gmt-2.org
- emacs-calendar-gmt-3.org
- emacs-calendar-gmt-4.org
- emacs-calendar-gmt-5.org
- emacs-calendar-gmt-6.org
- emacs-calendar-gmt-7.org
- emacs-calendar-gmt-8.org
- emacs-calendar-gmt-9.org
- emacs-calendar-gmt.org
- emacs-calendar-kolkata.org
- emacs-calendar-shanghai.org
- emacs-calendar-singapore.org
- emacs-calendar-toronto.org
- emacs-calendar-vancouver.org
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)
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:
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.*?)( |<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()
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.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('\\\\', '')
utc = datetime.datetime.fromtimestamp(e['DTSTART'].dt.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)
start = e['DTSTART'].dt.astimezone(zone)
end = e['DTEND'].dt.astimezone(zone)
files[t].write("""* %s
:PROPERTIES:
:LOCATION: %s
:END:
<%s>--<%s>
%s
""" % (e['SUMMARY'], e['LOCATION'], start.strftime(org_date), end.strftime(org_date), desc))
- Emacs Berlin (virtual, in English) https://emacs-berlin.org/ Wed Dec 25 0930 America/Vancouver - 1130 America/Chicago - 1230 America/Toronto - 1730 Etc/GMT - 1830 Europe/Berlin - 2300 Asia/Kolkata – Thu Dec 26 0130 Asia/Singapore
- Emacs APAC (virtual) https://emacs-apac.gitlab.io/ Sat Dec 28 0030 America/Vancouver - 0230 America/Chicago - 0330 America/Toronto - 0830 Etc/GMT - 0930 Europe/Berlin - 1400 Asia/Kolkata - 1630 Asia/Singapore
- M-x Research: TBA https://m-x-research.github.io/ Fri Jan 3 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1600 Etc/GMT - 1700 Europe/Berlin - 2130 Asia/Kolkata – Sat Jan 4 0000 Asia/Singapore
- Atelier Emacs Montpellier (in person) https://lebib.org/date/atelier-emacs Fri Jan 10 1800 Europe/Paris
- M-x Research: TBA https://m-x-research.github.io/ Wed Jan 15 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1600 Etc/GMT - 1700 Europe/Berlin - 2130 Asia/Kolkata – Thu Jan 16 0000 Asia/Singapore
- Emacs APAC (virtual) https://emacs-apac.gitlab.io/ Sat Jan 25 0030 America/Vancouver - 0230 America/Chicago - 0330 America/Toronto - 0830 Etc/GMT - 0930 Europe/Berlin - 1400 Asia/Kolkata - 1630 Asia/Singapore
- Emacs Berlin (virtual, in English) https://emacs-berlin.org/ Wed Jan 29 0930 America/Vancouver - 1130 America/Chicago - 1230 America/Toronto - 1730 Etc/GMT - 1830 Europe/Berlin - 2300 Asia/Kolkata – Thu Jan 30 0130 Asia/Singapore
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)))
(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)
(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