todo_archive: sort output
This commit is contained in:
parent
4e08da2614
commit
304b48fe20
1 changed files with 51 additions and 38 deletions
|
|
@ -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)
|
||||
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)
|
||||
if archived_count:
|
||||
print(
|
||||
f"Archived {done_count} completed todo(s). {remaining_count} remaining.",
|
||||
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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue