nb: passthrough todo subcommand
This commit is contained in:
parent
dc9ebef089
commit
4e08da2614
2 changed files with 9 additions and 0 deletions
|
|
@ -1,169 +0,0 @@
|
|||
#!/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