Skip to content
museslabsPublic

Repository files navigation

omma

omma (/'o.ma/) — from Greek ὄμμα: eye.

Eye-strain break reminder for the macOS menu bar, written in Rust.

omma-example

omma counts down to your next break from the menu bar. Just before it, a bead drops from the icon with a warning, then the break covers every display with a countdown. It can hold breaks back while you're in a meeting or a fullscreen app, and the schedule pauses while you're away from the keyboard.

omma needs macOS 26 or later. The warning and the break screen use Liquid Glass, and on an older system omma exits with omma: requires macOS 26 or later.

Install

With Homebrew:

brew tap museslabs/omma
brew install omma

With cargo (Rust 1.89 or later):

cargo install omma

From source:

git clone https://github.com/museslabs/omma
cd omma
cargo build --release

Or download the Apple silicon build from the releases page and unpack it. It isn't notarized. If macOS won't run it, clear its quarantine flag:

tar -xzf omma-v0.1.0-aarch64-apple-darwin.tar.gz
cd omma-v0.1.0-aarch64-apple-darwin
xattr -d com.apple.quarantine omma   # only if macOS blocks it

omma is a single binary with no Dock icon, not an app bundle. Put it on your PATH to run it as omma.

Setup

Start at login

With Homebrew:

brew services start omma

Otherwise, save this as ~/Library/LaunchAgents/com.museslabs.omma.plist, with the full path to your omma binary and to a log file. launchd doesn't expand ~.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.museslabs.omma</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Users/you/.cargo/bin/omma</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <dict>
    <key>Crashed</key>
    <true/>
  </dict>
  <key>ProcessType</key>
  <string>Interactive</string>
  <key>StandardErrorPath</key>
  <string>/Users/you/Library/Logs/omma.log</string>
</dict>
</plist>

Load it, which also starts omma now:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.museslabs.omma.plist

To stop starting it at login:

launchctl bootout gui/$(id -u)/com.museslabs.omma
rm ~/Library/LaunchAgents/com.museslabs.omma.plist

launchd restarts omma only after a crash. A second copy that finds omma already running exits, and stays stopped.

Started by launchd, omma behaves differently in two ways:

  • It doesn't see your shell's environment. If you set XDG_CONFIG_HOME, add it to the plist, or omma reads ~/.config/omma/config.toml:

    <key>EnvironmentVariables</key>
    <dict>
      <key>XDG_CONFIG_HOME</key>
      <string>/Users/you/.config</string>
    </dict>
  • It no longer borrows your terminal's Accessibility permission. Allow omma itself for the fullscreen and window-title rules; see Accessibility.

macOS shows a "Background Items Added" notice the first time.

Accessibility

The fullscreen and window-title rules of do not disturb need Accessibility permission. omma asks for it when it starts with one of them on, or when a save turns one on. Grant it in System Settings → Privacy & Security → Accessibility (Device Control and Data Access on macOS 27), and the rules work from the next break, with no restart. Started from a terminal, omma uses the terminal's permission instead of its own.

Released binaries aren't signed with a Developer ID, and macOS treats each upgrade as a different program. After upgrading, allow omma again in the same list. Until then the menu shows a ⚠ item for it.

Usage

Run omma and it shows up in the menu bar. By default a 20-second break comes every 15 minutes and a one-minute break every 75, and the icon blinks in the minute before each one.

Breaks

Before each break a bead drops from the menu-bar icon on a thread, with a small panel beside it that counts down and has Skip and Postpone buttons. In strict mode Skip becomes Dismiss, which only closes the warning. The warning never takes keyboard focus from the app you're typing in, and pre_break_warning = 0 turns it off.

The break screen covers every display with the break's name, a countdown in a bead whose colour drains as the break runs, an optional background image, and Skip and Postpone buttons. The countdown waits for the screen to fade in, which adds about 0.6 s to a break, or 0.25 s with Reduce Motion on. omma never becomes the active app, and your app gets the keyboard back when the break ends.

A sound plays when the warning shows up and when a break runs to its end. [sounds] in the config picks them.

The menu

The menu shows the time to the next break, with items to take a break now (the next one, a short one or a long one), Pause (for 30 minutes, an hour, two hours or the rest of the day), Enabled, Open config…, Help, the version and Quit. It also lists anything that needs fixing; see When something's wrong.

CLI

omma                          start the menu-bar app
omma --take-break  (-t)       take the next break now
omma --short-break (-s)       take a short break now
omma --long-break  (-l)       take a long break now
omma --enable      (-e)       enable the running instance
omma --disable     (-d)       disable the running instance
omma --pause <DURATION>       pause for 90s, 30m, 2h30m and so on, up to 24h
omma --quit        (-q)       quit the running instance
omma --status                 print the running instance's status
omma --reload                 reread the config now
omma --print-default-config   print the default config

Flags can't be combined. Each command waits for the running instance to act on it, then prints what it did or why it refused. The exit code is:

  • 0 when the command worked;
  • 1 when omma isn't running, or, when starting it, is already running;
  • 2 when the running instance refused the command (a break while omma is disabled, say, or --reload with a broken config), or the flags are invalid;
  • 3 when anything else went wrong.

The running instance holds a lock file and listens on a socket, both in ~/Library/Application Support/omma/.

When something's wrong

The menu lists problems under the status line, each marked ⚠. A long one is cut short, and hovering it shows all of it:

  • an error in the config file; clicking it opens the file;
  • a break image that couldn't be shown; clicking it opens the config file;
  • a warning shortcut the system wouldn't register;
  • a fullscreen or window-title rule without Accessibility permission; clicking it opens System Settings at the Accessibility list.

omma --status lists them too, and omma writes each one to stderr.

Configuration

The config file is $XDG_CONFIG_HOME/omma/config.toml, or ~/.config/omma/config.toml when XDG_CONFIG_HOME isn't set. omma creates it with the defaults on first run, and "Open config…" creates it again if it's gone. omma --print-default-config prints the default file, which documents every key with its range.

omma rereads the file a second or two after every save, during a break too. A warning or break already on screen keeps the buttons and shortcuts it has until it closes, and one the new config forbids does nothing. Parsing is strict:

  • A key omma doesn't know is an error. So is a top-level key placed under a [table] header, and the error says where it belongs.
  • Times are whole seconds within a range: intervals and postpone_duration 1 s to 24 h, break durations 1 s to 1 h, pre_break_warning 0 to 10 min, and idle_seconds 10 s to 24 h. With both kinds of break, short_break_interval must be less than long_break_interval.
  • A break image must be an absolute path or start with ~/, and the file must exist when the config loads.

On an error the menu shows it, with its line when it has one, and omma keeps the last config that loaded, or the defaults at startup. Fix the file and save, and the error goes.

Each break in the [[short_breaks]] and [[long_breaks]] lists has a name, and can set its own duration and image:

[[long_breaks]]
name     = "Take a walk"
duration = 300
image    = "~/Pictures/trail.jpg"

Keybindings

[keybindings]
skip     = "escape"   # on the break screen
postpone = "space"
warning_skip     = ""  # on the warning, from any app
warning_postpone = ""

skip and postpone work on the break screen once its buttons have appeared, a little over a second in. A key you were typing as the break began doesn't skip it.

warning_skip and warning_postpone are off by default. They're system-wide shortcuts that work from any app while the warning shows, and the warning never takes focus. Each needs cmd or ctrl, for example "cmd+shift+option+space".

A shortcut is one key after any of the modifiers cmd, shift, alt (or option) and ctrl, joined by +: a single character, or escape, space, enter, tab or delete (esc, return and backspace work too). Shift goes only with a letter, escape, space, enter or delete. "" turns a shortcut off. With allow_postpone on, skip and postpone must differ. A shortcut that breaks these rules is a config error. Postpone shortcuts need allow_postpone, and with strict_break on no shortcut works.

Smart pause

[smart_pause]
enabled      = true
idle_seconds = 60

After idle_seconds without keyboard or mouse input the schedule pauses. Your rest counts from your last input, threshold included. Once you're back the next break is a full interval away, and time away at least as long as a long break counts as one. Sleep counts as time away too, and waking the Mac never brings on a break at once.

Do not disturb

[do_not_disturb]
action = "skip"   # or "postpone"

# need no permission
apps_running  = ["zoom"]
apps_in_front = []        # only while the app is frontmost

# need Accessibility permission
fullscreen    = false     # while the frontmost window is fullscreen
window_titles = ["Zoom Meeting", '\bPlain\b']

omma checks the rules when a break comes due. When one holds, action = "skip" drops the break and "postpone" tries again after postpone_duration, even with allow_postpone off or strict_break on. The menu then says why, for example "Skipped the last break: Zoom is running", while omma waits for the next break.

  • apps_running and apps_in_front match part of an app's name or bundle id, ignoring case. "zoom" covers Zoom being open at all.
  • window_titles holds a break while a window on screen has a matching title. Minimized windows and hidden apps don't count. Each entry is a regex that ignores case. A plain word matches anywhere in a title: "Plain" also matches "explain". A TOML literal string with word boundaries, '\bPlain\b', matches the word alone. A browser window shows only its active tab's title.
  • Each request omma sends an app gives up after 0.25 s. An app that's hung, or slower than that, counts as having no matching window.

The fullscreen and window-title rules need Accessibility permission; see Accessibility.

Credits

omma's default schedule and most of its break prompts come from Safe Eyes. The eye prompts follow the 20-20-20 rule instead of eye rotations, which don't help, according to the American Academy of Ophthalmology.

omma talks to macOS through objc2. Its unsafe code is that interop, and each block says why it's sound.

License

GPL-3.0-or-later. See LICENSE.

Releases

Packages

Contributors

Languages