Search by

sugarcraft / candy-codetime

detain

Privacy-first coding-time tracker — design antecedent Rtarun3606k/TakaTime, native PHP on the SugarCraft stack.

Package info

github.com/sugarcraft/candy-codetime

Homepage

Documentation

Type:project

pkg:composer/sugarcraft/candy-codetime

Statistics

Installs: 0

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

dev-master 2026-11-04 07:32 UTC

This package is auto-updated.

Last update: 2026-11-05 01:00:28 UTC


README

candy-codetime

CandyCodetime

Formerly published as sugarcraft/sugar-tick (renamed 2026-10). Namespace changed: SugarCraft\Tick\ → SugarCraft\CodeTime\.

CI codecov Packagist Version License PHP

demo

candy-codetime — a privacy-first coding-time tracker for PHP 8.3+. Storage is local by default: one append-only JSONL file per day, with an optional embedded-SQLite backend (--backend=sqlite) for milestone tracking and indexed queries. Nothing leaves your machine — no cloud, no daemon, and no database server (SQLite is an in-process library reading a file you own). The dashboard is a SugarCraft TUI that reads whichever backend you selected.

Install

composer global require sugarcraft/candy-codetime
# the global bin dir varies by composer version and platform; ask composer:
BIN="$(composer global config bin-dir --absolute)"
"$BIN/candy-codetime" push myproj php src/Main.php 120

Or from a clone: composer install && php bin/candy-codetime.

Quick start

candy-codetime push myproj php src/Main.php 120   # record one heartbeat
candy-codetime                                    # open the dashboard
candy-codetime export csv 30                      # headless dump of 30 days

php examples/dashboard.php seeds a throwaway data directory with a pretend week and opens the dashboard, so you can see the TUI before recording anything real.

Commands

Command Arguments What it does
dashboard — Open the TUI (reads the selected backend). Default when no command is given. Without an interactive terminal it refuses with a pointer to export instead of hanging.
push <project> <language> <file> [duration] Record one heartbeat. duration is whole seconds (default 60); a non-numeric value is rejected, not silently read as 0.
milestone <add|list> [name] [description] Manage release markers. Milestones live only in SQLite, so this command forces the sqlite backend regardless of --backend; the dashboard never displays them.
export <csv|json|ics> [days] Dump heartbeats for the last N days (default 7, clamped to [1, 3660]).
gaps [days] List untracked stretches of at least 300 seconds within the last N days (default 7).
backup — Archive day files (*.jsonl) older than 30 days into <dir>-backups/. Copies only — it never deletes your data. The SQLite database is not archived: a byte copy of a live .db is a torn snapshot, so sqlite users must use sqlite3 <db> ".backup <dest>" or VACUUM INTO '<dest>' (the CLI prints this reminder when a .db is present).
help, --help, -h — Print usage.
--version, -v — Print the version.

Options (accepted before or after the command):

Option / env Effect
--backend=file|sqlite Storage for push/export/gaps/dashboard (default: file). Both forms work: --backend=sqlite and --backend sqlite. The sqlite backend needs the sqlite3 PHP extension; without it the CLI exits with a clear message instead of crashing.
CANDY_CODETIME_DIR=<path> Data directory (default $XDG_DATA_HOME/candy-codetime, falling back to ~/.local/share/candy-codetime — note that an unset HOME resolves to ./.local/share/…).

A storage failure ends any command with a one-line message on stderr and exit status 1 — never a bare PHP fatal.

Legacy SUGARTICK_* environment variables are still read (with a deprecation notice) and the old default data directory is migrated once on first launch. CLI messages follow LC_ALL/LC_MESSAGES/LANG across the 16 shipped locales.

Ignoring files

A .sugartrackignore in the current directory — or, failing that, in the tracked file's own directory (the first found wins) — holds glob patterns one per line; a push whose file matches is reported as skipped and never stored. A bare pattern (secret.log) matches by file name at any depth; a path pattern (vendor/*) matches below any vendor directory, whether the tracked path is relative or absolute.

Trust boundary: ignore files usually live inside tracked repositories, so their patterns are untrusted input to the tracking tool. A wildcard-only pattern (a bare *) matches every file name and silently zeroes that project's entire recorded time. When the loaded file carries such a pattern, the skip is no longer a quiet stdout line: push reports it on stderr with the ignore file's path, its pattern count, and the affected project — then still exits 0, because the opt-out is honored, not refused. To undo a repo-supplied plant, delete or narrow the file the notice names; to shadow a directory-level plant, run push from a working directory whose own .sugartrackignore (checked first) holds only honest patterns.

