EasyBar is a lightweight, scriptable macOS status bar built with SwiftUI and Lua.
It combines fast native widgets with flexible Lua widgets, so you can use built-ins for common system data and add custom widgets when you need something more specific. EasyBar is designed for a clean workflow on macOS and integrates especially well with AeroSpace.
EasyBar is heavily inspired by SketchyBar.
I used SketchyBar for years and it is a great product. EasyBar is not meant as a replacement. It is a more opinionated project that reflects my own setup and trade-offs.
A few choices are intentional:
- EasyBar is built specifically around AeroSpace
- there are no plans to support yabai
- the project prefers native Swift code wherever possible
- Lua is supported for custom widgets, but the core direction is to keep as much logic and UI in Swift as practical
So while EasyBar shares some ideas with SketchyBar, it aims to be a different kind of tool: a personal, strongly opinionated macOS bar focused on a Swift-first architecture and an AeroSpace-based workflow.
- Native macOS bar window built with SwiftUI
- Native built-in widgets plus Lua widgets
- AeroSpace integration for spaces, focused app state, and layout mode state
- Event-driven updates and interactive popups
- Calendar and network helper agents for permission-sensitive data
- Homebrew install and service workflow
- Logging and startup diagnostics for troubleshooting
EasyBar is distributed through Homebrew in the gi8lino/tap tap.
Add the tap:
brew tap gi8lino/tapInstall EasyBar:
brew install gi8lino/tap/easybarThis also installs the calendar and network helper agents. Start all services:
brew services start gi8lino/tap/easybar-calendar-agent
brew services start gi8lino/tap/easybar-network-agent
brew services start gi8lino/tap/easybarNote
By using EasyBar, you acknowledge that it is not notarized.
Notarization is one of Apple's distribution checks. In practice, it means sending binaries to Apple and dealing with their packaging and approval flow.
I do not mind the general idea of signing or notarization. I specifically do not want to spend time dealing with Apple's developer account, notarization pipeline, and release bureaucracy for this project.
The Homebrew install is meant to work out of the box in the common case. If macOS still blocks EasyBar or one of its helper agents with a Gatekeeper or malware verification warning on your machine, remove the quarantine attribute and start the services again.
If macOS blocks the app or CLI with a Gatekeeper or malware verification warning, remove quarantine and start it again:
xattr -dr com.apple.quarantine "$(brew --prefix)/opt/easybar/libexec/EasyBar.app"
xattr -dr com.apple.quarantine "$(brew --prefix)/opt/easybar-calendar-agent/libexec/EasyBarCalendarAgent.app"
xattr -dr com.apple.quarantine "$(brew --prefix)/opt/easybar-network-agent/libexec/EasyBarNetworkAgent.app"
xattr -d com.apple.quarantine "$(command -v easybar)"
brew services start gi8lino/tap/easybar-calendar-agent
brew services start gi8lino/tap/easybar-network-agent
brew services start gi8lino/tap/easybarEasyBar, the calendar agent, and the network agent share one logging config block.
Example:
[logging]
enabled = true
level = "debug"
directory = "~/.local/state/easybar"Supported levels:
infodebugtrace
Behavior:
infowrites normal operational logsdebugincludes debug output in addition to info, warnings, and errorstraceincludes everything, including very verbose trace logs
When file logging is enabled, EasyBar writes:
easybar.outcalendar-agent.outnetwork-agent.out
into the configured logging directory.
The app and helper agents no longer use legacy EASYBAR_DEBUG or EASYBAR_TRACE environment toggles. For normal app and agent logging, use logging.level in config.toml.
The easybar CLI still supports debug output independently through:
easybar --debugEASYBAR_DEBUG=1 easybar ...
That CLI-only environment behavior is just for the control client and does not change the main app or agent log level.
EasyBar uses two small helper agents:
easybar-calendar-agentownsEventKit, requests Calendar permission, watches calendar changes, and pushes cached snapshots to EasyBar over a local Unix socketeasybar-network-agentowns Wi-Fi and network state that depends on location permission, watches network changes, and pushes field updates to EasyBar over a local Unix socket
This keeps permission-sensitive APIs out of the main UI process and makes those widgets more reliable.
Both agents are enabled by default and can be turned off independently in config.toml with:
[agents.calendar]
enabled = true
[agents.network]
enabled = trueFor the network agent, you can also decide what happens when location permission is denied:
[agents.network]
allow_unauthorized_non_sensitive_fields = falseThe default is privacy-first: requests for Wi-Fi fields fail until location access is granted.
More details live in docs/AGENTS.md.
EasyBar exposes one local Unix control socket for easybar and other clients.
Commands are sent as typed JSON requests, not raw strings.
Example request shape:
{
"command": "<typed command name>"
}Responses are typed too:
{
"status": "accepted",
"message": null
}Supported commands:
workspace_changedfocus_changedspace_mode_changedmanual_refreshrestart_lua_runtimereload_config
easybar already speaks this protocol, so most users should use the CLI instead of talking to the socket directly.
EasyBar exposes three different runtime control actions because they solve different problems.
easybar --refresh:
- refreshes the bar and widgets using the currently loaded config
- pulls fresh data from agents
- re-emits refresh-style state so widgets can update immediately
- does not reread
config.tomlfrom disk - does not restart the Lua runtime
easybar --restart-lua-runtime:
- stops the current Lua runtime
- starts a fresh Lua runtime process
- reloads Lua widget files and resets Lua-side widget state
- does not reread
config.tomlfrom disk
easybar --reload-config:
- reloads
config.tomlfrom disk - rebuilds EasyBar using the new config
- reapplies native widgets and Lua runtime state against the updated configuration
Use --refresh when the config is already correct and you want fresh UI state or fresh agent-backed data.
Use --restart-lua-runtime when the Lua side is stuck, stale, or needs a full runtime reset.
Use --reload-config when you changed the config file itself.
If you use the built-in AeroSpace mode widget, you should trigger EasyBar whenever you change the current layout mode in AeroSpace.
For example:
alt-e = [
'layout tiles horizontal',
'exec-and-forget /opt/homebrew/bin/easybar --space-mode-changed'
]
alt-v = [
'layout tiles vertical',
'exec-and-forget /opt/homebrew/bin/easybar --space-mode-changed'
]
alt-s = [
'layout v_accordion',
'exec-and-forget /opt/homebrew/bin/easybar --space-mode-changed'
]
alt-shift-space = [
'layout floating tiling',
'exec-and-forget /opt/homebrew/bin/easybar --space-mode-changed'
]That tells EasyBar to refresh its AeroSpace-derived state after the layout mode changes, so widgets like the built-in AeroSpace mode widget update immediately.
EasyBar reads its runtime config from:
~/.config/easybar/config.toml
You can override that path with:
EASYBAR_CONFIG_PATH=/path/to/config.tomlA small example:
[builtins.spaces]
enabled = true
[builtins.calendar]
enabled = trueThe repository includes two config examples:
- config.defaults.toml full reference file with the current defaults and supported sections
- config.minimal.toml
smaller starter example with a native
systemgroup
Config details, native groups, and example layouts are documented in docs/CONFIG.md.
When something is wrong, the first thing to do is check whether EasyBar and its helper agents are actually running, whether they are duplicated, and whether the logs show a clear startup warning.
Check the Homebrew services:
brew services list | grep easybarCheck running processes:
pgrep -fl EasyBar
pgrep -fl easybar-calendar-agent
pgrep -fl easybar-network-agentCheck that only one main EasyBar process is running. EasyBar now refuses to start when another instance already holds its lock, but duplicate service or manual launches are still the first thing to rule out.
Check the control socket with the CLI:
easybar --refreshIf that fails, EasyBar may not be running, may have been blocked by macOS, or may have failed during startup.
If logging is enabled in your config, EasyBar writes useful startup information such as:
- bundle path and executable path
- config path and widget path
- enabled agents and socket paths
- screen geometry
- environment overrides
- whether required fonts are available
- whether another EasyBar instance is already running
Enable logging in config.toml:
[logging]
enabled = true
level = "debug"Then inspect the log output in your configured logging directory.
If you installed with Homebrew and are using services, also check Homebrew service logs:
tail -n 200 ~/Library/Logs/Homebrew/easybar/*.log
tail -n 200 ~/Library/Logs/Homebrew/easybar-calendar-agent/*.log
tail -n 200 ~/Library/Logs/Homebrew/easybar-network-agent/*.logIf your Homebrew setup writes logs somewhere else on your machine, use brew services info easybar and the corresponding agent services to find the actual paths.
For very verbose app and agent troubleshooting, temporarily raise the level to:
[logging]
enabled = true
level = "trace"Check whether the service is running:
brew services list | grep easybarTry launching the app directly:
open "$(brew --prefix)/opt/easybar/libexec/EasyBar.app"If that works but the service does not, restart the services:
brew services restart gi8lino/tap/easybar-calendar-agent
brew services restart gi8lino/tap/easybar-network-agent
brew services restart gi8lino/tap/easybarIf nothing appears, check logs for startup warnings and macOS permission or quarantine problems.
EasyBar now uses a single-instance guard. If a second instance starts, it logs a warning and exits.
You can detect duplicates with:
pgrep -fl EasyBarIf you accidentally launched both a Homebrew service and a manual app instance, stop the extra one:
pkill -x EasyBar
brew services restart gi8lino/tap/easybarIf you are also testing local builds from dist/, stop all services first so you do not mix service and manual runs.
EasyBar expects Symbols Nerd Font Mono for several icons. On startup it checks whether the font is installed and logs a warning if it is missing.
You can inspect installed fonts in Font Book, or from the terminal:
system_profiler SPFontsDataType | grep -B2 -A4 "Symbols Nerd Font Mono"If the font is missing, install Nerd Fonts and restart EasyBar. If the font is installed after EasyBar already started, restart the app and agents so the font check and layout run again.
Make sure the calendar agent is running:
brew services list | grep easybar-calendar-agent
pgrep -fl easybar-calendar-agentThen grant Calendar access in macOS settings. EasyBar exposes menu actions to open the relevant settings pages, and the calendar agent permission state is shown in the bar context menu.
If you changed permissions and nothing updates, restart the calendar agent and EasyBar:
brew services restart gi8lino/tap/easybar-calendar-agent
brew services restart gi8lino/tap/easybarMake sure the network agent is running:
brew services list | grep easybar-network-agent
pgrep -fl easybar-network-agentThe network agent depends on Location Services permission. If permission is denied or unresolved, Wi-Fi-specific fields may be unavailable by design.
Restart the network agent after changing permission settings:
brew services restart gi8lino/tap/easybar-network-agent
brew services restart gi8lino/tap/easybarIf your AeroSpace mode widget does not change after switching layouts, make sure your AeroSpace bindings call:
easybar --space-mode-changedFor example, after every layout ... command in your AeroSpace config, add an exec-and-forget call to EasyBar. Without that trigger, EasyBar may not know that the layout mode changed yet.
If watch_config = false, EasyBar will not automatically reload config changes. Either enable config watching or reload manually:
easybar --reload-configIf a reload is rejected, EasyBar keeps the last valid config and logs the parse or validation error. Check the logs instead of assuming the new file was accepted.
First try a normal refresh:
easybar --refreshThat refreshes the bar and widgets using the currently loaded config and pulls fresh data from agents, but it does not reload config from disk and it does not restart the Lua runtime.
If the Lua side itself seems stuck, restart it explicitly:
easybar --restart-lua-runtimeThe bar context menu item does the same thing:
Restart Lua Runtime
If that is still not enough, restart the whole app:
brew services restart gi8lino/tap/easybarIf a widget still fails, check your configured widgets_dir, Lua path, and any widget-specific logs or output.
A good recovery sequence is:
brew services stop gi8lino/tap/easybar
brew services stop gi8lino/tap/easybar-calendar-agent
brew services stop gi8lino/tap/easybar-network-agent
pkill -x EasyBar || true
pkill -x easybar-calendar-agent || true
pkill -x easybar-network-agent || true
brew services start gi8lino/tap/easybar-calendar-agent
brew services start gi8lino/tap/easybar-network-agent
brew services start gi8lino/tap/easybarThat clears the usual problems caused by duplicate instances, stale agent state, or mixed manual and service launches.
EasyBar is split into a few Swift packages/targets:
EasyBarSharedshared models, config loading, IPC types, and common utilitiesEasyBarthe main macOS status bar appEasyBarCtltheeasybarcommand-line client for talking to the control socketEasyBarCalendarAgenthelper app that owns EventKit and calendar snapshotsEasyBarNetworkAgenthelper app that owns Wi-Fi and network observationEasyBarNetworkAgentCoreshared network-agent implementation used by EasyBarNetworkAgent and also reused by the standalone wifi-snitch project
For implementation details, see the docs in docs/.
- docs/CONFIG.md config structure, native groups, and box-model rules
- docs/AGENTS.md calendar and network agents, permissions, and how EasyBar uses them
- docs/LUA_WIDGETS.md Lua widget authoring and interaction model
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.