Skip to content

Repository files navigation

EyeShield — Forced Eye-Break Enforcement

A cross-platform desktop app that enforces screen breaks instead of just suggesting them. Built for someone with a diagnosed eye condition who will actively bypass soft reminders.

Runs on Windows and Linux (X11) from a single codebase. The input-block primitive changes per platform (Windows low-level keyboard hook that swallows keys + best-effort BlockInput → Linux XGrabKeyboard + XGrabPointer), but the Electron app, the renderer, the scheduler, the override protocol, and the JSON-over-stdio contract with the native helper are identical across both.

Features

  • Locks the screen across all monitors for the configured break duration (physical-pixel sizing handles mixed-DPI / mixed-resolution multi-monitor layouts correctly)
  • Blocks all keyboard + mouse input system-wide
    • Windows: a WH_KEYBOARD_LL low-level keyboard hook swallows every key during a break (this hook is the keyboard-blocking primitive); BlockInput is attempted as best-effort for the mouse — on sessions where it returns ACCESS_DENIED, the fullscreen overlay covers the cursor instead
    • Linux/X11: XGrabKeyboard + XGrabPointer on the root window
  • Cannot be dismissed by clicking, Alt+Tab, Win+D, Esc, or any normal input (only the daily-capped Esc 5× override releases a break early)
  • Emergency override: press Esc 5× rapidly (limited to a few uses per day)
    • Implemented in the native helper, not the renderer — works even if the renderer is frozen
  • Crash-safe:
    • 5-second heartbeat from Electron; if it stops, the helper auto-releases the block and exits the lock state
    • A blocking.flag file records a crash mid-block, so the next launch can log a crash_during_block incident and send an initial unblock to release any lingering input grab
  • Calm "Liquid Glass" UI — animated gradient blobs, frosted glass panels, breathing guide
  • Stats: compliance %, streak counter, incident log, CSV export
  • Configurable work/break intervals, snooze allowance, schedule rules per time-of-day
  • Ambient sound during breaks (rain, waves, white noise — bundled under the Mixkit Free SFX License, see src/assets/sounds/LICENSE.txt)
  • Auto-start on login
  • System tray with quick pause/resume/snooze

Architecture (3-layer crash-isolated)

Electron Main (scheduler, tray, IPC hub)
        ↕  contextBridge (preload.ts)
Renderer (React: overlay, settings, onboarding)
        ↕  stdin/stdout JSON + heartbeat
