Rewrite things-cli in Rust #3

Merged
kglitchy merged 26 commits from kglitchy/rust-rewrite into main 2026-09-30 22:33:56 +00:00 AGit
Owner

Rewrites things-cli in Rust. The Go implementation is removed; the Rust crate lives at the repo root.

Commits

  • d5a6a8e — Rust port with the same things binary, command surface, config location, Things Cloud auth/sync, JSON output, and state replay (including Task7 and legacy Tombstone handling).
  • d28786e — replaces hand-rolled Go/Cobra emulation with mainstream crates: clap (CLI + help), clap_complete (shell completion), figment (config: flag > env > file > default), bs58 (Things IDs), and serde_json / chrono / percent-encoding in place of Go formatting reimplementations.
  • 70e44b3 — pins clap_complete to =4.6.11, since its unstable-dynamic feature can change between minor releases.
  • 04aa70b — deletes the Go code, moves the crate to the root (git mv, history preserved), switches the Makefile to cargo, moves the MIT notice to the root LICENSE (identical to the two Go LICENSE files), removes the empty package.json / package-lock.json.
  • 17ddd68 — replaces the Go-vs-Rust parity harness with end-to-end tests (tests/cloud.rs, tests/config.rs) that run the real binary against a fake Things Cloud in a temp home.
  • 18ba4c1 — README, skills/things-cli/SKILL.md, and docs/ updated for Rust.
  • 3a81fdf — fix (pre-existing Go bug): explicit null clears for sr, dd, sp, ato (and checklist sp) are applied in history order during replay. Previously a batch post-pass re-applied every sr: null, so a task created unscheduled and scheduled within the same sync batch lost its date and vanished from every view. The state cache version was bumped (now 3), so existing caches are rebuilt once by a full sync.
  • 56c4a57 — fix (pre-existing Go bug): Today (and Anytime) include deferred st=2 tasks whose start date has arrived, as the Things app does. Previously they were hidden from every view until a Things client flipped them to st=1. Protocol doc view mapping updated.
  • e4a0bd9 — docs: version-2 cache, Today/replay fixes, corrected escape examples, no hard-coded checkout path.

Review fixes (all bugs inherited from the Go version; each has a regression test):

  • af4cb5e — moving a task keeps it in one project or heading (--project clears the heading, heading lookup is scoped to --project).
  • d1b66cd — overdue deadlines show in Today; tasks with a future start stay out of Anytime.
  • c6def61 — repeating templates are detected by their repeat rule (rr), even before the first instance, and are kept out of Today/Anytime consistently.
  • 673b3a2 — headings resolve to their project in views and --project filters; tasks in closed projects' headings and in Someday projects are hidden from Anytime.
  • f1b92f2 — environment settings are taken verbatim (only THINGS_DEBUG is parsed), so THINGS_CLI_CACHE=0123 stays 0123.
  • ffddc56 — config is only read by commands that use the service, so a broken config file no longer blocks auth or completion.
  • 2d055ec — edit requires a change flag even when global flags like --json are given.
  • e69bb0d — logbook --page 0 is rejected.
  • 64b8ad6 — docs describe the OS cache directory (~/Library/Caches on macOS) and the fixed ~/.config/things-cli config directory.

Cleanup:

  • 758f676 — dead code removed, along with the blanket dead_code allowance.
  • 047c677 — Go state-cache compatibility dropped.
  • 033619f — task lists sort by a well-defined total order.
  • b7ca6d4, 2f1920e — trivial wrappers inlined; validation wording shared between the CLI and the service.
  • 89630d1, e33f5f0 — error messages and comments no longer mimic Go.
  • e27328c — behavior changes recorded; the design doc is marked historical.

Verification

From the repo root:

  • cargo fmt --check, cargo build --release, cargo clippy --all-targets -- -D warnings, make test: clean
  • cargo test: 136 passed; the live Things Cloud test stays ignored and opt-in and was not run
  • Before the Go code was removed, the parity harness passed against the Go binary: 41 command paths with an identical surface, 88/88 CLI cases, 180/180 data cases (outputs, exit codes, requests, commits, cache), 21/21 config cases.

The new end-to-end tests cover:

  • sync paging and request order
  • view output over a fixture history
  • commit order and payloads for create, edit, complete, cancel, delete, restore, checklist and purge
  • falling back to a full sync on a bad cache
  • auth errors
  • config precedence and YAML/JSON/TOML config files

Coverage that only compared output with the Go binary byte for byte is gone.

Changes for existing users

See "Changes from the Go version" in README.md. The main ones:

  • clap help layout and usage-error wording; exit codes unchanged.
  • Boolean flags no longer take a value (--json=false is a usage error).
  • Previously installed shell completion scripts must be regenerated with things completion <shell>.
  • --json no longer escapes <, >, &; parsed values are identical.
  • Config keys are case-sensitive; THINGS_DEBUG rejects values other than true/false/1/0; explicit --config must be YAML, JSON, or TOML.
  • The state cache uses a new format (version 3); Go-era and earlier Rust caches are discarded and rebuilt by one full sync, as is any unreadable cache.

Real-account check (read-only)

things today with the fixed build lists all five tasks the Things app shows in Today; the pre-fix build (and the Go binary) missed one or more. After the review fixes, Today is unchanged.

Not done

  • The live Things Cloud test hasn't been run against a real account.
  • Installed locally from this branch (make install) on the Linux host; the first run rebuilt the cache and Today matched the Things app. Not merged.
Rewrites things-cli in Rust. The Go implementation is removed; the Rust crate lives at the repo root. ## Commits - `d5a6a8e` — Rust port with the same `things` binary, command surface, config location, Things Cloud auth/sync, JSON output, and state replay (including Task7 and legacy Tombstone handling). - `d28786e` — replaces hand-rolled Go/Cobra emulation with mainstream crates: `clap` (CLI + help), `clap_complete` (shell completion), `figment` (config: flag > env > file > default), `bs58` (Things IDs), and `serde_json` / `chrono` / `percent-encoding` in place of Go formatting reimplementations. - `70e44b3` — pins `clap_complete` to `=4.6.11`, since its `unstable-dynamic` feature can change between minor releases. - `04aa70b` — deletes the Go code, moves the crate to the root (`git mv`, history preserved), switches the Makefile to cargo, moves the MIT notice to the root `LICENSE` (identical to the two Go `LICENSE` files), removes the empty `package.json` / `package-lock.json`. - `17ddd68` — replaces the Go-vs-Rust parity harness with end-to-end tests (`tests/cloud.rs`, `tests/config.rs`) that run the real binary against a fake Things Cloud in a temp home. - `18ba4c1` — README, `skills/things-cli/SKILL.md`, and `docs/` updated for Rust. - `3a81fdf` — fix (pre-existing Go bug): explicit `null` clears for `sr`, `dd`, `sp`, `ato` (and checklist `sp`) are applied in history order during replay. Previously a batch post-pass re-applied every `sr: null`, so a task created unscheduled and scheduled within the same sync batch lost its date and vanished from every view. The state cache version was bumped (now 3), so existing caches are rebuilt once by a full sync. - `56c4a57` — fix (pre-existing Go bug): Today (and Anytime) include deferred `st=2` tasks whose start date has arrived, as the Things app does. Previously they were hidden from every view until a Things client flipped them to `st=1`. Protocol doc view mapping updated. - `e4a0bd9` — docs: version-2 cache, Today/replay fixes, corrected escape examples, no hard-coded checkout path. Review fixes (all bugs inherited from the Go version; each has a regression test): - `af4cb5e` — moving a task keeps it in one project or heading (`--project` clears the heading, heading lookup is scoped to `--project`). - `d1b66cd` — overdue deadlines show in Today; tasks with a future start stay out of Anytime. - `c6def61` — repeating templates are detected by their repeat rule (`rr`), even before the first instance, and are kept out of Today/Anytime consistently. - `673b3a2` — headings resolve to their project in views and `--project` filters; tasks in closed projects' headings and in Someday projects are hidden from Anytime. - `f1b92f2` — environment settings are taken verbatim (only `THINGS_DEBUG` is parsed), so `THINGS_CLI_CACHE=0123` stays `0123`. - `ffddc56` — config is only read by commands that use the service, so a broken config file no longer blocks `auth` or `completion`. - `2d055ec` — `edit` requires a change flag even when global flags like `--json` are given. - `e69bb0d` — `logbook --page 0` is rejected. - `64b8ad6` — docs describe the OS cache directory (`~/Library/Caches` on macOS) and the fixed `~/.config/things-cli` config directory. Cleanup: - `758f676` — dead code removed, along with the blanket `dead_code` allowance. - `047c677` — Go state-cache compatibility dropped. - `033619f` — task lists sort by a well-defined total order. - `b7ca6d4`, `2f1920e` — trivial wrappers inlined; validation wording shared between the CLI and the service. - `89630d1`, `e33f5f0` — error messages and comments no longer mimic Go. - `e27328c` — behavior changes recorded; the design doc is marked historical. ## Verification From the repo root: - `cargo fmt --check`, `cargo build --release`, `cargo clippy --all-targets -- -D warnings`, `make test`: clean - `cargo test`: 136 passed; the live Things Cloud test stays ignored and opt-in and was **not** run - Before the Go code was removed, the parity harness passed against the Go binary: 41 command paths with an identical surface, 88/88 CLI cases, 180/180 data cases (outputs, exit codes, requests, commits, cache), 21/21 config cases. The new end-to-end tests cover: - sync paging and request order - view output over a fixture history - commit order and payloads for create, edit, complete, cancel, delete, restore, checklist and purge - falling back to a full sync on a bad cache - auth errors - config precedence and YAML/JSON/TOML config files Coverage that only compared output with the Go binary byte for byte is gone. ## Changes for existing users See "Changes from the Go version" in `README.md`. The main ones: - clap help layout and usage-error wording; exit codes unchanged. - Boolean flags no longer take a value (`--json=false` is a usage error). - Previously installed shell completion scripts must be regenerated with `things completion <shell>`. - `--json` no longer escapes `<`, `>`, `&`; parsed values are identical. - Config keys are case-sensitive; `THINGS_DEBUG` rejects values other than true/false/1/0; explicit `--config` must be YAML, JSON, or TOML. - The state cache uses a new format (version 3); Go-era and earlier Rust caches are discarded and rebuilt by one full sync, as is any unreadable cache. ## Real-account check (read-only) `things today` with the fixed build lists all five tasks the Things app shows in Today; the pre-fix build (and the Go binary) missed one or more. After the review fixes, Today is unchanged. ## Not done - The live Things Cloud test hasn't been run against a real account. - Installed locally from this branch (`make install`) on the Linux host; the first run rebuilt the cache and Today matched the Things app. Not merged.
Rewrite the CLI in Rust under rust/, leaving the Go implementation intact.
The crate mirrors the Go layering (cli -> things -> thingscloud) and keeps
the user-visible contract byte-compatible: Cobra-style help, pflag syntax and
error strings, the __complete protocol behind the existing completion
scripts, config/credential locations, the shared state-cache format, the
Things Cloud wire protocol (including Task7 replay and legacy Tombstone
handling), and --json output.

