Display status information in the Emacs echo area instead of the mode line, saving a line of screen real estate.
Most echo-area status packages use a timer that recomputes every segment on every tick, creates temporary buffers to measure pixel widths, and writes overlays even when nothing changed. That works, but it’s wasteful.
echo-bar takes a different approach: event-driven updates with
coalescing. Each segment declares what triggers it (hooks, advice, idle
timers). When a trigger fires, the segment is marked dirty and a single
update is scheduled via run-at-time 0 nil. Only dirty segments
recompute. Overlays are written only when the rendered string actually
changes.
The result is a status bar that feels instant and doesn’t burn CPU in the background.
echo-bar is not yet on MELPA. For now, clone the repo and point your config at it.
(use-package echo-bar
:ensure nil
:load-path "~/path/to/echo-bar"
:custom
(echo-bar-layout
'(:center ("buffer-position" "buffer-name" "major-mode")
:right ("project" "vcs" "time" "battery")))
:config
(echo-bar-mode 1))(use-package echo-bar
:straight (:host github :repo "chenanton/echo-bar")
:custom
(echo-bar-layout
'(:center ("buffer-position" "buffer-name" "major-mode")
:right ("project" "vcs" "time" "battery")))
:config
(echo-bar-mode 1))(use-package echo-bar
:ensure (:host github :repo "chenanton/echo-bar")
:custom
(echo-bar-layout
'(:center ("buffer-position" "buffer-name" "major-mode")
:right ("project" "vcs" "time" "battery")))
:config
(echo-bar-mode 1))(echo-bar-mode 1) ; enable
(echo-bar-mode -1) ; disableecho-bar-layout is a plist mapping zone keys to lists of segment names.
Three zones are supported:
| Zone | Positioning |
|---|---|
:left | Flush left |
:center | Centered (clamped to avoid right) |
:right | Right-aligned |
You can combine them however you like:
;; Right only (simplest)
(setq echo-bar-layout
'(:right ("buffer-name" "major-mode" "vcs" "time")))
;; Center + right
(setq echo-bar-layout
'(:center ("buffer-position" "buffer-name" "major-mode")
:right ("project" "vcs" "time" "battery")))
;; Left + right
(setq echo-bar-layout
'(:left ("buffer-name" "buffer-position")
:right ("major-mode" "vcs" "project" "time")))| Variable | Default | Description |
|---|---|---|
echo-bar-separator | " " | String between segments in a group |
echo-bar-right-padding | 1 | Chars of padding before the right edge |
echo-bar-center-right-gap | 4 | Min gap between center and right groups |
echo-bar-time-format | "%a %b %-e %-I:%M %p" | Format string passed to format-time-string |
echo-bar-idle-fallback-interval | 2.0 | Idle seconds before safety-net refresh |
Every segment has a face you can customize. They all inherit from
echo-bar-default (which inherits default) except echo-bar-dim
(inherits shadow) and echo-bar-warning (inherits warning).
;; Example: make the buffer name bold
(set-face-attribute 'echo-bar-buffer-name nil :weight 'bold)Or with use-package:
:custom-face
(echo-bar-buffer-name ((t (:inherit bold))))
(echo-bar-major-mode ((t (:inherit org-level-4))))| Name | Shows | Scope | Trigger |
|---|---|---|---|
buffer-name | Buffer name, [+] if dirty | buffer | post-command, save |
buffer-position | Line:column | buffer | post-command |
selection-info | Sel:3L,42C when region | buffer | post-command |
narrow | Narrow when narrowed | buffer | post-command |
macro | REC during kbd macro | buffer | post-command |
process | Active process info | buffer | post-command |
profiler | PROF when profiler runs | global | post-command |
major-mode | Mode name | buffer | mode change |
vcs | Branch name | buffer | file open/save, vc |
project | Project name | buffer | file open |
blame | Blame in magit-blame | buffer | blame mode hook |
text-scale | Zoom:+2 when scaled | buffer | text-scale hook |
repeat | REPEAT during repeat-mode | global | repeat-post-hook |
time | Formatted date/time | global | idle timer |
battery | Battery string | global | battery-update |
Segments only appear when they have something to show. nil or ""
means the segment is hidden and no separator is added.
(echo-bar-defsegment my-word-count
"Word count for the current buffer."
:triggers (:hooks (post-command-hook))
:fn (format "%dW" (count-words (point-min) (point-max)))
:face echo-bar-dim)Then add "my-word-count" to your layout.
Properties:
| Key | Description |
|---|---|
:fn | Body that returns a string (or nil to hide) |
:face | Face applied to the output |
:scope | :buffer (default) or :global |
:triggers | Plist of :hooks, :advice, :timers |
:setup | Body run once on first activation |
Trigger types:
:hooks– list of hook symbols (e.g.(post-command-hook after-save-hook)):advice– alist of(FUNCTION . HOW)pairs (e.g.((vc-refresh-state . :after))):timers– plist(:idle SECS :repeat BOOL)for idle-timer triggers
Traditional echo-area packages poll on a timer. echo-bar does this instead:
- Trigger – A hook fires or advice runs. The associated segment is marked dirty.
- Coalesce –
run-at-time 0 nilschedules a single update. Multiple triggers in the same command cycle collapse into one call. - Recompute – Only dirty segments re-evaluate their
:fn. Clean segments keep their cached output. - Render – Cached outputs are joined per zone and positioned with
space :align-todisplay properties. - Write – The rendered string is compared to the last write. If it’s identical, the overlay is not touched.
Messages are truncated with … to fit alongside the bar. If you need
the full message, check *Messages*.
Emacs 29.1 or later. No external dependencies.
- mini-echo – Similar concept, more segments out of the box. Uses a 300ms timer for updates.
- awesome-tray – Another echo-area status bar.
- doom-modeline – Not an echo-area package, but a well-optimized modeline with hook-cached segments. Good reference for the approach.
GPL-3.0-or-later. See LICENSE.
