use ratatui::{
style::Modifier,
text::{Line, Span},
};
use unicode_width::UnicodeWidthStr;
use crate::theme::ThemeStyles;
#[derive(Debug, Clone)]
pub struct DashboardInfo {
pub version: String,
pub model_id: String,
pub thinking_level: String,
pub project_name: String,
pub git_branch: Option<String>,
pub agents_md_path: Option<String>,
pub tool_names: Vec<String>,
pub skill_names: Vec<String>,
}
pub(crate) fn badge_line_count(names: &[String], width: usize) -> usize {
if names.is_empty() {
return 0;
}
let prefix = 2; let mut lines = 1;
let mut current_width = prefix;
for name in names {
let badge_w = UnicodeWidthStr::width(name.as_str()) + 3;
if current_width + badge_w > width && current_width > prefix {
lines += 1;
current_width = prefix + badge_w;
} else {
current_width += badge_w;
}
}
lines
}
pub(crate) fn compute_badge_lines(
names: &[String],
width: usize,
styles: &ThemeStyles,
) -> Vec<Line<'static>> {
if names.is_empty() {
return Vec::new();
}
let mut lines = Vec::new();
let prefix = 2;
let mut current_spans: Vec<Span<'static>> = vec![Span::raw(" ")];
let mut current_width = prefix;
for name in names {
let badge_text = format!("[{}] ", name);
let badge_w = UnicodeWidthStr::width(badge_text.as_str());
if current_width + badge_w > width && current_width > prefix {
lines.push(Line::from(std::mem::take(&mut current_spans)));
current_spans = vec![Span::raw(" ")];
current_width = prefix;
}
current_spans.push(Span::styled(badge_text, styles.primary));
current_width += badge_w;
}
if current_spans.len() > 1 {
lines.push(Line::from(current_spans));
}
lines
}
fn shorten_dashboard_path(path: &str) -> String {
crate::widgets::tool_renderer::shorten_path(path)
}
pub(crate) fn measure_dashboard(info: &DashboardInfo, width: u16) -> u16 {
let w = width as usize;
let mut h: usize = 0;
h += 1; h += 1; h += 1; h += 1; h += 1; h += 1;
if !info.tool_names.is_empty() {
h += 1; h += badge_line_count(&info.tool_names, w);
}
h += 1;
if !info.skill_names.is_empty() {
h += 1; h += badge_line_count(&info.skill_names, w);
h += 1; }
h += 1;
h as u16
}
pub(crate) fn dashboard_lines(
info: &DashboardInfo,
width: u16,
styles: &ThemeStyles,
) -> Vec<Line<'static>> {
let w = width as usize;
let mut lines = Vec::new();
lines.push(Line::from(vec![
Span::styled(" \u{25C8} ", styles.accent),
Span::styled(
format!("oxi v{}", info.version),
styles.accent.add_modifier(Modifier::BOLD),
),
]));
lines.push(Line::raw(""));
let model_display = info
.model_id
.split('/')
.next_back()
.unwrap_or(&info.model_id);
let provider = info.model_id.split('/').next().unwrap_or("");
let mut model_spans = vec![
Span::raw(" "),
Span::styled("Model ", styles.muted),
Span::styled(format!("{} ({})", model_display, provider), styles.normal),
];
if !info.thinking_level.is_empty() {
model_spans.push(Span::styled(" \u{00B7} ", styles.muted));
model_spans.push(Span::styled("Thinking: ", styles.muted));
model_spans.push(Span::styled(info.thinking_level.clone(), styles.warning));
}
lines.push(Line::from(model_spans));
let project = if let Some(ref branch) = info.git_branch {
format!("{} ({})", info.project_name, branch)
} else {
info.project_name.clone()
};
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled("Project ", styles.muted),
Span::styled(project, styles.normal),
]));
let agents = match &info.agents_md_path {
Some(p) => shorten_dashboard_path(p),
None => "not found".to_string(),
};
let agents_style = if info.agents_md_path.is_some() {
styles.success
} else {
styles.muted
};
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled("AGENTS.md ", styles.muted),
Span::styled(agents, agents_style),
]));
lines.push(Line::raw(""));
if !info.tool_names.is_empty() {
let header_text = format!(" \u{2500}\u{2500} Tools ({}) ", info.tool_names.len());
let header_w = UnicodeWidthStr::width(header_text.as_str());
let sep_remain = w.saturating_sub(header_w);
let header_full = format!("{}{}", header_text, "\u{2500}".repeat(sep_remain));
lines.push(Line::from(Span::styled(header_full, styles.border)));
let badges = compute_badge_lines(&info.tool_names, w, styles);
lines.extend(badges);
}
lines.push(Line::raw(""));
if !info.skill_names.is_empty() {
let header_text = format!(" \u{2500}\u{2500} Skills ({}) ", info.skill_names.len());
let header_w = UnicodeWidthStr::width(header_text.as_str());
let sep_remain = w.saturating_sub(header_w);
let header_full = format!("{}{}", header_text, "\u{2500}".repeat(sep_remain));
lines.push(Line::from(Span::styled(header_full, styles.border)));
let badges = compute_badge_lines(&info.skill_names, w, styles);
lines.extend(badges);
lines.push(Line::raw(""));
}
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled("Enter ", styles.primary),
Span::styled("send \u{00B7} ", styles.muted),
Span::styled("/help ", styles.primary),
Span::styled("commands \u{00B7} ", styles.muted),
Span::styled("Ctrl+C ", styles.primary),
Span::styled("interrupt", styles.muted),
]));
lines
}