fix: resolve CI compilation errors
Some checks failed
CI / test (push) Failing after 11m20s

This commit is contained in:
2026-02-05 15:17:53 +00:00
parent 38f2b0a0f5
commit 4e8d2b6d79

View File

@@ -0,0 +1,545 @@
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, Clear},
ExecutableCommand,
};
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span, Text},
widgets::{
Block, Borders, Cell, Gauge, List, ListItem, Paragraph, Row, Table, Tabs, Widget, Wrap,
},
Frame, Terminal,
};
use std::io::Stdout;
use std::path::PathBuf;
use crate::models::{AnalysisSummary, ByLanguage, ByPriority, Priority, TechDebtItem};
pub mod app;
pub use app::TuiApp;
#[derive(Debug, Clone, PartialEq)]
pub enum View {
Dashboard,
List,
Detail,
Export,
}
#[derive(Debug, Clone, PartialEq)]
enum SortOrder {
Priority,
File,
Line,
Keyword,
}
pub struct TuiState {
pub items: Vec<TechDebtItem>,
pub current_view: View,
pub selected_index: usize,
pub filter_text: String,
pub filter_priority: Option<Priority>,
pub sort_order: SortOrder,
pub show_help: bool,
pub path: PathBuf,
}
impl TuiState {
pub fn new(items: Vec<TechDebtItem>, path: PathBuf) -> Self {
Self {
items,
current_view: View::Dashboard,
selected_index: 0,
filter_text: String::new(),
filter_priority: None,
sort_order: SortOrder::Priority,
show_help: false,
path,
}
}
pub fn filtered_items(&self) -> Vec<&TechDebtItem> {
let mut items: Vec<&TechDebtItem> = self.items.iter().collect();
if !self.filter_text.is_empty() {
items.retain(|item| {
item.content.to_lowercase().contains(&self.filter_text.to_lowercase())
|| item
.location
.path
.to_string_lossy()
.to_lowercase()
.contains(&self.filter_text.to_lowercase())
});
}
if let Some(priority) = self.filter_priority {
items.retain(|item| item.priority == priority);
}
match self.sort_order {
SortOrder::Priority => items.sort_by(|a, b| b.priority.cmp(&a.priority)),
SortOrder::File => items.sort_by(|a, b| {
a.location
.path
.cmp(&b.location.path)
.then_with(|| a.location.line.cmp(&b.location.line))
}),
SortOrder::Line => items.sort_by(|a, b| a.location.line.cmp(&b.location.line)),
SortOrder::Keyword => items.sort_by(|a, b| a.keyword.cmp(&b.keyword)),
}
items
}
pub fn summary(&self) -> AnalysisSummary {
let by_priority = ByPriority::from_items(&self.items);
let by_language = ByLanguage::from_items(&self.items);
let complexity_distribution =
crate::models::ComplexityDistribution::from_items(&self.items);
AnalysisSummary {
total_items: self.items.len(),
by_priority,
by_language,
complexity_distribution,
}
}
}
fn draw_dashboard(f: &mut Frame<CrosstermBackend<Stdout>>, state: &TuiState, area: Rect) {
let summary = state.summary();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(10),
Constraint::Length(3),
])
.split(area);
let title = Paragraph::new("TECHDEBT TRACKER - Dashboard")
.style(Style::default().fg(Color::Cyan).bold());
f.render_widget(title, chunks[0]);
let main_content = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[1]);
let left_content = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(4),
Constraint::Length(4),
Constraint::Length(4),
Constraint::Min(0),
])
.split(main_content[0]);
let right_content = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(4),
Constraint::Length(4),
Constraint::Min(0),
])
.split(main_content[1]);
let priority_block = Block::default()
.title("Priority Breakdown")
.borders(Borders::ALL)
.style(Style::default().fg(Color::White));
f.render_widget(priority_block, left_content[0]);
let priority_content = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
])
.split(left_content[0].inner);
let critical_gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Red))
.label(&format!("Critical: {}", summary.by_priority.critical))
.ratio(if summary.total_items > 0 {
summary.by_priority.critical as f64 / summary.total_items as f64
} else {
0.0
});
f.render_widget(critical_gauge, priority_content[0]);
let high_gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Yellow))
.label(&format!("High: {}", summary.by_priority.high))
.ratio(if summary.total_items > 0 {
summary.by_priority.high as f64 / summary.total_items as f64
} else {
0.0
});
f.render_widget(high_gauge, priority_content[1]);
let medium_gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Blue))
.label(&format!("Medium: {}", summary.by_priority.medium))
.ratio(if summary.total_items > 0 {
summary.by_priority.medium as f64 / summary.total_items as f64
} else {
0.0
});
f.render_widget(medium_gauge, priority_content[2]);
let low_gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Green))
.label(&format!("Low: {}", summary.by_priority.low))
.ratio(if summary.total_items > 0 {
summary.by_priority.low as f64 / summary.total_items as f64
} else {
0.0
});
f.render_widget(low_gauge, priority_content[3]);
let total_block = Block::default()
.title("Total Items")
.borders(Borders::ALL)
.style(Style::default().fg(Color::White));
f.render_widget(total_block, left_content[1]);
let total_text = Paragraph::new(format!("{}", summary.total_items))
.style(Style::default().fg(Color::Cyan).bold());
f.render_widget(total_text, left_content[1].inner);
let complexity_block = Block::default()
.title("Complexity Distribution")
.borders(Borders::ALL)
.style(Style::default().fg(Color::White));
f.render_widget(complexity_block, left_content[2]);
let complexity_text = format!(
"Low: {} Medium: {} High: {} Critical: {}",
summary.complexity_distribution.low,
summary.complexity_distribution.medium,
summary.complexity_distribution.high,
summary.complexity_distribution.critical
);
let complexity_para = Paragraph::new(complexity_text);
f.render_widget(complexity_para, left_content[2].inner);
let language_block = Block::default()
.title("By Language")
.borders(Borders::ALL)
.style(Style::default().fg(Color::White));
f.render_widget(language_block, right_content[0]);
let lang_rows: Vec<Row> = summary
.by_language
.items
.iter()
.take(5)
.map(|lang| {
Row::new(vec![
Cell::from(lang.language.clone()),
Cell::from(lang.count.to_string()),
])
})
.collect();
let lang_table = Table::new(lang_rows)
.widths(&[Constraint::Percentage(60), Constraint::Percentage(40)])
.column_spacing(1);
f.render_widget(lang_table, right_content[0].inner);
let help_block = Block::default()
.title("Navigation Help")
.borders(Borders::ALL)
.style(Style::default().fg(Color::White));
f.render_widget(help_block, right_content[1]);
let help_text = "↑↓: Navigate | Tab: Switch View | q: Quit | /: Filter";
let help_para = Paragraph::new(help_text);
f.render_widget(help_para, right_content[1].inner);
let status_bar = Line::from(vec![
Span::styled(" Press ", Style::default().fg(Color::Gray)),
Span::styled("TAB", Style::default().fg(Color::Yellow).bold()),
Span::styled(" to view items | ", Style::default().fg(Color::Gray)),
Span::styled("q", Style::default().fg(Color::Yellow).bold()),
Span::styled(" to quit", Style::default().fg(Color::Gray)),
]);
f.render_widget(status_bar, chunks[2]);
}
fn draw_list(f: &mut Frame<CrosstermBackend<Stdout>>, state: &TuiState, area: Rect) {
let filtered = state.filtered_items();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(10),
Constraint::Length(3),
])
.split(area);
let filter_text = if state.filter_text.is_empty() {
format!("Filter ({} items) - Press / to filter", filtered.len())
} else {
format!(
"Filter: {} - {} results (Press / to edit, ESC to clear)",
state.filter_text,
filtered.len()
)
};
let filter_block = Paragraph::new(filter_text)
.style(Style::default().fg(Color::Yellow));
f.render_widget(filter_block, chunks[0]);
let list_items: Vec<ListItem> = filtered
.iter()
.enumerate()
.skip(state.selected_index.saturating_sub(10))
.take(chunks[1].height as usize - 2)
.map(|(idx, item)| {
let priority_color = match item.priority {
Priority::Critical => Color::Red,
Priority::High => Color::Yellow,
Priority::Medium => Color::Blue,
Priority::Low => Color::Green,
};
let content = format!(
"[{}] {}:{} - {} ({}/10) - {}",
item.keyword,
item.location.path.file_name().unwrap_or_default().to_string_lossy(),
item.location.line,
item.content.chars().take(40).collect::<String>(),
item.complexity_score,
item.metadata.language
);
let style = if *idx == state.selected_index {
Style::default()
.fg(priority_color)
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(priority_color)
};
ListItem::new(content).style(style)
})
.collect();
let list = List::new(list_items).block(
Block::default()
.title(format!("Items ({})", filtered.len()))
.borders(Borders::ALL),
);
f.render_widget(list, chunks[1]);
let status_bar = Line::from(vec![
Span::styled(" ↑↓: Navigate | ", Style::default().fg(Color::Gray)),
Span::styled("ENTER", Style::default().fg(Color::Yellow).bold()),
Span::styled(": View Detail | ", Style::default().fg(Color::Gray)),
Span::styled("TAB", Style::default().fg(Color::Yellow).bold()),
Span::styled(": Dashboard | ", Style::default().fg(Color::Gray)),
Span::styled("f", Style::default().fg(Color::Yellow).bold()),
Span::styled(": Filter | ", Style::default().fg(Color::Gray)),
Span::styled("s", Style::default().fg(Color::Yellow).bold()),
Span::styled(": Sort | ", Style::default().fg(Color::Gray)),
Span::styled("q", Style::default().fg(Color::Yellow).bold()),
Span::styled(": Quit", Style::default().fg(Color::Gray)),
]);
f.render_widget(status_bar, chunks[2]);
}
fn draw_detail(f: &mut Frame<CrosstermBackend<Stdout>>, state: &TuiState, area: Rect) {
let filtered = state.filtered_items();
if filtered.is_empty() {
let para = Paragraph::new("No items selected");
f.render_widget(para, area);
return;
}
let item = if let Some(item) = filtered.get(state.selected_index) {
item
} else {
state.selected_index = 0;
if let Some(item) = filtered.get(0) {
item
} else {
return;
}
};
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(5),
Constraint::Length(3),
])
.split(area);
let title = format!(
"Detail - {} at {}:{}",
item.keyword,
item.location.path.display(),
item.location.line
);
let title_block = Paragraph::new(title)
.style(Style::default().fg(Color::Cyan).bold());
f.render_widget(title_block, chunks[0]);
let detail_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[1]);
let left_details = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(2),
Constraint::Length(2),
Constraint::Length(2),
Constraint::Length(2),
Constraint::Min(0),
])
.split(detail_chunks[0]);
let priority_color = match item.priority {
Priority::Critical => Color::Red,
Priority::High => Color::Yellow,
Priority::Medium => Color::Blue,
Priority::Low => Color::Green,
};
let priority_line = Line::from(vec![
Span::styled("Priority: ", Style::default().fg(Color::Gray)),
Span::styled(item.priority.as_str(), Style::default().fg(priority_color).bold()),
]);
f.render_widget(priority_line, left_details[0]);
let complexity_line = Line::from(vec![
Span::styled("Complexity: ", Style::default().fg(Color::Gray)),
Span::styled(
format!("{}/10", item.complexity_score),
Style::default()
.fg(match item.complexity_score {
1..=3 => Color::Green,
4..=6 => Color::Yellow,
7..=8 => Color::Red,
_ => Color::Magenta,
})
.bold(),
),
]);
f.render_widget(complexity_line, left_details[1]);
let language_line = Line::from(vec![
Span::styled("Language: ", Style::default().fg(Color::Gray)),
Span::styled(&item.metadata.language, Style::default().fg(Color::Cyan)),
]);
f.render_widget(language_line, left_details[2]);
let type_line = Line::from(vec![
Span::styled("Type: ", Style::default().fg(Color::Gray)),
Span::styled(
format!("{:?}", item.comment_type),
Style::default().fg(Color::Blue),
),
]);
f.render_widget(type_line, left_details[3]);
let content_block = Block::default().title("Content").borders(Borders::ALL);
f.render_widget(content_block, left_details[4]);
let content_para = Paragraph::new(&item.content)
.wrap(Wrap { trim: true });
f.render_widget(content_para, left_details[4].inner);
let right_details = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(2),
Constraint::Length(2),
Constraint::Length(2),
Constraint::Min(0),
])
.split(detail_chunks[1]);
let path_line = Line::from(vec![
Span::styled("File: ", Style::default().fg(Color::Gray)),
Span::styled(
item.location.path.display().to_string(),
Style::default().fg(Color::White),
),
]);
f.render_widget(path_line, right_details[0]);
let location_line = Line::from(vec![
Span::styled("Line: ", Style::default().fg(Color::Gray)),
Span::styled(
item.location.line.to_string(),
Style::default().fg(Color::White),
),
Span::styled(" | Column: ", Style::default().fg(Color::Gray)),
Span::styled(
item.location.column.to_string(),
Style::default().fg(Color::White),
),
]);
f.render_widget(location_line, right_details[1]);
let question_line = Line::from(vec![
Span::styled("Contains Question: ", Style::default().fg(Color::Gray)),
Span::styled(
if item.metadata.is_question { "Yes" } else { "No" },
Style::default().fg(if item.metadata.is_question {
Color::Yellow
} else {
Color::Green
}),
),
]);
f.render_widget(question_line, right_details[2]);
let word_count_line = Line::from(vec![
Span::styled("Word Count: ", Style::default().fg(Color::Gray)),
Span::styled(item.metadata.word_count.to_string(), Style::default().fg(Color::White)),
]);
f.render_widget(word_count_line, right_details[3]);
let status_bar = Line::from(vec![
Span::styled(" ←: Back | ", Style::default().fg(Color::Gray)),
Span::styled("e", Style::default().fg(Color::Yellow).bold()),
Span::styled(": Export | ", Style::default().fg(Color::Gray)),
Span::styled("q", Style::default().fg(Color::Yellow).bold()),
Span::styled(": Quit", Style::default().fg(Color::Gray)),
]);
f.render_widget(status_bar, chunks[2]);
}
pub fn render(f: &mut Frame<CrosstermBackend<Stdout>>, state: &TuiState) {
let area = f.size();
let block = Block::default()
.style(Style::default().bg(Color::Black))
.borders(Borders::NONE);
f.render_widget(block, area);
match state.current_view {
View::Dashboard => draw_dashboard(f, state, area),
View::List => draw_list(f, state, area),
View::Detail => draw_detail(f, state, area),
View::Export => {
let para = Paragraph::new("Export feature - Coming soon!");
f.render_widget(para, area);
}
}
}