todo_archive: sort output

This commit is contained in:
Matthew Ryan Dillon 2025-12-16 10:25:20 -05:00
parent 4e08da2614
commit 304b48fe20

View file

@ -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 Extracts all checked and obsolete items, outputs them (preserving groups), and
from the original file. removes them from the original file.
""" """
import argparse import argparse
@ -25,6 +25,16 @@ def format_item(item: Item) -> str:
return f"[{item.state.value}] {item.description}" 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: def format_groups(groups: list[Group]) -> str:
"""Format a list of groups back to [x]it! syntax.""" """Format a list of groups back to [x]it! syntax."""
lines = [] lines = []
@ -41,28 +51,29 @@ def format_groups(groups: list[Group]) -> str:
return "\n".join(lines) 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: Returns:
(done_groups, remaining_groups) - Both preserve group structure. (archivable_groups, remaining_groups) - Both preserve group structure.
Empty groups are preserved in remaining_groups. Empty groups are preserved in remaining_groups.
""" """
done_groups = [] archivable_states = {State.CHECKED, State.OBSOLETE}
archivable_groups = []
remaining_groups = [] remaining_groups = []
for group in groups: for group in groups:
done_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 != State.CHECKED] remaining_items = [item for item in group.items if item.state not in archivable_states]
if done_items: if archivable_items:
done_groups.append(Group(title=group.title, items=done_items)) archivable_groups.append(Group(title=group.title, items=archivable_items))
# Always preserve the group in remaining, even if empty # Always preserve the group in remaining, even if empty
remaining_groups.append(Group(title=group.title, items=remaining_items)) 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]: 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: def main() -> int:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Archive completed todos from a [x]it! file.", description="Archive completed and obsolete todos from a [x]it! file.",
epilog="Done items are extracted and removed from the input file.", epilog="Done and obsolete items are extracted and removed from the input file.",
) )
parser.add_argument( parser.add_argument(
"input", "input",
@ -114,9 +125,7 @@ def main() -> int:
parser.add_argument( parser.add_argument(
"output", "output",
type=Path, type=Path,
nargs="?", help="Output file for archived todos",
default=None,
help="Output file for archived todos (default: stdout)",
) )
args = parser.parse_args() args = parser.parse_args()
@ -128,39 +137,43 @@ def main() -> int:
with open(args.input, "r") as fp: with open(args.input, "r") as fp:
groups = parse_file(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: if archivable_groups:
print("No completed todos found.", file=sys.stderr)
return 0
if args.output:
if args.output.exists(): if args.output.exists():
with open(args.output, "r") as fp: with open(args.output, "r") as fp:
existing_groups = parse_file(fp) existing_groups = parse_file(fp)
merged_groups = merge_groups(existing_groups, done_groups) merged_groups = merge_groups(existing_groups, archivable_groups)
else: 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: with open(args.output, "w") as fp:
fp.write(merged_output) fp.write(merged_output)
fp.write("\n") fp.write("\n")
else:
print(format_groups(done_groups))
remaining_output = format_groups(remaining_groups) remaining_output = format_groups(remaining_groups)
with open(args.input, "w") as fp: with open(args.input, "w") as fp:
if remaining_output: if remaining_output:
fp.write(remaining_output) fp.write(remaining_output)
fp.write("\n") 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) remaining_count = sum(len(g.items) for g in remaining_groups)
if archived_count:
print( print(
f"Archived {done_count} completed todo(s). {remaining_count} remaining.", f"Archived {archived_count} todo(s). {remaining_count} remaining.",
file=sys.stderr, file=sys.stderr,
) )
else:
print(f"Sorted {remaining_count} todo(s).", file=sys.stderr)
return 0 return 0