Skip to content

Add Daily Note feature with Obsidian template support#1

Open
kyu91 wants to merge 1 commit intotriptu:mainfrom
kyu91:feature/daily-note
Open

Add Daily Note feature with Obsidian template support#1
kyu91 wants to merge 1 commit intotriptu:mainfrom
kyu91:feature/daily-note

Conversation

@kyu91
Copy link
Copy Markdown

@kyu91 kyu91 commented Mar 24, 2026

Summary

This PR adds a Daily Note feature to Flowbar, inspired by Obsidian's daily note workflow.

  • Today tab — new footer button (and ⌥⌘D shortcut) opens today's daily note
  • Configurable filename format — Settings supports Obsidian-style tokens (YYYY, MM, DD, ddd, dddd), e.g. YYYY-MM-DD(ddd)2026-03-24(Mon)
  • Heading navigation — sidebar shows the full note at top, then each heading (H1–H6) as a clickable nav item; clicking a heading shows only that section
  • Create prompt — if today's note doesn't exist, shows a "Create Daily Note" button instead of an empty view
  • Obsidian template support — configure a .md template file in Settings; tokens substituted on creation: {{title}}, {{date}}, {{date:FORMAT}}, {{time}}
  • Edit / Preview toggle — pencil/eye button (⌘E) or double-click to edit; live file watcher syncs external edits from Obsidian

New files

  • DailyNoteContentView.swift — content view with edit/preview toggle and heading-scoped display
  • LiveMarkdownEditorView.swift — NSTextView-based editor with live heading + strikethrough formatting

Modified files

  • AppState.swift — daily note state, showDailyNote(), createDailyNote(), template resolution
  • SettingsState.swiftdailyNoteFormat + dailyNoteTemplatePath persisted settings
  • MarkdownParser.swiftextractHeadings() and sectionContent() helpers
  • SidebarView.swift — heading navigation sidebar when in daily note mode
  • SidebarFooter.swift — Today button
  • MainView.swift.dailyNote panel routing + ⌥⌘D shortcut
  • SettingsView.swift — Daily Note settings section

Test plan

  • Set folder path in Settings, configure format (e.g. YYYY-MM-DD)
  • Click Today tab → "Create Daily Note" appears if no note exists
  • Click Create → note is created (with template if configured)
  • Headings in the note appear as sidebar nav items
  • Clicking a heading shows only that section
  • Edit mode (⌘E) saves back to disk; external edits in Obsidian reload automatically
  • Format with weekday token (e.g. YYYY-MM-DD(ddd)) produces correct filename

🤖 Generated with Claude Code

- Today tab in sidebar footer opens today's daily note
- Daily note filename format configurable in Settings (Obsidian-style tokens: YYYY, MM, DD, ddd, dddd)
- Sidebar shows full note at top + headings (H1–H6) as navigation items
- Clicking a heading shows only that section's content
- If today's note doesn't exist, shows a "Create Daily Note" prompt
- Template file path configurable in Settings; supports Obsidian tokens: {{title}}, {{date}}, {{date:FORMAT}}, {{time}}
- Edit/preview toggle (⌘E) and double-click to edit
- Live file watcher reloads content when edited externally (e.g. in Obsidian)
- Keyboard shortcut ⌥⌘D to open Today tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 24, 2026

@kyu91 is attempting to deploy a commit to the triptu's projects Team on Vercel.

A member of the Team first needs to authorize it.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a comprehensive "Daily Note" feature to Flowbar, mirroring the popular Obsidian daily note workflow. It allows users to quickly access, create, and manage daily markdown notes with configurable filenames and template support. The feature includes a new live markdown editor with dynamic formatting, heading-based navigation within the note, and seamless integration with external editors like Obsidian through file watching and URL schemes.

