aboutsummaryrefslogblamecommitdiffstats
path: root/contrib/vdir/calcurse-vdir.py
blob: cf65fcb96dabf054abe404455b9e77f6bf03aeb2 (plain) (tree)

























































































































































































                                                                               
#!/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))