diff --git a/client/src/components/handlers/sidebar.rs b/client/src/components/handlers/sidebar.rs index a6192cc..f92c28f 100644 --- a/client/src/components/handlers/sidebar.rs +++ b/client/src/components/handlers/sidebar.rs @@ -8,10 +8,15 @@ use ratatui::{ use crate::config::colors::themes::Theme; use common::proto::multieko2::table_definition::{ProfileTreeResponse}; use ratatui::text::{Span, Line}; +use crate::components::utils::text::truncate_string; // Reduced sidebar width -const SIDEBAR_WIDTH: u16 = 12; +const SIDEBAR_WIDTH: u16 = 20; +// --- Icons --- +const ICON_PROFILE: &str = "📁"; +const ICON_TABLE: &str = "📄"; + pub fn calculate_sidebar_layout(show_sidebar: bool, main_content_area: Rect) -> (Option, Rect) { if show_sidebar { let chunks = Layout::default() @@ -36,18 +41,54 @@ pub fn render_sidebar( ) { let sidebar_block = Block::default().style(Style::default().bg(theme.bg)); let mut items = Vec::new(); + let profile_name_available_width = (SIDEBAR_WIDTH as usize).saturating_sub(3); + let table_name_available_width = (SIDEBAR_WIDTH as usize).saturating_sub(5); if let Some(profile_name) = selected_profile { - // Existing code for when a profile is selected... + // Find the selected profile in the tree + if let Some(profile) = profile_tree + .profiles + .iter() + .find(|p| &p.name == profile_name) + { + // Add profile name as header + items.push(ListItem::new(Line::from(vec![ + Span::styled(format!("{} ", ICON_PROFILE), Style::default().fg(theme.accent)), + Span::styled( + truncate_string(&profile.name, profile_name_available_width), + Style::default().fg(theme.highlight) + ), + ]))); + + // List tables for the selected profile + for table in &profile.tables { + // Get table name without year prefix to save space + let display_name = if table.name.starts_with("2025_") { + &table.name[5..] // Skip "2025_" prefix + } else { + &table.name + }; + items.push(ListItem::new(Line::from(vec![ + Span::raw(" "), // Indentation + Span::styled(format!("{} ", ICON_TABLE), Style::default().fg(theme.secondary)), + Span::styled( + truncate_string(display_name, table_name_available_width), + theme.fg + ), + ]))); + } + } } else { // Show full profile tree when no profile is selected (compact version) for (profile_idx, profile) in profile_tree.profiles.iter().enumerate() { // Profile header - more compact items.push(ListItem::new(Line::from(vec![ - Span::styled("◆", Style::default().fg(theme.accent)), - Span::styled(&profile.name, Style::default().fg(theme.highlight)), + Span::styled(format!("{} ", ICON_PROFILE), Style::default().fg(theme.accent)), + Span::styled( + &profile.name, + Style::default().fg(theme.highlight) + ), ]))); - // Tables with compact prefixes for (table_idx, table) in profile.tables.iter().enumerate() { let is_last_table = table_idx == profile.tables.len() - 1; @@ -68,18 +109,18 @@ pub fn render_sidebar( &table.name }; - let mut line = vec![ - Span::styled(prefix, Style::default().fg(theme.fg)), - Span::styled(display_name, Style::default().fg(theme.fg)), - ]; + // Adjust available width if dependency arrow is shown + let current_table_available_width = if !table.depends_on.is_empty() { + table_name_available_width.saturating_sub(1) + } else { + table_name_available_width + }; - // Show a simple indicator for dependencies instead of listing them - if !table.depends_on.is_empty() { - line.push(Span::styled( - "→", - Style::default().fg(theme.secondary) - )); - } + let line = vec![ + Span::styled(prefix, Style::default().fg(theme.fg)), + Span::styled(format!("{} ", ICON_TABLE), Style::default().fg(theme.secondary)), + Span::styled(truncate_string(display_name, current_table_available_width), Style::default().fg(theme.fg)), + ]; items.push(ListItem::new(Line::from(line))); } diff --git a/client/src/components/mod.rs b/client/src/components/mod.rs index 0a5dae9..05628fd 100644 --- a/client/src/components/mod.rs +++ b/client/src/components/mod.rs @@ -5,6 +5,7 @@ pub mod admin; pub mod common; pub mod form; pub mod auth; +pub mod utils; pub use handlers::*; pub use intro::*; @@ -12,3 +13,4 @@ pub use admin::*; pub use common::*; pub use form::*; pub use auth::*; +pub use utils::*; diff --git a/client/src/components/utils.rs b/client/src/components/utils.rs new file mode 100644 index 0000000..1145983 --- /dev/null +++ b/client/src/components/utils.rs @@ -0,0 +1,4 @@ +// src/components/utils.rs +pub mod text; + +pub use text::*; diff --git a/client/src/components/utils/text.rs b/client/src/components/utils/text.rs new file mode 100644 index 0000000..d37239f --- /dev/null +++ b/client/src/components/utils/text.rs @@ -0,0 +1,29 @@ +// src/components/utils/text.rs + +use unicode_width::UnicodeWidthStr; +use unicode_segmentation::UnicodeSegmentation; + +/// Truncates a string to a maximum width, adding an ellipsis if truncated. +/// Considers unicode character widths. +pub fn truncate_string(s: &str, max_width: usize) -> String { + if UnicodeWidthStr::width(s) <= max_width { + s.to_string() + } else { + let ellipsis = "…"; + let ellipsis_width = UnicodeWidthStr::width(ellipsis); + let mut truncated_width = 0; + let mut end_byte_index = 0; + + // Iterate over graphemes to handle multi-byte characters correctly + for (i, g) in s.grapheme_indices(true) { + let char_width = UnicodeWidthStr::width(g); + if truncated_width + char_width + ellipsis_width > max_width { + break; + } + truncated_width += char_width; + end_byte_index = i + g.len(); + } + + format!("{}{}", &s[..end_byte_index], ellipsis) + } +}