Skip to content

Commit f2e584e

Browse files
sdmj76claude
andcommitted
Fix Quick Look & Thumbnail review issues
- Add UTImportedTypeDeclarations to Thumbnail Info.plist (was missing, could prevent macOS from associating .cur/.ani with the thumbnail extension) - Remove force unwraps in fallback image paths (PreviewProvider, ThumbnailProvider) - Fix Timer concurrency: use Timer() + RunLoop.main.add with @mainactor dispatch - Add size guard in trimForegroundBounds to avoid timeout on large images - Gate NSLog behind #if DEBUG, use Logger for production - Log actual parse errors instead of silently discarding with try? - Mark static helpers as nonisolated to fix actor isolation warnings - Update submodule with review fixes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> (cherry picked from commit eaef3791e62dcb2d2ad66b96ac2acbf2a58148ad)
1 parent 880f366 commit f2e584e

5 files changed

Lines changed: 134 additions & 51 deletions

File tree

Mousecape/Mousecape

Mousecape/Mousecape.xcodeproj/project.pbxproj

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -893,10 +893,10 @@
893893
APPLICATION_EXTENSION_API_ONLY = YES;
894894
CODE_SIGN_ENTITLEMENTS = MousecapeQuickLook/MousecapeQuickLook.entitlements;
895895
CODE_SIGN_IDENTITY = "Apple Development";
896-
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
896+
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
897897
CODE_SIGN_STYLE = Automatic;
898898
CURRENT_PROJECT_VERSION = 1;
899-
DEVELOPMENT_TEAM = LZ4NKVK3NY;
899+
DEVELOPMENT_TEAM = "";
900900
ENABLE_APP_SANDBOX = YES;
901901
INFOPLIST_FILE = MousecapeQuickLook/Info.plist;
902902
LD_RUNPATH_SEARCH_PATHS = (
@@ -920,10 +920,10 @@
920920
APPLICATION_EXTENSION_API_ONLY = YES;
921921
CODE_SIGN_ENTITLEMENTS = MousecapeQuickLook/MousecapeQuickLook.entitlements;
922922
CODE_SIGN_IDENTITY = "Apple Development";
923-
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
923+
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
924924
CODE_SIGN_STYLE = Automatic;
925925
CURRENT_PROJECT_VERSION = 1;
926-
DEVELOPMENT_TEAM = LZ4NKVK3NY;
926+
DEVELOPMENT_TEAM = "";
927927
ENABLE_APP_SANDBOX = YES;
928928
INFOPLIST_FILE = MousecapeQuickLook/Info.plist;
929929
LD_RUNPATH_SEARCH_PATHS = (
@@ -946,10 +946,10 @@
946946
APPLICATION_EXTENSION_API_ONLY = YES;
947947
CODE_SIGN_ENTITLEMENTS = MousecapeQuickLookThumbnail/MousecapeQuickLookThumbnail.entitlements;
948948
CODE_SIGN_IDENTITY = "Apple Development";
949-
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
949+
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
950950
CODE_SIGN_STYLE = Automatic;
951951
CURRENT_PROJECT_VERSION = 1;
952-
DEVELOPMENT_TEAM = LZ4NKVK3NY;
952+
DEVELOPMENT_TEAM = "";
953953
ENABLE_APP_SANDBOX = YES;
954954
INFOPLIST_FILE = MousecapeQuickLookThumbnail/Info.plist;
955955
LD_RUNPATH_SEARCH_PATHS = (
@@ -973,10 +973,10 @@
973973
APPLICATION_EXTENSION_API_ONLY = YES;
974974
CODE_SIGN_ENTITLEMENTS = MousecapeQuickLookThumbnail/MousecapeQuickLookThumbnail.entitlements;
975975
CODE_SIGN_IDENTITY = "Apple Development";
976-
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
976+
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
977977
CODE_SIGN_STYLE = Automatic;
978978
CURRENT_PROJECT_VERSION = 1;
979-
DEVELOPMENT_TEAM = LZ4NKVK3NY;
979+
DEVELOPMENT_TEAM = "";
980980
ENABLE_APP_SANDBOX = YES;
981981
INFOPLIST_FILE = MousecapeQuickLookThumbnail/Info.plist;
982982
LD_RUNPATH_SEARCH_PATHS = (

Mousecape/MousecapeQuickLook/PreviewProvider.swift

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import Foundation
44
import QuickLookUI
55
import OSLog
66

7+
private struct UncheckedSendableWrapper<T>: @unchecked Sendable {
8+
let value: T
9+
init(_ value: T) { self.value = value }
10+
}
11+
712
@MainActor
813
final class PreviewViewController: NSViewController, @preconcurrency QLPreviewingController {
914
private struct PreviewPayload {
@@ -20,7 +25,10 @@ final class PreviewViewController: NSViewController, @preconcurrency QLPreviewin
2025
private var currentFrameIndex = 0
2126

2227
private func trace(_ message: String) {
28+
#if DEBUG
2329
NSLog("[MousecapeQuickLook] \(message)")
30+
#endif
31+
logger.debug("\(message, privacy: .public)")
2432
}
2533

2634
override func loadView() {
@@ -55,12 +63,13 @@ final class PreviewViewController: NSViewController, @preconcurrency QLPreviewin
5563
animationFrames = []
5664
currentFrameIndex = 0
5765

58-
DispatchQueue.global(qos: .userInitiated).async {
66+
let sendableHandler = UncheckedSendableWrapper(handler)
67+
Task.detached(priority: .userInitiated) {
5968
let payload = Self.decodePreviewPayload(for: url)
6069

61-
DispatchQueue.main.async { [weak self] in
70+
await MainActor.run { [weak self] in
6271
guard let self else {
63-
handler(nil)
72+
sendableHandler.value(nil)
6473
return
6574
}
6675

@@ -82,13 +91,18 @@ final class PreviewViewController: NSViewController, @preconcurrency QLPreviewin
8291
}
8392

8493
self.trace("preview image assigned")
85-
handler(nil)
94+
sendableHandler.value(nil)
8695
}
8796
}
8897
}
8998

90-
private static func decodePreviewPayload(for fileURL: URL) -> PreviewPayload? {
91-
guard let parsed = try? WindowsCursorParser.parse(fileURL: fileURL) else {
99+
private nonisolated static func decodePreviewPayload(for fileURL: URL) -> PreviewPayload? {
100+
let parsed: WindowsCursorParseResult
101+
do {
102+
parsed = try WindowsCursorParser.parse(fileURL: fileURL)
103+
} catch {
104+
let logger = Logger(subsystem: "com.sdmj76.Mousecape.QuickLook", category: "PreviewProvider")
105+
logger.error("Cursor parse failed: \(error.localizedDescription, privacy: .public)")
92106
return nil
93107
}
94108

@@ -105,7 +119,7 @@ final class PreviewViewController: NSViewController, @preconcurrency QLPreviewin
105119
)
106120
}
107121

108-
private static func extractFrames(from parsed: WindowsCursorParseResult) -> [CGImage] {
122+
private nonisolated static func extractFrames(from parsed: WindowsCursorParseResult) -> [CGImage] {
109123
if parsed.frameCount <= 1 {
110124
return [parsed.image]
111125
}
@@ -130,14 +144,15 @@ final class PreviewViewController: NSViewController, @preconcurrency QLPreviewin
130144
guard animationFrames.count > 1 else { return }
131145
stopAnimation()
132146
let interval = max(frameDuration, 1.0 / 60.0)
133-
animationTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
134-
guard let self else { return }
135-
self.currentFrameIndex = (self.currentFrameIndex + 1) % self.animationFrames.count
136-
self.imageView.image = self.animationFrames[self.currentFrameIndex]
137-
}
138-
if let animationTimer {
139-
RunLoop.main.add(animationTimer, forMode: .common)
147+
let timer = Timer(timeInterval: interval, repeats: true) { [weak self] _ in
148+
Task { @MainActor in
149+
guard let self else { return }
150+
self.currentFrameIndex = (self.currentFrameIndex + 1) % self.animationFrames.count
151+
self.imageView.image = self.animationFrames[self.currentFrameIndex]
152+
}
140153
}
154+
RunLoop.main.add(timer, forMode: .common)
155+
animationTimer = timer
141156
}
142157

143158
private func stopAnimation() {
@@ -166,26 +181,35 @@ final class PreviewViewController: NSViewController, @preconcurrency QLPreviewin
166181
bitmapInfo: bitmapInfo
167182
) else {
168183
logger.fault("Failed to create fallback CGContext")
169-
return icon.cgImage(forProposedRect: nil, context: nil, hints: nil) ?? CGImage(
170-
width: 1,
171-
height: 1,
172-
bitsPerComponent: 8,
173-
bitsPerPixel: 32,
174-
bytesPerRow: 4,
175-
space: colorSpace,
176-
bitmapInfo: CGBitmapInfo(rawValue: bitmapInfo),
177-
provider: CGDataProvider(data: Data([200, 200, 200, 255]) as CFData)!,
178-
decode: nil,
179-
shouldInterpolate: false,
180-
intent: .defaultIntent
181-
)!
184+
// Return a minimal 1x1 transparent image as absolute last resort
185+
let pixel = Data([0, 0, 0, 0])
186+
guard let p = CGDataProvider(data: pixel as CFData),
187+
let img = CGImage(width: 1, height: 1, bitsPerComponent: 8, bitsPerPixel: 32,
188+
bytesPerRow: 4, space: colorSpace,
189+
bitmapInfo: CGBitmapInfo(rawValue: bitmapInfo),
190+
provider: p, decode: nil, shouldInterpolate: false,
191+
intent: .defaultIntent) else {
192+
// Truly impossible to fail with valid constant data, but satisfy the compiler
193+
fatalError("Cannot create 1x1 fallback CGImage")
194+
}
195+
return img
182196
}
183197

184198
context.setFillColor(CGColor(gray: 0.85, alpha: 1.0))
185199
context.fill(CGRect(x: 0, y: 0, width: width, height: height))
186200
context.setStrokeColor(CGColor(gray: 0.65, alpha: 1.0))
187201
context.setLineWidth(4)
188202
context.stroke(CGRect(x: 8, y: 8, width: width - 16, height: height - 16))
189-
return context.makeImage()!
203+
guard let result = context.makeImage() else {
204+
logger.fault("Failed to create fallback image from CGContext")
205+
let pixel = Data([200, 200, 200, 255])
206+
let p = CGDataProvider(data: pixel as CFData)!
207+
return CGImage(width: 1, height: 1, bitsPerComponent: 8, bitsPerPixel: 32,
208+
bytesPerRow: 4, space: colorSpace,
209+
bitmapInfo: CGBitmapInfo(rawValue: bitmapInfo),
210+
provider: p, decode: nil, shouldInterpolate: false,
211+
intent: .defaultIntent)!
212+
}
213+
return result
190214
}
191215
}

Mousecape/MousecapeQuickLookThumbnail/Info.plist

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,51 @@
3737
<key>NSExtensionPrincipalClass</key>
3838
<string>$(PRODUCT_MODULE_NAME).ThumbnailProvider</string>
3939
</dict>
40+
<key>UTImportedTypeDeclarations</key>
41+
<array>
42+
<dict>
43+
<key>UTTypeConformsTo</key>
44+
<array>
45+
<string>public.image</string>
46+
<string>public.data</string>
47+
</array>
48+
<key>UTTypeDescription</key>
49+
<string>Windows Cursor</string>
50+
<key>UTTypeIdentifier</key>
51+
<string>com.sdmj76.mousecape.windows-cur</string>
52+
<key>UTTypeTagSpecification</key>
53+
<dict>
54+
<key>public.filename-extension</key>
55+
<array>
56+
<string>cur</string>
57+
</array>
58+
<key>public.mime-type</key>
59+
<array>
60+
<string>image/x-win-bitmap</string>
61+
</array>
62+
</dict>
63+
</dict>
64+
<dict>
65+
<key>UTTypeConformsTo</key>
66+
<array>
67+
<string>public.data</string>
68+
</array>
69+
<key>UTTypeDescription</key>
70+
<string>Windows Animated Cursor</string>
71+
<key>UTTypeIdentifier</key>
72+
<string>com.sdmj76.mousecape.windows-ani</string>
73+
<key>UTTypeTagSpecification</key>
74+
<dict>
75+
<key>public.filename-extension</key>
76+
<array>
77+
<string>ani</string>
78+
</array>
79+
<key>public.mime-type</key>
80+
<array>
81+
<string>application/x-navi-animation</string>
82+
</array>
83+
</dict>
84+
</dict>
85+
</array>
4086
</dict>
4187
</plist>

Mousecape/MousecapeQuickLookThumbnail/ThumbnailProvider.swift

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,30 @@ final class ThumbnailProvider: QLThumbnailProvider {
5555
let colorSpace = CGColorSpaceCreateDeviceRGB()
5656
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue
5757
let data = Data(repeating: 0, count: width * height * 4)
58-
let provider = CGDataProvider(data: data as CFData)!
59-
return CGImage(
60-
width: width,
61-
height: height,
62-
bitsPerComponent: 8,
63-
bitsPerPixel: 32,
64-
bytesPerRow: width * 4,
65-
space: colorSpace,
66-
bitmapInfo: CGBitmapInfo(rawValue: bitmapInfo),
67-
provider: provider,
68-
decode: nil,
69-
shouldInterpolate: false,
70-
intent: .defaultIntent
71-
)!
58+
guard let provider = CGDataProvider(data: data as CFData),
59+
let fallback = CGImage(
60+
width: width,
61+
height: height,
62+
bitsPerComponent: 8,
63+
bitsPerPixel: 32,
64+
bytesPerRow: width * 4,
65+
space: colorSpace,
66+
bitmapInfo: CGBitmapInfo(rawValue: bitmapInfo),
67+
provider: provider,
68+
decode: nil,
69+
shouldInterpolate: false,
70+
intent: .defaultIntent
71+
) else {
72+
// Last resort: return the parsed image or a 1x1 transparent pixel
73+
let pixel = Data([0, 0, 0, 0])
74+
let p = CGDataProvider(data: pixel as CFData)!
75+
return CGImage(width: 1, height: 1, bitsPerComponent: 8, bitsPerPixel: 32,
76+
bytesPerRow: 4, space: colorSpace,
77+
bitmapInfo: CGBitmapInfo(rawValue: bitmapInfo),
78+
provider: p, decode: nil, shouldInterpolate: false,
79+
intent: .defaultIntent)!
80+
}
81+
return fallback
7282
}
7383

7484
private static func aspectFitRect(contentSize: CGSize, in bounds: CGRect) -> CGRect {
@@ -86,6 +96,9 @@ final class ThumbnailProvider: QLThumbnailProvider {
8696
let height = image.height
8797
guard width > 0, height > 0 else { return nil }
8898

99+
// Skip trimming for unusually large images to stay within the system time budget.
100+
guard width * height <= 512 * 512 else { return nil }
101+
89102
let bytesPerPixel = 4
90103
let bytesPerRow = width * bytesPerPixel
91104
let colorSpace = CGColorSpaceCreateDeviceRGB()

0 commit comments

Comments
 (0)