use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
Frame,
};
use unicode_width::UnicodeWidthStr;
use crate::ui::{SplitDirection, ViewMode};
#[allow(unused_imports)]
use tmai_core::state::{AppState, MonitorScope, SortBy};
pub struct StatusBar;
impl StatusBar {
pub fn render(
frame: &mut Frame,
area: Rect,
state: &AppState,
view_mode: ViewMode,
split_offset: u16,
split_direction: SplitDirection,
) {
let mut spans = vec![];
let mode_label = match view_mode {
ViewMode::Both => format!(" {}% ", split_offset),
_ => format!(" {} ", view_mode.display_name()),
};
spans.push(Span::styled(
mode_label,
Style::default().fg(Color::White).bg(Color::DarkGray),
));
if view_mode == ViewMode::Both {
spans.push(Span::styled(
format!("|{}", split_direction.display_name()),
Style::default().fg(Color::DarkGray),
));
}
spans.push(Span::raw(" "));
Self::render_selection_info(&mut spans, state);
if state.is_passthrough_mode() {
spans.push(Span::styled(
" -- PASSTHROUGH -- ",
Style::default()
.fg(Color::Black)
.bg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(" ", Style::default()));
spans.push(Span::styled(
"Keys sent directly to pane ",
Style::default().fg(Color::White),
));
spans.push(Span::styled(
"Esc",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(":Exit ", Style::default().fg(Color::DarkGray)));
} else if state.is_input_mode() {
spans.push(Span::styled(
" -- INPUT -- ",
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(" ", Style::default()));
spans.push(Span::styled(
"Enter",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(":Send ", Style::default().fg(Color::DarkGray)));
spans.push(Span::styled(
"Esc",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
":Cancel ",
Style::default().fg(Color::DarkGray),
));
spans.push(Span::styled(
"<-/->",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(":Move ", Style::default().fg(Color::DarkGray)));
} else {
spans.push(Span::styled(
" j/k",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(":Nav ", Style::default().fg(Color::DarkGray)));
spans.push(Span::styled(
"y",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
":Approve ",
Style::default().fg(Color::DarkGray),
));
spans.push(Span::styled(
"i",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
":Input ",
Style::default().fg(Color::DarkGray),
));
spans.push(Span::styled(
"p",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
":Direct ",
Style::default().fg(Color::DarkGray),
));
spans.push(Span::styled(
"1-9",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
":Select ",
Style::default().fg(Color::DarkGray),
));
spans.push(Span::styled(
"t",
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
":Tasks ",
Style::default().fg(Color::DarkGray),
));
spans.push(Span::styled(
"T",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
":Teams ",
Style::default().fg(Color::DarkGray),
));
spans.push(Span::styled(
"h",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(":Help ", Style::default().fg(Color::DarkGray)));
spans.push(Span::styled(
"q",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(":Quit ", Style::default().fg(Color::DarkGray)));
}
spans.push(Span::raw(" "));
let attention_count = state.attention_count();
if attention_count > 0 {
spans.push(Span::styled(
format!(" {} needs attention ", attention_count),
Style::default()
.fg(Color::White)
.bg(Color::Red)
.add_modifier(Modifier::BOLD),
));
}
if let Some(notification) = state.active_notification() {
spans.push(Span::styled(
format!(" {} ", notification),
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD),
));
}
if let Some(error) = &state.error_message {
spans.push(Span::styled(
format!(" Error: {} ", error),
Style::default().fg(Color::White).bg(Color::Red),
));
}
if let Some(last_poll) = state.last_poll {
let elapsed = chrono::Utc::now()
.signed_duration_since(last_poll)
.num_seconds();
spans.push(Span::styled(
format!(" [{}s] ", elapsed),
Style::default().fg(Color::DarkGray),
));
}
let paragraph = Paragraph::new(Line::from(spans)).style(Style::default().bg(Color::Black));
frame.render_widget(paragraph, area);
}
fn render_selection_info(spans: &mut Vec<Span<'static>>, state: &AppState) {
if state.selection.is_on_create_new {
spans.push(Span::styled(" [+ New] ", Style::default().fg(Color::Green)));
} else if let Some(agent) = state.selected_agent() {
if agent.is_virtual {
spans.push(Span::styled(
" [Offline] ",
Style::default().fg(Color::DarkGray),
));
if let Some(ref team_info) = agent.team_info {
spans.push(Span::styled(
format!(
"{}/{} - Pane not found ",
team_info.team_name, team_info.member_name
),
Style::default().fg(Color::DarkGray),
));
}
} else {
let short_target = if agent.id.width() > 12 {
let truncated = truncate_to_width(&agent.id, 9);
format!("{}...", truncated)
} else {
agent.id.clone()
};
spans.push(Span::styled(
format!(" {} ", short_target),
Style::default().fg(Color::Cyan),
));
spans.push(Span::styled(
format!("{} ", agent.agent_type.short_name()),
Style::default().fg(Color::Yellow),
));
let detection_label = agent.detection_source.label();
let detection_color = match agent.detection_source {
tmai_core::agents::DetectionSource::HttpHook => Color::Cyan,
tmai_core::agents::DetectionSource::IpcSocket => Color::Green,
tmai_core::agents::DetectionSource::WebSocket => Color::Cyan,
tmai_core::agents::DetectionSource::CapturePane => Color::DarkGray,
};
let send_label = agent.send_capability.label();
let send_color = if agent.send_capability.can_send() {
detection_color
} else {
Color::Red
};
spans.push(Span::styled(
format!(
"[{}{}{}] ",
detection_label,
agent.send_capability.icon(),
send_label
),
Style::default().fg(send_color),
));
if let Some(team_info) = &agent.team_info {
spans.push(Span::styled(
format!("[{}/{}] ", team_info.team_name, team_info.member_name),
Style::default().fg(Color::Magenta),
));
}
if let Some(ref branch) = agent.git_branch {
let branch_color = if agent.git_dirty.unwrap_or(false) {
Color::Yellow
} else {
Color::Cyan
};
spans.push(Span::styled(
format!("[{}] ", branch),
Style::default().fg(branch_color),
));
}
}
}
}
}
fn truncate_to_width(s: &str, max_width: usize) -> String {
let mut result = String::new();
let mut current_width = 0;
for c in s.chars() {
let char_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
if current_width + char_width > max_width {
break;
}
result.push(c);
current_width += char_width;
}
result
}
#[cfg(test)]
mod tests {
}