<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P001-todo-list/ | updated: 2026-09-27 -->

# P001 Todo list

Add tasks with a priority, mark them done, delete them, and list them in the order added, by priority, or only the ones still pending - from a text menu.

EML-P project `projects/todo-list` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/todo-list` runs it and `eml project verify projects/todo-list` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: task-priority-bucketer (https://efficientnewlanguage.org/cases/045-task-priority-bucketer/), simple-queue (https://efficientnewlanguage.org/cases/040-simple-queue/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
9
1

1
   
1
Test
high
1
Test
5
3
x
3
42
4
7
7
```

Screen:

```text

== Todo: 0 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 9
Unknown choice: 9

== Todo: 0 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 1
title> 
A task needs a title.

== Todo: 0 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 1
title>    
A task needs a title.

== Todo: 0 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 1
title> Test
priority 1-3> high
Priority must be 1, 2 or 3.

== Todo: 0 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 1
title> Test
priority 1-3> 5
Priority must be 1, 2 or 3.

== Todo: 0 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 3
id> x
No task with that id.

== Todo: 0 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 3
id> 42
No task with that id.

== Todo: 0 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 4
id> 7
No task with that id.

== Todo: 0 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 7
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
Write report
2
1
Water plants
3
1
Fix bug
1
1
Call bank
2
2
3
2
5
6
4
3
2
7
```

Screen:

```text

== Todo: 0 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 1
title> Write report
priority 1-3> 2
Added #1.

== Todo: 1 task, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 1
title> Water plants
priority 1-3> 3
Added #2.

== Todo: 2 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 1
title> Fix bug
priority 1-3> 1
Added #3.

== Todo: 3 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 1
title> Call bank
priority 1-3> 2
Added #4.

== Todo: 4 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 2

-- all tasks: 4 --
  [ ] #1 Write report (medium)
  [ ] #2 Water plants (low)
  [ ] #3 Fix bug (high)
  [ ] #4 Call bank (medium)

== Todo: 4 tasks, 0 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 3
id> 2
Done: #2.

== Todo: 4 tasks, 1 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 5

-- by priority: 4 --
  [ ] #3 Fix bug (high)
  [ ] #1 Write report (medium)
  [ ] #4 Call bank (medium)
  [x] #2 Water plants (low)

== Todo: 4 tasks, 1 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 6

-- pending: 3 --
  [ ] #1 Write report (medium)
  [ ] #3 Fix bug (high)
  [ ] #4 Call bank (medium)

== Todo: 4 tasks, 1 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 4
id> 3
Deleted #3.

== Todo: 3 tasks, 1 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 2

-- all tasks: 3 --
  [ ] #1 Write report (medium)
  [x] #2 Water plants (low)
  [ ] #4 Call bank (medium)

== Todo: 3 tasks, 1 done ==
1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit
choice> 7
Bye.
```

## Modules

### main.eml

```eml
# P001 todo list: add tasks with a priority, mark them done, delete them, and
# list them in the order added, by priority, or only the ones still pending.
# The tasks live while the program runs; the first twenty projects do not
# save to files.
import tasks
import view

def ask_number(prompt):
    # The whole number typed, or None if what was typed is not one.
    try:
        return int(input(prompt))
    except ValueError:
        return None

[] => todo
1 => next_id
True => running
while running:
    view.menu(len(todo), tasks.count_done(todo))
    input("choice> ") => choice
    if choice == "1":
        input("title> ") => title
        if tasks.blank(title):
            "A task needs a title." ^0
        else:
            ask_number("priority 1-3> ") => p
            if p == None or p < 1 or p > 3:
                "Priority must be 1, 2 or 3." ^0
            else:
                tasks.add(todo, next_id, title, p) => todo
                ("Added #" + str(next_id) + ".") ^0
                next_id + 1 => next_id
    elif choice == "2":
        view.show("all tasks", todo)
    elif choice == "3":
        ask_number("id> ") => n
        if n != None and tasks.mark_done(todo, n):
            ("Done: #" + str(n) + ".") ^0
        else:
            "No task with that id." ^0
    elif choice == "4":
        ask_number("id> ") => n
        if n != None and tasks.find(todo, n) != None:
            tasks.remove(todo, n) => todo
            ("Deleted #" + str(n) + ".") ^0
        else:
            "No task with that id." ^0
    elif choice == "5":
        view.show("by priority", tasks.by_priority(todo))
    elif choice == "6":
        view.show("pending", tasks.pending(todo))
    elif choice == "7":
        False => running
    else:
        ("Unknown choice: " + choice) ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import tasks
import view

def ask_number(prompt):
    try:
        return int(input(prompt))
    except ValueError:
        return None

todo = []
next_id = 1
running = True
while running:
    view.menu(len(todo), tasks.count_done(todo))
    choice = input("choice> ")
    if choice == "1":
        title = input("title> ")
        if tasks.blank(title):
            print("A task needs a title.")
        else:
            p = ask_number("priority 1-3> ")
            if p == None or p < 1 or p > 3:
                print("Priority must be 1, 2 or 3.")
            else:
                todo = tasks.add(todo, next_id, title, p)
                print("Added #" + str(next_id) + ".")
                next_id = next_id + 1
    elif choice == "2":
        view.show("all tasks", todo)
    elif choice == "3":
        n = ask_number("id> ")
        if n != None and tasks.mark_done(todo, n):
            print("Done: #" + str(n) + ".")
        else:
            print("No task with that id.")
    elif choice == "4":
        n = ask_number("id> ")
        if n != None and tasks.find(todo, n) != None:
            todo = tasks.remove(todo, n)
            print("Deleted #" + str(n) + ".")
        else:
            print("No task with that id.")
    elif choice == "5":
        view.show("by priority", tasks.by_priority(todo))
    elif choice == "6":
        view.show("pending", tasks.pending(todo))
    elif choice == "7":
        running = False
    else:
        print("Unknown choice: " + choice)
print("Bye.")
```

