Skip to content

Releases: littekge/LazyPlanner

Lazyplanner v1.5.0

Choose a tag to compare

@littekge littekge released this 27 Jul 03:48

LazyPlanner v1.5.0 — polishing & auditing

The final planned release of the feature line. v1.5.0 adds no major features by design — it is a
consolidation release: an exhaustive spec-vs-code audit, a UI/keymap consistency sweep, six deep
hardening passes, and a full rework of how recurring items handle time zones.

133 commits since v1.4.0 — 6 features, ~60 fixes, 4 performance fixes, 1 owner-directed revert.


⚠️ Security fix

A malicious or compromised CalDAV server could steal your app password. A server-supplied resource
href of the form //other.host/x.ics is a protocol-relative URL, not a path, so it replaced the
configured endpoint's host. An authenticated PUT then delivered your Basic-auth app password and the
full calendar body
to that host, returned success, and the app marked the resource as cleanly synced
while your real server never saw the write. DELETE and MKCALENDAR were affected the same way, and a
poisoned href persisted locally so every later write re-targeted that host.

LazyPlanner now refuses any href that resolves off the configured endpoint's origin, and drops such an
href at ingest so it is never stored.

Note for reverse-proxy setups: if your server advertises hrefs on a different origin than the URL
in your config (e.g. it returns a public hostname while you connect to an internal one), those requests
now fail with an error naming both origins. Point url at the advertised name to fix it.


Highlights

Recurring items now keep the right day and time

A recurring item's anchor is written as local time with a TZID plus a generated VTIMEZONE, instead
of UTC. RFC 5545 evaluates a rule's BY* parts in the anchor's own zone, so a UTC anchor made locally
authored rules fire on the wrong day. Fixed:

  • Opening a recurring item's edit form and pressing Save without touching Repeat no longer rewrites
    the rule (it used to shift the series by a day and drop per-occurrence edits).
  • Creating a "Weekly on Tue" event no longer produces a series that fires every Monday.
  • A weekly 8pm meeting stays at 8pm across a DST change instead of drifting to 7pm.
  • A recurring item no longer loses the day its zone's DST change starts on. This affected 11 zones
    (Havana, Santiago, Coyhaique, Punta Arenas, Azores, Scoresbysund, São Paulo, Asunción and others) at
    every frequency, and a recurring task would skip that day permanently and push the gap to the server.
  • "Ends on date D" now includes D, for both all-day and timed items.
  • FREQ=YEARLY;BYMONTHDAY=n is read correctly (it fires 12×/year, and a grab used to make the event
    vanish).

One time zone, everywhere

The app now resolves one zone at startup and uses it for display, authoring and decoding. Previously
nine decode sites used the system zone while the UI used the app's resolved zone; when those disagreed an
all-day event could render on two days and "today" could land on the wrong day. $TZ is now honoured in
every form Go itself accepts (TZ=, TZ=:Zone, and absolute zoneinfo paths).

UI

  • Agenda board is clickable — single click selects, double click edits the row under the cursor.
  • +/- accordion now collapses the Detail pane along with the overview column.
  • Task form has a Location field. Previously a task's location was silently erased by a quick-set
    (sp/sd) or by opening the form and saving without changing anything.
  • sd on a recurring task re-anchors its repeat rule — moving a Monday-pinned weekly task to
    Wednesday no longer snaps back to Monday on the next completion.
  • j/k work in the Conflicts list and the account picker; q closes the account and colour pickers.
  • The bottom hint bar is mode-adaptive, and u (undo) keeps your place in the calendar instead of
    kicking you back to day navigation.

Sync reliability

  • A deleted item no longer wedges forever when it was already gone on the server — the delete used to
    retry every sync and disable the full-download short-circuit for that calendar permanently.
  • Numerous lost-update fixes across concurrent edit-vs-sync paths, conflict resolution, undo of a synced
    delete, and cross-list subtree moves.
  • A corrupt sync sidecar salvages its state instead of discarding it (which used to resurrect deletions).

Performance

Four hot paths de-quadratified — calendar redraw budgeting, LayoutDay overlap packing, time-grid
selection range, and month-grid navigation — so large calendars stay responsive, including on a
Raspberry Pi.


Quality work behind this release

  • 962 documented behaviours verified against the code; 21 divergences found and fixed.
  • 593 key×context cells checked across every mode, view and modal; 24 divergences fixed.
  • Hardening passes 19–24, each fix landing repro-first with a permanent regression test.
  • Full test suite runs green across ten time-zone configurations, plus fuzzing, -race, and
    display-stress sweeps.

