Skip to content

Fix data loss: save-and-delete in the favorites store deleted from the main list - #12

Open
MiMoHo wants to merge 1 commit into
haad:masterfrom
MiMoHo:favorites-save-delete-wrong-store
Open

Fix data loss: save-and-delete in the favorites store deleted from the main list#12
MiMoHo wants to merge 1 commit into
haad:masterfrom
MiMoHo:favorites-save-delete-wrong-store

Conversation

@MiMoHo

@MiMoHo MiMoHo commented Jul 30, 2026

Copy link
Copy Markdown

Data loss

Shift+S in the bezel saves the selected clipping to a file and then removes it. With the favorites store active (f in the bezel) it removed the wrong clipping, from the wrong list:

bool success = [flycutOperator saveFromStack];      // reads the favorites store — correct
[self performSelector:@selector(hideApp) withObject:nil afterDelay:0.2];
[self restoreStashedStoreAndUpdate];               // swaps the store AND the position back
if ( success && shift ) [flycutOperator clearItemAtStackPosition];

restoreStashedStore swaps clippingStore back to the main store and swaps stackPosition with stashedStackPosition. The delete that follows therefore hits the main list, at whatever position the user happened to be at there before pressing f.

So the favorite was saved and kept, and an unrelated clipping — one the user was never shown and never selected — was destroyed instead. Silently.

Fix

Delete while the store that was saved from is still the current one. clearItemAtStackPosition then removes exactly the clipping that was saved, and the hideApp delay and the restore follow unchanged.

Without an active stash restoreStashedStore returns NO, so plain Shift+S in the main list takes the same path as before — the behaviour change is confined to the favorites store.

Verification

A test program drives the real FlycutOperator and models both orders. Main list MAIN0..MAIN5 with the cursor at position 3, then f, favorites FAV0..FAV2 with the cursor at position 1, then Shift+S:

before (delete after restore):
  saved: FAV1
  main list  6 -> 5   MAIN5, MAIN4, MAIN3, MAIN1, MAIN0     <- MAIN2 destroyed
  favorites  3 -> 3   FAV2, FAV1, FAV0                      <- untouched

after (delete before restore):
  saved: FAV1
  main list  6 -> 6   MAIN5, MAIN4, MAIN3, MAIN2, MAIN1, MAIN0
  favorites  3 -> 2   FAV2, FAV0                            <- exactly FAV1 removed

The test deliberately does not call saveFromStack — its role in the defect is only to return success, and it writes a file to the Desktop — and it forces saveForgottenClippings, saveForgottenFavorites and savePreference off so nothing touches the disk.

Builds with Xcode 26.5 (BUILD SUCCEEDED), 76 warnings, identical to master built the same way.

Scope, and what is deliberately left alone

This changes only the order of the delete and the store restore in the bezel's s/S handler. Auditing the area turned up several other things around the favorites stash. They are pre-existing, independent of this change, and left for separate PRs so this one stays reviewable:

  • No cancel path restores the stash: Esc, hideApp, windowDidResignKey: and applicationWillResignActive: never call restoreStashedStoreAndUpdate, so after f + Esc the favorites store stays active and later copies land there.
  • saveEngine writes the current clippingStore under the jcList key without knowing about stashedStore, so while swapped it persists the favorites list as the main history. This is the root cause of the point above.
  • checkRememberNumPref:forPrimaryStore: passes YES unconditionally at one call site, so cancelling the resize warning for the favorites store writes its value under rememberNum.
  • saveFromStore:atIndex:withPrefix: ignores the BOOL from writeToFile: and always returns YES, so Shift+S deletes even when the file was not written.

One honest note about this change: with savePreference >= 2 ("after each change"), the now-earlier clearItemAtStackPosition triggers actionAfterListModification while the store is still swapped, so one saveEngine runs against the favorites store. That is the pre-existing aliasing above rather than something this patch introduces — on master the same write happened, and it persisted the wrong deletion. It corrects itself on the next list change or on quit. Fixing it properly means making saveEngine stash-aware, which belongs in its own change.

Not verified here

The bezel's own appearance during the ~0.2 s before it hides — one updateBezel on the favorites store, then the restore's updateBezel on the main store — was assessed from the code only. Keyboard synthesis is blocked by TCC from a terminal context, so the bezel cannot be driven programmatically. If you would like that frame checked on screen before merging, say so.


Authored and reviewed with Claude Code using Claude Opus 5 at xhigh reasoning effort. Cross-checked by a multi-agent adversarial review (independent Opus 5 agents on correctness, side effects, same-class defects elsewhere, and upstream fit; every finding then put to three skeptics tasked with refuting it — 20 raised, 6 survived, 14 refuted, and the surviving ones are the scope notes above). The patch applies cleanly to master and to all nine other open branches from this fork.

🤖 Generated with Claude Code

Shift+S in the bezel saves the selected clipping to a file and then removes it.
With the favorites store active ('f' in the bezel) it removed the wrong
clipping, from the wrong list:

    bool success = [flycutOperator saveFromStack];       // reads the favorites store
    [self performSelector:@selector(hideApp) ...];
    [self restoreStashedStoreAndUpdate];                 // swaps the store AND the position
    if ( success && shift ) [flycutOperator clearItemAtStackPosition];

-restoreStashedStore swaps clippingStore back to the main store and swaps
stackPosition with stashedStackPosition, so the delete that follows hits the
main list at whatever position the user had been at there before pressing 'f'.
The favorite was saved and kept, and an unrelated clipping - one the user was
never shown - was destroyed instead.

Deleting before the restore fixes it: while the store that was saved from is
still the current one, clearItemAtStackPosition removes exactly the clipping
that was saved. Without an active stash -restoreStashedStore returns NO and
the path is unchanged, so plain Shift+S in the main list behaves exactly as
before.

Verified with a test program driving the real FlycutOperator, modelling both
orders (main list MAIN0..MAIN5 with the cursor at position 3, then 'f',
favorites FAV0..FAV2 with the cursor at position 1, then Shift+S):

    before: main 6 -> 5, "MAIN2" gone, favorites unchanged   (data loss)
    after:  main unchanged at 6, favorites 3 -> 2, FAV1 gone (correct)

Builds with Xcode 26.5, 76 warnings, identical to master.

Co-Authored-By: Claude <noreply@anthropic.com>
@MiMoHo

MiMoHo commented Jul 30, 2026

Copy link
Copy Markdown
Author

Follow-up: #13 makes saveEngine stash-aware, which is the change this PR promised as separate. It also corrects an assessment in this PR's write-up — the aliasing described here as "transient … corrects itself on the next list change or on quit" is not transient: quitting is what makes it permanent, and with savePreference at "after each change" it needs no quit at all. Details and the before/after measurement are in #13.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant