feat: add persistent Markdown notes
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user