fix: the minibuffer row carries a pending status message (improvise-mai9)

`draw_bottom_bar` rendered the minibuffer INSTEAD of the status line, so a
status message written while any text-entry mode was open was never drawn — not
truncated, not overwritten, simply never rendered. A test asserting
`app.status_msg == "..."` passed in every one of those cases, which is why this
survived improvise-6t3's careful ordering of `CommitBuffer`'s effects so a
failure message would win: it won a race to a surface nobody could see.

Of the ticket's two candidates, the row is shared. `minibuffer_line` composes
the prompt and buffer on the left and any pending `status_msg` right-aligned,
so the bottom bar stays ONE row and the exact-frame geometry does not move.

The alternative — making the caller structurally unable to report while its
minibuffer is open — is what `CommitSave` does by hand today, and generalising
it means repeating that at every text-entry commit. The ticket calls that a
workaround; sharing the row costs one function and covers all eight modes at
once, including the `:w /bad/path` path Copilot found on PR #27.

The failure is now loud: `a_status_message_set_while_a_minibuffer_is_open_is_
on_the_bottom_row` asserts the composed row, not the field. Its negative arm is
not decoration — a composition that appended unconditionally would pass the
first test and draw two spaces of nothing on every minibuffer keystroke.

Injected fault: the `false` arm returning `String::new()`, i.e. the old
composition. The first test fails naming the missing message; the second passes.

Known and out of scope: `fill_line` pads on `left.len()`, which is bytes, so
the right-hand text sits two cells right of its mark whenever the left carries
the multi-byte cursor glyph. `draw_status` has always done this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-20 20:40:39 -07:00
parent fc059c7ce1
commit 94a3d1754b
+42 -1
View File
@@ -388,6 +388,17 @@ fn draw_tile_bar(f: &mut Frame, area: Rect, app: &App) {
);
}
/// The minibuffer row, with any pending status message riding on its right.
/// The minibuffer owns the bottom row and used to hide it — tui.md §3.
fn minibuffer_line(prompt: &str, buf: &str, status_msg: &str, width: u16) -> String {
let left = format!("{prompt}{buf}▌");
let right = match status_msg.is_empty() {
true => String::new(),
false => format!(" {status_msg} "),
};
fill_line(left, &right, width)
}
fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
if let Some(mb) = app.view_state.mode.minibuffer() {
let buf = app
@@ -396,7 +407,7 @@ fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
.get(&mb.buffer_key)
.map(|s| s.as_str())
.unwrap_or("");
let text = format!("{}{}▌", mb.prompt, buf);
let text = minibuffer_line(&mb.prompt, buf, &app.view_state.status_msg, area.width);
f.render_widget(
Paragraph::new(text).style(
Style::default()
@@ -527,6 +538,36 @@ fn draw_welcome(f: &mut Frame, area: Rect) {
}
}
#[cfg(test)]
mod bottom_bar_tests {
use super::minibuffer_line;
/// improvise-mai9: the message used not to be drawn at all — see
/// context/test-evidence/tui.md.
#[test]
fn a_status_message_set_while_a_minibuffer_is_open_is_on_the_bottom_row() {
let line = minibuffer_line("Category: ", "North", "category already exists", 80);
assert!(
line.contains("Category: North"),
"the minibuffer still owns the left of the row: {line:?}"
);
assert!(
line.contains("category already exists"),
"the status message must reach the user: {line:?}"
);
}
#[test]
fn with_no_message_pending_the_minibuffer_row_is_only_the_minibuffer() {
let line = minibuffer_line("Category: ", "North", "", 80);
assert_eq!(
line.trim_end(),
"Category: North\u{258c}",
"an empty status adds nothing but padding"
);
}
}
#[cfg(test)]
mod key_conversion_tests {
use super::*;