Port the offline test suite and the live destructive test (ignored and
gated behind THINGS_LIVE_TEST=1), and add rust/parity/, a fake Things Cloud
plus scripts that diff the Go and Rust binaries command by command.
- CLI: clap derive replaces the Cobra/pflag-compatible parser and help
  renderer (command.rs); same commands, aliases, flags, arguments, and exit
  codes (usage errors still exit 1; repeated flags keep the last value).
- Completion: clap_complete's dynamic engine replaces the Cobra __complete
  protocol and the embedded Cobra scripts; entity titles still come from the
  state cache. Installed completion scripts must be regenerated.
- Config: figment replaces the Viper emulation; same file location, env
  names, and flag > env > file > default precedence (YAML, JSON, TOML).
- gofmt.rs removed: messages use {:?}, JSON uses serde_json, timestamps use
  chrono's serde, URL escaping uses percent-encoding; go_timestamps.tsv
  fixture dropped. Base58 uses bs58; cache dir uses dirs.
- Unreadable or outdated state caches now trigger a full sync instead of
  failing; Go-written caches are still read.
- Parity harness: compares parsed JSON and semantic requests, checks the
  command/flag surface (surface_parity.py) instead of help bytes, compares
  completion candidates, and compares error text only for app-level results.
The unstable-dynamic feature can change between minor releases.
Delete main.go, go.mod/go.sum, internal/ and things/, move the crate
out of rust/ with git mv, and drive install/test/live through cargo.
The upstream MIT notice that things/ and internal/thingscloud/ carried
(identical copies) is kept as the root LICENSE. Remove the empty
package.json/package-lock.json leftovers.
rust/parity compared the Rust binary against the Go one, which no longer
exists. Port the cases that test the Rust CLI on its own into
integration tests that run the binary against a tiny_http fake Things
Cloud:

- tests/cloud.rs: paged full sync and cache reuse (request sequence,
  headers, cache file mode), views over a fixture history with Task7
  updates, deletions and tombstones, commit sequences and payloads for
  create/edit/complete/cancel/delete/restore/checklist/purge, rejected
  writes that must not commit, corrupt/foreign/ahead caches falling back
  to a full sync, auth and network errors, and dynamic completion from
  the cache.
- tests/config.rs: flag > environment > file precedence for endpoint,
  cache and debug; explicit YAML/JSON/TOML config; missing and broken
  config files; THINGS_CLIENT_VERSION; credential sources and auth
  status/clear.
