Skip to content
intertermux-codePublic

About

Local-first task ledger for macOS, Android and CLI — plain-JSON store, human+AI co-editing without clobbering, natural-language quick-add, recurrence. No cloud, no account.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TaskDeck

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

Setup

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 bar

The 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.app

Gatekeeper 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.

Capture

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.

Two writers, one file

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.

tdk

~/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, writability

Every 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.

Syncing with the phone

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 it

First 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

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 attached

It 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 convergence

Assistant integration

A 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.

Development

cd ~/TaskDeck/app
swift build && swift test              # 63 tests: parser matrix, merge races, renderers
../build/make-app.sh

Verification 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 appearance

Troubleshooting

Mic 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.

Not included

Accounts, multi-device write sync, push notifications, calendar import, nested subtasks, drag-to-reorder. The hosted read-only mirror is a separate step.

License

MIT — see LICENSE.

About

Local-first task ledger for macOS, Android and CLI — plain-JSON store, human+AI co-editing without clobbering, natural-language quick-add, recurrence. No cloud, no account.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages