summaryrefslogtreecommitdiff
path: root/cal.py
diff options
context:
space:
mode:
authorTobias Brox <tobias@redpill-linpro.com>2022-04-18 10:07:34 +0200
committerTobias Brox <tobias@redpill-linpro.com>2022-04-18 10:07:34 +0200
commit36fbbe2654d2f73a7583d9538787fd807b781b0e (patch)
treea58a786ddc4575c6c948e2f2f1cc343435d18462 /cal.py
parent4111bcda1bd37df518153273fc5c8add3b684134 (diff)
downloadcalendar-cli-36fbbe2654d2f73a7583d9538787fd807b781b0e.zip
Work in progress on new cli
(I was working on this several months ago - then I got interrupted, forgot to commit, never got time to look back)
Diffstat (limited to 'cal.py')
-rwxr-xr-xcal.py171
1 files changed, 144 insertions, 27 deletions
diff --git a/cal.py b/cal.py
index f1b9b10..222a2f7 100755
--- a/cal.py
+++ b/cal.py
@@ -38,8 +38,18 @@ import caldav
## See https://click.palletsprojects.com/en/8.0.x/api/#click.ParamType and
## /usr/lib/*/site-packages/click/types.py on how to do this.
-## TODO ...
+## TODO: maybe find those attributes through the icalendar library?
+attr_txt_one = ['location', 'description', 'geo', 'organizer', 'summary']
+attr_txt_many = ['categories', 'comment', 'contact', 'resources']
+
+## TODO ... (and should be moved somewhere else)
def _parse_timespec(timespec):
+ """
+ will parse a timespec and return two timestamps. timespec can be
+ in ISO8601 interval format, as format 1, 2 or 3 as described at
+ https://en.wikipedia.org/wiki/ISO_8601#Time_intervals - or it can
+ be on calendar-cli format (i.e. 2021-01-08 15:00:00+1h)
+ """
raise NotImplementedError()
@click.group()
@@ -58,7 +68,6 @@ def cli(ctx, **kwargs):
This command will eventually replace calendar-cli.
"""
-
## The cli function will prepare a context object, a dict containing the
## caldav_client, principal and calendar
@@ -90,31 +99,123 @@ def test(ctx):
"""
click.echo("Seems like everything is OK")
-attr_txt_one = ['location', 'description', 'geo', 'organizer', 'summary']
-attr_txt_many = ['categories', 'comment', 'contact', 'resources']
-
-def set_attr_options(func):
+def _set_attr_options(func):
+ """
+ decorator that will add options --set-categories, --set-description etc
+ """
for foo in attr_txt_one:
- func = click.option()
+ func = click.option(f"--set-{foo}", help=f"Set ical attribute {foo}")(func)
+ for foo in attr_txt_many:
+ func = click.option(f"--set-{foo}", help=f"Set ical attribute {foo}", multiple=True)(func)
+ return func
@cli.group()
+@click.option('--all/--none', default=None, help='Select all (or none) of the objects. Overrides all other selection options.')
+@click.option('--uid', multiple=True, help='select an object with a given uid (or select more object with given uids)')
+@click.option('--abort-on-missing-uid/--ignore-missing-uid', default=False, help='Abort if (one or more) uids are not found (default: silently ignore missing uids)')
+@click.pass_context
+def select(ctx, all, uid, abort_on_missing_uid, **kwargs):
+ """
+ select/search/filter tasks/events, for listing/editing/deleting, etc
+ """
+ objs = []
+ ctx.obj['objs'] = objs
+
+ ## TODO: move all search/filter/select logic to caldav library?
+
+ ## handle all/none options
+ if all is False: ## means --none.
+ return
+ if all:
+ for c in ctx.obj['calendars']:
+ objs.append(c.objects)
+
+ ## uid(s)
+ missing_uids = []
+ for uid_ in uid:
+ cnt = 0
+ for c in ctx.obj['calendars']:
+ try:
+ objs.append(c.object_by_uid(uid_))
+ cnt += 1
+ except caldav.error.NotFoundError:
+ pass
+ if not cnt:
+ missing_uids.append(uid_)
+ if abort_on_missing_uid and missing_uids:
+ raise click.Abort(f"Did not find the following uids in any calendars: {missing_uids}")
+
+@select.command()
+@click.pass_context
+def list(ctx, **kwargs):
+ """
+ print out a list of tasks/events/journals
+ """
+ raise NotImplementedError()
+
+@select.command()
+@click.option('--multi-delete/--no-multi-delete', default=None, help="Delete multiple things without confirmation prompt")
+@click.pass_context
+def delete(ctx, multi_delete, **kwargs):
+ """
+ delete the selected item(s)
+ """
+ objs = ctx.obj['objs']
+ if multi_delete is None and len(objs)>1:
+ multi_delete = click.confirm(f"OK to delete {len(objs)} items?")
+ if len(objs)>1 and not multi_delete:
+ raise click.Abort(f"Not going to delete {len(objs)} items")
+ for obj in objs:
+ obj.delete()
+
+@select.command()
+@click.pass_context
+def complete(ctx, **kwargs):
+ raise NotImplementedError()
+
+@select.command()
+@click.pass_context
+def calculate_panic_time(ctx, **kwargs):
+ raise NotImplementedError()
+
+@select.command()
+@click.pass_context
+def sum_hours(ctx, **kwargs):
+ raise NotImplementedError()
+
+## TODO: all combinations of --first-calendar, --no-first-calendar, --multi-add, --no-multi-add should be tested
+@cli.group()
@click.option('-l', '--add-ical-line', multiple=True, help="extra ical data to be injected")
-@click.option('--multi-add/--no-multi-add', default=False, help="Add things to multiple calendars")
-@click.option('--first-calendar/--no-first-calendar', default=False, help="Add things to the first given calendar")
+@click.option('--multi-add/--no-multi-add', default=None, help="Add things to multiple calendars")
+@click.option('--first-calendar/--no-first-calendar', default=None, help="Add things only to the first calendar found")
+@_set_attr_options
@click.pass_context
def add(ctx, **kwargs):
- if (len(ctx.obj['calendars'])>1 and
- not kwargs['multi_add'] and
- not click.confirm(f"Multiple calendars given. Do you want to duplicate to {len(ctx.obj['calendars'])} calendars? (tip: use option --multi-add to avoid this prompt in the future)")):
+ """
+ Save new objects on calendar(s)
+ """
+ if len(ctx.obj['calendars'])>1 and kwargs['multi_add'] is False:
+ raise click.Abort("Giving up: Multiple calendars given, but --no-multi-add is given")
+ ## TODO: crazy-long if-conditions can be refactored - see delete on how it's done there
+ if (kwargs['first_calendar'] or
+ (len(ctx.obj['calendars'])>1 and
+ not kwargs['multi_add'] and
+ not click.confirm(f"Multiple calendars given. Do you want to duplicate to {len(ctx.obj['calendars'])} calendars? (tip: use option --multi-add to avoid this prompt in the future)"))):
calendar = ctx.obj['calendars'][0]
## TODO: we need to make sure f"{calendar.name}" will always work or something
- if (kwargs['first_calendar'] or
- click.confirm(f"First calendar on the list has url {calendar.url} - should we add there? (tip: use --calendar-url={calendar.url} or --first_calendar to avoid this prompt in the future)")):
+ if (kwargs['first_calendar'] is not None and
+ (kwargs['first_calendar'] or
+ click.confirm(f"First calendar on the list has url {calendar.url} - should we add there? (tip: use --calendar-url={calendar.url} or --first_calendar to avoid this prompt in the future)"))):
ctx.obj['calendars'] = [ calendar ]
else:
- raise click.Abort()
+ raise click.Abort("Giving up: Multiple calendars found/given, please specify which calendar you want to use")
+
ctx.obj['ical_fragment'] = "\n".join(kwargs['add_ical_line'])
-
+ ctx.obj['add_args'] = {}
+ for x in kwargs:
+ if x.startswith('set_'):
+ ctx.obj['add_args'][x[4:]] = kwargs[x]
+
@add.command()
@click.pass_context
@click.option('-d', '--ical-data', '--ical', help="ical object to be added")
@@ -129,30 +230,46 @@ def ical(ctx, ical_data, ical_file):
c.save_event(ical)
@add.command()
-def todo():
- click.echo("soon you should be able to add tasks to your calendar")
- raise NotImplementedError("foo")
+@click.argument('summary', nargs=-1)
+@click.pass_context
+def todo(ctx, summary):
+ """
+ Creates a new task with given SUMMARY
+
+ Examples:
+
+ kal add todo "fix all known bugs in calendar-cli"
+ kal add todo --set-due=2050-12-10 "release calendar-cli version 42.0.0"
+ """
+ summary = " ".join(summary)
+ ctx.obj['add_args']['summary'] = (ctx.obj['add_args']['summary'] or '') + summary
+ if not ctx.obj['add_args']['summary']:
+ raise click.Abort("denying to add a TODO with no summary given")
+ return
+ for cal in ctx.obj['calendars']:
+ todo = cal.save_todo(ical=ctx.obj['ical_fragment'], **ctx.obj['add_args'], no_overwrite=True)
+ click.echo(f"uid={todo.id}")
@add.command()
## TODO
-@click.argument('description')
+@click.argument('summary')
@click.argument('timespec')
@click.pass_context
-def event(ctx, description, timespec):
+def event(ctx, summary, timespec):
"""
- Creates a new event with given DESCRIPTION at the time specifed through TIMESPEC.
-
- TIMESPEC is an ISO-formatted date or timestamp, optionally with a postfixed interval.
+ Creates a new event with given SUMMARY at the time specifed through TIMESPEC.
- Examples:
+ TIMESPEC is an ISO-formatted date or timestamp, optionally with a postfixed interval
+.
+ Examples:
kal add event "final bughunting session" 2004-11-25+5d
kal add event "release party" 2004-11-30T19:00+2h
"""
- for cal in ctx['calendars']:
+ for cal in ctx.obj['calendars']:
(dtstart, dtend) = _parse_timespec(timespec)
#event = cal.add_event(
- click.echo(uid)
+ click.echo(f"uid={uid}")
def journal():
click.echo("soon you should be able to add journal entries to your calendar")