diff --git a/home/bin/executable_todo_archive b/home/bin/executable_todo_archive index c737366..fdc2838 100644 --- a/home/bin/executable_todo_archive +++ b/home/bin/executable_todo_archive @@ -6,10 +6,10 @@ # ] # /// """ -Archive completed (DONE) todos from a [x]it! file. +Archive completed (DONE) and obsolete todos from a [x]it! file. -Extracts all checked items, outputs them (preserving groups), and removes them -from the original file. +Extracts all checked and obsolete items, outputs them (preserving groups), and +removes them from the original file. """ import argparse @@ -25,6 +25,16 @@ def format_item(item: Item) -> str: return f"[{item.state.value}] {item.description}" +def sort_groups(groups: list[Group]) -> list[Group]: + """ + Sort groups: untitled groups first, then alphabetically by title. + """ + untitled = [g for g in groups if not g.title] + titled = [g for g in groups if g.title] + titled.sort(key=lambda g: g.title.lower()) + return untitled + titled + + def format_groups(groups: list[Group]) -> str: """Format a list of groups back to [x]it! syntax.""" lines = [] @@ -41,28 +51,29 @@ def format_groups(groups: list[Group]) -> str: return "\n".join(lines) -def extract_done_items(groups: list[Group]) -> tuple[list[Group], list[Group]]: +def extract_archivable_items(groups: list[Group]) -> tuple[list[Group], list[Group]]: """ - Split groups into done and remaining items. + Split groups into archivable (done/obsolete) and remaining items. Returns: - (done_groups, remaining_groups) - Both preserve group structure. + (archivable_groups, remaining_groups) - Both preserve group structure. Empty groups are preserved in remaining_groups. """ - done_groups = [] + archivable_states = {State.CHECKED, State.OBSOLETE} + archivable_groups = [] remaining_groups = [] for group in groups: - done_items = [item for item in group.items if item.state == State.CHECKED] - remaining_items = [item for item in group.items if item.state != State.CHECKED] + archivable_items = [item for item in group.items if item.state in archivable_states] + remaining_items = [item for item in group.items if item.state not in archivable_states] - if done_items: - done_groups.append(Group(title=group.title, items=done_items)) + if archivable_items: + archivable_groups.append(Group(title=group.title, items=archivable_items)) # Always preserve the group in remaining, even if empty remaining_groups.append(Group(title=group.title, items=remaining_items)) - return done_groups, remaining_groups + return archivable_groups, remaining_groups def merge_groups(existing: list[Group], new: list[Group]) -> list[Group]: @@ -103,8 +114,8 @@ def merge_groups(existing: list[Group], new: list[Group]) -> list[Group]: def main() -> int: parser = argparse.ArgumentParser( - description="Archive completed todos from a [x]it! file.", - epilog="Done items are extracted and removed from the input file.", + description="Archive completed and obsolete todos from a [x]it! file.", + epilog="Done and obsolete items are extracted and removed from the input file.", ) parser.add_argument( "input", @@ -114,9 +125,7 @@ def main() -> int: parser.add_argument( "output", type=Path, - nargs="?", - default=None, - help="Output file for archived todos (default: stdout)", + help="Output file for archived todos", ) args = parser.parse_args() @@ -128,39 +137,43 @@ def main() -> int: with open(args.input, "r") as fp: groups = parse_file(fp) - done_groups, remaining_groups = extract_done_items(groups) + archivable_groups, remaining_groups = extract_archivable_items(groups) - if not done_groups: - print("No completed todos found.", file=sys.stderr) - return 0 - - if args.output: + if archivable_groups: if args.output.exists(): with open(args.output, "r") as fp: existing_groups = parse_file(fp) - merged_groups = merge_groups(existing_groups, done_groups) + merged_groups = merge_groups(existing_groups, archivable_groups) else: - merged_groups = done_groups + merged_groups = archivable_groups - merged_output = format_groups(merged_groups) + merged_output = format_groups(sort_groups(merged_groups)) with open(args.output, "w") as fp: fp.write(merged_output) fp.write("\n") - else: - print(format_groups(done_groups)) - - remaining_output = format_groups(remaining_groups) - with open(args.input, "w") as fp: - if remaining_output: - fp.write(remaining_output) + remaining_output = format_groups(remaining_groups) + with open(args.input, "w") as fp: + if remaining_output: + fp.write(remaining_output) + fp.write("\n") + elif args.output.exists(): + # No new items to archive, but re-sort existing archive + with open(args.output, "r") as fp: + existing_groups = parse_file(fp) + sorted_output = format_groups(sort_groups(existing_groups)) + with open(args.output, "w") as fp: + fp.write(sorted_output) fp.write("\n") - done_count = sum(len(g.items) for g in done_groups) + archived_count = sum(len(g.items) for g in archivable_groups) remaining_count = sum(len(g.items) for g in remaining_groups) - print( - f"Archived {done_count} completed todo(s). {remaining_count} remaining.", - file=sys.stderr, - ) + if archived_count: + print( + f"Archived {archived_count} todo(s). {remaining_count} remaining.", + file=sys.stderr, + ) + else: + print(f"Sorted {remaining_count} todo(s).", file=sys.stderr) return 0