From 89d998d0248672272e63f330a717eb5d5b9d5445 Mon Sep 17 00:00:00 2001 From: Matthew Ryan Dillon Date: Mon, 15 Dec 2025 14:27:02 -0500 Subject: [PATCH] archive_todos: initial script --- home/bin/executable_archive_todos | 169 ++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 home/bin/executable_archive_todos diff --git a/home/bin/executable_archive_todos b/home/bin/executable_archive_todos new file mode 100644 index 0000000..c737366 --- /dev/null +++ b/home/bin/executable_archive_todos @@ -0,0 +1,169 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "stage-left", +# ] +# /// +""" +Archive completed (DONE) todos from a [x]it! file. + +Extracts all checked items, outputs them (preserving groups), and removes them +from the original file. +""" + +import argparse +import sys +from pathlib import Path + +from stage_left import parse_file +from stage_left.types import State, Group, Item + + +def format_item(item: Item) -> str: + """Format an item back to [x]it! syntax.""" + return f"[{item.state.value}] {item.description}" + + +def format_groups(groups: list[Group]) -> str: + """Format a list of groups back to [x]it! syntax.""" + lines = [] + + for i, group in enumerate(groups): + if group.title: + if i > 0: + lines.append("") # Blank line before group title + lines.append(group.title) + + for item in group.items: + lines.append(format_item(item)) + + return "\n".join(lines) + + +def extract_done_items(groups: list[Group]) -> tuple[list[Group], list[Group]]: + """ + Split groups into done and remaining items. + + Returns: + (done_groups, remaining_groups) - Both preserve group structure. + Empty groups are preserved in remaining_groups. + """ + done_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] + + if done_items: + done_groups.append(Group(title=group.title, items=done_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 + + +def merge_groups(existing: list[Group], new: list[Group]) -> list[Group]: + """ + Merge new groups into existing groups. + + Groups with matching titles have their items combined. + New groups without a matching title are appended. + """ + merged: dict[str | None, Group] = {} + order: list[str | None] = [] + + for group in existing: + if group.title in merged: + # Extend existing group's items + merged[group.title] = Group( + title=group.title, + items=merged[group.title].items + group.items, + ) + else: + merged[group.title] = Group(title=group.title, items=list(group.items)) + order.append(group.title) + + for group in new: + if group.title in merged: + # Merge into existing group + merged[group.title] = Group( + title=group.title, + items=merged[group.title].items + group.items, + ) + else: + # New group + merged[group.title] = Group(title=group.title, items=list(group.items)) + order.append(group.title) + + return [merged[title] for title in order] + + +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.", + ) + parser.add_argument( + "input", + type=Path, + help="Path to the [x]it! file to process", + ) + parser.add_argument( + "output", + type=Path, + nargs="?", + default=None, + help="Output file for archived todos (default: stdout)", + ) + + args = parser.parse_args() + + if not args.input.exists(): + print(f"Error: Input file not found: {args.input}", file=sys.stderr) + return 1 + + with open(args.input, "r") as fp: + groups = parse_file(fp) + + done_groups, remaining_groups = extract_done_items(groups) + + if not done_groups: + print("No completed todos found.", file=sys.stderr) + return 0 + + if args.output: + if args.output.exists(): + with open(args.output, "r") as fp: + existing_groups = parse_file(fp) + merged_groups = merge_groups(existing_groups, done_groups) + else: + merged_groups = done_groups + + merged_output = format_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) + fp.write("\n") + + done_count = sum(len(g.items) for g in done_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, + ) + + return 0 + + +if __name__ == "__main__": + sys.exit(main())