Requirements: macOS 15.7+, Xcode 16.2+, Homebrew
brew install swiftformatCode style is enforced by SwiftFormat via CI — rules are in .swiftformat at the project root.
graph TD
EM["EdgeDetector<br/>(mouse monitor)"] -->|"edge hit"| SP["SidePanelController<br/>(NSWindow)"]
HK["ShortcutManager<br/>(Carbon hotkey)"] -->|"toggle"| SP
SP -->|"host"| SUI["SwiftUI Views"]
SUI -->|"observe"| NS["NoteStore (@Observable)"]
NS -->|"read / write"| FS["FileStorage"]
FS -->|"YAML + .md"| Disk[("~/Documents/EdgeMark/")]
SUI -->|"observe"| AS["AppSettings (@Observable)"]
AS -->|"persist"| UD["UserDefaults"]
SUI -->|"observe"| US["UpdateState (@Observable)"]
US -->|"check · download · install"| UC["UpdateChecker / Installer"]
UC -->|"GitHub API"| GH["GitHub Releases"]
Log["OSLog (5 categories)"] -.->|"Console.app"| CA["Diagnostic Logs"]
EdgeMark/
├── App/ # Entry point + global state
│ ├── EdgeMarkApp.swift # @main, menu bar utility (LSUIElement)
│ ├── AppDelegate.swift # Lifecycle, storage migration, shortcut setup
│ └── ContentView.swift # Navigation shell (folders → notes → editor)
│
├── Core/ # Business logic — no SwiftUI imports
│ ├── Editor/
│ │ ├── MarkdownEditorView.swift # WKWebView ↔ CodeMirror 6 bridge
│ │ ├── ReadOnlyMarkdownView.swift # Read-only Markdown preview (trash)
│ │ ├── SlashCommandHandler.swift # /h1, /todo, /code, /quote routing
│ │ └── SlashCommandPopup.swift # Floating autocomplete popup
│ ├── Settings/
│ │ └── AppSettings.swift # @Observable — sort order, date format, prefs
│ ├── Shortcuts/
│ │ ├── ShortcutManager.swift # Carbon RegisterEventHotKey global shortcut
│ │ ├── ShortcutSettings.swift # UserDefaults persistence for settings
│ │ └── KeyCodeTranslator.swift # Virtual key code → display string mapping
│ ├── Storage/
│ │ ├── NoteStore.swift # @Observable — note CRUD, trash, folders
│ │ ├── FileStorage.swift # Plain .md files with YAML front matter
│ │ ├── Note.swift # Note model (id, title, body, timestamps)
│ │ ├── Folder.swift # Folder model
│ │ └── TrashedFolder.swift # Trashed folder with expiry metadata
│ ├── Updates/
│ │ ├── UpdateChecker.swift # GitHub Releases API, version comparison
│ │ ├── UpdateDownloader.swift # URLSession delegate with progress tracking
│ │ ├── UpdateInstaller.swift # DMG mount → verify → copy → replace → restart
│ │ ├── UpdateModels.swift # GitHubRelease, UpdateProgress, UpdateError
│ │ ├── UpdateState.swift # @Observable — update UI state machine
│ │ └── ChecksumVerifier.swift # SHA256 verification via CryptoKit
│ └── Window/
│ ├── SidePanelController.swift # NSWindowController — show/hide/animate
│ ├── EdgeDetector.swift # Global mouse monitor → edge activation
│ ├── SettingsWindowController.swift # Settings window lifecycle
│ └── UpdateWindowController.swift # Update window lifecycle
│
├── UI/ # SwiftUI views
│ ├── EditorScreen.swift # Editor chrome (header, editor, footer)
│ ├── Navigation/
│ │ ├── HomeFolderView.swift # Folder list with create/rename/trash
│ │ ├── NoteListView.swift # Note cards with search, sort, context menus
│ │ └── TrashView.swift # Trash browser with restore/delete/empty
│ ├── Components/ # Reusable UI (HeaderIconButton, NoteCardView,
│ │ ├── NSContextMenuModifier.swift # NSMenu context menus with SF Symbol icons
│ │ ├── NoteListMenus.swift # Note/folder context menu builders
│ │ └── ... # InlineRenameEditor, EmptyStateView, etc.
│ └── Settings/
│ ├── SettingsView.swift # Tab container (General, Behavior, Keyboard, About)
│ ├── GeneralSettingsTab.swift # Appearance, language, system, storage
│ ├── BehaviorSettingsTab.swift# Panel position, edge activation, auto-hide
│ ├── KeyboardSettingsTab.swift# Shortcut recorder + local shortcuts
│ ├── AboutSettingsTab.swift # Version info, links, copyright
│ └── UpdateView.swift # Download progress, verify, install UI
│
├── Shared/Utils/
│ ├── L10n.swift # JSON-based i18n runtime
│ ├── Log.swift # OSLog — 5 categories
│ └── Debouncer.swift # Generic debounce utility
│
└── Resources/
├── Editor/ # CodeMirror 6 bundle
│ ├── editor.html # WKWebView host page
│ ├── editor-bundle.js # CM6 + WYSIWYG plugin
│ └── styles.css # Editor theme
└── Locales/ # i18n strings
├── en.json # English
└── zh-Hans.json # Simplified Chinese
| Pattern | Detail |
|---|---|
| @Observable | NoteStore, AppSettings, and UpdateState use the @Observable macro — views read properties directly, no @Published needed |
| MainActor by default | SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor. All types are @MainActor unless explicitly opted out |
| AppKit + SwiftUI hybrid | NSHostingView embeds SwiftUI inside a borderless NSWindow. Panel lifecycle managed by SidePanelController (AppKit), UI rendered by SwiftUI |
| File-based storage | Notes are plain .md files with YAML front matter — no database, readable by any Markdown editor |
| Carbon hotkeys | Global shortcut uses RegisterEventHotKey (Carbon API) since NSEvent.addGlobalMonitorForEvents can't intercept key events |
| JSON i18n | L10n loads locale JSON at runtime. Access: l10n["key"] or l10n.t("key", arg1, arg2) for interpolation |
| OSLog diagnostics | 5 categorized loggers (app, storage, window, shortcuts, updates). View in Console.app with subsystem:io.github.ender-wang.EdgeMark |
| DMG auto-update | UpdateChecker queries GitHub Releases API. UpdateInstaller: mount DMG → verify bundle ID → copy → replace → restart |
EdgeMark uses a custom JSON-based i18n system. Currently supported:
| Language | File | Status |
|---|---|---|
| English | Resources/Locales/en.json |
✅ |
| Simplified Chinese | Resources/Locales/zh-Hans.json |
✅ |
- Copy
Resources/Locales/en.json - Rename to your language code (e.g.
ja.json,ko.json,fr.json,de.json) - Translate the values (keep the keys as-is)
- Submit a PR
No code changes needed — the app picks up new locale files automatically.