use humantime::format_duration; use std::path::PathBuf; use std::time::Duration; use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthStr; use zellij_tile::prelude::*; use crate::single_screen::UnifiedSearchResult; use crate::ui::{PaneUiInfo, SessionUiInfo, TabUiInfo}; use crate::{ActiveScreen, NewSessionInfo}; // --------------------------------------------------------------- // Render cache for unified results // --------------------------------------------------------------- /// Pre-computed data for a single visible row, independent of selection state. #[derive(Clone)] pub struct CachedRowData { pub session_name: String, pub indices: Vec, pub original_index: usize, pub kind: CachedRowKind, // Pre-formatted strings (computed once, reused across renders) pub full_details: String, pub abbr_details: String, pub full_tag: &'static str, pub abbr_tag: &'static str, // Pre-computed widths pub name_width: usize, pub full_details_width: usize, pub abbr_details_width: usize, pub full_tag_width: usize, // Color range data for details cell pub details_color_ranges: DetailsColorRanges, pub abbr_details_color_ranges: DetailsColorRanges, } #[derive(Clone)] pub enum CachedRowKind { Active, Resurrectable, } /// Byte-offset ranges for coloring details cells. #[derive(Clone, Default)] pub struct DetailsColorRanges { pub ranges: Vec<(usize, std::ops::Range)>, // (color_index, range) } /// Cached intermediate representation of the unified results table. /// /// Stored on `SingleScreenState` and rebuilt only when `unified_results` /// changes (via `update_search_term` / `SessionUpdate`). Selection changes /// and timer re-renders reuse the cached data. #[derive(Default)] pub struct UnifiedResultsRenderCache { /// Filtered rows (current session excluded), with pre-formatted strings. pub rows: Vec, /// Max column widths across all cached rows. pub full_name_width: usize, pub full_details_width: usize, pub abbr_details_width: usize, pub full_tag_width: usize, } impl UnifiedResultsRenderCache { /// Rebuild the cache from the current `unified_results`. pub fn rebuild(&mut self, results: &[UnifiedSearchResult]) { self.rows.clear(); self.full_name_width = 0; self.full_details_width = 0; self.abbr_details_width = 0; self.full_tag_width = 0; for (orig_i, result) in results.iter().enumerate() { let is_current = matches!( result, UnifiedSearchResult::ActiveSession { is_current_session: true, .. } ); if is_current { continue; } let row = match result { UnifiedSearchResult::ActiveSession { indices, session_name, connected_users, tab_count, pane_count, .. } => { let client_word = if *connected_users == 1 { "client" } else { "clients" }; let tab_str = format!("{}", tab_count); let pane_str = format!("{}", pane_count); let conn_str = format!("{}", connected_users); // Full details let full_details = format!( "{} tabs, {} panes, {} {}", tab_str, pane_str, conn_str, client_word ); let full_details_ranges = { let tab_end = tab_str.len(); let pane_offset = tab_str.len() + " tabs, ".len(); let pane_end = pane_offset + pane_str.len(); let conn_offset = pane_end + " panes, ".len(); let conn_end = conn_offset + conn_str.len(); DetailsColorRanges { ranges: vec![ (1, 0..tab_end), (2, pane_offset..pane_end), (2, conn_offset..conn_end), ], } }; // Abbreviated details let abbr_details = format!("{}t, {}p, {}c", tab_str, pane_str, conn_str); let abbr_details_ranges = { let tab_end = tab_str.len(); let pane_offset = tab_str.len() + "t, ".len(); let pane_end = pane_offset + pane_str.len(); let conn_offset = pane_end + "p, ".len(); let conn_end = conn_offset + conn_str.len(); DetailsColorRanges { ranges: vec![ (1, 0..tab_end), (2, pane_offset..pane_end), (2, conn_offset..conn_end), ], } }; let name_width = session_name.width(); let full_details_width = full_details.width(); let abbr_details_width = abbr_details.width(); CachedRowData { session_name: session_name.clone(), indices: indices.clone(), original_index: orig_i, kind: CachedRowKind::Active, full_details, abbr_details, full_tag: "[ATTACH]", abbr_tag: "[A]", name_width, full_details_width, abbr_details_width, full_tag_width: "[ATTACH]".len(), details_color_ranges: full_details_ranges, abbr_details_color_ranges: abbr_details_ranges, } }, UnifiedSearchResult::ResurrectableSession { indices, session_name, ctime, .. } => { let duration = humantime::format_duration(*ctime).to_string(); let mut formatted_duration = String::new(); for part in duration.split_whitespace() { if !part.ends_with('s') { if !formatted_duration.is_empty() { formatted_duration.push(' '); } formatted_duration.push_str(part); } } if formatted_duration.is_empty() { formatted_duration.push_str("<1m"); } let full_details = format!("Created {} ago", formatted_duration); let full_details_ranges = { let created_len = "Created ".len(); let duration_end = created_len + formatted_duration.len(); DetailsColorRanges { ranges: vec![(2, created_len..duration_end)], } }; let abbr_details = format!("{} ago", formatted_duration); let abbr_details_ranges = { let duration_end = formatted_duration.len(); DetailsColorRanges { ranges: vec![(2, 0..duration_end)], } }; let name_width = session_name.width(); let full_details_width = full_details.width(); let abbr_details_width = abbr_details.width(); CachedRowData { session_name: session_name.clone(), indices: indices.clone(), original_index: orig_i, kind: CachedRowKind::Resurrectable, full_details, abbr_details, full_tag: "[RESURRECT]", abbr_tag: "[R]", name_width, full_details_width, abbr_details_width, full_tag_width: "[RESURRECT]".len(), details_color_ranges: full_details_ranges, abbr_details_color_ranges: abbr_details_ranges, } }, }; // Update max widths if row.name_width > self.full_name_width { self.full_name_width = row.name_width; } if row.full_details_width > self.full_details_width { self.full_details_width = row.full_details_width; } if row.abbr_details_width > self.abbr_details_width { self.abbr_details_width = row.abbr_details_width; } if row.full_tag_width > self.full_tag_width { self.full_tag_width = row.full_tag_width; } self.rows.push(row); } } } #[derive(Debug)] pub struct ListItem { pub name: String, pub session_name: Option>, pub tab_name: Option>, pub pane_name: Option>, colors: Colors, } impl ListItem { pub fn from_session_info(session_ui_info: &SessionUiInfo, colors: Colors) -> Self { let session_ui_line = build_session_ui_line(session_ui_info, colors); ListItem { name: session_ui_info.name.clone(), session_name: Some(session_ui_line), tab_name: None, pane_name: None, colors, } } pub fn from_tab_info( session_ui_info: &SessionUiInfo, tab_ui_info: &TabUiInfo, colors: Colors, ) -> Self { let session_ui_line = build_session_ui_line(session_ui_info, colors); let tab_ui_line = build_tab_ui_line(tab_ui_info, colors); ListItem { name: tab_ui_info.name.clone(), session_name: Some(session_ui_line), tab_name: Some(tab_ui_line), pane_name: None, colors, } } pub fn from_pane_info( session_ui_info: &SessionUiInfo, tab_ui_info: &TabUiInfo, pane_ui_info: &PaneUiInfo, colors: Colors, ) -> Self { let session_ui_line = build_session_ui_line(session_ui_info, colors); let tab_ui_line = build_tab_ui_line(tab_ui_info, colors); let pane_ui_line = build_pane_ui_line(pane_ui_info, colors); ListItem { name: pane_ui_info.name.clone(), session_name: Some(session_ui_line), tab_name: Some(tab_ui_line), pane_name: Some(pane_ui_line), colors, } } pub fn line_count(&self) -> usize { let mut line_count = 0; if self.session_name.is_some() { line_count += 1 }; if self.tab_name.is_some() { line_count += 1 }; if self.pane_name.is_some() { line_count += 1 }; line_count } pub fn render(&self, indices: Option>, max_cols: usize) -> Vec { let mut lines_to_render = vec![]; if let Some(session_name) = &self.session_name { let indices = if self.tab_name.is_none() && self.pane_name.is_none() { indices.clone() } else { None }; let mut line_to_render = LineToRender::new(self.colors); let mut remaining_cols = max_cols; for span in session_name { span.render( indices.clone().map(|i| { ( SpanStyle::ForegroundBold( self.colors.palette.text_unselected.emphasis_3, ), i, ) }), &mut line_to_render, &mut remaining_cols, ); } lines_to_render.push(line_to_render); } if let Some(tab_name) = &self.tab_name { let indices = if self.pane_name.is_none() { indices.clone() } else { None }; let mut line_to_render = LineToRender::new(self.colors); let mut remaining_cols = max_cols; for span in tab_name { span.render( indices.clone().map(|i| { ( SpanStyle::ForegroundBold( self.colors.palette.text_unselected.emphasis_3, ), i, ) }), &mut line_to_render, &mut remaining_cols, ); } lines_to_render.push(line_to_render); } if let Some(pane_name) = &self.pane_name { let mut line_to_render = LineToRender::new(self.colors); let mut remaining_cols = max_cols; for span in pane_name { span.render( indices.clone().map(|i| { ( SpanStyle::ForegroundBold( self.colors.palette.text_unselected.emphasis_3, ), i, ) }), &mut line_to_render, &mut remaining_cols, ); } lines_to_render.push(line_to_render); } lines_to_render } } #[derive(Debug)] pub enum UiSpan { UiSpanTelescope(UiSpanTelescope), TruncatableUiSpan(TruncatableUiSpan), } impl UiSpan { pub fn render( &self, indices: Option<(SpanStyle, Vec)>, line_to_render: &mut LineToRender, remaining_cols: &mut usize, ) { match self { UiSpan::UiSpanTelescope(ui_span_telescope) => { ui_span_telescope.render(line_to_render, remaining_cols) }, UiSpan::TruncatableUiSpan(truncatable_ui_span) => { truncatable_ui_span.render(indices, line_to_render, remaining_cols) }, } } } #[allow(dead_code)] // in the future this will be moved to be its own component #[derive(Debug)] pub enum SpanStyle { None, Bold, Foreground(PaletteColor), ForegroundBold(PaletteColor), } impl SpanStyle { pub fn style_string(&self, to_style: &str) -> String { match self { SpanStyle::None => to_style.to_owned(), SpanStyle::Bold => format!("\u{1b}[1m{}\u{1b}[22m", to_style), SpanStyle::Foreground(color) => match color { PaletteColor::EightBit(byte) => { format!("\u{1b}[38;5;{byte}m{}\u{1b}[39m", to_style) }, PaletteColor::Rgb((r, g, b)) => { format!("\u{1b}[38;2;{};{};{}m{}\u{1b}[39m", r, g, b, to_style) }, }, SpanStyle::ForegroundBold(color) => match color { PaletteColor::EightBit(byte) => { format!("\u{1b}[38;5;{byte};1m{}\u{1b}[39;22m", to_style) }, PaletteColor::Rgb((r, g, b)) => { format!("\u{1b}[38;2;{};{};{};1m{}\u{1b}[39;22m", r, g, b, to_style) }, }, } } } impl Default for SpanStyle { fn default() -> Self { SpanStyle::None } } fn truncate_to_width(text: &str, max_width: usize) -> String { use unicode_width::UnicodeWidthChar; let mut result = String::new(); let mut current_width = 0; for ch in text.chars() { let ch_width = ch.width().unwrap_or(0); if current_width + ch_width > max_width { break; } result.push(ch); current_width += ch_width; } result } #[derive(Debug, Default)] pub struct TruncatableUiSpan { text: String, style: SpanStyle, } impl TruncatableUiSpan { pub fn new(text: String, style: SpanStyle) -> Self { TruncatableUiSpan { text, style } } pub fn render( &self, indices: Option<(SpanStyle, Vec)>, line_to_render: &mut LineToRender, remaining_cols: &mut usize, ) { let mut rendered = String::new(); let truncated = if *remaining_cols >= self.text.width() { self.text.clone() } else { let mut truncated = String::new(); for character in self.text.chars() { if truncated.width() + character.width().unwrap_or(0) <= *remaining_cols { truncated.push(character); } else { break; } } truncated }; match indices { Some((index_style, indices)) => { for (i, character) in truncated.chars().enumerate() { // TODO: optimize this by splitting the string up by its indices and only pushing those // chu8nks if indices.contains(&i) { rendered.push_str(&index_style.style_string(&character.to_string())); } else { rendered.push_str(&self.style.style_string(&character.to_string())); } } }, None => { rendered.push_str(&self.style.style_string(&truncated)); }, } *remaining_cols = remaining_cols.saturating_sub(truncated.width()); line_to_render.append(&rendered); } } #[derive(Debug, Default)] pub struct UiSpanTelescope(Vec); impl UiSpanTelescope { pub fn new(string_and_lengths: Vec) -> Self { UiSpanTelescope(string_and_lengths) } pub fn render(&self, line_to_render: &mut LineToRender, remaining_cols: &mut usize) { for string_and_length in &self.0 { if string_and_length.length < *remaining_cols { line_to_render.append(&string_and_length.string); *remaining_cols -= string_and_length.length; break; } } } } #[derive(Debug, Default, Clone)] pub struct StringAndLength { pub string: String, pub length: usize, } impl StringAndLength { pub fn new(string: String, length: usize) -> Self { StringAndLength { string, length } } } #[derive(Debug, Clone)] pub struct LineToRender { line: String, is_selected: bool, truncated_result_count: usize, colors: Colors, } impl LineToRender { pub fn new(colors: Colors) -> Self { LineToRender { line: String::default(), is_selected: false, truncated_result_count: 0, colors, } } pub fn append(&mut self, to_append: &str) { self.line.push_str(to_append) } pub fn make_selected_as_search(&mut self, add_arrows: bool) { self.is_selected = true; let arrows = if add_arrows { self.colors.shortcuts(" <↓↑> ") } else { " ".to_owned() }; match self.colors.palette.list_selected.background { PaletteColor::EightBit(byte) => { self.line = format!( "\u{1b}[48;5;{byte}m\u{1b}[K\u{1b}[48;5;{byte}m{arrows}{}", self.line ); }, PaletteColor::Rgb((r, g, b)) => { self.line = format!( "\u{1b}[48;2;{};{};{}m\u{1b}[K\u{1b}[48;2;{};{};{}m{arrows}{}", r, g, b, r, g, b, self.line ); }, } } pub fn make_selected(&mut self, add_arrows: bool) { self.is_selected = true; let arrows = if add_arrows { self.colors.shortcuts("<←↓↑→>") } else { " ".to_owned() }; match self.colors.palette.list_selected.background { PaletteColor::EightBit(byte) => { self.line = format!( "\u{1b}[48;5;{byte}m\u{1b}[K\u{1b}[48;5;{byte}m{arrows}{}", self.line ); }, PaletteColor::Rgb((r, g, b)) => { self.line = format!( "\u{1b}[48;2;{};{};{}m\u{1b}[K\u{1b}[48;2;{};{};{}m{arrows}{}", r, g, b, r, g, b, self.line ); }, } } pub fn render(&self) -> String { let mut line = self.line.clone(); let more = if self.truncated_result_count > 0 { self.colors .exit_code_error(&format!(" [+{}]", self.truncated_result_count)) } else { String::new() }; line.push_str(&more); if self.is_selected { self.line.clone() } else { format!("\u{1b}[49m {}", line) } } pub fn add_truncated_results(&mut self, result_count: usize) { self.truncated_result_count += result_count; } } pub fn build_session_ui_line(session_ui_info: &SessionUiInfo, colors: Colors) -> Vec { let mut ui_spans = vec![]; let tab_count_text = session_ui_info.tabs.len(); let total_pane_count_text = session_ui_info .tabs .iter() .fold(0, |acc, tab| acc + tab.panes.len()); let tab_count = format!("{}", tab_count_text); let tab_count_styled = colors.tab_count(&tab_count); let total_pane_count = format!("{}", total_pane_count_text); let total_pane_count_styled = colors.pane_count(&total_pane_count); let session_name = &session_ui_info.name; let connected_users = format!("{}", session_ui_info.connected_users); let connected_users_styled = colors.connected_users(&connected_users); let session_bullet_span = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![StringAndLength::new( format!(" > "), 3, )])); let session_name_span = UiSpan::TruncatableUiSpan(TruncatableUiSpan::new( session_name.clone(), SpanStyle::ForegroundBold(colors.palette.text_unselected.emphasis_0), )); let tab_and_pane_count = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![ StringAndLength::new( format!(" ({tab_count_styled} tabs, {total_pane_count_styled} panes)"), 2 + tab_count.width() + 7 + total_pane_count.width() + 7, ), StringAndLength::new( format!(" ({tab_count_styled}, {total_pane_count_styled})"), 2 + tab_count.width() + 2 + total_pane_count.width() + 3, ), ])); let connected_users_count = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![ StringAndLength::new( format!(" [{connected_users_styled} connected users]"), 2 + connected_users.width() + 17, ), StringAndLength::new( format!(" [{connected_users_styled}]"), 2 + connected_users.width() + 1, ), ])); ui_spans.push(session_bullet_span); ui_spans.push(session_name_span); ui_spans.push(tab_and_pane_count); ui_spans.push(connected_users_count); if session_ui_info.is_current_session { let current_session_indication = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![ StringAndLength::new( colors.current_session_marker(&format!(" ")), 18, ), StringAndLength::new(colors.current_session_marker(&format!(" ")), 10), StringAndLength::new(colors.current_session_marker(&format!(" ")), 4), ])); ui_spans.push(current_session_indication); } ui_spans } pub fn build_tab_ui_line(tab_ui_info: &TabUiInfo, colors: Colors) -> Vec { let mut ui_spans = vec![]; let tab_name = &tab_ui_info.name; let pane_count_text = tab_ui_info.panes.len(); let pane_count = format!("{}", pane_count_text); let pane_count_styled = colors.pane_count(&pane_count); let tab_bullet_span = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![StringAndLength::new( format!(" - "), 4, )])); let tab_name_span = UiSpan::TruncatableUiSpan(TruncatableUiSpan::new( tab_name.clone(), SpanStyle::ForegroundBold(colors.palette.text_unselected.emphasis_1), )); let connected_users_count_span = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![ StringAndLength::new( format!(" ({pane_count_styled} panes)"), 2 + pane_count.width() + 7, ), StringAndLength::new( format!(" ({pane_count_styled})"), 2 + pane_count.width() + 1, ), ])); ui_spans.push(tab_bullet_span); ui_spans.push(tab_name_span); ui_spans.push(connected_users_count_span); ui_spans } pub fn build_pane_ui_line(pane_ui_info: &PaneUiInfo, colors: Colors) -> Vec { let mut ui_spans = vec![]; let pane_name = pane_ui_info.name.clone(); let exit_code = pane_ui_info.exit_code.map(|exit_code_number| { let exit_code = format!("{}", exit_code_number); let exit_code = if exit_code_number == 0 { colors.session_and_folder_entry(&exit_code) } else { colors.exit_code_error(&exit_code) }; exit_code }); let pane_bullet_span = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![StringAndLength::new( format!(" > "), 6, )])); ui_spans.push(pane_bullet_span); let pane_name_span = UiSpan::TruncatableUiSpan(TruncatableUiSpan::new(pane_name, SpanStyle::Bold)); ui_spans.push(pane_name_span); if let Some(exit_code) = exit_code { let pane_name_span = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![ StringAndLength::new( format!(" (EXIT CODE: {exit_code})"), 13 + exit_code.width() + 1, ), StringAndLength::new(format!(" ({exit_code})"), 2 + exit_code.width() + 1), ])); ui_spans.push(pane_name_span); } ui_spans } pub fn minimize_lines( total_count: usize, line_count_to_remove: usize, selected_index: Option, ) -> (usize, usize, usize, usize) { // returns: (start_index, anchor_index, end_index, lines_left_to_remove) let (count_to_render, line_count_to_remove) = if line_count_to_remove > total_count { (1, line_count_to_remove.saturating_sub(total_count) + 1) } else { (total_count.saturating_sub(line_count_to_remove), 0) }; let anchor_index = selected_index.unwrap_or(0); // 5 let mut start_index = anchor_index.saturating_sub(count_to_render / 2); let mut end_index = start_index + count_to_render; if end_index > total_count { start_index = start_index.saturating_sub(end_index - total_count); end_index = total_count; } (start_index, anchor_index, end_index, line_count_to_remove) } pub fn render_prompt(search_term: &str, colors: Colors, x: usize, y: usize) { let prompt = colors.session_and_folder_entry(&format!("Search:")); let search_term = colors.bold(&format!("{}_", search_term)); println!( "\u{1b}[{};{}H\u{1b}[0m{} {}\n", y + 1, x, prompt, search_term ); } pub fn render_single_screen_prompt( search_term: &str, enter_action: Option<&str>, colors: Colors, x: usize, y: usize, ) { let prompt = colors.session_name_prompt("Session:"); let search_term_display = colors.bold(&format!("{}_", search_term)); let enter_hint = match enter_action { Some(action) => { let enter_key = colors.shortcuts(""); format!(" {} - {}", enter_key, action) }, None => String::new(), }; println!( "\u{1b}[m\u{1b}[{};{}H\u{1b}[0m{} {}{}\n", y + 2, x + 1, prompt, search_term_display, enter_hint, ); } pub fn render_unified_results( cache: &UnifiedResultsRenderCache, selected_index: Option, max_rows: usize, max_cols: usize, _colors: Colors, x: usize, y: usize, ) { if cache.rows.is_empty() { return; } // Map selected_index (from original results) to filtered/cached position let filtered_selected = selected_index.and_then(|sel| cache.rows.iter().position(|r| r.original_index == sel)); // Calculate viewport range over the cached (already filtered) list let total = cache.rows.len(); let data_rows = max_rows.saturating_sub(1); // 1 for the empty header let (start, end) = if data_rows >= total { (0, total) } else { let anchor = filtered_selected.unwrap_or(0); let half = data_rows / 2; let mut s = anchor.saturating_sub(half); let mut e = s + data_rows; if e > total { e = total; s = total.saturating_sub(data_rows); } (s, e) }; // Hidden-item counts (single pass over two slices) let (above_active, above_resurrectable) = count_by_kind(&cache.rows[..start]); let (below_active, below_resurrectable) = count_by_kind(&cache.rows[end..]); let has_hidden_above = above_active > 0 || above_resurrectable > 0; let has_hidden_below = below_active > 0 || below_resurrectable > 0; let has_hidden = has_hidden_above || has_hidden_below; // 4th column content strings let tab_header_full = " Complete"; let tab_header_short = ""; let above_summary_full = if has_hidden_above { format!( "[+{} Active] [+{} Exited]", above_active, above_resurrectable ) } else { String::new() }; let above_summary_short = if has_hidden_above { format!("[+{}] [+{}]", above_active, above_resurrectable) } else { String::new() }; let below_summary_full = if has_hidden_below { format!( "[+{} Active] [+{} Exited]", below_active, below_resurrectable ) } else { String::new() }; let below_summary_short = if has_hidden_below { format!("[+{}] [+{}]", below_active, below_resurrectable) } else { String::new() }; let max_summary_full_width = std::cmp::max( if has_hidden_above { above_summary_full.width() } else { 0 }, if has_hidden_below { below_summary_full.width() } else { 0 }, ); let max_summary_short_width = std::cmp::max( if has_hidden_above { above_summary_short.width() } else { 0 }, if has_hidden_below { below_summary_short.width() } else { 0 }, ); let full_fourth_col_width = std::cmp::max( tab_header_full.width(), if has_hidden { max_summary_full_width } else { 1 }, ); let short_fourth_col_width = std::cmp::max( tab_header_short.width(), if has_hidden { max_summary_short_width } else { 1 }, ); // Use pre-computed widths from cache — no format!() allocations needed let (abbreviate_details, abbreviate_tags, abbreviate_fourth_col, name_max_width) = compute_reduction_tier( cache.full_name_width, cache.full_details_width, cache.full_tag_width, full_fourth_col_width, cache.abbr_details_width, short_fourth_col_width, max_cols, ); // Build table from cached data let mut table = Table::new(); // Empty header row table = table.add_styled_row(vec![ Text::new(" "), Text::new(" "), Text::new(" "), Text::new(" "), ]); let visible_count = end - start; for (row_index, row) in cache.rows[start..end].iter().enumerate() { let is_selected = filtered_selected == Some(start + row_index); // Name cell — use cached string, only truncate if needed let display_name = match name_max_width { Some(max_w) => truncate_to_width(&row.session_name, max_w), None => row.session_name.clone(), }; let display_indices: Vec = row .indices .iter() .filter(|&&i| i < display_name.chars().count()) .cloned() .collect(); let mut name_cell = Text::new(display_name).color_range(1, ..); if !display_indices.is_empty() { name_cell = name_cell.color_indices(3, display_indices); } // Details and tag cells — use cached pre-formatted strings let color_ranges = if abbreviate_details { &row.abbr_details_color_ranges } else { &row.details_color_ranges }; let details_text = if abbreviate_details { &row.abbr_details } else { &row.full_details }; let mut details_cell = Text::new(details_text); for (color_idx, range) in &color_ranges.ranges { details_cell = details_cell.color_range(*color_idx, range.clone()); } let tag_text = if abbreviate_tags { row.abbr_tag } else { row.full_tag }; let tag_cell = Text::new(tag_text).color_range(0, ..); // 4th column let fourth_cell = if row_index == 0 && has_hidden_above { let (summary_text, active_count, resurrectable_count) = if abbreviate_fourth_col { (&above_summary_short, above_active, above_resurrectable) } else { (&above_summary_full, above_active, above_resurrectable) }; Text::new(summary_text) .color_substring(2, &format!("+{}", active_count)) .color_substring(2, &format!("+{}", resurrectable_count)) } else if row_index == 0 && selected_index.is_none() { let tab_hint_text = if abbreviate_fourth_col { tab_header_short } else { tab_header_full }; Text::new(tab_hint_text).color_substring(3, "") } else if row_index == visible_count - 1 && has_hidden_below { let (summary_text, active_count, resurrectable_count) = if abbreviate_fourth_col { (&below_summary_short, below_active, below_resurrectable) } else { (&below_summary_full, below_active, below_resurrectable) }; Text::new(summary_text) .color_substring(2, &format!("+{}", active_count)) .color_substring(2, &format!("+{}", resurrectable_count)) } else { Text::new(" ") }; if is_selected { table = table.add_styled_row(vec![ name_cell.selected(), details_cell.selected(), tag_cell.selected(), fourth_cell, ]); } else { table = table.add_styled_row(vec![name_cell, details_cell, tag_cell, fourth_cell]); } } print_table_with_coordinates(table, x, y, Some(max_cols), Some(max_rows)); } fn count_by_kind(rows: &[CachedRowData]) -> (usize, usize) { let mut active = 0; let mut resurrectable = 0; for row in rows { match row.kind { CachedRowKind::Active => active += 1, CachedRowKind::Resurrectable => resurrectable += 1, } } (active, resurrectable) } pub fn render_screen_toggle( active_screen: ActiveScreen, x: usize, y: usize, max_cols: usize, background: &PaletteColor, ) { let key_indication_text = ""; let (new_session_text, running_sessions_text, exited_sessions_text) = if max_cols > 66 { ("New Session", "Attach to Session", "Resurrect Session") } else { ("New", "Attach", "Resurrect") }; let key_indication_len = key_indication_text.chars().count() + 1; let first_ribbon_length = new_session_text.chars().count() + 4; let second_ribbon_length = running_sessions_text.chars().count() + 4; let key_indication_x = x; let first_ribbon_x = key_indication_x + key_indication_len; let second_ribbon_x = first_ribbon_x + first_ribbon_length; let third_ribbon_x = second_ribbon_x + second_ribbon_length; let mut new_session_text = Text::new(new_session_text); let mut running_sessions_text = Text::new(running_sessions_text); let mut exited_sessions_text = Text::new(exited_sessions_text); match active_screen { ActiveScreen::NewSession => { new_session_text = new_session_text.selected(); }, ActiveScreen::AttachToSession => { running_sessions_text = running_sessions_text.selected(); }, ActiveScreen::ResurrectSession => { exited_sessions_text = exited_sessions_text.selected(); }, ActiveScreen::SingleScreen => { // SingleScreen does not use the tab toggle; this arm exists for exhaustiveness return; }, } let bg_color = match background { PaletteColor::Rgb((r, g, b)) => format!("\u{1b}[48;2;{};{};{}m\u{1b}[0K", r, g, b), PaletteColor::EightBit(color) => format!("\u{1b}[48;5;{}m\u{1b}[0K", color), }; print_text_with_coordinates( Text::new(key_indication_text).color_range(3, ..).opaque(), key_indication_x, y, None, None, ); println!("\u{1b}[{};{}H{}", y + 1, first_ribbon_x, bg_color); print_ribbon_with_coordinates(new_session_text, first_ribbon_x, y, None, None); print_ribbon_with_coordinates(running_sessions_text, second_ribbon_x, y, None, None); print_ribbon_with_coordinates(exited_sessions_text, third_ribbon_x, y, None, None); } fn render_new_session_folder_prompt( new_session_info: &NewSessionInfo, colors: Colors, x: usize, y: usize, max_cols: usize, ) { match new_session_info.new_session_folder.as_ref() { Some(new_session_folder) => { let folder_prompt = "New session folder:"; let short_folder_prompt = "Folder:"; let new_session_path = new_session_folder.clone(); let new_session_folder = new_session_folder.display().to_string(); let change_folder_shortcut_text = ""; let change_folder_shortcut = colors.shortcuts(&change_folder_shortcut_text); let to_change = "to change"; let reset_folder_shortcut_text = ""; let reset_folder_shortcut = colors.shortcuts(reset_folder_shortcut_text); let to_reset = "to reset"; if max_cols >= folder_prompt.width() + new_session_folder.width() + change_folder_shortcut_text.width() + to_change.width() + reset_folder_shortcut_text.width() + to_reset.width() + 8 { print!( "\u{1b}[m{}{} {} ({} {}, {} {})", format!("\u{1b}[{};{}H", y + 1, x + 1), colors.session_name_prompt(folder_prompt), colors.session_and_folder_entry(&new_session_folder), change_folder_shortcut, to_change, reset_folder_shortcut, to_reset, ); } else if max_cols >= short_folder_prompt.width() + new_session_folder.width() + change_folder_shortcut_text.width() + to_change.width() + reset_folder_shortcut_text.width() + to_reset.width() + 8 { print!( "\u{1b}[m{}{} {} ({} {}, {} {})", format!("\u{1b}[{};{}H", y + 1, x + 1), colors.session_name_prompt(short_folder_prompt), colors.session_and_folder_entry(&new_session_folder), change_folder_shortcut, to_change, reset_folder_shortcut, to_reset, ); } else if max_cols >= short_folder_prompt.width() + new_session_folder.width() + change_folder_shortcut_text.width() + reset_folder_shortcut_text.width() + 5 { print!( "\u{1b}[m{}{} {} ({}/{})", format!("\u{1b}[{};{}H", y + 1, x + 1), colors.session_name_prompt(short_folder_prompt), colors.session_and_folder_entry(&new_session_folder), change_folder_shortcut, reset_folder_shortcut, ); } else { let total_len = short_folder_prompt.width() + change_folder_shortcut_text.width() + reset_folder_shortcut_text.width() + 5; let max_path_len = max_cols.saturating_sub(total_len); let truncated_path = truncate_path( new_session_path, new_session_folder.width().saturating_sub(max_path_len), ); print!( "\u{1b}[m{}{} {} ({}/{})", format!("\u{1b}[{};{}H", y + 1, x + 1), colors.session_name_prompt(short_folder_prompt), colors.session_and_folder_entry(&truncated_path), change_folder_shortcut, reset_folder_shortcut, ); } }, None => { let folder_prompt = "New session folder:"; let short_folder_prompt = "Folder:"; let change_folder_shortcut_text = ""; let change_folder_shortcut = colors.shortcuts(change_folder_shortcut_text); let to_set = "to set"; if max_cols >= folder_prompt.width() + change_folder_shortcut_text.width() + to_set.width() + 4 { print!( "\u{1b}[m{}{} ({} {})", format!("\u{1b}[{};{}H", y + 1, x + 1), colors.session_name_prompt(folder_prompt), change_folder_shortcut, to_set, ); } else if max_cols >= short_folder_prompt.width() + change_folder_shortcut_text.width() + to_set.width() + 4 { print!( "\u{1b}[m{}{} ({} {})", format!("\u{1b}[{};{}H", y + 1, x + 1), colors.session_name_prompt(short_folder_prompt), change_folder_shortcut, to_set, ); } else { print!( "\u{1b}[m{}{} {}", format!("\u{1b}[{};{}H", y + 1, x + 1), colors.session_name_prompt(short_folder_prompt), change_folder_shortcut, ); } }, } } pub fn render_new_session_block( new_session_info: &NewSessionInfo, colors: Colors, max_rows_of_new_session_block: usize, max_cols_of_new_session_block: usize, x: usize, y: usize, ) { let enter = colors.shortcuts(""); if new_session_info.entering_new_session_name() { let prompt = "New session name:"; let long_instruction = "when done, blank for random"; let new_session_name = new_session_info.name(); if max_cols_of_new_session_block > prompt.width() + long_instruction.width() + new_session_name.width() + 15 { println!( "\u{1b}[m{}{} {}_ ({} {})", format!("\u{1b}[{};{}H", y + 1, x + 1), colors.session_name_prompt(prompt), colors.session_and_folder_entry(&new_session_name), enter, long_instruction, ); } else { let space_for_new_session_name = max_cols_of_new_session_block.saturating_sub(prompt.width() + 18); let new_session_name = if new_session_name.width() > space_for_new_session_name { let mut truncated = String::new(); for character in new_session_name.chars().rev() { if truncated.width() + character.width().unwrap_or(0) < space_for_new_session_name { truncated.push(character); } else { break; } } format!("...{}", truncated.chars().rev().collect::()) } else { new_session_name.to_owned() }; println!( "\u{1b}[m{}{} {}_ {}", format!("\u{1b}[{};{}H", y + 1, x + 1), colors.session_name_prompt(prompt), colors.session_and_folder_entry(&new_session_name), enter, ); } } else if new_session_info.entering_layout_search_term() { let new_session_name = if new_session_info.name().is_empty() { "" } else { new_session_info.name() }; let prompt = "New session name:"; let long_instruction = "to correct"; let esc = colors.shortcuts(""); if max_cols_of_new_session_block > prompt.width() + long_instruction.width() + new_session_name.width() + 15 { println!( "\u{1b}[m{}{}: {} ({} to correct)", format!("\u{1b}[{};{}H", y + 1, x + 1), colors.session_name_prompt(prompt), colors.session_and_folder_entry(new_session_name), esc, ); } else { println!( "\u{1b}[m{}{}: {} {}", format!("\u{1b}[{};{}H", y + 1, x + 1), colors.session_name_prompt("New session name"), colors.session_and_folder_entry(new_session_name), esc, ); } render_layout_selection_list( new_session_info, max_rows_of_new_session_block.saturating_sub(8), max_cols_of_new_session_block, x, y + 1, ); } render_new_session_folder_prompt( new_session_info, colors, x, (y + max_rows_of_new_session_block).saturating_sub(3), max_cols_of_new_session_block, ); } pub fn render_layout_selection_list( new_session_info: &NewSessionInfo, max_rows_of_new_session_block: usize, max_cols_of_new_session_block: usize, x: usize, y: usize, ) { let layout_search_term = new_session_info.layout_search_term(); let search_term_len = layout_search_term.width(); let layout_indication_line = if max_cols_of_new_session_block > 73 + search_term_len { Text::new(format!( "New session layout: {}_ (Search and select from list, when done)", layout_search_term )) .color_range(2, ..20 + search_term_len) .color_range(3, 20..20 + search_term_len) .color_range(3, 52 + search_term_len..59 + search_term_len) } else { Text::new(format!( "New session layout: {}_ ", layout_search_term )) .color_range(2, ..20 + search_term_len) .color_range(3, 20..20 + search_term_len) .color_range(3, 22 + search_term_len..) }; print_text_with_coordinates(layout_indication_line, x, y + 1, None, None); println!(); let mut table = Table::new(); for (i, (layout_info, indices, is_selected)) in new_session_info .layouts_to_render(max_rows_of_new_session_block) .into_iter() .enumerate() { let layout_name = layout_info.name(); let layout_name_len = layout_name.width(); let is_builtin = layout_info.is_builtin(); if i > max_rows_of_new_session_block.saturating_sub(1) { break; } else { let mut layout_cell = if is_builtin { Text::new(format!("{} (built-in)", layout_name)) .color_range(1, 0..layout_name_len) .color_range(0, layout_name_len + 1..) .color_indices(3, indices) } else { Text::new(format!("{}", layout_name)) .color_range(1, ..) .color_indices(3, indices) }; if is_selected { layout_cell = layout_cell.selected(); } let arrow_cell = if is_selected { Text::new(format!("<↓↑>")).selected().color_range(3, ..) } else { Text::new(format!(" ")).color_range(3, ..) }; table = table.add_styled_row(vec![arrow_cell, layout_cell]); } } let table_y = y + 3; print_table_with_coordinates(table, x, table_y, None, None); } pub fn render_error(error_text: &str, rows: usize, columns: usize, x: usize, y: usize) { print_text_with_coordinates( Text::new(format!("Error: {}", error_text)).color_range(3, ..), x, y + rows, Some(columns), None, ); } pub fn render_renaming_session_screen( new_session_name: &str, rows: usize, columns: usize, x: usize, y: usize, ) { if rows == 0 || columns == 0 { return; } let text = Text::new(format!( "New name for current session: {}_ ( when done)", new_session_name )) .color_range(2, ..29) .color_range( 3, 33 + new_session_name.width()..40 + new_session_name.width(), ); print_text_with_coordinates(text, x, y, None, None); if new_session_name.contains('/') { let error_text = "Error: session name cannot contain '/'"; print_text_with_coordinates( Text::new(error_text).color_range(3, ..), x, y + 2, None, None, ); } } pub fn render_controls_line( active_screen: ActiveScreen, max_cols: usize, colors: Colors, x: usize, y: usize, ) -> usize { const HELP_PREFIX_LEN: usize = 6; let x = x + 1; let shows_help_prefix = match active_screen { ActiveScreen::NewSession => { if max_cols >= 50 { print!( "\u{1b}[m\u{1b}[{y};{x}H\u{1b}[1mHelp: Fill in the form to start a new session." ); true } else { false } }, ActiveScreen::AttachToSession => { let rename = colors.shortcuts(""); let rename_text = colors.bold("Rename"); let disconnect = colors.shortcuts(""); let disconnect_text = colors.bold("Disconnect others"); let kill = colors.shortcuts(""); let kill_text = colors.bold("Kill"); let kill_all = colors.shortcuts(""); let kill_all_text = colors.bold("Kill all"); if max_cols > 90 { print!( "\u{1b}[m\u{1b}[{y};{x}HHelp: {rename} - {rename_text}, {disconnect} - {disconnect_text}, {kill} - {kill_text}, {kill_all} - {kill_all_text}" ); true } else if max_cols >= 28 { print!("\u{1b}[m\u{1b}[{y};{x}H{rename}/{disconnect}/{kill}/{kill_all}"); false } else { false } }, ActiveScreen::ResurrectSession => { let arrows = colors.shortcuts("<↓↑>"); let navigate = colors.bold("Navigate"); let enter = colors.shortcuts(""); let select = colors.bold("Resurrect"); let del = colors.shortcuts(""); let del_text = colors.bold("Delete"); let del_all = colors.shortcuts(""); let del_all_text = colors.bold("Delete all"); if max_cols > 83 { print!( "\u{1b}[m\u{1b}[{y};{x}HHelp: {arrows} - {navigate}, {enter} - {select}, {del} - {del_text}, {del_all} - {del_all_text}" ); true } else if max_cols >= 28 { print!("\u{1b}[m\u{1b}[{y};{x}H{arrows}/{enter}/{del}/{del_all}"); false } else { false } }, ActiveScreen::SingleScreen => { let rename = colors.shortcuts(""); let rename_text = colors.bold("Rename"); let disconnect = colors.shortcuts(""); let disconnect_full_text = colors.bold("Disconnect others"); let disconnect_short_text = colors.bold("Disconnect"); let kill = colors.shortcuts(""); let kill_text = colors.bold("Kill/Delete"); // Full: "Help: - Rename, - Disconnect others, - Kill/Delete" = 76 chars if max_cols > 76 { print!( "\u{1b}[m\u{1b}[{y};{x}HHelp: {rename} - {rename_text}, {disconnect} - {disconnect_full_text}, {kill} - {kill_text}" ); true // Medium: "Help: - Rename, - Disconnect, - Kill/Delete" = 69 chars } else if max_cols > 69 { print!( "\u{1b}[m\u{1b}[{y};{x}HHelp: {rename} - {rename_text}, {disconnect} - {disconnect_short_text}, {kill} - {kill_text}" ); true // Compact: "//" = 23 chars } else if max_cols >= 23 { print!("\u{1b}[m\u{1b}[{y};{x}H{rename}/{disconnect}/{kill}"); false } else { false } }, }; if shows_help_prefix { HELP_PREFIX_LEN } else { 0 } } fn format_elapsed_time(elapsed_millis: u64) -> String { // Convert elapsed milliseconds to Duration let elapsed_duration = Duration::from_millis(elapsed_millis); // Use humantime to format the duration, same as in resurrectable_sessions.rs let duration_str = format_duration(elapsed_duration).to_string(); let duration_parts = duration_str.split_whitespace(); let mut formatted_duration = String::new(); for part in duration_parts { if !part.ends_with('s') { if !formatted_duration.is_empty() { formatted_duration.push(' '); } formatted_duration.push_str(part); } } if formatted_duration.is_empty() { format!("just now") } else { format!("{} ago", formatted_duration) } } pub fn render_unsaved_changes_line( max_cols: usize, x: usize, y: usize, last_saved_timestamp: Option, ) { // Declare all text components let shortcut_text = ""; let full_action_text = "Save current session for resurrection"; let medium_action_text = "Save session"; let short_action_text = "Save"; let separator = " - "; let space = " "; let time_text = match last_saved_timestamp { Some(timestamp) => { let elapsed = format_elapsed_time(timestamp); format!("(saved {})", elapsed) }, None => "(not saved)".to_string(), }; // Calculate component widths let shortcut_width = shortcut_text.width(); let separator_width = separator.width(); let space_width = space.width(); let time_width = time_text.width(); // Calculate total widths for each display mode // Format: "{shortcut}{separator}{action}{space}{time}" let full_width = shortcut_width + separator_width + full_action_text.width() + space_width + time_width; let medium_width = shortcut_width + separator_width + medium_action_text.width() + space_width + time_width; let short_width = shortcut_width + separator_width + short_action_text.width() + space_width + time_width; let minimal_width = shortcut_width + space_width + time_width; // Select appropriate message based on available width let msg = if max_cols >= full_width { format!( "{}{}{}{}{}", shortcut_text, separator, full_action_text, space, time_text ) } else if max_cols >= medium_width { format!( "{}{}{}{}{}", shortcut_text, separator, medium_action_text, space, time_text ) } else if max_cols >= short_width { format!( "{}{}{}{}{}", shortcut_text, separator, short_action_text, space, time_text ) } else if max_cols >= minimal_width { format!("{}{}{}", shortcut_text, space, time_text) } else { return; // Not enough space to render }; let text = Text::new(&msg) .color_substring(3, shortcut_text) .color_substring(2, &time_text); print_text_with_coordinates(text, x, y, None, None); } // Maps the various prompts and UI elements to the colors to present them with // // Since this plugin predates the UI components, this is a developer // convenience to keep the coloration of dialogs organized by descriptive names // // It will be obviated once everything is migrated to UI components from zellij-tile #[derive(Debug, Default, Clone, Copy)] pub struct Colors { pub palette: Styling, } impl Colors { pub fn new(palette: Styling) -> Self { Colors { palette } } pub fn bold(&self, text: &str) -> String { format!("\u{1b}[1m{}\u{1b}[22m", text) } fn color(&self, color: &PaletteColor, text: &str) -> String { match color { PaletteColor::EightBit(byte) => { format!("\u{1b}[38;5;{};1m{}\u{1b}[39;22m", byte, text) }, PaletteColor::Rgb((r, g, b)) => { format!("\u{1b}[38;2;{};{};{};1m{}\u{1b}[39;22m", r, g, b, text) }, } } pub fn session_name_prompt(&self, text: &str) -> String { self.color(&self.palette.exit_code_success.base, text) } pub fn connected_users(&self, text: &str) -> String { self.color(&self.palette.text_unselected.emphasis_2, text) } pub fn session_and_folder_entry(&self, text: &str) -> String { self.color(&self.palette.text_unselected.emphasis_0, text) } pub fn current_session_marker(&self, text: &str) -> String { self.color(&self.palette.text_unselected.emphasis_0, text) } pub fn pane_count(&self, text: &str) -> String { self.color(&self.palette.text_unselected.emphasis_2, text) } pub fn exit_code_error(&self, text: &str) -> String { self.color(&self.palette.exit_code_error.base, text) } pub fn tab_count(&self, text: &str) -> String { self.color(&self.palette.text_unselected.emphasis_1, text) } pub fn shortcuts(&self, text: &str) -> String { self.color(&self.palette.text_unselected.emphasis_3, text) } } /// Computes the width reduction tier for the unified results table. /// /// Returns `(abbreviate_details, abbreviate_tags, abbreviate_fourth_col, name_max_width)`. /// - Tier 0: everything fits at full size /// - Tier 1: abbreviate details only /// - Tier 2: abbreviate details + tags /// - Tier 3: abbreviate details + tags + 4th column /// - Tier 4: abbreviate all + truncate session names pub fn compute_reduction_tier( full_name_width: usize, full_details_width: usize, full_tag_width: usize, full_fourth_col_width: usize, abbr_details_width: usize, short_fourth_col_width: usize, max_cols: usize, ) -> (bool, bool, bool, Option) { let full_total = full_name_width + full_details_width + full_tag_width + full_fourth_col_width + 4; if full_total <= max_cols { // Everything fits at full size (false, false, false, None) } else { // Reduction 1: abbreviate details let total_after_details = full_name_width + abbr_details_width + full_tag_width + full_fourth_col_width + 4; if total_after_details <= max_cols { (true, false, false, None) } else { // Reduction 2: abbreviate tags let abbr_tag_width = 3; // "[A]" or "[R]" let total_after_tags = full_name_width + abbr_details_width + abbr_tag_width + full_fourth_col_width + 4; if total_after_tags <= max_cols { (true, true, false, None) } else { // Reduction 3: abbreviate 4th column let total_after_fourth = full_name_width + abbr_details_width + abbr_tag_width + short_fourth_col_width + 4; if total_after_fourth <= max_cols { (true, true, true, None) } else { // Reduction 4: truncate session names let available_for_name = max_cols.saturating_sub( abbr_details_width + abbr_tag_width + short_fourth_col_width + 4, ); (true, true, true, Some(available_for_name)) } } } } } fn truncate_path(path: PathBuf, mut char_count_to_remove: usize) -> String { let mut truncated = String::new(); let component_count = path.iter().count(); for (i, component) in path.iter().enumerate() { let mut component_str = component.to_string_lossy().to_string(); if char_count_to_remove > 0 { truncated.push(component_str.remove(0)); if i != 0 && i + 1 != component_count { truncated.push('/'); } char_count_to_remove = char_count_to_remove.saturating_sub(component_str.width().saturating_sub(1)); } else { truncated.push_str(&component_str); if i != 0 && i + 1 != component_count { truncated.push('/'); } } } truncated } #[cfg(test)] mod tests { use super::*; // --------------------------------------------------------------- // Section 8: Width Responsiveness (compute_reduction_tier) // --------------------------------------------------------------- #[test] fn test_8_1_tier_0_full_width_everything_fits() { // full_total = 20 + 30 + 11 + 15 + 4 = 80, max_cols = 120 let (abbr_details, abbr_tags, abbr_fourth, name_max) = compute_reduction_tier(20, 30, 11, 15, 10, 5, 120); assert_eq!( (abbr_details, abbr_tags, abbr_fourth, name_max), (false, false, false, None) ); } #[test] fn test_8_2_tier_1_abbreviate_details_only() { // full_total = 20 + 30 + 11 + 15 + 4 = 80 // after abbr details: 20 + 10 + 11 + 15 + 4 = 60 // max_cols = 70: full doesn't fit (80 > 70), but abbr details fits (60 <= 70) let (abbr_details, abbr_tags, abbr_fourth, name_max) = compute_reduction_tier(20, 30, 11, 15, 10, 5, 70); assert_eq!( (abbr_details, abbr_tags, abbr_fourth, name_max), (true, false, false, None) ); } #[test] fn test_8_3_tier_2_abbreviate_details_and_tags() { // full_total = 20 + 30 + 11 + 15 + 4 = 80 // after abbr details: 20 + 10 + 11 + 15 + 4 = 60 // after abbr tags: 20 + 10 + 3 + 15 + 4 = 52 // max_cols = 55: abbr details doesn't fit (60 > 55), abbr tags fits (52 <= 55) let (abbr_details, abbr_tags, abbr_fourth, name_max) = compute_reduction_tier(20, 30, 11, 15, 10, 5, 55); assert_eq!( (abbr_details, abbr_tags, abbr_fourth, name_max), (true, true, false, None) ); } #[test] fn test_8_4_tier_3_abbreviate_details_tags_and_fourth_col() { // full_total = 20 + 30 + 11 + 15 + 4 = 80 // after abbr details: 20 + 10 + 11 + 15 + 4 = 60 // after abbr tags: 20 + 10 + 3 + 15 + 4 = 52 // after abbr fourth: 20 + 10 + 3 + 5 + 4 = 42 // max_cols = 45: abbr tags doesn't fit (52 > 45), abbr fourth fits (42 <= 45) let (abbr_details, abbr_tags, abbr_fourth, name_max) = compute_reduction_tier(20, 30, 11, 15, 10, 5, 45); assert_eq!( (abbr_details, abbr_tags, abbr_fourth, name_max), (true, true, true, None) ); } #[test] fn test_8_5_tier_4_truncate_session_names() { // full_total = 20 + 30 + 11 + 15 + 4 = 80 // after abbr fourth: 20 + 10 + 3 + 5 + 4 = 42 // max_cols = 30: doesn't fit at tier 3 (42 > 30) // available_for_name = 30 - (10 + 3 + 5 + 4) = 8 let (abbr_details, abbr_tags, abbr_fourth, name_max) = compute_reduction_tier(20, 30, 11, 15, 10, 5, 30); assert_eq!((abbr_details, abbr_tags, abbr_fourth), (true, true, true)); assert_eq!(name_max, Some(8)); // 30 - (10 + 3 + 5 + 4) = 8 } }