use crate::analyzer::{AnalysisReport, MudaType};
use crate::ptx::{BugSeverity, PtxBugAnalyzer, PtxBugReport};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{
Block, Borders, List, ListItem, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
},
Frame, Terminal,
};
use std::io::{self, Stdout};
pub struct TuiApp {
pub ptx_source: String,
pub report: AnalysisReport,
pub bug_report: PtxBugReport,
pub source_scroll: u16,
pub sidebar_visible: bool,
pub should_quit: bool,
source_lines: usize,
}
impl TuiApp {
#[must_use]
pub fn new(ptx_source: String, report: AnalysisReport) -> Self {
let source_lines = ptx_source.lines().count();
let bug_report = PtxBugAnalyzer::strict().analyze(&ptx_source);
Self {
ptx_source,
report,
bug_report,
source_scroll: 0,
sidebar_visible: true,
should_quit: false,
source_lines,
}
}
pub fn handle_key(&mut self, key: KeyCode) {
match key {
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
KeyCode::Char('s') => self.sidebar_visible = !self.sidebar_visible,
KeyCode::Down | KeyCode::Char('j') => self.scroll_down(),
KeyCode::Up | KeyCode::Char('k') => self.scroll_up(),
KeyCode::PageDown => self.page_down(),
KeyCode::PageUp => self.page_up(),
KeyCode::Home => self.source_scroll = 0,
KeyCode::End => self.scroll_to_end(),
_ => {}
}
}
fn scroll_down(&mut self) {
if (self.source_scroll as usize) < self.source_lines.saturating_sub(1) {
self.source_scroll = self.source_scroll.saturating_add(1);
}
}
fn scroll_up(&mut self) {
self.source_scroll = self.source_scroll.saturating_sub(1);
}
fn page_down(&mut self) {
self.source_scroll = self
.source_scroll
.saturating_add(20)
.min(self.source_lines.saturating_sub(1) as u16);
}
fn page_up(&mut self) {
self.source_scroll = self.source_scroll.saturating_sub(20);
}
fn scroll_to_end(&mut self) {
self.source_scroll = self.source_lines.saturating_sub(1) as u16;
}
}
pub fn run_tui(ptx_source: String, report: AnalysisReport) -> io::Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = TuiApp::new(ptx_source, report);
let result = run_app(&mut terminal, &mut app);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
result
}
fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>, app: &mut TuiApp) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
app.handle_key(key.code);
}
}
if app.should_quit {
return Ok(());
}
}
}
fn ui(frame: &mut Frame<'_>, app: &TuiApp) {
let size = frame.area();
let main_chunks = if app.sidebar_visible {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
.split(size)
} else {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(100)])
.split(size)
};
render_source_pane(frame, app, main_chunks[0]);
if app.sidebar_visible && main_chunks.len() > 1 {
render_sidebar(frame, app, main_chunks[1]);
}
}
fn render_source_pane(frame: &mut Frame<'_>, app: &TuiApp, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(3), Constraint::Length(3)])
.split(area);
let lines: Vec<Line<'_>> = app
.ptx_source
.lines()
.enumerate()
.map(|(i, line)| {
let line_num = format!("{:4} ", i + 1);
let highlighted = highlight_ptx_line(line);
Line::from(vec![
Span::styled(line_num, Style::default().fg(Color::DarkGray)),
highlighted,
])
})
.collect();
let source_block = Block::default()
.title(format!(" PTX: {} ", app.report.name))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan));
let paragraph = Paragraph::new(lines)
.block(source_block)
.scroll((app.source_scroll, 0));
frame.render_widget(paragraph, chunks[0]);
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓"));
let mut scrollbar_state =
ScrollbarState::new(app.source_lines).position(app.source_scroll as usize);
frame.render_stateful_widget(scrollbar, chunks[0], &mut scrollbar_state);
let status = Line::from(vec![
Span::styled(
" q",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw(":Quit "),
Span::styled(
"s",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw(":Sidebar "),
Span::styled(
"↑↓",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw(":Scroll "),
Span::styled(
"PgUp/Dn",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw(":Page "),
]);
let status_block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray));
let status_para = Paragraph::new(status).block(status_block);
frame.render_widget(status_para, chunks[1]);
}
fn render_sidebar(frame: &mut Frame<'_>, app: &TuiApp, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(8), Constraint::Length(6), Constraint::Length(5), Constraint::Length(6), Constraint::Min(4), ])
.split(area);
render_register_widget(frame, app, chunks[0]);
render_memory_widget(frame, app, chunks[1]);
render_roofline_widget(frame, app, chunks[2]);
render_bugs_widget(frame, app, chunks[3]);
render_warnings_widget(frame, app, chunks[4]);
}
fn render_register_widget(frame: &mut Frame<'_>, app: &TuiApp, area: Rect) {
let regs = &app.report.registers;
let total = regs.total();
let occupancy = app.report.estimated_occupancy;
let status_color = if total < 64 {
Color::Green
} else if total < 128 {
Color::Yellow
} else {
Color::Red
};
let items = vec![
ListItem::new(format!(".f32: {:3} / 255", regs.f32_regs)),
ListItem::new(format!(".b32: {:3} / 255", regs.b32_regs)),
ListItem::new(format!(".b64: {:3} / 255", regs.b64_regs)),
ListItem::new(format!(".pred: {:2} / 8", regs.pred_regs)), ListItem::new(Line::from(vec![
Span::raw(format!("Total: {} → ", total)),
Span::styled(
format!("{:.0}% occ", occupancy * 100.0),
Style::default()
.fg(status_color)
.add_modifier(Modifier::BOLD),
),
])),
];
let block = Block::default()
.title(" Registers ")
.borders(Borders::ALL)
.border_style(Style::default().fg(status_color));
let list = List::new(items).block(block);
frame.render_widget(list, area);
}
fn render_memory_widget(frame: &mut Frame<'_>, app: &TuiApp, area: Rect) {
let mem = &app.report.memory;
let coal_pct = mem.coalesced_ratio * 100.0;
let status_color = if coal_pct >= 90.0 {
Color::Green
} else if coal_pct >= 70.0 {
Color::Yellow
} else {
Color::Red
};
let items = vec![
ListItem::new(format!("Global ld: {}", mem.global_loads)),
ListItem::new(format!("Global st: {}", mem.global_stores)),
ListItem::new(Line::from(vec![
Span::raw("Coalesced: "),
Span::styled(
format!("{:.1}%", coal_pct),
Style::default()
.fg(status_color)
.add_modifier(Modifier::BOLD),
),
])),
];
let block = Block::default()
.title(" Memory ")
.borders(Borders::ALL)
.border_style(Style::default().fg(status_color));
let list = List::new(items).block(block);
frame.render_widget(list, area);
}
fn render_roofline_widget(frame: &mut Frame<'_>, app: &TuiApp, area: Rect) {
let roof = &app.report.roofline;
let bound_text = if roof.memory_bound {
Span::styled("Memory-bound", Style::default().fg(Color::Yellow))
} else {
Span::styled("Compute-bound", Style::default().fg(Color::Green))
};
let items = vec![
ListItem::new(format!("AI: {:.2} FLOP/B", roof.arithmetic_intensity)),
ListItem::new(Line::from(vec![Span::raw("Bottleneck: "), bound_text])),
];
let block = Block::default()
.title(" Roofline ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Blue));
let list = List::new(items).block(block);
frame.render_widget(list, area);
}
fn render_bugs_widget(frame: &mut Frame<'_>, app: &TuiApp, area: Rect) {
let bug_report = &app.bug_report;
let critical = bug_report.count_by_severity(BugSeverity::Critical);
let high = bug_report.count_by_severity(BugSeverity::High);
let medium = bug_report.count_by_severity(BugSeverity::Medium);
let status_color = if critical > 0 {
Color::Red
} else if high > 0 {
Color::Yellow
} else {
Color::Green
};
let items = if bug_report.bugs.is_empty() {
vec![ListItem::new(Line::from(vec![
Span::styled("✓ ", Style::default().fg(Color::Green)),
Span::raw("No bugs detected"),
]))]
} else {
vec![
ListItem::new(Line::from(vec![Span::styled(
format!("P0 Critical: {}", critical),
Style::default().fg(if critical > 0 {
Color::Red
} else {
Color::Green
}),
)])),
ListItem::new(Line::from(vec![Span::styled(
format!("P1 High: {}", high),
Style::default().fg(if high > 0 {
Color::Yellow
} else {
Color::Green
}),
)])),
ListItem::new(Line::from(vec![Span::styled(
format!("P2 Medium: {}", medium),
Style::default().fg(if medium > 0 {
Color::Blue
} else {
Color::Green
}),
)])),
]
};
let block = Block::default()
.title(" Bug Hunt ")
.borders(Borders::ALL)
.border_style(Style::default().fg(status_color));
let list = List::new(items).block(block);
frame.render_widget(list, area);
}
fn render_warnings_widget(frame: &mut Frame<'_>, app: &TuiApp, area: Rect) {
let items: Vec<ListItem<'_>> = if app.report.warnings.is_empty() {
vec![ListItem::new(Line::from(vec![
Span::styled("✓ ", Style::default().fg(Color::Green)),
Span::raw("No Muda detected"),
]))]
} else {
app.report
.warnings
.iter()
.map(|w| {
let icon = match w.muda_type {
MudaType::Transport => ("⚠ ", Color::Yellow),
MudaType::Waiting => ("⏳", Color::Yellow),
MudaType::Overprocessing => ("🔄", Color::Yellow),
};
ListItem::new(Line::from(vec![
Span::styled(icon.0, Style::default().fg(icon.1)),
Span::raw(&w.description),
]))
})
.collect()
};
let border_color = if app.report.warnings.is_empty() {
Color::Green
} else {
Color::Yellow
};
let block = Block::default()
.title(" Muda (Waste) ")
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color));
let list = List::new(items).block(block);
frame.render_widget(list, area);
}
fn highlight_ptx_line(line: &str) -> Span<'static> {
let line = line.to_string();
let trimmed = line.trim();
if trimmed.starts_with("//") {
return Span::styled(line, Style::default().fg(Color::DarkGray));
}
if trimmed.starts_with('.') {
return Span::styled(line, Style::default().fg(Color::Magenta));
}
if trimmed.ends_with(':') && !trimmed.contains(' ') {
return Span::styled(line, Style::default().fg(Color::Cyan));
}
if line.starts_with('\t') || line.starts_with(" ") {
if trimmed.starts_with("ld.") || trimmed.starts_with("st.") {
return Span::styled(line, Style::default().fg(Color::Yellow));
}
if trimmed.starts_with("add")
|| trimmed.starts_with("sub")
|| trimmed.starts_with("mul")
|| trimmed.starts_with("mad")
|| trimmed.starts_with("fma")
{
return Span::styled(line, Style::default().fg(Color::Green));
}
if trimmed.starts_with("bra") || trimmed.starts_with("ret") || trimmed.starts_with("setp") {
return Span::styled(line, Style::default().fg(Color::Red));
}
}
Span::raw(line)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analyzer::{MemoryPattern, RegisterUsage, RooflineMetric};
fn sample_report() -> AnalysisReport {
AnalysisReport {
name: "test_kernel".to_string(),
target: "PTX".to_string(),
registers: RegisterUsage {
f32_regs: 24,
b32_regs: 18,
b64_regs: 12,
pred_regs: 4,
..Default::default()
},
memory: MemoryPattern {
global_loads: 100,
global_stores: 50,
coalesced_ratio: 0.95,
..Default::default()
},
roofline: RooflineMetric {
arithmetic_intensity: 2.5,
theoretical_peak_gflops: 15000.0,
memory_bound: true,
},
warnings: vec![],
instruction_count: 150,
estimated_occupancy: 0.875,
}
}
#[test]
fn f026_tui_app_creation() {
let ptx = ".entry test() { ret; }".to_string();
let report = sample_report();
let app = TuiApp::new(ptx, report);
assert!(!app.should_quit);
}
#[test]
fn f027_resize_terminal() {
let ptx = (0..50)
.map(|i| format!(" add.f32 %f{}, %f{}, %f{}", i, i, i + 1))
.collect::<Vec<_>>()
.join("\n");
let report = sample_report();
let mut app = TuiApp::new(ptx, report);
for _ in 0..25 {
app.handle_key(KeyCode::Down);
}
let scroll_before = app.source_scroll;
assert_eq!(app.source_scroll, scroll_before);
assert!(!app.should_quit);
app.handle_key(KeyCode::Down);
assert_eq!(app.source_scroll, scroll_before + 1);
}
#[test]
fn f029_toggle_sidebar() {
let ptx = ".entry test() { ret; }".to_string();
let report = sample_report();
let mut app = TuiApp::new(ptx, report);
assert!(app.sidebar_visible);
app.handle_key(KeyCode::Char('s'));
assert!(!app.sidebar_visible);
app.handle_key(KeyCode::Char('s'));
assert!(app.sidebar_visible);
}
#[test]
fn f030_quit_tui() {
let ptx = ".entry test() { ret; }".to_string();
let report = sample_report();
let mut app = TuiApp::new(ptx, report);
assert!(!app.should_quit);
app.handle_key(KeyCode::Char('q'));
assert!(app.should_quit);
}
#[test]
fn test_scroll_down() {
let ptx = "line1\nline2\nline3\nline4\nline5".to_string();
let report = sample_report();
let mut app = TuiApp::new(ptx, report);
assert_eq!(app.source_scroll, 0);
app.handle_key(KeyCode::Down);
assert_eq!(app.source_scroll, 1);
app.handle_key(KeyCode::Char('j'));
assert_eq!(app.source_scroll, 2);
}
#[test]
fn test_scroll_up() {
let ptx = "line1\nline2\nline3".to_string();
let report = sample_report();
let mut app = TuiApp::new(ptx, report);
app.source_scroll = 2;
app.handle_key(KeyCode::Up);
assert_eq!(app.source_scroll, 1);
app.handle_key(KeyCode::Char('k'));
assert_eq!(app.source_scroll, 0);
}
#[test]
fn test_scroll_bounds() {
let ptx = "line1\nline2".to_string();
let report = sample_report();
let mut app = TuiApp::new(ptx, report);
app.source_scroll = 1;
app.handle_key(KeyCode::Down);
assert_eq!(app.source_scroll, 1);
app.source_scroll = 0;
app.handle_key(KeyCode::Up);
assert_eq!(app.source_scroll, 0);
}
#[test]
fn test_page_navigation() {
let ptx = (0..100)
.map(|i| format!("line{}", i))
.collect::<Vec<_>>()
.join("\n");
let report = sample_report();
let mut app = TuiApp::new(ptx, report);
app.handle_key(KeyCode::PageDown);
assert_eq!(app.source_scroll, 20);
app.handle_key(KeyCode::PageUp);
assert_eq!(app.source_scroll, 0);
}
#[test]
fn test_home_end() {
let ptx = (0..50)
.map(|i| format!("line{}", i))
.collect::<Vec<_>>()
.join("\n");
let report = sample_report();
let mut app = TuiApp::new(ptx, report);
app.handle_key(KeyCode::End);
assert_eq!(app.source_scroll, 49);
app.handle_key(KeyCode::Home);
assert_eq!(app.source_scroll, 0);
}
#[test]
fn test_highlight_ptx_comment() {
let span = highlight_ptx_line("// This is a comment");
assert_eq!(span.style.fg, Some(Color::DarkGray));
}
#[test]
fn test_highlight_ptx_directive() {
let span = highlight_ptx_line(".entry test()");
assert_eq!(span.style.fg, Some(Color::Magenta));
}
#[test]
fn test_highlight_ptx_memory() {
let span = highlight_ptx_line(" ld.global.f32 %f1, [%rd1]");
assert_eq!(span.style.fg, Some(Color::Yellow));
}
#[test]
fn test_highlight_ptx_arithmetic() {
let span = highlight_ptx_line(" add.f32 %f1, %f2, %f3");
assert_eq!(span.style.fg, Some(Color::Green));
}
#[test]
fn test_highlight_ptx_control() {
let span = highlight_ptx_line(" ret;");
assert_eq!(span.style.fg, Some(Color::Red));
}
#[test]
fn f028_sync_scroll_source_asm() {
let ptx = (0..100)
.map(|i| format!(" add.f32 %f{}, %f{}, %f{}", i, i, i + 1))
.collect::<Vec<_>>()
.join("\n");
let report = sample_report();
let mut app = TuiApp::new(ptx, report);
assert_eq!(app.source_scroll, 0);
for i in 1..=10 {
app.handle_key(KeyCode::Down);
assert_eq!(app.source_scroll, i, "Scroll position should update");
}
assert_eq!(app.source_scroll, 10, "Source/ASM should be at position 10");
app.handle_key(KeyCode::PageUp);
assert_eq!(app.source_scroll, 0, "Should scroll back to top");
}
}