From 6669e99e0d3139b2d759029f517e0b4f37e121ca Mon Sep 17 00:00:00 2001 From: Pawan Sedara Date: Sun, 26 Jul 2026 17:11:12 +1000 Subject: [PATCH] fix: start cleaning on first launch --- README.md | 14 ++-- .../PlainLinkMenu/Sources/PlainLinkMenu.swift | 71 +++++++++++-------- docs/LAUNCH.md | 1 + docs/MENUBAR.md | 3 +- docs/releases/v0.1.0-preview.3.md | 2 + tests/macos_app_bundle.rs | 4 +- 6 files changed, 59 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 3c018ab..e7532ae 100644 --- a/README.md +++ b/README.md @@ -28,16 +28,22 @@ PlainLink is the open-source, local-first URL-cleaning engine with transparent, ## Install the developer preview > [!WARNING] -> PlainLink is early developer-preview software for technical testers. The app is ad-hoc signed, but it is not yet Developer ID-signed or notarized. macOS Gatekeeper will warn on first launch. Please expect rough edges and report incorrect URL changes. +> PlainLink is early developer-preview software for technical testers. The app is ad-hoc signed, but it is not yet Developer ID-signed or notarized. macOS may say the downloaded app is damaged until its quarantine attribute is removed. Please expect rough edges and report incorrect URL changes. 1. Open [GitHub Releases](https://github.com/HexCodeYT/PlainLink/releases) and download the ZIP for your Mac: - The file ending in `-macos-arm64.zip` for Apple silicon (M1, M2, M3, M4, or newer). - The file ending in `-macos-x86_64.zip` for an Intel Mac. 2. Open the ZIP and drag `PlainLink.app` into `/Applications`. -3. In Finder, open **Applications**, Control-click `PlainLink`, choose **Open**, then choose **Open** again. -4. PlainLink appears in the menu bar and starts watching copied URLs. +3. Open Terminal and remove quarantine from this app only, then launch it: -If macOS does not show the second **Open** button, try launching PlainLink once, then go to **System Settings → Privacy & Security**, find the blocked-app message, and choose **Open Anyway**. + ```sh + xattr -dr com.apple.quarantine /Applications/PlainLink.app + open /Applications/PlainLink.app + ``` + +4. PlainLink appears in the menu bar and starts watching copied URLs automatically. + +The `xattr` command only removes macOS's downloaded-file quarantine marker from `PlainLink.app`. If you do not want to do that, use the source build below instead. If the [Releases](https://github.com/HexCodeYT/PlainLink/releases) page has no ZIP for your Mac, use the source build below. Signing and notarisation are pending; the preview ZIP and its checksum will be clearly labelled on the release. diff --git a/app/macos/PlainLinkMenu/Sources/PlainLinkMenu.swift b/app/macos/PlainLinkMenu/Sources/PlainLinkMenu.swift index 63bdfc7..5703254 100644 --- a/app/macos/PlainLinkMenu/Sources/PlainLinkMenu.swift +++ b/app/macos/PlainLinkMenu/Sources/PlainLinkMenu.swift @@ -179,7 +179,7 @@ private final class PlainLinkCommand { private final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { private let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) private let runner = PlainLinkCommand() - private let firstRunKey = "PlainLinkHasSeenFirstRun" + private let automaticEnableKey = "PlainLinkHasAutomaticallyEnabledWatcherV1" private var watcherStatus: WatcherStatus = .unknown("Checking status...") private var isWorking = false private var refreshTimer: Timer? @@ -199,7 +199,7 @@ private final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate configureStatusButton() rebuildMenu() refreshStatus() - showFirstRunIfNeeded() + enableCleaningOnFirstLaunch() refreshTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { [weak self] _ in self?.refreshStatus() @@ -442,7 +442,7 @@ private final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate } @objc private func showGettingStarted() { - showFirstRunGuide() + showGettingStartedGuide(cleaningEnabled: watcherStatus == .running) } @objc private func quit() { @@ -508,42 +508,53 @@ private final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate } } - private func showFirstRunIfNeeded() { - guard !UserDefaults.standard.bool(forKey: firstRunKey) else { + private func enableCleaningOnFirstLaunch() { + guard !UserDefaults.standard.bool(forKey: automaticEnableKey) else { return } - UserDefaults.standard.set(true, forKey: firstRunKey) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in - self?.showFirstRunGuide() + guard let self else { + return + } + + self.runCommand( + title: "Start PlainLink", + arguments: ["install", "--interval-ms", "\(self.selectedInterval)"] + ) { [weak self] result in + guard let self else { + return + } + + if result.succeeded { + UserDefaults.standard.set(true, forKey: self.automaticEnableKey) + self.showGettingStartedGuide(cleaningEnabled: true) + } else { + self.showCommandResult(title: "Could not start cleaning", result: result) + } + } } } - private func showFirstRunGuide() { + private func showGettingStartedGuide(cleaningEnabled: Bool) { let alert = NSAlert() - alert.messageText = "PlainLink lives in your menu bar" - alert.informativeText = [ - "Enable cleaning when you are ready.", - "", - "PlainLink runs locally, watches copied URLs, removes known tracking parameters, and keeps the last original link so you can restore it.", - "", - "You can pause cleaning, run doctor, and copy diagnostics from the menu." - ].joined(separator: "\n") + alert.messageText = cleaningEnabled ? "PlainLink is cleaning copied links" : "PlainLink lives in your menu bar" + alert.informativeText = cleaningEnabled + ? [ + "Cleaning started automatically.", + "", + "PlainLink runs locally, watches copied URLs, removes known tracking parameters, and keeps the last original link so you can restore it.", + "", + "You can pause cleaning, run doctor, and copy diagnostics from the menu." + ].joined(separator: "\n") + : [ + "Cleaning is currently off. Choose Enable Cleaning from the PlainLink menu to start the watcher.", + "", + "PlainLink runs locally and preserves the last original link so you can restore it." + ].joined(separator: "\n") alert.alertStyle = .informational - - let canEnable = watcherStatus != .running - alert.addButton(withTitle: canEnable ? "Enable Cleaning" : "OK") - - if canEnable { - alert.addButton(withTitle: "Not Now") - } - - let response = runAlert(alert) - - if canEnable && response == .alertFirstButtonReturn { - toggleCleaning() - } + alert.addButton(withTitle: "OK") + showAlert(alert) } private func showCommandResult(title: String, result: CommandResult) { diff --git a/docs/LAUNCH.md b/docs/LAUNCH.md index 388fd0a..93f6111 100644 --- a/docs/LAUNCH.md +++ b/docs/LAUNCH.md @@ -23,6 +23,7 @@ Secondary angles by audience: - [ ] Publish one public GitHub Release with one obvious macOS artifact and checksum. - [ ] Make sure the release clearly says whether the artifact is an ad-hoc-signed, unnotarized preview or a Developer ID-signed/notarized build. - [ ] Make the GitHub repository README the HN landing page, with the release download and source-build path visible above the fold. +- [x] Document the exact `xattr` quarantine workaround required by the ad-hoc-signed preview. - [x] Record a 10 to 15 second demo showing copy, automatic cleanup, and paste. - [ ] Capture screenshots for the menu bar, inspection result, and restore flow. - [ ] Enable GitHub Discussions for rule requests, testing reports, ideas, and show-and-tell posts. diff --git a/docs/MENUBAR.md b/docs/MENUBAR.md index 2126612..2ded5db 100644 --- a/docs/MENUBAR.md +++ b/docs/MENUBAR.md @@ -93,7 +93,8 @@ flowchart TB ## Design Notes - The app is a user-level status bar app with `LSUIElement`. -- The first-run dialog is intentionally short and can enable cleaning directly. +- On first launch, the app installs and starts the watcher automatically, then confirms that cleaning is active. +- After a user pauses cleaning, later app launches preserve that choice; cleaning can be re-enabled from the menu. - The app icon is generated from reviewable Swift drawing code instead of a checked-in binary source asset. - The app shells out to the embedded `plainlink` binary instead of duplicating core logic. - `plainlink install` copies the embedded CLI to the stable user path before starting the watcher. diff --git a/docs/releases/v0.1.0-preview.3.md b/docs/releases/v0.1.0-preview.3.md index d289e58..ad9aaba 100644 --- a/docs/releases/v0.1.0-preview.3.md +++ b/docs/releases/v0.1.0-preview.3.md @@ -10,6 +10,8 @@ PlainLink v0.1.0 Preview 3 is an ad-hoc-signed, unnotarized technical preview fo - Fixture-backed rule verification. - HN-ready installation, Gatekeeper, privacy, and technical-preview guidance. - Multi-resolution app icon verification during packaging. +- Automatic watcher installation and startup on first launch. +- Exact recovery steps for macOS's misleading downloaded-app “damaged” warning. ## Artifact diff --git a/tests/macos_app_bundle.rs b/tests/macos_app_bundle.rs index d4da195..bdef032 100644 --- a/tests/macos_app_bundle.rs +++ b/tests/macos_app_bundle.rs @@ -43,7 +43,9 @@ fn macos_menu_app_assets_are_wired() { assert!(source.contains("--smoke-test")); assert!(source.contains("PlainLinkIntervalMilliseconds")); - assert!(source.contains("PlainLinkHasSeenFirstRun")); + assert!(source.contains("PlainLinkHasAutomaticallyEnabledWatcherV1")); + assert!(source.contains("enableCleaningOnFirstLaunch")); + assert!(source.contains("Cleaning started automatically.")); assert!(source.contains("Getting Started")); assert!(source.contains("PlainLink lives in your menu bar")); assert!(plist.contains("LSUIElement"));