From fa221419ddd4f08acbcdf36ee9bd966f31be21fd Mon Sep 17 00:00:00 2001 From: robcholz <84130577+robcholz@users.noreply.github.com> Date: Sat, 7 Feb 2026 16:37:31 -0500 Subject: [PATCH] feat: added explain command to display network --- src/bin/vibebox-cli.rs | 29 +++++++++-- src/instance.rs | 19 ++++++++ src/tui.rs | 107 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 145 insertions(+), 10 deletions(-) diff --git a/src/bin/vibebox-cli.rs b/src/bin/vibebox-cli.rs index e79300b..377b062 100644 --- a/src/bin/vibebox-cli.rs +++ b/src/bin/vibebox-cli.rs @@ -168,12 +168,13 @@ fn handle_command(command: Command, cwd: &PathBuf, config_override: Option<&Path } Command::Explain => { let config = config::load_config_with_path(cwd, config_override); - let rows = build_mount_rows(cwd, &config)?; - if rows.is_empty() { - println!("No mounts configured."); + let mounts = build_mount_rows(cwd, &config)?; + let networks = build_network_rows(cwd)?; + if mounts.is_empty() && networks.is_empty() { + println!("No mounts or network info available."); return Ok(()); } - tui::render_mounts_table(&rows)?; + tui::render_explain_tables(&mounts, &networks)?; Ok(()) } } @@ -252,6 +253,26 @@ fn build_mount_rows(cwd: &Path, config: &config::Config) -> Result Result> { + let instance_dir = cwd.join(session_manager::INSTANCE_DIR_NAME); + let mut vm_ip = "-".to_string(); + if let Ok(Some(ip)) = instance::read_instance_vm_ip(&instance_dir) { + vm_ip = ip; + } + let host_to_vm = if vm_ip == "-" { + "ssh: :22".to_string() + } else { + format!("ssh: {vm_ip}:22") + }; + let row = tui::NetworkListRow { + network_type: "NAT".to_string(), + vm_ip: vm_ip.clone(), + host_to_vm, + vm_to_host: "none".to_string(), + }; + Ok(vec![row]) +} + fn default_mounts(cwd: &Path) -> Result> { let project_name = cwd .file_name() diff --git a/src/instance.rs b/src/instance.rs index c31ba9c..b578830 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -162,6 +162,25 @@ pub(crate) fn load_or_create_instance_config( Ok(config) } +fn read_instance_config( + instance_dir: &Path, +) -> Result, Box> { + let config_path = instance_dir.join(INSTANCE_FILENAME); + if !config_path.exists() { + return Ok(None); + } + let raw = fs::read_to_string(&config_path)?; + let config = toml::from_str::(&raw)?; + Ok(Some(config)) +} + +pub fn read_instance_vm_ip( + instance_dir: &Path, +) -> Result, Box> { + let config = read_instance_config(instance_dir)?; + Ok(config.and_then(|cfg| cfg.vm_ipv4)) +} + pub fn touch_last_active(instance_dir: &Path) -> Result<(), Box> { let mut config = load_or_create_instance_config(instance_dir)?; let now = OffsetDateTime::now_utc().format(&Rfc3339)?; diff --git a/src/tui.rs b/src/tui.rs index 4b12a9f..1a4371c 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -115,6 +115,14 @@ pub struct MountListRow { pub default_mount: String, } +#[derive(Debug, Clone)] +pub struct NetworkListRow { + pub network_type: String, + pub vm_ip: String, + pub host_to_vm: String, + pub vm_to_host: String, +} + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] struct PageLayout { header: Rect, @@ -253,6 +261,65 @@ pub fn render_mounts_table(rows: &[MountListRow]) -> Result<()> { let mut buffer = Buffer::empty(Rect::new(0, 0, width, height)); let area = Rect::new(0, 0, width, height); + render_mounts_table_into(rows, area, &mut buffer); + + let mut stdout = io::stdout(); + execute!(stdout, Clear(ClearType::All), MoveTo(0, 0), Show)?; + write_buffer_with_style(&buffer, &mut stdout)?; + stdout.flush()?; + Ok(()) +} + +pub fn render_explain_tables(mounts: &[MountListRow], networks: &[NetworkListRow]) -> Result<()> { + let (width, _) = crossterm::terminal::size()?; + if width == 0 { + return Ok(()); + } + + let mounts_height = if mounts.is_empty() { + 0 + } else { + (mounts.len() as u16).saturating_add(3) + }; + let networks_height = if networks.is_empty() { + 0 + } else { + (networks.len() as u16).saturating_add(3) + }; + let gap = if mounts_height > 0 && networks_height > 0 { + 1 + } else { + 0 + }; + let total_height = mounts_height + .saturating_add(gap) + .saturating_add(networks_height); + if total_height == 0 { + return Ok(()); + } + + let mut buffer = Buffer::empty(Rect::new(0, 0, width, total_height)); + let mut y = 0u16; + + if mounts_height > 0 { + let area = Rect::new(0, y, width, mounts_height); + render_mounts_table_into(mounts, area, &mut buffer); + y = y.saturating_add(mounts_height).saturating_add(gap); + } + + if networks_height > 0 { + let area = Rect::new(0, y, width, networks_height); + render_networks_table_into(networks, area, &mut buffer); + } + + let mut stdout = io::stdout(); + execute!(stdout, Clear(ClearType::All), MoveTo(0, 0), Show)?; + write_buffer_with_style(&buffer, &mut stdout)?; + stdout.flush()?; + Ok(()) +} + +fn render_mounts_table_into(rows: &[MountListRow], area: Rect, buffer: &mut Buffer) { let header = Row::new(vec![ Cell::from("Host"), Cell::from("Guest"), @@ -286,13 +353,41 @@ pub fn render_mounts_table(rows: &[MountListRow]) -> Result<()> { .block(Block::default().title("Mounts").borders(Borders::ALL)) .column_spacing(1); - table.render(area, &mut buffer); + table.render(area, buffer); +} - let mut stdout = io::stdout(); - execute!(stdout, Clear(ClearType::All), MoveTo(0, 0), Show)?; - write_buffer_with_style(&buffer, &mut stdout)?; - stdout.flush()?; - Ok(()) +fn render_networks_table_into(rows: &[NetworkListRow], area: Rect, buffer: &mut Buffer) { + let header = Row::new(vec![ + Cell::from("Type"), + Cell::from("VM IP"), + Cell::from("Host \u{2192} VM"), + Cell::from("VM \u{2192} Host"), + ]) + .style(Style::default().fg(Color::Cyan)); + + let table_rows = rows.iter().map(|row| { + Row::new(vec![ + Cell::from(row.network_type.clone()), + Cell::from(row.vm_ip.clone()), + Cell::from(row.host_to_vm.clone()), + Cell::from(row.vm_to_host.clone()), + ]) + }); + + let table = Table::new( + table_rows, + [ + Constraint::Length(8), + Constraint::Length(16), + Constraint::Min(24), + Constraint::Min(20), + ], + ) + .header(header) + .block(Block::default().title("Network").borders(Borders::ALL)) + .column_spacing(1); + + table.render(area, buffer); } pub fn passthrough_vm_io(