CleanCopy is a macOS menu bar utility designed to streamline the process of sharing web links. Its primary function is to take a URL copied to the clipboard, fetch the title of the corresponding webpage, and replace the clipboard content with a rich text (RTF) link using the fetched title as the display text.
The application can operate in two modes:
- Manual Mode: The user explicitly clicks "Convert URL" in the menu bar to process the current clipboard content.
- Automatic Mode (enabled by default): The application monitors the clipboard for changes and automatically attempts to convert copied URLs. The user can toggle this mode off via the menu bar item.
The application is built using SwiftUI for the menu bar interface and leverages standard macOS frameworks like AppKit, UserNotifications, ServiceManagement, Foundation, and Combine for core functionalities. It runs as an "accessory" application, meaning it doesn't have a Dock icon or a main application window, operating solely through its menu bar icon.
The conversion process is managed by the ClipboardManager.
A. Manual Trigger:
- User copies a URL to the system clipboard (e.g.,
https://www.example.com). - User clicks the CleanCopy menu bar icon and selects the "Convert URL" menu item.
- This triggers the
processClipboardContentmethod.
B. Automatic Trigger (when "Auto Convert" is enabled):
- User copies a URL to the system clipboard.
- Clipboard Monitoring: A
Timerpolls theNSPasteboard.general.changeCountevery second. This monitoring starts automatically when the app launches. - Change Detection: If the
changeCountdiffers from the last known value:- The
ClipboardManagerchecks if the clipboard content contains Rich Text (.rtfor.rtfd). If so, it ignores the change to prevent processing its own output or other rich text. - If the content is likely plain text, it triggers the
processClipboardContentmethod.
- The
C. Core Conversion Process (processClipboardContent):
- Read Clipboard: The application reads the current string content from the general pasteboard (
NSPasteboard.general). - Validate URL: It checks if the clipboard string is a valid URL with a scheme (e.g.,
http,https). If not, the process stops silently. - Fetch Page Title (Asynchronous):
- A
URLSessiondata task is initiated to fetch the HTML content of the URL. - A timeout is set to prevent indefinite waiting.
- A
- Parse Title:
- The fetched HTML data is decoded (trying UTF-8 then ISO Latin 1).
- A regular expression (
<title[^>]*>(.*?)</title>) is used to extract the content of the<title>tag. - Whitespace is trimmed from the extracted title.
- Handle Title Fallbacks:
- If no title tag is found, the title is empty after trimming, or the HTML cannot be decoded, the URL's host or the full URL string is used as a fallback title.
- Handle Fetch Errors:
- If a network error occurs during the fetch (
URLSessionerror), a user notification is displayed indicating the failure. The clipboard is updated with a link using the URL itself as the text only if the clipboard content hasn't changed again since the fetch began.
- If a network error occurs during the fetch (
- Create Rich Text Link:
- An
NSAttributedStringis created with the determined title (fetched or fallback). - The
.linkattribute is applied to the string, pointing to the original URL.
- An
- Update Clipboard & Notify:
- The system pasteboard's contents are cleared.
- The newly created
NSAttributedString(rich text link) is written to the pasteboard. - The internal
lastChangeCountis updated to match the pasteboard's new state. - A user notification is displayed confirming the successful update (requires notification permissions).
The menu also provides standard "About" and "Quit" options.
The application's startup and background behavior are managed primarily by the AppDelegate:
- Activation Policy: On launch (
applicationDidFinishLaunching), the app's activation policy is set to.accessory, hiding the Dock icon and preventing a main window from appearing. - Notification Permissions & Prompt:
- The app requests user authorization for sending notifications (used for error reporting and success confirmation).
- It sets the
AppDelegateas the notification center delegate to handle foreground notifications. - After requesting authorization, it checks the current notification settings. If notifications are explicitly denied, and the user hasn't been prompted before (tracked via
UserDefaults), it displays anNSAlertexplaining the benefits of notifications and provides a button to open System Settings directly to the app's notification preferences.
- Login Item Prompt (First Launch):
- The app checks
UserDefaultsto see if it has previously prompted the user about launching at login. - If not prompted before, it displays an
NSAlertasking the user if they want CleanCopy to start automatically on login. - If the user agrees, it uses the
ServiceManagementframework (SMAppService.mainApp.register()) to register itself as a login item. This relies on configuration within the app'sInfo.plist.
- The app checks
The application follows a relatively simple structure based on SwiftUI's App lifecycle and standard AppKit patterns:
CleanCopyApp(struct): The main entry point conforming to SwiftUI'sAppprotocol. It defines theMenuBarExtrascene which creates the menu bar icon and its associated menu. It initializes and holds theClipboardManageras a@StateObject.AppDelegate(class): AnNSObjectconforming toNSApplicationDelegateandUNUserNotificationCenterDelegate. It's integrated into the SwiftUI lifecycle via@NSApplicationDelegateAdaptor. Handles application-level events like launch, termination (optional), notification handling (including checking status and prompting if denied), and the login item registration logic.ClipboardManager(class): AnObservableObjectcontaining the core logic for the application's primary function. It manages:- The state for automatic conversion (
@Published var isAutoConvertEnabled), which defaults totrue. - A
Timerfor polling the clipboard when automatic mode is active. Monitoring is started automatically during initialization because the default state is enabled. - Tracking the last pasteboard
changeCountto detect changes. - The
processClipboardContentmethod containing the steps for reading the clipboard, validating URLs, fetching/parsing web page titles, creating rich text links, handling errors, and updating the clipboard. - Methods to
startMonitoring(),stopMonitoring(), andtoggleAutoConvert(). - Triggering user notifications for success and errors.
- The state for automatic conversion (
MenuBarExtra(SwiftUI Scene): Defines the UI element in the system menu bar, including its icon (link.circle) and the menu items:- "Convert URL": Manually triggers
processClipboardContent. - "Auto Convert" (with checkmark): Toggles the
isAutoConvertEnabledstate inClipboardManagerviatoggleAutoConvert(). The checkmark (checkmark/ empty space) reflects the current state, starting as checked by default. - "About": Shows a standard About dialog.
- "Quit": Terminates the application (ensuring the monitoring timer is stopped).
- "Convert URL": Manually triggers
graph TD
subgraph User Interaction
direction LR
UserCopy[User Copies URL] --> PB(NSPasteboard);
UserClickManual[User Clicks "Convert URL"] --> CM(ClipboardManager);
UserClickToggle[User Clicks "Auto Convert"] --> CM;
end
subgraph CleanCopyApp UI
direction LR
MB["MenuBarExtra"]
MB -- Displays --> ToggleState{Auto Convert State (Default: On)};
MB -- Action --> UserClickManual;
MB -- Action --> UserClickToggle;
end
subgraph ClipboardManager Logic
direction TB
style AutoConvertState fill:#lightgrey,stroke:#333,stroke-width:2px
Init[Initialization] --> AutoConvertState[@Published isAutoConvertEnabled = true];
Init --> StartMonitor[startMonitoring()]; # Start timer on init
AutoConvertState -- Controls --> TimerManagement;
UserClickToggle --> ToggleMethod[toggleAutoConvert()];
ToggleMethod --> AutoConvertState;
subgraph TimerManagement
direction LR
ToggleMethod -- Enables --> StartMonitor;
ToggleMethod -- Disables --> StopMonitor[stopMonitoring()];
StartMonitor --> Timer{Timer (1s)};
StopMonitor --> Timer_Invalidate[Timer Invalidate];
Timer -- Fires --> CheckClipboard[checkClipboard()];
end
CheckClipboard -- Reads --> PB_ChangeCount{Pasteboard.changeCount};
CheckClipboard --> CompareCount{Compare with lastChangeCount};
CompareCount -- No Change --> EndPoll(End Poll Cycle);
CompareCount -- Change Detected --> UpdateLastCount[Update lastChangeCount];
UpdateLastCount --> CheckPBTypes{Check Pasteboard Types};
CheckPBTypes -- Contains RTF? --> EndPoll;
CheckPBTypes -- Plain Text? --> ProcessContent[processClipboardContent()];
UserClickManual -- Triggers --> ProcessContent;
subgraph Core Processing
direction TB
ProcessContent -- Reads --> PB_String{Pasteboard String};
ProcessContent --> ValidateURL{Validate URL};
ValidateURL -- Invalid --> EndProcess(End Processing);
ValidateURL -- Valid --> FetchTitle[Fetch Page Title Async];
FetchTitle -- Failure --> HandleError[handleError()];
FetchTitle -- Success --> CreateLink[createRichTextLink()];
HandleError -- If Clipboard Unchanged --> CreateFallbackLink[createRichTextLink (Fallback)];
HandleError --> NotifyError[Show Error Notification];
CreateFallbackLink --> UpdatePB_Error[Update Pasteboard];
CreateLink --> UpdatePB_Success[Update Pasteboard];
UpdatePB_Success --> UpdateLastCountSuccess[Update lastChangeCount];
UpdatePB_Success --> NotifySuccess[Show Success Notification];
UpdatePB_Error --> UpdateLastCountError[Update lastChangeCount];
end
end
CoreProcessing -- Writes --> PB;
PB -- changeCount --> ClipboardManager_Logic;
PB -- String --> ClipboardManager_Logic;
PB -- Types --> ClipboardManager_Logic;
CleanCopyApp_UI -- Observes --> AutoConvertState;
- Development: The project is built using Xcode and the Swift toolchain.
- Makefile: A
Makefileis provided to simplify common tasks:make build(ormake): Compiles the Debug configuration of the app into the./builddirectory.make run: Kills any existing instance, builds (if needed), and runs the Debug app.make package: Builds the app (if needed) and creates a distributable DMG file (CleanCopy.dmg) in the project root using thecreate-dmgutility.make clean: Removes the./builddirectory and theCleanCopy.dmgfile.make reset: Cleans build artifacts, preferences, and attempts removal from /Applications.
- Prerequisites:
- Xcode and Command Line Tools.
create-dmg(installable via Homebrew:brew install create-dmg) is required for themake packagecommand.
Key files and directories in the project:
CleanCopy/CleanCopyApp.swift: Contains the main SwiftUIAppstruct,AppDelegate, andClipboardManagerclass.CleanCopy/Info.plist: Application configuration file (bundle identifier, version, login item helper configuration, etc.).CleanCopy/CleanCopy.entitlements: Defines application sandbox entitlements/permissions.CleanCopy/Assets.xcassets: Contains app icons and other image assets.CleanCopy.xcodeproj/: Xcode project file and settings.Makefile: Defines build, package, and clean commands.README.md: Basic project description and build instructions.dmg-resources/: Contains assets used during DMG creation (background image, license)..gitignore: Specifies intentionally untracked files for Git.
The project relies primarily on standard macOS frameworks provided by Apple:
- SwiftUI: For defining the
MenuBarExtrainterface. - AppKit: Underlying framework for macOS applications, used for
NSApplication,NSPasteboard,NSAlert,NSAttributedString,NSWorkspace, etc. - UserNotifications: For requesting permissions, checking settings, and displaying system notifications.
- ServiceManagement: For registering the application as a login item.
- Foundation: Core utilities (URL, URLSession, String, Error handling, UserDefaults, Timer, etc.).
- Combine: Used for the
@Publishedproperty wrapper inObservableObject(ClipboardManager).
No external third-party libraries are used.