#+TITLE: khalel

=khalel=: Interacting through Emacs with locally-stored calendars via the
console application =khal= and syncing with remote CalDAV calendars using
=vdirsyncer=.

[[file:screenshot_agenda.png]]

=khalel= allows to access calendars stored in =.ics= files and provides means of
listing existing events, edit them as well as to create new ones largely through
an org-mode interface.

/Please note/: This code is in an early state. =khalel= shares limitations of
=khal= especially when it comes to calendar entry fields that are not handled at
the moment by either =khalel=, =khal= or org-mode's =.ics= exporter. Simple
scheduling of events works fine but do not expect features found in more complex
PIM applications such as invitees or even time zone support.

On the other hand, it is all text files, so hack away! :)

* Why =khal= and =khalel=
=khal= is a simple and fast way to read, create and edit calendar entries. It
does, however, have a limited feature set. If you need to deal with many shared
calendar invites and want to edit events with complicated repeat patterns, this
might not be the right tool. If you have a "simple" CalDAV calendar for your own
needs /or/ just want to see upcoming calendar events pop up in the org-mode
agenda in Emacs, then read ahead. Also check out =khal='s [[https://khal.readthedocs.io/en/latest/index.html#features][features and
limitations in its online documentation]] and the additional list of limitations
below.

Adjusting the output of =khal= slightly, one can quite easily get something that already resembles an
org-mode file (link to the online documentation: [[https://khal.readthedocs.io/en/latest/usage.html][khal usage]]):

#+begin_src bash :results output
khal list --format "* {title} \n <{start-date} {start-time}-{end-time}> \n {location} \n {description}" --day-format "" today 10d
#+end_src

#+RESULTS:
: * DnD mit den Toten Charaktären \n <2021-09-04 21:00-23:00> \n  \n
: * DHL \n <2021-09-09 13:00-16:00> \n  \n
: * Ge blod \n <2021-09-09 13:00-19:00> \n  \n
: * Rebeckas släkt \n <2021-09-11 16:00-19:00> \n  \n
: * Plocka 🍄 \n <2021-09-12 -> \n  \n
: * IcewindDale DnD \n <2021-09-12 16:00-19:00> \n  \n

This is showing the events in the coming 10 days from now. This gives us
something to work with and means we don't have to touch the =.ics= files
directly. =khalel= takes this a step further and imports a configurable range of
events into a nicely-formated org-mode file.

Furthermore, =khalel= provides a wrapper around =khal= and =org-mode= export
functionality to create new events via org-mode capture templates. Even editing
existing events is possible through a console-interface to =khal=.

* Use case

I sync my remote CalDAV calendars using [[https://github.com/pimutils/vdirsyncer][vdirsyncer]] which mirrors all events from
my different calendars locally. This calendar store can be accessed through
=khal= and =khalel=. I am mostly interested in seeing upcoming events in the
org-mode agenda and to quickly create new events with the familiar org-capture
mechanism.

This requires a small chain of steps: most notably, in my setup, events created
through =khalel= require an additional import step to be visible in org-mode.

#+begin_src ditaa :file sync_scheme.png
  +-------+    +----------+    +-------+    +-------+    +---------+
  |       |--->|          |--->|       |--->|       |--->|         |
  | remote|    |vdirsyncer|    |  khal |    |khalel |    | org mode|
  |       |<===|          |<===|       |===>|       |===>|         |
  +-------+    +----------+    +-------+    +-------+    +---------+
                                   ^
                                   :
                                   :
               +----------+    +--------+
               |          |    |        |       --> existing events
               | org mode |===>| khalel |
               |          |    |        |       ==> new events
               +----------+    +--------+

#+end_src

#+RESULTS:
[[file:sync_scheme.png]]

Of course, the synchronization and update of the imported org-file can be done
in one go! In fact, within the org-file, clickable links allow to either edit a
given event or sync & update all -- of course, without ever leaving Emacs!

** Features and limitations

Advantages with the approach:
- integrates neatly into my personal, Emacs-centric work-flow relying heavily on org-mode
- easy to customize with some elisp
- local calendar stores can easily be backed up (or even edited)
- synchronization can be done manually (when online) or automatized via cron job
  or from within Emacs

Disadvantages:
- several steps requiring different tools
- more initial set-up work
- currently, many features of =.ics=/CalDAV are not supported:
  - no timezones
  - no organizers/invitees
  - ...
- code is still evolving and features might change from one version to the next

Besides calendars, this setup can be extended to contacts using:
- [[https://github.com/pimutils/vdirsyncer][vdirsyncer]] (again) to mirror contacts from CardDAV server locally,
- [[https://github.com/scheibler/khard][khard]] to access them via the command line, and
- [[https://github.com/DamienCassou/khardel][khardel]] for integration into Emacs (not by me).
** Similar packages
- [[https://github.com/dengste/org-caldav][org-caldav]] :: Focuses on synchronizing Org mode files /to/ CalDAV servers where =khalel= mostly imports to and creates events from Org mode. Allows to use more Org mode features but some information might not be transferred to CalDAV. Employs synchronization code running in Emacs instead of using external tools. *Note*: /currently missing maintainer/, see https://github.com/dengste/org-caldav/issues/234
* Getting started
** Installing and configuring =vdirsyncer=
Follow the installation instructions at [[https://github.com/pimutils/vdirsyncer]].

The configuration is explained in detail in the [[http://vdirsyncer.pimutils.org/en/stable/config.html#][online manual]] including a
[[http://vdirsyncer.pimutils.org/en/stable/tutorial.html][tutorial and minimal example]].

When done with the configuration, run =vdirsyncer discover= to test the setup
and then =vdirsyncer sync= to run the synchronization for the first time.

** Installing and configuring =khal=

Simply download the package for your preferred distribution or [[https://khal.readthedocs.io/en/latest/install.html][follow the
installation instructions]]. The latter might be the preferred option, as you need
version =0.10.4= or later.

You can create a configuration interactively by running =khal configure= or
simply use the one below and save it to =~/.config/khal/config=:

#+begin_src conf
[calendars]

[[my_calendar_local]]
path = ~/.calendar/*
type = discover

[locale]
timeformat = %H:%M
dateformat = %Y-%m-%d
longdateformat = %Y-%m-%d %a
datetimeformat = %Y-%m-%d %H:%M
longdatetimeformat = %Y-%m-%d %H:%M
#+end_src

Make sure that the =longdateformat= includes the day of the week in short form
(=%a=) as this makes sure that org-mode recognizes the time stamps correctly
when importing. You can test the settings by running
#+begin_src bash :results output
khal printformats
#+end_src

#+RESULTS:
: longdatetimeformat: 2013-12-21 21:45
: datetimeformat: 2013-12-21 21:45
: longdateformat: 2013-12-21 lör
: dateformat: 2013-12-21
: timeformat: 21:45

The weekday's short form will appear in your configured local language.

You might want to set up a default calendar as well or do that in the =khalel= configuration step below.

** Install =khalel=
The package is available from MELPA: [[https://melpa.org/#/khalel][file:https://melpa.org/packages/khalel-badge.svg]]

Install it through =package-install=.

Alternatively, you can download the source code from [[https://gitlab.com/hperrey/khalel]]

To load the package, I recommend [[https://github.com/jwiegley/use-package][use-package]].
*** Doom Emacs
If you are using [[https://github.com/doomemacs/doomemacs][Doom Emacs]], you can install and load =khalel= by adding
#+begin_src emacs-lisp
(package! khalel)
#+end_src
to your =packages.el= and
#+begin_src emacs-lisp
(use-package! khalel
  :after org
  :config
  (khalel-add-capture-template))
#+end_src
to your =config.el=. Then execute =./doom sync= in the =~/.emacs.d/bin/=
directory to trigger the download of the package.

** Configuring =khalel=
First, make sure that the right =khal= and =vdirsyncer= executables will be used, e.g.
#+begin_src emacs-lisp
(setq khalel-khal-command "~/.local/bin/khal")
(setq khalel-vdirsyncer-command "vdirsyncer")
#+end_src

You might want to customize the values for capture template key and import file for khalel:
#+begin_src emacs-lisp
(setq khalel-capture-key "e")
(setq khalel-import-org-file (concat org-directory "/" "calendar.org"))
#+end_src

 =calendar.org= is also in my list of agenda files. There the new events will end up in after the next sync.

*Warning*: =calendar.org= is being overwritten on each import to avoid
 collecting duplicates inside the file! The default is therefore to set the file
 up in read-only mode. The confirmation prompt for overwriting the file can be
 disabled via:
#+begin_src emacs-lisp
(setq khalel-import-org-file-confirm-overwrite nil)
#+end_src

And I never plan too long into the future, so the next 30 days will be more than enough to fill my agenda view:
#+begin_src emacs-lisp
(setq khalel-import-end-date "+30d")
#+end_src
In the same manner, you can set a start date by changing the value of ~khalel-import-start-date~ which defaults to "today". Both variables accept the format supported by ~org-read-date~.

Using these settings, we can now set up a capture template using a helper routine:
#+begin_src emacs-lisp
(khalel-add-capture-template)
#+end_src
Put this call into your Emacs configuration file. The above command will also
register an export hook that is run when the capture is finalized to trigger the
export to =khal=.

** First steps
You can import current events matching the defined date range through
=khalel-import-events= or create new ones through =org-capture= and pressing =e=
(default key) for a new calendar event.

You might want to consider adding the org file with the imported events
(=calendar.org= in the above example) to your org agenda.

If you visit the org file with the imported events, you will notice links below
each event: using these (or by calling =khalel-edit-calendar-event=) you can
edit existing events through =khal= from within Emacs.

To synchronize new, edited or remote events use either the links in the imported
calendar org file or call =khalel-run-vdirsyncer=.
* Tips and tricks
** Creating repeating events
When capturing new events, you can create simple repeating patterns using the
org timestamp syntax with repeater intervals. For example,
#+begin_example
SCHEDULED: <2021-12-07 tis +1w>
#+end_example
sets the corresponding event to repeat every week. See section "Timestamps" in the org manual for more details.

For irregular repeating patterns, you can create several events with the same
basic information by adding further timestamps and ranges to the description
field of the capture template:
#+begin_example
,* example event
SCHEDULED: <2021-11-21 sön 13:27>--<2021-11-21 sön 19:22>
:PROPERTIES:
:CREATED: [2021-11-21 sön 13:27]
:CALENDAR:
:CATEGORY: event
:LOCATION:
:APPT_WARNTIME: 10
:ID:       99c11a2c-bdbd-4625-81b8-4d61729ce64f
:END:
repeats:
- <2021-11-22 mån 17:01-20:01>
- <2021-11-23 tis 19:00>--<2021-11-23 tis 21:21>
#+end_example

For each of the timestamps in the bottom, additional events (with unique IDs)
will be created through the ics export. Please not that the =SCHEDULED=
for the main event is expected to be always present, even when further events
are added as part of the description.

However, with the above approach, each event will carry the repeated time stamps
as part of their description, potentially leading to repeated entries in the Org
agenda view. A cleaner though more verbose approach is using sub-headings:

#+begin_example
,* lecture 1 in example course
SCHEDULED: <2021-11-22 Mon 08:15>--<2021-11-22 Mon 10:00>
:PROPERTIES:
:CREATED: [2021-11-21 sön 13:27]
:CALENDAR: teaching
:CATEGORY: event
:LOCATION: lecture hall A
:APPT_WARNTIME: 10
:ID:       99c11a2c-bdbd-4625-81b8-4d61729ce64f
:END:
,** lecture 2 in example course
SCHEDULED: <2021-11-23 Tue 08:15>--<2021-11-23 Tue 10:00>
,** lecture 3 in example course
SCHEDULED: <2021-11-24 Wed 10:15>--<2021-11-24 Wed 12:00>
:PROPERTIES:
:LOCATION: lecture hall D
:END:
#+end_example

Note that all sub-headings need an individual =SCHEDULED= entry but inherit the
properties of the top entry in the tree (here, for example, the =LOCATION= for
"lecture 2") if not overwritten explicitly. In the export, each sub-tree will be
created as separate calendar event and can even have its own repeat pattern.
** Default calendar
If you are usually only importing into a single calendar, then you can define this as a default calendar:
#+begin_src emacs-lisp
(setq khalel-default-calendar "privat")
#+end_src
This will replace any dialog asking for a calendar to save captures into.
** Limiting import of events from a single calendar
If you call =khal-import-events= with a prefix argument (e.g. =C-u=), the import will be limited to the default calendar defined in =khal-default-calendar=.
** Importing calendars into separate =org= files
If you have several calendars that you would like to import into separate =org= files, you can define your own import routines like this:
#+begin_src emacs-lisp
(defun hanno/import-current-work-events ()
  "Import only work events via `khalel-import-events`."
  (interactive)
  (let ((current-prefix-arg '(4))
    (khalel-default-calendar "work")
    (khalel-import-org-file (concat org-directory "work-events.org")))
      (call-interactively #'khalel-import-events)))
#+end_src
This limits the import to a single calendar =work= and stores it in the file
=work-events.org=. Consider to also modify =khalel-import-org-file-header= and
=khalel-import-format= to make them reflect your customization.

In this configuration, you might want to disable the automatic import when
performing a capture, synchronization or edit, by disabling
=khalel-import-events-after-capture=, =khalel-import-events-after-vdirsyncer=
and =khalel-import-events-after-khal-edit=, respectively.
** Customizing the =org= file that events are imported into
If you want to customize the khalel template, e.g. if you want the scheduled date to be a property, you can do this the following way:

#+begin_src emacs-lisp
(setq khalel-import-format "* {title} {cancelled}\n\
  :PROPERTIES:\n:CALENDAR: {calendar}\n\
  :LOCATION: {location}\n\
  :ID: {uid}\n\
  :END:\n\
  SCHEDULED: <{start-date-long} {start-time}>--<{end-date-long} {end-time}>\n\
  - Description: {description}\n\
  - URL: {url}\n- Organizer: {organizer}\n\n\
  [[elisp:(khalel-edit-calendar-event)][Edit this event]]\
      [[elisp:(progn (khalel-run-vdirsyncer) (khalel-import-events))]\
  [Sync and update all]]\n")
#+end_src

You can also change the variable =khalel-import-org-file-header= if you want to change the header of the generated file (e.g. if you want to add a file tag or something like that).
** Integration into mail readers for handling invitations :EXPERIMENTAL:
If you are using =gnus= (or =mu4e= with =gnu='s Article mode) to read mails in Emacs, then there is
now (rudimentary) support for handling iCalendar event invitations. Much of the
functionality is provided by =gnus-icalendar= which will show a summary of the
event and buttons to /accept/ or /decline/ the event. =khalel= taps into this to
add two buttons into any mail with an invitation: /Show Agenda/ and /khal
import/. The former simply shows the Org mode agenda for the day the event is
scheduled while the latter extracts the event and imports it via =khal import=.

- If you are using =mu4e=, you can find more information on the configuration here: https://www.djcbsoftware.nl/code/mu/mu4e/iCalendar.html
- For =gnus=, look into the documentation for =gnus-icalendar=.

After loading and setting up either of the above, the =khal= integration can be enabled by
#+begin_src emacs-lisp
(require 'khalel-icalendar)
#+end_src

* Troubleshooting
** Getting warning message =Ignoring unsafe file local variable: buffer-read-only= when running =khalel-import-events=
The =calendar.org= file in which the current events are imported into, is set
to =read-only-mode= as any changes to this file would be overwritten by the next
import. This is done via so-called "file local variables" which, by default, are
ignored by Emacs until they are marked "safe" by the user.

To mark this particular variable as safe, set the variable
=safe-local-variable-values= in your Emacs configuration, e.g.:
#+begin_src emacs-lisp
(setq safe-local-variable-values
   (quote
    ((buffer-read-only . 1))))
#+end_src
** The file with imported events is empty/contains no scheduled items after running =khal-import-events=
This can have a number of reasons. First check your =khal= installation by running in the terminal:
#+begin_src sh
khal list today 30d
#+end_src

If you get an error message or empty output, please double-check your =khal=
configuration and make sure that you have events scheduled during the next
month.

In case you do get output from the above command but the file =khalel= imports
into is still empty, please check your =*Messages*= buffer for error messages
and continue in the corresponding section.

Also check that you have set ~khalel-import-start-date~ and
~khalel-import-end-date~ to appropriate values.

** =Searching for program: No such file or directory, khal=
This indicates that =khal= could not be found. Run
#+begin_src sh
which khal
#+end_src
and then adjust the variable =khalel-khal-command= to match this path.

** Error message =khal exited with non-zero exit code; see buffer ‘*khal-errors*’ for details.=
As stated in the error message, open the =*khal-errors*= buffer to see the exact
cause of the error.

A likely reason for this error is =khalel= relying on VCALENDAR fields not
supported in the installed =khal= version (e.g. =khal= reporting =critical:
'url'=). Double-check that your version matches the required one:
#+begin_src sh
khal --version
#+end_src

See the above section on =khal= installation for the version requirements.

Should a later version than the above mentioned cause any errors, then please
report this problem and include the version of =khal= and the contents of the
=*khal-errors*= buffer.
** =khal= gives =PytzUsageWarning= messages
This might be a [[https://github.com/pimutils/khal/issues/1092][known issue (#1092)]] in =khal=. One approach is to downgrade to
an earlier version of =tzlocal=:
#+begin_src sh
sudo pip install tzlocal==2
#+end_src

Other options are discussed in the linked issue tracker in case this downgrade
is not suitable for you.

* Reporting issues
Please open an issue on [[https://gitlab.com/hperrey/khalel/-/issues][gitlab]] (preferred) or write an [[mailto:hanno@hoowl.se][email]].
