diff options
Diffstat (limited to 'contrib')
-rw-r--r-- | contrib/caldav/README.md | 41 | ||||
-rwxr-xr-x | contrib/caldav/calcurse-caldav.py | 273 | ||||
-rw-r--r-- | contrib/caldav/config.sample | 23 | ||||
-rwxr-xr-x | contrib/caldav/hooks/post-sync | 34 | ||||
-rwxr-xr-x | contrib/hooks/post-save | 38 | ||||
-rwxr-xr-x | contrib/hooks/pre-load | 10 | ||||
-rw-r--r-- | contrib/import/DST-et.ical | 11 | ||||
-rw-r--r-- | contrib/import/DST-eu.ical | 11 | ||||
-rw-r--r-- | contrib/import/February.ical | 11 | ||||
-rw-r--r-- | contrib/import/Monday-first-last.ical | 11 | ||||
-rw-r--r-- | contrib/import/Tuesday-Thursday.ical | 11 | ||||
-rw-r--r-- | contrib/import/Wednesdays-all.ical | 11 | ||||
-rw-r--r-- | contrib/setup.cfg | 3 | ||||
-rw-r--r-- | contrib/vdir/README.md | 3 | ||||
-rwxr-xr-x | contrib/vdir/calcurse-vdirsyncer | 2 |
15 files changed, 328 insertions, 165 deletions
diff --git a/contrib/caldav/README.md b/contrib/caldav/README.md index b6f6cbf..b464dc9 100644 --- a/contrib/caldav/README.md +++ b/contrib/caldav/README.md @@ -17,10 +17,12 @@ Usage ----- calcurse-caldav requires an up-to-date version of calcurse and a configuration -file located at ~/.calcurse/caldav/config. An example configuration file can be -found under contrib/caldav/config.sample in the calcurse source tree. You will -also need to install *httplib2* for Python 3 using *pip* (e.g. `pip3 install ---user httplib2`) or your distribution's package manager. +file located at $XDG_CONFIG_HOME/calcurse/caldav/config +(~/.local/share/calcurse/caldav/config) or ~/.calcurse/caldav/config if +~/.calcurse exists. An example configuration file can be found under +contrib/caldav/config.sample in the calcurse source tree. You will also need to +install *httplib2* for Python 3 using *pip* (e.g. `pip3 install --user +httplib2`) or your distribution's package manager. If you run calcurse-caldav for the first time, you need to provide the `--init` argument. You can choose between the following initialization modes: @@ -32,20 +34,21 @@ argument. You can choose between the following initialization modes: For subsequent calcurse-caldav invocations, you don't need to specify any additional parameters. -You can specify a username and password for basic authentication in the -config file. Alternatively, the password can be passed securely from another -program (such as *pass*) via the `CALCURSE_CALDAV_PASSWORD` environment variable like -so: -``` -CALCURSE_CALDAV_PASSWORD=$(pass show calcurse) calcurse-caldav -``` +Specify your HTTP Basic authentication credentials under the config file's +`Auth` section. The most secure approach is to save your password in a CLI +encrypted password store (_e.g.,_ [pass](https://www.passwordstore.org/)), and +then set `PasswordCommand` to the shell command used to retrieve it. +If security is not a priority, you may store your password in plain text +instead. Hooks ----- -You can place scripts in `$HOME/.calcurse/caldav/hooks/` to trigger actions at -certain events. To enable a hook, add a script with one of the following names -to this directory. Also make sure the scripts are executable. +You can place scripts in `$XDG_CONFIG_HOME/calcurse/caldav/hooks/` +(`~/.config/calcurse/caldav/hooks`) or `~/.calcurse/caldav/hooks` if +`~/.calcurse` exists in order to trigger actions at certain events. To enable a +hook, add a script with one of the following names to this directory. Also make +sure the scripts are executable. *pre-sync*:: Executed before the data files are synchronized. @@ -59,10 +62,12 @@ How It Works ------------ calcurse-caldav creates a so-called synchronization database at -`~/.calcurse/caldav/sync.db` that always keeps a snapshot of the last time the -script was executed. When running the script, it compares the objects on the -server and the local objects with that snapshot to identify items that were -added or deleted. It then +`$XDG_DATA_HOME/calcurse/caldav/sync.db` +(`~/.local/share/calcurse/caldav/sync.db`) or `~/.calcurse/caldav/sync.db` if +`~/.calcurse` exists that always keeps a snapshot of the last time the script +was executed. When running the script, it compares the objects on the server +and the local objects with that snapshot to identify items that were added or +deleted. It then * downloads new objects from the server and imports them into calcurse, * deletes local objects that no longer exist on the server, diff --git a/contrib/caldav/calcurse-caldav.py b/contrib/caldav/calcurse-caldav.py index d247f80..f9488e6 100755 --- a/contrib/caldav/calcurse-caldav.py +++ b/contrib/caldav/calcurse-caldav.py @@ -3,24 +3,92 @@ import argparse import base64 import configparser -import httplib2 import os +import pathlib import re +import shlex import subprocess import sys import textwrap -import urllib.parse import xml.etree.ElementTree as etree +import httplib2 + # Optional libraries for OAuth2 authentication try: - from oauth2client.client import OAuth2WebServerFlow, HttpAccessTokenRefreshError - from oauth2client.file import Storage import webbrowser + + from oauth2client.client import HttpAccessTokenRefreshError, OAuth2WebServerFlow + from oauth2client.file import Storage except ModuleNotFoundError: pass +class Config: + _map = {} + + def __init__(self, fn): + self._map = { + 'Auth': { + 'Password': None, + 'PasswordCommand': None, + 'Username': None, + }, + 'CustomHeaders': {}, + 'General': { + 'AuthMethod': 'basic', + 'Binary': 'calcurse', + 'Debug': False, + 'DryRun': True, + 'HTTPS': True, + 'Hostname': None, + 'InsecureSSL': False, + 'Path': None, + 'SyncFilter': 'cal,todo', + 'Verbose': False, + }, + 'OAuth2': { + 'ClientID': None, + 'ClientSecret': None, + 'RedirectURI': 'http://127.0.0.1', + 'Scope': None, + }, + } + + config = configparser.RawConfigParser() + config.optionxform = str + if verbose: + print('Loading configuration from ' + configfn + '...') + try: + config.read_file(open(fn)) + except FileNotFoundError: + die('Configuration file not found: {}'.format(fn)) + + for sec in config.sections(): + if sec not in self._map: + die('Unexpected config section: {}'.format(sec)) + + if not self._map[sec]: + # Import section with custom key-value pairs. + self._map[sec] = dict(config.items(sec)) + continue + + # Import section with predefined keys. + for key, val in config.items(sec): + if key not in self._map[sec]: + die('Unexpected config key in section {}: {}'.format(sec, key)) + if type(self._map[sec][key]) == bool: + self._map[sec][key] = config.getboolean(sec, key) + else: + self._map[sec][key] = val + + def section(self, section): + return self._map[section] + + def get(self, section, key): + return self._map[section][key] + + def msgfmt(msg, prefix=''): lines = [] for line in msg.splitlines(): @@ -37,8 +105,10 @@ def die(msg): def check_dir(dir): - if not os.path.isdir(dir): - die("invalid directory: {0}".format(dir)) + try: + pathlib.Path(dir).mkdir(parents=True, exist_ok=True) + except FileExistsError: + die("{} is not a directory".format(dir)) def die_atnode(msg, node): @@ -145,8 +215,8 @@ def calcurse_version(): def get_auth_headers(): if not username or not password: return {} - user_password = ('{}:{}'.format(username, password)).encode('ascii') - user_password = base64.b64encode(user_password).decode('ascii') + user_password = ('{}:{}'.format(username, password)).encode('utf-8') + user_password = base64.b64encode(user_password).decode('utf-8') headers = {'Authorization': 'Basic {}'.format(user_password)} return headers @@ -351,18 +421,21 @@ def push_object(conn, objhash): if not headers: return None - - etag = None headerdict = dict(headers) - if 'etag' in headerdict: - etag = headerdict['etag'] - while not etag: + + # Retrieve href from server to match server-side format. Retrieve ETag + # unless it can be extracted from the PUT response already. + ret_href, ret_etag = None, headerdict.get('etag') + while not ret_etag or not ret_href: etagdict = get_etags(conn, [href]) - if etagdict: - etag = next(iter(etagdict.values())) - etag = etag.strip('"') + if not etagdict: + continue + ret_href, new_etag = next(iter(etagdict.items())) + # Favor ETag from PUT response to avoid race condition. + if not ret_etag: + ret_etag = new_etag - return (urllib.parse.quote(href), etag) + return (ret_href, ret_etag.strip('"')) def push_objects(objhashes, conn, syncdb, etagdict): @@ -519,11 +592,29 @@ def run_hook(name): nsmap = {"D": "DAV:", "C": "urn:ietf:params:xml:ns:caldav"} # Initialize default values. -configfn = os.path.expanduser("~/.calcurse/caldav/config") -lockfn = os.path.expanduser("~/.calcurse/caldav/lock") -syncdbfn = os.path.expanduser("~/.calcurse/caldav/sync.db") -hookdir = os.path.expanduser("~/.calcurse/caldav/hooks/") -oauth_file = os.path.expanduser("~/.calcurse/caldav/oauth2_cred") +if os.path.isdir(os.path.expanduser("~/.calcurse")): + caldav_path = os.path.expanduser("~/.calcurse/caldav") + check_dir(caldav_path) + + configfn = os.path.join(caldav_path, "config") + hookdir = os.path.join(caldav_path, "hooks") + oauth_file = os.path.join(caldav_path, "oauth2_cred") + lockfn = os.path.join(caldav_path, "lock") + syncdbfn = os.path.join(caldav_path, "sync.db") +else: + xdg_config_home = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) + xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share")) + caldav_config = os.path.join(xdg_config_home, "calcurse", "caldav") + caldav_data = os.path.join(xdg_data_home, "calcurse", "caldav") + check_dir(caldav_config) + check_dir(caldav_data) + + configfn = os.path.join(caldav_config, "config") + hookdir = os.path.join(caldav_config, "hooks") + oauth_file = os.path.join(caldav_config, "oauth2_cred") + + lockfn = os.path.join(caldav_data, "lock") + syncdbfn = os.path.join(caldav_data, "sync.db") # Parse command line arguments. parser = argparse.ArgumentParser('calcurse-caldav') @@ -568,105 +659,57 @@ verbose = args.verbose debug = args.debug debug_raw = args.debug_raw -# Read environment variables -password = os.getenv('CALCURSE_CALDAV_PASSWORD') - # Read configuration. -config = configparser.RawConfigParser() -if verbose: - print('Loading configuration from ' + configfn + '...') -try: - config.read_file(open(configfn)) -except FileNotFoundError as e: - die('Configuration file not found: {}'.format(configfn)) - -if config.has_option('General', 'InsecureSSL'): - insecure_ssl = config.getboolean('General', 'InsecureSSL') +config = Config(configfn) + +authmethod = config.get('General', 'AuthMethod').lower() +calcurse = [config.get('General', 'Binary')] +debug = debug or config.get('General', 'Debug') +dry_run = config.get('General', 'DryRun') +hostname = config.get('General', 'Hostname') +https = config.get('General', 'HTTPS') +insecure_ssl = config.get('General', 'InsecureSSL') +path = config.get('General', 'Path') +sync_filter = config.get('General', 'SyncFilter') +verbose = verbose or config.get('General', 'Verbose') + +if os.getenv('CALCURSE_CALDAV_PASSWORD'): + # This approach is deprecated, but preserved for backwards compatibility + password = os.getenv('CALCURSE_CALDAV_PASSWORD') +elif config.get('Auth', 'Password'): + password = config.get('Auth', 'Password') +elif config.get('Auth', 'PasswordCommand'): + tokenized_cmd = shlex.split(config.get('Auth', 'PasswordCommand')) + password = subprocess.run(tokenized_cmd, capture_output=True).stdout.decode('UTF-8') else: - insecure_ssl = False + password = None -# Read config for "HTTPS" option (default=True) -if config.has_option('General', 'HTTPS'): - https = config.getboolean('General', 'HTTPS') -else: - https = True +username = config.get('Auth', 'Username') -if config.has_option('General', 'Binary'): - calcurse = [config.get('General', 'Binary')] -else: - calcurse = ['calcurse'] +client_id = config.get('OAuth2', 'ClientID') +client_secret = config.get('OAuth2', 'ClientSecret') +redirect_uri = config.get('OAuth2', 'RedirectURI') +scope = config.get('OAuth2', 'Scope') + +custom_headers = config.section('CustomHeaders') +# Append data directory to calcurse command. if datadir: check_dir(datadir) calcurse += ['-D', datadir] -if config.has_option('General', 'DryRun'): - dry_run = config.getboolean('General', 'DryRun') -else: - dry_run = True - -if not verbose and config.has_option('General', 'Verbose'): - verbose = config.getboolean('General', 'Verbose') - -if not debug and config.has_option('General', 'Debug'): - debug = config.getboolean('General', 'Debug') - -if config.has_option('General', 'AuthMethod'): - authmethod = config.get('General', 'AuthMethod').lower() -else: - authmethod = 'basic' - -if config.has_option('General', 'SyncFilter'): - sync_filter = config.get('General', 'SyncFilter') - - invalid_filter_values = validate_sync_filter() - - if len(invalid_filter_values): - die('Invalid value(s) in SyncFilter option: ' + ', '.join(invalid_filter_values)) -else: - sync_filter = 'cal,todo' - -if config.has_option('Auth', 'UserName'): - username = config.get('Auth', 'UserName') -else: - username = None - -if config.has_option('Auth', 'Password') and not password: - password = config.get('Auth', 'Password') - -if config.has_section('CustomHeaders'): - custom_headers = dict(config.items('CustomHeaders')) -else: - custom_headers = {} - -if config.has_option('OAuth2', 'ClientID'): - client_id = config.get('OAuth2', 'ClientID') -else: - client_id = None - -if config.has_option('OAuth2', 'ClientSecret'): - client_secret = config.get('OAuth2', 'ClientSecret') -else: - client_secret = None - -if config.has_option('OAuth2', 'Scope'): - scope = config.get('OAuth2', 'Scope') -else: - scope = None - -if config.has_option('OAuth2', 'RedirectURI'): - redirect_uri = config.get('OAuth2', 'RedirectURI') -else: - redirect_uri = 'http://127.0.0.1' - -# Change URl prefix according to HTTP/HTTPS -if https: - urlprefix = "https://" -else: - urlprefix = "http://" - -hostname = config.get('General', 'HostName') -path = '/' + config.get('General', 'Path').strip('/') + '/' +# Validate sync filter. +invalid_filter_values = validate_sync_filter() +if len(invalid_filter_values): + die('Invalid value(s) in SyncFilter option: ' + ', '.join(invalid_filter_values)) + +# Ensure host name and path are defined and initialize *_uri. +if not hostname: + die('Hostname missing in configuration.') +if not path: + die('Path missing in configuration.') +urlprefix = "https://" if https else "http://" +path = '/{}/'.format(path.strip('/')) hostname_uri = urlprefix + hostname absolute_uri = hostname_uri + path @@ -699,9 +742,7 @@ try: # Connect to the server. if verbose: print('Connecting to ' + hostname + '...') - conn = httplib2.Http() - if insecure_ssl: - conn.disable_ssl_certificate_validation = True + conn = httplib2.Http(disable_ssl_certificate_validation=insecure_ssl) if authmethod == 'oauth2': # Authenticate with OAuth2 and authorize HTTP object @@ -764,7 +805,7 @@ try: # Write the synchronization database. save_syncdb(syncdbfn, syncdb) - #Clear OAuth2 credentials if used + # Clear OAuth2 credentials if used. if authmethod == 'oauth2': conn.clear_credentials() diff --git a/contrib/caldav/config.sample b/contrib/caldav/config.sample index 4d87681..0ba8fa8 100644 --- a/contrib/caldav/config.sample +++ b/contrib/caldav/config.sample @@ -1,15 +1,22 @@ # If you want to synchronize calcurse with a CalDAV server using -# calcurse-caldav, create a new directory ~/.calcurse/caldav/, copy this file -# to ~/.calcurse/caldav/config and adjust the configuration below. +# calcurse-caldav, create a new directory at $XDG_CONFIG_HOME/calcurse/caldav/ +# (~/.config/calcurse/caldav/) and $XDG_DATA_HOME/calcurse/caldav/ +# (~/.local/share/calcurse/caldav/) and copy this file to +# $XDG_CONFIG_HOME/calcurse/caldav/config and adjust the configuration below. +# Alternatively, if using ~/.calcurse, create a new directory at +# ~/.calcurse/caldav/ and copy this file to ~/.calcurse/caldav/config and adjust +# the configuration file below. [General] # Path to the calcurse binary that is used for importing/exporting items. Binary = calcurse -# Host name of the server that hosts CalDAV. +# Host name of the server that hosts CalDAV. Do NOT prepend a protocol prefix, +# such as http:// or https://. Append :<port> for a port other than 80. Hostname = some.hostname.com -# Path to the CalDAV calendar on the host specified above. +# Path to the CalDAV calendar on the host specified above. This is the base +# path following your host name in the URL. Path = /path/to/calendar/on/the/server/ # Type of authentication to use. Must be "basic" or "oauth2" @@ -41,11 +48,13 @@ DryRun = Yes # Enable this if you want detailed logs written to stdout. Verbose = Yes -# Credentials for HTTP Basic Authentication. Leave this commented out if you do -# not want to use authentication. +# Credentials for HTTP Basic Authentication (if required). +# Set `Password` to your password in plaintext (unsafe), +# or `PasswordCommand` to a shell command that retrieves it (recommended). #[Auth] #Username = user -#Password = pass +#Password = password +#PasswordCommand = pass baikal # Optionally specify additional HTTP headers here. #[CustomHeaders] diff --git a/contrib/caldav/hooks/post-sync b/contrib/caldav/hooks/post-sync index 1ac028c..db0059b 100755 --- a/contrib/caldav/hooks/post-sync +++ b/contrib/caldav/hooks/post-sync @@ -4,14 +4,32 @@ # repository, it automatically makes a commit whenever synchronizing with a # CalDAV server. # -# In order to install this hook, copy this file to ~/.calcurse/caldav/hooks/. +# In order to install this hook, copy this file to +# $XDG_CONFIG_HOME/calcurse/caldav/hooks/ (~/.config/calcurse/caldav/hooks/) or +# ~/.calcurse/caldav/hooks/ if using ~/.calcurse. -cd "$HOME"/.calcurse/ +data_dir="$HOME/.calcurse" +config_dir="$HOME/.calcurse" -# If the data directory is under version control, create a Git commit. -if [ -d .git -a -x "$(which git)" ]; then - git add apts conf keys todo - if ! git diff-index --quiet --cached HEAD; then - git commit -m "Automatic commit by the post-sync hook" - fi +if [ ! -d "$data_dir" ]; then + data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/calcurse" + config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/calcurse" fi + +# Do not do anything when synchronizing with a CalDAV server. +[ -f "$data_dir/caldav/lock" ] && exit + +# If the directory is under version control, create a Git commit. +commit_dir() { + cd "$1" >/dev/null 2>&1 || return + shift + if [ -d .git ] && command -v git >/dev/null; then + git add "$@" + if ! git diff-index --quiet --cached HEAD; then + git commit -m "Automatic commit by the post-sync hook" + fi + fi +} + +commit_dir "$data_dir" apts todo +commit_dir "$config_dir" conf keys diff --git a/contrib/hooks/post-save b/contrib/hooks/post-save index bb582e2..6e3f11e 100755 --- a/contrib/hooks/post-save +++ b/contrib/hooks/post-save @@ -2,26 +2,42 @@ # # This is an example hook. It does two things whenever you save the data files: # -# 1. Make a commit if the calcurse data directory contains a Git repository. +# 1. Make a commit if the calcurse directories contain a Git repository. # 2. Synchronize with a CalDAV server if calcurse-caldav is configured. # -# In order to install this hook, copy this file to ~/.calcurse/hooks/. +# In order to install this hook, copy this file to +# $XDG_CONFIG_HOME/calcurse/hooks/ (~/.config/calcurse/hooks/) or +# ~/.calcurse/hooks/ if using ~/.calcurse. -cd "$HOME"/.calcurse/ +data_dir="$HOME/.calcurse" +config_dir="$HOME/.calcurse" + +if [ ! -d "$data_dir" ]; then + data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/calcurse" + config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/calcurse" +fi # Do not do anything when synchronizing with a CalDAV server. -[ -f caldav/lock ] && exit +[ -f "$data_dir/caldav/lock" ] && exit -# If the data directory is under version control, create a Git commit. -if [ -d .git -a -x "$(which git)" ]; then - git add apts conf keys todo - if ! git diff-index --quiet --cached HEAD; then - git commit -m "Automatic commit by the post-save hook" +# If the directory is under version control, create a Git commit. +commit_dir() { + cd "$1" >/dev/null 2>&1 || return + shift + if [ -d .git ] && command -v git >/dev/null; then + git add "$@" + if ! git diff-index --quiet --cached HEAD; then + git commit -m "Automatic commit by the post-save hook" + fi fi -fi +} + +commit_dir "$data_dir" apts todo +commit_dir "$config_dir" conf keys # Optionally run the CalDAV synchronization script in the background. -if [ -d caldav -a -x "$(which calcurse-caldav)" ]; then +cd "$data_dir" || exit +if [ -d caldav ] && command -v calcurse-caldav >/dev/null; then ( date="$(date +'%b %d %H:%M:%S')" echo "$date Running calcurse-caldav from the post-save hook..." diff --git a/contrib/hooks/pre-load b/contrib/hooks/pre-load index c9ab62d..d69fbde 100755 --- a/contrib/hooks/pre-load +++ b/contrib/hooks/pre-load @@ -3,15 +3,19 @@ # This is an example hook. It synchronizes calcurse with a CalDAV server before # loading the data files. # -# In order to install this hook, copy this file to ~/.calcurse/hooks/. +# In order to install this hook, copy this file to +# $XDG_CONFIG_HOME/calcurse/hooks/ (~/.config/calcurse/hooks/) or +# ~/.calcurse/hooks/ if using ~/.calcurse. -cd "$HOME"/.calcurse/ +[ -d "$HOME/.calcurse" ] && data_dir="$HOME/.calcurse" || data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/calcurse" + +cd "$data_dir" || exit # Do not do anything when synchronizing with a CalDAV server. [ -f caldav/lock ] && exit # Run the CalDAV synchronization script in the background. -if [ -d caldav -a -x "$(which calcurse-caldav)" ]; then +if [ -d caldav ] && command -v calcurse-caldav >/dev/null; then ( date="$(date +'%b %d %H:%M:%S')" echo "$date Running calcurse-caldav from the pre-load hook..." diff --git a/contrib/import/DST-et.ical b/contrib/import/DST-et.ical new file mode 100644 index 0000000..116fe95 --- /dev/null +++ b/contrib/import/DST-et.ical @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID: +BEGIN:VEVENT +DTSTAMP: +UID: +DTSTART;VALUE=DATE:20200329 +SUMMARY:Daylight Saving Time begins (EDT) +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +END:VEVENT +END:VCALENDAR diff --git a/contrib/import/DST-eu.ical b/contrib/import/DST-eu.ical new file mode 100644 index 0000000..e536cf0 --- /dev/null +++ b/contrib/import/DST-eu.ical @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID: +BEGIN:VEVENT +DTSTAMP: +UID: +DTSTART;VALUE=DATE:20200329 +SUMMARY:Daylight Saving Time begins (CEST) +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 +END:VEVENT +END:VCALENDAR diff --git a/contrib/import/February.ical b/contrib/import/February.ical new file mode 100644 index 0000000..15168d4 --- /dev/null +++ b/contrib/import/February.ical @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID: +BEGIN:VEVENT +DTSTAMP: +UID: +DTSTART:20200228T080000 +SUMMARY:Penultimate day in February +RRULE:FREQ=MONTHLY;BYMONTHDAY=-2;BYMONTH=2 +END:VEVENT +END:VCALENDAR diff --git a/contrib/import/Monday-first-last.ical b/contrib/import/Monday-first-last.ical new file mode 100644 index 0000000..431bebb --- /dev/null +++ b/contrib/import/Monday-first-last.ical @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID: +BEGIN:VEVENT +DTSTAMP: +UID: +DTSTART:20200106T120000 +SUMMARY:First and last Monday of the month +RRULE:FREQ=MONTHLY;BYDAY=1MO,-1MO;COUNT=10 +END:VEVENT +END:VCALENDAR diff --git a/contrib/import/Tuesday-Thursday.ical b/contrib/import/Tuesday-Thursday.ical new file mode 100644 index 0000000..84c5adb --- /dev/null +++ b/contrib/import/Tuesday-Thursday.ical @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID: +BEGIN:VEVENT +DTSTAMP: +UID: +DTSTART:20200102T120000 +SUMMARY:Every Tuesday and Thursday +RRULE:FREQ=WEEKLY;BYDAY=TU,TH;COUNT=10 +END:VEVENT +END:VCALENDAR diff --git a/contrib/import/Wednesdays-all.ical b/contrib/import/Wednesdays-all.ical new file mode 100644 index 0000000..40730b8 --- /dev/null +++ b/contrib/import/Wednesdays-all.ical @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID: +BEGIN:VEVENT +DTSTAMP: +UID: +DTSTART;VALUE=DATE:20200603 +SUMMARY:All Wednesdays in June and July +RRULE:FREQ=MONTHLY;BYDAY=WE;BYMONTH=6,7;UNTIL=20220630 +END:VEVENT +END:VCALENDAR diff --git a/contrib/setup.cfg b/contrib/setup.cfg new file mode 100644 index 0000000..0691fee --- /dev/null +++ b/contrib/setup.cfg @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 100 +max-complexity = 16 diff --git a/contrib/vdir/README.md b/contrib/vdir/README.md index b040d04..676b94c 100644 --- a/contrib/vdir/README.md +++ b/contrib/vdir/README.md @@ -39,7 +39,8 @@ 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`. +using the `-D` flag if it differs from the default `$XDG_DATA_HOME/calcurse` +(`~/.local/share/calcurse`) or `~/.calcurse`. Integration with vdirsyncer --------------------------- diff --git a/contrib/vdir/calcurse-vdirsyncer b/contrib/vdir/calcurse-vdirsyncer index c5371b5..2ac849a 100755 --- a/contrib/vdir/calcurse-vdirsyncer +++ b/contrib/vdir/calcurse-vdirsyncer @@ -34,7 +34,7 @@ if [ "$#" -lt 1 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then usage fi -DATADIR="$HOME/.calcurse" +[ -d "$HOME/.calcurse" ] && DATADIR="$HOME/.calcurse" || DATADIR="${XDG_DATA_HOME:-$HOME/.local/share}/calcurse" VERBOSE="" FORCE="" |