263 lines
9.9 KiB
EmacsLisp
263 lines
9.9 KiB
EmacsLisp
;;; ics2org.el -- convert icalendar to org
|
|
|
|
;; Copyright (C) 2010, 2011 Michael Markert
|
|
;; Author: Michael Markert <markert.michael@googlemail.com>
|
|
;; Created: 2010/12/29
|
|
;; Version: 0.3.1
|
|
;; Keywords: org, calendar
|
|
|
|
;; This file is NOT part of Emacs.
|
|
;;
|
|
;; This program is free software; you can redistribute it and/or
|
|
;; modify it under the terms of the GNU General Public License
|
|
;; as published by the Free Software Foundation; either version 2
|
|
;; of the License, or (at your option) any later version.
|
|
;;
|
|
;; This program is distributed in the hope that it will be useful,
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
;; GNU General Public License for more details.
|
|
;;
|
|
;; You should have received a copy of the GNU General Public License
|
|
;; along with this program; if not, write to the Free Software
|
|
;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
;; MA 02110-1301, USA.
|
|
|
|
;;; Commentary:
|
|
;;
|
|
;; Installation:
|
|
;;
|
|
;; (require 'ical2org)
|
|
;;
|
|
|
|
;;; Code:
|
|
|
|
(require 'icalendar)
|
|
(require 'org)
|
|
(eval-when-compile
|
|
(require 'cl))
|
|
|
|
(defconst ical2org/version "0.3.1")
|
|
|
|
(defgroup ical2org nil
|
|
"Convert iCalendar files to orgmode files."
|
|
:link '(url-link :tag "Homepage" "http://github.com/cofi/ical2org")
|
|
:group 'calendar
|
|
:prefix "ical2org/")
|
|
|
|
(defcustom ical2org/event-format
|
|
"* {SUMMARY} at {LOCATION} :{CATEGORY}:
|
|
{TIME}
|
|
{ORGANIZER}
|
|
{URL}
|
|
{DESCRIPTION}"
|
|
"String used to format an event.
|
|
Syntax is {FIELD} valid values for FIELD are: SUMMARY, LOCATION, TIME, URL,
|
|
DESCRIPTION, ORGANIZER, CATEGORY. Namely the slots of the `ical2org/event'
|
|
struct (capitalized)."
|
|
:type '(string))
|
|
|
|
(defcustom ical2org/category-separator ":"
|
|
"String used to separate multiple categories."
|
|
:type '(string))
|
|
|
|
(defcustom ical2org/completing-read #'ido-completing-read
|
|
"Function used for completing read.
|
|
Has to be compatible to `completing-read'."
|
|
:type '(function))
|
|
|
|
(defun ical2org/convert-file (fname outfile &optional nosave)
|
|
"Convert ical events from file `FNAME' to `OUTFILE' and save when `NOSAVE' is non-nil."
|
|
(interactive "fFile to convert: \nFSave as: \nP")
|
|
(let ((events
|
|
(with-temp-buffer
|
|
(insert-file-contents (expand-file-name fname))
|
|
(ical2org/import-buffer (current-buffer)))))
|
|
(save-current-buffer
|
|
(find-file outfile)
|
|
(goto-char (point-max))
|
|
(newline)
|
|
(dolist (e events)
|
|
(insert (ical2org/format e))
|
|
(newline))
|
|
(unless nosave
|
|
(save-buffer)))))
|
|
|
|
(defun ical2org/import-to-agenda (fname &optional nosave)
|
|
"Import ical events from file `FNAME' to agenda file (will be prompted).
|
|
Saves when `NOSAVE' is non-nil."
|
|
(interactive "fFile to import: \nP")
|
|
(let ((agenda-file (funcall ical2org/completing-read
|
|
"Agenda file: "
|
|
(org-agenda-files)))
|
|
(events
|
|
(with-temp-buffer
|
|
(insert-file-contents (expand-file-name fname))
|
|
(ical2org/import-buffer (current-buffer)))))
|
|
(save-current-buffer
|
|
(find-file agenda-file)
|
|
(goto-char (point-max))
|
|
(newline)
|
|
(dolist (e events)
|
|
(insert (ical2org/format e))
|
|
(newline))
|
|
(unless nosave
|
|
(save-buffer)))))
|
|
|
|
(defun ical2org/buffer-to-buffer (in out)
|
|
"Convert ical events from buffer `IN' to buffer `OUT'."
|
|
(interactive "bIn: \nBOut: ")
|
|
(save-current-buffer
|
|
(let ((events (ical2org/import-buffer in)))
|
|
(set-buffer (generate-new-buffer out))
|
|
(dolist (e events)
|
|
(insert (ical2org/format e))
|
|
(newline))
|
|
(set-window-buffer nil out)
|
|
(org-mode))))
|
|
|
|
;; private
|
|
|
|
;; output formatting
|
|
(defun ical2org/format (event)
|
|
"Replace formatstrings with slots of `EVENT'."
|
|
(replace-regexp-in-string "{.*?}"
|
|
(lambda (z)
|
|
(cdr (assoc z
|
|
`(("{SUMMARY}" . ,(ical2org/event-summary event))
|
|
("{LOCATION}" . ,(ical2org/event-location event))
|
|
("{TIME}" . ,(ical2org/event-org-timestr event))
|
|
("{URL}" . ,(ical2org/event-url event))
|
|
("{DESCRIPTION}" . ,(ical2org/event-description event))
|
|
("{ORGANIZER}" . ,(ical2org/event-organizer event))
|
|
("{CATEGORY}" . ,(mapconcat 'identity
|
|
(ical2org/event-category event)
|
|
ical2org/category-separator)))
|
|
)))
|
|
ical2org/event-format
|
|
t t))
|
|
|
|
(defun ical2org/org-recurrent (event start-decoded start-time end-time)
|
|
"Wrap `icalendar--convert-recurring-to-diary' diary in an org timestamp."
|
|
(format "<%s>"
|
|
(icalendar--convert-recurring-to-diary event start-decoded
|
|
start-time end-time)))
|
|
|
|
(defun ical2org/org-timestamp (start end)
|
|
"Format `START' and `END' as `org-time-stamp'."
|
|
(let ((start-time (nth 2 start))
|
|
(end-time (nth 2 end))
|
|
(start (car start))
|
|
(end (car end)))
|
|
(if end
|
|
(format "%s--%s" (ical2org/org-time-fmt start start-time)
|
|
(ical2org/org-time-fmt end end-time))
|
|
(if start
|
|
(ical2org/org-time-fmt start start-time)))))
|
|
|
|
(defun ical2org/org-time-fmt (time &optional with-hm)
|
|
"Format `TIME' as `org-time-stamp', if `WITH-HM' is non-nil included hh:mm.
|
|
`TIME' is an decoded time as returned from `decode-time'."
|
|
(let ((fmt (if with-hm
|
|
(cdr org-time-stamp-formats)
|
|
(car org-time-stamp-formats)))
|
|
(encoded-time (apply 'encode-time time)))
|
|
(format-time-string fmt encoded-time)))
|
|
|
|
;; entry processing
|
|
|
|
(defstruct ical2org/event
|
|
(summary "")
|
|
(location "")
|
|
(org-timestr "")
|
|
(url "")
|
|
(description "")
|
|
(organizer "")
|
|
(category '()))
|
|
|
|
(defun ics2org/datetime (property event zone-map)
|
|
"Return datetime values for `PROPERTY' of `EVENT' with `ZONE-MAP'.
|
|
Return a triple of (decoded isodate time).
|
|
Where `decoded' is a decoded datetime,
|
|
`isodate' a date as yy mm dd string,
|
|
`time' a time as hh:mm string."
|
|
(let* ((dt (icalendar--get-event-property event property))
|
|
(zone (icalendar--find-time-zone
|
|
(icalendar--get-event-property-attributes event property) zone-map))
|
|
(decoded (icalendar--decode-isodatetime dt nil zone-map)))
|
|
(list decoded
|
|
(icalendar--datetime-to-iso-date decoded)
|
|
(ignore-errors
|
|
(icalendar--datetime-to-colontime decoded)))))
|
|
|
|
(defun ical2org/get-property (event property &optional default clean)
|
|
"Return `PROPERTY' of `EVENT' or `DEFAULT'."
|
|
(let ((prop (or (icalendar--get-event-property event property)
|
|
default)))
|
|
(if clean
|
|
(icalendar--convert-string-for-import prop)
|
|
prop)))
|
|
|
|
(defun ical2org/get-org-timestr (event zone-map)
|
|
"Return org-timestring for `EVENT' with `ZONE-MAP'."
|
|
(let* ((start (ics2org/datetime 'DTSTART event zone-map))
|
|
(start-day (nth 1 start))
|
|
(start-time (nth 2 start))
|
|
(end (ics2org/datetime 'DTEND event zone-map))
|
|
(end-day (or (nth 1 end) start-day))
|
|
(end-time (or (nth 2 end) start-time))
|
|
(rrule (icalendar--get-event-property event 'RRULE))
|
|
(rdate (icalendar--get-event-property event 'RDATE))
|
|
(duration (icalendar--get-event-property event 'DURATION)))
|
|
(when duration
|
|
(let ((new-end (icalendar--add-decoded-times
|
|
(car start)
|
|
(icalendar--decode-isoduration duration))))
|
|
(setq end-day (icalendar--datetime-to-iso-date new-end))
|
|
(setq end-time (icalendar--datetime-to-colontime new-end))
|
|
(setq end (list new-end end-day end-time))))
|
|
|
|
(cond
|
|
(rrule (ical2org/org-recurrent event (car start) start-time end-time))
|
|
(t (ical2org/org-timestamp start end)))))
|
|
|
|
(defun ical2org/extract-event (ical-event zone-map)
|
|
"Extracts `ical2org/event' from `ICAL-EVENT' using the timezone map `ZONE-MAP'."
|
|
(let ((summary (ical2org/get-property ical-event 'SUMMARY "" t))
|
|
(location (ical2org/get-property ical-event 'LOCATION "" t))
|
|
(org-timestr (ical2org/get-org-timestr ical-event zone-map))
|
|
(url (ical2org/get-property ical-event 'URL ""))
|
|
(description (ical2org/get-property ical-event 'DESCRIPTION "" t))
|
|
(organizer (ical2org/get-property ical-event 'ORGANIZER "" t))
|
|
(category (split-string (ical2org/get-property ical-event 'CATEGORIES "" t)
|
|
"," t)))
|
|
(make-ical2org/event :summary summary
|
|
:location location
|
|
:org-timestr org-timestr
|
|
:url url
|
|
:description description
|
|
:organizer organizer
|
|
:category category)))
|
|
|
|
(defun ical2org/import-elements (ical-elements)
|
|
"Collects events from `ICAL-ELEMENTS' into a list of `ical2org/event's."
|
|
(let ((events (icalendar--all-events ical-elements))
|
|
(zone-map (icalendar--convert-all-timezones ical-elements)))
|
|
(loop for event in events
|
|
collect (ical2org/extract-event event zone-map))))
|
|
|
|
(defun ical2org/import-buffer (buffer)
|
|
"Return all events in icalendar `BUFFER' as `ical2org/event's."
|
|
(save-current-buffer
|
|
(set-buffer (icalendar--get-unfolded-buffer buffer))
|
|
(goto-char (point-min))
|
|
(if (re-search-forward "^BEGIN:VCALENDAR\\s-*$" nil t)
|
|
(progn
|
|
(beginning-of-line)
|
|
(ical2org/import-elements (icalendar--read-element nil nil)))
|
|
(message "Buffer does not contain icalendar contents!"))))
|
|
|
|
(provide 'ical2org)
|
|
|
|
;;; ical2org.el ends here
|