### tasks.eml

```eml
# P001 todo list - the task rules, kept apart from the screen so they can be
# read on their own. A task is a list [id, title, priority, done]: priority 1
# is high, 2 medium, 3 low; done is True or False. Ids are never reused.

def add(tasks, task_id, title, priority):
    # A new list with the task at the end; the old list is left as it was.
    return tasks + [[task_id, title, priority, False]]

def blank(text):
    # True if the text is empty or only spaces - such a title is refused.
    for c in text:
        if c != " ":
            return False
    return True

def find(tasks, task_id):
    # The task with this id, or None.
    for t in tasks:
        if t[0] == task_id:
            return t
    return None

def mark_done(tasks, task_id):
    # Marks the task done in place. True if there was such a task.
    find(tasks, task_id) => t
    if t == None:
        return False
    True => t[3]
    return True

def remove(tasks, task_id):
    # A new list without the task with this id.
    return [t for t in tasks if t[0] != task_id]

def by_priority(tasks):
    # A new list, highest priority first. Tasks of equal priority keep the
    # order they were added in: an insertion sort that places each task after
    # every task already there with the same or a higher priority.
    [] => ordered
    for t in tasks:
        0 => i
        while i < len(ordered) and ordered[i][2] <= t[2]:
            i + 1 => i
        ordered[0:i] + [t] + ordered[i:len(ordered)] => ordered
    return ordered

def pending(tasks):
    # Only the tasks not done yet, in the order they were added.
    return [t for t in tasks if not t[3]]

def count_done(tasks):
    0 => n
    for t in tasks:
        if t[3]:
            n + 1 => n
    return n
```

Python projection of tasks.eml:

```python
def add(tasks, task_id, title, priority):
    return tasks + [[task_id, title, priority, False]]

def blank(text):
    for c in text:
        if c != " ":
            return False
    return True

def find(tasks, task_id):
    for t in tasks:
        if t[0] == task_id:
            return t
    return None

def mark_done(tasks, task_id):
    t = find(tasks, task_id)
    if t == None:
        return False
    t[3] = True
    return True

def remove(tasks, task_id):
    return [t for t in tasks if t[0] != task_id]

def by_priority(tasks):
    ordered = []
    for t in tasks:
        i = 0
        while i < len(ordered) and ordered[i][2] <= t[2]:
            i = i + 1
        ordered = ordered[0:i] + [t] + ordered[i:len(ordered)]
    return ordered

def pending(tasks):
    return [t for t in tasks if not t[3]]

def count_done(tasks):
    n = 0
    for t in tasks:
        if t[3]:
            n = n + 1
    return n
```

### view.eml

```eml
# P001 todo list - what the screen shows: the menu and the task lists.

def priority_name(p):
    if p == 1:
        return "high"
    if p == 2:
        return "medium"
    return "low"

def line(t):
    # One task as a row: [x] #2 Water plants (low)
    "[ ]" => mark
    if t[3]:
        "[x]" => mark
    return mark + " #" + str(t[0]) + " " + t[1] + " (" + priority_name(t[2]) + ")"

def show(title, tasks):
    "" ^0
    ("-- " + title + ": " + str(len(tasks)) + " --") ^0
    if len(tasks) == 0:
        "  (nothing here)" ^0
    for t in tasks:
        ("  " + line(t)) ^0

def count(n, word):
    # "1 task", "2 tasks"
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def menu(total, done):
    "" ^0
    ("== Todo: " + count(total, "task") + ", " + str(done) + " done ==") ^0
    "1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit" ^0
```

Python projection of view.eml:

```python
def priority_name(p):
    if p == 1:
        return "high"
    if p == 2:
        return "medium"
    return "low"

def line(t):
    mark = "[ ]"
    if t[3]:
        mark = "[x]"
    return mark + " #" + str(t[0]) + " " + t[1] + " (" + priority_name(t[2]) + ")"

def show(title, tasks):
    print("")
    print("-- " + title + ": " + str(len(tasks)) + " --")
    if len(tasks) == 0:
        print("  (nothing here)")
    for t in tasks:
        print("  " + line(t))

def count(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def menu(total, done):
    print("")
    print("== Todo: " + count(total, "task") + ", " + str(done) + " done ==")
    print("1) add  2) list  3) done  4) delete  5) by priority  6) pending  7) quit")
```

## README

# P001 - Todo list

Add tasks with a priority (1 high, 2 medium, 3 low), mark them done, delete
them, and list them three ways: in the order added, by priority, or only the
ones still pending. A text menu; the tasks live while the program runs.

- `main.eml` - the menu loop and the checks on what is typed
- `tasks.eml` - the task rules: add, find, mark done, remove, sort by priority
  (stable: equal priorities keep the order they were added in), pending
- `view.eml` - the menu and the task lists as they appear on screen

Sessions: `sessions/basic.in` adds four tasks - two of them medium, so the
priority list shows that equal priorities keep the order they were added in -
and uses every list; `sessions/bad-input.in` types an unknown choice, an empty
title and one of spaces only, a priority that is a word and one out of range,
and ids that are not there.

Built on the verified corpus cases `task-priority-bucketer` (tasks with a
1-10 priority split into groups) and `simple-queue` (a list kept in arrival
order).
