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
|
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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue