1use anyhow::{Context, Result};
2use chrono::{DateTime, Timelike, Utc};
3use colored::Colorize;
4use crossterm::{
5 event::{self, Event, KeyCode},
6 terminal::{
7 disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
8 },
9 ExecutableCommand,
10};
11use ratatui::{
12 backend::CrosstermBackend, layout::{Alignment, Constraint, Direction, Layout, Rect},
13 style::{Color, Modifier, Style},
14 symbols,
15 widgets::{
16 Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, ListItem, Paragraph,
17 Sparkline, Tabs,
18 },
19 Frame, Terminal,
20};
21use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23use std::fs;
24use std::io;
25use std::path::PathBuf;
26use std::process::Command;
27use crate::license;
28#[derive(Debug, Serialize, Deserialize, Clone)]
29pub struct BuildMetrics {
30 pub timestamp: DateTime<Utc>,
31 pub command: String,
32 pub duration_seconds: f64,
33 pub success: bool,
34 pub error_count: usize,
35 pub warning_count: usize,
36 pub incremental: bool,
37 pub profile: String,
38 pub features: Vec<String>,
39 pub dependencies_compiled: usize,
40 pub crate_units_compiled: usize,
41 pub memory_peak_mb: Option<f64>,
42 pub cpu_usage_percent: Option<f64>,
43}
44#[derive(Debug, Serialize, Deserialize)]
45pub struct DependencyMetrics {
46 pub name: String,
47 pub version: String,
48 pub compile_time_seconds: f64,
49 pub size_bytes: u64,
50 pub features: Vec<String>,
51}
52#[derive(Debug, Serialize, Deserialize)]
53pub struct TideData {
54 pub builds: Vec<BuildMetrics>,
55 pub dependencies: HashMap<String, DependencyMetrics>,
56 pub daily_summary: HashMap<String, DailySummary>,
57}
58#[derive(Debug, Serialize, Deserialize, Clone)]
59pub struct DailySummary {
60 pub date: String,
61 pub total_builds: usize,
62 pub successful_builds: usize,
63 pub failed_builds: usize,
64 pub total_time_seconds: f64,
65 pub avg_build_time: f64,
66 pub total_errors: usize,
67 pub total_warnings: usize,
68}
69pub struct TideCharts {
70 data: TideData,
71 data_file: PathBuf,
72 selected_tab: usize,
73}
74impl TideCharts {
75 pub fn new() -> Result<Self> {
76 let data_file = dirs::home_dir()
77 .context("Could not find home directory")?
78 .join(".shipwreck")
79 .join("tide_data.json");
80 let data = if data_file.exists() {
81 let content = fs::read_to_string(&data_file)?;
82 serde_json::from_str(&content).unwrap_or_default()
83 } else {
84 TideData::default()
85 };
86 Ok(Self {
87 data,
88 data_file,
89 selected_tab: 0,
90 })
91 }
92 pub fn record_build(&mut self, metrics: BuildMetrics) -> Result<()> {
93 self.data.builds.push(metrics.clone());
94 let date = metrics.timestamp.date_naive().to_string();
95 let summary = self
96 .data
97 .daily_summary
98 .entry(date.clone())
99 .or_insert(DailySummary {
100 date,
101 total_builds: 0,
102 successful_builds: 0,
103 failed_builds: 0,
104 total_time_seconds: 0.0,
105 avg_build_time: 0.0,
106 total_errors: 0,
107 total_warnings: 0,
108 });
109 summary.total_builds += 1;
110 if metrics.success {
111 summary.successful_builds += 1;
112 } else {
113 summary.failed_builds += 1;
114 }
115 summary.total_time_seconds += metrics.duration_seconds;
116 summary.avg_build_time = summary.total_time_seconds
117 / summary.total_builds as f64;
118 summary.total_errors += metrics.error_count;
119 summary.total_warnings += metrics.warning_count;
120 if self.data.builds.len() > 10000 {
121 self.data.builds = self
122 .data
123 .builds[self.data.builds.len() - 10000..]
124 .to_vec();
125 }
126 self.save()?;
127 Ok(())
128 }
129 pub fn analyze_dependencies(&mut self) -> Result<()> {
130 println!("🔍 Analyzing dependency compile times...");
131 let output = Command::new("cargo").args(&["build", "--timings"]).output()?;
132 if output.status.success() {
133 println!(
134 "✅ Timing data collected. Check target/cargo-timings/ for detailed report."
135 );
136 }
137 let metadata = cargo_metadata::MetadataCommand::new().exec()?;
138 for package in metadata.packages {
139 if package.source.is_some() {
140 let dep_metrics = DependencyMetrics {
141 name: package.name.clone(),
142 version: package.version.to_string(),
143 compile_time_seconds: 0.0,
144 size_bytes: 0,
145 features: package.features.keys().cloned().collect(),
146 };
147 self.data.dependencies.insert(package.name, dep_metrics);
148 }
149 }
150 self.save()?;
151 Ok(())
152 }
153 pub fn show_interactive(&mut self) -> Result<()> {
154 enable_raw_mode()?;
155 let mut stdout = io::stdout();
156 stdout.execute(EnterAlternateScreen)?;
157 let backend = CrosstermBackend::new(stdout);
158 let mut terminal = Terminal::new(backend)?;
159 let res = self.run_interactive(&mut terminal);
160 disable_raw_mode()?;
161 terminal.backend_mut().execute(LeaveAlternateScreen)?;
162 terminal.show_cursor()?;
163 res
164 }
165 fn run_interactive<B: ratatui::backend::Backend>(
166 &mut self,
167 terminal: &mut Terminal<B>,
168 ) -> Result<()> {
169 loop {
170 terminal.draw(|f| self.ui(f))?;
171 if event::poll(std::time::Duration::from_millis(100))? {
172 if let Event::Key(key) = event::read()? {
173 match key.code {
174 KeyCode::Char('q') | KeyCode::Esc => break,
175 KeyCode::Tab => {
176 self.selected_tab = (self.selected_tab + 1) % 4;
177 }
178 KeyCode::BackTab => {
179 self.selected_tab = if self.selected_tab > 0 {
180 self.selected_tab - 1
181 } else {
182 3
183 };
184 }
185 _ => {}
186 }
187 }
188 }
189 }
190 Ok(())
191 }
192 fn ui(&self, frame: &mut Frame) {
193 let chunks = Layout::default()
194 .direction(Direction::Vertical)
195 .constraints([
196 Constraint::Length(3),
197 Constraint::Min(0),
198 Constraint::Length(3),
199 ])
200 .split(frame.size());
201 let titles = vec!["Overview", "Performance", "Dependencies", "Trends"];
202 let tabs = Tabs::new(titles)
203 .block(Block::default().borders(Borders::ALL).title("🌊 Tide Charts"))
204 .highlight_style(
205 Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
206 )
207 .select(self.selected_tab);
208 frame.render_widget(tabs, chunks[0]);
209 match self.selected_tab {
210 0 => self.render_overview(frame, chunks[1]),
211 1 => self.render_performance(frame, chunks[1]),
212 2 => self.render_dependencies(frame, chunks[1]),
213 3 => self.render_trends(frame, chunks[1]),
214 _ => {}
215 }
216 let help = Paragraph::new("Press Tab to switch views | q to quit")
217 .style(Style::default().fg(Color::DarkGray))
218 .alignment(Alignment::Center)
219 .block(Block::default().borders(Borders::ALL));
220 frame.render_widget(help, chunks[2]);
221 }
222 fn render_overview(&self, frame: &mut Frame, area: Rect) {
223 let chunks = Layout::default()
224 .direction(Direction::Vertical)
225 .constraints([
226 Constraint::Length(8),
227 Constraint::Length(8),
228 Constraint::Min(5),
229 ])
230 .split(area);
231 let recent_builds = self.data.builds.iter().rev().take(50).collect::<Vec<_>>();
232 let success_rate = if !recent_builds.is_empty() {
233 let successful = recent_builds.iter().filter(|b| b.success).count();
234 (successful as f64 / recent_builds.len() as f64) * 100.0
235 } else {
236 0.0
237 };
238 let stats = vec![
239 format!("Total Builds: {}", self.data.builds.len()),
240 format!("Success Rate: {:.1}%", success_rate),
241 format!("Avg Build Time: {:.2}s", self.get_avg_build_time()),
242 format!("Dependencies: {}", self.data.dependencies.len()),
243 ];
244 let stats_widget = Paragraph::new(stats.join("\n"))
245 .block(Block::default().borders(Borders::ALL).title("📊 Statistics"))
246 .style(Style::default().fg(Color::White));
247 frame.render_widget(stats_widget, chunks[0]);
248 let gauge = Gauge::default()
249 .block(
250 Block::default().borders(Borders::ALL).title("🎯 Build Success Rate"),
251 )
252 .gauge_style(Style::default().fg(Color::Green).bg(Color::Black))
253 .percent(success_rate as u16)
254 .label(format!("{:.1}%", success_rate));
255 frame.render_widget(gauge, chunks[1]);
256 let sparkline_data: Vec<u64> = recent_builds
257 .iter()
258 .map(|b| (b.duration_seconds * 10.0) as u64)
259 .collect();
260 let sparkline = Sparkline::default()
261 .block(
262 Block::default().borders(Borders::ALL).title("⏱️ Recent Build Times"),
263 )
264 .data(&sparkline_data)
265 .style(Style::default().fg(Color::Cyan));
266 frame.render_widget(sparkline, chunks[2]);
267 }
268 fn render_performance(&self, frame: &mut Frame, area: Rect) {
269 let chunks = Layout::default()
270 .direction(Direction::Horizontal)
271 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
272 .split(area);
273 let recent: Vec<_> = self.data.builds.iter().rev().take(10).collect();
274 let data: Vec<(f64, f64)> = recent
275 .iter()
276 .enumerate()
277 .map(|(i, b)| (i as f64, b.duration_seconds))
278 .collect();
279 if !data.is_empty() {
280 let datasets = vec![
281 Dataset::default().name("Build Time").marker(symbols::Marker::Dot)
282 .style(Style::default().fg(Color::Cyan))
283 .graph_type(ratatui::widgets::GraphType::Line).data(& data)
284 ];
285 let max_time = data.iter().map(|(_, t)| *t).fold(0.0, f64::max);
286 let max_x = (data.len() - 1) as f64;
287 let chart = Chart::new(datasets)
288 .block(
289 Block::default()
290 .borders(Borders::ALL)
291 .title("📈 Build Performance"),
292 )
293 .x_axis(
294 Axis::default()
295 .title("Recent Builds")
296 .style(Style::default().fg(Color::Gray))
297 .bounds([0.0, max_x]),
298 )
299 .y_axis(
300 Axis::default()
301 .title("Time (s)")
302 .style(Style::default().fg(Color::Gray))
303 .bounds([0.0, max_time * 1.2]),
304 );
305 frame.render_widget(chart, chunks[0]);
306 } else {
307 let placeholder = Paragraph::new(
308 "No build data yet. Run some cargo commands!",
309 )
310 .block(
311 Block::default()
312 .borders(Borders::ALL)
313 .title("📈 Build Performance"),
314 )
315 .style(Style::default().fg(Color::Gray));
316 frame.render_widget(placeholder, chunks[0]);
317 }
318 let mut daily_summaries: Vec<_> = self.data.daily_summary.values().collect();
319 daily_summaries.sort_by(|a, b| b.date.cmp(&a.date));
320 if !daily_summaries.is_empty() {
321 let formatted_dates: Vec<String> = daily_summaries
322 .iter()
323 .take(7)
324 .map(|s| {
325 if s.date.starts_with("2025-") {
326 s.date[5..].to_string()
327 } else {
328 s.date.clone()
329 }
330 })
331 .collect();
332 let daily_data: Vec<(&str, u64)> = formatted_dates
333 .iter()
334 .zip(daily_summaries.iter().take(7))
335 .map(|(date_str, summary)| (
336 date_str.as_str(),
337 summary.total_builds as u64,
338 ))
339 .collect();
340 let max_builds = daily_data.iter().map(|(_, v)| *v).max().unwrap_or(1);
341 let bar_chart = BarChart::default()
342 .block(Block::default().borders(Borders::ALL).title("📅 Daily Builds"))
343 .data(&daily_data)
344 .bar_width(6)
345 .bar_gap(1)
346 .max(max_builds)
347 .style(Style::default().fg(Color::Yellow))
348 .value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
349 frame.render_widget(bar_chart, chunks[1]);
350 } else {
351 let placeholder = Paragraph::new("No daily build data yet.")
352 .block(Block::default().borders(Borders::ALL).title("📅 Daily Builds"))
353 .style(Style::default().fg(Color::Gray));
354 frame.render_widget(placeholder, chunks[1]);
355 }
356 }
357 fn render_dependencies(&self, frame: &mut Frame, area: Rect) {
358 let mut deps: Vec<(&String, &DependencyMetrics)> = self
359 .data
360 .dependencies
361 .iter()
362 .collect();
363 deps.sort_by(|a, b| {
364 b.1.compile_time_seconds.partial_cmp(&a.1.compile_time_seconds).unwrap()
365 });
366 let items: Vec<ListItem> = deps
367 .iter()
368 .take(20)
369 .map(|(name, metrics)| {
370 let content = format!(
371 "{} v{} - {:.2}s", name, metrics.version, metrics
372 .compile_time_seconds
373 );
374 ListItem::new(content)
375 })
376 .collect();
377 let list = List::new(items)
378 .block(
379 Block::default()
380 .borders(Borders::ALL)
381 .title("📦 Dependency Compile Times"),
382 )
383 .highlight_style(Style::default().add_modifier(Modifier::BOLD))
384 .highlight_symbol(">> ");
385 frame.render_widget(list, area);
386 }
387 fn render_trends(&self, frame: &mut Frame, area: Rect) {
388 let chunks = Layout::default()
389 .direction(Direction::Vertical)
390 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
391 .split(area);
392 let trend_text = self.analyze_trends();
393 let trends = Paragraph::new(trend_text)
394 .block(
395 Block::default()
396 .borders(Borders::ALL)
397 .title("📊 Build Trends Analysis"),
398 )
399 .style(Style::default().fg(Color::White))
400 .wrap(ratatui::widgets::Wrap {
401 trim: true,
402 });
403 frame.render_widget(trends, chunks[0]);
404 let recommendations = self.get_recommendations();
405 let rec_items: Vec<ListItem> = recommendations
406 .iter()
407 .map(|r| ListItem::new(r.as_str()))
408 .collect();
409 let rec_list = List::new(rec_items)
410 .block(Block::default().borders(Borders::ALL).title("💡 Recommendations"))
411 .style(Style::default().fg(Color::Green));
412 frame.render_widget(rec_list, chunks[1]);
413 }
414 fn analyze_trends(&self) -> String {
415 if self.data.builds.is_empty() {
416 return "No build data available yet.".to_string();
417 }
418 let recent = &self.data.builds[self.data.builds.len().saturating_sub(100)..];
419 let old = &self.data.builds[..self.data.builds.len().saturating_sub(100)];
420 let recent_avg = if !recent.is_empty() {
421 recent.iter().map(|b| b.duration_seconds).sum::<f64>() / recent.len() as f64
422 } else {
423 0.0
424 };
425 let old_avg = if !old.is_empty() {
426 old.iter().map(|b| b.duration_seconds).sum::<f64>() / old.len() as f64
427 } else {
428 recent_avg
429 };
430 let improvement = ((old_avg - recent_avg) / old_avg * 100.0).abs();
431 let trend = if recent_avg < old_avg {
432 format!("✅ Build times improved by {:.1}%", improvement)
433 } else if recent_avg > old_avg {
434 format!("⚠️ Build times increased by {:.1}%", improvement)
435 } else {
436 "→ Build times stable".to_string()
437 };
438 let error_trend = self.analyze_error_trend();
439 let peak_times = self.find_peak_build_times();
440 format!("{}\n{}\n{}", trend, error_trend, peak_times)
441 }
442 fn analyze_error_trend(&self) -> String {
443 let recent = &self.data.builds[self.data.builds.len().saturating_sub(50)..];
444 let total_errors: usize = recent.iter().map(|b| b.error_count).sum();
445 let total_warnings: usize = recent.iter().map(|b| b.warning_count).sum();
446 format!("Recent: {} errors, {} warnings", total_errors, total_warnings)
447 }
448 fn find_peak_build_times(&self) -> String {
449 if self.data.builds.is_empty() {
450 return String::new();
451 }
452 let mut hour_builds: HashMap<u32, Vec<f64>> = HashMap::new();
453 for build in &self.data.builds {
454 let hour = build.timestamp.hour();
455 hour_builds.entry(hour).or_default().push(build.duration_seconds);
456 }
457 let mut peak_hour = 0;
458 let mut peak_avg = 0.0;
459 for (hour, times) in &hour_builds {
460 let avg = times.iter().sum::<f64>() / times.len() as f64;
461 if avg > peak_avg {
462 peak_avg = avg;
463 peak_hour = *hour;
464 }
465 }
466 format!("Peak build time: {}:00 (avg {:.2}s)", peak_hour, peak_avg)
467 }
468 fn get_recommendations(&self) -> Vec<String> {
469 let mut recommendations = Vec::new();
470 if self.get_avg_build_time() > 60.0 {
471 recommendations
472 .push("Consider using cargo-nextest for faster test runs".to_string());
473 recommendations
474 .push("Enable incremental compilation in Cargo.toml".to_string());
475 }
476 let recent_failures = self
477 .data
478 .builds
479 .iter()
480 .rev()
481 .take(10)
482 .filter(|b| !b.success)
483 .count();
484 if recent_failures > 3 {
485 recommendations
486 .push(
487 "High failure rate detected - consider running clippy".to_string(),
488 );
489 recommendations
490 .push("Set up pre-commit hooks to catch issues earlier".to_string());
491 }
492 if self.data.dependencies.len() > 100 {
493 recommendations
494 .push("Large dependency count - audit with cargo-outdated".to_string());
495 recommendations
496 .push(
497 "Consider using cargo-machete to find unused dependencies"
498 .to_string(),
499 );
500 }
501 let incremental_builds = self
502 .data
503 .builds
504 .iter()
505 .filter(|b| b.incremental)
506 .count();
507 if incremental_builds < self.data.builds.len() / 2 {
508 recommendations
509 .push("Enable incremental compilation for faster rebuilds".to_string());
510 }
511 recommendations
512 }
513 fn get_avg_build_time(&self) -> f64 {
514 if self.data.builds.is_empty() {
515 0.0
516 } else {
517 let total: f64 = self.data.builds.iter().map(|b| b.duration_seconds).sum();
518 total / self.data.builds.len() as f64
519 }
520 }
521 fn save(&self) -> Result<()> {
522 let json = serde_json::to_string_pretty(&self.data)?;
523 fs::write(&self.data_file, json)?;
524 Ok(())
525 }
526 pub fn export_csv(&self, path: &PathBuf) -> Result<()> {
527 let mut csv = String::new();
528 csv.push_str("timestamp,command,duration,success,errors,warnings\n");
529 for build in &self.data.builds {
530 csv.push_str(
531 &format!(
532 "{},{},{},{},{},{}\n", build.timestamp.to_rfc3339(), build.command,
533 build.duration_seconds, build.success, build.error_count, build
534 .warning_count
535 ),
536 );
537 }
538 fs::write(path, csv)?;
539 println!("✅ Build metrics exported to {}", path.display());
540 Ok(())
541 }
542}
543impl Default for TideData {
544 fn default() -> Self {
545 Self {
546 builds: Vec::new(),
547 dependencies: HashMap::new(),
548 daily_summary: HashMap::new(),
549 }
550 }
551}
552pub fn check_sailor_tracker(command: &str) -> Result<bool> {
553 println!("⛵ Sailor tracking command '{}' - checking the winds!", command.cyan());
554 let license_manager = license::LicenseManager::new()?;
555 match license_manager.enforce_license(command) {
556 Ok(_) => {
557 println!(
558 "✅ Sailor reports: Command '{}' has fair winds!", command.green()
559 );
560 println!(" ⛵ All sails set - ready to catch the wind!");
561 Ok(true)
562 }
563 Err(e) => {
564 if e.to_string().contains("limit") {
565 println!("⚠️ Sailor warning: Command wind exceeded!");
566 println!(" ⛵ Change course to: https://cargo.do/checkout");
567 println!(" ⛵ Upgrade for unlimited sailing commands");
568 } else if e.to_string().contains("License not found") {
569 println!("❌ Sailor emergency: No navigation charts found!");
570 println!(" ⛵ Get charts with 'cm register <key>'");
571 } else {
572 println!(
573 "❌ Sailor distress: Command tracking failed: {}", e.to_string()
574 .red()
575 );
576 println!(" ⛵ Man overboard - secure the ship!");
577 }
578 Ok(false)
579 }
580 }
581}