Known issues

  1. A timed recurring task due inside a DST "spring forward" gap drifts by an hour, permanently.
    Needs a timed recurring task whose due time falls in the missing hour (02:00–02:59 in most of the
    US/EU/AU, 00:00–00:59 in Havana/Santiago/Azores, 01:00–01:59 in the UK). All-day tasks and all events
    are unaffected, as are non-DST zones. Pre-existing, not new in this release.
  2. Day bucketing is not gap-safe in the 11 midnight-transition zones — day boundaries can be an hour
    off on the transition day, and "tomorrow" in quick-add can resolve to today.
  3. Changing your system time zone requires restarting the app. The zone is resolved once at startup;
    :config reload does not refresh it. Switching accounts refreshes the display zone but not the decode
    zone, so restart after a zone change rather than switching accounts.

Full detail: docs/audit/COVERAGE.md and docs/audit/passes/PASS-24.md.


Install

Download the binary for your platform, verify it, and put it on your PATH:

sha256sum -c sha256sums.txt --ignore-missing
chmod +x lazyplanner_linux_amd64
sudo mv lazyplanner_linux_amd64 /usr/local/bin/lazyplanner
lazyplanner version

Raspberry Pi builds are included (arm64, armv7, armv6).

First run writes a commented starter config to ~/.config/lazyplanner/config.toml — fill in one
[[account]] block with your CalDAV URL, username and a NextCloud app password, then run
lazyplanner again.

Upgrading from v1.4.0: no config or cache migration needed. Existing recurring items keep their
current behaviour until their rule is next edited, at which point they gain the corrected zone anchoring.

Full Changelog: v1.4.0...v1.5.0

Lazyplanner v1.4.0

Choose a tag to compare

@littekge littekge released this 24 Jul 14:32

v1.4.0 — SELECT mode: multi-select & bulk operations

LazyPlanner gains a vim-style multi-select layer. Press V to anchor a
selection, extend it with the ordinary movement keys, then apply one bulk
action to the whole range.

New

SELECT mode (V)

  • Works in three contexts: the task tree, the un-drilled calendar
    (a range of days), and a drilled day's item list.
  • The range is a contiguous anchor→cursor span, extended with normal motion:
    hjkl/arrows, gg/G, f/b, and vim counts (10j).
  • The status bar shows a SELECT badge and a live N selected count; the
    selected range is highlighted in the view (reverse-video rows in the tree,
    outlined day boxes on the calendar).
  • Modes nest vim-style: DRILL → SELECT → GRAB, with each Esc unwinding
    exactly one level.

Bulk operations (one keystroke, one undo step)

  • Spacecomplete every selected task (children before parents, so a
    folder and its last open subtask can be finished in one pass; recurring
    todos advance one occurrence, exactly like single-item Space).
  • ddelete the selection, with a single confirmation naming the real
    resource count (a selected subtask of a selected parent is absorbed, never
    counted or deleted twice).
  • y / Ycut / copy multiple subtrees to the clipboard (task tree
    only), then p/P pastes them together — including across lists.
  • mbulk grab: shift every selected item by whole days (h/l) or
    weeks (j/k) as one uniform date-shift. Esc returns to SELECT with the
    range intact so a grab can be retried.
  • Every bulk action is all-or-nothing: a mid-batch failure rolls back
    cleanly, and a full success is a single u undo step.
  • Items an action can't apply to (read-only calendars, recurring events,
    undated tasks, folders with open subtasks, already-done tasks) are skipped
    and counted — the flash reports exactly what was skipped and why, and
    confirmation counts always match what is actually acted on.

Fixed

  • The always-visible help bar now shows the active grab's controls and
    granularity for the whole grab (single grab: ±hour/±day per context;
    bulk grab: ±day/±week) instead of the ordinary controls line — or, mid
    bulk-grab, a stale SELECT · hjkl extend line that no longer matched what
    the keys did.
  • A malformed RELATED-TO parent cycle in hand-edited/foreign .ics data
    could hang the UI during a bulk delete's ancestor walk — now cycle-guarded,
    consistent with the "malformed iCalendar is never fatal" invariant.
  • V pressed while an overview panel (Calendars/Tasks) is focused flashes a
    hint instead of silently anchoring a selection that motion could never
    extend.
  • A bare 0 in SELECT no longer leaks through to the week/day hour-zoom
    reset (a 0 continuing a count, e.g. 10j, still works); modified motion
    keys (Ctrl-arrows) can no longer sneak a pane resize in mid-select.

Docs

  • README.md keybindings table gains V (and a previously missing m grab
    row); the ?/:help cheat sheet gains a Select section.

Full gate green (go test ./..., go vet, staticcheck, race + display-stress suites) on every commit.

