A minimal cross-platform breathing overlay — a friendly indicator and reminder to take full, deep breaths while looking at screens. Research indicates we blink less and breathe more shallowly when staring at displays, and this is intended as a small tool to help counter that.
The overlay is a translucent always-on-top window that gently expands on inhale and contracts on exhale. Inhale, post-inhale hold, exhale, and post-exhale hold durations are all configurable. A good starting point is 4 seconds in and 4 seconds out; eventually 6 and 8 with the out twice as long as the in (to engage the parasympathetic nervous system). Box breathing is 4 / 4 / 4 / 4. Take breaks if intense feelings arise — it's important not to overdo it.
The information and guidance provided by this app are intended for general informational purposes only and are not medical advice. The creator is not a medical professional. Always seek the advice of a qualified healthcare provider with any questions about your health, and do not disregard or delay professional medical advice because of this app. Use is at your own risk.
Pre-built binaries for each OS are on the Releases page. Using the latest release is recommended; if you hit a problem, please open an issue.
Mac
Windows — install the MSIX from the Microsoft Store, or grab the standalone .exe from Releases.
Linux — install the Snap from the Snap Store, or build from source.
Only one shortcut ships bound by default:
| Shortcut | Action |
|---|---|
| Ctrl+Shift+, | Open / close Preferences |
Every other action (Start / Stop / Reset / Quit) is unbound on first launch so exhale never collides with another app's global shortcut without the user opting in. Customise via either path:
- Right-click the Start / Stop / Reset / Quit buttons in the Preferences panel → "Change Shortcut…" → press your combo
- Tray menu → "Keyboard Shortcuts ▶" → pick any of the five actions to start a capture
Press Esc in the capture overlay to cancel. "Reset Shortcut to Default" in the right-click menu restores the per-action factory value (which, for everything except Preferences, is "unbound"). Reset to Defaults in the panel clears all custom bindings too.
Notice: A high opacity value can obscure the Preferences pane in the current workspace. Bind a Reset shortcut as described above and use it to recover, or:
- Swipe to a different workspace.
- Close Preferences from the menu bar.
- Re-open Preferences in the current workspace and adjust Opacity.
- Switch back.
Single Rust workspace (rust/) producing one cross-platform binary.
- Renderer:
wgpu+ a single WGSL fragment shader (crates/exhale-render) - Window system:
winit - Settings UI:
egui(hand-rolled stepper, segmented picker, control buttons painted directly viaegui::Painterto matchNSSegmentedControl/NSStepperlook) - AppKit interop: typed FFI via
objc2for the menu-bar, status-bar level, andNSApplicationActivationPolicypaths.platform/mac.rsandtimers.rsstill use rawmsg_send!where typed bindings don't exist yet (window-level juggling, NSUserNotification plumbing) - Threading model: per-overlay-window render thread + per-window
wgpu::Deviceso overlay frame delivery isn't gated by the main thread's message queue or the settings window's GPU submissions - Animation cadence: 24 fps while the breath animation is running (matches the legacy Swift
MetalBreathingController); drops to 1 fps when the controller has nothing dynamic to draw (paused, fullscreen-with-matching-colors tint, or all-zero durations). Hardcoded — per-frame CPU runs ≤ 2 % on every scene tested, so the earlier user-tunable preset was removed
exhale-core— settings +SettingsDiff, breathing controller (deadline-scheduled background thread), poison-tolerant lock helpers, easing tables. Zero GUI deps.exhale-render—wgpurenderer + WGSL fragment shader, headless benchmarking harness (cargo run --example cpu_bench).exhale-app— winit event loop, split egui settings panel (settings_window.rs+widgets.rs+theme.rs), per-overlay render thread, tray, hotkeys, platform glue (objc2/windows-sys/x11-dl).
The cargo run family builds and then launches the binary in one step. The cargo build family only compiles — you have to invoke the binary yourself afterwards.
| Command | Builds | Runs | Build profile |
|---|---|---|---|
cargo run |
Yes | Yes | Dev (debug, fast compile) |
cargo run --release |
Yes | Yes | Release (optimised) |
cargo build |
Yes | No | Dev |
cargo build --release |
Yes | No | Release |
All commands run from rust/. Use dev builds while iterating (compile is ~10× faster), release for the real binary you'd ship or benchmark. Binaries land at:
- Dev:
rust/target/debug/exhale(or.exeon Windows) - Release:
rust/target/release/exhale(or.exeon Windows)
cargo doc --no-deps --workspace --openGenerates HTML docs for the three local crates and opens them in your browser. --no-deps skips the ~200 dependency crates so you only see exhale's own types. See LEARNING.md for a beginner's tour of the codebase.
After cargo build, run the binary directly without going through cargo:
macOS / Linux
./target/release/exhale # release
./target/debug/exhale # devWindows (PowerShell or cmd)
.\target\release\exhale.exe # release
.\target\debug\exhale.exe # devmacOS — no extra prerequisites beyond Rust.
Windows — no extra prerequisites. Works with both the MSVC and GNU toolchains.
Linux — exhale dynamically loads several system libraries at run time. To build AND run on Debian/Ubuntu:
sudo apt install \
libgtk-3-dev libayatana-appindicator3-dev \
libwayland-dev libxkbcommon-dev libxdo-dev \
libssl-dev pkg-configOn Fedora/RHEL:
sudo dnf install \
gtk3-devel libayatana-appindicator-gtk3-devel \
wayland-devel libxkbcommon-devel libxdo-devel \
openssl-devel pkgconf-pkg-configIf you're running a pre-built binary (not compiling from source), the bare runtime packages are enough — drop the -dev suffixes:
sudo apt install libgtk-3-0 libayatana-appindicator3-1 libwayland-client0 libxkbcommon0 libxdo3 libssl3X11 and Xfixes are loaded via x11-dl at run time using whatever's already installed by the X11 desktop, so they're not in the list.
What each one is for:
libgtk-3+libayatana-appindicator3: system-tray icon backendlibwayland-client+libxkbcommon: winit's Wayland + keyboard inputlibxdo:global-hotkeycrate's X11 keyboard binding (libxdo.so.3at runtime)libssl: TLS for crates that fetch over HTTPSpkg-config: build-time library discovery (compile-only)
Settings are saved as TOML under the platform config dir (via the directories crate's ProjectDirs::from("com", "peterklingelhofer", "exhale")):
| Platform | Path |
|---|---|
| macOS (dev / standalone) | ~/Library/Application Support/com.peterklingelhofer.exhale/settings.toml |
| macOS (Mac App Store) | ~/Library/Containers/peterklingelhofer.exhale/Data/Library/Application Support/com.peterklingelhofer.exhale/settings.toml |
| Windows | %APPDATA%\peterklingelhofer\exhale\config\settings.toml |
| Linux | ~/.config/exhale/settings.toml |
The MAS path differs because the App Store build runs sandboxed; the sandbox redirects ~/Library/Application Support writes into the per-app container. Settings are reloaded on launch and persisted on every change via a debounced background writer thread; corrupt TOML is logged and the file is rewritten with defaults.
- macOS: the overlay floats above fullscreen apps (screen-saver window level), joins every Space, and stays out of Cmd+Tab.
AppVisibilitytogglesNSApp.setActivationPolicybetween.regularand.accessory. - Windows: the overlay uses
WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TOPMOST.AppVisibilitytogglesWS_EX_APPWINDOW/WS_EX_TOOLWINDOWon the settings window so "DockOnly" shows a taskbar entry and "TopBarOnly" hides it. Tested + recommended on Windows 11; some Windows 10 GPU + driver combinations report onlyOpaquealpha modes to Vulkan, in which case exhale falls back to the windowed mode described below. - Linux (X11): click-through via
XFixesSetWindowShapeRegionwith an empty input region; always-on-top via_NET_WM_STATE_ABOVE; workspace-spanning via_NET_WM_STATE_STICKY;AppVisibilitytoggles_NET_WM_STATE_SKIP_TASKBAR/SKIP_PAGERon the settings window. - Linux (Wayland): exhale picks one of two paths at startup based on whether the compositor exposes alpha-capable swap chains to wgpu:
- Compositor supports alpha (rare on current Mutter/GNOME, supported by some KWin setups): the overlay is placed at
AlwaysOnBottombecause Wayland's security model doesn't surface a portable click-through / always-on-top protocol to winit (wp_input_regionisn't exposed). Your app windows cover the overlay by default; to see the breath animation, narrow your foreground windows so they don't fill the whole screen and the animation shows through the gap. - Compositor only exposes Opaque alpha (typical real-hardware Wayland session on Ubuntu / Fedora GNOME): exhale falls back to the windowed mode described below. For full topmost + click-through overlay behavior on Linux, log out and pick an X11 session at the login screen.
- Compositor supports alpha (rare on current Mutter/GNOME, supported by some KWin setups): the overlay is placed at
- Windowed-mode fallback (Wayland sessions without alpha, some Windows 10 + Vulkan combinations, WARP / Microsoft Basic Render Driver, remote-desktop sessions): the breath animation runs in a 480×360 movable, resizable "exhale" window with normal decorations and full window-manager participation (Alt-Tab, taskbar, native close button). You can use it two ways: (1) as a foreground window, watching the breath animation directly the same way you'd watch any other app, or (2) as an edge-strip overlay, by sending the window behind your other apps (Alt-Tab past it / click on the window manager to lower it), switching exhale to Rectangle mode, and narrowing the windows in front so the animation shows through the side / bottom strips you've left open. The Stop button (and the global Stop hotkey, if bound) hides this window; clicking the window's native close X does the same thing — both halt the animation but leave the tray icon and settings panel running, so Start brings the animation window back. The settings panel is still the way to fully quit (Quit button, or close the settings window on Linux).
Live A/B on macOS (M3 Max, default settings, single monitor, settings window closed). 30 s window, 15 samples via ps -o %cpu; both numbers normalised to one CPU core:
| Build | avg CPU | range |
|---|---|---|
| Swift (Release) | 4.95 % | 3.2 – 6.6 |
| Rust (Release) | 3.19 % | 1.5 – 4.3 |
Rust runs about 36 % lower CPU in steady state. The delta is statistically robust (means ~5σ apart) but small in absolute terms (~1.8 percentage points). Opening the settings window adds roughly 1–2 pp on both builds; each additional monitor adds another ~0.2–0.4 pp on Rust (one render thread per overlay).
Reproduce via cargo run --release --example cpu_bench -p exhale-render for the headless per-frame number, or by running both binaries side-by-side under ps -o %cpu for the live-process number above.
Per-release build, sign, package, and store-upload instructions for every supported target live in DEPLOYMENT.md. Quick summary:
| Target | Script |
|---|---|
| Mac App Store | rust/scripts/bundle-mas.sh |
| Microsoft Store | rust\scripts\bundle-msix.ps1 |
| Snap Store | CI builds, manual upload via Multipass snap-creds VM |
Linux .deb / AppImage |
cargo deb + rust/scripts/bundle-appimage.sh |
Windows standalone .exe |
cargo build --release |
CI in .github/workflows/release.yml builds every artifact on a v* tag.
For tinkerers, distros where the Snap doesn't fit (Alpine, NixOS, immutable distros), or anyone who'd rather just read 200 lines of Python and tweak constants at the top of a file:
git clone https://github.com/peterklingelhofer/exhale.git
cd exhale/python
python main.pyModify the constants at the top of python/main.py for inhale/exhale duration in seconds, shape mode, and full-screen toggle.
The Rust binary is the recommended path on every supported OS, including Wayland. On a typical Wayland desktop the compositor doesn't expose alpha-capable swap chains, so the Rust binary opens as a regular movable window — you can either watch the animation directly in that window OR send it behind your other apps and narrow them so the animation peeks through the edges, exactly the same "make room for the overlay" trick this Python script uses in its bars mode (see the Linux (Wayland) platform note above for details). The Python script is a hackable single-file alternative, not a performance recommendation.
A Perl version of this exists at https://github.com/franco3445/Breathing.
The implementations below are superseded by the Rust port above and are kept in the repo for historical reference only. They will not receive new features or fixes. Use the Rust binary on every supported OS.
The original macOS-only implementation, written in SwiftUI + Metal. The Rust port is a strict superset: same overlay, same hotkeys, same settings, plus Windows and Linux support, with measurably lower per-frame CPU on every complex scene (see Performance table above). The Mac App Store listing will be updated to the Rust build going forward; the Swift source remains for reference.
git clone https://github.com/peterklingelhofer/exhale.git
cd exhale/swift
xed .Cross-platform Electron build that predates the Rust port. The Rust binary covers macOS + Windows + Linux from a single ~10 MB native executable, with far lower CPU than the Electron build (which bundles a full Chromium runtime). Settings live in localStorage and have to be edited via DevTools; the Rust port has a real settings UI.
git clone https://github.com/peterklingelhofer/exhale.git
cd exhale/typescript
pnpm install
pnpm startTo recompile automatically with electron-reload:
pnpm watch



