use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
};
use crate::ui::SelectItemMetadata;
use crate::utils::format_relative_time;
pub const PREVIEW_LABEL_WIDTH: u16 = 9;
pub const PREVIEW_MAX_WIDTH: u16 = 60;
pub const PREVIEW_FILE_INDENT: u16 = 2;
pub const PREVIEW_FILE_PATH_OFFSET: u16 = 4;
const TRUNCATION_SUFFIX: &str = "...";
const TRUNCATION_SUFFIX_LEN: usize = 3;
pub fn truncate_string(s: &str, max_len: usize) -> String {
let char_count = s.chars().count();
if char_count <= max_len || max_len <= TRUNCATION_SUFFIX_LEN {
return s.to_string();
}
let truncated: String = s.chars().take(max_len - TRUNCATION_SUFFIX_LEN).collect();
format!("{}{}", truncated, TRUNCATION_SUFFIX)
}
pub fn render_labeled_row(
buf: &mut Buffer,
x: u16,
y: u16,
label: &str,
value: &str,
value_color: Color,
) {
buf.set_string(x, y, label, Style::default().fg(Color::DarkGray));
buf.set_string(
x + PREVIEW_LABEL_WIDTH,
y,
value,
Style::default().fg(value_color),
);
}
pub fn render_preview_metadata(
buf: &mut Buffer,
inner: Rect,
metadata: &SelectItemMetadata,
preview_width: u16,
) {
let max_y = inner.y + inner.height;
let mut current_y = inner.y + 2;
if let Some(ref change_status) = metadata.change_status {
if current_y >= max_y {
return;
}
render_labeled_row(
buf,
inner.x,
current_y,
"Status: ",
change_status.status_label(),
change_status.status_color(),
);
current_y += 1;
}
if let Some(ref sync_status) = metadata.sync_status {
if current_y >= max_y {
return;
}
let sync_color = if sync_status.is_synced() {
Color::Green
} else {
Color::Yellow
};
render_labeled_row(
buf,
inner.x,
current_y,
"Sync: ",
&sync_status.display(),
sync_color,
);
current_y += 1;
}
current_y += 1;
if current_y >= max_y {
return;
}
let relative_time = format_relative_time(&metadata.last_commit_date);
buf.set_string(
inner.x,
current_y,
format!("Updated: {}", relative_time),
Style::default().fg(Color::DarkGray),
);
current_y += 1;
if current_y >= max_y {
return;
}
buf.set_string(
inner.x,
current_y,
format!("By: {}", metadata.last_committer_name),
Style::default().fg(Color::DarkGray),
);
current_y += 1;
if current_y >= max_y {
return;
}
let max_msg_len = (preview_width as usize).saturating_sub(18);
let commit_msg = truncate_string(&metadata.last_commit_message, max_msg_len);
buf.set_string(
inner.x,
current_y,
format!("Commit: {}", commit_msg),
Style::default().fg(Color::DarkGray),
);
current_y += 1;
if let Some(ref change_status) = metadata.change_status {
if !change_status.changed_files.is_empty() {
current_y += 1; if current_y >= max_y {
return;
}
buf.set_string(
inner.x,
current_y,
"Recent changes:",
Style::default().fg(Color::DarkGray),
);
current_y += 1;
let max_path_len = (preview_width as usize).saturating_sub(6);
for file in &change_status.changed_files {
if current_y >= max_y {
return;
}
buf.set_string(
inner.x + PREVIEW_FILE_INDENT,
current_y,
format!("{} ", file.status),
Style::default().fg(file.status_color()),
);
let display_path = truncate_string(&file.path, max_path_len);
buf.set_string(
inner.x + PREVIEW_FILE_PATH_OFFSET,
current_y,
&display_path,
Style::default().fg(Color::DarkGray),
);
current_y += 1;
}
}
}
}
pub fn calculate_preview_height(metadata: &Option<SelectItemMetadata>) -> u16 {
let Some(metadata) = metadata else {
return 3; };
let mut height = 4;
if metadata.change_status.is_some() {
height += 1; }
if metadata.sync_status.is_some() {
height += 1; }
height += 4;
if let Some(ref change_status) = metadata.change_status {
if !change_status.changed_files.is_empty() {
height += 2 + change_status.changed_files.len().min(5) as u16; }
}
height
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_string_short() {
let result = truncate_string("hello", 10);
assert_eq!(result, "hello");
}
#[test]
fn test_truncate_string_exact() {
let result = truncate_string("hello", 5);
assert_eq!(result, "hello");
}
#[test]
fn test_truncate_string_long() {
let result = truncate_string("hello world", 8);
assert_eq!(result, "hello...");
}
#[test]
fn test_truncate_string_min_length() {
let result = truncate_string("hello", 3);
assert_eq!(result, "hello");
}
#[test]
fn test_truncate_string_unicode() {
let result = truncate_string("日本語テスト", 6);
assert_eq!(result, "日本語テスト");
let result = truncate_string("日本語テスト", 5);
assert_eq!(result, "日本...");
}
#[test]
fn test_calculate_preview_height_none() {
assert_eq!(calculate_preview_height(&None), 3);
}
}