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
+46 -11
View File
@@ -35,7 +35,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
private var launcherFrameBeforeReview: NSRect?
private var translation: TranslationController?
private var emojiPicker: EmojiPickerController?
private var isCommandOpen: Bool { translation != nil || emojiPicker != nil }
private var notes: NotesController?
private let notesDirectory: URL?
private var isCommandOpen: Bool { activeCommand != nil }
private var activeCommand: Application.Command?
private var pendingPresentation: (() -> Void)?
private static let emojiIcon: NSImage = {
@@ -52,11 +54,12 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
}()
init(history: LaunchHistory = LaunchHistory(), uninstaller: ApplicationUninstaller = ApplicationUninstaller(),
defaults: UserDefaults = .standard, pasteboard: NSPasteboard = .general) {
defaults: UserDefaults = .standard, pasteboard: NSPasteboard = .general, notesDirectory: URL? = nil) {
self.history = history
self.uninstaller = uninstaller
self.defaults = defaults
self.pasteboard = pasteboard
self.notesDirectory = notesDirectory
let window = LauncherPanel(
contentRect: NSRect(x: 0, y: 0, width: 750, height: 474),
styleMask: [.borderless],
@@ -85,6 +88,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
else { self.moveSelection(by: offset) }
}
window.commandNavigation = { [weak self] key in self?.emojiPicker?.handleControlKey(key) ?? false }
window.usesTextNavigation = { [weak self] in self?.notes != nil }
window.dismiss = { [weak self] in self?.dismiss() }
window.canUseNumberShortcuts = { [weak self] in
guard let self else { return false }
@@ -122,7 +126,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
uninstallReview?.goBack()
return
}
closeCommand()
guard closeCommand() else { return }
actions.close(animated: false)
(window as? LauncherPanel)?.beginNumberHintSession()
let frontmost = NSWorkspace.shared.frontmostApplication
@@ -140,7 +144,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
showWindow(nil)
NSApp.activate()
window?.makeKeyAndOrderFront(nil)
window?.makeFirstResponder(searchField)
if let notes { notes.focusEditor() }
else { window?.makeFirstResponder(searchField) }
(window as? LauncherPanel)?.sampleNumberHints()
(searchField.currentEditor() as? NSTextView)?.insertionPointColor = SunsetPalette.accent
refreshApplications()
@@ -164,7 +169,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
uninstallReview?.goBack()
return
}
closeCommand()
guard closeCommand() else { return }
actions.close(animated: false)
alignmentGuides?.end()
let previous = previousApplication
@@ -567,10 +572,18 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
cell.locationLabel.isHidden = true
cell.kindLabel.attributedStringValue = history.isFavorite(entry)
? ApplicationCell.favoriteCommandKind : ApplicationCell.commandKind
cell.imageView?.image = entry.command == .emoji ? Self.emojiIcon
: NSImage(systemSymbolName: "character.bubble", accessibilityDescription: nil)
switch entry.command {
case .emoji:
cell.imageView?.image = Self.emojiIcon
cell.toolTip = "Search and copy emoji"
case .notes:
cell.imageView?.image = NSImage(systemSymbolName: "note.text", accessibilityDescription: nil)
cell.toolTip = "Write notes with Markdown and images"
default:
cell.imageView?.image = NSImage(systemSymbolName: "character.bubble", accessibilityDescription: nil)
cell.toolTip = "Translate English to Japanese"
}
cell.imageView?.contentTintColor = entry.command == .emoji ? nil : SunsetPalette.accent
cell.toolTip = entry.command == .emoji ? "Search and copy emoji" : "Translate English to Japanese"
} else {
let application = results[row]
cell.textField?.stringValue = application.name
@@ -794,11 +807,18 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
@objc private func goBackCommand() {
translation?.goBack()
emojiPicker?.goBack()
notes?.goBack()
}
private func closeCommand() {
func saveNotes() -> Bool { notes?.save() ?? true }
@discardableResult
private func closeCommand() -> Bool {
guard saveNotes() else { return false }
notes?.closed?()
translation?.goBack()
emojiPicker?.close()
return true
}
private func showCommand(_ command: Application.Command) {
@@ -825,6 +845,12 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
controller = tool
placeholder = "Search emoji by name or :shortcode:…"
accessibilityLabel = "Search emoji"
case .notes:
let tool = NotesController(defaults: defaults, directory: notesDirectory)
notes = tool
controller = tool
placeholder = "Conjure Notes"
accessibilityLabel = "Conjure Notes"
}
let previousQuery = searchField.stringValue
let previousPlaceholder = searchField.placeholderAttributedString
@@ -833,6 +859,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
controller.view.removeFromSuperview()
self.translation = nil
self.emojiPicker = nil
self.notes = nil
self.searchField.isEnabled = true
self.activeCommand = nil
self.commandBack.isHidden = true
self.commandBack.target = nil
@@ -851,6 +879,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
}
translation?.closed = close
emojiPicker?.closed = close
notes?.closed = close
actions.close(animated: false)
(window as? LauncherPanel)?.resetNumberHints()
openButton.keyEquivalent = ""
@@ -864,6 +893,10 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
attributes: previousPlaceholder?.attributes(at: 0, effectiveRange: nil) ?? [:]
)
searchField.setAccessibilityLabel(accessibilityLabel)
if command == .notes {
searchField.stringValue = "Conjure Notes"
searchField.isEnabled = false
}
for view in launcherViews.dropFirst() {
view.isHidden = true
view.setAccessibilityHidden(true)
@@ -876,7 +909,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
controller.view.topAnchor.constraint(equalTo: searchField.bottomAnchor, constant: 16),
controller.view.bottomAnchor.constraint(equalTo: content.bottomAnchor),
])
window?.makeFirstResponder(searchField)
if let notes { notes.focusEditor() }
else { window?.makeFirstResponder(searchField) }
}
}
@@ -1115,6 +1149,7 @@ final class LauncherPanel: NSPanel {
var numberHintsChanged: (() -> Void)?
var launchNumber: ((Int) -> Void)?
var commandNavigation: ((String) -> Bool)?
var usesTextNavigation: (() -> Bool)?
private var numberShortcuts = CommandNumberShortcuts()
private var commandHoldTask: Task<Void, Never>?
var numberHintsVisible: Bool { numberShortcuts.isVisible }
@@ -1218,7 +1253,7 @@ final class LauncherPanel: NSPanel {
}
resetNumberHints(modifierFlags: event.modifierFlags)
}
if event.type == .keyDown, attachedSheet == nil,
if event.type == .keyDown, attachedSheet == nil, usesTextNavigation?() != true,
event.modifierFlags.intersection([.control, .command, .option, .shift]) == .control {
if commandNavigation?(event.charactersIgnoringModifiers?.lowercased() ?? "") == true { return }
switch event.charactersIgnoringModifiers?.lowercased() {