markor icon indicating copy to clipboard operation
markor copied to clipboard

Recurrent task

Open emacsway opened this issue 6 months ago • 4 comments

⚠️ 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

-

emacsway avatar Jun 30 '25 23:06 emacsway

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()

harshad1 avatar Jul 05 '25 17:07 harshad1

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 avatar Jul 12 '25 17:07 harshad1

@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

emacsway avatar Aug 13 '25 00:08 emacsway

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!

gsantner avatar Aug 13 '25 15:08 gsantner