Native Helper (C# .NET 8 self-contained)
  ├─ Windows: WH_KEYBOARD_LL hook (keyboard block + override) + best-effort BlockInput
  └─ Linux:   XGrabKeyboard + XGrabPointer on the root window

The native helper is a separate process, not a Node native module — on purpose: if Electron's event loop freezes, the blocking + override still work, and if Electron crashes entirely, the helper detects the lost heartbeat and releases the block.

The Electron bridge picks the right helper binary at runtime via process.platform. The Windows and Linux helpers speak the same JSON protocol, so the bridge, the scheduler, the renderer, and the override logic are completely platform-agnostic.

Project Structure

shared/types.ts                          — Single source of truth for all shared interfaces
electron/
├── main.ts                              — Entry point: bootstrap, CSP, event wiring, lifecycle
├── preload.ts                           — contextBridge API surface
├── ipc-channels.ts                      — IPC channel name constants
├── ipc/index.ts                         — All IPC handler registrations
├── store/
│   ├── store.ts                         — JSON-file settings store (atomic writes)
│   └── settings-validator.ts            — Sanitizes untrusted IPC input
├── db/db.ts                             — JSON-file break history + incident log
├── scheduler/scheduler.ts               — Wall-clock-anchored countdown state machine
├── helper/bridge.ts                     — Native helper process IPC + heartbeat + auto-restart
│                                          (Windows + Linux path resolution)
├── safety/
│   ├── log.ts                           — File logger with rotation + EPIPE hardening
│   └── crash-guard.ts                   — Flag-file crash detection
├── tray/tray-controller.ts              — System tray icon + context menu
└── windows/
    ├── window-manager.ts                — Reusable BrowserWindow lifecycle
    └── overlay-manager.ts               — Per-monitor fullscreen break overlay
                                            (physical-pixel sizing + display-change rebuilds)
src/
├── settings/                            — Settings shell + tabs (General, Schedule, Sound, Theme, Stats)
├── onboarding/                          — 5-step onboarding wizard
├── overlay/                             — Break overlay UI
├── components/                          — GlassPanel, Countdown, BreathingGuide, useAmbientSound, useTheme, icons, sound-sources
├── assets/                              — self-hosted fonts (Sora, Space Grotesk, Space Mono) + bundled ambient sounds
├── types/global.ts                      — Window.eyeshield type declaration
└── styles/index.css                     — Tailwind + glass/theme CSS
native-helper/
├── EyeBreakHelper/                      — Windows C# .NET 8 self-contained helper exe
│   ├── EyeBreakHelper.csproj            — win-x64, self-contained, single-file
│   ├── Program.cs                       — Block state machine + daily override cap + watchdog
│   └── KeyboardHook.cs                  — WH_KEYBOARD_LL hook (keyboard block + override)
└── LinuxBreakHelper/                    — Linux C# .NET 8 self-contained helper
    ├── LinuxBreakHelper.csproj          — linux-x64, self-contained, single-file
    ├── Program.cs                       — XGrabKeyboard + XGrabPointer + X11 event loop + override
    └── X11Bindings.cs                   — P/Invoke surface for libX11.so.6

Key Design Decisions

  • JSON files over SQLite — avoids node-gyp/VS Build Tools issues; fine at this data volume
  • Self-contained .NET helpers — both the Windows and Linux helpers bundle the .NET 8 runtime so users don't need to install it
  • Identical JSON-over-stdio protocol across platforms — the Windows and Linux helpers are 1:1 interchangeable from the bridge's perspective. bridge.ts switches only on the binary name and the resource path.
  • Wall-clock-anchored scheduler — computes remaining from Date.now() - epoch, so setInterval drift doesn't accumulate
  • Physical-pixel multi-monitor sizing — overlay windows are sized using display.bounds × display.scaleFactor, so a 4K secondary monitor at 1.5× scaling is covered with the full 3840×2160, not clipped to the primary monitor's logical size
  • Display-change rebuilds — listening to screen.display-added / display-removed / display-metrics-changed re-fits overlays when a monitor is hot-plugged or its scale changes
  • Atomic file writes — both Store and Db use temp-file + rename pattern
  • Log rotation — log appends (not truncates) on launch; rotates at 1MB to .1
  • CSP on all windows — strict Content-Security-Policy blocks all remote loading
  • No Google Fonts CDN — Sora / Space Grotesk / Space Mono are bundled locally (SIL OFL); zero network dependency
  • Shared typesshared/types.ts is the single source of truth

Development

Prerequisites

  • Node.js 20+
  • .NET 8 SDK (only needed if you're rebuilding the native helpers; the produced binaries are self-contained)
  • Platform-specific:
    • Windows 10/11 — Visual Studio Build Tools are not required (we don't use node-gyp; the C# helper builds with dotnet publish alone)
    • Linux (X11 session)libX11.so.6 must be present on the host (already installed on every mainstream distro). The self-contained .NET publish embeds the .NET runtime.

Install

npm install

Available Scripts

Command Description
npm run dev Vite + Electron concurrently (dev server on :5173)
npm run build Build renderer + electron TypeScript
npm run build:helper dotnet publish the Windows C# helper (self-contained, win-x64)
npm run build:helper:linux dotnet publish the Linux/X11 C# helper (self-contained, linux-x64)
npm run package:win Full build → NSIS installer in dist/
npm run package:linux Full build → AppImage in dist/
npm run typecheck TypeScript check for both renderer + electron

Run in dev

Windows:

npm run build:helper
npm run dev

Linux (X11):

npm run build:helper:linux
npm run dev

Build installers

Platform Command Output
Windows npm run package:win dist/EyeShield Setup 1.0.0.exe (NSIS)
Linux npm run package:linux dist/EyeShield-1.0.0.AppImage

To launch the Linux AppImage:

chmod +x dist/EyeShield-1.0.0.AppImage
./dist/EyeShield-1.0.0.AppImage

Linux Support

EyeShield runs on Linux with an X11 session. The native input-block helper (native-helper/LinuxBreakHelper/) uses X11's XGrabKeyboard and XGrabPointer to lock input on the root window. It speaks the same JSON-over-stdio protocol the Windows helper does, so the Electron bridge does not need to know which OS it is running on — it just picks the right binary based on process.platform.

Supported: X11 (xorg) sessions. The helper grabs the keyboard and pointer on the root window; the emergency override (Esc 5× within 3s) is honored through an X11 event loop running on a background thread.

Not supported in this slice (known followups):

  • Wayland — Wayland's security model does not let a regular application grab input compositor-wide. On Wayland the helper logs "no X11 display — block is a no-op; relying on overlay for enforcement" and EyeShield falls back to a fullscreen overlay only (no enforcement). Real enforcement on Wayland requires a per-DE privileged extension (GNOME's mutter or KDE's kwin scripting), which is out of scope for this first slice. To test real input blocking on Linux, log into an X11 session (most login screens offer "GNOME on Xorg" / "Ubuntu on Xorg").
  • Headless / no $DISPLAY — the helper stays alive but the block is a no-op (overlay-only enforcement); the bridge still receives status responses so the rest of the app keeps working.

Required system libraries (most distros have these installed already):

  • libX11.so.6 — used for input grabbing. The self-contained .NET publish embeds the .NET runtime; libX11 must be present on the host.

Safety Notes

  • BlockInput does not block Ctrl+Alt+Del — this is by design; it's the OS-level safety valve.
  • The emergency override is implemented in the native helper, not in the Electron renderer — so it works even if the renderer is frozen.
  • If your antivirus flags the Windows keyboard hook: this is expected for any app that intercepts low-level input. The helper is open-source and does nothing beyond the keyboard hook (input blocking + override listener) and the best-effort BlockInput call.
  • On launch, EyeShield checks a blocking.flag file in the user-data directory. If present (indicating an unclean exit during a block), it logs a crash_during_block incident, clears the flag, and sends an initial unblock to the helper so any lingering input grab is released.
  • On X11, XGrabKeyboard does not intercept server-level shortcuts like Ctrl+Alt+F1 — these switch to a different VT and the grab is released cleanly when the user returns.

License

MIT — Copyright (c) 2026 Sabbir.

See CONTRIBUTING.md if you'd like to help out. Vulnerabilities: SECURITY.md.

About

Forced eye-break enforcement desktop app for Windows & Linux (X11) — blocks all input during breaks, can't be dismissed, Esc 5x emergency override, crash-safe

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages