Add README, new_cycle, weekly_review, and update habits.sh
This commit is contained in:
parent
b5b58c8b90
commit
fa9b90ae1b
4 changed files with 507 additions and 16 deletions
163
new_cycle
Executable file
163
new_cycle
Executable file
|
|
@ -0,0 +1,163 @@
|
|||
#!/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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue