diff --git a/src/graphics.rs b/src/graphics.rs new file mode 100644 index 0000000..a27d826 --- /dev/null +++ b/src/graphics.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; + +#[non_exhaustive] +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] +pub struct Graphics { + pub device: GraphicsDevice, + pub display: Display, +} + +#[non_exhaustive] +#[derive(Debug, Clone, Copy, Default, strum::Display, PartialEq, Deserialize, Serialize)] +pub enum Display { + #[default] + None, + Gtk, + Sdl, + EglHeadless, +} + +#[non_exhaustive] +#[derive(Debug, Clone, Copy, strum::Display, PartialEq, Deserialize, Serialize)] +pub enum GraphicsDevice { + Std { vgamem: u16 }, + Qxl { vgamem: u16 }, + Virtio(VirtioConfig), + None, +} + +impl Default for GraphicsDevice { + fn default() -> Self { + GraphicsDevice::Std { vgamem: 16 } + } +} + +#[non_exhaustive] +#[derive(Debug, Clone, Copy, Default, PartialEq, Deserialize, Serialize)] +pub struct VirtioConfig { + pub enable_3d: bool, + pub venus: bool, +} + +impl Graphics { + pub fn to_qemu_arg(&self) -> Vec { + let display = match self.display { + Display::None => vec!["-display".to_string(), "none".to_string()], + Display::EglHeadless => vec!["-display".to_string(), "egl-headless".to_string()], + Display::Sdl => vec!["-display".to_string(), "sdl".to_string()], + Display::Gtk => vec!["-display".to_string(), "gtk".to_string()], + }; + + let device = match self.device { + GraphicsDevice::Std { vgamem } => { + vec![ + "-vga".to_string(), + "none".to_string(), + "-device".to_string(), + format!("VGA,vgamem_mb={}", vgamem), + ] + } + GraphicsDevice::Qxl { vgamem } => { + vec![ + "-vga".to_string(), + "none".to_string(), + "-device".to_string(), + format!("qxl-vga,vgamem_mb={}", vgamem), + ] + } + GraphicsDevice::Virtio(config) => { + let mut arg = vec!["-vga".to_string(), "none".to_string()]; + + if config.enable_3d { + if config.venus { + arg.extend(vec![ + "-device".to_string(), + "virtio-vga-gl,hostmem=8G,blob=true,venus=true".to_string(), + ]); + } else { + arg.extend(vec![ + "-device".to_string(), + "virtio-vga-gl,hostmem=8G,blob=true".to_string(), + ]); + } + } else { + arg.extend(vec!["-device".to_string(), "virtio-vga".to_string()]); + } + + arg + } + GraphicsDevice::None => { + vec!["-vga".to_string(), "none".to_string()] + } + }; + + let mut args = Vec::new(); + args.extend(display); + args.extend(device); + args + } +} diff --git a/src/lib.rs b/src/lib.rs index 2c1f7d6..60762d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod confirmation; pub mod event; pub mod firmware; pub mod fs; +pub mod graphics; pub mod handlers; pub mod help; pub mod network; diff --git a/src/qemu.rs b/src/qemu.rs index 8600da2..aca153b 100644 --- a/src/qemu.rs +++ b/src/qemu.rs @@ -26,14 +26,7 @@ impl Qemu { let vm_events_path = VM::get_events_file(vm.id); let mut command = match vm.arch { - Arch::X86_64 => { - let mut command = Command::new("qemu-system-x86_64"); - command - .arg("-device") - .arg("VGA,edid=on,xres=1920,yres=1080"); - - command - } + Arch::X86_64 => Command::new("qemu-system-x86_64"), Arch::Aarch64 => { let mut command = Command::new("qemu-system-aarch64"); command @@ -42,8 +35,6 @@ impl Qemu { .arg("-cpu") .arg("max") .arg("-device") - .arg("virtio-gpu-pci") - .arg("-device") .arg("qemu-xhci") .arg("-device") .arg("usb-kbd"); @@ -58,8 +49,6 @@ impl Qemu { .arg("-cpu") .arg("max") .arg("-device") - .arg("virtio-gpu-pci") - .arg("-device") .arg("qemu-xhci") .arg("-device") .arg("usb-kbd"); @@ -93,6 +82,8 @@ impl Qemu { command.args(fs.to_qemu_arg()); } + command.args(vm.graphics.to_qemu_arg()); + command .arg("-daemonize") .arg("-qmp") diff --git a/src/vm.rs b/src/vm.rs index 6d277b3..0be4df9 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -33,6 +33,7 @@ use crate::{ firmware, fs::Filesystem, get_kudu_data_dir, get_kudu_run_dir, + graphics::Graphics, network::{Network, NetworkBackend, bridge::Bridge}, notification::{self, Notification, NotificationLevel}, os::Os::{self, TempleOS}, @@ -57,6 +58,7 @@ pub struct VM { pub networks: Vec, pub remote_access: Option, pub fs: Vec, + pub graphics: Graphics, #[serde(skip)] pub downloading: Arc, @@ -278,6 +280,7 @@ impl VM { fs: data.fs, state: RunState::shutdown, remote_access: data.remote_access, + graphics: data.graphics, }; let data = serde_json::to_string_pretty(&vm)?; @@ -343,6 +346,8 @@ impl VM { self.fs = data.fs; + self.graphics = data.graphics; + path.push("vm.json"); let mut file = File::create(&path)?; let vm = serde_json::to_string_pretty(&self)?; @@ -915,6 +920,17 @@ impl VM { ]), Line::from(""), ]), + ListItem::from(vec![ + Line::from(vec![ + Span::from("Graphics").bold().fg(Color::Yellow), + Span::from(" ".repeat(9)), + Span::from(format!( + "Card: {} -- Display: {}", + self.graphics.device, self.graphics.display, + )), + ]), + Line::from(""), + ]), ]); if let Some(vnc_info) = self.vnc.clone() diff --git a/src/vmbuilder.rs b/src/vmbuilder.rs index e806391..34ad94f 100644 --- a/src/vmbuilder.rs +++ b/src/vmbuilder.rs @@ -1,6 +1,7 @@ mod access; mod advanced; mod fs; +mod graphics; mod hardware; mod overview; mod port; @@ -26,6 +27,7 @@ use crate::{ confirmation::cancel::CancelConfirmation, event::Event::{self}, fs::Filesystem, + graphics::Graphics, network::Network, os::Os::{self}, storage::Disk, @@ -67,6 +69,7 @@ pub struct VMBuildData { pub disks: Vec, pub fs: Vec, pub remote_access: Option, + pub graphics: Graphics, } impl VMBuilder { diff --git a/src/vmbuilder/advanced.rs b/src/vmbuilder/advanced.rs index 9bab2e3..52de6ae 100644 --- a/src/vmbuilder/advanced.rs +++ b/src/vmbuilder/advanced.rs @@ -16,7 +16,7 @@ use crate::{ event::Event, network, os::Os::{ArchLinux, TempleOS}, - vmbuilder::{VMBuildData, access, fs, hardware, overview, port, storage}, + vmbuilder::{VMBuildData, access, fs, graphics, hardware, overview, port, storage}, }; #[derive(Debug, Clone, PartialEq)] @@ -28,6 +28,7 @@ pub enum Section { Network, PortForwarding, RemoteAccess, + Graphics, Summary, } @@ -42,6 +43,7 @@ pub struct Advanced { pub network: network::builder::NetworkBuilder, pub port_fowrwaring: port::PortForwarding, pub remote_access: access::RemoteAccessBuilder, + pub graphics: graphics::GraphicsBuilder, } impl Default for Advanced { @@ -65,6 +67,7 @@ impl Advanced { network, port_fowrwaring, remote_access: access::RemoteAccessBuilder::new(), + graphics: graphics::GraphicsBuilder::new(), } } @@ -83,6 +86,7 @@ impl Advanced { disks: self.storage.disks(), fs: self.fs.filesystems(), remote_access: self.remote_access.access(), + graphics: self.graphics.build(), } } @@ -98,6 +102,10 @@ impl Advanced { self.remote_access.validate() } + fn validate_graphics_section(&mut self) -> bool { + self.graphics.validate() + } + pub fn handle_key_events(&mut self, key_event: KeyEvent, sender: Sender) -> Result<()> { if self.storage.new_disk_popup() { self.storage @@ -137,9 +145,15 @@ impl Advanced { self.focused_section = Section::PortForwarding } Section::PortForwarding => self.focused_section = Section::RemoteAccess, + Section::RemoteAccess => { if self.validate_remote_access() { - self.focused_section = Section::Summary + self.focused_section = Section::Graphics; + } + } + Section::Graphics => { + if self.validate_graphics_section() { + self.focused_section = Section::Summary; } } Section::Summary => { @@ -178,7 +192,12 @@ impl Advanced { self.focused_section = Section::PortForwarding } } - Section::Summary => self.focused_section = Section::RemoteAccess, + Section::Graphics => { + if self.validate_graphics_section() { + self.focused_section = Section::RemoteAccess; + } + } + Section::Summary => self.focused_section = Section::Graphics, }, _ => match &self.focused_section { Section::Overview => { @@ -208,6 +227,9 @@ impl Advanced { Section::RemoteAccess => { self.remote_access.handle_key_events(key_event); } + Section::Graphics => { + self.graphics.handle_key_events(key_event); + } Section::Summary => {} }, } @@ -254,6 +276,11 @@ impl Advanced { Line::from(" Remote Access 󰢹 "), Line::from(""), ]), + ListItem::new(vec![ + Line::from(""), + Line::from(" Graphics 󰢮 "), + Line::from(""), + ]), ListItem::new(vec![ Line::from(""), Line::from(" Summary 󱇗 "), @@ -272,7 +299,7 @@ impl Advanced { .direction(Direction::Vertical) .constraints([ Constraint::Fill(1), - Constraint::Length(27), + Constraint::Length(30), Constraint::Fill(1), ]) .split(frame.area())[1]; @@ -358,6 +385,10 @@ impl Advanced { self.remote_access.render(frame, area, cancel_popup); } + Section::Graphics => { + self.graphics.render(frame, area); + } + Section::Summary => { let mut items = Vec::new(); @@ -368,6 +399,7 @@ impl Advanced { items.extend(self.network.summary()); items.extend(self.port_fowrwaring.summary()); items.extend(self.remote_access.summary()); + items.extend(self.graphics.summary()); let list_width = items.iter().map(|item| item.width()).max().unwrap() as u16; let list = List::new(items); @@ -524,6 +556,9 @@ impl Advanced { ])] } } + Section::Graphics => { + vec![Line::from(vec![Span::from("⇄").bold(), Span::from(" Nav")])] + } Section::Summary => { vec![Line::from(vec![ Span::from("Esc").bold(), diff --git a/src/vmbuilder/graphics.rs b/src/vmbuilder/graphics.rs new file mode 100644 index 0000000..b78ad7c --- /dev/null +++ b/src/vmbuilder/graphics.rs @@ -0,0 +1,401 @@ +use ratatui::{ + Frame, + layout::{Constraint, Margin, Rect}, + style::Stylize, + text::{Line, Span}, + widgets::{ListItem, Row, Table}, +}; + +use crossterm::event::{ + KeyCode::{self}, + KeyEvent, +}; +use tui_input::{Input, backend::crossterm::EventHandler}; + +use crate::graphics::{Display, Graphics, GraphicsDevice, VirtioConfig}; + +#[derive(Debug, Clone, PartialEq, Default)] +enum Section { + #[default] + Device, + Display, + Memory, + Enable3d, + Venus, +} + +#[derive(Debug, Clone, Default)] +struct UserInputField { + field: Input, + error: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, strum::Display)] +enum Device { + #[default] + Std, + Qxl, + Virtio, + None, +} + +#[derive(Debug, Clone, Default)] +pub struct GraphicsBuilder { + section: Section, + device: Device, + display: Display, + memory: UserInputField, + enable_3d: bool, + venus: bool, +} + +impl GraphicsBuilder { + pub fn new() -> Self { + Self { + section: Section::default(), + device: Device::default(), + display: Display::default(), + memory: UserInputField { + field: "16".into(), + error: None, + }, + enable_3d: false, + venus: false, + } + } + + pub fn build(&self) -> Graphics { + match self.device { + Device::None => Graphics { + device: GraphicsDevice::None, + display: Display::None, + }, + + Device::Std => { + let memory = self.memory.field.value().parse::().unwrap(); + Graphics { + device: GraphicsDevice::Std { vgamem: memory }, + display: self.display, + } + } + Device::Qxl => { + let memory = self.memory.field.value().parse::().unwrap(); + Graphics { + device: GraphicsDevice::Qxl { vgamem: memory }, + display: self.display, + } + } + Device::Virtio => Graphics { + device: GraphicsDevice::Virtio(VirtioConfig { + enable_3d: self.enable_3d, + venus: self.venus, + }), + display: self.display, + }, + } + } + + pub fn handle_key_events(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Down | KeyCode::Char('j') => match self.section { + Section::Device => match self.device { + Device::None => {} + _ => { + self.section = Section::Display; + } + }, + Section::Display => match self.device { + Device::Std | Device::Qxl => { + self.section = Section::Memory; + } + Device::Virtio => { + self.section = Section::Enable3d; + } + Device::None => { + unreachable!() + } + }, + Section::Memory => match self.device { + Device::Std | Device::Qxl => { + self.section = Section::Device; + } + _ => { + unreachable!() + } + }, + Section::Enable3d => { + self.section = Section::Venus; + } + Section::Venus => { + self.section = Section::Device; + } + }, + KeyCode::Up | KeyCode::Char('k') => match self.section { + Section::Device => {} + Section::Display => {} + Section::Memory => {} + Section::Enable3d => {} + Section::Venus => {} + }, + _ => match self.section { + Section::Device => match key_event.code { + KeyCode::Char('l') | KeyCode::Right => match self.device { + Device::Std => { + self.device = Device::Qxl; + self.memory = UserInputField { + field: Input::from("16"), + error: None, + }; + } + Device::Qxl => { + self.device = Device::Virtio; + } + Device::Virtio => { + self.device = Device::None; + } + Device::None => { + self.device = Device::Std; + self.memory = UserInputField { + field: Input::from("16"), + error: None, + }; + } + }, + KeyCode::Char('h') | KeyCode::Left => match self.device { + Device::Std => { + self.device = Device::None; + } + Device::Qxl => { + self.device = Device::Std; + self.memory = UserInputField { + field: Input::from("16"), + error: None, + }; + } + Device::Virtio => { + self.device = Device::Qxl; + self.memory = UserInputField { + field: Input::from("16"), + error: None, + }; + } + Device::None => { + self.device = Device::Virtio; + } + }, + _ => {} + }, + Section::Display if self.device != Device::None => match key_event.code { + KeyCode::Char('l') | KeyCode::Right => match self.display { + Display::None => { + self.display = Display::Gtk; + } + Display::Gtk => { + self.display = Display::Sdl; + } + Display::Sdl => { + self.display = Display::EglHeadless; + } + Display::EglHeadless => { + self.display = Display::None; + } + }, + KeyCode::Char('h') | KeyCode::Left => {} + _ => {} + }, + Section::Memory => { + self.memory + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + Section::Enable3d => match key_event.code { + KeyCode::Left | KeyCode::Char('h') | KeyCode::Right | KeyCode::Char('l') => { + self.enable_3d = !self.enable_3d; + + if self.enable_3d && self.display == Display::None { + self.display = Display::EglHeadless; + } + } + _ => {} + }, + Section::Venus => match key_event.code { + KeyCode::Left | KeyCode::Char('h') | KeyCode::Right | KeyCode::Char('l') => { + self.venus = !self.venus; + + if self.venus && self.display == Display::None { + self.display = Display::EglHeadless; + } + } + _ => {} + }, + _ => {} + }, + } + } + + pub fn validate(&mut self) -> bool { + let mut valid = true; + + self.memory.error = None; + + match self.device { + Device::Std | Device::Qxl => { + let memory = self.memory.field.value(); + + if memory.is_empty() { + self.memory.error = Some("Field required".into()); + valid = false; + } else { + match self.memory.field.value().parse::() { + Ok(v) => { + if v == 0 { + self.memory.error = Some("Memory value can not be 0".into()); + valid = false; + } + } + Err(_) => { + self.memory.error = Some("Memory value should be a number".into()); + valid = false; + } + } + } + } + _ => {} + } + + valid + } + + pub fn summary(&self) -> Vec> { + vec![ListItem::from(vec![ + Line::from(vec![ + Span::from("Graphics").bold(), + Span::from(" ".repeat(12)), + Span::from(format!( + "Card: {} -- Display: {}", + self.device, self.display, + )), + ]), + Line::from(""), + ])] + } + + pub fn render(&mut self, frame: &mut Frame, area: Rect) { + let widths = [ + Constraint::Length(25), + Constraint::Length(60), + Constraint::Length(3), + ]; + let mut rows = vec![ + Row::new(vec![ + { + if self.section == Section::Device { + Span::from("> Card").bold() + } else { + Span::from(" Card") + } + }, + Span::from(format!("< {} >", self.device)), + ]), + Row::new(Line::from("")), + ]; + + if self.device != Device::None { + rows.extend(vec![ + Row::new(vec![ + { + if self.section == Section::Display { + Span::from("> Display").bold() + } else { + Span::from(" Display") + } + }, + Span::from(format!("< {} >", self.display)), + ]), + Row::new(Line::from("")), + ]) + } + + match self.device { + Device::Std | Device::Qxl => { + rows.extend([ + Row::new(vec![ + { + if self.section == Section::Memory { + Span::from("> Memory").bold() + } else { + Span::from(" Memory") + } + }, + Span::from({ + let original_length = self.memory.field.to_string().len(); + let target_length = 65_usize; + + self.memory + .field + .to_string() + .chars() + .chain(std::iter::repeat_n( + ' ', + target_length.saturating_sub(original_length), + )) + .collect::() + }) + .on_dark_gray(), + Span::from("MiB"), + ]), + Row::new(vec![ + Span::from(""), + Span::from(self.memory.clone().error.unwrap_or("".into())).red(), + ]), + ]); + } + Device::Virtio => rows.extend(vec![ + Row::new(vec![ + { + if self.section == Section::Enable3d { + Span::from("> 3D Acceleration").bold() + } else { + Span::from(" 3D Acceleration") + } + }, + Span::from({ + if self.enable_3d { + "[x] Enabled [ ] Disabled" + } else { + "[ ] Enabled [x] Disabled" + } + }), + ]), + Row::new(Line::from("")), + Row::new(vec![ + { + if self.section == Section::Venus { + Span::from("> Venus").bold() + } else { + Span::from(" Venus") + } + }, + Span::from({ + if self.venus { + "[x] Enabled [ ] Disabled" + } else { + "[ ] Enabled [x] Disabled" + } + }), + ]), + ]), + _ => {} + } + + let table = Table::new(rows, widths); + + frame.render_widget( + table, + area.inner(Margin { + horizontal: 2, + vertical: 2, + }), + ); + } +} diff --git a/src/vmbuilder/quick.rs b/src/vmbuilder/quick.rs index 8b6fa03..7c440f9 100644 --- a/src/vmbuilder/quick.rs +++ b/src/vmbuilder/quick.rs @@ -8,6 +8,7 @@ use crate::{ access::{RemoteAccess, vnc::VNC}, cloudinit::Cloudinit, event::Event, + graphics::Graphics, network, os::{Os, debian::DebianRelease, ubuntu::UbuntuRelease}, vmbuilder::VMBuildData, @@ -300,6 +301,7 @@ impl Quick { fs: Vec::new(), cloudinit: Some(self.cloudinit().unwrap()), //FIX: remote_access, + graphics: Graphics::default(), } } diff --git a/src/vmedit.rs b/src/vmedit.rs index d91d5cd..bfd6abd 100644 --- a/src/vmedit.rs +++ b/src/vmedit.rs @@ -1,7 +1,9 @@ pub mod fs; +pub mod graphics; pub mod network; pub mod port; pub mod storage; + use anyhow::Result; use std::{cell::RefCell, collections::HashMap, path::PathBuf, rc::Rc, sync::mpsc::Sender}; @@ -21,6 +23,7 @@ use crate::{ access::{RemoteAccess, vnc::VncBuilder}, event::Event, fs::Filesystem, + graphics::Graphics, network::Network, storage::{Disk, Drive, Interface}, vm::{VM, VmId}, @@ -41,6 +44,7 @@ enum Section { Network, PortForwarding, RemoteAccess, + Graphics, } impl Section { @@ -52,6 +56,7 @@ impl Section { Section::Network => 3, Section::PortForwarding => 4, Section::RemoteAccess => 5, + Section::Graphics => 6, } } } @@ -73,6 +78,7 @@ pub struct EditVM { fs: fs::FsEdit, network: network::NetworkEdit, port_forwarding: port::PortForwarding, + graphics: graphics::GraphicsEdit, vnc: VncBuilder, pub vm: VM, } @@ -88,6 +94,7 @@ pub struct VMEditData { pub networks: Vec, pub remote_access: Option, pub fs: Vec, + pub graphics: Graphics, } impl EditVM { @@ -125,6 +132,7 @@ impl EditVM { fs: fs::FsEdit::new(vm.fs.clone()), network: network::NetworkEdit::new(networks.clone()), port_forwarding: port::PortForwarding::new(networks), + graphics: graphics::GraphicsEdit::new(&vm.graphics), vnc, vm: vm.clone(), } @@ -185,6 +193,10 @@ impl EditVM { self.vnc.validate() } + pub fn validate_graphics(&mut self) -> bool { + self.graphics.validate() + } + pub fn handle_key_events( &mut self, key_event: KeyEvent, @@ -236,8 +248,10 @@ impl EditVM { networks, fs: self.fs.build(), remote_access: self.vnc.build().map(RemoteAccess::Vnc), + graphics: self.graphics.build(), })); } + KeyCode::Tab => match self.section { Section::Hardware(_) => { if self.validate_harware_section() { @@ -259,6 +273,11 @@ impl EditVM { } Section::RemoteAccess => { if self.validate_remote_access() { + self.section = Section::Graphics; + } + } + Section::Graphics => { + if self.validate_graphics() { self.section = Section::Hardware(HardwareSection::default()) } } @@ -266,7 +285,7 @@ impl EditVM { KeyCode::BackTab => match self.section { Section::Hardware(_) => { if self.validate_harware_section() { - self.section = Section::RemoteAccess; + self.section = Section::Graphics; } } Section::Storage => { @@ -289,6 +308,11 @@ impl EditVM { self.section = Section::PortForwarding; } } + Section::Graphics => { + if self.validate_graphics() { + self.section = Section::RemoteAccess; + } + } }, _ => match &self.section { Section::Hardware(hardware_section) => match hardware_section { @@ -336,6 +360,9 @@ impl EditVM { Section::RemoteAccess => { self.vnc.handle_key_events(key_event); } + Section::Graphics => { + self.graphics.handle_key_events(key_event); + } }, } @@ -376,6 +403,11 @@ impl EditVM { Line::from(" Remote Access 󰢹 "), Line::from(""), ]), + ListItem::new(vec![ + Line::from(""), + Line::from(" Graphics 󰢮 "), + Line::from(""), + ]), ]; let list = List::new(sections) @@ -389,7 +421,7 @@ impl EditVM { .direction(Direction::Vertical) .constraints([ Constraint::Fill(1), - Constraint::Length(24), + Constraint::Length(27), Constraint::Fill(1), ]) .margin(1) @@ -559,6 +591,9 @@ impl EditVM { Section::RemoteAccess => { self.vnc.render(frame, area, false); } + Section::Graphics => { + self.graphics.render(frame, area); + } } } @@ -817,7 +852,7 @@ impl EditVM { } } } - Section::RemoteAccess => { + Section::RemoteAccess | Section::Graphics => { vec![Line::from(vec![ Span::from("k,↑").bold(), Span::from(" Up"), diff --git a/src/vmedit/graphics.rs b/src/vmedit/graphics.rs new file mode 100644 index 0000000..23f1c89 --- /dev/null +++ b/src/vmedit/graphics.rs @@ -0,0 +1,408 @@ +use ratatui::{ + Frame, + layout::{Constraint, Margin, Rect}, + style::Stylize, + text::{Line, Span}, + widgets::{Row, Table}, +}; + +use crossterm::event::{ + KeyCode::{self}, + KeyEvent, +}; +use tui_input::{Input, backend::crossterm::EventHandler}; + +use crate::graphics::{Display, Graphics, GraphicsDevice, VirtioConfig}; + +#[derive(Debug, Clone, PartialEq, Default)] +enum Section { + #[default] + Device, + Display, + Memory, + Enable3d, + Venus, +} + +#[derive(Debug, Clone, Default)] +struct UserInputField { + field: Input, + error: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, strum::Display)] +enum Device { + #[default] + Std, + Qxl, + Virtio, + None, +} + +#[derive(Debug, Clone, Default)] +pub struct GraphicsEdit { + section: Section, + device: Device, + display: Display, + memory: UserInputField, + enable_3d: bool, + venus: bool, +} + +impl GraphicsEdit { + pub fn new(graphics: &Graphics) -> Self { + let mut memory = 16; + let mut enable_3d = false; + let mut venus = false; + + let device = match graphics.device { + GraphicsDevice::Std { vgamem: v } => { + memory = v; + Device::Std + } + GraphicsDevice::Qxl { vgamem: v } => { + memory = v; + Device::Qxl + } + GraphicsDevice::Virtio(config) => { + enable_3d = config.enable_3d; + venus = config.venus; + Device::Virtio + } + GraphicsDevice::None => Device::None, + }; + + Self { + section: Section::default(), + device, + display: graphics.display, + memory: UserInputField { + field: Input::from(memory.to_string()), + error: None, + }, + enable_3d, + venus, + } + } + + pub fn build(&self) -> Graphics { + match self.device { + Device::None => Graphics { + device: GraphicsDevice::None, + display: Display::None, + }, + + Device::Std => { + let memory = self.memory.field.value().parse::().unwrap(); + Graphics { + device: GraphicsDevice::Std { vgamem: memory }, + display: self.display, + } + } + Device::Qxl => { + let memory = self.memory.field.value().parse::().unwrap(); + Graphics { + device: GraphicsDevice::Qxl { vgamem: memory }, + display: self.display, + } + } + Device::Virtio => Graphics { + device: GraphicsDevice::Virtio(VirtioConfig { + enable_3d: self.enable_3d, + venus: self.venus, + }), + display: self.display, + }, + } + } + + pub fn handle_key_events(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Down | KeyCode::Char('j') => match self.section { + Section::Device => match self.device { + Device::None => {} + _ => { + self.section = Section::Display; + } + }, + Section::Display => match self.device { + Device::Std | Device::Qxl => { + self.section = Section::Memory; + } + Device::Virtio => { + self.section = Section::Enable3d; + } + Device::None => { + unreachable!() + } + }, + Section::Memory => match self.device { + Device::Std | Device::Qxl => { + self.section = Section::Device; + } + _ => { + unreachable!() + } + }, + Section::Enable3d => { + self.section = Section::Venus; + } + Section::Venus => { + self.section = Section::Device; + } + }, + KeyCode::Up | KeyCode::Char('k') => match self.section { + Section::Device => {} + Section::Display => {} + Section::Memory => {} + Section::Enable3d => {} + Section::Venus => {} + }, + _ => match self.section { + Section::Device => match key_event.code { + KeyCode::Char('l') | KeyCode::Right => match self.device { + Device::Std => { + self.device = Device::Qxl; + self.memory = UserInputField { + field: Input::from("16"), + error: None, + }; + } + Device::Qxl => { + self.device = Device::Virtio; + } + Device::Virtio => { + self.device = Device::None; + } + Device::None => { + self.device = Device::Std; + self.memory = UserInputField { + field: Input::from("16"), + error: None, + }; + } + }, + KeyCode::Char('h') | KeyCode::Left => match self.device { + Device::Std => { + self.device = Device::None; + } + Device::Qxl => { + self.device = Device::Std; + self.memory = UserInputField { + field: Input::from("16"), + error: None, + }; + } + Device::Virtio => { + self.device = Device::Qxl; + self.memory = UserInputField { + field: Input::from("16"), + error: None, + }; + } + Device::None => { + self.device = Device::Virtio; + } + }, + _ => {} + }, + Section::Display if self.device != Device::None => match key_event.code { + KeyCode::Char('l') | KeyCode::Right => match self.display { + Display::None => { + self.display = Display::Gtk; + } + Display::Gtk => { + self.display = Display::Sdl; + } + Display::Sdl => { + self.display = Display::EglHeadless; + } + Display::EglHeadless => { + self.display = Display::None; + } + }, + KeyCode::Char('h') | KeyCode::Left => {} + _ => {} + }, + Section::Memory => { + self.memory + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + Section::Enable3d => match key_event.code { + KeyCode::Left | KeyCode::Char('h') | KeyCode::Right | KeyCode::Char('l') => { + self.enable_3d = !self.enable_3d; + + if self.enable_3d && self.display == Display::None { + self.display = Display::EglHeadless; + } + } + _ => {} + }, + Section::Venus => match key_event.code { + KeyCode::Left | KeyCode::Char('h') | KeyCode::Right | KeyCode::Char('l') => { + self.venus = !self.venus; + + if self.venus && self.display == Display::None { + self.display = Display::EglHeadless; + } + } + _ => {} + }, + _ => {} + }, + } + } + + pub fn validate(&mut self) -> bool { + let mut valid = true; + + self.memory.error = None; + + match self.device { + Device::Std | Device::Qxl => { + let memory = self.memory.field.value(); + + if memory.is_empty() { + self.memory.error = Some("Field required".into()); + valid = false; + } else { + match self.memory.field.value().parse::() { + Ok(v) => { + if v == 0 { + self.memory.error = Some("Memory value can not be 0".into()); + valid = false; + } + } + Err(_) => { + self.memory.error = Some("Memory value should be a number".into()); + valid = false; + } + } + } + } + _ => {} + } + + valid + } + + pub fn render(&mut self, frame: &mut Frame, area: Rect) { + let widths = [ + Constraint::Length(25), + Constraint::Length(60), + Constraint::Length(3), + ]; + let mut rows = vec![ + Row::new(vec![ + { + if self.section == Section::Device { + Span::from("> Card").bold() + } else { + Span::from(" Card") + } + }, + Span::from(format!("< {} >", self.device)), + ]), + Row::new(Line::from("")), + ]; + + if self.device != Device::None { + rows.extend(vec![ + Row::new(vec![ + { + if self.section == Section::Display { + Span::from("> Display").bold() + } else { + Span::from(" Display") + } + }, + Span::from(format!("< {} >", self.display)), + ]), + Row::new(Line::from("")), + ]) + } + + match self.device { + Device::Std | Device::Qxl => { + rows.extend([ + Row::new(vec![ + { + if self.section == Section::Memory { + Span::from("> Memory").bold() + } else { + Span::from(" Memory") + } + }, + Span::from({ + let original_length = self.memory.field.to_string().len(); + let target_length = 65_usize; + + self.memory + .field + .to_string() + .chars() + .chain(std::iter::repeat_n( + ' ', + target_length.saturating_sub(original_length), + )) + .collect::() + }) + .on_dark_gray(), + Span::from("MiB"), + ]), + Row::new(vec![ + Span::from(""), + Span::from(self.memory.clone().error.unwrap_or("".into())).red(), + ]), + ]); + } + Device::Virtio => rows.extend(vec![ + Row::new(vec![ + { + if self.section == Section::Enable3d { + Span::from("> 3D Acceleration").bold() + } else { + Span::from(" 3D Acceleration") + } + }, + Span::from({ + if self.enable_3d { + "[x] Enabled [ ] Disabled" + } else { + "[ ] Enabled [x] Disabled" + } + }), + ]), + Row::new(Line::from("")), + Row::new(vec![ + { + if self.section == Section::Venus { + Span::from("> Venus").bold() + } else { + Span::from(" Venus") + } + }, + Span::from({ + if self.venus { + "[x] Enabled [ ] Disabled" + } else { + "[ ] Enabled [x] Disabled" + } + }), + ]), + ]), + _ => {} + } + + let table = Table::new(rows, widths); + + frame.render_widget( + table, + area.inner(Margin { + horizontal: 2, + vertical: 2, + }), + ); + } +}