Skip to content

Commit 4ca2110

Browse files
Pandey-JiiiSNGWN
authored andcommitted
v1.2.2: copy fidelity, multi-monitor fix, explicit entitlements
- Fix copy fidelity: store text verbatim (no leading/trailing trim) so paste from history reproduces what the user actually copied. - Fix multi-monitor placement: panel opens on the cursor's screen, not NSScreen.main. - Commit explicit `Clip Board.entitlements` with network.client and network.server set to false — reviewers can diff the signed binary against a source-controlled entitlements file with `codesign -d --entitlements -`. - Launch-at-login now defaults to OFF on fresh installs and only reconciles with SMAppService when the user has explicitly set the preference (no silent auto-registration). - Move ImageStore thumbnail downscale off the main thread. - Hoist ContentView snapshot off body-recompute path (onChange-driven). - Tighten logger privacy on error.localizedDescription to `.private`. - Cap AppIconProvider cache at 200 IDs (was unbounded). - Cache AppPaths URLs (no per-access fileExists syscall). - Drop dead HotkeyManager.unregisterHotkey(). - Drop ENABLE_TESTABILITY=YES (no test target consumes it). - Fix MACOSX_DEPLOYMENT_TARGET inconsistency (project 26.0 → 14.0). - Drop emoji from CFBundleDisplayName. - Docs: correct sandboxed app-support path in README; add pasteboard plaintext caveat and entitlements verification command in SECURITY.md.
1 parent ec3b0bf commit 4ca2110

8 files changed

Lines changed: 181 additions & 68 deletions

File tree

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,37 @@
22

