OS/new_cycle

164 lines
4.7 KiB
Text
Raw Normal View History

#!/usr/bin/env python3
import argparse
import re
from datetime import date, datetime, timedelta
from pathlib import Path
H1_RE = re.compile(r"^# (?!#)")
CYCLE_RE = re.compile(
r"^# Cycle (\d+) \((\d{2}/\d{2}/\d{4}) - (\d{2}/\d{2}/\d{4})\)$"
)
def previous_sun_sat(today: date):
# End on the most recent Saturday.
days_since_saturday = (today.weekday() + 2) % 7
end = today - timedelta(days=days_since_saturday)
start = end - timedelta(days=6)
return start, end
def fmt(d: date):
return d.strftime("%m/%d/%Y")
def parse_date(s: str):
return datetime.strptime(s, "%m/%d/%Y").date()
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"file",
nargs="?",
default="~/.local/share/os/tasks.md",
help="Task markdown file (default: ~/.local/share/os/tasks.md)",
)
parser.add_argument("--today", help="Override today, format YYYY-MM-DD")
args = parser.parse_args()
path = Path(args.file).expanduser()
lines = path.read_text().splitlines(keepends=True)
today = (
datetime.strptime(args.today, "%Y-%m-%d").date()
if args.today
else date.today()
)
target_start, target_end = previous_sun_sat(today)
cycles = []
for line in lines:
m = CYCLE_RE.match(line.strip())
if m:
cycles.append((int(m.group(1)), parse_date(m.group(2)), parse_date(m.group(3))))
max_cycle = max((cycle[0] for cycle in cycles), default=0)
latest_end = max((cycle[2] for cycle in cycles), default=None)
if latest_end is not None and latest_end >= target_end:
print(f"Nothing to do; latest cycle already ends {fmt(latest_end)}")
return
missing_cycles = []
if latest_end is None:
missing_cycles.append((max_cycle + 1, target_start, target_end))
else:
cycle_start = latest_end + timedelta(days=1)
cycle_num = max_cycle + 1
while cycle_start + timedelta(days=6) <= target_end:
cycle_end = cycle_start + timedelta(days=6)
missing_cycles.append((cycle_num, cycle_start, cycle_end))
cycle_start += timedelta(days=7)
cycle_num += 1
if not missing_cycles:
print("Nothing to do")
return
current_start = None
for i, line in enumerate(lines):
if line.strip() == "# Current":
current_start = i
break
if current_start is None:
raise SystemExit("Could not find '# Current' section")
current_end = len(lines)
for i in range(current_start + 1, len(lines)):
if H1_RE.match(lines[i]):
current_end = i
break
done_header = None
for i in range(current_start + 1, current_end):
if lines[i].strip() == "## Done":
done_header = i
break
if done_header is None:
raise SystemExit("Could not find '## Done' under '# Current'")
done_start = done_header + 1
done_end = current_end
for i in range(done_start, current_end):
if lines[i].startswith("## "):
done_end = i
break
done_lines = lines[done_start:done_end]
while done_lines and not done_lines[0].strip():
done_lines.pop(0)
while done_lines and not done_lines[-1].strip():
done_lines.pop()
# Empty Current -> Done section. If there are no done items, still create
# the new cycle block with an empty Done section.
lines = lines[:done_start] + ["\n"] + lines[done_end:]
# Re-find end of Current after edit.
current_start = next(i for i, line in enumerate(lines) if line.strip() == "# Current")
insert_at = len(lines)
for i in range(current_start + 1, len(lines)):
if H1_RE.match(lines[i]):
insert_at = i
break
cycle_block = []
latest_cycle = missing_cycles[-1]
for cycle_num, cycle_start, cycle_end in reversed(missing_cycles):
cycle_block.extend([
f"# Cycle {cycle_num} ({fmt(cycle_start)} - {fmt(cycle_end)})\n",
"\n",
])
if (cycle_num, cycle_start, cycle_end) == latest_cycle:
cycle_block.extend(done_lines)
cycle_block.append("\n")
prefix = lines[:insert_at]
suffix = lines[insert_at:]
if prefix and prefix[-1].strip():
prefix.append("\n")
if suffix and suffix[0].strip():
cycle_block.append("\n")
path.write_text("".join(prefix + cycle_block + suffix))
if len(missing_cycles) == 1:
cycle_num, cycle_start, cycle_end = missing_cycles[0]
print(f"Created # Cycle {cycle_num} ({fmt(cycle_start)} - {fmt(cycle_end)})")
else:
first_num = missing_cycles[0][0]
last_num = missing_cycles[-1][0]
print(f"Created cycles {first_num}-{last_num}")
if __name__ == "__main__":
main()