use ratatui::{Frame, prelude::*, text::Line, widgets::Paragraph};
use crate::keys::KeyHint;
use crate::ui::views::{BlameView, DiffView};
fn hint_to_span(hint: &KeyHint) -> Span<'static> {
Span::styled(
format!(" [{}] {} ", hint.key, hint.label),
Style::default().fg(Color::Black).bg(hint.color),
)
}
fn hint_width(hint: &KeyHint) -> usize {
hint.key.len() + hint.label.len() + 5
}
fn total_hints_width(hints: &[KeyHint]) -> usize {
hints.iter().enumerate().fold(0, |acc, (i, hint)| {
acc + hint_width(hint) + if i > 0 { 1 } else { 0 }
})
}
fn build_line(hints: &[KeyHint]) -> Line<'static> {
let mut spans = Vec::with_capacity(hints.len() * 2);
for (i, hint) in hints.iter().enumerate() {
if i > 0 {
spans.push(Span::raw(" "));
}
spans.push(hint_to_span(hint));
}
Line::from(spans)
}
fn build_content(hints: &[KeyHint], width: u16) -> Vec<Line<'static>> {
let width = width as usize;
if total_hints_width(hints) <= width {
return vec![build_line(hints)];
}
let mut first_row_width = 0;
let mut split_index = hints.len();
for (i, hint) in hints.iter().enumerate() {
let w = hint_width(hint) + if i > 0 { 1 } else { 0 };
if first_row_width + w > width {
split_index = i;
break;
}
first_row_width += w;
}
let split_index = split_index.max(1);
let (first_hints, second_hints) = hints.split_at(split_index);
vec![
build_line(first_hints),
Line::from(""), build_line(second_hints),
]
}
pub fn build_status_bar_with_prefix(
prefix: Vec<Span<'static>>,
hints: &[KeyHint],
) -> Line<'static> {
let mut spans = prefix;
for hint in hints {
spans.push(Span::raw(" "));
spans.push(hint_to_span(hint));
}
Line::from(spans)
}
pub fn status_hints_height(hints: &[KeyHint], width: u16) -> u16 {
if total_hints_width(hints) > width as usize {
3 } else {
1
}
}
fn status_bar_area(frame: &Frame, hints: &[KeyHint]) -> Option<Rect> {
let area = frame.area();
if area.height < 2 {
return None;
}
let height = status_hints_height(hints, area.width);
let actual_height = if area.height < height + 1 { 1 } else { height };
Some(Rect {
x: area.x,
y: area.y + area.height - actual_height,
width: area.width,
height: actual_height,
})
}
pub fn render_status_hints(frame: &mut Frame, hints: &[KeyHint]) {
let Some(status_area) = status_bar_area(frame, hints) else {
return;
};
let content = if status_area.height >= 3 {
build_content(hints, status_area.width)
} else {
vec![build_line(hints)]
};
frame.render_widget(Paragraph::new(content), status_area);
}
pub fn render_diff_status_bar(frame: &mut Frame, diff_view: &DiffView) {
let hints = crate::keys::DIFF_VIEW_HINTS;
let Some(status_area) = status_bar_area(frame, hints) else {
return;
};
let context = diff_view.current_context();
let prefix = vec![
Span::styled(
format!(" {} ", diff_view.revision),
Style::default().fg(Color::Black).bg(Color::Yellow),
),
Span::raw(" "),
Span::styled(format!(" {} ", context), Style::default().fg(Color::Cyan)),
];
let status = build_status_bar_with_prefix(prefix, hints);
frame.render_widget(Paragraph::new(status), status_area);
}
pub fn render_blame_status_bar(frame: &mut Frame, blame_view: &BlameView) {
let hints = crate::keys::BLAME_VIEW_HINTS;
let Some(status_area) = status_bar_area(frame, hints) else {
return;
};
let file_path = blame_view.file_path();
let prefix = vec![
Span::styled(
format!(" {} ", file_path),
Style::default().fg(Color::Black).bg(Color::Yellow),
),
Span::raw(" "),
];
let status = build_status_bar_with_prefix(prefix, hints);
frame.render_widget(Paragraph::new(status), status_area);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hint_to_span() {
let hint = KeyHint {
key: "q",
label: "Quit",
color: Color::Red,
};
let span = hint_to_span(&hint);
assert!(span.content.contains("[q]"));
assert!(span.content.contains("Quit"));
}
#[test]
fn test_hint_width() {
let hint = KeyHint {
key: "q",
label: "Quit",
color: Color::Red,
};
assert_eq!(hint_width(&hint), 10);
}
#[test]
fn test_build_line() {
let hints = &[
KeyHint {
key: "q",
label: "Quit",
color: Color::Red,
},
KeyHint {
key: "?",
label: "Help",
color: Color::Cyan,
},
];
let line = build_line(hints);
assert!(!line.spans.is_empty());
}
#[test]
fn test_build_content_single_line() {
let hints = &[KeyHint {
key: "q",
label: "Quit",
color: Color::Red,
}];
let content = build_content(hints, 80);
assert_eq!(content.len(), 1);
}
#[test]
fn test_build_content_two_lines() {
let hints = &[
KeyHint {
key: "a",
label: "AAAA",
color: Color::Red,
},
KeyHint {
key: "b",
label: "BBBB",
color: Color::Red,
},
];
let content = build_content(hints, 15);
assert_eq!(content.len(), 3); }
#[test]
fn test_build_status_bar_with_prefix() {
let prefix = vec![Span::raw("Test: ")];
let hints = &[KeyHint {
key: "q",
label: "Quit",
color: Color::Red,
}];
let line = build_status_bar_with_prefix(prefix, hints);
assert!(!line.spans.is_empty());
}
#[test]
fn test_status_hints_height_single() {
let hints = &[KeyHint {
key: "q",
label: "Quit",
color: Color::Red,
}];
assert_eq!(status_hints_height(hints, 80), 1);
}
#[test]
fn test_status_hints_height_multi() {
let hints = &[
KeyHint {
key: "a",
label: "AAAA",
color: Color::Red,
},
KeyHint {
key: "b",
label: "BBBB",
color: Color::Red,
},
];
assert_eq!(status_hints_height(hints, 15), 3);
}
#[test]
fn test_build_content_extremely_narrow() {
let hints = &[
KeyHint {
key: "a",
label: "AAAA",
color: Color::Red,
},
KeyHint {
key: "b",
label: "BBBB",
color: Color::Red,
},
];
let content = build_content(hints, 5);
assert_eq!(content.len(), 3);
assert!(!content[0].spans.is_empty());
}
}