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.
- 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_LLlow-level keyboard hook swallows every key during a break (this hook is the keyboard-blocking primitive);BlockInputis attempted as best-effort for the mouse — on sessions where it returnsACCESS_DENIED, the fullscreen overlay covers the cursor instead - Linux/X11:
XGrabKeyboard+XGrabPointeron the root window
- Windows: a
- Cannot be dismissed by clicking, Alt+Tab, Win+D, Esc, or any normal input
(only the daily-capped
Esc5× override releases a break early) - Emergency override: press
Esc5× 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.flagfile records a crash mid-block, so the next launch can log acrash_during_blockincident and send an initialunblockto 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
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.
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
- 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.tsswitches only on the binary name and the resource path. - Wall-clock-anchored scheduler — computes remaining from
Date.now() - epoch, sosetIntervaldrift 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-changedre-fits overlays when a monitor is hot-plugged or its scale changes - Atomic file writes — both
StoreandDbuse 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 types —
shared/types.tsis the single source of truth
- 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 publishalone) - Linux (X11 session) —
libX11.so.6must be present on the host (already installed on every mainstream distro). The self-contained .NET publish embeds the .NET runtime.
- Windows 10/11 — Visual Studio Build Tools are not required (we
don't use node-gyp; the C# helper builds with
npm install| 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 |
Windows:
npm run build:helper
npm run devLinux (X11):
npm run build:helper:linux
npm run dev| 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.AppImageEyeShield 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'smutteror KDE'skwinscripting), 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.
BlockInputdoes not blockCtrl+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
BlockInputcall. - On launch, EyeShield checks a
blocking.flagfile in the user-data directory. If present (indicating an unclean exit during a block), it logs acrash_during_blockincident, clears the flag, and sends an initialunblockto the helper so any lingering input grab is released. - On X11,
XGrabKeyboarddoes not intercept server-level shortcuts likeCtrl+Alt+F1— these switch to a different VT and the grab is released cleanly when the user returns.
MIT — Copyright (c) 2026 Sabbir.
See CONTRIBUTING.md if you'd like to help out. Vulnerabilities: SECURITY.md.