Full Changelog: v1.3.0...v1.4.0

Lazyplanner v1.3.0

Choose a tag to compare

@littekge littekge released this 24 Jul 00:20

v1.3.0 — Recurrence editing

The headline of v1.3.0 is a full recurrence-rule editor. Until now, the only way to make something repeat was the quick-add parser, and an existing repeat rule couldn't be changed in-app at all. Now every repeat rule can be created and edited from the full form, with Google-Calendar-style expressiveness — plus a safer delete flow, vim-style form navigation, and a batch of recurrence bug fixes.

✨ New: the Repeat field

The full create/edit form (open with e, or the Shift-create keys) now has a Repeat field:

  • Presets built from the item's own date — None, Daily, Weekly on <weekday>, Monthly on day <n>, Yearly on <month day>.
  • Custom… for anything more specific (see below).
  • The Detail pane shows a recurring item's rule in plain English (e.g. “every 2 weeks on Tue, Thu until Dec 12, 2026”).

Picking a rule on a one-off item makes it repeat; Repeat → None clears it.

Custom… sub-form

A compact, focused dialog for any in-vocabulary rule — it shows only the fields relevant to what you pick:

  • Every N days / weeks / months / years
  • Weekly: choose the weekdays on a single toggle strip (/ to move, Space to toggle) — e.g. Tuesday + Thursday
  • Monthly: by day-of-month or by the nth / last weekday (derived from the start date, so the rule can never contradict its own anchor)
  • Yearly on the start date
  • Ends: never · on a date · after N times

Editing scope

Editing, deleting, or grabbing a recurring item still prompts for this occurrence · this & future · all, and rule changes now honor it: all rewrites the master, this & future gives the split-off series its own rule, this occurrence stays a one-off.

Rules it doesn't understand are preserved

An imported rule outside the editable vocabulary shows as “Custom rule (kept)” and is preserved byte-for-byte unless you deliberately overwrite it. An unchanged rule is never rewritten.

🛡️ New: safer calendar/list deletes

Deleting a calendar or task list is not undoable, so it no longer uses the one-button confirm. You now type the collection's name to confirm — a mismatch keeps the dialog open. (Deleting individual events/tasks is unchanged; those are still undoable with u.)

⌨️ Improved: form navigation & dialog styling

  • Full-screen forms use the app's NORMAL/DRILL model instead of Tab-only movement: in NORMAL, j/k/arrows step between fields and buttons and Enter acts on the highlighted one; drilling into a field lets you type, with Esc stepping back out. The mode badge shows which you're in.
  • All dialogs — forms, confirmations, and pickers — now share one look: an accent rounded border, a contextual title, and the terminal's own background.
  • Selection highlighting is legible on both light and dark terminals everywhere, dropdowns included.

🐛 Fixes

  • Recurring events no longer disappear when grabbed. Moving a repeating event by a day (grab mode, “all” scope) now shifts the whole series to the new day instead of leaving it stranded on the old one and vanishing from the calendar. An opaque imported rule blocks the day-move with a hint rather than risk corrupting it.
  • Repeat/priority and other form dropdowns render their selected row legibly (no more white-on-white).
  • Arrow keys now steer an open form dropdown correctly.
  • Opening the Custom… sub-form over the edit form no longer strands the form open-but-unreachable when it closes.

Full Changelog: v1.2.0...v1.3.0

Lazyplanner v1.2.0

Choose a tag to compare

@littekge littekge released this 23 Jul 15:45

LazyPlanner v1.2.0 — Smarter Quick-Add

This release makes the one-line quick-add input far more capable. When you create an event or task, you can now express times, dates, recurrence, and location inline — and the parser tells you when something looks like a typo instead of silently burying it in the title.

Quick-add stays deliberately conservative: a token is only claimed when it clearly matches a documented form, and anything unrecognized becomes part of the title. Tokens are whitespace-delimited, so bob@example.com and task!5 remain inert title text.

