use crossterm::{
queue,
style::{Color, Print},
};
use std::io::Write;
use crate::app_state::AppState;
use crate::ui::text::print_colored_text;
pub const USERS_TAB_NAME: &str = "Users";
pub const TOPOLOGY_TAB_NAME: &str = "Topology";
#[inline]
pub fn users_tab_index(tabs: &[String]) -> Option<usize> {
tabs.iter().position(|t| t == USERS_TAB_NAME)
}
#[inline]
pub fn is_users_tab_active(state: &AppState) -> bool {
users_tab_index(&state.tabs).is_some_and(|i| i == state.current_tab)
}
#[inline]
pub fn topology_tab_index(tabs: &[String]) -> Option<usize> {
tabs.iter().position(|t| t == TOPOLOGY_TAB_NAME)
}
#[inline]
pub fn is_topology_tab_active(state: &AppState) -> bool {
topology_tab_index(&state.tabs).is_some_and(|i| i == state.current_tab)
}
#[inline]
pub fn is_reserved_tab(name: &str) -> bool {
matches!(name, "All" | USERS_TAB_NAME | TOPOLOGY_TAB_NAME)
}
#[inline]
pub fn host_tab_count(tabs: &[String]) -> usize {
tabs.iter().filter(|t| !is_reserved_tab(t)).count()
}
pub fn draw_tabs<W: Write>(stdout: &mut W, state: &AppState, cols: u16) {
let mut labels: Vec<(String, Color)> = Vec::new();
let mut available_width = cols.saturating_sub(8);
if !state.tabs.is_empty() {
let all_tab = &state.tabs[0];
let tab_width = all_tab.len() as u16 + 2;
if available_width >= tab_width {
if state.current_tab == 0 {
labels.push((format!(" {all_tab} "), Color::Black));
} else {
labels.push((format!(" {all_tab} "), Color::White));
}
available_width -= tab_width;
}
}
let node_tabs: Vec<_> = state
.tabs
.iter()
.enumerate()
.skip(1) .skip(state.tab_scroll_offset)
.collect();
for (i, tab) in node_tabs {
let connection_status = state.connection_status.get(tab);
let display_name = if tab == "All" {
tab.to_string()
} else if let Some(status) = connection_status {
if tab.contains('@') && !tab.starts_with("http") {
let base = format_ssh_tab_label(tab);
match status.transport_chip.as_deref() {
Some(chip) if !chip.is_empty() => format!("{base}·{chip}"),
_ => base,
}
} else {
status.actual_hostname.as_ref().unwrap_or(tab).clone()
}
} else {
tab.to_string()
};
let tab_width = display_name.len() as u16 + 2; if available_width < tab_width {
break; }
let color = if state.current_tab == i {
Color::Black } else {
let is_connected = if tab != "All" {
state
.connection_status
.get(tab)
.map(|status| status.is_connected)
.unwrap_or(true) } else {
true };
if is_connected {
Color::White } else {
Color::DarkGrey }
};
labels.push((format!(" {display_name} "), color));
available_width -= tab_width;
}
render_tab_labels(stdout, labels);
render_tab_separator(stdout, cols);
}
fn render_tab_labels<W: Write>(stdout: &mut W, labels: Vec<(String, Color)>) {
queue!(stdout, Print("Tabs: ")).unwrap();
for (text, color) in labels {
if color == Color::Black {
print_colored_text(stdout, &text, Color::White, Some(Color::Blue), None);
} else {
print_colored_text(stdout, &text, color, None, None);
}
}
queue!(stdout, Print("\r\n")).unwrap();
}
fn render_tab_separator<W: Write>(stdout: &mut W, cols: u16) {
let separator = "─".repeat(cols as usize);
print_colored_text(stdout, &separator, Color::DarkGrey, None, None);
queue!(stdout, Print("\r\n")).unwrap();
}
pub fn format_ssh_tab_label(host_id: &str) -> String {
let without_default_port = host_id.strip_suffix(":22").unwrap_or(host_id);
format!("ssh://{without_default_port}")
}
#[allow(dead_code)]
pub fn calculate_tab_visibility(state: &AppState, cols: u16) -> TabVisibility {
let mut available_width = cols.saturating_sub(8);
if !state.tabs.is_empty() {
let all_tab_width = state.tabs[0].len() as u16 + 2;
available_width = available_width.saturating_sub(all_tab_width);
}
let mut last_visible_node_tab = state.tab_scroll_offset;
for (node_index, tab) in state
.tabs
.iter()
.enumerate()
.skip(1)
.skip(state.tab_scroll_offset)
{
let display_name = if tab.contains('@') && !tab.starts_with("http") {
let base = format_ssh_tab_label(tab);
match state
.connection_status
.get(tab)
.and_then(|s| s.transport_chip.as_deref())
{
Some(chip) if !chip.is_empty() => format!("{base}·{chip}"),
_ => base,
}
} else if let Some(connection_status) = state.connection_status.get(tab) {
connection_status
.actual_hostname
.as_ref()
.unwrap_or(tab)
.clone()
} else {
tab.to_string()
};
let tab_width = display_name.len() as u16 + 2;
if available_width < tab_width {
break;
}
available_width -= tab_width;
last_visible_node_tab = node_index - 1; }
TabVisibility {
first_visible: state.tab_scroll_offset,
last_visible: last_visible_node_tab + 1, has_more_left: state.tab_scroll_offset > 0,
has_more_right: last_visible_node_tab + 1 < state.tabs.len() - 1,
}
}
#[allow(dead_code)]
pub struct TabVisibility {
pub first_visible: usize,
pub last_visible: usize,
pub has_more_left: bool,
pub has_more_right: bool,
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_state() -> AppState {
let mut state = AppState::new();
state.tabs = vec![
"All".to_string(),
"host1".to_string(),
"host2".to_string(),
"host3".to_string(),
];
state.is_local_mode = false; state
}
#[test]
fn test_tab_visibility_calculation() {
let state = create_test_state();
let visibility = calculate_tab_visibility(&state, 80);
assert_eq!(visibility.first_visible, 0);
assert!(!visibility.has_more_left);
assert!(!visibility.has_more_right || state.tabs.len() > 4);
}
#[test]
fn test_tab_visibility_with_scroll() {
let mut state = create_test_state();
state.tab_scroll_offset = 1;
let visibility = calculate_tab_visibility(&state, 80);
assert_eq!(visibility.first_visible, 1);
assert!(visibility.has_more_left);
}
#[test]
fn ssh_tab_label_strips_default_port() {
assert_eq!(
format_ssh_tab_label("admin@dgx-01:22"),
"ssh://admin@dgx-01"
);
}
#[test]
fn ssh_tab_label_preserves_custom_port() {
assert_eq!(
format_ssh_tab_label("admin@dgx-01:2222"),
"ssh://admin@dgx-01:2222"
);
}
#[test]
fn host_tab_count_excludes_reserved_tabs() {
let mut tabs = vec![
"All".to_string(),
USERS_TAB_NAME.to_string(),
TOPOLOGY_TAB_NAME.to_string(),
];
for i in 0..50 {
tabs.push(format!("host-{i}"));
}
assert_eq!(host_tab_count(&tabs), 50);
assert_eq!(host_tab_count(&["All".to_string()]), 0);
assert_eq!(host_tab_count(&[]), 0);
}
#[test]
fn is_reserved_tab_matches_cluster_level_tabs() {
assert!(is_reserved_tab("All"));
assert!(is_reserved_tab(USERS_TAB_NAME));
assert!(is_reserved_tab(TOPOLOGY_TAB_NAME));
assert!(!is_reserved_tab("dgx-01"));
assert!(!is_reserved_tab("admin@dgx-01:22"));
}
}