td is a personal Rust task manager for an Obsidian vault. It reads and writes a Markdown TODO.md file using the obsidian-tasks checkbox format, while adding stable task IDs, recurrence handling, backlog promotion/demotion, and an optional terminal UI.
The binary is named td. The Cargo package is currently named td-cli.
- Add tasks to active tasks or backlog.
- List actionable, upcoming, backlog, completed, or all active tasks.
- Mark tasks as done and archive them automatically.
- Create the next occurrence automatically for recurring tasks.
- Promote backlog items to active tasks.
- Demote active tasks back to the backlog.
- Match tasks by stable ID prefix or by description substring.
- Migrate legacy task files by assigning missing IDs.
- Merge a legacy
## Latersection into## Backlog. - Move checked tasks from active sections into
## Archive. - Use a terminal UI with keyboard navigation and inline add/edit forms.
- Ship an agent skill at
skills/td/SKILL.mdso agents use the CLI instead of editingTODO.mddirectly.
td expects a Markdown file with these sections:
# TODO
## Tasks
- [ ] Active task
## Backlog
- [ ] Deferred task
## Archive
- [x] Completed task ✅ 2026-05-08The canonical sections are:
| Section | Meaning |
|---|---|
## Tasks |
Active tasks. Default list mode shows only actionable tasks here. |
## Backlog |
Deferred tasks and migrated Later entries. |
## Archive |
Completed tasks. |
Each task is a Markdown checkbox:
- [ ] Pay nursery #project/admin #home ⏫ 🔁 every month on the 1st 📅 2026-05-01 <!-- tid:a1b2c3d4 -->Supported task metadata:
| Metadata | Format | Notes |
|---|---|---|
| Stable ID | <!-- tid:a1b2c3d4 --> |
8 hexadecimal characters. Hidden by Obsidian preview. |
| Priority | ⏫, 🔼, 🔽 |
High, medium, low. |
| Due date | 📅 YYYY-MM-DD |
Parsed as a date without timezone. |
| Recurrence | 🔁 every month |
Stored in canonical text form. |
| Project | #project/<slug> |
Created from --project; spaces become -, value is lowercased. |
| Tags | #tag |
Any non-project tags. |
| Completion date | ✅ YYYY-MM-DD |
Added when a task is archived. |
This repository exposes a Nix flake with a default package, app, and development shell. Use this path when Nix is available; it avoids manual archive downloads and gives reproducible builds.
Prerequisites:
- Nix with flakes enabled.
- Network access to GitHub when installing from the public repository.
Run from the local checkout:
nix run . -- --help
nix run . -- listBuild from the local checkout:
nix build
./result/bin/td --helpInstall from the local checkout:
nix profile install .
td --helpInstall from the public GitHub repository, using the current default branch:
nix profile install github:arkan/td
td --helpInstall a pinned release tag:
nix profile install github:arkan/td/v0.1.1
td --helpRun without installing from the public GitHub repository:
nix run github:arkan/td -- --helpRun a pinned release tag without installing:
nix run github:arkan/td/v0.1.1 -- --helpOpen a development shell with Rust tooling:
nix developDownload the archive for your platform from the latest GitHub Release, verify its checksum, extract it, and put the td binary on your PATH.
Prerequisites:
- GitHub CLI (
gh). tar.sha256sumon Linux, orshasumon macOS.
Choose the target for your platform:
| Platform | Target |
|---|---|
| Linux x86_64 | x86_64-unknown-linux-gnu |
| macOS Intel | x86_64-apple-darwin |
| macOS Apple Silicon | aarch64-apple-darwin |
| Windows x86_64 | x86_64-pc-windows-msvc |
Linux example:
version="v0.1.1"
target="x86_64-unknown-linux-gnu"
gh release download "${version}" \
--repo arkan/td \
--pattern "td-${version}-${target}.tar.gz*" \
--dir .
if command -v sha256sum >/dev/null 2>&1; then
sha256sum -c "td-${version}-${target}.tar.gz.sha256"
else
shasum -a 256 -c "td-${version}-${target}.tar.gz.sha256"
fi
tar -xzf "td-${version}-${target}.tar.gz"
install -m 755 "td-${version}-${target}/td" "$HOME/.local/bin/td"
td --helpmacOS Intel uses the same commands with this target:
target="x86_64-apple-darwin"macOS Apple Silicon uses the same commands with this target:
target="aarch64-apple-darwin"Windows PowerShell:
$version = "v0.1.1"
$target = "x86_64-pc-windows-msvc"
gh release download $version `
--repo arkan/td `
--pattern "td-$version-$target.tar.gz*" `
--dir .
$archive = "td-$version-$target.tar.gz"
$expected = (Get-Content "$archive.sha256").Split(' ')[0].ToUpperInvariant()
$actual = (Get-FileHash $archive -Algorithm SHA256).Hash
if ($actual -ne $expected) { throw "Checksum mismatch" }
tar -xzf $archive
# Add the extracted td-$version-$target directory to PATH, then run:
td.exe --helpRequirements:
- Rust stable.
- Cargo.
Install the binary from this repository:
cargo install --path .
td --helpRun without installing:
cargo run -- list
cargo run -- add "Pay nursery" -p high -d 2026-05-01td resolves the target todo file in this order:
TODO_FILEenvironment variable.~/.config/td/config.tomlwith atodo_filefield.- Fallback:
<home>/TODO.md, where<home>comes fromHOMEorUSERPROFILE.
Use a one-off file:
TODO_FILE=/path/to/TODO.md td listPersistent config:
# ~/.config/td/config.toml
todo_file = "/absolute/path/to/TODO.md"If the target file does not exist, commands that load it will fail with a read error. Create the file with the expected sections before using td.
td <command> [options]Commands:
| Command | Purpose |
|---|---|
td add <description> [flags] |
Add a task to ## Tasks or ## Backlog. |
td list [mode] |
List tasks. |
td done <id-or-text> |
Mark an active or backlog task as completed. |
td promote <id-or-text> |
Move a backlog task to active tasks. |
td demote <id-or-text> |
Move an active task to backlog. |
td tui |
Open the interactive terminal UI. |
td add <description> [flags]Flags:
| Flag | Values | Meaning |
|---|---|---|
-p, --priority |
high, medium, low, h, m, l |
Set priority. |
-d, --due |
YYYY-MM-DD |
Set due date. |
-r, --recur |
recurrence pattern | Set recurrence. |
-j, --project |
text | Set #project/<slug>. |
-t, --tag |
text | Add a tag. Can be repeated. |
-b, --backlog |
boolean | Add to ## Backlog instead of ## Tasks. |
Examples:
td add "Pay nursery"
td add "Pay nursery" -p high -d 2026-05-01 -r monthly:1 -j admin -t home
td add "Read Rust release notes" --backlog -t readingCLI --recur accepts compact recurrence patterns:
| Input | Stored as |
|---|---|
daily |
🔁 every day |
weekly |
🔁 every week |
weekly:monday |
🔁 every Monday |
monthly |
🔁 every month |
monthly:1 |
🔁 every month on the 1st |
monthly:28 |
🔁 every month on the 28th |
yearly |
🔁 every year |
3-days |
🔁 every 3 days |
2-weeks |
🔁 every 2 weeks |
6-months |
🔁 every 6 months |
1-year |
🔁 every 1 years |
When a recurring task is completed:
- The current task is moved to
## Archivewith✅ YYYY-MM-DD. - A new unchecked task is created in the original section.
- The new due date is computed from the previous due date when present, otherwise from today.
- The next due date is always strictly in the future.
- The new task receives a new stable ID.
td list
td list --upcoming
td list --all
td list --backlog
td list --completedModes are mutually exclusive:
| Mode | Scope | Filter |
|---|---|---|
| default | ## Tasks |
Due today/past or no due date. |
--upcoming |
## Tasks |
Future due dates only. |
--all |
## Tasks |
All active tasks. |
--backlog |
## Backlog |
All backlog items. |
--completed |
## Archive |
All archived items. |
Non-completed lists are sorted by priority descending, then due date descending. Completed tasks are sorted by completion date descending.
The output is a terminal table with priority, ID, description, and metadata. Set COLUMNS if you need deterministic table width in tests or scripts:
COLUMNS=100 td list --alltd done <id-or-text>The matcher first tries an ID prefix, then falls back to a case-insensitive substring search in task descriptions.
Examples:
td done a1b2
td done nurseryIf several tasks match, td exits with an error and prints the matching IDs. Use a longer ID prefix or a more specific text query.
td done searches active tasks and backlog tasks. If the ID belongs to an archived task, it reports that the task is already archived.
td is intentionally conservative when it would rewrite the todo file:
- Every command takes an exclusive lock file under
<home>/.config/td/locks/for the full read-modify-write cycle. td tuikeeps the same lock for the whole interactive session.- Unknown Markdown sections or non-task content after the first known todo section are treated as unsupported content.
- If unsupported content is present,
tdrefuses to save instead of silently dropping it.
This means the file may contain frontmatter and introductory text before ## Tasks, but mutable content should stay inside the canonical task sections as task lines.
td promote <id-or-text>Moves a task from ## Backlog to ## Tasks.
td list --backlog
td promote bbbbtd demote <id-or-text>Moves a task from ## Tasks to ## Backlog.
td list --all
td demote aaaaLaunch the TUI:
td tuiTabs:
To-do: active actionable tasks.Upcoming: active future tasks.Backlog: deferred tasks.Archive: completed tasks.
Keybindings:
| Key | Action |
|---|---|
q, Esc |
Quit when no popup is open. |
↑, ↓ |
Move selection. |
Tab, → |
Next tab. |
← |
Previous tab. |
a |
Open add popup. Adds to backlog when the current tab is Backlog. |
e |
Open edit popup for selected task. |
d |
Mark selected task as done, except in Archive. |
p |
Promote selected backlog task. |
m |
Demote selected active/upcoming task. |
k |
Move selected task up in its section. |
j |
Move selected task down in its section. |
o |
Open URLs found in the selected task description. |
Popup keybindings:
| Key | Action |
|---|---|
Esc |
Close popup without saving. |
Enter |
Save add/edit form. |
Tab |
Move to next field. |
↑, ↓ |
Cycle priority when the priority field is selected. |
| Text input | Insert text in text fields. |
Backspace |
Delete last character in text fields. |
The URL opener uses open on macOS and xdg-open on Linux. URL opening is not supported on Windows in the current implementation.
On load, td may rewrite the file to keep it canonical:
- Tasks without
<!-- tid:xxxxxxxx -->receive a generated ID. - Duplicate IDs are regenerated for later duplicates.
## Lateris treated as legacy backlog and rendered back as## Backlog.- Checked tasks found in
## Tasksor## Backlogare moved to## Archive. - Archived tasks without
✅ YYYY-MM-DDreceive today's completion date.
Writes are atomic: td writes to TODO.md.tmp, then renames it over the original file.
If td reports an unsupported-content rewrite refusal, move the reported Markdown lines outside the todo file or convert them to canonical task lines before retrying.
The repository includes the agent skill at:
skills/td/SKILL.md
The skill tells agents to use td for task mutations instead of editing the Markdown todo file directly. The skill assumes that the td binary is already available on PATH, so install the binary first with Nix, a GitHub Release download, or cargo install --path ..
Install the skill from the public GitHub repository with npx skills add:
npx --yes skills add https://github.com/arkan/td.git --skill td --agent claude-code -yInstall the skill from a local checkout:
npx --yes skills add . --skill td --agent claude-code -yInstall it globally instead of project-locally:
npx --yes skills add https://github.com/arkan/td.git --skill td --agent claude-code --global -yInstall it for every supported agent:
npx --yes skills add https://github.com/arkan/td.git --skill td --agent '*' -yList the skills exposed by this repository without installing:
npx --yes skills add . --listAfter installation, keep td on PATH and ensure TODO_FILE or ~/.config/td/config.toml points to the intended Markdown file.
Useful commands:
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-targets --all-features --locked
cargo run -- listWith a temporary todo file:
tmpdir="$(mktemp -d)"
cat > "${tmpdir}/TODO.md" <<'EOF'
# TODO
## Tasks
## Backlog
## Archive
EOF
TODO_FILE="${tmpdir}/TODO.md" cargo run -- add "Smoke task" -p high
TODO_FILE="${tmpdir}/TODO.md" cargo run -- list --all
TODO_FILE="${tmpdir}/TODO.md" cargo run -- done SmokeMakefile shortcuts:
make build
make run
make install
make cleanThe CI workflow runs on pushes and pull requests to main on Ubuntu, macOS, and Windows:
cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo test --all-targets --all-features --locked
- Update
Cargo.tomlversion when needed. - Run the local verification commands.
- Commit the release changes.
- Push
main. - Wait for CI to pass on
main. - Create and publish a GitHub Release whose tag matches
Cargo.tomlexactly (0.1.1→v0.1.1).
Example:
version="v0.1.1"
cargo_version="$(cargo pkgid | sed 's/.*@//')"
test "${version}" = "v${cargo_version}"
git tag "${version}"
git push origin main "${version}"
gh run list --repo arkan/td --branch main --workflow CI --limit 1
gh release create "${version}" --verify-tag --title "${version}" --notes "Release ${version}"The Release workflow starts when the GitHub Release is published. Before packaging, it verifies formatting, Clippy, tests, and that the release tag matches the Cargo package version. It then builds, smoke-tests, and uploads these assets:
| Target | Runner | Asset |
|---|---|---|
x86_64-unknown-linux-gnu |
ubuntu-22.04 |
td-<tag>-x86_64-unknown-linux-gnu.tar.gz |
x86_64-apple-darwin |
macos-15-intel |
td-<tag>-x86_64-apple-darwin.tar.gz |
aarch64-apple-darwin |
macos-15 |
td-<tag>-aarch64-apple-darwin.tar.gz |
x86_64-pc-windows-msvc |
windows-2022 |
td-<tag>-x86_64-pc-windows-msvc.tar.gz |
Each archive contains the binary, README.md, and LICENSE. Each archive also has a sibling .sha256 checksum asset.
The configured todo file does not exist or is not readable. Check TODO_FILE, ~/.config/td/config.toml, and file permissions.
The config file exists but is not valid TOML. Keep it minimal:
todo_file = "/absolute/path/to/TODO.md"Dates must use YYYY-MM-DD.
For CLI input, use compact recurrence patterns such as daily, weekly:monday, monthly:1, or 3-days.
Your query matches multiple tasks. Run td list --all or td list --backlog, then use a longer ID prefix.
td detected Markdown that it cannot preserve safely during a rewrite. It refuses to save to avoid data loss. Move the reported lines outside TODO.md or convert them to canonical checkbox task lines under ## Tasks, ## Backlog, or ## Archive.
Check the GitHub Actions Release workflow logs. The workflow only runs for published releases, not for draft releases that remain unpublished.
Use a modern terminal with Unicode support. The table and TUI use box drawing characters and emoji metadata.