Native macOS (SwiftUI + AppKit) daily-notes app over a plain markdown vault (Logseq-compatible).
./scripts/build_and_install test # generate project + build + run XCTest — the verification command
./scripts/build_and_install release # Release build, install to ~/Applications, launch
xcodegen generate # regenerate DayStream.xcodeproj from project.yml (after adding files)Always run xcodegen generate after creating/moving source files, and ./scripts/build_and_install test before declaring work done. There is no linter configured; keep style consistent with surrounding code.
project.ymlis the source of truth for the Xcode project; the.xcodeprojis generated and committed.Sources/Model/— pure logic, no UI:BlockTree(outliner parser + todo toggling/syncing),VaultStore(vault I/O, watching, migration),JournalDate(filename formats),CarryForward,WikiName. Most logic is unit-tested inTests/.Sources/Model/RemindersSync.swift— Apple Reminders mirror.RemindersSync.planis a pure state machine (vault open tasks + reminder snapshots + persistedRemindersSyncState→ action plan; title-normalized keys, not reminder ids) and is unit-tested without EventKit; reminder titles carry the task's section header (ADMIN/task), and imports namedHEADER/taskland under that header in today's note (the- [[HEADER]]section is created when missing).RemindersSyncEngineowns theEKEventStore, reconciles off-main on its own queue, and writes back only throughVaultStore.syncTodoState/addTask. Vault mutations reach it viaVaultStore.onVaultMutated; its own echo writes are ignored via a short self-write window — don't add a second mutation hook.Sources/Model/CalendarSync.swift— Apple Calendar today-section.CalendarSync.mergedTextis a pure merge (today's text + event snapshots → updated text, unit-tested without EventKit): events land under- [[TODAY]]as plain bulletsTitle, 9:00 AM, anchored to their events by hiddenevent:: <id>#<start>markers so re-syncs dedupe and moved events rewrite in place; entries are deliberately not tasks (no counters, carry-forward or Reminders mirroring).CalendarSyncEnginefetches the local day's events off-main and writes throughVaultStore.write; refreshes on launch/rollover (AppModel.applyScheduledForToday),EKEventStoreChangedand a 5-minute backstop.Sources/Views/— SwiftUI:DailyStreamView(List ofDaySectionView),CalendarView,PageView(wikilink pages + linked references),SettingsView. The editor is the view —DaySectionViewandPageViewrenderMarkdownEditorViewdirectly with live markdown preview; there is no rendered/editing split and no edit mode to enter or leave.Sources/Editor/MarkdownEditorView.swift— AppKitNSTextViewwrapper, content-fitting (full note height; the stream scrolls, the editor doesn't), with outliner key bindings. Space-bar handling uses a local keyDown monitor because SwiftUI's ScrollView eats unmodified space.[[autocomplete is a non-activating NSPanel (WikiSuggestController) that must never take key focus. The text view owns the text while editing: never re-introduce a per-keystrokeBinding/@Stateround-trip through SwiftUI (it re-rendered the section's glass chrome every key press — laggy typing, jumpy caret). External text reaches the editor only viapushedText; callers keep live text inEditorSession(a class held by@State, so keystrokes don't invalidate views). Typing saves go throughVaultStore.writeAsync(I/O + parse off-main, generation-guarded so stale snapshots can't roll backdays). Live markdown preview (Sources/Editor/LiveMarkdown.swift+EditorTextView'sNSLayoutManagerDelegate): syntax on lines away from the caret is hidden by nulling glyphs — the raw characters never leave the text storage, so saves/undo/autocomplete/outliner all see plain markdown. Don't implement hiding by mutating the storage instead; and keep syntax styling and hiding on the same regexes (both live inLiveMarkdown). The editor draws gutter glyphs over hidden bullet prefixes (checkboxes for task lines, dashes for plain bullets — only on non-active lines; the horizontaltextContainerInsetis the gutter, text starts at a fixed column); clicking a drawn checkbox (or the marker word on the caret's line, or ⌘⏎) toggles the task. Hidden bookkeeping property lines collapse to zero height viashouldSetLineFragmentRect— don't leave blank stripes.added::/completed::drive the drawn duration badge (drawDurationBadge), and a toggle to DONE stampscompleted::in-editor (stampCompletion) so badges work like the old rendered checkboxes. ⌘⏎ firesonTodoToggled→ callers echo the state across notes viaVaultStore.syncTodoState(the editor never does cross-note writes itself). Every day is an editor now, so focus is opt-in: never auto-makeFirstResponderat creation (scrolling materializes cells); focus only ridescaretAtEndRequest(⌘N / add-todo).Sources/App/—DayStreamApp(Window + Settings),AppModel(selection/navigation state),AppSettings(UserDefaults-backed),GlobalQuickAddController(system-wide quick-add shortcut → opens app + new todo today).@Observableeverywhere;VaultStoremutatesdaysin place to avoid full-list SwiftUI rebuilds (block IDs areline-<index>so unchanged rows keep identity across edits).
- Files are the database: all writes go through
VaultStore.write(atomic + in-place refresh). Watcher-triggered reloads are suppressed for ~0.8s after our own writes. - Todo syncing across notes must stay off the main thread (see
syncQueueinVaultStore); the sync algorithm parses once and edits lines in place — never change line counts intoggledFileTextoutput. - Journal filename formats:
yyyy-MM-dd.md(new),yyyy_MM_dd.md,dd-MM-yyyy.md(legacy). New files always use ISO format.migrateLegacyFilenames()backs up tobackup/before renaming. - macOS SwiftUI
ScrollView+LazyVStackmaterializes rows lazily but never releases them — scrolling a large vault ballooned memory to ~1GB. The stream usesList(NSTableView-backed, recycles cells) instead; don't switch it back. Recycled cells meanDaySectionViewmust reset edit state when itsdaychanges. - Journal filenames render in the local timezone (
allFilenames), but parsing stays UTC —startOfDayinstants are local midnight, whose UTC day differs in UTC+/- zones. Don't "simplify" either side. - UI strings are plain English; code comments explain why, not what. No comments unless necessary.
- macOS 15+ deployment target; Swift 5, minimal strict concurrency.
developis the default branch and where all work lands — feature branches merge into it.mainis release-only: it is updated solely by mergingdevelopfor a release; no direct commits.- Releases: merge
develop→main, bumpMARKETING_VERSIONinproject.yml, tagvX.Y.Zonmain, push the tag. - CI (
.github/workflows/build.yml): build + test on pushes/PRs tomain/develop, release zip onv*tags, deploy GitHub Pages onmainpushes. - Commit style: short imperative subject lines, like existing history.