33
All notable changes to this project are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows [Semantic Versioning](https://semver.org/).
44

5+
## [1.2.2] — 2026-05-30
6+
7+
### Fixed
8+
9+
- **Copy fidelity.** Leading/trailing whitespace on copied text is no longer stripped from the stored item. Previously, copying ` --flag` or ` :` would lose the leading space when pasted back from history; now items are stored byte-identical to what hit the clipboard. (Empty/whitespace-only copies are still skipped, and dedupe still applies — but on the raw value.)
10+
- **Multi-monitor panel placement.** The floating panel now opens on the screen the cursor is on, not the screen with the key window. Triggering the hotkey from a secondary display no longer clamps the panel onto your primary display.
11+
- **Thumbnail downscale moved off the main thread.** ImageIO downscale for newly captured screenshots now runs on the persistence I/O queue, eliminating a UI hitch when capturing large images.
12+
13+
### Changed
14+
15+
- **Launch-at-Login defaults to OFF.** Fresh installs no longer silently register themselves as a LaunchServices job; users opt in explicitly via the menu. Existing installs are unaffected (the stored preference takes precedence).
16+
- **App display name is now plain "Clip Board"** (dropped the `📎` emoji from `CFBundleDisplayName` — it rendered inconsistently in Spotlight/mini-bar contexts).
17+
- **Snapshot computation hoisted out of view body.** The filter/partition pass now runs only when its inputs (items, search text, visible limit) actually change, instead of on every hover/selection tick.
18+
19+
### Security / hygiene
20+
21+
- **Explicit `Clip Board.entitlements` checked in.** `com.apple.security.network.client` and `com.apple.security.network.server` are now explicitly `false` in a source-controlled file — reviewers can diff the signed binary's entitlements against the repo's source of truth with `codesign -d --entitlements - "Clip Board.app"`.
22+
- **Logger privacy tightened.** `error.localizedDescription` values are now logged with `privacy: .private` so disk paths or framework-derived text don't appear in system logs as `.public`.
23+
- **AppIconProvider cache capped** at 200 distinct bundle IDs (was unbounded).
24+
- **Sandboxed path corrected in docs.** The README and source comments now reflect that history actually lives under the app's sandbox container, not `~/Library/Application Support`.
25+
- **Pasteboard plaintext caveat** added to `SECURITY.md` (window between auto-paste and the next copy).
26+
27+
### Removed
28+
29+
- Dead `HotkeyManager.unregisterHotkey()` (never called).
30+
- Unused `import Combine` in `UIViews.swift`.
31+
- `ENABLE_TESTABILITY = YES` from Debug config (no test target consumes it).
32+
- Inconsistent `MACOSX_DEPLOYMENT_TARGET = 26.0` at the project level (target was 14.0; project now matches).
33+
34+
[1.2.2]: https://github.com/Light-House-Group/Clip-Board/releases/tag/v1.2.2
35+
536
## [1.2.1] — 2026-05-30
637

738
### Changed

Clip Board.xcodeproj/project.pbxproj

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,6 @@
162162
DEBUG_INFORMATION_FORMAT = dwarf;
163163
DEVELOPMENT_TEAM = 474XD43PW6;
164164
ENABLE_STRICT_OBJC_MSGSEND = YES;
165-
ENABLE_TESTABILITY = YES;
166165
ENABLE_USER_SCRIPT_SANDBOXING = YES;
167166
GCC_C_LANGUAGE_STANDARD = gnu17;
168167
GCC_DYNAMIC_NO_PIC = NO;
@@ -179,7 +178,7 @@
179178
GCC_WARN_UNUSED_FUNCTION = YES;
180179
GCC_WARN_UNUSED_VARIABLE = YES;
181180
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
182-
MACOSX_DEPLOYMENT_TARGET = 26.0;
181+
MACOSX_DEPLOYMENT_TARGET = 14.0;
183182
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
184183
MTL_FAST_MATH = YES;
185184
ONLY_ACTIVE_ARCH = YES;
@@ -237,7 +236,7 @@
237236
GCC_WARN_UNUSED_FUNCTION = YES;
238237
GCC_WARN_UNUSED_VARIABLE = YES;
239238
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
240-
MACOSX_DEPLOYMENT_TARGET = 26.0;
239+
MACOSX_DEPLOYMENT_TARGET = 14.0;
241240
MTL_ENABLE_DEBUG_INFO = NO;
242241
MTL_FAST_MATH = YES;
243242
SDKROOT = macosx;
@@ -252,17 +251,18 @@
252251
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
253252
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
254253
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
254+
CODE_SIGN_ENTITLEMENTS = "Clip Board/Clip Board.entitlements";
255255
CODE_SIGN_STYLE = Automatic;
256256
COMBINE_HIDPI_IMAGES = YES;
257-
CURRENT_PROJECT_VERSION = 4;
257+
CURRENT_PROJECT_VERSION = 5;
258258
DEVELOPMENT_TEAM = 474XD43PW6;
259259
ENABLE_APP_SANDBOX = YES;
260260
ENABLE_HARDENED_RUNTIME = YES;
261261
ENABLE_PREVIEWS = YES;
262262
ENABLE_USER_SELECTED_FILES = readonly;
263263
GENERATE_INFOPLIST_FILE = YES;
264264
INFOPLIST_FILE = "Clip-Board-Info.plist";
265-
INFOPLIST_KEY_CFBundleDisplayName = "📎 Clipboard";
265+
INFOPLIST_KEY_CFBundleDisplayName = "Clip Board";
266266
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
267267
INFOPLIST_KEY_LSUIElement = YES;
268268
INFOPLIST_KEY_NSHumanReadableCopyright = "";
@@ -271,7 +271,7 @@
271271
"@executable_path/../Frameworks",
272272
);
273273
MACOSX_DEPLOYMENT_TARGET = 14;
274-
MARKETING_VERSION = 1.2.1;
274+
MARKETING_VERSION = 1.2.2;
275275
PRODUCT_BUNDLE_IDENTIFIER = Siddharth.Sangwa.ClipBoard;
276276
PRODUCT_NAME = "$(TARGET_NAME)";
277277
REGISTER_APP_GROUPS = YES;
@@ -291,17 +291,18 @@
291291
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
292292
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
293293
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
294+
CODE_SIGN_ENTITLEMENTS = "Clip Board/Clip Board.entitlements";
294295
CODE_SIGN_STYLE = Automatic;
295296
COMBINE_HIDPI_IMAGES = YES;
296-
CURRENT_PROJECT_VERSION = 4;
297+
CURRENT_PROJECT_VERSION = 5;
297298
DEVELOPMENT_TEAM = 474XD43PW6;
298299
ENABLE_APP_SANDBOX = YES;
299300
ENABLE_HARDENED_RUNTIME = YES;
300301
ENABLE_PREVIEWS = YES;
301302
ENABLE_USER_SELECTED_FILES = readonly;
302303
GENERATE_INFOPLIST_FILE = YES;
303304
INFOPLIST_FILE = "Clip-Board-Info.plist";
304-
INFOPLIST_KEY_CFBundleDisplayName = "📎 Clipboard";
305+
INFOPLIST_KEY_CFBundleDisplayName = "Clip Board";
305306
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
306307
INFOPLIST_KEY_LSUIElement = YES;
307308
INFOPLIST_KEY_NSHumanReadableCopyright = "";
@@ -310,7 +311,7 @@
310311
"@executable_path/../Frameworks",
311312
);
312313
MACOSX_DEPLOYMENT_TARGET = 14;
313-
MARKETING_VERSION = 1.2.1;
314+
MARKETING_VERSION = 1.2.2;
314315
PRODUCT_BUNDLE_IDENTIFIER = Siddharth.Sangwa.ClipBoard;
315316
PRODUCT_NAME = "$(TARGET_NAME)";
316317
REGISTER_APP_GROUPS = YES;

