use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
const EMBEDDED_PROFILES: &str = include_str!("../../assets/metadata/qemu_profiles.toml");
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BiosRomConfig {
pub required: bool,
#[serde(default = "default_bios_label")]
pub label: String,
#[serde(default)]
pub default_filename: Option<String>,
#[serde(default)]
pub hint: Option<String>,
}
fn default_bios_label() -> String {
"BIOS/ROM File".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QemuProfile {
pub display_name: String,
pub category: String,
pub emulator: String,
pub memory_mb: u32,
pub cpu_cores: u32,
#[serde(default)]
pub cpu_model: Option<String>,
#[serde(default)]
pub machine: Option<String>,
pub vga: String,
#[serde(default)]
pub audio: Vec<String>,
pub network_model: String,
#[serde(default = "default_network_backend")]
pub network_backend: String,
pub disk_interface: String,
pub disk_size_gb: u32,
#[serde(default)]
pub enable_kvm: bool,
#[serde(default)]
pub uefi: bool,
#[serde(default)]
pub tpm: bool,
#[serde(default)]
pub rtc_localtime: bool,
#[serde(default)]
pub usb_tablet: bool,
#[serde(default = "default_display")]
pub display: String,
#[serde(default)]
pub extra_args: Vec<String>,
#[serde(default)]
pub iso_url: Option<String>,
#[serde(default)]
pub notes: Option<String>,
#[serde(default)]
pub bios_rom: Option<BiosRomConfig>,
}
fn default_network_backend() -> String {
"user".to_string()
}
fn default_display() -> String {
"gtk".to_string()
}
impl Default for QemuProfile {
fn default() -> Self {
Self {
display_name: "Unknown OS".to_string(),
category: "alternative".to_string(),
emulator: "qemu-system-x86_64".to_string(),
memory_mb: 2048,
cpu_cores: 2,
cpu_model: Some("host".to_string()),
machine: Some("q35".to_string()),
vga: "std".to_string(),
audio: vec!["intel-hda".to_string(), "hda-duplex".to_string()],
network_model: "e1000".to_string(),
network_backend: "user".to_string(),
disk_interface: "ide".to_string(),
disk_size_gb: 32,
enable_kvm: true,
uefi: false,
tpm: false,
rtc_localtime: false,
usb_tablet: true,
display: "gtk".to_string(),
extra_args: vec![],
iso_url: None,
notes: None,
bios_rom: None,
}
}
}
impl QemuProfile {
#[allow(dead_code)]
pub fn has_free_iso(&self) -> bool {
self.iso_url.is_some()
}
#[allow(dead_code)]
pub fn is_x86(&self) -> bool {
self.emulator.contains("x86_64") || self.emulator.contains("i386")
}
#[allow(dead_code)]
pub fn is_x86_64(&self) -> bool {
self.emulator.contains("x86_64")
}
pub fn summary(&self) -> String {
let mut parts = vec![];
if self.memory_mb >= 1024 {
parts.push(format!("{}GB RAM", self.memory_mb / 1024));
} else {
parts.push(format!("{}MB RAM", self.memory_mb));
}
parts.push(format!("{}GB", self.disk_size_gb));
if self.uefi {
parts.push("UEFI".to_string());
}
if self.disk_interface == "virtio" {
parts.push("virtio".to_string());
}
parts.join(", ")
}
}
#[derive(Debug, Default)]
pub struct QemuProfileStore {
profiles: HashMap<String, QemuProfile>,
}
impl QemuProfileStore {
pub fn new() -> Self {
Self {
profiles: HashMap::new(),
}
}
pub fn load_embedded() -> Self {
let mut store = Self::new();
match toml::from_str::<HashMap<String, QemuProfile>>(EMBEDDED_PROFILES) {
Ok(profiles) => {
store.profiles = profiles;
}
Err(e) => {
eprintln!("Warning: Failed to parse embedded QEMU profiles: {}", e);
}
}
store
}
pub fn load_user_overrides(&mut self, path: &Path) {
if !path.exists() {
return;
}
match std::fs::read_to_string(path) {
Ok(content) => match toml::from_str::<HashMap<String, QemuProfile>>(&content) {
Ok(user_profiles) => {
for (id, profile) in user_profiles {
self.profiles.insert(id, profile);
}
}
Err(e) => {
eprintln!("Warning: Failed to parse user QEMU profiles: {}", e);
}
},
Err(e) => {
eprintln!("Warning: Failed to read user QEMU profiles: {}", e);
}
}
}
pub fn get(&self, os_id: &str) -> Option<&QemuProfile> {
self.profiles.get(os_id)
}
#[allow(dead_code)]
pub fn get_or_default(&self, os_id: &str) -> QemuProfile {
self.profiles
.get(os_id)
.cloned()
.unwrap_or_else(QemuProfile::default)
}
#[allow(dead_code)]
pub fn list_all(&self) -> Vec<(&String, &QemuProfile)> {
let mut profiles: Vec<_> = self.profiles.iter().collect();
profiles.sort_by(|a, b| a.1.display_name.cmp(&b.1.display_name));
profiles
}
pub fn list_by_category(&self, category: &str) -> Vec<(&String, &QemuProfile)> {
let mut profiles: Vec<_> = self
.profiles
.iter()
.filter(|(_, p)| p.category == category)
.collect();
profiles.sort_by(|a, b| a.1.display_name.cmp(&b.1.display_name));
profiles
}
#[allow(dead_code)]
pub fn categories(&self) -> Vec<String> {
let mut categories: Vec<String> =
self.profiles.values().map(|p| p.category.clone()).collect();
categories.sort();
categories.dedup();
categories
}
pub fn category_display_name(category: &str) -> &'static str {
match category {
"windows" => "Windows",
"linux" => "Linux",
"bsd" => "BSD",
"unix" => "Unix",
"classic-mac" => "Classic Mac",
"macos" => "macOS",
"alternative" => "Alternative",
"retro" => "Retro",
"mobile" => "Mobile / Android",
"infrastructure" => "Infrastructure",
"utilities" => "Utilities",
_ => "Other",
}
}
#[allow(dead_code)]
pub fn list_with_free_iso(&self) -> Vec<(&String, &QemuProfile)> {
let mut profiles: Vec<_> = self
.profiles
.iter()
.filter(|(_, p)| p.iso_url.is_some())
.collect();
profiles.sort_by(|a, b| a.1.display_name.cmp(&b.1.display_name));
profiles
}
#[allow(dead_code)]
pub fn list_x86_profiles(&self) -> Vec<(&String, &QemuProfile)> {
let mut profiles: Vec<_> = self.profiles.iter().filter(|(_, p)| p.is_x86()).collect();
profiles.sort_by(|a, b| a.1.display_name.cmp(&b.1.display_name));
profiles
}
#[allow(dead_code)]
pub fn search(&self, query: &str) -> Vec<(&String, &QemuProfile)> {
let query_lower = query.to_lowercase();
let mut profiles: Vec<_> = self
.profiles
.iter()
.filter(|(id, p)| {
id.to_lowercase().contains(&query_lower)
|| p.display_name.to_lowercase().contains(&query_lower)
})
.collect();
profiles.sort_by(|a, b| a.1.display_name.cmp(&b.1.display_name));
profiles
}
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.profiles.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.profiles.is_empty()
}
#[allow(dead_code)]
pub fn generic_profile_for_category(category: &str) -> &'static str {
match category {
"windows" => "generic-windows",
"linux" => "generic-linux",
"bsd" => "generic-bsd",
_ => "generic-other",
}
}
}
#[cfg(test)]
#[path = "tests/qemu_profiles.rs"]
mod tests;