Skip to content

fix(macos): the donut opens and flips while another app holds Secure Input - #855

Merged
KnifeLemon merged 3 commits into
KnifeLemon:mainfrom
lightscape-jm:fix/macos-secure-input
Nov 3, 2026
Merged

KnifeLemon merged 3 commits into
KnifeLemon:mainfrom
lightscape-jm:fix/macos-secure-input

Conversation

@lightscape-jm

Copy link
Copy Markdown
Contributor

Note

Standalone fix, based on main (26cc434a), no dependency on any other PR. Two commits, each reviewable on its
own:
the first makes the donut open, the second makes Tab flip it. Together they fix one problem; apart, the second
can't be tried, as the donut wouldn't open.

What does this change?

On macOS, while another app holds Secure Input, Filee's donut stops working:

  1. The Option / Command gestures don't start, so the drag donut never opens.
  2. Tab (or the flip key set in Shortcuts) doesn't flip a donut that is already open.

Terminal holds Secure Input whenever Secure Keyboard Entry is on, and sometimes keeps it after it leaves the front.
Password fields hold it too. Anyone who uses Terminal, or Filee's Open in Terminal action, can hit this.

Both come from the same rule: while another app holds Secure Input, macOS hides keys from anything that watches the
keyboard from outside, which is how Filee's global hook works. Windows and Linux are unchanged.

Type of change

  • Bug fix
  • New feature or improvement
  • New format or engine
  • Translation
  • Documentation
  • Build, CI or dependencies

How to reproduce

You need something holding Secure Input while it's not the app in front. Either of these:

  • A helper (repeatable): save this as hold.swift, run swift hold.swift in Terminal, then switch to Finder
    within the minute. It holds Secure Input from the background, which is what Terminal did in the reports.

    // Holds Secure Input for 60 s from the background, then lets go.
    import Carbon
    import Foundation
    EnableSecureEventInput()
    print("holding Secure Input: \(IsSecureEventInputEnabled())")
    Thread.sleep(forTimeInterval: 60)
    DisableSecureEventInput()
  • Terminal: tick Terminal → Secure Keyboard Entry. Terminal normally lets go when it leaves the front, so this
    reproduces only when it doesn't (it happened twice in two days here, with no trigger found).

Then, in Finder, Option-drag a picture:

main This PR
Option-drag in Finder nothing (the gesture never starts) the donut opens
Tab while holding it over the donut nothing flips, every press
Mouse wheel over the donut during the drag nothing flips (the window gets the wheel itself; tried on the same change in a build with other branches)
Drop on a slice (no donut) converts; Finder is in front again afterwards

Commit 1: the gestures start (read modifier keys…)

Cause, measured: libuiohook (via SharpHook) rebuilds the modifier mask of every event from
CGEventSourceKeyState, which answers "up" for held keys while another app holds Secure Input. In a diagnostic build,
Option was logged going down, and the next click, with Option still held, arrived with mask None. Without Secure
Input the same call says "down". CGEventSourceFlagsState and the event's own flags stay right (they are what
NSEvent.modifierFlags reports).

Change: IPlatformServices.CurrentModifiers(): null by default; on macOS the flags state. TriggerService takes a
mouse gesture's modifiers from it when present: at a press, and on moves while a button is held (a modifier pressed
after the button). Buttons still come from the hook.

Not fixable here: the Ctrl+Option+Space keyboard shortcut, as Secure Input withholds the key press itself.

Commit 2: Tab flips (the flip key works…)

Cause, measured (diagnostic build, Secure Input held from the background; the donut open, waiting for the flip key):

Secure Input on Secure Input off
Key events reaching the global hook none (only modifier changes) every press
CGEventSourceKeyState(kVK_Tab), polled every 15 ms never down (0 / 468 samples) down while pressed (44 / 479)
Key presses reaching the donut's window once it has the keyboard every press (4 / 4) every press

No way of watching the keyboard sees Tab under Secure Input, but the app in front always gets its keys.

Change: on macOS the drag donut takes the keyboard once files are dragged onto it (RadialController.TakeKeyboard),
and Tab reaches its window like any key press. The drag carries on.

  • When: when files are dragged onto it, not when the gesture starts: a modifier-drag without files (a rubber-band
    selection) takes nothing.
  • One flip per press (按 Tab 时圆环有时连续翻转两次,或停留在同一面 #504): while the window has the keyboard the hook's copy of a press is ignored
    (HookFlipsKey). That follows the window's IsActive value through PropertyChanged, not the Activated event,
    which can come before the value changes.
  • Giving it back: IPlatformServices.RememberFrontApp / ReturnFocus (NSWorkspace.frontmostApplication,
    NSRunningApplication.activateWithOptions:). The keyboard goes back once a conversion starts or the donut closes,
    unless the user has clicked into another app meanwhile. It stays with Filee when the drop opens a window of Filee's own
    (a tool, the merge order, the save dialog of Ask every time), or that window would open behind Finder.
  • Not on Windows: the donut window's own comment says taking the keyboard during a drag breaks Explorer's drag.

How was it tested?

  • dotnet format --verify-no-changes
  • dotnet test
  • Tried it in the app: macOS 26.6.2, Apple Silicon, this branch alone with default settings, Secure Input held
    from the background (the helper above).

Test suites, this branch and main at 26cc434a on the same Mac:

Project main This PR
Filee.App.Tests 1,631 passed, 10 failed, 10 skipped 1,636 passed (the 5 new tests), 10 failed, 10 skipped
Filee.Cli.Tests 55 passed, 1 skipped 55 passed, 1 skipped
Filee.Core.Tests 784 passed, 2 skipped 784 passed, 2 skipped
Filee.Engines.Tests 2,087 passed, 5 failed, 60 skipped 2,087 passed, 5 failed, 60 skipped
Filee.Platform.Tests 95 passed, 27 skipped 95 passed, 27 skipped
Filee.Setup.Tests 156 passed, 49 failed, 1 skipped 156 passed, 49 failed, 1 skipped

Every failure is the same test on both, by name, so none comes from this PR. On this Mac:

  • App (10): First_run_engine_setup_renders (7 languages), Engines_page_shows_versions_descriptions_and_download_speed,
    EnginesPageTests.A_second_engine_can_be_installed_while_one_downloads_and_waits_its_turn and
    DonutHighlightTests.A_lost_animation_frame_does_not_keep_the_slice_dark.
  • Engines (5): Homebrew's FFmpeg here has no Vorbis, Theora or AMR encoder (Encoder not found), and one
    LibreOffice ODS / XLS round trip.
  • Setup (49): the Windows installer's tests, run on macOS.

New and changed tests:

  • TriggerServiceLifecycleTests: a press takes the modifiers the platform reports when the hook's mask lost them; a
    modifier pressed after the button counts; without a platform reading (Windows, Linux) the hook's mask decides.
  • DonutFlipTests (macOS only, skipped elsewhere): the donut takes the keyboard only once files are on it; the window's
    press alone flips (the Secure Input case); with the hook's copy too it flips once; after a click elsewhere the hook
    flips again; the keyboard is given back after a conversion and kept for the crop tool's window.
  • The two existing drag-mode hook tests take the keyboard away first on macOS, so they keep testing the hook path
    Windows and Linux use.

Do the tests guard the fix? I undid each part and checked that a test fails every time:

Undone Caught by
Press modifiers from the platform A_press_takes_the_modifiers_the_platform_reports…, A_modifier_pressed_after_the_button_counts…
Modifiers on moves while a button is held A_modifier_pressed_after_the_button_counts…
The donut takes the keyboard both macOS flip tests
The hook ignored while the window has the keyboard both macOS flip tests
The flip source follows IsActive both macOS flip tests
The keyboard kept for a tool's window …stays_for_a_tools_window

One of these caught a gap in my own test: A_modifier_pressed_after_the_button_counts… passed without the move change,
because the hook thread sometimes handled the press after the test had already set the modifier. It now waits for the
press first.

About #784 (drops on a lit slice sometimes not delivered on macOS): one suspect there is the donut window changing
during the drag, and commit 2 makes the window take the keyboard mid-drag. Measured on this Mac, drops on lit slices
without Secure Input, 15 drags each onto the same lit slice:

Drops delivered Drag briefly left the window before the drop
main 15 / 15 2 times, in 1 drag (the first after launch)
This PR 15 / 15 12 times, in 6 drags

So no drop was lost, but taking the keyboard does make macOS report the drag leaving and re-entering the donut more
often. I tried the alternative that avoids it: making the donut the key window without activating Filee (it is
already a non-activating panel). Tab arrived (30 / 30) and the extra leaves were gone (0), but the keyboard then stayed
with Filee after the drop, even with the donut closed, and asking Finder to take it back does nothing while Finder is
still the active app. I found no public way to hand it back, so this PR activates Filee and steps back afterwards. If
the extra leaves turn out to matter for #784, the key-window route is where I'd look.

Screenshots (UI changes)

No visual change.

Checklist

  • New UI strings are in all src/Filee.App/Assets/i18n/*.json files (none added)
  • New third-party code or binaries are listed in THIRD-PARTY-NOTICES.md (none added)
  • The PR does one thing (unrelated changes go in a separate PR)

Not in this PR

  • Upstream: libuiohook could read CGEventSourceFlagsState itself; commit 1 can go once SharpHook does. I can open
    that issue upstream if you'd like.
  • Terminal keeping Secure Input from the background is Terminal's behaviour, not Filee's.

🤖 Generated with Claude Code

lightscape-jm and others added 2 commits October 30, 2026 17:11
…ds Secure Input

While another app holds Secure Input (Terminal's Secure Keyboard Entry
left on in the background, a password field), the donut's Option/Command
gestures stopped in Filee: every press arrived without modifiers.

SharpHook's libuiohook rebuilds its modifier mask on every event with
CGEventSourceKeyState, which answers "up" for held keys while another
app holds Secure Input. CGEventSourceFlagsState and the event's own
flags stay right (what NSEvent.modifierFlags reports, and what other
gesture apps read).

- IPlatformServices.CurrentModifiers(): null by default; on macOS the
  flags state (CGEventSourceFlagsState).
- TriggerService takes mouse gesture modifiers from it when present: at
  a press, and on moves while a button is held. Buttons still come from
  the hook. Windows and Linux are unchanged.
- The Ctrl+Option+Space shortcut can't be helped: Secure Input withholds
  the key press itself from other apps.

Checked on macOS 26.6 with a helper app holding Secure Input: without
the change Option+drag does nothing, with it the donut opens. To drop
once SharpHook reads the flags state itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
During a drag the donut heard the flip key only through the global hook, and while another app holds Secure
Input (Terminal with Secure Keyboard Entry, which can keep it from the background) macOS gives no keys to a
hook: Tab never flipped the donut. Measured: no key events reached the hook and CGEventSourceKeyState never read
Tab down, while the app in front still got every key.

On macOS the drag donut now takes the keyboard once files are dragged onto it, and Tab reaches its window like
any key press; the drag carries on. The hook's copy of a press is ignored meanwhile (one flip per press). The
keyboard goes back to the app that had it once a conversion starts or the donut closes, unless the user clicked
elsewhere, and stays with Filee when the drop opens a window of its own (a tool, the merge order, the save
dialog). Windows is unchanged: taking the keyboard there breaks Explorer's drag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the bug Something isn't working label Oct 30, 2026

@KnifeLemon KnifeLemon left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @lightscape-jm, this is a careful piece of work: the measurements make both causes clear, and the "undone → caught by" table is exactly what I'd want for a change like this. I've started CI (first-time contributors need a maintainer to approve the run).

The code reads right to me. Commit 1 is contained: CurrentModifiers() is null everywhere but macOS, and on macOS it only replaces the mask at presses and held-button moves. Commit 2 changes how the drag donut behaves on every Mac, not only under Secure Input, so I'd like it tried on another Mac before merging. A few things I'd like checked there:

  1. Filee's own windows during a drag. With the main window or Settings open (behind Finder), Option-drag a picture onto the donut. Does Filee's other window stay where it was, or does it come to the front over Finder when the donut takes the keyboard?
  2. No Dock icon. The same drag and Tab flip with Show in Dock off (Filee as an accessory app), and Finder in front again after the drop.
  3. Without Secure Input. Tab flips exactly once per press (#504), and drops on lit slices still convert (#784).

@SeoJCalive 이 PR도 Mac에서 확인 부탁드려도 될까요? CI가 끝나면 portable-osx-arm64 아티팩트로 위 1–3번을 봐 주시면 돼요. 재현이 필요하면 PR 본문의 hold.swift로 백그라운드에서 Secure Input을 잡을 수 있어요(Option-드래그로 도넛이 뜨는지, Tab으로 뒤집히는지).

On the upstream note: an issue on libuiohook would be welcome if you'd like to open it yourself; we'll drop commit 1 once SharpHook reads the flags state.

…acos-secure-input

# Conflicts:
#	src/Filee.Core/Platform/IPlatformServices.cs
#	src/Filee.Platform.MacOS/MacOSPlatformServices.cs
@lightscape-jm

Copy link
Copy Markdown
Contributor Author

Thanks for the review and for starting CI!

I merged main to resolve the conflict with #808 (RestoreWindow and the new keyboard calls sit side by side; the donut gives the keyboard back before it hides, and the window class is restored after). Format check and the donut, trigger and platform tests pass; the one failing test, A_lost_animation_frame_does_not_keep_the_slice_dark, fails the same way on main here.

Your three checks, on my Mac (macOS 26.6.2, Apple Silicon) with the merged branch alone and default settings:

  1. Filee's own windows: with the main window open behind Finder, the drag, a Tab flip and the drop left Finder in front; the main window didn't come forward.
  2. No Dock icon (Show Filee in the Dock off): Tab flipped, and Finder was in front after the drop.
  3. Without Secure Input: 5 drags with one Tab each, plus 2 more: all 7 landed on back-face group slices (Rotate right, Rotate 180°, Flip horizontal), which takes exactly one flip each, and all 7 converted, none lost.

That's the same Mac as before, so a second one is still welcome. Thanks, @SeoJCalive!

On libuiohook: I'll open that issue there.

@SeoJCalive

Copy link
Copy Markdown
Collaborator

@lightscape-jm Thanks for running the three checks and sorting out the #808 merge. I'll try it on my Mac too, including the Secure Input case with hold.swift, and post what I see. Just so you know, it's also macOS 26.6.2 on Apple silicon, so it's a second machine but not a second OS version.

@KnifeLemon main을 합친 커밋(58a51eac)의 CI가 승인 대기 상태라 아직 portable-osx-arm64 아티팩트가 없어요. 승인해 주시면 그 아티팩트로 리뷰의 1–3번과 Secure Input 상황까지 확인하고 결과 남길게요.

@KnifeLemon

Copy link
Copy Markdown
Owner

Thanks, @lightscape-jm. The #808 merge reads right: the donut gives the keyboard back before it hides, and RestoreWindow still runs on every hide and before close, so the two changes don't step on each other. Your three checks cover what I asked; thanks for the numbers on the flips too.

I've approved CI for the merge commit (58a51eac).

@SeoJCalive CI 승인했어요. 빌드가 끝나면 portable-osx-arm64 아티팩트로 확인 부탁드려요. 같은 macOS 26.6.2라도 다른 기기에서 보는 의미가 있어요. 특히 Secure Input을 hold.swift로 잡은 상태에서 Option-드래그로 도넛이 뜨는지, Tab이 한 번 누를 때 한 번만 뒤집히는지 봐 주세요.

@KnifeLemon

KnifeLemon commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

@SeoJCalive 확인 부탁드려요. 위에서 Mac에서 hold.swift로 Secure Input 경우까지 직접 해 보겠다고 하셨는데, 혹시 해 보셨나요?

이 세 가지만 알려 주시면 돼요.

  1. 다른 앱이 Secure Input을 잡고 있을 때(hold.swift 실행 중), 트리거로 도넛이 열리고 뒤집히나요?
  2. 도넛이 닫힌 뒤 원래 앱으로 입력이 돌아가나요?
  3. 로그(~/Library/Application Support/Filee/logs)에 이상한 점은 없나요?

결과만 주시면 바로 머지할게요.

@SeoJCalive

Copy link
Copy Markdown
Collaborator

@KnifeLemon 늦어서 죄송해요. CI 승인하신 걸 놓쳤어요. 방금 portable-osx-arm64(58a51eac)로 제 Mac(Apple silicon, macOS 26.6.2)에서 확인했어요. 세 가지 모두 문제없었습니다.

  1. Secure Input 중 도넛: hold.swift로 Secure Input을 잡은 상태에서 Finder에서 Option-드래그하니 도넛이 떴고, Tab을 누를 때마다 한 번씩 뒤집혔어요(두 번). 그대로 JPG 칸에 놓으니 변환도 됐습니다. Secure Input이 실제로 걸려 있었는지는 같은 시간에 CGEventSourceKeyState로 Tab을 폴링해서 확인했어요. 직접 누른 Tab이 한 번도 보이지 않았고(0 / 24,582), Option은 flags state에 보였습니다.
  2. 입력 복귀: 놓은 뒤 앞에 있는 앱이 Finder로 돌아와 있었어요.
  3. 로그: 시험 구간에는 드래그, 변환, 메모리 정리 INFO 세 줄뿐이고 경고나 오류는 없었어요. 종료할 때도 _windowLayerContext 중단이나 크래시 리포트는 없었습니다.

참고로 이 시험은 키보드와 마우스를 직접 눌러서 했어요. 프로그램으로 보낸 키는 Secure Input 중에도 그대로 보여서, 그걸로는 이 상황이 재현되지 않더라고요.

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @lightscape-jm, and thanks @SeoJCalive for the careful check on a real Mac (macOS 26.6.2, Apple silicon) with portable-osx-arm64 at 58a51ea:

  • While another app holds Secure Input (hold.swift, confirmed by polling: the Tab key never showed), Option-drag in Finder opened the donut, Tab flipped it each time, and the drop on JPG converted.
  • Focus went back to Finder after the drop.
  • The log has only the drag, conversion and memory-clean-up lines: no warnings, no _windowLayerContext stop, no crash report on quit.

CI is green, so merging.

@KnifeLemon
KnifeLemon merged commit a890d6a into KnifeLemon:main Nov 3, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants