Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Benchmarks/performance/LaunchBenchmark.swift
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,20 @@ private func measureOnce(options: Options) throws -> Sample {
let process = Process()
process.executableURL = executable
process.arguments = [options.documentPath]
let preferencesSuite = "ink.margin.editor.benchmark.\(UUID().uuidString)"
let readyURL = FileManager.default.temporaryDirectory
.appendingPathComponent("margin-ready-\(UUID().uuidString)", isDirectory: false)
let preferencesRoot = FileManager.default.temporaryDirectory
.appendingPathComponent("margin-benchmark-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(
at: preferencesRoot.appendingPathComponent("Library/Preferences", isDirectory: true),
withIntermediateDirectories: true
)
process.environment = ProcessInfo.processInfo.environment.merging([
"CFFIXED_USER_HOME": preferencesRoot.path,
"CFPREFERENCES_AVOID_DAEMON": "1",
"MARGIN_BENCHMARK_READY_FILE": readyURL.path,
"MARGIN_PREFERENCES_SUITE": preferencesSuite,
]) { _, benchmarkValue in benchmarkValue }
process.standardOutput = FileHandle.nullDevice
process.standardError = FileHandle.nullDevice
Expand All @@ -237,7 +247,17 @@ private func measureOnce(options: Options) throws -> Sample {
}
defer {
stop(process)
if let benchmarkDefaults = UserDefaults(suiteName: preferencesSuite) {
benchmarkDefaults.removePersistentDomain(forName: preferencesSuite)
benchmarkDefaults.synchronize()
}
let preferencesFile = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Preferences", isDirectory: true)
.appendingPathComponent(preferencesSuite)
.appendingPathExtension("plist")
try? FileManager.default.removeItem(at: preferencesFile)
try? FileManager.default.removeItem(at: readyURL)
try? FileManager.default.removeItem(at: preferencesRoot)
}

let timeout = UInt64(options.timeoutMilliseconds) * 1_000_000
Expand Down
7 changes: 4 additions & 3 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ All panes use standard AppKit split-view behavior, system materials, native focu

- A new or comment-free document begins with the source alone. The comment inspector opens automatically only when a document already contains review threads or the user starts a comment.
- Standalone files hide the navigator; directory workspaces reveal it. Closing either sidebar gives its space to the document without changing the outer window frame.
- The initial window uses a generous 1180 × 780 working size when the screen permits. Later launches restore only a usable, on-screen frame, and the document pane remains the elastic region during live resize and full screen.
- Opening several files—through the Open panel, Finder, or the CLI—places them in native tabs. An empty tab is reused, duplicate paths focus their existing tab, and separate windows remain available with `⌘N`.
- The initial window uses a generous 1180 × 780 working size when the screen permits. Later launches restore each window to its prior physical display when available, with a usable on-screen fallback when a display is disconnected; the document pane remains the elastic region during live resize and full screen.
- Opening several files—through the Open panel, Finder, or the CLI—places them in native tabs. An empty start tab is reused, duplicate paths focus their existing tab, and separate windows remain available.
- `⌘T` creates an editable, automatically saved untitled Markdown scratchpad in the current tab group. `⌘N` creates one in a separate window, and `⌘⇧S` promotes it to an ordinary named Markdown file without changing its tab position.
- Tabs follow browser conventions: `⌘T`, `⌘W`, `⌃Tab`, `⌃⇧Tab`, and `⌘1`…`⌘9`. Pane focus uses `⌃1`…`⌃3` so it never competes with tab selection.
- Reader presentation and file-provider change watching begin only after they are requested, away from the main interaction path. Stale reader work is discarded when the user switches files or modes.
- A normal relaunch restores tabs and lightweight view state, but an explicit CLI target always wins. Persistence is deferred to a utility queue and never joins the first-window path.
- A normal relaunch restores independent windows, their physical displays, native tab groups and order, the selected window and tab, usable window frames, scratchpad contents, and lightweight editor state. An explicit CLI target always wins. Session persistence is deferred to a utility queue, and scratchpads remain ordinary private Markdown files rather than database records.
- New comments or replies arriving from another process never steal focus or open the inspector. They add a restrained numeric tab/toolbar signal and a temporary **New** filter until the affected thread is deliberately visited.

## Typography and color
Expand Down
16 changes: 12 additions & 4 deletions Docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,18 @@ directory indexing, filesystem watching, comparison loading, collaboration
overviews, and stage presentation are created only when requested so they do
not join the initial window path.

The application stores only lightweight local presentation state such as open
tabs, pane visibility, selection, scroll position, and the active thread. An
explicit file or directory supplied by the user takes precedence over session
restoration.
The application stores only lightweight local presentation state such as the
independent-window and native-tab hierarchy, pane visibility, selection, scroll
position, display affinity, and the active thread. Stable native restoration
identifiers let AppKit preserve system-managed window state; custom placement
metadata provides physical-display continuity and an on-screen fallback when a
display is unavailable. Untitled scratchpads are ordinary Markdown
files below the user's private Application Support directory; the session holds
only their stable identity and presentation state. Scratch contents use the
same atomic autosave path as named documents, are flushed synchronously during
a normal quit, and move to the selected destination only after Save As
succeeds. An explicit file or directory supplied by the user takes precedence
over session restoration.

## CLI contract

Expand Down
17 changes: 16 additions & 1 deletion Docs/RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,22 @@

## Unreleased

No changes yet.
- On current macOS, independent windows now return to their previous Spaces
without pulling the user away from the active Space during cold launch. Once
running, ordinary Space changes perform no window creation or activation;
deliberate Dock or Finder activation follows native macOS app switching.
- Added durable untitled Markdown scratchpads. `⌘T` creates one in the current
tab group, `⌘N` creates one in a separate window, and `⌘⇧S` promotes it to a
named file. Scratch contents are private file-backed Markdown, autosave while
editing, flush synchronously on quit, and require an explicit Save As or
discard decision before a nonempty scratchpad tab closes.
- Expanded session restoration from one flattened tab list to the complete
independent-window and native-tab hierarchy, including tab order, selected
tabs, usable window frames, cursor and scroll continuity, and open
scratchpads. Existing version-1 sessions migrate to a single restored window.
- Integrated the durable session with native AppKit window restoration.
Physical-display identity and relative placement are retained, with an
on-screen fallback when a monitor is disconnected or its layout changes.

## 0.5.1 — native reading and review fixes

Expand Down
24 changes: 24 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,27 @@ This product includes software developed by OpenProse, Inc.
The Apache License, Version 2.0 does not grant permission to use the Margin or
OpenProse trade names, trademarks, service marks, or product names except as
required for reasonable and customary use in describing the origin of the work.

This product includes software adapted from yabai:

The MIT License (MIT)

Copyright (c) 2019 Åsmund Vikane

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
12 changes: 11 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,20 @@ var targets: [Target] = [

#if os(macOS)
products.append(.executable(name: "MarginAppBinary", targets: ["MarginApp"]))
targets.append(
.target(
name: "MarginPrivateSupport",
path: "Sources/MarginPrivateSupport",
publicHeadersPath: "include",
linkerSettings: [
.linkedFramework("CoreFoundation"),
]
)
)
targets.append(
.executableTarget(
name: "MarginApp",
dependencies: ["MarginCore"],
dependencies: ["MarginCore", "MarginPrivateSupport"],
path: "Sources/MarginApp"
)
)
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ Open a file for quick editing or a directory for a native, lazily loaded navigat

In the app you can:

- create an automatically saved untitled Markdown scratchpad with `⌘T`, or a scratchpad in a separate window with `⌘N`, then promote it to an ordinary file with `⌘⇧S`;
- return after a normal quit to the same independent windows and displays, tab groups, selected tabs, cursor positions, and scroll positions;
- edit literal Markdown with restrained syntax cues, delimiter pairing, list continuation, native undo, find, spellcheck, and accessibility support;
- switch to a bounded, typography-first reader view with `⌘⇧R`;
- select a passage and comment from the inline affordance, context menu, or `⌘⌥M`;
Expand Down
2 changes: 2 additions & 0 deletions Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
<string>Copyright © 2026 OpenProse, Inc.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>LSUIElement</key>
<true/>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
Expand Down
Loading