✨ New

  • Relative datesnext <weekday> (the next one, +7 days), next week, next month, and in N days/weeks/months, on top of the existing today/tomorrow/fri/jul 20/7/20/2026-07-20 forms. Month math clamps correctly (Jan 31 + 1 month → Feb 28/29, never spilling into March).
  • Time ranges5-6pm, 14:00-15:30, and friends. A right-side am/pm distributes to a bare left half (5-6pm = 5pm–6pm), and an end at or before the start crosses midnight (11pm-1am). Events take the range's end (or the usual 1-hour default); a task's due is the range's start. A bare number is never read as a time.
  • Simple recurrencedaily/weekly/monthly/yearly, every day/week/month/year, every <weekday> (weekly on that day), and every <month> <day> (yearly on that date). This is the first way to create a recurring item entirely in-app. With no explicit date typed, the recurrence anchors the start/due itself (every mon → the next Monday); an explicit date always wins.
  • Location@cafeteria or @"room 204" for multi-word values, set on both events and tasks. Tasks now carry a Location and show it in the Detail pane.
  • Obvious-typo warnings with keep-open re-prompt — a mistyped priority (!hgh), an unclosed @"…, a fuzzy follower (next tuedsay, in 3 dayz), or an impossible time/date (25:00, 5-6xm, 2026-07-40) keeps the quick-add input open showing the warning and creates nothing. Resubmit the identical text to accept it as-is. Warnings fire only on an unmistakable intent anchor — plausible prose like My Event!!!!!, email bob@example.com, 24/7, and http://x.com stay silent.

🎁 Free riders

:goto and the quick-set-due sd command both run through the same parser, so they gained the entire relative-date family with no extra work.

📝 Notes

  • The full edit form still does not rewrite an existing recurrence rule — that's coming in v1.3.0 (recurrence-rule UI). Quick-add is currently the only in-app way to create a recurring item.
  • Full grammar is documented in :help (new Quick-add tokens section) and the README.

Verification

All six build steps landed repro-first with a green full gate (go test ./..., go vet, staticcheck, go build). The iCalendar/quick-add fuzz targets were extended with the new grammar plus an invariant that a warning only ever fires alongside a real intent anchor (~1M execs clean).

Full Changelog: v1.1.0...v1.2.0

Lazyplanner v1.1.0

Choose a tag to compare

@littekge littekge released this 22 Jul 15:30

LazyPlanner v1.1.0 — Account Switching

LazyPlanner now supports multiple CalDAV accounts, switchable in-app without a restart.

⚠️ Breaking change: config schema

The single [server] section is removed. Accounts are now [[account]] blocks, each with a required unique name:

[[account]]
name = "personal"
url = "https://cloud.example.com/remote.php/dav"
username = "you"
password_command = "bw get password lazyplanner"

Upgrading: rename your [server] section to [[account]] and add a name. A config still containing [server] refuses to start with a clear migration message — nothing is silently ignored. Your existing local cache is reused as-is; no data migration needed.

New: multi-account support

  • Several configured accounts, one active at a time — no merged view; each account's data stays fully separate on disk.
  • :account <name> switches directly; bare :account opens a picker listing your accounts with the active one marked.
  • Switching cleanly winds down the current account (pending edits are flushed to the server, best-effort and time-bounded) and reopens the new one — no restart, and no state can leak between accounts.
  • The status bar shows the active account whenever more than one is configured.
  • The last-active account is remembered across launches (in global.json under the data dir). A removed or renamed account falls back to the first configured block — you can never be stranded.
  • :config reload picks up account changes live: an added or renamed account appears in the picker immediately (the active account's connection still can't be hot-swapped — use :account or restart).
  • Zero configured accounts remains a valid fully-offline mode over the local cache.

Fixes & hardening

Hardening Pass 18 audited the new multi-account surface plus a deep re-sweep of the sync core; every finding is fixed with a regression test:

  • A delete landing during an in-flight background push is no longer lost — previously the completing push could silently resurrect the just-deleted item, permanently undoing the deletion. Now the deletion is honored and propagates to the server on the next sync.
  • Startup can no longer hang on a pathological config file — a deeply nested TOML structure caused quadratic decode time; nesting depth is now bounded with a clear error.
  • The :account picker highlight was illegible (white-on-white) on default-background terminals — now the same theme-adaptive reverse-video highlight as every other list.

Compatibility

  • Full backward compatibility for all calendar data — the local cache format, sync behavior, and account-id derivation are unchanged.
  • Prebuilt binaries: Linux amd64 and Raspberry Pi (arm64/armv7/armv6), plus a Windows build.

Full Changelog: v1.0.2...v1.1.0

LazyPlanner v1.0.2

Choose a tag to compare

@littekge littekge released this 21 Jul 03:28

A bug-fix release addressing two issues in calendar rendering and background sync. No config changes or migration needed.

Bug fixes

  • Multi-day timed events now render correctly in the month and week/day views.
    An event with a set start and end time that spans several days (e.g. 11:00 am Jul 23 → 5:00 pm Jul 26) was displayed wrong:

    • Month view repeated the start time on every day of the span. It now shows the start time on the first day, the title alone on the days in between, and the end time (prefixed ) on the final day.
    • Week / day view only drew the event on its first day and dropped it thereafter. It now appears on every day it covers, as a per-day block clipped to each column — from the start time down on the first day, filling the whole column on the days in between, and down to the end time on the last day.
  • The automatic background sync no longer interrupts editing.
    The debounced push that runs a few seconds after a change could fire while you had a create/edit form open, silently discarding what you were typing. The automatic sync triggers (the debounced push and the periodic sync) are now deferred while a form is open and resume as soon as you close it. Manual sync (r / :sync) and the conflict-safety guarantees are unchanged.

