A menu-bar task ledger for macOS. One JSON file on disk, written by two authors: you (typing or speaking) and the assistant (editing the same file).
TaskDeck/
tasks.json the ledger — the only file that is authoritative
LEDGER.md generated human view, never edited by hand
bin/tdk command-line interface (built from source)
build/TaskDeck.app the menu-bar app (built from source)
app/ Swift package (TaskDeckKit logic, TaskDeckApp shell, tdk, taskd)
android/ native Android client (Java, no Gradle)
seed/ sample fixture showing the ledger format
Clone it where the app expects to live, then build:
git clone https://github.com/intertermux-code/taskdeck ~/TaskDeck
cd ~/TaskDeck/app && ../build/make-app.sh # builds CLI tools + app bundle, ~30s
open ~/TaskDeck/build/TaskDeck.app # lives in the menu barThe menu-bar item shows a number: tasks due today, or ↑N when something is
overdue. Click it for the panel. ⌥⌘N opens capture from any app. ⌥⌘L opens
the full ledger window.
make-app.sh signs ad-hoc by default. To stop macOS re-asking for microphone
access on every rebuild, sign with a real identity and install to a stable path:
IDENTITY="Apple Development" ~/TaskDeck/build/make-app.sh --install
open ~/Applications/TaskDeck.appGatekeeper reports a locally built bundle as rejected. That is expected and
does not stop open from working; it only matters if you download the app.
Your real ledger lives at ~/TaskDeck/tasks.json and is git-ignored — cloning
starts you with an empty store. seed/sample.json shows the file format if
you want to import existing tasks.
Type one line, press Return. A mic button (or ⌘M) dictates into the same field
using on-device speech recognition; the first use asks for Microphone and Speech
Recognition permission.
⌘? lists every key the app accepts. Deleting a line offers ⌘Z for ten
seconds, so a stray ⌫ costs nothing and there is no confirmation dialog in the
way of a capture.
Quick-add syntax, parsed locally with no network round-trip:
| you write | becomes |
|---|---|
today tonight tomorrow next week end of month |
due date |
in 3 days in 2 hours on friday next monday dec 5 2026-12-25 the 15th |
due date |
at 5pm @6p 17:00 noon morning evening |
clock time on that day |
every day every weekday every week every other week every month every monday |
recurrence |
!urgent !high !low urgent high priority |
priority |
#safari #api-limits |
tags |
someday |
parked, no date |
text -- detail or note: detail |
note body |
Dates without a time mean "sometime that day" and land at 09:00, which is a
marker rather than a deadline — the row stays in Today until midnight, not
Overdue. Dictation debris (add a note, remind me to, um) is stripped.
Prose with no keywords is filed untouched.
Bands, top to bottom: Overdue · Today · Upcoming · Open · Someday · Done.
Open is undated working material; Someday is only what you parked. Done rows
drop out after 7 days.
tasks.json is written atomically and reconciled on every save: before writing,
the app re-reads the file and merges per task by updatedAt, with deletions
tracked as tombstones. So an assistant edit landing while your panel is open
cannot clobber your task, and yours cannot clobber theirs. The app polls the file
and reloads within about a second of an outside change.
Hand-editing the JSON is safe. Hand-editing LEDGER.md is not — it is
regenerated from the store.
~/TaskDeck/bin/tdk add "call the office tomorrow at 5pm #client !high"
~/TaskDeck/bin/tdk ls # grouped ledger
~/TaskDeck/bin/tdk ls --json
~/TaskDeck/bin/tdk show <id-prefix> # id, unique prefix, or unique title match
~/TaskDeck/bin/tdk done <id-prefix>
~/TaskDeck/bin/tdk note <id-prefix> "blocked on X" --author you
~/TaskDeck/bin/tdk snooze <id-prefix> 3
~/TaskDeck/bin/tdk due <id-prefix> 2026-10-01
~/TaskDeck/bin/tdk priority <id-prefix> urgent
~/TaskDeck/bin/tdk tag <id-prefix> errands
~/TaskDeck/bin/tdk title <id-prefix> "new wording"
~/TaskDeck/bin/tdk rm <id-prefix>
~/TaskDeck/bin/tdk mirror # rewrite LEDGER.md (+ $TASKDECK_MIRROR)
~/TaskDeck/bin/tdk doctor # store path, parse status, counts, writabilityEvery mutation rewrites the generated views, so they cannot drift. Exit codes:
0 ok, 1 bad usage or unwritable store, 2 an ambiguous or unknown task
reference — it lists near matches rather than guessing.
TASKDECK_HOME relocates the whole store (used by the tests).
TASKDECK_MIRROR names a second markdown view to keep in sync. A file that is
not ours is copied aside before the first mirror overwrites it.
taskd is a small LAN server over the same tasks.json. Start it on the Mac:
~/TaskDeck/bin/taskd --lan # port 8770; --port N to change itFirst run writes a 32-character token to ~/TaskDeck/sync.token (mode 0600) —
print it and type it into the phone's sync settings along with this Mac's
address, e.g. 192.168.1.10:8770. Without --lan it binds loopback only, which
is the safe default.
There are only two endpoints: GET /health and POST /sync. Sync is a
whole-ledger exchange — the phone posts its entire copy, the Mac merges it with
the same per-task last-write-wins and tombstone rules the local store uses,
saves, and returns the merged result, which the phone then merges into its own
copy. Both sides converge without any per-record operations, so order of syncs
does not matter and an unchanged re-push is a no-op. The phone keeps working
fully offline; the Mac must be awake and on the same network for a sync to land.
This is a LAN trust model: token-gated, cleartext, no certificates. Do not expose it beyond your network.
android/ holds a second client: native Java, no Kotlin, no Gradle, no
AndroidX — built straight with aapt2, javac, d8, zipalign and apksigner.
cd ~/TaskDeck/android && ./build-apk.sh # -> build/TaskDeck-0.1.0.apk
./build-apk.sh --install # adb install + launch, phone attachedIt keeps its own copy of the ledger in the app's private storage and works
entirely offline. Sync posts the whole ledger to taskd, which merges it into
the Mac's store and returns the merged result; the phone merges that into its
own copy. Both sides converge because the merge is per-task last-write-wins
with tombstoned deletes, so sync order never matters and re-pushing an
unchanged ledger is a no-op.
First run on the phone: menu → settings → host <your-mac-lan-ip>, port
8770, token from cat ~/TaskDeck/sync.token → Sync. Start the server first
with ~/TaskDeck/bin/taskd --lan.
Capture on the phone uses the keyboard's own mic key or the mic button, which hands off to the system speech dialog — no microphone permission to grant. The same quick-add grammar is parsed on-device by a Java port of the Mac parser, validated against the Swift original case by case rather than by eye.
Useful harnesses, both runnable on a plain JVM:
java -cp <classes> StorageSelfTest <tasks.json> # 216 assertions
java -cp <classes> QuickAddSelfTest # 453 assertions
java -cp <classes> SyncRoundTrip <phoneDir> <macDir> <port> # live convergenceA scheduled automation can read the ledger each morning, list what is still
open, and record what you report back. It should mutate through tdk, never by
editing the markdown.
cd ~/TaskDeck/app
swift build && swift test # 63 tests: parser matrix, merge races, renderers
../build/make-app.shVerification hooks, all opt-in through the environment:
TASKDECK_OPEN=ledger # or panel raise a surface at launch, no clicking needed
TASKDECK_SHOT=/tmp/x.png render the front window to a PNG (no Screen Recording grant)
TASKDECK_SHOT_EXIT=1 quit once written
TASKDECK_APPEARANCE=light|dark override the system appearanceMic does nothing. The capture bar prints the exact permission that is missing. Menu → Microphone settings opens the right pane; grant it and relaunch.
⌥⌘N does nothing. Another app already holds that combination. The menu
shows whether registration succeeded; the menu-bar click still works.
Launch at login fails. It needs a signed bundle at a stable path — run
make-app.sh --install and start it from ~/Applications.
tasks.json will not parse. The store refuses to overwrite it and moves the
bad file to tasks.corrupt-<timestamp>.json. Fix or restore by hand; nothing is
discarded automatically.
Accounts, multi-device write sync, push notifications, calendar import, nested subtasks, drag-to-reorder. The hosted read-only mirror is a separate step.
MIT — see LICENSE.