#!/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())