Data layout

$CANDY_CODETIME_DIR/
  2026-10-23.jsonl      one appendable line per heartbeat
  candy-codetime.db     created only by --backend=sqlite
<dir>-backups/          created by `backup`

Each line of a day file is one JSON object: {"time": <unix seconds>, "project": <string>, "language": <string>, "file": <absolute path>, "duration": <seconds>, "tags": <array>}. tags is always empty for now — no CLI command sets one yet, and readers should ignore unknown keys.

Day files are keyed by the machine's local calendar date, so history written while crossing time zones stays filed under the date it was tracked. Nothing is ever deleted: beats accumulate until you remove them by hand or archive them with backup (which copies and never trims).

Fixing a bad entry

There is no edit command, and that is deliberate — the store is plain files. With the file backend, close the dashboard (or press r afterwards), fix or drop the offending JSON line in the day file, and reload. With the sqlite backend, open the database with sqlite3 and fix the row in heartbeats.

Privacy

The data directory and every file in it are owner-private: directories are 0700, day files and the database are 0600, created under a narrowed umask so they are never world-readable for an instant — and a directory or file left loose by an older version is tightened on the next write (if that fails, the write aborts rather than storing your paths in the open — and every print/echo of a stored project or file name goes through a control-byte strip, so a doctored store cannot paint escape sequences into your terminal). The backup archive under <dir>-backups/ is held to the same contract: it is created 0700 and every copied day file is tightened to 0600, because an archive of those files is the same path map one directory over. Heartbeats carry absolute file paths and project names; treat the directory accordingly.

Colour is driven by the detected ColorProfile for the terminal: a profile below 256-colour (which NO_COLOR=1, CLICOLOR=0 and TERM=dumb all land on) renders the dashboard without any tints, and a resize or capability report mid-session updates it live.

Keys (dashboard)

Key Action
← Move the window back one day
→ Move it forward one day
r Reload from disk
q / Esc Quit
Ctrl+C Quit

The two ranking panes render side by side from 62 columns up (two 30-cell frames plus a gap) and stack vertically below that. Every dashboard line is clipped to the terminal width — frames fill it exactly, nothing wraps into garbage on narrow shells, and days that cannot be drawn are counted ("+N earlier") rather than silently dropped.

Architecture

File Role
Heartbeat Value object — one activity sample (project, language, file, duration, tags).
Store JSONL-backed reader/appender; implements HeartbeatReader; enforces the private modes above.
Storage\SqliteBackend Optional indexed storage; implements HeartbeatReader; also owns milestones.
Storage\HeartbeatReader The read contract (loadRange/invalidate) both storages satisfy — what lets every subcommand honour --backend uniformly.
Stats Pure fold — totals per project / language + per-day timeline buckets.
Dashboard SugarCraft Model — window shifts one day per press (←/→), reload (r), quit; reads through a HeartbeatReader.
Renderer View — header + ranking panes + Sparkline timeline (SugarCharts); colour-ladder aware (honours the detected ColorProfile).
Report\GapsReport Pure gap detection over sorted heartbeats (threshold 300 s).
Backup\AutoBackup Copy-only archiver; throws loudly on any failed copy.
Ignore\SugarTrackIgnore .sugartrackignore matcher used by push.
Milestone Value object for sqlite markers.
Export\CsvExporter / JsonExporter / IcalExporter export formats behind ExporterInterface (the tabular pair shares TabularExporterInterface).
Support\Version Single-source version constant.
Lang i18n facade (16 locales under lang/).

The Store is intentionally append-only at the file level, so editor plugins can >> echo a JSON line directly without coordinating with the dashboard — the trade-off is honest: raw >> redirection does not take the advisory lock push uses, so a hand-echoed line could theoretically interleave; plugin integrations should flock or just call push. The dashboard re-reads on launch and on r; it has no clock subscription, so an open panel never refreshes by itself.

Shared foundations

candy-codetime uses sugar-async for cooperative cancellation: Store::append() polls an optional CancellationToken immediately before writing (PHP cannot interrupt a blocking write mid-flight). The CLI never arms a token; the seam exists for embedding hosts that want abortable writers.

Test

composer install
vendor/bin/phpunit

Credits & inspiration

Design antecedent: Rtarun3606k/TakaTime; SugarCraft is developed as a native PHP project.