Clip Board/AppAndCrypto.swift

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,19 @@ final class Preferences {
5757
}
5858

5959
var launchAtLogin: Bool {
60-
get { defaults.object(forKey: Keys.launchAtLogin) as? Bool ?? true }
60+
get { defaults.object(forKey: Keys.launchAtLogin) as? Bool ?? false }
6161
set {
6262
defaults.set(newValue, forKey: Keys.launchAtLogin)
6363
applyLaunchAtLogin(newValue)
6464
}
6565
}
6666

67+
/// Reconcile the SMAppService registration with the stored preference, but only when
68+
/// the user has *explicitly* set a value. A fresh install must not silently register
69+
/// itself with LaunchServices — that's exactly the kind of invisible-install behavior
70+
/// the rest of this app is built to avoid.
6771
func syncLaunchAtLoginOnStartup() {
72+
guard defaults.object(forKey: Keys.launchAtLogin) != nil else { return }
6873
applyLaunchAtLogin(launchAtLogin)
6974
}
7075

@@ -76,7 +81,7 @@ final class Preferences {
7681
if on { try SMAppService.mainApp.register() }
7782
else { try SMAppService.mainApp.unregister() }
7883
} catch {
79-
Log.app.error("Launch-at-login toggle failed: \(error.localizedDescription, privacy: .public)")
84+
Log.app.error("Launch-at-login toggle failed: \(error.localizedDescription, privacy: .private)")
8085
}
8186
}
8287
}
@@ -185,23 +190,29 @@ final class KeyManager {
185190

186191
// MARK: - App Support Paths
187192

188-
/// Centralized on-disk locations, each created with tight permissions on first access.
193+
/// Centralized on-disk locations, each created with tight permissions once at first use.
194+
///
195+
/// Under the app sandbox, `applicationSupportDirectory` resolves to the container path
196+
/// (`~/Library/Containers/<bundle-id>/Data/Library/Application Support/ClipboardManager`),
197+
/// not the global `~/Library/Application Support`. Either way, it's per-user and
198+
/// inaccessible to other apps without the user explicitly granting access.
189199
enum AppPaths {
190-
/// `~/Library/Application Support/ClipboardManager` (0700).
191-
static var base: URL {
200+
/// `…/Application Support/ClipboardManager` (0700). Cached — created once.
201+
static let base: URL = {
192202
let fm = FileManager.default
193-
let appSupport = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
203+
let appSupport = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
204+
?? fm.temporaryDirectory
194205
let folder = appSupport.appendingPathComponent("ClipboardManager", isDirectory: true)
195206
ensureDirectory(folder)
196207
return folder
197-
}
208+
}()
198209

199-
/// `…/ClipboardManager/images` (0700) — encrypted per-image files.
200-
static var imagesFolder: URL {
210+
/// `…/ClipboardManager/images` (0700) — encrypted per-image files. Cached.
211+
static let imagesFolder: URL = {
201212
let folder = base.appendingPathComponent("images", isDirectory: true)
202213
ensureDirectory(folder)
203214
return folder
204-
}
215+
}()
205216

206217
private static func ensureDirectory(_ url: URL) {
207218
let fm = FileManager.default
@@ -210,7 +221,7 @@ enum AppPaths {
210221
try fm.createDirectory(at: url, withIntermediateDirectories: true,
211222
attributes: [.posixPermissions: 0o700])
212223
} catch {
213-
Log.persistence.error("Failed to create \(url.lastPathComponent, privacy: .public): \(error.localizedDescription, privacy: .public)")
224+
Log.persistence.error("Failed to create \(url.lastPathComponent, privacy: .public): \(error.localizedDescription, privacy: .private)")
214225
}
215226
}
216227
}
@@ -249,21 +260,25 @@ final class ImageStore {
249260
}
250261

251262
/// Encrypts and writes PNG bytes asynchronously (file mode 0600). Seeds the in-memory
252-
/// caches synchronously so the new item is renderable before the write completes.
263+
/// `pendingPNG` synchronously so the new item is already renderable (decode via
264+
/// `loadPNG`) before the write lands; the ImageIO thumbnail downscale runs on `ioQueue`
265+
/// (large screenshots are too heavy to decode on main).
253266
func save(png: Data, fileName: String) {
254267
pendingLock.lock(); pendingPNG[fileName] = png; pendingLock.unlock()
255-
if let thumb = Self.makeThumbnail(from: png, maxPixelSize: 800) {
256-
thumbnailCache.setObject(thumb, forKey: cacheKey(fileName, 800))
257-
}
258268

259269
let url = url(for: fileName)
270+
let thumbKey = cacheKey(fileName, 800)
260271
ioQueue.async {
272+
// NSCache is thread-safe; populate the thumbnail off-main.
273+
if let thumb = Self.makeThumbnail(from: png, maxPixelSize: 800) {
274+
self.thumbnailCache.setObject(thumb, forKey: thumbKey)
275+
}
261276
do {
262277
let encrypted = try KeyManager.shared.encrypt(data: png)
263278
try encrypted.write(to: url, options: .atomic)
264279
try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
265280
} catch {
266-
Log.persistence.error("Image save failed: \(error.localizedDescription, privacy: .public)")
281+
Log.persistence.error("Image save failed: \(error.localizedDescription, privacy: .private)")
267282
}
268283
self.pendingLock.lock(); self.pendingPNG[fileName] = nil; self.pendingLock.unlock()
269284
}
@@ -378,7 +393,7 @@ final class PersistenceManager {
378393
try encrypted.write(to: url, options: .atomic)
379394
try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
380395
} catch {
381-
Log.persistence.error("Save failed: \(error.localizedDescription, privacy: .public)")
396+
Log.persistence.error("Save failed: \(error.localizedDescription, privacy: .private)")
382397
}
383398
}
384399
}
@@ -390,14 +405,14 @@ final class PersistenceManager {
390405
do {
391406
data = try Data(contentsOf: url)
392407
} catch {
393-
Log.persistence.error("Failed to read history file: \(error.localizedDescription, privacy: .public)")
408+
Log.persistence.error("Failed to read history file: \(error.localizedDescription, privacy: .private)")
394409
return []
395410
}
396411
let decrypted: Data
397412
do {
398413
decrypted = try KeyManager.shared.decrypt(data)
399414
} catch {
400-
Log.persistence.error("Decrypt failed; quarantining file. \(error.localizedDescription, privacy: .public)")
415+
Log.persistence.error("Decrypt failed; quarantining file. \(error.localizedDescription, privacy: .private)")
401416
quarantine(url: url)
402417
return []
403418
}
@@ -418,9 +433,9 @@ final class PersistenceManager {
418433
let target = url.deletingLastPathComponent().appendingPathComponent("history.broken-\(ts)")
419434
do {
420435
try FileManager.default.moveItem(at: url, to: target)
421-
Log.persistence.info("Quarantined corrupt history to \(target.lastPathComponent, privacy: .public)")
436+
Log.persistence.info("Quarantined corrupt history to \(target.lastPathComponent, privacy: .private)")
422437
} catch {
423-
Log.persistence.error("Quarantine failed: \(error.localizedDescription, privacy: .public)")
438+
Log.persistence.error("Quarantine failed: \(error.localizedDescription, privacy: .private)")
424439
}
425440
}
426441
}
@@ -434,7 +449,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
434449
func applicationDidFinishLaunching(_ notification: Notification) {
435450
// 1. Encryption key must exist before persistence load.
436451
do { try KeyManager.shared.ensureKeyExists() }
437-
catch { Log.crypto.error("ensureKeyExists failed: \(error.localizedDescription, privacy: .public)") }
452+
catch { Log.crypto.error("ensureKeyExists failed: \(error.localizedDescription, privacy: .private)") }
438453

439454
// 2. Load persisted history.
440455
itemsVM = ItemsViewModel()

Clip Board/Clip Board.entitlements

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.app-sandbox</key>
6+
<true/>
7+
<key>com.apple.security.network.client</key>
8+
<false/>
9+
<key>com.apple.security.network.server</key>
10+
<false/>
11+
<key>com.apple.security.device.audio-input</key>
12+
<false/>
13+
<key>com.apple.security.device.camera</key>
14+
<false/>
15+
<key>com.apple.security.personal-information.location</key>
16+
<false/>
17+
<key>com.apple.security.personal-information.addressbook</key>
18+
<false/>
19+
<key>com.apple.security.personal-information.calendars</key>
20+
<false/>
21+
<key>com.apple.security.files.user-selected.read-only</key>
22+
<true/>
23+
</dict>
24+
</plist>

Clip Board/History.swift

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -81,18 +81,20 @@ final class ItemsViewModel: ObservableObject {
8181

8282
private func scheduleSave() { saveSubject.send(()) }
8383

84-
/// Adds a text item; preserves internal whitespace/newlines, only trims leading/trailing,
85-
/// and truncates beyond `maxItemChars`. Dedupes by exact equality of the stored text
86-
/// (an existing match moves to the top and refreshes its date + source app).
84+
/// Adds a text item. Stores the copied text **verbatim** (whitespace preserved) so that
85+
/// pasting from history reproduces what the user actually copied — important for things
86+
/// like ` --flag`, ` :`, or anything where a leading/trailing space carries meaning.
87+
/// Truncates beyond `maxItemChars`. Dedupes by exact-equal stored text (an existing match
88+
/// moves to the top and refreshes its date + source app).
8789
func addText(_ text: String, sourceBundleID: String? = nil, sourceAppName: String? = nil) {
88-
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
89-
guard !trimmed.isEmpty else { return }
90+
// Skip all-whitespace copies, but DO NOT mutate the stored value with trim.
91+
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
9092

9193
let stored: String
92-
if trimmed.count > Self.maxItemChars {
93-
stored = String(trimmed.prefix(Self.maxItemChars)) + Self.truncationMarker
94+
if text.count > Self.maxItemChars {
95+
stored = String(text.prefix(Self.maxItemChars)) + Self.truncationMarker
9496
} else {
95-
stored = trimmed
97+
stored = text
9698
}
9799

98100
if let existingIndex = items.firstIndex(where: { !$0.isImage && $0.text == stored }) {
@@ -391,18 +393,6 @@ final class HotkeyManager {
391393
}
392394
}
393395

394-
func unregisterHotkey() {
395-
if let ref = hotKeyRef {
396-
let status = UnregisterEventHotKey(ref)
397-
if status != noErr { Log.hotkey.error("Unregister hotkey failed: \(status)") }
398-
hotKeyRef = nil
399-
}
400-
if let handlerRef = eventHandlerRef {
401-
RemoveEventHandler(handlerRef)
402-
eventHandlerRef = nil
403-
}
404-
handler = nil
405-
}
406396
}
407397

408398
// MARK: - Auto Paster
@@ -518,8 +508,12 @@ final class AutoPaster {
518508

519509
/// Resolves and caches app icons by bundle identifier, for the per-item source-app chip.
520510
/// Main-thread only (NSWorkspace); cheap and cached, so safe to call during view body.
511+
/// Capped at `cacheLimit` distinct bundle IDs — well above any realistic distinct-app
512+
/// count for one user's clipboard history, but bounded so a long-lived session can't grow
513+
/// without limit.
521514
enum AppIconProvider {
522515
private static var cache: [String: NSImage] = [:]
516+
private static let cacheLimit = 200
523517

524518
static func icon(forBundleID id: String?) -> NSImage? {
525519
guard let id, !id.isEmpty else { return nil }
@@ -530,7 +524,10 @@ enum AppIconProvider {
530524
} else if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: id) {
531525
icon = NSWorkspace.shared.icon(forFile: url.path)
532526
}
533-
if let icon { cache[id] = icon }
527+
if let icon {
528+
if cache.count >= cacheLimit { cache.removeAll(keepingCapacity: true) }
529+
cache[id] = icon
530+
}
534531
return icon
535532
}
536533
}

0 commit comments

Comments
 (0)