Skip to content

Commit 826774a

Browse files
committed
Add reapply mode to eliminate cursor flash on Helper startup
- Refactor applyCape() to applyCapeInternal(dictionary, isReapply) - Add applyCapeReapply() and applyCapeAtPathReapply() functions - Update listen.m: all 3 call sites use reapply mode (startSessionMonitor, UserSpaceChanged, reconfigurationCallback) - Add MCLibraryController applyCapeReapply: method - AppState.swift: smart dispatch based on MCAppliedCursor comparison - Update CLAUDE.md: document reapply mode architecture Reapply mode skips resetAllCursors() + backupAllCursors() when re-applying the same cape, eliminating the visible flash back to system default cursors. CGSRegisterCursorWithImages() can directly overwrite registered cursors. Fresh apply (with reset) is preserved for: switching capes, first-time application, CLI mousecloak apply, and explicit reset to default. (cherry picked from commit e45cf6fd7f204288bfecb52e6a5a743a009da351)
1 parent 336e3af commit 826774a

4 files changed

Lines changed: 73 additions & 10 deletions

File tree

Mousecape/Mousecape

Mousecape/mousecloak/apply.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ extern BOOL applyCursorForIdentifier(NSUInteger frameCount, CGFloat frameDuratio
1818
extern BOOL applyCapeForIdentifier(NSDictionary *cursor, NSString *identifier, BOOL restore);
1919
extern BOOL applyCape(NSDictionary *dictionary);
2020
extern BOOL applyCapeAtPath(NSString *path);
21+
// Reapply mode: skip resetAllCursors() + backupAllCursors() to avoid cursor flash.
22+
// Use when re-applying the same cape (Helper startup, session change, display reconfiguration).
23+
extern BOOL applyCapeReapply(NSDictionary *dictionary);
24+
extern BOOL applyCapeAtPathReapply(NSString *path);
2125

2226
NS_ASSUME_NONNULL_END
2327

Mousecape/mousecloak/apply.m

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -268,14 +268,18 @@ BOOL applyCapeForIdentifier(NSDictionary *cursor, NSString *identifier, BOOL res
268268
return applyCursorForIdentifier(frameCount.unsignedIntegerValue, frameDuration.doubleValue, hotSpot, size, images, identifier, 0);
269269
}
270270

271-
BOOL applyCape(NSDictionary *dictionary) {
271+
// Internal implementation with reapply mode support.
272+
// When isReapply is YES, skip resetAllCursors() + backupAllCursors() to avoid
273+
// the visible flash when re-applying the same cape (e.g. Helper startup, session change).
274+
// CGSRegisterCursorWithImages() can directly overwrite already-registered cursors.
275+
static BOOL applyCapeInternal(NSDictionary *dictionary, BOOL isReapply) {
272276
@autoreleasepool {
273277
NSDictionary *cursors = dictionary[MCCursorDictionaryCursorsKey];
274278
NSString *name = dictionary[MCCursorDictionaryCapeNameKey];
275279
NSNumber *version = dictionary[MCCursorDictionaryCapeVersionKey];
276280

277281
MMLog("========================================");
278-
MMLog("=== APPLYING CAPE ===");
282+
MMLog("=== APPLYING CAPE (%s) ===", isReapply ? "REAPPLY" : "FRESH");
279283
MMLog("========================================");
280284
MMLog("Cape name: %s", name.UTF8String);
281285
MMLog("Cape identifier: %s", [dictionary[MCCursorDictionaryIdentifierKey] UTF8String]);
@@ -286,10 +290,14 @@ BOOL applyCape(NSDictionary *dictionary) {
286290
MMLog(" - %s", key.UTF8String);
287291
}
288292

289-
MMLog("--- Calling resetAllCursors ---");
290-
resetAllCursors();
291-
MMLog("--- Calling backupAllCursors ---");
292-
backupAllCursors();
293+
if (!isReapply) {
294+
MMLog("--- Calling resetAllCursors ---");
295+
resetAllCursors();
296+
MMLog("--- Calling backupAllCursors ---");
297+
backupAllCursors();
298+
} else {
299+
MMLog("--- Skipping reset/backup (reapply mode) ---");
300+
}
293301

294302
MMLog("--- Applying cursors ---");
295303

@@ -344,6 +352,14 @@ BOOL applyCape(NSDictionary *dictionary) {
344352
}
345353
}
346354

355+
BOOL applyCape(NSDictionary *dictionary) {
356+
return applyCapeInternal(dictionary, NO);
357+
}
358+
359+
BOOL applyCapeReapply(NSDictionary *dictionary) {
360+
return applyCapeInternal(dictionary, YES);
361+
}
362+
347363
BOOL applyCapeAtPath(NSString *path) {
348364
MMLog("========================================");
349365
MMLog("=== applyCapeAtPath ===");
@@ -386,3 +402,46 @@ BOOL applyCapeAtPath(NSString *path) {
386402
MMLog(BOLD RED "Could not parse valid cape file" RESET);
387403
return NO;
388404
}
405+
406+
BOOL applyCapeAtPathReapply(NSString *path) {
407+
MMLog("========================================");
408+
MMLog("=== applyCapeAtPathReapply ===");
409+
MMLog("========================================");
410+
MMLog("Input path: %s", path ? path.UTF8String : "(null)");
411+
412+
// Validate path
413+
if (!path || path.length == 0) {
414+
MMLog(BOLD RED "Invalid path" RESET);
415+
return NO;
416+
}
417+
418+
// Resolve symlinks and check for path traversal
419+
NSString *realPath = [path stringByResolvingSymlinksInPath];
420+
NSString *standardPath = [realPath stringByStandardizingPath];
421+
422+
MMLog("Real path: %s", realPath.UTF8String);
423+
MMLog("Standard path: %s", standardPath.UTF8String);
424+
MMLog("File exists: %s", [[NSFileManager defaultManager] fileExistsAtPath:standardPath] ? "YES" : "NO");
425+
MMLog("File readable: %s", [[NSFileManager defaultManager] isReadableFileAtPath:standardPath] ? "YES" : "NO");
426+
427+
// Validate file extension
428+
if (![[standardPath pathExtension] isEqualToString:@"cape"]) {
429+
MMLog(BOLD RED "Invalid file extension - must be .cape" RESET);
430+
return NO;
431+
}
432+
433+
// Check file exists and is readable
434+
if (![[NSFileManager defaultManager] isReadableFileAtPath:standardPath]) {
435+
MMLog(BOLD RED "File not readable at path" RESET);
436+
return NO;
437+
}
438+
439+
MMLog("Loading cape file...");
440+
NSDictionary *cape = [NSDictionary dictionaryWithContentsOfFile:standardPath];
441+
if (cape) {
442+
MMLog("Cape file loaded successfully, applying in reapply mode...");
443+
return applyCapeReapply(cape);
444+
}
445+
MMLog(BOLD RED "Could not parse valid cape file" RESET);
446+
return NO;
447+
}

Mousecape/mousecloak/listen.m

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ static void UserSpaceChanged(SCDynamicStoreRef store, CFArrayRef changedKeys, vo
7676

7777
// Only attempt to apply if there's a valid cape path
7878
if (appliedPath) {
79-
BOOL success = applyCapeAtPath(appliedPath);
79+
BOOL success = applyCapeAtPathReapply(appliedPath);
8080
MMLog("Apply result: %s", success ? "SUCCESS" : "FAILED");
8181
if (!success) {
8282
MMLog(BOLD RED "Application of cape failed" RESET);
@@ -107,7 +107,7 @@ void reconfigurationCallback(CGDirectDisplayID display,
107107
NSString *capePath = appliedCapePathForUser(NSUserName());
108108
MMLog("Cape path: %s", capePath ? capePath.UTF8String : "(none)");
109109
if (capePath) {
110-
BOOL success = applyCapeAtPath(capePath);
110+
BOOL success = applyCapeAtPathReapply(capePath);
111111
MMLog("Apply result: %s", success ? "SUCCESS" : "FAILED");
112112
}
113113
float scale;
@@ -227,7 +227,7 @@ void startSessionMonitor(void) {
227227
// Apply the cape for the user on load (if configured)
228228
NSString *initialCapePath = appliedCapePathForUser(NSUserName());
229229
if (initialCapePath) {
230-
BOOL applySuccess = applyCapeAtPath(initialCapePath);
230+
BOOL applySuccess = applyCapeAtPathReapply(initialCapePath);
231231
MMLog("Initial apply result: %s", applySuccess ? "SUCCESS" : "FAILED");
232232
} else {
233233
MMLog("No cape configured - running in standby mode");

0 commit comments

Comments
 (0)