Markdown import/export + opt-in note encryption#103
Conversation
Adds a Sync section to settings with server URL, username, password, Test Connection and Sync Now actions. New sync module implements a minimal CalDAV client (PROPFIND principal/home, REPORT VTODOs, PUT) and a sync engine that pulls remote VTODO calendars as lists and syncs tasks both ways using last-modified. Tested against the build; runtime sync needs a CalDAV server (e.g. Stalwart) to validate end-to-end.
Stalwart (and others) return <calendar-data> as CDATA so the iCal payload survives XML escaping. quick-xml fires that as Event::CData, which the multistatus parser ignored, causing fetch_todos to return zero items and sync to appear no-op. Listen for CData and accumulate text chunks across events.
- LocalStorage::update_task now bumps last_modified_date_time so the sync engine sees local edits as newer than remote. - Add LocalStorage::replace_task that preserves LMD; sync engine uses it on pull to avoid ping-pong. - Pages emit Output::Mutated on save-class events (add/complete/ delete/title submit/expand/sub-task ops; details: title/favorite/ priority/due-date). Keystroke-grade writes (TitleUpdate, Editor) are intentionally skipped to avoid spam — periodic sync picks them up. - App handles Mutated by dispatching SyncNow if configured and not already syncing. - Add 60s subscription emitting SyncTick to drive periodic sync.
Three fixes for CalDAV sync. - parse_ical_datetime: chrono's DateTime::parse_from_str rejects a literal 'Z' as a timezone specifier, so VTODO timestamps like 20260405T170617Z always failed to parse and fell back to Utc::now(). This made every local task's last_modified equal to the most recent pull time, which then equaled (or trailed) the remote's reparsed "now" in the push comparison, so push never fired. Strip trailing Z and parse via NaiveDateTime in UTC. Tests cover Zulu / floating / date-only forms. - Nav model duplicates after sync: PopulateLists appended without clearing the segmented_button model, so each sync re-added every list. Clear the model first, then restore the previously active list by id. - Password storage: move CalDAV password from cosmic-config (plaintext on disk) to libsecret via the keyring crate. Existing config-stored passwords migrate into the keyring on first launch and are cleared from the config file. Username/server URL stay in cosmic-config. - Surface PUT failures in the Sync status line (added `failed` count) and log response bodies on non-2xx PUTs.
Account / keyring
- Move CalDAV password to the system keyring (Secret Service /
cosmic-keyring); drop the legacy plaintext-password migration and
the now-unused sync_password field on TasksConfig.
- Replace the description-embedded "caldav:URL" marker with a proper
List::remote_url field; legacy lists migrate on first sync.
- Account settings panel gains a status row, helper text under each
input, last-synced relative timestamp, and a destructive Sign-out
button that wipes config and keyring entry.
Sync triggers
- Sync icon in the header bar (configured-only, disables while
running).
- "Sync now" entries in the View menu and per-list right-click menu.
UI polish
- Due-date badge on every task row ("Today" / "Tomorrow" /
"Yesterday" / weekday / YYYY-MM-DD), localized.
- Sort by due date (Earliest/Latest); completed tasks always sink to
the bottom regardless of sort.
Bug fixes
- Pulled VTODOs now appear immediately in the active list. SetList
short-circuited when the list id was unchanged, so post-sync the
view stayed stale until the user reselected. New
Message::ReloadTasks is dispatched after every successful sync.
- Date dialog Complete handler called details::update directly and
dropped RefreshTask/Mutated; routed through Message::Details(..) so
the in-memory task and sidebar refresh and a sync is triggered.
- "invalid SecondaryMap key used" panic: ReloadTasks rebuilds the
slotmap, so message handlers could arrive with stale DefaultKeys.
All hot-path SecondaryMap accesses now use .get() with bail-out.
- Date picker stored UTC midnight, which shifted to the previous day
for any UTC-negative offset. Stores local-midnight (as UTC) and
emits VALUE=DATE for all-day RFC encoding so other clients show the
same calendar day.
- Rename / Set-Icon dialogs now correctly target the entity passed in
from the nav context menu instead of the active list.
- CalDAV calendar URLs without a trailing slash had Url::join()
silently replace the last segment; trailing slashes are now
enforced at discovery.
- Removed unsafe impl Send for List (PathBuf is already Send).
- Dropped the dead sqlx dependency and Error::Sqlx variant.
iCalendar interop
- Use icalendar::Todo::get_due() so all RFC 5545 forms (DATE,
DATE-TIME UTC / floating / TZID) are accepted; textual fallback
parser also accepts ISO-8601 extended forms (with separators and
with offset).
- Always emit DTSTAMP, some servers refuse VTODOs without it.
- Use Todo::completed() for COMPLETED so it's a proper UTC date-time.
Release housekeeping
- 0.3.0 metainfo entry; flatpak finish-args gain --share=network and
--talk-name=org.freedesktop.secrets; <internet> changed from
offline-only to always.
- README gets a CalDAV section; new CHANGELOG.md
(Keep-a-Changelog).
- Reorganized .gitignore.
- About dialog reads version from CARGO_PKG_VERSION.
Tests
- 12 unit tests cover legacy-marker parsing, remote_url precedence,
marker stripping, ISO-8601 (UTC, offset, extended), garbage
rejection, and the all-day VALUE=DATE round-trip.
Adds a markdown-import flow and a save-to-file branch on the existing Export dialog, plus opt-in at-rest encryption of the Task::notes field using a key from the system keyring. Refs cosmic-utils#21 (markdown import), cosmic-utils#28 (clipboard-only export workaround). Markdown import (cosmic-utils#21): - File menu → Import from markdown… opens a file-path dialog. - Permissive parser: H1 → list name, H2/deeper → parent task, bullets (- / * / + / 1.) become tasks, [ ] / [x] checkboxes are honored, indentation maps to sub-tasks. ~ in paths is expanded. - 8 unit tests including a regression test built from a realistic multi-section TODO file. Markdown export (cosmic-utils#28): - Existing dialog gains a path input + "Save to file…" tertiary button alongside the original Copy button. Default destination is ~/Documents/<list>.md. Save creates parent dirs as needed. Notes-at-rest encryption: - New Settings → Privacy → "Encrypt notes at rest" toggle. - ChaCha20-Poly1305 with a 32-byte key in keyring service dev.edfloreshz.Tasks.notes; auto-generated on first use. - Encrypted payloads are wrapped with an `enc:v1:` magic prefix so reads auto-detect format. Toggling on/off requires no migration: writes follow the flag, reads always try to decrypt. - CalDAV roundtrips deliberately see plaintext (decryption happens at the storage read boundary) so the sync engine pushes the readable form, preserving interop with other CalDAV clients on the same calendar. - Storage gains Arc<AtomicBool> so all clones in the app observe the toggle without re-plumbing. Stacked on top of cosmic-utils#102 — the diff will look combined until that lands. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the import path-input dialog and the export save-path field with the XDG Desktop Portal file chooser via rfd's xdg-portal backend. ashpd was already a transitive dep; rfd is added directly with default-features off so we only pull in the portal path on Linux. Why: the previous path-input UX is fragile under Flatpak — the manifest only grants `--filesystem=xdg-config/cosmic:ro`, so any user-typed path into ~/ would have failed silently. The portal returns a sandbox-pierced fd per file, so no broader filesystem grant is needed. Changes: - File → Import from markdown… now opens the Open dialog directly; no intermediate DialogPage::Import is constructed. - Export dialog drops the path text_input. The "Save to file…" tertiary button opens the Save dialog with a slugified <list>.md as the pre-filled name. - Drop expand_user_path / DialogPage::Import / DialogAction::ExportSave — all dead code now. - New ApplicationAction futures: ImportFromFile / SaveExportToFile and their *Result variants. Cancel is a no-op (sentinel "cancelled" string). Errors go to tracing. Flatpak manifest is unchanged on purpose — the portal handles access.
|
Pushed 985272d to address the path-input UX issue I caught after the initial push:
Tests still 23/23 green. |
|
Could you rebase this branch? There are conflicts. |
|
Started the rebase and hit a wall — the upstream Right move is probably to re-apply this PR as a port on top of the new architecture rather than force-push a rebase. Happy to do that, but wanted to check first:
|
|
I'd appreciate it if you could respond directly, I understand you're using AI for this PR and I'm happy to discuss the changes, but I'd prefer to hear from a human.
|
Adds markdown import, a save-to-file branch on the existing Export dialog, and opt-in at-rest encryption of
Task::notes.Note
Stacked on top of #102. The diff in this PR will look combined until that one lands. The new work itself lives in a single commit on
feat/markdown-io-and-encrypted-notes(branched offfeat/caldav-sync); rebasing onto a merged #102 will drop the CalDAV diff cleanly.Refs #21 (markdown import), #28 (clipboard-only export workaround).
Markdown import — closes #21
~/).# H1→ list name (first one wins; subsequent H1s are treated as parent tasks).## H2/ deeper → parent task whose children are the bullets that follow.-,*,+,1.,1). Checkboxes[ ]/[x]/[X]honored.Save markdown to file — refs #28
~/Documents/<list-slug>.md. Parent dirs are created on save.Opt-in note encryption
dev.edfloreshz.Tasks.notes(masteraccount). Generated on first use; never leaves the device.enc:v1:<base64(nonce|ciphertext)>. Reads auto-detect the magic prefix, so flipping the flag in either direction is non-destructive — old plaintext stays readable, old ciphertext stays readable, and writes follow the current flag.LocalStoragegained anArc<AtomicBool>so every clone in the app observes the toggle without re-plumbing.Why bundle these together
These three are the small surface area where I needed to touch the dialog/menu/storage layers anyway, and they share the import/export theme. The encryption piece is intentionally local-only and opt-in so it composes cleanly with #102's CalDAV path without changing what hits the wire.
New deps
chacha20poly1305— pure-Rust AEAD.rand— only for the 32-byte key + 12-byte nonce.Tests
23 total pass: the 8 markdown-import tests, 3 crypto round-trip tests, plus the 12 CalDAV/sync tests from #102.
Out of scope