use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
prelude::*,
widgets::{Block, Borders, Clear, Paragraph, Wrap},
};
use crate::app::App;
use crate::vm::QemuConfig;
pub fn render(app: &App, frame: &mut Frame) {
let area = frame.area();
let dialog_width = 70.min(area.width.saturating_sub(4));
let dialog_height = 30.min(area.height.saturating_sub(4));
let dialog_area = centered_rect(dialog_width, dialog_height, area);
frame.render_widget(Clear, dialog_area);
let vm_name = app.selected_vm()
.map(|vm| vm.display_name())
.unwrap_or_else(|| "Unknown".to_string());
let block = Block::default()
.title(format!(" {} - Configuration ", vm_name))
.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 h_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(2), Constraint::Min(1), Constraint::Length(2), ])
.split(inner);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Min(10), Constraint::Length(1), Constraint::Length(2), ])
.split(h_chunks[1]);
if let Some(vm) = app.selected_vm() {
render_config(&vm.config, chunks[1], frame);
} else {
let msg = Paragraph::new("No VM selected")
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center);
frame.render_widget(msg, chunks[1]);
}
let help = Paragraph::new("[r] View raw script [Esc] Back")
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center);
frame.render_widget(help, chunks[3]);
}
fn render_config(config: &QemuConfig, area: Rect, frame: &mut Frame) {
let mut lines = Vec::new();
lines.push(Line::from(vec![
Span::styled("Emulator: ", Style::default().fg(Color::Yellow)),
Span::raw(config.emulator.command()),
]));
lines.push(Line::from(vec![
Span::styled("Architecture: ", Style::default().fg(Color::Yellow)),
Span::raw(config.emulator.architecture()),
]));
lines.push(Line::from(vec![
Span::styled("Memory: ", Style::default().fg(Color::Yellow)),
Span::raw(format!("{} MB", config.memory_mb)),
]));
lines.push(Line::from(vec![
Span::styled("CPU Cores: ", Style::default().fg(Color::Yellow)),
Span::raw(format!("{}", config.cpu_cores)),
]));
if let Some(ref model) = config.cpu_model {
lines.push(Line::from(vec![
Span::styled("CPU Model: ", Style::default().fg(Color::Yellow)),
Span::raw(model.clone()),
]));
}
if let Some(ref machine) = config.machine {
lines.push(Line::from(vec![
Span::styled("Machine: ", Style::default().fg(Color::Yellow)),
Span::raw(machine.clone()),
]));
}
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled("VGA: ", Style::default().fg(Color::Yellow)),
Span::raw(format!("{:?}", config.vga)),
]));
if !config.audio_devices.is_empty() {
let audio_str = config.audio_devices
.iter()
.map(|a| format!("{:?}", a))
.collect::<Vec<_>>()
.join(", ");
lines.push(Line::from(vec![
Span::styled("Audio: ", Style::default().fg(Color::Yellow)),
Span::raw(audio_str),
]));
}
if let Some(ref net) = config.network {
let backend_str = match &net.backend {
crate::vm::qemu_config::NetworkBackend::User => "user/SLIRP (NAT)".to_string(),
crate::vm::qemu_config::NetworkBackend::Passt => "passt".to_string(),
crate::vm::qemu_config::NetworkBackend::Bridge(name) => format!("bridge: {}", name),
crate::vm::qemu_config::NetworkBackend::None => "none".to_string(),
};
lines.push(Line::from(vec![
Span::styled("Network: ", Style::default().fg(Color::Yellow)),
Span::raw(format!("{} ({})", net.model, backend_str)),
]));
if !net.port_forwards.is_empty() {
lines.push(Line::from(Span::styled(
" Forwarded ports:",
Style::default().fg(Color::DarkGray),
)));
for pf in &net.port_forwards {
lines.push(Line::from(format!(" {} {} -> {}", pf.protocol, pf.host_port, pf.guest_port)));
}
}
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
"Disks:",
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
)));
for disk in &config.disks {
let path = disk.path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
lines.push(Line::from(format!(
" {} ({:?}, {})",
path, disk.format, disk.interface
)));
}
lines.push(Line::from(""));
let mut features = Vec::new();
if config.enable_kvm {
features.push("KVM");
}
if config.uefi {
features.push("UEFI");
}
if config.tpm {
features.push("TPM");
}
if !features.is_empty() {
lines.push(Line::from(vec![
Span::styled("Features: ", Style::default().fg(Color::Yellow)),
Span::raw(features.join(", ")),
]));
}
let snapshot_support = if config.supports_snapshots() {
Span::styled("Yes", Style::default().fg(Color::Green))
} else {
Span::styled("No (raw disk)", Style::default().fg(Color::Red))
};
lines.push(Line::from(vec![
Span::styled("Snapshots: ", Style::default().fg(Color::Yellow)),
snapshot_support,
]));
let para = Paragraph::new(lines)
.wrap(Wrap { trim: false });
frame.render_widget(para, area);
}
pub fn render_raw_script(app: &App, frame: &mut Frame) {
let area = frame.area();
let dialog_width = 90.min(area.width.saturating_sub(4));
let dialog_height = 40.min(area.height.saturating_sub(4));
let dialog_area = centered_rect(dialog_width, dialog_height, area);
frame.render_widget(Clear, dialog_area);
let vm_name = app.selected_vm()
.map(|vm| vm.display_name())
.unwrap_or_else(|| "Unknown".to_string());
let modified_indicator = if app.script_editor_modified { " [modified]" } else { "" };
let block = Block::default()
.title(format!(" {} - launch.sh{} ", vm_name, modified_indicator))
.borders(Borders::ALL)
.border_style(if app.script_editor_modified {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::Cyan)
})
.style(Style::default().bg(Color::Black));
let inner = block.inner(dialog_area);
frame.render_widget(block, dialog_area);
let v_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(1), Constraint::Length(1), ])
.split(inner);
let editor_area = v_chunks[0];
let help_area = v_chunks[1];
let h_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(5), Constraint::Min(1), ])
.split(editor_area);
let line_num_area = h_chunks[0];
let text_area = h_chunks[1];
let visible_height = text_area.height as usize;
let total_lines = app.script_editor_lines.len();
let scroll_offset = app.raw_script_scroll as usize;
let start_line = scroll_offset;
let end_line = (scroll_offset + visible_height).min(total_lines);
let line_numbers: Vec<Line> = (start_line..end_line)
.map(|i| {
let style = if i == app.script_editor_cursor.0 {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::DarkGray)
};
Line::styled(format!("{:4} ", i + 1), style)
})
.collect();
let line_nums_widget = Paragraph::new(line_numbers);
frame.render_widget(line_nums_widget, line_num_area);
let text_width = text_area.width as usize;
let h_scroll = app.script_editor_h_scroll;
let text_lines: Vec<Line> = (start_line..end_line)
.map(|i| {
let line = app.script_editor_lines.get(i).map(|s| s.as_str()).unwrap_or("");
let visible_line = if h_scroll < line.len() {
&line[h_scroll..]
} else {
""
};
let display_line: String = visible_line.chars().take(text_width).collect();
if i == app.script_editor_cursor.0 {
Line::styled(display_line, Style::default().fg(Color::White))
} else {
Line::styled(display_line, Style::default().fg(Color::Gray))
}
})
.collect();
let text_widget = Paragraph::new(text_lines);
frame.render_widget(text_widget, text_area);
let cursor_line = app.script_editor_cursor.0;
let cursor_col = app.script_editor_cursor.1;
if cursor_line >= scroll_offset && cursor_line < scroll_offset + visible_height {
let screen_y = text_area.y + (cursor_line - scroll_offset) as u16;
let screen_x = if cursor_col >= h_scroll {
let col_in_view = cursor_col - h_scroll;
if col_in_view < text_width {
text_area.x + col_in_view as u16
} else {
text_area.x + text_area.width - 1
}
} else {
text_area.x
};
frame.set_cursor_position((screen_x, screen_y));
}
let help_text = if app.script_editor_modified {
"[Ctrl+S] Save [Esc] Cancel [↑/↓/←/→] Navigate [PgUp/PgDn] Scroll"
} else {
"[Esc] Back [↑/↓/←/→] Navigate [PgUp/PgDn] Scroll"
};
let help = Paragraph::new(help_text)
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center);
frame.render_widget(help, help_area);
}
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)
}