archive_todos: initial script
This commit is contained in:
parent
5c8fba3b77
commit
89d998d024
1 changed files with 169 additions and 0 deletions
169
home/bin/executable_archive_todos
Normal file
169
home/bin/executable_archive_todos
Normal file
|
|
@ -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())
|
||||||
Loading…
Add table
Add a link
Reference in a new issue