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>
This commit is contained in:
parent
5eff08777b
commit
d26164fb72
@ -2,7 +2,7 @@ AUTOMAKE_OPTIONS= foreign
|
|||||||
|
|
||||||
ACLOCAL_AMFLAGS = -I m4
|
ACLOCAL_AMFLAGS = -I m4
|
||||||
|
|
||||||
SUBDIRS = po src test scripts contrib/caldav
|
SUBDIRS = po src test scripts contrib/caldav contrib/vdir
|
||||||
|
|
||||||
if ENABLE_DOCS
|
if ENABLE_DOCS
|
||||||
SUBDIRS += doc
|
SUBDIRS += doc
|
||||||
|
@ -152,8 +152,9 @@ AM_CONDITIONAL(CALCURSE_MEMORY_DEBUG, test x$memdebug = xyes)
|
|||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# Create Makefiles
|
# Create Makefiles
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
AC_OUTPUT(Makefile doc/Makefile src/Makefile test/Makefile scripts/Makefile \
|
AC_OUTPUT(Makefile doc/Makefile src/Makefile test/Makefile \
|
||||||
po/Makefile.in po/Makefile contrib/caldav/Makefile)
|
scripts/Makefile po/Makefile.in po/Makefile \
|
||||||
|
contrib/caldav/Makefile contrib/vdir/Makefile)
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# Summary
|
# Summary
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
|
13
contrib/vdir/Makefile.am
Normal file
13
contrib/vdir/Makefile.am
Normal file
@ -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)/$<" "$@"
|
58
contrib/vdir/README.md
Normal file
58
contrib/vdir/README.md
Normal file
@ -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
|
186
contrib/vdir/calcurse-vdir.py
Executable file
186
contrib/vdir/calcurse-vdir.py
Executable file
@ -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))
|
71
contrib/vdir/calcurse-vdirsyncer
Executable file
71
contrib/vdir/calcurse-vdirsyncer
Executable file
@ -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"
|
Loading…
x
Reference in New Issue
Block a user