Highlights

  • Daily Note Feature: Introduced a new 'Daily Note' feature, allowing users to create and manage daily notes inspired by Obsidian's workflow, accessible via a 'Today' button or keyboard shortcut.
  • Configurable Filename & Templates: Added settings for customizing daily note filenames using Obsidian-style date tokens (e.g., YYYY-MM-DD) and supporting markdown templates with token substitution (e.g., {{title}}, {{date}}).
  • Heading-based Navigation: Implemented a sidebar navigation for daily notes, displaying headings (H1-H6) as clickable items to filter the main view to specific sections of the note.
  • Live Markdown Editor: Integrated a new LiveMarkdownEditorView that provides real-time formatting for headings and strikethrough for completed todo items directly within the daily note editing experience.
  • External Sync & Obsidian Integration: Daily notes are saved to disk, automatically reload on external changes (e.g., from Obsidian), and can be opened directly in Obsidian via a dedicated button.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a well-designed Daily Note feature, complete with Obsidian-style template support and heading navigation. The implementation is solid, with good use of SwiftUI and AppKit integration. My review focuses on a few critical areas to improve robustness and prevent potential data corruption. Specifically, I've pointed out issues with how file content is updated, which could lead to incorrect modifications if the note contains duplicate content. I've also highlighted several places where file I/O errors are silently ignored, which could result in data loss. Finally, there are a couple of minor suggestions for performance improvements in the new live markdown editor. Overall, this is a great addition to the app.

guard let url = appState.dailyNoteURL else { return }
if let heading = appState.dailyNoteSelectedHeading {
let old = MarkdownParser.sectionContent(for: heading, in: appState.dailyNoteContent)
appState.dailyNoteContent = appState.dailyNoteContent.replacingOccurrences(of: old, with: newContent)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Using replacingOccurrences(of: with:) to save the edited section is risky and can lead to data corruption. If the original content of the section (old) appears elsewhere in the note, this method will replace all instances, not just the one being edited. A more robust approach is to identify the exact range (e.g., line numbers) of the section within the full document and replace only that specific range.

Comment on lines +243 to +246
try? content.write(to: fileURL, atomically: true, encoding: .utf8)
dailyNoteExists = true
loadDailyNoteContent(from: fileURL)
watchDailyNote(at: fileURL)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using try? to write the daily note file can lead to an inconsistent UI state. If the write operation fails, the error is ignored, but the code proceeds to set dailyNoteExists = true and attempts to load the (non-existent or outdated) file. This can cause unexpected behavior. It's safer to handle the error from content.write explicitly, for example with a do-catch block, and only update the app state on a successful write.

        do {
            try content.write(to: fileURL, atomically: true, encoding: .utf8)
            dailyNoteExists = true
            loadDailyNoteContent(from: fileURL)
            watchDailyNote(at: fileURL)
        } catch {
            print("Failed to create daily note: \(error.localizedDescription)")
            dailyNoteExists = false
        }

let displayLines = appState.dailyNoteDisplayContent.components(separatedBy: "\n")
guard lineIndex < displayLines.count else { return }
let targetLine = displayLines[lineIndex]
guard let fullIndex = lines.firstIndex(where: { $0 == targetLine }),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Finding the line to toggle by searching for its content with firstIndex(where: { $0 == targetLine }) is not reliable. If the note contains duplicate lines (e.g., two identical todo items), this could find and toggle the wrong line, especially if one of them is outside the currently displayed section. A safer method would be to calculate the line's absolute index within the full document instead of searching by its string value.

lines[lineIndex] = toggled
}
appState.dailyNoteContent = lines.joined(separator: "\n")
try? appState.dailyNoteContent.write(to: url, atomically: true, encoding: .utf8)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using try? to save the file after toggling a todo silently ignores potential write errors. This could lead to the UI showing a toggled state while the change was not actually saved to disk, causing data inconsistency. It's better to use a do-catch block to handle errors, at least by logging them, to ensure data integrity.

let trimmed = line.trimmingCharacters(in: .whitespaces)
let level = trimmed.prefix(while: { $0 == "#" }).count
if level >= 1, level <= 6, trimmed.dropFirst(level).hasPrefix(" ") {
let sizes: [CGFloat] = [baseSize * 2.0, baseSize * 1.6, baseSize * 1.3, baseSize * 1.15, baseSize * 1.05, baseSize]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The sizes array is re-created every time applyLineStyle is called. Since this function is executed for every line during text formatting, and the values are constant, defining this array as a static let constant on LiveMarkdownEditorView would be more efficient.

let indent = String(line.prefix(while: { $0 == " " || $0 == "\t" }))
let trimmed = line.trimmingCharacters(in: .whitespaces)

let emptyPrefixes = ["-", "- [ ]", "- [x]", "- [X]", "*"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The emptyPrefixes array is re-created on every newline event. For better performance, this could be defined as a static let constant. Furthermore, using a Set instead of an Array would make the contains check more efficient (O(1) average time complexity vs. O(n)).

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.

1 participant