use super::super::surface::SPINNER;
use super::super::theme::{accent, on_accent, secondary};
use super::action_line::{
CANCEL_HINT, SEPARATOR, action_text, bare_action_width, busy_row_plan, running_call,
};
use crate::interactive::session_prompt::StatusView;
use crate::interactive::tui::types::App;
use crate::interactive::tui::wrap::cell_width;
use ratatui::{
Frame,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
};
use status_segments::{BarWords, bar_spans, bar_words, fit_bar, full_fit, segments_width};
#[path = "status_segments.rs"]
mod status_segments;
pub(super) fn approval_colour(mode: &str) -> Color {
match mode {
"read-only" => super::super::theme::success(),
"ask" => super::super::theme::warning(),
"never" => super::super::theme::danger(),
"bypass" => super::super::theme::danger(),
_ => secondary(),
}
}
#[cfg(test)]
pub(crate) fn status_words_for_test(view: &StatusView) -> String {
let words = bar_words(view);
bar_spans(&words, full_fit(&words), Color::Reset)
.iter()
.map(|span| span.content.as_ref())
.collect()
}
#[cfg(test)]
pub(crate) fn bar_words_for_test(view: &StatusView, hint: &'static str, width: u16) -> String {
let words = bar_words(view);
let mut spans = Vec::new();
push_bar_row(
&mut spans,
&words,
hint,
width as usize,
Color::Reset,
Style::default(),
);
spans.iter().map(|span| span.content.as_ref()).collect()
}
fn push_bar_row(
spans: &mut Vec<Span<'static>>,
words: &BarWords,
hint: &'static str,
width: usize,
bg: Color,
bar: Style,
) {
let keep = fit_bar(words, cell_width(hint), width);
spans.extend(bar_spans(words, keep, bg));
if keep.hint {
let pad = width.saturating_sub(segments_width(words, keep) + cell_width(hint));
spans.push(Span::styled(" ".repeat(pad), bar));
spans.push(Span::styled(hint, bar));
}
}
fn unseen_span(count: usize, bg: Color) -> Option<Span<'static>> {
if count == 0 {
return None;
}
let noun = if count == 1 { "line" } else { "lines" };
Some(Span::styled(
format!("{count} new {noun} below · Shift+End to catch up "),
Style::default().bg(bg).fg(accent()),
))
}
pub(in crate::interactive::tui) fn draw_status(
frame: &mut Frame<'_>,
app: &App,
status: &StatusView,
area: Rect,
) {
let bar = Style::default()
.bg(super::super::theme::status_bg())
.fg(secondary());
let bg = super::super::theme::status_bg();
let unseen = unseen_span(app.unseen_new_rows(), bg);
let words = bar_words(status);
let line = if app.is_busy() {
let frame_char = SPINNER[app.spinner % SPINNER.len()];
let elapsed = app
.request
.started
.map(|start| start.elapsed().as_secs())
.unwrap_or(0);
let segments = bar_spans(&words, full_fit(&words), bg);
let widths: Vec<usize> = segments.iter().map(|span| span.width()).collect();
let unseen_width = unseen.as_ref().map(|span| span.width()).unwrap_or(0);
let elapsed_width = cell_width(&format!("{elapsed}s "));
let frame_width = cell_width(&format!(" {frame_char} "));
let full_action = action_text(
app.request.activity.as_deref(),
running_call(app),
usize::MAX,
);
let plan = busy_row_plan(
area.width as usize,
unseen_width,
frame_width,
elapsed_width,
&widths,
cell_width(&full_action),
bare_action_width(app.request.activity.as_deref()),
);
let doing = action_text(
app.request.activity.as_deref(),
running_call(app),
plan.action_room,
);
let mut spans = Vec::new();
if plan.lead_painted
&& let Some(span) = unseen
{
spans.push(span);
}
spans.push(Span::styled(
format!(" {frame_char} {doing}{elapsed}s "),
Style::default().bg(bg).fg(accent()),
));
spans.push(Span::styled(SEPARATOR, bar));
spans.extend(segments.into_iter().take(plan.status_segments));
let used = spans.iter().map(Span::width).sum::<usize>();
let hint_width = cell_width(CANCEL_HINT);
if used + hint_width < area.width as usize {
let pad = area.width as usize - used - hint_width;
spans.push(Span::styled(" ".repeat(pad), bar));
}
spans.push(Span::styled(CANCEL_HINT, bar));
Line::from(spans)
} else if app.overlays.selection_mode {
let mut spans = Vec::new();
let mut lead_width = 0usize;
if let Some(span) = unseen {
lead_width += span.width();
spans.push(span);
}
let badge = Span::styled(
" SELECT ",
Style::default()
.bg(accent())
.fg(on_accent())
.add_modifier(Modifier::BOLD),
);
lead_width += badge.width();
spans.push(badge);
push_bar_row(
&mut spans,
&words,
"drag to copy · Ctrl+O to resume scrolling",
(area.width as usize).saturating_sub(lead_width),
bg,
bar,
);
Line::from(spans)
} else {
let mut spans = Vec::new();
let mut lead_width = 0usize;
if let Some(span) = unseen {
lead_width = span.width();
spans.push(span);
}
push_bar_row(
&mut spans,
&words,
"? for help",
(area.width as usize).saturating_sub(lead_width),
bg,
bar,
);
Line::from(spans)
};
frame.render_widget(Paragraph::new(line).style(bar), area);
}
#[cfg(test)]
#[path = "status_cancel_tests.rs"]
mod cancel_tests;
#[cfg(test)]
#[path = "status_cell_tests.rs"]
mod status_cell_tests;
#[cfg(test)]
#[path = "status_split_tests.rs"]
mod status_split_tests;
#[cfg(test)]
#[path = "status_tests.rs"]
mod tests;