Tutorial
Add tasks
Validate a title and save a new task between runs.
Start with chapter 5's program and restored sample tasks.txt. Reading a file is useful, but a task tool should let you add tasks without opening an editor.
Validate and append a task
Add InvalidTitle to the TaskError declaration, then add this function after load_tasks:
def add_task(tasks: List[Task], title: String) -> Void ! TaskError
if ! valid_title(title)
throw TaskError.InvalidTitle
end
@append(tasks, Task(title: title, done: false))
endThe function accepts the existing list and a title. Calling @append changes that list, including the value seen by the caller. Tea's lists have shared reference semantics. A const binding prevents rebinding the variable; it doesn't freeze the list's contents.
We validate before appending. Empty titles, titles made only of spaces or tabs, and titles containing line breaks are rejected. Other characters, including | and brackets, are ordinary title text. Leading and trailing spaces are preserved.
Encode and save the list
Add encode_tasks and save_tasks before add_task:
def encode_tasks(tasks: List[Task]) -> String
var content = ""
for task in tasks
const marker = if(task.done) "[x]" else "[ ]"
content = content + `${marker} ${task.title}\n`
end
content
end
def save_tasks(file: String, tasks: List[Task]) -> Void ! fs.FsError
fs.write_file_atomic(file, encode_tasks(tasks))
endEncoding reverses the decoding process: each task becomes a marker, a title, and a newline. The number shown by list is deliberately absent from the saved record.
fs.write_file_atomic stages the new contents before replacing the file. That avoids leaving a partially written list if writing fails. It doesn't coordinate multiple writers; this tutorial's tool is intended for one local user at a time.
Add the command branch
Inside run, before the final usage() call, add the add branch from the complete checkpoint. It checks for exactly two arguments: the command and one title. It loads the existing tasks, calls add_task, saves the result, and prints confirmation only after saving succeeds.
Update help to mention add "title", and add a TaskError.InvalidTitle case to the outer catch block. That case prints tasks: title must be nonempty and on one line and exits with status 2.
Save your first new task
Quote a title containing spaces so your shell passes it as one argument:
tea tasks.tea add "Write a test"Added task 4.Now run a separate process:
tea tasks.tea list1. [ ] Learn Tea
2. [ ] Build a CLI
4. [ ] Write a testTask 3 is still complete, so the default view hides it. Open tasks.txt to see the new fourth line. If you started with no data file, adding a task creates it and assigns number 1.
Complete checkpoint
Keep your existing tasks.txt, including the new task. This source replaces the previous tasks.tea:
Complete tasks.tea for this chapter
tasks.tea
use args from "std.args"
use fs from "std.fs"
use string from "std.string"
struct Task {
title: String
done: Bool
}
def format_task(task: Task, number: Int) -> String
const marker = if(task.done) "[x]" else "[ ]"
`${number}. ${marker} ${task.title}`
end
def show_tasks(tasks: List[Task], show_all: Bool) -> Void
if @len(tasks) == 0
@println("No tasks yet.")
return
end
var number = 1
var shown = 0
for task in tasks
if show_all ||!task.done
@println(format_task(task, number))
shown = shown + 1
end
number = number + 1
end
if shown == 0
@println("No open tasks.")
end
end
error TaskError {
InvalidData(line: Int)
InvalidTitle
}
def valid_title(title: String) -> Bool
const without_spaces = string.replace(title, " ", "")
const without_tabs = string.replace(without_spaces, "\t", "")
@len(without_tabs) > 0 &&!string.contains(title, "\n") &&!string.contains(title, "\r")
end
def decode_tasks(content: String) -> List[Task] ! TaskError
var tasks: List[Task] = []
var number = 1
for line in string.lines(string.replace(content, "\r\n", "\n"))
if @len(line) < 5 ||!(string.starts_with(line, "[ ] ") || string.starts_with(line, "[x] "))
throw TaskError.InvalidData(number)
end
const title = line[4..@len(line)]
if ! valid_title(title)
throw TaskError.InvalidData(number)
end
@append(tasks, Task(title: title, done: string.starts_with(line, "[x] ")))
number = number + 1
end
tasks
end
def load_tasks(file: String) -> List[Task] ! { fs.FsError, TaskError }
if ! fs.exists(file)
return[]
end
decode_tasks(fs.read_file(file))
end
def encode_tasks(tasks: List[Task]) -> String
var content = ""
for task in tasks
const marker = if(task.done) "[x]" else "[ ]"
content = content + `${marker} ${task.title}\n`
end
content
end
def save_tasks(file: String, tasks: List[Task]) -> Void ! fs.FsError
fs.write_file_atomic(file, encode_tasks(tasks))
end
def add_task(tasks: List[Task], title: String) -> Void ! TaskError
if ! valid_title(title)
throw TaskError.InvalidTitle
end
@append(tasks, Task(title: title, done: false))
end
def help() -> Void
@println("Usage: tasks [list [--all] | add \"title\" | --help]")
@println("Tasks are numbered from 1. Listing shows open tasks; --all includes completed tasks.")
end
def usage() -> Void
@eprintln("tasks: invalid arguments; run tasks --help for usage")
@exit(2)
end
def run() -> Void ! { fs.FsError, TaskError }
var argv = args.all()
if @len(argv) == 0
argv = ["list"]
end
if argv[0] == "--help" && @len(argv) == 1
help()
return
end
if argv[0] == "list"
if @len(argv) > 2 ||(@len(argv) == 2 && argv[1] != "--all")
usage()
return
end
const tasks = load_tasks("tasks.txt")
show_tasks(tasks, @len(argv) == 2)
return
end
if argv[0] == "add" && @len(argv) == 2
const tasks = load_tasks("tasks.txt")
add_task(tasks, argv[1])
save_tasks("tasks.txt", tasks)
@println(`Added task ${@len(tasks)}.`)
return
end
usage()
end
try run() catch err
case is fs.FsError.OperationFailed
@eprintln(`tasks: cannot ${err.operation} '${err.path}': ${err.message}`)
@exit(1)
case is TaskError.InvalidData
@eprintln(`tasks: invalid data on line ${err.line}; expected [ ] title or [x] title`)
@exit(1)
case is TaskError.InvalidTitle
@eprintln("tasks: title must be nonempty and on one line")
@exit(2)
case _
@eprintln("tasks: unexpected error")
@exit(1)
endTry it yourself
Try adding an empty title with tea tasks.tea add "", then list all tasks. Did the rejected operation change the file?
Expected result
The command reports that the title must be nonempty and on one line. No task is appended, and the saved file stays unchanged. The next successful addition will still use the next available position.
You now have persistent state: one process changes data that another process can read. Next, we'll update an existing task.
Read more: filesystem writes, collection semantics.