use ratatui::{
buffer::Buffer,
layout::Rect,
text::{Line, Span},
widgets::{Paragraph, Widget},
};
use crate::config::AppearanceConfig;
use crate::package::Runner;
use crate::tui::theme::Theme;
pub struct Header<'a> {
project_name: &'a str,
runner: Runner,
theme: &'a Theme,
show_icons: bool,
}
impl<'a> Header<'a> {
pub fn new(
project_name: &'a str,
runner: Runner,
theme: &'a Theme,
config: &AppearanceConfig,
) -> Self {
Self {
project_name,
runner,
theme,
show_icons: config.icons,
}
}
fn build_line(&self, width: u16) -> Line<'a> {
let icon = if self.show_icons {
self.runner.icon()
} else {
""
};
let help_hint = "[?]";
let icon_len = if self.show_icons { 2 } else { 0 }; let runner_part = format!(" {} ", self.runner.display_name());
let help_len = help_hint.len() + 2; let fixed_parts = icon_len + runner_part.len() + help_len + 4;
let max_project_len = (width as usize).saturating_sub(fixed_parts);
let project_display = truncate_with_ellipsis(self.project_name, max_project_len);
let mut spans = Vec::new();
spans.push(Span::raw(" "));
if self.show_icons && !icon.is_empty() {
spans.push(Span::styled(
format!("{} ", icon),
self.theme.header_project(),
));
}
spans.push(Span::styled(project_display, self.theme.header_project()));
let left_len = spans.iter().map(|s| s.content.len()).sum::<usize>();
let right_content = format!("{} {} ", runner_part, help_hint);
let padding_len = (width as usize).saturating_sub(left_len + right_content.len());
if padding_len > 0 {
spans.push(Span::styled(" ".repeat(padding_len), self.theme.header()));
}
spans.push(Span::styled(runner_part, self.theme.header_runner()));
spans.push(Span::styled(help_hint, self.theme.header()));
spans.push(Span::raw(" "));
Line::from(spans)
}
}
impl Widget for Header<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.height == 0 {
return;
}
let line = self.build_line(area.width);
let paragraph = Paragraph::new(line).style(self.theme.header());
paragraph.render(area, buf);
}
}
pub fn truncate_with_ellipsis(s: &str, max_len: usize) -> String {
let char_count = s.chars().count();
if char_count <= max_len {
s.to_string()
} else if max_len == 0 {
String::new()
} else if max_len <= 3 {
s.chars().take(max_len).collect()
} else {
let truncated: String = s.chars().take(max_len - 1).collect();
format!("{}…", truncated)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_with_ellipsis() {
assert_eq!(truncate_with_ellipsis("hello", 10), "hello");
assert_eq!(truncate_with_ellipsis("hello world", 8), "hello w…");
assert_eq!(truncate_with_ellipsis("hi", 2), "hi");
assert_eq!(truncate_with_ellipsis("hello", 5), "hello");
assert_eq!(truncate_with_ellipsis("hello", 4), "hel…");
}
#[test]
fn test_truncate_very_short() {
assert_eq!(truncate_with_ellipsis("hello", 3), "hel");
assert_eq!(truncate_with_ellipsis("hello", 2), "he");
assert_eq!(truncate_with_ellipsis("hello", 1), "h");
}
#[test]
fn test_truncate_zero_length() {
assert_eq!(truncate_with_ellipsis("hello", 0), "");
}
#[test]
fn test_truncate_unicode() {
assert_eq!(truncate_with_ellipsis("こんにちは", 6), "こんにちは"); assert_eq!(truncate_with_ellipsis("こんにちは", 5), "こんにちは"); assert_eq!(truncate_with_ellipsis("こんにちは", 4), "こんに…");
assert_eq!(truncate_with_ellipsis("🚀🎉🔥", 4), "🚀🎉🔥"); assert_eq!(truncate_with_ellipsis("🚀🎉🔥", 3), "🚀🎉🔥"); assert_eq!(truncate_with_ellipsis("🚀🎉🔥🎯", 4), "🚀🎉🔥🎯"); assert_eq!(truncate_with_ellipsis("🚀🎉🔥🎯", 3), "🚀🎉🔥"); assert_eq!(truncate_with_ellipsis("🚀🎉🔥🎯🌟", 4), "🚀🎉🔥…");
assert_eq!(truncate_with_ellipsis("hello世界", 8), "hello世界"); assert_eq!(truncate_with_ellipsis("hello世界", 7), "hello世界"); assert_eq!(truncate_with_ellipsis("hello世界", 6), "hello…"); }
#[test]
fn test_header_build_line() {
let theme = Theme::default();
let config = AppearanceConfig::default();
let header = Header::new("my-project", Runner::Npm, &theme, &config);
let line = header.build_line(80);
let content: String = line.spans.iter().map(|s| s.content.to_string()).collect();
assert!(content.contains("my-project"));
assert!(content.contains("npm"));
assert!(content.contains("[?]"));
}
#[test]
fn test_header_unicode_project_name() {
let theme = Theme::default();
let config = AppearanceConfig::default();
let header = Header::new("日本語プロジェクト", Runner::Npm, &theme, &config);
let line = header.build_line(80);
let content: String = line.spans.iter().map(|s| s.content.to_string()).collect();
assert!(content.contains("日本語"));
assert!(content.contains("npm"));
}
}