ADR 0062: The status bar message line¶
Status¶
Accepted
Context¶
The editor had no way to say anything. An unconfigured language server,
a : command that doesn't exist, a plugin that failed to load, a write
that failed — all of it was silence, which reads as "broken" rather
than as "not set up". That is exactly how an external tester concluded
LSP didn't work in the packaged builds.
Decision¶
Two problems, not one¶
Worth separating before building anything, because conflating them is how editors end up with toast spam.
State is continuously true or false — "a language server is running", "a build is in flight", "you are in Normal mode". The answer is wanted at a glance, at any moment. A message is the wrong shape for it: it fires once and is gone, so anyone who looked away can never find out. The tester's complaint was this kind — they didn't miss a message, there was nothing to look at.
Events happen at an instant and are stale a second later: saved, no matches, config reloaded, command not found.
This ADR builds the second. State gets a persistent status-bar segment, next.
One funnel¶
Everything the editor says goes through EditorViewport::notify(level,
text), which emits messagePosted; MainWindow owns the rendering.
One place decides what a message looks like.
This is the single most important decision here, and it is not about this feature: three ways of telling the user something becomes six within a month, all slightly different. That is precisely what happened with animation durations before ADR 0053 consolidated them, and the fix was more expensive than the rule would have been.
notify() is public, so panels — and eventually plugins — can reach it.
A plugin that cannot say "that file isn't a thing" is a plugin that
fails silently.
The status bar, not toasts¶
The message occupies the free middle of the status bar, immediately
right of the mode label. Mode and Ln/Col keep their places and never
move.
Floating toasts were rejected: they cover the text you are reading, they stack, and they are the most "app-like" chrome there is — in a project whose entire aesthetic is no lines, no boxes, state carried by opacity (ADR 0007, ADR 0022). The status bar is already there, already the place you look for state, and is what vim itself uses, in an editor that defaults to Vim mode.
Messages replace each other rather than queueing. A queue means being told about something that happened several seconds ago, which is worse than missing it.
Three tiers, separated by dwell — not by colour¶
| rendering | dwell | |
|---|---|---|
| Info | text colour, one opacity tier down | 3s |
| Warning | full-strength text colour | 6s |
| Error | diagnostic_error |
until you do something else |
Info drops a tier because a confirmation you did not ask for should not shout as loudly as the mode label beside it — the same "secondary lines drop one tier" move the About panel and welcome screen already make (ADR 0055). A warning is a sentence you are meant to finish reading, usually saying the thing you just asked for did not happen, so it does not drop and it stays twice as long.
Errors reuse the one non-monochrome colour the theme already sanctions,
the same diagnostic_error as the gutter dot and the underline. No new
hue enters the palette.
An error stays until you do something else¶
Vim's rule, and the reasoning is not aesthetic: the one message you must not miss is the one saying a thing you asked for did not happen. A save that failed while you were looking at your other monitor is exactly how people lose work. So errors have no timeout.
"Doing something else" is any cursor move or edit. statusChanged
already fires on exactly those and on nothing that isn't the user
(ADR 0023), so this needs no new signal and cannot miss a case a new one
would. Sticky dismissal is armed one event-loop turn after the message
appears, so the keystroke that caused the error cannot also dismiss it
before it has been seen.
Switching buffers clears the message too: one raised by the file you just left, sitting in the bar above a different file's text, reads as being about this one.
Long text elides, rather than pushing the readout off the edge¶
A message with a long path elides in the middle, with the full string on
hover. The alternative — letting the label grow — pushes Ln/Col off a
narrow window, trading a permanent readout for a transient one.
Consequences¶
Five call sites wired, chosen so the thing is judgeable rather than
theoretical: save success, save failure, unknown : command, no search
matches, and config hot-reload.
Two of those were silent bugs rather than missing features:
- A failed write was indistinguishable from a successful one. The dirty marker simply stayed. You found out that your file wasn't on disk by noticing an asterisk that should have cleared.
- A typo in a
:command looked exactly like a command that ran and did nothing — ADR 0025's deliberate silent no-op, which was the right call when there was nowhere to say otherwise.
Search reports "no matches" on Enter only, never while typing: incremental search passes through "no matches" on the way to almost every real query, and a message per keystroke is noise.
Verified live: saved notif.c renders one tier below the mode label
(192 vs 225 measured) and is gone by 4s; unknown command: frobnicate
sits at full strength; could not write ro.txt renders red, survives
several seconds untouched, and clears the instant j moves the cursor;
switching tabs clears a message raised by the other buffer.
ctest 9/9, clean build, zero warnings.
Deliberately not built here:
:messageshistory, so a message you missed is recoverable. TheOutputPanelalready exists as a docked, non-modal, streaming surface and should grow a second mode rather than a new widget being invented.- The persistent LSP state segment — the actual fix for the tester's report, and state rather than an event.
- Desktop notifications. Wrong layer, new dependency, and an editor interrupting your desktop to say "saved" is obnoxious.