feat: add persistent Markdown notes

This commit is contained in:
2026-10-17 00:53:28 +02:00
parent 0f49209d95
commit e36a8bbc6b
13 changed files with 584 additions and 23 deletions
+2
View File
@@ -4,6 +4,7 @@ public struct Application: Identifiable, Sendable {
public enum Command: String, CaseIterable, Sendable {
case translate
case emoji
case notes
}
public static let commands = Command.allCases.map { Application(command: $0) }
public let command: Command?
@@ -22,6 +23,7 @@ public struct Application: Identifiable, Sendable {
let name = switch command {
case .translate: "Conjure Translate"
case .emoji: "Conjure Emoji"
case .notes: "Conjure Notes"
}
self.init(canonicalURL: URL(string: "conjure-command://\(command.rawValue)")!, name: name, command: command)
}
+4
View File
@@ -120,6 +120,8 @@ public final class GlobalShortcuts {
case "c": return "Copy"
case "v": return "Paste"
case "a": return "Select All"
case "n": return "New Note"
case "z": return "Undo"
case "m": return "Minimize"
case "k": return "entry actions"
case "return", "enter": return "Open, Copy, Translate, or confirm uninstall"
@@ -128,6 +130,8 @@ public final class GlobalShortcuts {
}
} else if candidate.modifiers == [.command, .option], key == "h" {
return "Hide Others"
} else if candidate.modifiers == [.command, .shift], key == "z" {
return "Redo"
} else if candidate.modifiers == .control {
switch key {
case "h", "j", "k", "l": return "launcher and emoji navigation"
+74
View File
@@ -0,0 +1,74 @@
import AppKit
public struct Note: Identifiable {
public let id: UUID
public let contents: NSAttributedString
public let title: String
init(id: UUID, contents: NSAttributedString) {
self.id = id
self.contents = NSAttributedString(attributedString: contents)
var title = "Untitled Note"
contents.string.enumerateLines { line, stop in
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
title = String(trimmed.prefix(60))
stop = true
}
}
self.title = title
}
}
@MainActor
public final class NotesStore {
public private(set) var notes: [Note] = []
private let directory: URL
public init(directory: URL = FileManager.default.urls(
for: .applicationSupportDirectory, in: .userDomainMask
)[0].appendingPathComponent("Conjure/Notes", isDirectory: true)) throws {
self.directory = directory
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let files = try FileManager.default.contentsOfDirectory(
at: directory, includingPropertiesForKeys: nil
).filter { $0.pathExtension.lowercased() == "rtfd" }
.sorted { $0.lastPathComponent < $1.lastPathComponent }
notes = try files.map { url in
guard let id = UUID(uuidString: url.deletingPathExtension().lastPathComponent) else {
throw CocoaError(.fileReadCorruptFile)
}
let contents = try NSAttributedString(
url: url, options: [.documentType: NSAttributedString.DocumentType.rtfd],
documentAttributes: nil
)
return Note(id: id, contents: contents)
}
}
public func create() throws -> UUID {
let note = Note(id: UUID(), contents: NSAttributedString(string: ""))
try save(note)
notes.append(note)
notes.sort { $0.id.uuidString < $1.id.uuidString }
return note.id
}
public func update(id: UUID, contents: NSAttributedString) throws {
guard let index = notes.firstIndex(where: { $0.id == id }) else {
throw CocoaError(.fileNoSuchFile)
}
let note = Note(id: id, contents: contents)
try save(note)
notes[index] = note
}
private func save(_ note: Note) throws {
let wrapper = try note.contents.fileWrapper(
from: NSRange(location: 0, length: note.contents.length),
documentAttributes: [.documentType: NSAttributedString.DocumentType.rtfd]
)
let url = directory.appendingPathComponent(note.id.uuidString + ".rtfd", isDirectory: true)
try wrapper.write(to: url, options: .atomic, originalContentsURL: url)
}
}