summaryrefslogtreecommitdiffstats
path: root/contrib/vdir/calcurse-vdir.py
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/vdir/calcurse-vdir.py')
-rwxr-xr-xcontrib/vdir/calcurse-vdir.py186
1 files changed, 186 insertions, 0 deletions
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))