use crate::config::AppConfig;
use crate::models::{GitlabMrState, MergeabilityStatus, Pipeline, PipelineState, TrackedMr};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span, Text};
pub fn render_pipelines_text(mr: &TrackedMr) -> Text<'static> {
let mut lines = vec![
Line::from(vec![Span::styled(
format!("Pipelines — MR !{}", mr.id),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
)]),
Line::from(vec![Span::raw("")]),
];
if mr.pipelines.is_empty() {
lines.push(Line::from(vec![Span::styled(
"No pipelines found for this MR.",
Style::default().fg(Color::DarkGray),
)]));
} else {
for pipeline in &mr.pipelines {
lines.extend(render_pipeline_block(pipeline));
lines.push(Line::from(vec![Span::raw("")]));
}
}
Text::from(lines)
}
fn render_pipeline_block(pipeline: &Pipeline) -> Vec<Line<'static>> {
let (status_icon, status_color) = pipeline_status_style(&pipeline.status);
let mut lines = vec![Line::from(vec![
Span::styled(
format!("#{} ", pipeline.id),
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
),
Span::styled(
status_icon,
Style::default()
.fg(status_color)
.add_modifier(Modifier::BOLD),
),
])];
if pipeline.jobs.is_empty() {
lines.push(Line::from(vec![Span::styled(
" No jobs.",
Style::default().fg(Color::DarkGray),
)]));
return lines;
}
let mut current_stage = String::new();
for job in &pipeline.jobs {
if job.stage != current_stage {
current_stage = job.stage.clone();
lines.push(Line::from(vec![Span::styled(
format!(" ▸ {}", current_stage),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)]));
}
let (job_icon, job_color) = job_status_style(&job.status);
let duration = job
.duration
.map(|d| format!(" ({:.0}s)", d))
.unwrap_or_default();
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(job_icon, Style::default().fg(job_color)),
Span::raw(format!(" {}{}", job.name, duration)),
]));
}
lines
}
fn pipeline_status_style(state: &PipelineState) -> (&'static str, Color) {
match state {
PipelineState::Success => ("✔ passed", Color::Green),
PipelineState::Failed => ("✘ failed", Color::Red),
PipelineState::Running => ("⟳ running", Color::Cyan),
PipelineState::Pending => ("◔ pending", Color::Yellow),
PipelineState::Canceled => ("⊘ canceled", Color::DarkGray),
PipelineState::Skipped => ("⊝ skipped", Color::DarkGray),
PipelineState::Created => ("○ created", Color::White),
PipelineState::Unknown => ("? unknown", Color::DarkGray),
}
}
fn job_status_style(status: &str) -> (&'static str, Color) {
match status {
"success" => ("✔", Color::Green),
"failed" => ("✘", Color::Red),
"running" => ("⟳", Color::Cyan),
"pending" => ("◔", Color::Yellow),
"canceled" => ("⊘", Color::DarkGray),
"skipped" => ("⊝", Color::DarkGray),
"created" => ("○", Color::White),
_ => ("?", Color::DarkGray),
}
}
pub fn create_chip_span(label: &str, config: &AppConfig) -> Span<'static> {
let (bg, fg) = config.get_label_style(label);
Span::styled(
format!(" {} ", label),
Style::default().bg(bg).fg(fg).add_modifier(Modifier::BOLD),
)
}
pub fn render_safe_inspector_text(mr: &TrackedMr, config: &AppConfig) -> Text<'static> {
let updated_at_display = mr
.updated_at
.as_deref()
.map(|s| s.get(..19).unwrap_or(s).replace('T', " "))
.unwrap_or_else(|| "Unknown".to_string());
let (badge_icon, badge_color) = config.activity_badge(mr.updated_at.as_deref());
let (state_label, state_fg, state_bg) = match mr.state {
GitlabMrState::Opened => (" Open ", Color::Black, Color::Green),
GitlabMrState::Merged => (" Merged ", Color::Black, Color::Magenta),
GitlabMrState::Closed => (" Closed ", Color::Black, Color::Red),
};
let mergeability_line: Option<Line<'static>> = if mr.state == GitlabMrState::Opened {
let (merge_text, merge_fg, merge_bg) = match mr.mergeability {
MergeabilityStatus::Mergeable => ("Mergeable", Color::Black, Color::LightGreen),
MergeabilityStatus::Conflict => ("Conflict", Color::White, Color::Red),
MergeabilityStatus::NeedsRebase => ("Rebase", Color::Black, Color::Yellow),
MergeabilityStatus::Unknown => ("Unknown", Color::DarkGray, Color::Black),
};
Some(Line::from(vec![
Span::raw("Merge : "),
Span::styled(
format!(" {merge_text} "),
Style::default()
.fg(merge_fg)
.bg(merge_bg)
.add_modifier(Modifier::BOLD),
),
]))
} else {
None
};
let mut lines = vec![
Line::from(vec![Span::styled(
format!("MR ID : !{}", mr.id),
Style::default()
.fg(ratatui::style::Color::Cyan)
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
)]),
Line::from(vec![
Span::raw("State : "),
Span::styled(
state_label,
Style::default()
.fg(state_fg)
.bg(state_bg)
.add_modifier(Modifier::BOLD),
),
]),
];
if let Some(line) = mergeability_line {
lines.push(line);
}
lines.push(Line::from(vec![
Span::raw("Author : "),
Span::styled(
format!("@{}", mr.author),
Style::default().add_modifier(Modifier::BOLD),
),
]));
lines.push(Line::from(vec![
Span::raw("Assignee : "),
Span::styled(
format!("@{}", mr.assignee),
Style::default().add_modifier(Modifier::BOLD),
),
]));
lines.push(Line::from(vec![
Span::raw("Milestone: "),
Span::styled(
mr.milestone.clone(),
Style::default().fg(ratatui::style::Color::Cyan),
),
]));
lines.push(Line::from(vec![
Span::raw("Updated : "),
Span::styled(
updated_at_display,
Style::default().fg(ratatui::style::Color::Yellow),
),
Span::raw(" "),
Span::styled(
badge_icon,
Style::default()
.fg(badge_color)
.add_modifier(Modifier::BOLD),
),
]));
if !mr.labels.is_empty() {
let mut label_spans = vec![Span::raw("Labels : ")];
for label in &mr.labels {
label_spans.push(create_chip_span(label, config));
label_spans.push(Span::raw(" "));
}
lines.push(Line::from(label_spans));
} else {
lines.push(Line::from(vec![
Span::raw("Labels : "),
Span::styled("None", Style::default().dark_gray()),
]));
}
lines.push(Line::from(vec![Span::styled(
"───────────────────────────────────",
Style::default().fg(ratatui::style::Color::DarkGray),
)]));
lines.push(Line::from(vec![Span::styled(
"Description:",
Style::default()
.fg(ratatui::style::Color::Yellow)
.add_modifier(Modifier::BOLD),
)]));
lines.push(Line::from(vec![Span::raw("")]));
if mr.description.trim().is_empty() {
lines.push(Line::from(vec![Span::styled(
"No description text provided.",
Style::default().dark_gray(),
)]));
return Text::from(lines);
}
for raw_line in mr.description.lines() {
let trimmed = raw_line.trim();
if trimmed.starts_with("# ") || trimmed.starts_with("## ") {
let text = trimmed.trim_start_matches('#').trim();
lines.push(Line::from(vec![Span::styled(
text.to_string(),
Style::default()
.fg(ratatui::style::Color::Cyan)
.add_modifier(Modifier::BOLD),
)]));
} else if trimmed.starts_with("### ") {
let text = trimmed.trim_start_matches("### ");
lines.push(Line::from(vec![Span::styled(
text.to_string(),
Style::default()
.fg(ratatui::style::Color::Yellow)
.add_modifier(Modifier::BOLD),
)]));
} else if trimmed == "---" {
lines.push(Line::from(vec![Span::styled(
"───────────────────────────────────",
Style::default().fg(ratatui::style::Color::DarkGray),
)]));
} else {
let mut spans = Vec::new();
let mut line_content = raw_line.to_string();
if trimmed.starts_with("- ") {
spans.push(Span::styled(
" • ",
Style::default().fg(ratatui::style::Color::Yellow),
));
line_content = raw_line.replacen("- ", "", 1);
} else if trimmed.starts_with("* ") {
spans.push(Span::styled(
" • ",
Style::default().fg(ratatui::style::Color::Yellow),
));
line_content = raw_line.replacen("* ", "", 1);
}
let bold_parts = line_content.split("**");
let mut is_bold = false;
for bold_part in bold_parts {
let code_parts = bold_part.split('`');
let mut is_code = false;
for code_part in code_parts {
let mut style = Style::default();
if is_bold {
style = style.add_modifier(Modifier::BOLD);
}
if is_code {
style = style.fg(ratatui::style::Color::Magenta);
}
spans.push(Span::styled(code_part.to_string(), style));
is_code = !is_code;
}
is_bold = !is_bold;
}
lines.push(Line::from(spans));
}
}
Text::from(lines)
}