aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorvxid <maxime.treca@gmail.com>2019-03-02 16:12:15 +0100
committerLukas Fleischer <lfleischer@calcurse.org>2019-03-14 21:21:46 +0100
commitd26164fb725e22f8eebd8d94ad59cab3921b8a6f (patch)
treeb647b4588e2e71bf2260bc18eceeb2a0704c1d0f
parent5eff08777b643c0e112921eb6cb78381361acb68 (diff)
downloadcalcurse-d26164fb725e22f8eebd8d94ad59cab3921b8a6f.tar.gz
calcurse-d26164fb725e22f8eebd8d94ad59cab3921b8a6f.zip
Add support for vdir synchronization
Add a script to synchronize calcurse with a VDIR collection. Add a wrapper script around vdirsyncer to automatically synchronize calcurse data to a vdirsyncer collection. Add script documentation and Makefile. Signed-off-by: Lukas Fleischer <lfleischer@calcurse.org>
-rw-r--r--Makefile.am2
-rw-r--r--configure.ac5
-rw-r--r--contrib/vdir/Makefile.am13
-rw-r--r--contrib/vdir/README.md58
-rwxr-xr-xcontrib/vdir/calcurse-vdir.py186
-rwxr-xr-xcontrib/vdir/calcurse-vdirsyncer71
6 files changed, 332 insertions, 3 deletions
diff --git a/Makefile.am b/Makefile.am
index fb428c1..e470777 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -2,7 +2,7 @@ AUTOMAKE_OPTIONS= foreign
ACLOCAL_AMFLAGS = -I m4
-SUBDIRS = po src test scripts contrib/caldav
+SUBDIRS = po src test scripts contrib/caldav contrib/vdir
if ENABLE_DOCS
SUBDIRS += doc
diff --git a/configure.ac b/configure.ac
index c7db268..1f6ec61 100644
--- a/configure.ac
+++ b/configure.ac
@@ -152,8 +152,9 @@ AM_CONDITIONAL(CALCURSE_MEMORY_DEBUG, test x$memdebug = xyes)
#-------------------------------------------------------------------------------
# Create Makefiles
#-------------------------------------------------------------------------------
-AC_OUTPUT(Makefile doc/Makefile src/Makefile test/Makefile scripts/Makefile \
- po/Makefile.in po/Makefile contrib/caldav/Makefile)
+AC_OUTPUT(Makefile doc/Makefile src/Makefile test/Makefile \
+ scripts/Makefile po/Makefile.in po/Makefile \
+ contrib/caldav/Makefile contrib/vdir/Makefile)
#-------------------------------------------------------------------------------
# Summary
#-------------------------------------------------------------------------------
diff --git a/contrib/vdir/Makefile.am b/contrib/vdir/Makefile.am
new file mode 100644
index 0000000..2bb7f0b
--- /dev/null
+++ b/contrib/vdir/Makefile.am
@@ -0,0 +1,13 @@
+AUTOMAKE_OPTIONS = foreign
+
+dist_bin_SCRIPTS = \
+ calcurse-vdir
+
+EXTRA_DIST = \
+ calcurse-vdir.py
+
+CLEANFILES = \
+ calcurse-vdir
+
+calcurse-vdir: calcurse-vdir.py
+ cp "$(srcdir)/$<" "$@"
diff --git a/contrib/vdir/README.md b/contrib/vdir/README.md
new file mode 100644
index 0000000..b040d04
--- /dev/null
+++ b/contrib/vdir/README.md
@@ -0,0 +1,58 @@
+calcurse-vdir
+===============
+
+calcurse-vdir is a Python script designed to export and import data to and
+from directories following the
+[vdir](http://vdirsyncer.pimutils.org/en/stable/vdir.html) storage format.
+This data can then be synced with various remotes using tools like
+[vdirsyncer](https://github.com/pimutils/vdirsyncer).
+Please note that the script is alpha software! This means that:
+
+* We are eagerly looking for testers to run the script and give feedback! If
+ you find any bugs, please report them to the calcurse mailing lists or to the
+ GitHub bug tracker. If the script works fine for you, please report back as
+ well!
+
+* The script might still have bugs. MAKE BACKUPS, especially before running
+ calcurse-vdir with the `-f` flag!
+
+Usage
+-----
+
+calcurse-vdir requires an up-to-date version of calcurse and python.
+To run calcurse-vdir, call the script using
+
+```sh
+calcurse-vdir <action> <vdir>
+```
+
+where `action` is either `import` or `export` and where `vdir` is the local
+directory to interact with.
+
+When importing events, calcurse-vdir imports every event found in the vdir
+directory that is not also present in calcurse. When exporting events,
+calcurse-vdir does the opposite and writes any new event to the vdir directory.
+
+These operations are non-destructive by default, meaning that no event will be
+deleted by the script. The `-f` flag can be used to make the origin mirror the
+destination, potentially deleting events in the destination that are no longer
+present in the origin.
+
+You can optionally specify an alternative directory for local calcurse data
+using the `-D` flag if it differs from the default `~/.calcurse`.
+
+Integration with vdirsyncer
+---------------------------
+
+A vdirsyncer synchronisation script `calcurse-vdirsyncer` is can be found in
+the `contrib` directory. This script wraps event export, vdirsyncer
+synchronization and imports in a single call. Run `calcurse-vdirsyncer -h` for
+more information.
+
+Planned Updates
+---------------
+
+* Support for hook directories
+* Enable filtering of imported and exported items (events, todos)
+* Improve event parsing robustness
+* Add testing support
diff --git a/contrib/vdir/calcurse-vdir.py b/contrib/vdir/calcurse-vdir.py
new file mode 100755
index 0000000..cf65fcb
--- /dev/null
+++ b/contrib/vdir/calcurse-vdir.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+
+import argparse
+import io
+import os
+import re
+import subprocess
+import sys
+import textwrap
+
+
+def msgfmt(msg, prefix=''):
+ """Format a message"""
+ lines = []
+ for line in msg.splitlines():
+ lines += textwrap.wrap(line, 80 - len(prefix))
+ return '\n'.join([prefix + line for line in lines])
+
+
+def log(msg):
+ """Print a formatted message"""
+ print(msgfmt(msg))
+
+
+def die(msg):
+ """Exit on error"""
+ sys.exit(msgfmt(msg, prefix="error: "))
+
+
+def check_binary(binary):
+ """Check if a binary is available in $PATH"""
+ try:
+ subprocess.call([binary, '--version'], stdout=subprocess.DEVNULL)
+ except FileNotFoundError:
+ die("{0} is not available.".format(binary))
+
+
+def check_directory(directory):
+ """Check if a directory exists"""
+ if not os.path.isdir(directory):
+ die("invalid directory: {0}".format(directory))
+
+
+def file_to_uid(file):
+ """Return the uid of an ical file"""
+ uid = file.replace(vdir, "").replace(".ics", "")
+ return uid
+
+
+def write_file(file, contents):
+ """Write to file"""
+ if verbose:
+ log("Writing event {0}".format(file_to_uid(file)))
+ with open(file, 'w') as f:
+ f.write(contents)
+
+
+def remove_file(file):
+ """Remove file"""
+ if verbose:
+ log("Deleting event {0}".format(file_to_uid(file)))
+ if os.path.isfile(file):
+ os.remove(file)
+
+
+def calcurse_export():
+ """Return raw calcurse data"""
+ command = calcurse + ['-xical', '--export-uid']
+ proc = subprocess.Popen(command, stdout=subprocess.PIPE)
+ return [x for x in io.TextIOWrapper(proc.stdout, encoding="utf-8")]
+
+
+def calcurse_remove(uid):
+ """Remove calcurse event by uid"""
+ if verbose:
+ log("Removing event {0} from calcurse".format(uid))
+ command = calcurse + ['-P', '--filter-hash=' + uid]
+ subprocess.call(command)
+
+
+def calcurse_import(file):
+ """Import ics file to calcurse"""
+ if verbose:
+ log("Importing event {0} to calcurse".format(file_to_uid(file)))
+ command = calcurse + ['-i', file]
+ subprocess.call(command, stdout=subprocess.DEVNULL)
+
+
+def calcurse_list():
+ """Return all calcurse item uids"""
+ command = calcurse + [
+ '-G',
+ '--format-apt=%(hash)\\n',
+ '--format-recur-apt=%(hash)\\n',
+ '--format-event=%(hash)\\n',
+ '--format-recur-event=%(hash)\\n',
+ '--format-todo=%(hash)\\n'
+ ]
+ proc = subprocess.Popen(command, stdout=subprocess.PIPE)
+ return [x.strip() for x in io.TextIOWrapper(proc.stdout, encoding="utf-8")]
+
+
+def parse_calcurse_data(raw):
+ """Parse raw calcurse data to a uid/ical dictionary"""
+
+ header = ''.join(raw[:3])
+ regex = '(BEGIN:(VEVENT|VTODO).*?END:(VEVENT|VTODO).)'
+ events = [x[0] for x in re.findall(regex, ''.join(raw), re.DOTALL)]
+
+ items = {}
+
+ for item in events:
+ uid = re.findall('UID:(.*?)\n', item)[0]
+ items[uid] = header + item + "END:VCALENDAR\n"
+
+ return items
+
+
+def calcurse_to_vdir():
+ """Export calcurse data to vdir"""
+ raw_events = calcurse_export()
+ events = parse_calcurse_data(raw_events)
+
+ files_vdir = [x for x in os.listdir(vdir)]
+ files_calc = [uid + ".ics" for uid in events]
+
+ if force:
+ for file in [f for f in files_vdir if f not in files_calc]:
+ remove_file(os.path.join(vdir, file))
+
+ for uid, event in events.items():
+ file = uid + ".ics"
+ if file not in files_vdir:
+ write_file(os.path.join(vdir, file), event)
+
+
+def vdir_to_calcurse():
+ """Import vdir data to calcurse"""
+ files_calc = [x + '.ics' for x in calcurse_list()]
+ files_vdir = [x for x in os.listdir(vdir) if x.endswith('.ics')]
+
+ for file in [f for f in files_vdir if f not in files_calc]:
+ calcurse_import(os.path.join(vdir, file))
+
+ if force:
+ for file in [f for f in files_calc if f not in files_vdir]:
+ calcurse_remove(file[:-4])
+
+
+parser = argparse.ArgumentParser('calcurse-vdir')
+parser.add_argument('action', choices=['import', 'export'],
+ help='export or import calcurse data')
+parser.add_argument('vdir',
+ help='path to the vdir collection directory')
+parser.add_argument('-D', '--datadir', action='store', dest='datadir',
+ default=None,
+ help='path to the calcurse data directory')
+parser.add_argument('-f', '--force', action='store_true', dest='force',
+ default=False,
+ help='enable destructive import and export')
+parser.add_argument('-v', '--verbose', action='store_true', dest='verbose',
+ default=False,
+ help='print status messages to stdout')
+args = parser.parse_args()
+
+action = args.action
+datadir = args.datadir
+force = args.force
+verbose = args.verbose
+vdir = args.vdir
+
+check_directory(vdir)
+
+check_binary('calcurse')
+calcurse = ['calcurse']
+
+if datadir:
+ check_directory(datadir)
+ calcurse += ['-D', datadir]
+
+if action == 'import':
+ vdir_to_calcurse()
+elif action == 'export':
+ calcurse_to_vdir()
+else:
+ die("Invalid action {0}.".format(action))
diff --git a/contrib/vdir/calcurse-vdirsyncer b/contrib/vdir/calcurse-vdirsyncer
new file mode 100755
index 0000000..c5371b5
--- /dev/null
+++ b/contrib/vdir/calcurse-vdirsyncer
@@ -0,0 +1,71 @@
+#!/bin/sh
+
+set -e
+
+usage() {
+ echo "usage: calcurse-vdirsyncer vdir [-h] [-f] [-v] [-D] datadir"
+ exit
+}
+
+set_vdir() {
+ if [ ! -d "$1" ]; then
+ echo "error: $1 is not a valid vdir directory."
+ exit 1
+ else
+ VDIR="$1"
+ fi
+}
+
+set_datadir() {
+ if [ -z "$1" ]; then
+ echo "error: no datadir specified."
+ usage
+ fi
+ if [ ! -d "$1" ]; then
+ echo "error: $1 is not a valid data directory."
+ exit 1
+ else
+ DATADIR="$1"
+ shift
+ fi
+}
+
+if [ "$#" -lt 1 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
+ usage
+fi
+
+DATADIR="$HOME/.calcurse"
+VERBOSE=""
+FORCE=""
+
+set_vdir "$1"
+shift
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ -D|--datadir)
+ shift
+ set_datadir "$1"
+ shift
+ ;;
+ -h|--help)
+ usage
+ ;;
+ -f|--force)
+ FORCE="-f"
+ shift
+ ;;
+ -v|--verbose)
+ VERBOSE="-v"
+ shift
+ ;;
+ *)
+ echo "error: invalid argument $1"
+ usage
+ ;;
+ esac
+done
+
+calcurse-vdir export "$VDIR" -D "$DATADIR" "$FORCE" "$VERBOSE" && \
+ vdirsyncer sync && \
+ calcurse-vdir import "$VDIR" -D "$DATADIR" "$FORCE" "$VERBOSE"