use std::path::Path;
use std::process::Command;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span;
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use ratatui::Frame;
use ratatui::layout::Rect;
use crate::app::App;
pub fn render(frame: &mut Frame, area: Rect, app: &App) {
let (chunks, next_chunk) = crate::screens::common::render_file_input(
frame,
area,
app,
"Enter path to a manifest file:",
"diff",
);
if let Some(ref result) = app.result_fragment {
let text = match result.as_str() {
Some(s) => s.to_string(),
None => result.to_string(),
};
let has_changes = text.contains("Added") || text.contains("Removed");
let result_color = if text.starts_with('\u{2718}') {
Color::Red
} else if has_changes {
Color::Yellow
} else {
Color::Green
};
let result_block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(result_color))
.title(Span::styled(
" Manifest Diff ",
Style::default()
.fg(result_color)
.add_modifier(Modifier::BOLD),
));
let result_para = Paragraph::new(text)
.style(Style::default().fg(Color::White))
.block(result_block)
.wrap(Wrap { trim: false });
if next_chunk < chunks.len() {
frame.render_widget(result_para, chunks[next_chunk]);
}
}
}
pub fn run_diff(path_str: &str) -> serde_json::Value {
let path = Path::new(path_str.trim());
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => return serde_json::json!(format!("\u{2718} Could not read file: {}", e)),
};
let current: serde_json::Value = match serde_json::from_str(&content) {
Ok(v) => v,
Err(e) => return serde_json::json!(format!("\u{2718} Invalid JSON: {}", e)),
};
let tag = match Command::new("git")
.args(["describe", "--tags", "--abbrev=0"])
.output()
{
Ok(output) if output.status.success() => {
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
_ => return serde_json::json!(format!("\u{2718} No git tags found — cannot diff")),
};
let path_str_clean = path_str.trim();
let old_content = match Command::new("git")
.args(["show", &format!("{}:{}", tag, path_str_clean)])
.output()
{
Ok(output) if output.status.success() => {
String::from_utf8_lossy(&output.stdout).to_string()
}
_ => {
return serde_json::json!(format!(
"\u{2718} Could not read {} from tag {}",
path_str_clean, tag
))
}
};
let old: serde_json::Value = match serde_json::from_str(&old_content) {
Ok(v) => v,
Err(e) => {
return serde_json::json!(format!(
"\u{2718} Could not parse old manifest from {}: {}",
tag, e
))
}
};
let name = current
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let mut report = format!("Diff: {} (current vs {})\n", name, tag);
let old_bindings = collect_all_paths(old.get("bindings").and_then(|v| v.as_object()), "");
let new_bindings = collect_all_paths(current.get("bindings").and_then(|v| v.as_object()), "");
let added: Vec<&String> = new_bindings.iter().filter(|p| !old_bindings.contains(p)).collect();
let removed: Vec<&String> = old_bindings.iter().filter(|p| !new_bindings.contains(p)).collect();
let old_caps: Vec<String> = old
.get("capabilities")
.and_then(|v| v.as_object())
.map(|o| o.keys().cloned().collect())
.unwrap_or_default();
let new_caps: Vec<String> = current
.get("capabilities")
.and_then(|v| v.as_object())
.map(|o| o.keys().cloned().collect())
.unwrap_or_default();
let added_caps: Vec<&String> = new_caps.iter().filter(|c| !old_caps.contains(c)).collect();
let removed_caps: Vec<&String> = old_caps.iter().filter(|c| !new_caps.contains(c)).collect();
let old_slots: Vec<String> = old
.get("slots")
.and_then(|v| v.as_array())
.map(|a| a.iter().filter_map(|s| s.get("id").and_then(|v| v.as_str()).map(|s| s.to_string())).collect())
.unwrap_or_default();
let new_slots: Vec<String> = current
.get("slots")
.and_then(|v| v.as_array())
.map(|a| a.iter().filter_map(|s| s.get("id").and_then(|v| v.as_str()).map(|s| s.to_string())).collect())
.unwrap_or_default();
let added_slots: Vec<&String> = new_slots.iter().filter(|s| !old_slots.contains(s)).collect();
let removed_slots: Vec<&String> = old_slots.iter().filter(|s| !new_slots.contains(s)).collect();
let has_changes = !added.is_empty()
|| !removed.is_empty()
|| !added_caps.is_empty()
|| !removed_caps.is_empty()
|| !added_slots.is_empty()
|| !removed_slots.is_empty();
if !has_changes {
report.push_str("\nNo changes since last tag.\n");
return serde_json::json!(report);
}
if !added.is_empty() {
report.push_str(&format!("\nAdded bindings ({}):\n", added.len()));
for p in &added {
report.push_str(&format!(" + {}\n", p));
}
}
if !removed.is_empty() {
report.push_str(&format!("\nRemoved bindings ({}):\n", removed.len()));
for p in &removed {
report.push_str(&format!(" - {}\n", p));
}
}
if !added_caps.is_empty() {
report.push_str(&format!("\nAdded capabilities ({}):\n", added_caps.len()));
for c in &added_caps {
report.push_str(&format!(" + {}\n", c));
}
}
if !removed_caps.is_empty() {
report.push_str(&format!("\nRemoved capabilities ({}):\n", removed_caps.len()));
for c in &removed_caps {
report.push_str(&format!(" - {}\n", c));
}
}
if !added_slots.is_empty() {
report.push_str(&format!("\nAdded slots ({}):\n", added_slots.len()));
for s in &added_slots {
report.push_str(&format!(" + {}\n", s));
}
}
if !removed_slots.is_empty() {
report.push_str(&format!("\nRemoved slots ({}):\n", removed_slots.len()));
for s in &removed_slots {
report.push_str(&format!(" - {}\n", s));
}
}
serde_json::json!(report)
}
fn collect_all_paths(obj: Option<&serde_json::Map<String, serde_json::Value>>, prefix: &str) -> Vec<String> {
let mut paths = Vec::new();
if let Some(map) = obj {
for (key, value) in map {
let full = if prefix.is_empty() {
key.clone()
} else {
format!("{}.{}", prefix, key)
};
if let Some(members) = value.get("members").and_then(|v| v.as_object()) {
paths.extend(collect_all_paths(Some(members), &full));
} else {
paths.push(full);
}
}
}
paths
}