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
19
README.md
Normal file
19
README.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# OS
|
||||||
|
|
||||||
|
This is a set of productivity scripts bash/python to create a "life operating system."
|
||||||
|
Inspired mainly by August Bradley's Notion PPV system (which I previously built inside of Anytype).
|
||||||
|
The issue with Anytype (and Notion) is that there are too many clicks needed.
|
||||||
|
While Notion has better automation, I think nothing can beat the functionality of just scripts.
|
||||||
|
That and the fact that I want to have all the files as markdown in case I want to search/collect statistics or plots/process with a local llm inspired this project.
|
||||||
|
|
||||||
|
## Functionality and status
|
||||||
|
|
||||||
|
Here is the list of functionality that I'm working towards.
|
||||||
|
|
||||||
|
- [x] Cycles: Basic task weekly task log with history stored in a task.md file.
|
||||||
|
- [x] Habits: Keep track of habits and metrics in a habits.md file
|
||||||
|
- [x] Weekly Review: Review habits/completed actions and write some notes for each week.
|
||||||
|
- [ ] Monthly Review: Review habits/completed actions/and weekly accomplishments and update plans for each month.
|
||||||
|
- [ ] Vault: Knowledge vault for tracking notes
|
||||||
|
- [ ] Projects: Project management file that can be synced to pull tasks associated with the project and links to notes from vaults.
|
||||||
|
- [ ] Daily Mindset Shaping: Generates a view into a daily routine for mindset shaping.
|
||||||
73
habits.sh
73
habits.sh
|
|
@ -19,41 +19,79 @@ ensure_file() {
|
||||||
touch "$HABITS_FILE"
|
touch "$HABITS_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
date_add_days() {
|
||||||
|
local day="$1" offset="$2" date_arg epoch
|
||||||
|
|
||||||
|
date_arg="$day $offset days"
|
||||||
|
if date -d "$date_arg" +%F >/dev/null 2>&1; then
|
||||||
|
date -d "$date_arg" +%F
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# BSD/macOS date. Parse at noon to avoid midnight DST edges.
|
||||||
|
epoch="$(date -j -f '%F %H:%M:%S' "$day 12:00:00" +%s)"
|
||||||
|
epoch=$((epoch + offset * 86400))
|
||||||
|
date -r "$epoch" +%F
|
||||||
|
}
|
||||||
|
|
||||||
open_habits() {
|
open_habits() {
|
||||||
ensure_file
|
ensure_file
|
||||||
"${EDITOR:-vi}" "$HABITS_FILE"
|
"${EDITOR:-vi}" "$HABITS_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
new_day() {
|
print_day() {
|
||||||
ensure_file
|
local day="$1"
|
||||||
|
|
||||||
local date
|
printf '# %s\n\n' "$day"
|
||||||
date="$(date +%F)"
|
|
||||||
|
|
||||||
if grep -q "^# $date$" "$HABITS_FILE"; then
|
|
||||||
echo "A habits entry for $date already exists: $HABITS_FILE" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local tmp
|
|
||||||
tmp="$(mktemp)"
|
|
||||||
|
|
||||||
{
|
|
||||||
printf '# %s\n\n' "$date"
|
|
||||||
printf '## Habits\n\n'
|
printf '## Habits\n\n'
|
||||||
while IFS= read -r habit; do
|
while IFS= read -r habit; do
|
||||||
[ -n "$habit" ] && printf -- '- [ ] %s\n' "$habit"
|
[ -n "$habit" ] && printf -- '- [ ] %s\n' "$habit"
|
||||||
done <<< "$HABITS"
|
done <<< "$HABITS"
|
||||||
printf '\n## Metrics \n\n'
|
printf '\n## Metrics \n\n'
|
||||||
while IFS= read -r metric; do
|
while IFS= read -r metric; do
|
||||||
[ -n "$metric" ] && printf -- '- %s\n' "$metric" ":"
|
[ -n "$metric" ] && printf -- '- %s\n' "$metric:"
|
||||||
done <<< "$METRICS"
|
done <<< "$METRICS"
|
||||||
printf '\n'
|
printf '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
new_day() {
|
||||||
|
ensure_file
|
||||||
|
|
||||||
|
local today latest start day tmp added
|
||||||
|
today="$(date +%F)"
|
||||||
|
latest="$(grep -E '^# [0-9]{4}-[0-9]{2}-[0-9]{2}$' "$HABITS_FILE" | sed 's/^# //' | sort | tail -n 1 || true)"
|
||||||
|
|
||||||
|
if [ -n "$latest" ] && [[ "$latest" > "$today" ]]; then
|
||||||
|
echo "Latest habits entry is in the future: $latest" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$latest" = "$today" ]; then
|
||||||
|
echo "Habits entries already exist through $today: $HABITS_FILE"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$latest" ]; then
|
||||||
|
start="$(date_add_days "$latest" 1)"
|
||||||
|
else
|
||||||
|
start="$today"
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
added=0
|
||||||
|
|
||||||
|
{
|
||||||
|
day="$today"
|
||||||
|
while [[ "$day" > "$start" || "$day" = "$start" ]]; do
|
||||||
|
print_day "$day"
|
||||||
|
added=$((added + 1))
|
||||||
|
day="$(date_add_days "$day" -1)"
|
||||||
|
done
|
||||||
cat "$HABITS_FILE"
|
cat "$HABITS_FILE"
|
||||||
} > "$tmp"
|
} > "$tmp"
|
||||||
|
|
||||||
mv "$tmp" "$HABITS_FILE"
|
mv "$tmp" "$HABITS_FILE"
|
||||||
echo "Added $date to $HABITS_FILE"
|
echo "Added $added habits entr$( [ "$added" -eq 1 ] && printf 'y' || printf 'ies' ) through $today to $HABITS_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
remaining_habits() {
|
remaining_habits() {
|
||||||
|
|
@ -80,6 +118,7 @@ case "${1:-}" in
|
||||||
;;
|
;;
|
||||||
new)
|
new)
|
||||||
new_day
|
new_day
|
||||||
|
open_habits
|
||||||
;;
|
;;
|
||||||
left)
|
left)
|
||||||
remaining_habits
|
remaining_habits
|
||||||
|
|
|
||||||
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()
|
||||||
270
weekly_review
Executable file
270
weekly_review
Executable file
|
|
@ -0,0 +1,270 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
H1_RE = re.compile(r"^# (?!#)")
|
||||||
|
DATE_H1_RE = re.compile(r"^# (\d{4}-\d{2}-\d{2})\s*$")
|
||||||
|
CYCLE_RE = re.compile(r"^# Cycle \d+ \((\d{2}/\d{2}/\d{4}) - (\d{2}/\d{2}/\d{4})\)\s*$")
|
||||||
|
CHECK_RE = re.compile(r"^- \[( |x|X)\] (.+)$")
|
||||||
|
METRIC_RE = re.compile(r"^-?\s*([^:]+):\s*(.*)$")
|
||||||
|
|
||||||
|
|
||||||
|
# Edit this markdown to change the weekly review template appended to each file.
|
||||||
|
REVIEW_TEMPLATE = """## Accomplishments
|
||||||
|
|
||||||
|
## Disapointments
|
||||||
|
|
||||||
|
## Highs
|
||||||
|
|
||||||
|
## Lows
|
||||||
|
|
||||||
|
## What I learned
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_iso(s: str) -> date:
|
||||||
|
return datetime.strptime(s, "%Y-%m-%d").date()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_us(s: str) -> date:
|
||||||
|
return datetime.strptime(s, "%m/%d/%Y").date()
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_us(d: date) -> str:
|
||||||
|
return d.strftime("%m/%d/%Y")
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_file(d: date) -> str:
|
||||||
|
return d.strftime("%m-%d-%Y")
|
||||||
|
|
||||||
|
|
||||||
|
def last_completed_sun_sat(today: date) -> tuple[date, date]:
|
||||||
|
days_since_saturday = (today.weekday() + 2) % 7
|
||||||
|
end = today - timedelta(days=days_since_saturday)
|
||||||
|
start = end - timedelta(days=6)
|
||||||
|
return start, end
|
||||||
|
|
||||||
|
|
||||||
|
def dates_between(start: date, end: date) -> list[date]:
|
||||||
|
days = []
|
||||||
|
current = start
|
||||||
|
while current <= end:
|
||||||
|
days.append(current)
|
||||||
|
current += timedelta(days=1)
|
||||||
|
return days
|
||||||
|
|
||||||
|
|
||||||
|
def section_lines(lines: list[str], header_index: int) -> list[str]:
|
||||||
|
end = len(lines)
|
||||||
|
for i in range(header_index + 1, len(lines)):
|
||||||
|
if H1_RE.match(lines[i]):
|
||||||
|
end = i
|
||||||
|
break
|
||||||
|
return lines[header_index + 1 : end]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_habits(path: Path, start: date, end: date):
|
||||||
|
entries = {d: {"habits": {}, "metrics": {}} for d in dates_between(start, end)}
|
||||||
|
habit_names = []
|
||||||
|
metric_names = []
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
return entries, habit_names, metric_names
|
||||||
|
|
||||||
|
lines = path.read_text().splitlines()
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
m = DATE_H1_RE.match(line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
|
||||||
|
day = parse_iso(m.group(1))
|
||||||
|
if day < start or day > end:
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_section = None
|
||||||
|
for entry_line in section_lines(lines, i):
|
||||||
|
stripped = entry_line.strip()
|
||||||
|
if stripped == "## Habits":
|
||||||
|
current_section = "habits"
|
||||||
|
continue
|
||||||
|
if stripped == "## Metrics":
|
||||||
|
current_section = "metrics"
|
||||||
|
continue
|
||||||
|
if stripped.startswith("## "):
|
||||||
|
current_section = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
if current_section == "habits":
|
||||||
|
hm = CHECK_RE.match(stripped)
|
||||||
|
if hm:
|
||||||
|
name = hm.group(2).strip()
|
||||||
|
entries[day]["habits"][name] = hm.group(1).lower() == "x"
|
||||||
|
if name not in habit_names:
|
||||||
|
habit_names.append(name)
|
||||||
|
elif current_section == "metrics":
|
||||||
|
mm = METRIC_RE.match(stripped)
|
||||||
|
if mm:
|
||||||
|
name = mm.group(1).strip()
|
||||||
|
value = mm.group(2).strip()
|
||||||
|
entries[day]["metrics"][name] = value
|
||||||
|
if name not in metric_names:
|
||||||
|
metric_names.append(name)
|
||||||
|
|
||||||
|
return entries, habit_names, metric_names
|
||||||
|
|
||||||
|
|
||||||
|
def extract_done_tasks(path: Path, start: date, end: date) -> list[str]:
|
||||||
|
if not path.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
lines = path.read_text().splitlines()
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
m = CYCLE_RE.match(line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
if parse_us(m.group(1)) == start and parse_us(m.group(2)) == end:
|
||||||
|
body = section_lines(lines, i)
|
||||||
|
while body and not body[0].strip():
|
||||||
|
body.pop(0)
|
||||||
|
while body and not body[-1].strip():
|
||||||
|
body.pop()
|
||||||
|
if body and body[0].strip() == "## Done":
|
||||||
|
body = body[1:]
|
||||||
|
while body and not body[0].strip():
|
||||||
|
body.pop(0)
|
||||||
|
return body
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def markdown_table(headers: list[str], rows: list[list[str]]) -> str:
|
||||||
|
table = [headers] + rows
|
||||||
|
widths = [max(len(str(row[i])) for row in table) for i in range(len(headers))]
|
||||||
|
|
||||||
|
def fmt_row(row: list[str]) -> str:
|
||||||
|
cells = [str(cell).ljust(widths[i]) for i, cell in enumerate(row)]
|
||||||
|
return "| " + " | ".join(cells) + " |"
|
||||||
|
|
||||||
|
separator = ["-" * width for width in widths]
|
||||||
|
return (
|
||||||
|
"\n".join(
|
||||||
|
[fmt_row(headers), fmt_row(separator), *[fmt_row(row) for row in rows]]
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_habit_chart(entries, habit_names: list[str], start: date, end: date) -> str:
|
||||||
|
days = dates_between(start, end)
|
||||||
|
headers = ["Habit"] + [d.strftime("%a %m/%d") for d in days] + ["Total"]
|
||||||
|
rows = []
|
||||||
|
for habit in habit_names:
|
||||||
|
total = 0
|
||||||
|
row = [habit]
|
||||||
|
for day in days:
|
||||||
|
value = entries[day]["habits"].get(habit)
|
||||||
|
if value is True:
|
||||||
|
row.append("✓")
|
||||||
|
total += 1
|
||||||
|
elif value is False:
|
||||||
|
row.append("—")
|
||||||
|
else:
|
||||||
|
row.append("")
|
||||||
|
row.append(f"{total}/7")
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
return "No habit entries found for this week.\n"
|
||||||
|
return markdown_table(headers, rows)
|
||||||
|
|
||||||
|
|
||||||
|
def build_metrics_chart(
|
||||||
|
entries, metric_names: list[str], start: date, end: date
|
||||||
|
) -> str:
|
||||||
|
days = dates_between(start, end)
|
||||||
|
headers = ["Metric"] + [d.strftime("%a %m/%d") for d in days]
|
||||||
|
rows = []
|
||||||
|
for metric in metric_names:
|
||||||
|
rows.append(
|
||||||
|
[metric] + [entries[day]["metrics"].get(metric, "") for day in days]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
return "No metrics found for this week.\n"
|
||||||
|
return markdown_table(headers, rows)
|
||||||
|
|
||||||
|
|
||||||
|
def load_review_markdown() -> str:
|
||||||
|
return REVIEW_TEMPLATE.rstrip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Create a weekly review markdown file."
|
||||||
|
)
|
||||||
|
parser.add_argument("--today", help="Override today, format YYYY-MM-DD")
|
||||||
|
parser.add_argument("--start", help="Override week start, format YYYY-MM-DD")
|
||||||
|
parser.add_argument("--end", help="Override week end, format YYYY-MM-DD")
|
||||||
|
parser.add_argument(
|
||||||
|
"--force", action="store_true", help="Overwrite an existing review file"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-open", action="store_true", help="Do not open the review in $EDITOR"
|
||||||
|
)
|
||||||
|
parser.add_argument("--output-dir", default="~/.local/share/os/weeks")
|
||||||
|
parser.add_argument("--habits-file", default="~/.local/share/os/habits.md")
|
||||||
|
parser.add_argument("--tasks-file", default="~/.local/share/os/tasks.md")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
today = parse_iso(args.today) if args.today else date.today()
|
||||||
|
if args.start or args.end:
|
||||||
|
if not args.start or not args.end:
|
||||||
|
raise SystemExit("Use --start and --end together")
|
||||||
|
start = parse_iso(args.start)
|
||||||
|
end = parse_iso(args.end)
|
||||||
|
else:
|
||||||
|
start, end = last_completed_sun_sat(today)
|
||||||
|
|
||||||
|
output_dir = Path(args.output_dir).expanduser()
|
||||||
|
output_path = output_dir / f"{fmt_file(start)}--{fmt_file(end)}.md"
|
||||||
|
if output_path.exists() and not args.force:
|
||||||
|
print(f"Review already exists: {output_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
habits_file = Path(args.habits_file).expanduser()
|
||||||
|
tasks_file = Path(args.tasks_file).expanduser()
|
||||||
|
|
||||||
|
habit_entries, habit_names, metric_names = parse_habits(habits_file, start, end)
|
||||||
|
done_tasks = extract_done_tasks(tasks_file, start, end)
|
||||||
|
review_markdown = load_review_markdown()
|
||||||
|
|
||||||
|
content = []
|
||||||
|
content.append(f"# Weekly Review ({fmt_us(start)} - {fmt_us(end)})\n\n")
|
||||||
|
content.append("## Habits\n\n")
|
||||||
|
content.append(build_habit_chart(habit_entries, habit_names, start, end))
|
||||||
|
content.append("\n## Metrics\n\n")
|
||||||
|
content.append(build_metrics_chart(habit_entries, metric_names, start, end))
|
||||||
|
content.append("\n## Done Tasks\n\n")
|
||||||
|
if done_tasks:
|
||||||
|
content.append("\n".join(done_tasks).rstrip() + "\n")
|
||||||
|
else:
|
||||||
|
content.append("No done tasks found for this week.\n")
|
||||||
|
content.append("\n")
|
||||||
|
content.append(review_markdown)
|
||||||
|
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path.write_text("".join(content))
|
||||||
|
print(f"Created {output_path}")
|
||||||
|
|
||||||
|
if not args.no_open:
|
||||||
|
subprocess.run([os.environ.get("EDITOR", "vi"), str(output_path)], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue