use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
prelude::*,
style::{Color, Modifier, Style},
widgets::{Block, Borders, Clear, Paragraph},
};
use crate::app::{App, Screen};
use crate::hardware::{check_multi_gpu_passthrough_status, LookingGlassConfig};
pub fn render(app: &App, frame: &mut Frame) {
let area = frame.area();
let dialog_width = 72.min(area.width.saturating_sub(4));
let dialog_height = 24.min(area.height.saturating_sub(4));
let dialog_area = centered_rect(dialog_width, dialog_height, area);
frame.render_widget(Clear, dialog_area);
let block = Block::default()
.title(" Multi-GPU Passthrough Setup ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan))
.style(Style::default().bg(Color::Black));
let inner = block.inner(dialog_area);
frame.render_widget(block, dialog_area);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([
Constraint::Length(6), Constraint::Length(1), Constraint::Length(5), Constraint::Length(1), Constraint::Length(4), Constraint::Min(1), Constraint::Length(2), ])
.split(inner);
render_system_status(frame, chunks[0]);
let sep1 = Paragraph::new("─".repeat(chunks[1].width as usize))
.style(Style::default().fg(Color::DarkGray));
frame.render_widget(sep1, chunks[1]);
render_gpu_info(app, frame, chunks[2]);
let sep2 = Paragraph::new("─".repeat(chunks[3].width as usize))
.style(Style::default().fg(Color::DarkGray));
frame.render_widget(sep2, chunks[3]);
render_looking_glass_config(app, frame, chunks[4]);
render_help(frame, chunks[6]);
}
fn render_system_status(frame: &mut Frame, area: Rect) {
let status = check_multi_gpu_passthrough_status();
let mut lines = Vec::new();
lines.push(Line::styled(
"System Requirements:",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
));
let iommu_icon = if status.iommu_enabled { "[+]" } else { "[-]" };
let iommu_style = if status.iommu_enabled {
Color::Green
} else {
Color::Red
};
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(iommu_icon, Style::default().fg(iommu_style)),
Span::raw(" IOMMU enabled (required for GPU passthrough)"),
]));
let vfio_icon = if status.vfio_loaded { "[+]" } else { "[-]" };
let vfio_style = if status.vfio_loaded {
Color::Green
} else {
Color::Red
};
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(vfio_icon, Style::default().fg(vfio_style)),
Span::raw(" VFIO modules loaded"),
]));
let gpu_ok = status.available_gpus > 0;
let gpu_icon = if gpu_ok { "[+]" } else { "[-]" };
let gpu_style = if gpu_ok { Color::Green } else { Color::Red };
let gpu_text = format!(
" {} GPU{} available for passthrough",
status.available_gpus,
if status.available_gpus == 1 { "" } else { "s" }
);
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(gpu_icon, Style::default().fg(gpu_style)),
Span::raw(gpu_text),
]));
let lg_client = LookingGlassConfig::find_client();
let lg_ok = lg_client.is_some();
let lg_icon = if lg_ok { "[+]" } else { "[-]" };
let lg_style = if lg_ok { Color::Green } else { Color::Yellow };
let lg_text = if let Some(ref path) = lg_client {
format!(" Looking Glass client: {}", path.display())
} else {
" Looking Glass client not found (optional)".to_string()
};
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(lg_icon, Style::default().fg(lg_style)),
Span::raw(lg_text),
]));
let para = Paragraph::new(lines);
frame.render_widget(para, area);
}
fn render_gpu_info(app: &App, frame: &mut Frame, area: Rect) {
let mut lines = Vec::new();
lines.push(Line::styled(
"Selected GPUs for Passthrough:",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
));
let selected_gpus: Vec<_> = app
.selected_pci_devices
.iter()
.filter_map(|&idx| app.pci_devices.get(idx))
.filter(|d| d.is_gpu())
.collect();
if selected_gpus.is_empty() {
lines.push(Line::styled(
" No GPUs selected for passthrough",
Style::default().fg(Color::Yellow),
));
lines.push(Line::styled(
" Use PCI Passthrough to select a GPU",
Style::default().fg(Color::DarkGray),
));
} else {
for gpu in selected_gpus {
let vendor_color = if gpu.is_nvidia() {
Color::Green
} else if gpu.is_amd() {
Color::Red
} else {
Color::White
};
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
format!("{} - ", gpu.address),
Style::default().fg(Color::DarkGray),
),
Span::styled(&gpu.device_name, Style::default().fg(vendor_color)),
]));
}
}
let para = Paragraph::new(lines);
frame.render_widget(para, area);
}
fn render_looking_glass_config(app: &App, frame: &mut Frame, area: Rect) {
let mut lines = Vec::new();
lines.push(Line::styled(
"Looking Glass Configuration:",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
));
lines.push(Line::from(vec![
Span::raw(" IVSHMEM Size: "),
Span::styled(
format!("{}MB", app.config.default_ivshmem_size_mb),
Style::default().fg(Color::White),
),
Span::styled(
" (configure in Settings)",
Style::default().fg(Color::DarkGray),
),
]));
let auto_launch = if app.config.looking_glass_auto_launch {
"Yes"
} else {
"No"
};
lines.push(Line::from(vec![
Span::raw(" Auto-launch client: "),
Span::styled(auto_launch, Style::default().fg(Color::White)),
]));
let para = Paragraph::new(lines);
frame.render_widget(para, area);
}
fn render_help(frame: &mut Frame, area: Rect) {
let help = Paragraph::new("[p] PCI Passthrough [s] Settings [Esc] Back")
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center);
frame.render_widget(help, area);
}
pub fn handle_input(app: &mut App, key: KeyEvent) -> anyhow::Result<()> {
match key.code {
KeyCode::Esc => {
app.selected_menu_item = 0; app.pop_screen();
}
KeyCode::Char('p') | KeyCode::Char('P') => {
app.load_pci_devices()?;
app.restore_pci_selections();
app.selected_menu_item = 0;
app.push_screen(Screen::PciPassthrough);
}
KeyCode::Char('s') | KeyCode::Char('S') => {
app.push_screen(Screen::Settings);
}
_ => {}
}
Ok(())
}
fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
let x = area.x + (area.width.saturating_sub(width)) / 2;
let y = area.y + (area.height.saturating_sub(height)) / 2;
Rect::new(x, y, width, height)
}