1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//! A 1-row strip that sits BELOW the statusline. Hosts:
//! - vim `:` ex-command line (when `pending_display()` starts with `:`)
//! - the most recent toast message, dimmed, as a passive echo
//! - blank otherwise
//!
//! The vim cmdline previously rendered into the statusline's middle gap;
//! moving it here puts it where vim/neovim users reach for it and gives
//! the statusline gap back to chord-pending state (`d`, `cw`, `gqap` …).
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use crate::app::App;
use crate::ui::theme;
pub fn draw(frame: &mut Frame, app: &mut App, area: Rect) {
if area.width == 0 || area.height == 0 {
app.rects.cmdline_bar = None;
return;
}
// Register the bar's rect so a click here opens the ex-cmdline.
// Same affordance as typing `:` from anywhere — gives the user
// a mouse path to the cmdline that doesn't require knowing the
// chord. User-requested 2026-06-18.
app.rects.cmdline_bar = Some(area);
// Reset the in-flight indicator rect on every frame; populated
// below if any async HTTP op is running. Click on the indicator
// → :http.abort (mirrors Esc-on-bar behavior).
app.rects.cmdline_inflight = None;
// Toast target rect cleared each frame; populated below if the
// toast text mentions a `[name]` we can reveal.
app.rects.cmdline_toast_target = None;
let t = theme::cur();
// Default — blank line in the statusline's darker bg so it visually
// belongs with the statusline above.
let bg = t.bg_darker;
// Vim cmdline takes priority — `pending_display()` returns `:foo▏bar`
// form when the user is mid-`:`. Anything that doesn't start with `:`
// is a chord-pending hint (`d`, `gq`, …) which still belongs in the
// statusline mid space.
let pending = app.pending_display();
if let Some(line) = pending.as_deref()
&& line.starts_with(':')
{
let style = Style::default()
.fg(t.yellow)
.bg(bg)
.add_modifier(Modifier::BOLD);
frame.render_widget(
Paragraph::new(Line::from(Span::styled(line.to_string(), style)))
.style(Style::default().bg(bg)),
area,
);
return;
}
// 2026-06-19 polish — show in-flight async HTTP work as a
// persistent status indicator. Transient toasts fade after
// ~3s; long-running ops (bench, sync, lookup) used to leave
// the user with no visible progress signal. Right side of
// the bar so it doesn't fight with the toast echo.
// Build a per-op `name (Ns)` entry so the user sees how long
// each has been running. Elapsed comes from App.*_started
// (Instant); falls back to no-suffix if a stamp is missing.
let now = std::time::Instant::now();
let fmt_with_elapsed = |name: &str, start: Option<std::time::Instant>| match start {
Some(s) => format!("{name} ({}s)", now.duration_since(s).as_secs()),
None => name.to_string(),
};
let mut inflight: Vec<String> = Vec::new();
if app.http_bench_rx.is_some() {
// 2026-06-20 — bench shows live `(done/total · Ns)` if
// progress is tracked; falls back to elapsed-only.
let label = if let Some((progress, total)) = &app.http_bench_progress {
let done = progress.load(std::sync::atomic::Ordering::Relaxed);
match app.http_bench_started {
Some(s) => format!(
"bench ({done}/{total} · {}s)",
now.duration_since(s).as_secs()
),
None => format!("bench ({done}/{total})"),
}
} else {
fmt_with_elapsed("bench", app.http_bench_started)
};
inflight.push(label);
}
if app.http_sync_rx.is_some() {
inflight.push(fmt_with_elapsed("sync", app.http_sync_started));
}
if app.lookup_fire_rx.is_some() {
inflight.push(fmt_with_elapsed("lookup", app.lookup_fire_started));
}
// No cmdline → mirror the live toast (dim) so messages persist in a
// known location, not just floating top-right.
let toast = app.live_toast().map(|s| s.to_string());
let inflight_text = if inflight.is_empty() {
String::new()
} else {
format!("⟳ {} running…", inflight.join(", "))
};
let mut spans: Vec<Span<'static>> = Vec::new();
if let Some(t_msg) = toast {
// 2026-06-20 — detect `[scratch-name]` mentions so click
// can reveal the matching scratch pane. Capture the first
// bracketed name; render it in yellow to suggest it's
// interactable.
if let (Some(lb), Some(rb)) = (t_msg.find('['), t_msg.find(']'))
&& lb < rb
{
let head = &t_msg[..lb];
let name = &t_msg[lb..=rb]; // includes brackets
let tail = &t_msg[rb + 1..];
spans.push(Span::styled(
head.to_string(),
Style::default().fg(t.comment).bg(bg),
));
spans.push(Span::styled(
name.to_string(),
Style::default()
.fg(t.yellow)
.bg(bg)
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
));
spans.push(Span::styled(
tail.to_string(),
Style::default().fg(t.comment).bg(bg),
));
// Register a click rect over JUST the bracketed name
// so clicking reveals that scratch pane.
let name_x = area.x + head.chars().count() as u16;
app.rects.cmdline_toast_target = Some((
Rect {
x: name_x,
y: area.y,
width: name.chars().count() as u16,
height: 1,
},
name.trim_matches(['[', ']']).to_string(),
));
} else {
spans.push(Span::styled(t_msg, Style::default().fg(t.comment).bg(bg)));
}
}
if !inflight_text.is_empty() {
// Pad to right-align the inflight indicator. Computed
// against the visible width of the toast on the left.
let toast_w: usize = spans.iter().map(|s| s.content.chars().count()).sum();
let inflight_chars = inflight_text.chars().count();
let pad = (area.width as usize)
.saturating_sub(toast_w + inflight_chars)
.saturating_sub(1);
spans.push(Span::styled(" ".repeat(pad), Style::default().bg(bg)));
spans.push(Span::styled(
inflight_text,
Style::default()
.fg(t.yellow)
.bg(bg)
.add_modifier(Modifier::BOLD),
));
// Click rect spans the indicator text only.
let indicator_x = area.x + (toast_w + pad) as u16;
app.rects.cmdline_inflight = Some(Rect {
x: indicator_x,
y: area.y,
width: inflight_chars as u16,
height: 1,
});
}
if spans.is_empty() {
frame.render_widget(
Paragraph::new(Line::from(Span::raw(""))).style(Style::default().bg(bg)),
area,
);
} else {
frame.render_widget(
Paragraph::new(Line::from(spans)).style(Style::default().bg(bg)),
area,
);
}
}