Skip to content

Markdown import/export + opt-in note encryption#103

Open
atayozcan wants to merge 7 commits intocosmic-utils:mainfrom
atayozcan:feat/markdown-io-and-encrypted-notes
Open

Markdown import/export + opt-in note encryption#103
atayozcan wants to merge 7 commits intocosmic-utils:mainfrom
atayozcan:feat/markdown-io-and-encrypted-notes

Conversation

@atayozcan
Copy link
Copy Markdown

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 off feat/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

  • File → Import from markdown… opens a file-path dialog (supports ~/).
  • Permissive parser:
    • # H1 → list name (first one wins; subsequent H1s are treated as parent tasks).
    • ## H2 / deeper → parent task whose children are the bullets that follow.
    • Bullets accepted: -, *, +, 1., 1). Checkboxes [ ] / [x] / [X] honored.
    • Indentation (2 spaces or 1 tab) → sub-task nesting, arbitrarily deep.
  • Imports always create a new list, so existing CalDAV-bound lists are never overwritten.
  • 8 unit tests, including one built from a realistic multi-section TODO file structure.

Save markdown to file — refs #28

  • Existing Export dialog gains a path input + Save to file… tertiary button next to the original Copy button.
  • Default suggested path: ~/Documents/<list-slug>.md. Parent dirs are created on save.
  • Doesn't fix the underlying clipboard bug from Copying exported tasks doesn't work #28, but gives users a working alternative when their clipboard backend can't accept the copy.

Opt-in note encryption

  • New Settings → Privacy → Encrypt notes at rest toggle.
  • ChaCha20-Poly1305 with a 32-byte key stored in the system keyring under service dev.edfloreshz.Tasks.notes (master account). Generated on first use; never leaves the device.
  • Encrypted payloads are wrapped 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.
  • CalDAV roundtrips deliberately see plaintext. Decryption happens at the storage read boundary, so the sync engine pushes the readable form to the server, preserving interop with other clients sharing the same calendar.
  • The toggle warms the keyring entry up front so the user gets the unlock prompt at toggle time, not the first time they edit a note.
  • Implementation note: LocalStorage gained an Arc<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.

test result: ok. 23 passed; 0 failed; 0 ignored; 0 measured

Out of scope

  • A "re-encrypt all existing notes now" one-shot — auto-detection on read makes this unnecessary day-to-day, and it would just be churn.
  • Encrypting notes on the wire for CalDAV — would break interop; that's a different feature (and would need a per-list opt-in on top of this).

atayozcan and others added 7 commits April 28, 2026 03:15
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.
@atayozcan
Copy link
Copy Markdown
Author

Pushed 985272d to address the path-input UX issue I caught after the initial push:

  • The path field would have failed silently in Flatpak — the manifest only grants `--filesystem=xdg-config/cosmic:ro`, so any user-typed path under `~/` would have hit a permission error.
  • Switched both dialogs to the XDG Desktop Portal file chooser via `rfd` with the `xdg-portal` backend. `ashpd` was already a transitive dep; `rfd` is the one new direct dep, default-features off so only the portal path is pulled in.
  • The portal hands back a sandbox-pierced fd per file, so the Flatpak manifest stays untouched (no broader `--filesystem=home` grant needed).
  • Import dialog dropped entirely — the picker is the dialog. Export dialog keeps its preview but the path-input is replaced by a "Save to file…" tertiary button that opens the portal Save dialog with a slugified `.md` pre-filled.

Tests still 23/23 green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Import markdown lists

1 participant