Merge rust/README.md into the root README (setup via make install, Rust
API example, development and layout, crates) with a short "Changes from
the Go version" section for existing users. Switch the things-cli skill
to cargo/make, and point docs/ at the Rust module paths; protocol facts
are unchanged.
kglitchy changed title from Add Rust port of things-cli to Rewrite things-cli in Rust 2026-09-30 03:20:08 +00:00
Replay decoded sr/dd/sp/ato as plain Options, so an explicit null looked
like an omitted key; update_state then re-applied every sr:null in the
batch after the whole batch. A create with sr:null followed in the same
batch by a scheduling update ended up with no scheduled date and dropped
out of Today.

Decode these fields as double Options so a present null clears the value
at its own position in the history, and remove the post-pass. Bump the
state cache version so caches built by the old reducer are replayed.
Things.app shows an st=2 task in Today as soon as its sr arrives and only
rewrites it to st=1 later, when a client next opens. Today required st=1,
and Upcoming only covers sr from tomorrow on, so such tasks were hidden
from every view until the flip.

Today now includes pending st=1 and st=2 tasks with sr of today or
earlier (repeating templates excluded), and Anytime includes the same
tasks, matching the app. Document the current view mapping.
Document the version-2 cache (Go caches are discarded), the Today and
replay fixes, correct two garbled escape examples, and drop the
hard-coded ~/repos checkout path.
Setting a project now clears the heading and setting a heading clears
the direct project link, matching how Things stores a task under a
heading (agr set, pr empty). --clear-project also leaves the heading,
--clear-heading keeps the task in the heading's project, and a heading
is looked up in --project when both are given, for edit and create.
Today counted only deadlines due today, so an overdue deadline vanished
from every view; Things.app keeps it in Today. Anytime let any st=1 task
through even when its start date was still ahead; it now requires no
start date or one that has arrived.
A template was only recognized once an instance pointed at it through
rt, so a new repeating task looked like a plain Someday task. The
reducer now records whether rr is set, and Today and Anytime exclude
templates whatever their start state or deadline. The cache format
moves to version 3 so existing caches replay and pick up rr.
A task under a heading has an empty pr, so --project filters missed it
and tasks under a heading of a completed or trashed project stayed
visible. Visibility and the project filter now include the heading and
its project. Anytime also leaves out tasks of projects that are in
Someday or not started yet, as Things.app does.
Every environment value went through figment's value parser, so
THINGS_CLI_CACHE=0123 became "123", 1.10 became "1.1", and [a] failed.
Only THINGS_DEBUG is parsed now; the rest are strings. figment's env
feature is no longer needed.
Settings were loaded before every command except help, so a malformed
config file broke auth set/status/clear, completion, and validation
errors. They are now resolved when the service is built.
Global flags such as --json counted as an edit change, so 'edit X --json'
got past validation and failed only after loading credentials.
--page 0 was quietly treated as page 1 while negative pages were
rejected. Pages below 1 now fail validation before any network access.
The cache lives in the OS cache directory (~/Library/Caches on macOS),
not always ~/.cache. The config directory is ~/.config/things-cli on
every OS unless XDG_CONFIG_HOME is set. Also note the cache format bump
to version 3.
Drop the no-op copied(), unread response and history fields, ListOption
(callers always passed the default), is_task_kind, Note.type_tag,
to_value, TaskUpdate::scheduled/build, and the unread ListRequest.page.
Test-only helpers are compiled only for tests.
load_cache discards any cache that is not the current version, so the
null-tolerant list and map decoders and Go's zero CreationDate default
could never apply. CreationDate is now optional and the Go-cache test is
gone. Wire decoding is unchanged.
Replace the hand-written merge sort, which emulated Go's non-transitive
comparator, with sort_by over a total order: dated tasks first by start
date, then undated tasks, ties by title and UUID. Reword the comment on
null-for-empty JSON lists around script compatibility.
Call State::update and unix_float directly instead of through
update_state and unix_timestamp.
The CLI's pre-credential checks and the service's checks worded the same
failures differently (e.g. '--deadline must use YYYY-MM-DD' vs 'invalid
--deadline'). Both now use one set of messages, and the service's heading
check trims like the CLI's.
Replace 'unexpected end of JSON input', 'EOF', 'unexpected/expected
newline', PascalCase operation names, and 'exec: {:?}' with plain
messages.
Reword comments and test names that referred to Go, Cobra, pflag, or
Viper, and document the XDG location and the yml/json/toml variants in
the --config help.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kglitchy/things-cli!3
No description provided.