diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..ac76452 --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,329 @@ +use serde::{Deserialize, Serialize}; + +use crossterm::event::{KeyCode, KeyEvent}; +use rustix::path::Arg; + +use std::path::PathBuf; + +use tui_input::{Input, backend::crossterm::EventHandler}; + +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout, Margin}, + style::{Style, Stylize}, + text::{Line, Span, Text}, + widgets::{Block, BorderType, Borders, Clear}, +}; + +use crate::fs::Section::ReadOnly; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Filesystem { + pub driver: FsDriver, + pub source_path: PathBuf, + pub mount_tag: String, + pub readonly: bool, +} + +impl Filesystem { + pub fn to_qemu_arg(&self) -> Vec { + match self.driver { + FsDriver::Virtio9pPci => { + let readonly = if self.readonly { "on" } else { "off" }; + vec![ + "-virtfs".to_string(), + format!( + "local,path={},mount_tag={},security_model=none,readonly={}", + self.source_path.to_string_lossy(), + self.mount_tag, + readonly + ), + ] + } + } + } +} + +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, strum::Display, Default, Serialize, Deserialize)] +pub enum FsDriver { + #[default] + #[strum(to_string = "9pfs")] + Virtio9pPci, +} + +// NewFs + +#[derive(Debug, Clone, Default, PartialEq)] +enum Section { + #[default] + SourcePath, + MountTag, + ReadOnly, +} + +#[derive(Debug, Clone, Default)] +pub struct NewFs { + section: Section, + driver: FsDriver, + source_path: UserInputField, + mount_tag: UserInputField, + readonly: bool, +} + +#[derive(Debug, Clone, Default)] +struct UserInputField { + field: Input, + error: Option, +} + +impl NewFs { + pub fn new() -> Self { + Self::default() + } + + pub fn handle_key_events(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Down => match self.section { + Section::SourcePath => { + self.section = Section::MountTag; + } + Section::MountTag => { + self.section = Section::ReadOnly; + } + ReadOnly => { + self.section = Section::SourcePath; + } + }, + KeyCode::Up => match self.section { + Section::SourcePath => { + self.section = Section::ReadOnly; + } + Section::MountTag => { + self.section = Section::SourcePath; + } + ReadOnly => { + self.section = Section::MountTag; + } + }, + _ => match self.section { + Section::SourcePath => { + self.source_path + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + Section::MountTag => { + self.mount_tag + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + Section::ReadOnly => match key_event.code { + KeyCode::Left | KeyCode::Char('l') | KeyCode::Right | KeyCode::Char('h') => { + self.readonly = !self.readonly; + } + _ => {} + }, + }, + } + } + + pub fn validate(&mut self) -> bool { + let mut valid = true; + + let source_path = self.source_path.field.value(); + let mount_tag = self.mount_tag.field.value(); + + if source_path.is_empty() { + self.source_path.error = Some("Field required".into()); + valid = false; + } else { + if !PathBuf::from(source_path).exists() { + self.source_path.error = Some("Invalid Path".into()); + valid = false; + } + } + + if mount_tag.is_empty() { + self.mount_tag.error = Some("Field required".into()); + valid = false; + } else { + if mount_tag.chars().any(|c| !c.is_alphanumeric()) { + self.source_path.error = Some("Mount tag should be alphanumeric".into()); + valid = false; + } + } + + valid + } + + pub fn build(&self) -> Filesystem { + Filesystem { + driver: FsDriver::Virtio9pPci, + source_path: PathBuf::from(self.source_path.field.value().to_string()), + mount_tag: self.mount_tag.field.value().to_string(), + readonly: self.readonly, + } + } + + pub fn render(&self, frame: &mut Frame) { + let area = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(19), + Constraint::Fill(1), + ]) + .margin(1) + .split(frame.area())[1]; + + let area = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Length(80), + Constraint::Fill(1), + ]) + .margin(1) + .split(area)[1]; + + frame.render_widget(Clear, area); + + frame.render_widget( + Block::new() + .borders(Borders::all()) + .title(" Filesystem  ") + .border_type(BorderType::Thick) + .title_alignment(ratatui::layout::HorizontalAlignment::Center) + .border_style(Style::default().yellow()), + area, + ); + + let area = area.inner(Margin { + horizontal: 4, + vertical: 2, + }); + + let (driver_block, source_block, mount_tag_block, readonly_block) = { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(2), + Constraint::Length(2), + Constraint::Length(2), + Constraint::Length(2), + ]) + .margin(1) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(area); + + (chunks[0], chunks[1], chunks[2], chunks[3]) + }; + + let driver = Text::from(Line::from(vec![ + Span::from(" Driver").bold(), + Span::from(" ".repeat(7)), + Span::from(self.driver.to_string()), + ])); + + let source_path = Text::from(vec![ + Line::from(vec![ + { + if self.section == Section::SourcePath { + Span::from("> Source Path").bold() + } else { + Span::from(" Source Path") + } + }, + Span::from(" ".repeat(2)), + Span::from({ + let original_length = self.source_path.field.to_string().len(); + let target_length = 60_usize; + + self.source_path + .field + .to_string() + .chars() + .chain(std::iter::repeat_n( + ' ', + target_length.saturating_sub(original_length), + )) + .collect::() + }) + .on_dark_gray(), + ]), + Line::from(vec![ + Span::from(" ".repeat(15)), + Span::from(self.source_path.clone().error.unwrap_or("".to_string())).red(), + ]), + ]); + + let mount_tag = Text::from(vec![ + Line::from(vec![ + { + if self.section == Section::MountTag { + Span::from("> Mount Tag").bold() + } else { + Span::from(" Mount Tag") + } + }, + Span::from(" ".repeat(4)), + Span::from({ + let original_length = self.mount_tag.field.to_string().len(); + let target_length = 60_usize; + + self.mount_tag + .field + .to_string() + .chars() + .chain(std::iter::repeat_n( + ' ', + target_length.saturating_sub(original_length), + )) + .collect::() + }) + .on_dark_gray(), + ]), + Line::from(vec![ + Span::from(" ".repeat(15)), + Span::from(self.mount_tag.clone().error.unwrap_or("".to_string())).red(), + ]), + ]); + + let readonly = Text::from(Line::from(vec![ + { + if self.section == Section::ReadOnly { + Span::from("> Read only").bold() + } else { + Span::from(" Read only") + } + }, + Span::from(" ".repeat(4)), + Span::from({ + if self.readonly { + "[x] Yes [ ] No" + } else { + "[ ] Yes [x] No" + } + }), + ])); + + frame.render_widget(driver, driver_block); + frame.render_widget(source_path, source_block); + frame.render_widget(mount_tag, mount_tag_block); + frame.render_widget(readonly, readonly_block); + + match self.section { + Section::SourcePath if self.source_path.field.visual_cursor() < 56 => { + let x = area.x + self.source_path.field.visual_cursor() as u16 + 16; + let y = area.y + 4; + frame.set_cursor_position((x, y)); + } + Section::MountTag if self.mount_tag.field.visual_cursor() < 56 => { + let x = area.x + self.mount_tag.field.visual_cursor() as u16 + 16; + let y = area.y + 7; + frame.set_cursor_position((x, y)); + } + _ => {} + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 52a727f..2c1f7d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ pub mod cloudinit; pub mod confirmation; pub mod event; pub mod firmware; +pub mod fs; pub mod handlers; pub mod help; pub mod network; diff --git a/src/network/builder.rs b/src/network/builder.rs index 426ce86..4d547e6 100644 --- a/src/network/builder.rs +++ b/src/network/builder.rs @@ -114,7 +114,7 @@ impl NetworkBuilder { vec![ Line::from(vec![ Span::from("Networks").bold(), - Span::from(" ".repeat(13)), + Span::from(" ".repeat(11)), Span::from(" - "), ]), Line::from(""), diff --git a/src/network/port_forwarding.rs b/src/network/port_forwarding.rs index 6f9b226..eb1aace 100644 --- a/src/network/port_forwarding.rs +++ b/src/network/port_forwarding.rs @@ -218,7 +218,7 @@ impl MappingBuilder { frame.render_widget( Block::new() .borders(Borders::all()) - .title(" New Port Forwaring  ") + .title(" New Port Forwarding  ") .border_type(BorderType::Thick) .title_alignment(ratatui::layout::HorizontalAlignment::Center) .border_style(Style::default().yellow()), diff --git a/src/qemu.rs b/src/qemu.rs index 0803eeb..8600da2 100644 --- a/src/qemu.rs +++ b/src/qemu.rs @@ -89,6 +89,10 @@ impl Qemu { command.arg("-vnc").arg("none"); } + for fs in &vm.fs { + command.args(fs.to_qemu_arg()); + } + command .arg("-daemonize") .arg("-qmp") diff --git a/src/vm.rs b/src/vm.rs index aa19479..6d277b3 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -30,7 +30,9 @@ use crate::{ DownloadEvent, Event::{self, Download, VMStarted}, }, - firmware, get_kudu_data_dir, get_kudu_run_dir, + firmware, + fs::Filesystem, + get_kudu_data_dir, get_kudu_run_dir, network::{Network, NetworkBackend, bridge::Bridge}, notification::{self, Notification, NotificationLevel}, os::Os::{self, TempleOS}, @@ -54,6 +56,7 @@ pub struct VM { pub drives: Vec, pub networks: Vec, pub remote_access: Option, + pub fs: Vec, #[serde(skip)] pub downloading: Arc, @@ -272,6 +275,7 @@ impl VM { events_state: ListState::default(), vnc: None, networks: data.networks, + fs: data.fs, state: RunState::shutdown, remote_access: data.remote_access, }; @@ -337,6 +341,8 @@ impl VM { self.networks = data.networks; + self.fs = data.fs; + path.push("vm.json"); let mut file = File::create(&path)?; let vm = serde_json::to_string_pretty(&self)?; @@ -762,6 +768,46 @@ impl VM { lines } }), + ListItem::from({ + let mut lines = Vec::new(); + + if self.fs.is_empty() { + vec![ + Line::from(vec![ + Span::from("Filesystem").bold().fg(Color::Yellow), + Span::from(" ".repeat(3)), + Span::from(" - "), + ]), + Line::from(""), + ] + } else { + lines.push(Line::from(vec![ + Span::from("Filesystem").bold().fg(Color::Yellow), + Span::from(" ".repeat(7)), + Span::from("Source Path ").bold(), + Span::from(" ".repeat(10)), + Span::from(" Mount Tag ").bold(), + Span::from(" ".repeat(12)), + Span::from(" Readonly ").bold(), + Span::from(" ".repeat(2)), + Span::from(" Drive ").bold(), + ])); + lines.push(Line::from("")); + for fs in &self.fs { + lines.push(Line::from(vec![ + Span::from(" ".repeat(17)), + Span::from(format!("{:23}", fs.source_path.to_string_lossy())), + Span::from(format!("{:15}", fs.mount_tag)), + Span::from(" ".repeat(8)), + Span::from(format!("{:3}", if fs.readonly { "Yes" } else { "No" })), + Span::from(" ".repeat(9)), + Span::from(fs.driver.to_string()), + ])) + } + lines.push(Line::from("")); + lines + } + }), ListItem::from({ let mut lines = Vec::new(); if self.networks.is_empty() { diff --git a/src/vmbuilder.rs b/src/vmbuilder.rs index 2f6d949..e806391 100644 --- a/src/vmbuilder.rs +++ b/src/vmbuilder.rs @@ -1,5 +1,6 @@ mod access; mod advanced; +mod fs; mod hardware; mod overview; mod port; @@ -24,6 +25,7 @@ use crate::{ access::RemoteAccess, confirmation::cancel::CancelConfirmation, event::Event::{self}, + fs::Filesystem, network::Network, os::Os::{self}, storage::Disk, @@ -63,6 +65,7 @@ pub struct VMBuildData { pub networks: Vec, pub enable_uefi: bool, pub disks: Vec, + pub fs: Vec, pub remote_access: Option, } @@ -102,6 +105,11 @@ impl VMBuilder { return Ok(()); } + if advanced.fs.new_fs_popup() { + advanced.fs.handle_key_events(key_event); + return Ok(()); + } + if advanced.port_fowrwaring.new_mapping_popup() { advanced.port_fowrwaring.handle_key_events(key_event); return Ok(()); diff --git a/src/vmbuilder/advanced.rs b/src/vmbuilder/advanced.rs index 3636d99..dc42f29 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, hardware, overview, port, storage}, + vmbuilder::{VMBuildData, access, fs, hardware, overview, port, storage}, }; #[derive(Debug, Clone, PartialEq)] @@ -24,6 +24,7 @@ pub enum Section { Overview, Hardware, Storage, + Filesystem, Network, PortForwarding, RemoteAccess, @@ -37,8 +38,9 @@ pub struct Advanced { pub overview: overview::Overview, pub hardware: hardware::Hardware, pub storage: storage::Storage, + pub fs: fs::FsBuilder, pub network: network::builder::NetworkBuilder, - pub port_fowrwaring: port::PortForwaring, + pub port_fowrwaring: port::PortForwarding, pub remote_access: access::RemoteAccessBuilder, } @@ -51,13 +53,14 @@ impl Default for Advanced { impl Advanced { pub fn new() -> Advanced { let network = network::builder::NetworkBuilder::new(); - let port_fowrwaring = port::PortForwaring::new(network.networks()); + let port_fowrwaring = port::PortForwarding::new(network.networks()); Self { focused_section: Section::Overview, section_state: ListState::default().with_selected(Some(0)), overview: overview::Overview::new(), hardware: hardware::Hardware::new(), + fs: fs::FsBuilder::new(), storage: storage::Storage::new(), network, port_fowrwaring, @@ -78,6 +81,7 @@ impl Advanced { enable_uefi: self.hardware.enable_uefi(), networks: self.network.networks().take(), disks: self.storage.disks(), + fs: self.fs.filesystems(), remote_access: self.remote_access.access(), } } @@ -123,7 +127,11 @@ impl Advanced { self.focused_section = Section::Storage; } } - Section::Storage => self.focused_section = Section::Network, + Section::Storage => self.focused_section = Section::Filesystem, + Section::Filesystem => { + self.focused_section = Section::Network; + } + Section::Network => { self.port_fowrwaring.refresh(); self.focused_section = Section::PortForwarding @@ -157,9 +165,12 @@ impl Advanced { } } Section::Storage => self.focused_section = Section::Hardware, + Section::Filesystem => { + self.focused_section = Section::Storage; + } Section::Network => { self.port_fowrwaring.refresh(); - self.focused_section = Section::Storage + self.focused_section = Section::Filesystem } Section::PortForwarding => self.focused_section = Section::Network, Section::RemoteAccess => { @@ -181,6 +192,9 @@ impl Advanced { self.storage .handle_key_events(key_event, self.hardware.arch()); } + Section::Filesystem => { + self.fs.handle_key_events(key_event); + } Section::Network => { self.network.handle_key_events(key_event); } @@ -220,6 +234,11 @@ impl Advanced { Line::from(" Storage 󱛟 "), Line::from(""), ]), + ListItem::new(vec![ + Line::from(""), + Line::from(" Filesystem  "), + Line::from(""), + ]), ListItem::new(vec![ Line::from(""), Line::from(" Network 󰛳 "), @@ -227,7 +246,7 @@ impl Advanced { ]), ListItem::new(vec![ Line::from(""), - Line::from(" Port Forwaring  "), + Line::from(" Port Forwarding  "), Line::from(""), ]), ListItem::new(vec![ @@ -332,6 +351,10 @@ impl Advanced { self.storage.render(frame, area); } + Section::Filesystem => { + self.fs.render(frame, area); + } + Section::Network => { self.network.render(frame, area); } @@ -358,6 +381,7 @@ impl Advanced { items.extend(self.overview.summary()); items.extend(self.hardware.summary()); items.extend(self.storage.summary()); + items.extend(self.fs.summary()); items.extend(self.network.summary()); items.extend(self.port_fowrwaring.summary()); items.extend(self.remote_access.summary()); @@ -442,6 +466,49 @@ impl Advanced { ])] } } + Section::Filesystem => { + if self.fs.new_fs_popup() { + vec![Line::from(vec![ + Span::from("↑").bold(), + Span::from(" Up"), + Span::from(" | "), + Span::from("↓").bold(), + Span::from(" Down"), + Span::from(" | "), + Span::from("h,←").bold(), + Span::from(" Left"), + Span::from(" | "), + Span::from("l,→").bold(), + Span::from(" Right"), + Span::from(" | "), + Span::from("Esc").bold(), + Span::from(" Cancel"), + Span::from(" | "), + Span::from("Enter").bold(), + Span::from(" Confirm"), + ])] + } else { + vec![Line::from(vec![ + Span::from("k,↑").bold(), + Span::from(" Up"), + Span::from(" | "), + Span::from("j,↓").bold(), + Span::from(" Down"), + Span::from(" | "), + Span::from("n").bold(), + Span::from(" Add"), + Span::from(" | "), + Span::from("d").bold(), + Span::from(" Delete"), + Span::from(" | "), + Span::from("Esc").bold(), + Span::from(" Cancel"), + Span::from(" | "), + Span::from("⇄").bold(), + Span::from(" Nav"), + ])] + } + } Section::PortForwarding => { if self.port_fowrwaring.new_mapping_popup() { vec![Line::from(vec![ diff --git a/src/vmbuilder/fs.rs b/src/vmbuilder/fs.rs new file mode 100644 index 0000000..9a531b2 --- /dev/null +++ b/src/vmbuilder/fs.rs @@ -0,0 +1,186 @@ +use ratatui::{ + Frame, + layout::{Constraint, Direction, Flex, Layout, Margin, Rect}, + style::{Style, Stylize}, + text::{Line, Span, Text}, + widgets::{ListItem, Row, Table, TableState}, +}; + +use crossterm::event::{KeyCode, KeyEvent}; + +use crate::fs::{Filesystem, NewFs}; + +#[derive(Debug, Clone, Default)] +pub struct FsBuilder { + fs: Vec, + fs_state: TableState, + new_fs: Option, +} + +impl FsBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn new_fs_popup(&self) -> bool { + self.new_fs.is_some() + } + + pub fn filesystems(&self) -> Vec { + self.fs.clone() + } + + pub fn handle_key_events(&mut self, key_event: KeyEvent) { + if let Some(new_fs) = &mut self.new_fs { + match key_event.code { + KeyCode::Esc => { + self.new_fs = None; + } + KeyCode::Enter => { + if new_fs.validate() { + let fs = new_fs.build(); + + self.fs.push(fs); + self.new_fs = None; + + if self.fs_state.selected().is_none() { + self.fs_state.select(Some(0)); + } + } + } + + _ => { + new_fs.handle_key_events(key_event); + } + } + + return; + } + + match key_event.code { + KeyCode::Down | KeyCode::Char('j') => { + if let Some(index) = self.fs_state.selected() { + self.fs_state + .select(Some(index.saturating_add(1).min(self.fs.len() - 1))); + } + } + KeyCode::Up | KeyCode::Char('k') => { + if let Some(index) = self.fs_state.selected() { + self.fs_state.select(Some(index.saturating_sub(1))); + } + } + KeyCode::Char('d') => { + if let Some(index) = self.fs_state.selected() { + self.fs.remove(index); + } + } + KeyCode::Char('n') => { + self.new_fs = Some(NewFs::new()); + } + _ => {} + } + } + + pub fn summary(&self) -> Vec> { + vec![ListItem::from({ + let mut lines = Vec::new(); + + if self.fs.is_empty() { + vec![ + Line::from(vec![ + Span::from("Filesystem").bold(), + Span::from(" ".repeat(9)), + Span::from(" - "), + ]), + Line::from(""), + ] + } else { + lines.push(Line::from(vec![ + Span::from("Filesystem").bold(), + Span::from(" ".repeat(10)), + Span::from("Source Path ").bold(), + Span::from(" ".repeat(10)), + Span::from(" Mount Tag ").bold(), + Span::from(" ".repeat(12)), + Span::from(" Readonly ").bold(), + Span::from(" ".repeat(2)), + Span::from(" Drive ").bold(), + ])); + lines.push(Line::from("")); + for fs in &self.fs { + lines.push(Line::from(vec![ + Span::from(" ".repeat(20)), + Span::from(format!("{:23}", fs.source_path.to_string_lossy())), + Span::from(format!("{:15}", fs.mount_tag)), + Span::from(" ".repeat(8)), + Span::from(format!("{:3}", if fs.readonly { "Yes" } else { "No" })), + Span::from(" ".repeat(9)), + Span::from(fs.driver.to_string()), + ])) + } + lines.push(Line::from("")); + lines + } + })] + } + pub fn render(&mut self, frame: &mut Frame, area: Rect) { + if self.fs.is_empty() { + let area = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(1), + Constraint::Fill(1), + ]) + .flex(Flex::Center) + .split(area)[1]; + + let message = Text::from("Press n to add a shared fs").centered(); + frame.render_widget(message, area); + } else { + let widths = [ + Constraint::Length(20), + Constraint::Length(20), + Constraint::Length(8), + Constraint::Length(10), + ]; + let fs = self.fs.iter().map(|fs| { + Row::new(vec![ + fs.source_path.to_string_lossy().to_string(), + fs.mount_tag.clone(), + { + if fs.readonly { + "Yes".to_string() + } else { + "No".to_string() + } + }, + fs.driver.to_string(), + ]) + }); + + let fs = Table::new(fs, widths) + .header( + Row::new(vec!["Source Path", "Mount Tag", "Readonly", "Driver"]) + .style(Style::new().bold()) + .bottom_margin(1), + ) + .flex(ratatui::layout::Flex::SpaceAround) + .row_highlight_style(Style::new().on_dark_gray()) + .column_spacing(1); + + frame.render_stateful_widget( + fs, + area.inner(Margin { + horizontal: 2, + vertical: 2, + }), + &mut self.fs_state, + ); + } + + if let Some(new_fs) = &self.new_fs { + new_fs.render(frame); + } + } +} diff --git a/src/vmbuilder/port.rs b/src/vmbuilder/port.rs index cd2cdaa..fecf962 100644 --- a/src/vmbuilder/port.rs +++ b/src/vmbuilder/port.rs @@ -16,16 +16,16 @@ use crate::network::{ }; #[derive(Debug, Clone)] -pub struct PortForwaring { +pub struct PortForwarding { mappings: Vec<(NetworkId, PortMapping)>, mapping_state: TableState, new_mapping: Option, networks: Rc>>, } -impl PortForwaring { +impl PortForwarding { pub fn new(networks: Rc>>) -> Self { - PortForwaring { + PortForwarding { mappings: Vec::new(), mapping_state: TableState::new(), new_mapping: None, @@ -132,7 +132,7 @@ impl PortForwaring { vec![ Line::from(vec![ Span::from("Port Forwarding ").bold(), - Span::from(" ".repeat(4)), + Span::from(" ".repeat(3)), Span::from(" - "), ]), Line::from(""), diff --git a/src/vmbuilder/quick.rs b/src/vmbuilder/quick.rs index 358361e..762ee57 100644 --- a/src/vmbuilder/quick.rs +++ b/src/vmbuilder/quick.rs @@ -304,6 +304,7 @@ impl Quick { enable_uefi: true, networks: vec![network::Network::default()], disks: Vec::new(), + fs: Vec::new(), cloudinit: Some(self.cloudinit().unwrap()), //FIX: remote_access, } diff --git a/src/vmedit.rs b/src/vmedit.rs index b26dbfe..4762d85 100644 --- a/src/vmedit.rs +++ b/src/vmedit.rs @@ -1,3 +1,4 @@ +pub mod fs; pub mod network; pub mod port; pub mod storage; @@ -19,6 +20,7 @@ use crate::{ Arch, access::{RemoteAccess, vnc::VncBuilder}, event::Event, + fs::Filesystem, network::Network, storage::{Disk, Drive, Interface}, vm::{VM, VmId}, @@ -35,6 +37,7 @@ struct UserInputField { enum Section { Hardware(HardwareSection), Storage, + Filesystem, Network, PortForwarding, RemoteAccess, @@ -45,9 +48,10 @@ impl Section { match self { Section::Hardware(_) => 0, Section::Storage => 1, - Section::Network => 2, - Section::PortForwarding => 3, - Section::RemoteAccess => 4, + Section::Filesystem => 2, + Section::Network => 3, + Section::PortForwarding => 4, + Section::RemoteAccess => 5, } } } @@ -66,6 +70,7 @@ pub struct EditVM { vcpu: UserInputField, memory: UserInputField, storage: storage::StorageEdit, + fs: fs::FsEdit, network: network::NetworkEdit, port_forwarding: port::PortForwarding, vnc: VncBuilder, @@ -82,6 +87,7 @@ pub struct VMEditData { pub new_memory: u32, pub networks: Vec, pub remote_access: Option, + pub fs: Vec, } impl EditVM { @@ -116,6 +122,7 @@ impl EditVM { error: None, }, storage: storage::StorageEdit::new(drives), + fs: fs::FsEdit::new(vm.fs.clone()), network: network::NetworkEdit::new(networks.clone()), port_forwarding: port::PortForwarding::new(networks), vnc, @@ -127,6 +134,7 @@ impl EditVM { self.port_forwarding.new_mapping_popup() | self.storage.new_drive_popup() | self.network.new_network_popup() + | self.fs.new_fs_popup() } fn validate_harware_section(&mut self) -> bool { @@ -188,6 +196,11 @@ impl EditVM { return Ok(()); } + if self.fs.new_fs_popup() { + self.fs.handle_key_events(key_event); + return Ok(()); + } + if self.network.new_network_popup() { self.network.handle_key_events(key_event); return Ok(()); @@ -221,6 +234,7 @@ impl EditVM { new_vcpu: self.vcpu.field.value().parse::().unwrap(), new_memory: self.memory.field.value().parse::().unwrap(), networks, + fs: self.fs.build(), remote_access: self.vnc.build().map(RemoteAccess::Vnc), })); } @@ -231,6 +245,9 @@ impl EditVM { } } Section::Storage => { + self.section = Section::Filesystem; + } + Section::Filesystem => { self.section = Section::Network; } Section::Network => { @@ -252,10 +269,16 @@ impl EditVM { self.section = Section::RemoteAccess; } } - Section::Storage => self.section = Section::Hardware(HardwareSection::default()), + Section::Storage => { + self.section = Section::Hardware(HardwareSection::default()); + } + + Section::Filesystem => { + self.section = Section::Storage; + } Section::Network => { self.port_forwarding.refresh(); - self.section = Section::Storage; + self.section = Section::Filesystem; } Section::PortForwarding => { self.section = Section::Network; @@ -299,6 +322,11 @@ impl EditVM { Section::Storage => { self.storage.handle_key_events(key_event, arch); } + + Section::Filesystem => { + self.fs.handle_key_events(key_event); + } + Section::Network => { self.network.handle_key_events(key_event); } @@ -328,6 +356,11 @@ impl EditVM { Line::from(" Storage 󱛟 "), Line::from(""), ]), + ListItem::new(vec![ + Line::from(""), + Line::from(" Filesystem  "), + Line::from(""), + ]), ListItem::new(vec![ Line::from(""), Line::from(" Network 󰛳 "), @@ -335,7 +368,7 @@ impl EditVM { ]), ListItem::new(vec![ Line::from(""), - Line::from(" Port Forwaring  "), + Line::from(" Port Forwarding  "), Line::from(""), ]), ListItem::new(vec![ @@ -514,6 +547,9 @@ impl EditVM { Section::Storage => { self.storage.render(frame, area); } + Section::Filesystem => { + self.fs.render(frame, area); + } Section::Network => { self.network.render(frame, area); } @@ -546,6 +582,69 @@ impl EditVM { Span::from(" Nav"), ])] } + Section::Filesystem => { + if self.fs.new_fs_popup() { + self.fs.help() + } else { + if block_width >= 113 { + vec![Line::from(vec![ + Span::from("k,↑").bold(), + Span::from(" Up"), + Span::from(" | "), + Span::from("j,↓").bold(), + Span::from(" Down"), + Span::from(" | "), + Span::from("n").bold(), + Span::from(" Add"), + Span::from(" | "), + Span::from("d").bold(), + Span::from(" Delete"), + Span::from(" | "), + Span::from("u").bold(), + Span::from(" Undo"), + Span::from(" | "), + Span::from("Esc").bold(), + Span::from(" Cancel"), + Span::from(" | "), + Span::from("Enter").bold(), + Span::from(" Confirm"), + Span::from(" | "), + Span::from("⇄").bold(), + Span::from(" Nav"), + ])] + } else { + vec![ + Line::from(vec![ + Span::from("k,↑").bold(), + Span::from(" Up"), + Span::from(" | "), + Span::from("j,↓").bold(), + Span::from(" Down"), + Span::from(" | "), + Span::from("n").bold(), + Span::from(" Add"), + Span::from(" | "), + Span::from("d").bold(), + Span::from(" Delete"), + Span::from(" | "), + Span::from("u").bold(), + Span::from(" Undo"), + Span::from(" | "), + ]), + Line::from(vec![ + Span::from("Esc").bold(), + Span::from(" Cancel"), + Span::from(" | "), + Span::from("Enter").bold(), + Span::from(" Confirm"), + Span::from(" | "), + Span::from("⇄").bold(), + Span::from(" Nav"), + ]), + ] + } + } + } Section::Storage if self.storage.new_drive_popup() => self.storage.help(), _ => { if self.new_popup() { diff --git a/src/vmedit/fs.rs b/src/vmedit/fs.rs new file mode 100644 index 0000000..9db3981 --- /dev/null +++ b/src/vmedit/fs.rs @@ -0,0 +1,218 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Constraint, Rect}, + style::{Style, Stylize}, + text::{Line, Span}, + widgets::{Row, Table, TableState}, +}; + +use crate::fs::{Filesystem, NewFs}; + +#[derive(Debug, Clone)] +pub struct FsEdit { + fs_state: TableState, + new_fs: Option, + added_fs: Vec, + deleted_fs: Vec, + fs: Vec, +} + +impl FsEdit { + pub fn new(fs: Vec) -> Self { + let fs_state = if fs.is_empty() { + TableState::default() + } else { + TableState::default().with_selected(Some(0)) + }; + + Self { + fs_state, + new_fs: None, + added_fs: Vec::new(), + deleted_fs: Vec::new(), + fs, + } + } + + pub fn new_fs_popup(&self) -> bool { + self.new_fs.is_some() + } + + pub fn build(&self) -> Vec { + let mut fs = self.fs.clone(); + + for deleted_fs in &self.deleted_fs { + fs.retain(|f| f != deleted_fs); + } + + fs.extend(self.added_fs.clone()); + + fs + } + + pub fn handle_key_events(&mut self, key_event: KeyEvent) { + if let Some(new_fs) = &mut self.new_fs { + match key_event.code { + KeyCode::Esc => { + self.new_fs = None; + } + KeyCode::Enter => { + if new_fs.validate() { + let fs = new_fs.build(); + + self.added_fs.push(fs); + self.new_fs = None; + + if self.fs_state.selected().is_none() { + self.fs_state.select(Some(0)); + } + } + } + + _ => { + new_fs.handle_key_events(key_event); + } + } + + return; + } + + match key_event.code { + KeyCode::Down | KeyCode::Char('j') => { + if let Some(index) = self.fs_state.selected() { + self.fs_state.select(Some( + index + .saturating_add(1) + .min(self.fs.len() + self.added_fs.len() - 1), + )); + } + } + KeyCode::Up | KeyCode::Char('k') => { + if let Some(index) = self.fs_state.selected() { + self.fs_state.select(Some(index.saturating_sub(1))); + } + } + KeyCode::Char('d') => { + if let Some(index) = self.fs_state.selected() { + if index < self.fs.len() { + let fs = &self.fs[index]; + self.deleted_fs.push(fs.clone()); + } else { + let index = index.saturating_sub(self.fs.len()); + self.added_fs.remove(index); + } + } + } + KeyCode::Char('u') => { + if let Some(index) = self.fs_state.selected() + && let Some(fs) = self.fs.get(index) + { + self.deleted_fs.retain(|deleted_fs| deleted_fs != fs); + } + } + KeyCode::Char('n') => { + self.new_fs = Some(NewFs::new()); + } + _ => {} + } + } + + pub fn render(&mut self, frame: &mut Frame, area: Rect) { + let widths = [ + Constraint::Length(6), + Constraint::Length(20), + Constraint::Length(20), + Constraint::Length(8), + Constraint::Length(10), + ]; + + let vm_fs = self.fs.iter().map(|fs| { + let to_delete = self.deleted_fs.contains(fs); + Row::new(vec![ + Line::from({ + if to_delete { + "Delete".to_string() + } else { + String::new() + } + }), + Line::from(fs.source_path.to_string_lossy().to_string()), + Line::from(fs.mount_tag.clone()), + Line::from({ + if fs.readonly { + "Yes".to_string() + } else { + "No".to_string() + } + }), + Line::from(fs.driver.to_string()), + ]) + .style(if to_delete { + Style::new().red() + } else { + Style::default() + }) + }); + + let added_fs = self.added_fs.iter().map(|fs| { + Row::new(vec![ + Line::from("New"), + Line::from(fs.source_path.to_string_lossy().to_string()), + Line::from(fs.mount_tag.clone()), + Line::from({ + if fs.readonly { + "Yes".to_string() + } else { + "No".to_string() + } + }), + Line::from(fs.driver.to_string()), + ]) + .green() + }); + + let mut fs = Vec::new(); + + fs.extend(vm_fs); + fs.extend(added_fs); + + let fs = Table::new(fs, widths) + .header( + Row::new(vec!["", "Source Path", "Mount Tag", "Readonly", "Driver"]) + .style(Style::new().bold()) + .bottom_margin(1), + ) + .flex(ratatui::layout::Flex::SpaceAround) + .row_highlight_style(Style::new().on_dark_gray()) + .column_spacing(1); + + frame.render_stateful_widget(fs, area, &mut self.fs_state); + + if let Some(new_fs) = &self.new_fs { + new_fs.render(frame); + } + } + + pub fn help(&self) -> Vec> { + vec![Line::from(vec![ + Span::from("↑").bold(), + Span::from(" Up"), + Span::from(" | "), + Span::from("↓").bold(), + Span::from(" Down"), + Span::from(" | "), + Span::from("h,←").bold(), + Span::from(" Left"), + Span::from(" | "), + Span::from("l,→").bold(), + Span::from(" Right"), + Span::from(" | "), + Span::from("Esc").bold(), + Span::from(" Cancel"), + Span::from(" | "), + Span::from("Enter").bold(), + Span::from(" Confirm"), + ])] + } +}