Recurrent task
⚠️ This issue respects the following points: ⚠️
- [x] This is a enhancement/feature request. Not a bug or question.
- [x] The topic is not already reported at Issues. (I've searched it).
- [x] Markor is up to date. See Releases for the latest version. Updates are available from F-Droid and GitHub.
- [x] The wanted feature/enhancement is not present in the latest development version (git master). (Please download and try the test version of Markor, named Marder. Don't worry; Markor and Marder appear as completely separate applications. You can install both side-by-side, and Markor's settings are not touched. If your desired feature is present, you don't need to open this issue. The change will be part of the next Markor update.)
Description
Hi, any plan to support a recurrent task? See:
- https://updatenotes.blog/todotxt-recurring-tasks/
- https://github.com/todotxt/todo.txt/issues/97
Information
Android version: latest Device: App Version:
Source
F-Droid
Format / File type
todo.txt
Additional info / Log
-
An external script would be perfect for this.
Here is ChatGPT trying to 1shot the task
#!/usr/bin/env python3
"""
Termux-compatible script to rollover recurring todo.txt tasks.
For each completed task in TODO_FILE containing rec:…
• copy it verbatim to DONE_FILE
• remove its leading "x " + completion/creation dates
• bump its t:/due: dates per rec:(+)X(d|b|w|m|y)
• re-append it (undone) into TODO_FILE
"""
import re
from datetime import date, timedelta
# ── user-hard-coded paths ─────────────────────────────────────────────────────
TODO_FILE = "/data/data/com.termux/files/home/todo.txt"
DONE_FILE = "/data/data/com.termux/files/home/done.txt"
# ── helpers ────────────────────────────────────────────────────────────────────
iso_date = re.compile(r"\d{4}-\d{2}-\d{2}")
def parse_iso(s):
y, m, d = map(int, s.split("-"))
return date(y, m, d)
def days_in_month(y, m):
if m == 2:
return 29 if (y % 400 == 0 or (y % 100 != 0 and y % 4 == 0)) else 28
return [31,28,31,30,31,30,31,31,30,31,30,31][m-1]
def add_months(dt, n):
total_month = dt.month - 1 + n
y, m = dt.year + total_month // 12, total_month % 12 + 1
d = min(dt.day, days_in_month(y, m))
return date(y, m, d)
def add_years(dt, n):
y = dt.year + n
d = min(dt.day, days_in_month(y, dt.month))
return date(y, dt.month, d)
def adjust(dt, amount, unit):
if unit == 'd':
return dt + timedelta(days=amount)
if unit == 'b':
# business days: skip weekends
out = dt
step = 1 if amount > 0 else -1
rem = abs(amount)
while rem:
out += timedelta(days=step)
if out.weekday() < 5:
rem -= 1
return out
if unit == 'w':
return dt + timedelta(weeks=amount)
if unit == 'm':
return add_months(dt, amount)
if unit == 'y':
return add_years(dt, amount)
raise ValueError(f"unknown unit {unit}")
# ── main processing ────────────────────────────────────────────────────────────
def main():
with open(TODO_FILE, 'r') as f:
lines = [ln.rstrip("\n") for ln in f]
new_tasks = []
done_append = []
for ln in lines:
if not ln.startswith("x ") or "rec:" not in ln:
continue
# 1) send original to done.txt
done_append.append(ln)
# 2) tokenize to extract completion, creation and body
parts = ln.split()
# parts[0]=='x'
comp = parts[1]
idx = 2
# optional creation date?
if idx < len(parts) and iso_date.fullmatch(parts[idx]):
idx += 1
body_tokens = parts[idx:]
# 3) pull out tags
rec_tok = next((t for t in body_tokens if t.startswith("rec:")), None)
t_tok = next((t for t in body_tokens if t.startswith("t:")), None)
due_tok = next((t for t in body_tokens if t.startswith("due:")), None)
# parse rec:
rec_val = rec_tok.split(":",1)[1]
strict = rec_val.startswith("+")
rv = rec_val[1:] if strict else rec_val
m = re.fullmatch(r"(\d+)([dbwmy]?)", rv)
if not m:
continue # skip malformed
amt, unit = int(m.group(1)), m.group(2) or 'd'
# parse dates
t_date = parse_iso(t_tok.split(":",1)[1]) if t_tok else None
due_date = parse_iso(due_tok.split(":",1)[1]) if due_tok else None
comp_date= parse_iso(comp) if iso_date.fullmatch(comp) else None
# compute new t/due
new_t = new_due = None
if t_date:
new_t = adjust(t_date, amt, unit)
if due_date:
if strict or not t_date:
new_due = adjust(due_date, amt, unit)
else:
# normal rec with both t & due
offset = due_date - t_date
new_due = new_t + offset
# 4) rebuild
desc = " ".join(tok for tok in body_tokens
if not any(tok.startswith(pref) for pref in ("rec:","t:","due:")))
tags = []
if new_t:
tags.append(f"t:{new_t.isoformat()}")
if new_due:
tags.append(f"due:{new_due.isoformat()}")
tags.append(f"rec:{rec_val}")
new_tasks.append(f"{desc} " + " ".join(tags))
# filter out completed recurrences from original list
remaining = [ln for ln in lines if not (ln.startswith("x ") and "rec:" in ln)]
# write back
with open(TODO_FILE, 'w') as f:
for ln in remaining + new_tasks:
f.write(ln + "\n")
with open(DONE_FILE, 'a') as f:
for ln in done_append:
f.write(ln + "\n")
if __name__ == "__main__":
main()
On further thought,
This + reminders seem like a thing I would to implement
When would be the best time to create a repeat task?
We could:
- auto trigger it on marking a task done
- have an action to parse and create new tasks
@harshad1 , thanks. I think that the experience of org-mode could be usefull for thought, see:
- https://github.com/mrcnski/org-recur
- https://orgmode.org/manual/Repeated-tasks.html
- https://orgzlyrevived.com/docs#repeated-tasks
Calendar event/reminder scheduling is sure something helpful, regardless of language/syntax. Noting ahead that it can be tricky to get it **reliable** working (showing up at the right time). Especially: on boot / app not open.
That be said, improvements are welcome!