Full changelog: v1.0.1...v1.0.2

Lazyplanner v1.0.1

Choose a tag to compare

@littekge littekge released this 20 Jul 20:30

LazyPlanner v1.0.1

A hotfix release: one user-facing bug fix on top of v1.0.0 — and the first
release to ship pre-built binaries, so you can download and run without a
Go toolchain.

🐛 Bug fixes

  • A background sync no longer resets your place. Previously every sync — and
    with periodic/debounced sync, that's constant — snapped the task-tree
    highlight back to the first task, and kicked you out of a drilled-in calendar
    day back to day navigation. Your task selection and your calendar day-drill
    now survive a sync. (The agenda view was already unaffected.)

📦 Downloads

Statically-linked, single-file binaries are attached below — no runtime
dependencies:

  • Linuxlazyplanner_linux_amd64
  • Raspberry Pilazyplanner_linux_arm64 (64-bit Pi OS · Pi 3/4/5, Zero 2 W),
    lazyplanner_linux_armv7 (32-bit Pi OS · Pi 2/3/4), `la
    (Pi 1 / Pi Zero / Zero W)
  • Windowslazyplanner_windows_amd64.exe
  • macOSlazyplanner_darwin_arm64 (Apple Silicon), lazyplanner_darwin_amd64 (Intel)

Download the file for your platform, make it executable, and put it on your PATH:

chmod +x lazyplanner_linux_amd64
sudo install -m0755 lazyplanner_linux_amd64 /usr/local/bin/lazyplanner
lazyplanner            # first run writes a starter config

Verify your download against the attached sha256sums.txt:

sha256sum -c sha256sums.txt --ignore-missing

▎ The macOS and Windows binaries are unsigned, so Gatekeep
▎ warn on first launch. Building from source (make build) also works as before.

Full changelog: https://github.com/littekge/LazyPlanner/compare/v1.0.0...v1.0.1

Lazyplanner v1.0.0

Choose a tag to compare

@littekge littekge released this 19 Jul 03:22

The first stable release. LazyPlanner is a keyboard-first terminal TUI (lazygit-style) for managing your calendar and todos, with offline-first two-way CalDAV sync — built for NextCloud, written in Go.

Highlights

  • Two-way CalDAV sync — offline-first: the app opens instantly from a local cache and works without network, syncing changes both ways in the background or on demand. ETag-based and conflict-safe: it never silently overwrites — a true conflict keeps both versions and flags it.
  • Deep subtask hierarchies — arbitrary-depth nesting rendered as a collapsible tree and navigated like a file explorer; a "folder" is just a task with children.
  • Calendar views — month grid plus week/day hourly time-grids, showing events and dated tasks; drill into a day for 2D spatial navigation.
  • Recurring events & tasks — full "this occurrence / this and future / all" editing for events, and NextCloud-style advancing for repeating todos.
  • Grab mode, quick-add, yank/paste, undo — vim-flavored single-key actions and chords, a smart-parsed quick-add line, and a : command mode.
  • A well-behaved CalDAV citizen — never drops or mangles iCal properties it doesn't understand (the "iron rule"), detects and respects read-only calendars, and renders exact truecolor calendar colors that match NextCloud.

Platforms

  • Linux is the primary target, including a Raspberry Pi dedicated-terminal / kiosk setup (arm64 / armv7 / armv6 cross-builds via make cross).
  • Windows is a secondary compatibility build.

Install

Requires Go 1.26.4+ (dependencies are vendored, so no network is needed to build):

git clone https://github.com/littekge/LazyPlanner
cd LazyPlanner
make build        # or: go build -o lazyplanner ./cmd/lazyplanner
./lazyplanner     # first run writes a starter config; fill in [server] and rerun

You'll need a NextCloud CalDAV server and a NextCloud app password (Settings → Security). See the README (https://github.com/littekge/LazyPlanner#readme) for configuration, keybindings,
and the Raspberry Pi kiosk guide.

Quality

Ships after an extensive hardening effort — 17 adversarial audit passes over p-loss, and concurrency, each finding fixed repro-first with a permanentregression test (see docs/audit/).

License: MIT

Full Changelog: https://github.com/littekge/LazyPlanner/commits/v1.0.0