diff --git a/todoman/cli.py b/todoman/cli.py index 774c4f40..ce7bef16 100644 --- a/todoman/cli.py +++ b/todoman/cli.py @@ -467,6 +467,30 @@ def done(ctx, todos): click.echo(ctx.formatter.detailed(todo)) +@cli.command() +@pass_ctx +@click.argument( + "todos", + nargs=-1, + required=True, + type=click.IntRange(0), + callback=_validate_todos, +) +# @catch_errors +def undo(ctx, todos): + """Undo one or more tasks.""" + toremove = [] + for todo in todos: + removable = todo.undo() + if removable: + toremove.append(removable) + ctx.db.save(todo) + click.echo(ctx.formatter.detailed(todo)) + + for todo in toremove: + ctx.db.delete(todo) + + @cli.command() @pass_ctx @click.argument( diff --git a/todoman/model.py b/todoman/model.py index bb4044a3..a14a2448 100644 --- a/todoman/model.py +++ b/todoman/model.py @@ -252,6 +252,27 @@ def complete(self) -> None: self.percent_complete = 100 self.status = "COMPLETED" + def undo(self) -> Todo | None: + """ + Immediately restores this todo. Marks it as needs action, resets the + percentage to 0 and deletes the completed_at datetime. + + If this todo belongs to a series, the one created at completion will + be deleted. + + Returns the todo that will be deleted. + """ + original = None + if self.is_recurring and self.related: + original = self.related.pop() + self.rrule = original.rrule + + self.completed_at = None + self.percent_complete = 0 + self.status = "NEEDS-ACTION" + + return original + @cached_property def path(self) -> str: if not self.list: