Skip to content

Make 'auto-disable after fade' optional (default off) — keeps tracking on - #5

Open
chunzhi716 wants to merge 1 commit into
oxremy:mainfrom
chunzhi716:feature/optional-auto-disable-on-fade
Open

Make 'auto-disable after fade' optional (default off) — keeps tracking on#5
chunzhi716 wants to merge 1 commit into
oxremy:mainfrom
chunzhi716:feature/optional-auto-disable-on-fade

Conversation

@chunzhi716

@chunzhi716 chunzhi716 commented Jul 15, 2026

Copy link
Copy Markdown

Summary

Closes the root cause discussed in #4.

Previously, after the screen faded for fadeTimeoutDuration (6s) without a blink, handleFadeTimeout() set eyeTrackingEnabled = false, which fully disabled eye tracking until the user manually re-enabled it from the menu bar. For users who want the app to always track their eyes, this silently stops the eye-strain protection exactly when they're most at risk (deep focus / not blinking), and they may forget to turn it back on.

Changes

  • Constants.swift: add defaultAutoDisableOnFade = false and the autoDisableOnFade UserDefaults key.
  • PreferencesService.swift: add @Published var autoDisableOnFade, load it from UserDefaults, and persist changes.
  • FadeService.swift: handleFadeTimeout() now only disables tracking when autoDisableOnFade is enabled (guard). When off, the fade is purely a reminder and tracking stays on.
  • StatusBarController.swift: add an "Auto-disable after fade" checkbox to the menu (default unchecked), wired to the new preference.

Behavior

Note on verification

No problem
image

Addresses #4.

Previously, after the screen faded for fadeTimeoutDuration (6s) without a
blink, eye tracking was fully disabled (eyeTrackingEnabled = false), silently
stopping the app's eye-strain protection until manually re-enabled.

This adds an 'Auto-disable after fade' preference (default off). When off, the
fade remains a blink reminder but tracking stays on. Addresses issue oxremy#4.
@oxremy

oxremy commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Hey — this is awesome, thank you so much.

A bit of design context from my side, since I think it’ll make the last tweak before merge feel obvious:

Why auto-disable existed in the first place

The core BlinkMore loop was always meant to be simple:

  1. Eyes stay open past Time Between Blinks → screen fades (the reminder)
  2. You blink → fade clears instantly
  3. Repeat

That “fade stays up until you blink” behavior is the heart of the reminder. The fade timeout (fadeTimeoutDuration, originally 15s, later tightened to 6s) was a safety valve I added on top of that — not the main product loop. If the screen stayed faded with no blink (walked away from the Mac, detection got stuck, glasses angle issues, etc.), I didn’t want the app to leave a full-screen fade up and keep the camera running forever. BlinkMoreFree already uses a good chunk of power when tracking is on, so timeout → clear the fade → turn tracking off was my escape hatch for those stuck/away cases.

Your catch in #4 is totally fair though: for people who want always-on protection, that safety valve can quietly defeat the whole point. Making it optional (default off) is the right product call, and the prefs/menu wiring here looks clean and consistent with how the rest of the app is set up. Love it.

One gap before merge

Right now handleFadeTimeout() still always calls removeFade(), and only skips disabling tracking when auto-disable is off.

That fixes the “tracking silently turns off” bug — which is huge — but with the new default it creates a hole in the reminder loop:

  • Fade scheduling in StatusBarController.setupEyeTrackingObservers() only runs when isEyeOpen changes
  • So: stare → fade → timeout removes fade → eyes still open → no new schedule → no further reminder until a blink flips state

For the deep-focus case you described, you’d get one reminder and then a quiet gap with the camera still on. That wasn’t a problem when timeout always disabled tracking; once “keep tracking on” is the default, this becomes the main path and we should make it feel right.

Suggested fix

Lean into the original design for the default case: if auto-disable is off, timeout should do nothing — leave the fade up until a blink. StatusBarController already clears the fade when isEyeOpen goes false. If auto-disable is on, keep the old safety valve (remove fade + disable tracking).

In FadeService.handleFadeTimeout():

private func handleFadeTimeout() {
    print("Fade timeout reached after \(fadeTimeoutInterval)s")

    // Auto-disable off: keep fade up as the reminder until they blink.
    // (Original BlinkMore loop — fade persists until eyes close.)
    guard preferencesService.autoDisableOnFade else { return }

    // Auto-disable on: safety valve — clear stuck fade and release the camera.
    removeFade()

    DispatchQueue.main.async {
        self.preferencesService.eyeTrackingEnabled = false
        print("Disabled eye tracking after fade timeout (auto-disable enabled)")
    }
}

That gives:

  • Default (off): always-on reminder — fade stays until blink, tracking stays on
  • Opt-in (on): original safety valve — timeout clears fade and turns tracking off (power / walked-away cases)

Also worth updating the comment on Constants.fadeTimeoutDuration so it doesn’t still read like timeout always disables tracking.

(If you’d rather timeout still dismiss the fade when auto-disable is off, you’d need to re-arm the blink-threshold timer afterward while eyes are still open — doable, but leaving the fade up is simpler and matches the original reminder design better.)

Quick checks

  1. Default off: enable tracking, stare past threshold → fade stays through the 6s timeout; tracking stays on; blink clears it; stare again → fade returns
  2. Opt-in on: same path → after ~6s, fade clears and Enable Eye Tracking turns off
  3. Blink during fade (both settings) → fade clears immediately as usual

Again — thank you. You spotted something that was easy for me to take for granted as the person who added that timeout, and this PR makes the free app better for the always-on use case I know a lot of people want. Push that small tweak and I’ll be happy to get this merged. Really appreciate you building this with me ❤️

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.

3 participants