on main help sign in
feat: added explain command to display network
fa22141robcholz
src/bin/vibebox-cli.rs+25−4
@@ -168,12 +168,13 @@ fn handle_command(command: Command, cwd: &PathBuf, config_override: Option<&Path
168 }
168 }
169 Command::Explain => {
169 Command::Explain => {
170 let config = config::load_config_with_path(cwd, config_override);
170 let config = config::load_config_with_path(cwd, config_override);
171− let rows = build_mount_rows(cwd, &config)?;
171+ let mounts = build_mount_rows(cwd, &config)?;
172+ let networks = build_network_rows(cwd)?;
172− if rows.is_empty() {
173+ if mounts.is_empty() && networks.is_empty() {
173− println!("No mounts configured.");
174+ println!("No mounts or network info available.");
174 return Ok(());
175 return Ok(());
175 }
176 }
176− tui::render_mounts_table(&rows)?;
177+ tui::render_explain_tables(&mounts, &networks)?;
177 Ok(())
178 Ok(())
178 }
179 }
179 }
180 }
@@ -252,6 +253,26 @@ fn build_mount_rows(cwd: &Path, config: &config::Config) -> Result<Vec<tui::Moun
252 Ok(rows)
253 Ok(rows)
253}
254}
254
255
256+fn build_network_rows(cwd: &Path) -> Result<Vec<tui::NetworkListRow>> {
257+ let instance_dir = cwd.join(session_manager::INSTANCE_DIR_NAME);
258+ let mut vm_ip = "-".to_string();
259+ if let Ok(Some(ip)) = instance::read_instance_vm_ip(&instance_dir) {
260+ vm_ip = ip;
261+ }
262+ let host_to_vm = if vm_ip == "-" {
263+ "ssh: <pending>:22".to_string()
264+ } else {
265+ format!("ssh: {vm_ip}:22")
266+ };
267+ let row = tui::NetworkListRow {
268+ network_type: "NAT".to_string(),
269+ vm_ip: vm_ip.clone(),
270+ host_to_vm,
271+ vm_to_host: "none".to_string(),
272+ };
273+ Ok(vec![row])
274+}
275+
255fn default_mounts(cwd: &Path) -> Result<Vec<tui::MountListRow>> {
276fn default_mounts(cwd: &Path) -> Result<Vec<tui::MountListRow>> {
256 let project_name = cwd
277 let project_name = cwd
257 .file_name()
278 .file_name()
src/instance.rs+19
@@ -162,6 +162,25 @@ pub(crate) fn load_or_create_instance_config(
162 Ok(config)
162 Ok(config)
163}
163}
164
164
165+fn read_instance_config(
166+ instance_dir: &Path,
167+) -> Result<Option<InstanceConfig>, Box<dyn std::error::Error>> {
168+ let config_path = instance_dir.join(INSTANCE_FILENAME);
169+ if !config_path.exists() {
170+ return Ok(None);
171+ }
172+ let raw = fs::read_to_string(&config_path)?;
173+ let config = toml::from_str::<InstanceConfig>(&raw)?;
174+ Ok(Some(config))
175+}
176+
177+pub fn read_instance_vm_ip(
178+ instance_dir: &Path,
179+) -> Result<Option<String>, Box<dyn std::error::Error>> {
180+ let config = read_instance_config(instance_dir)?;
181+ Ok(config.and_then(|cfg| cfg.vm_ipv4))
182+}
183+
165pub fn touch_last_active(instance_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
184pub fn touch_last_active(instance_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
166 let mut config = load_or_create_instance_config(instance_dir)?;
185 let mut config = load_or_create_instance_config(instance_dir)?;
167 let now = OffsetDateTime::now_utc().format(&Rfc3339)?;
186 let now = OffsetDateTime::now_utc().format(&Rfc3339)?;
src/tui.rs+101−6
@@ -115,6 +115,14 @@ pub struct MountListRow {
115 pub default_mount: String,
115 pub default_mount: String,
116}
116}
117
117
118+#[derive(Debug, Clone)]
119+pub struct NetworkListRow {
120+ pub network_type: String,
121+ pub vm_ip: String,
122+ pub host_to_vm: String,
123+ pub vm_to_host: String,
124+}
125+
118#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
126#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
119struct PageLayout {
127struct PageLayout {
120 header: Rect,
128 header: Rect,
@@ -253,6 +261,65 @@ pub fn render_mounts_table(rows: &[MountListRow]) -> Result<()> {
253 let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
261 let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
254 let area = Rect::new(0, 0, width, height);
262 let area = Rect::new(0, 0, width, height);
255
263
264+ render_mounts_table_into(rows, area, &mut buffer);
265+
266+ let mut stdout = io::stdout();
267+ execute!(stdout, Clear(ClearType::All), MoveTo(0, 0), Show)?;
268+ write_buffer_with_style(&buffer, &mut stdout)?;
269+ stdout.flush()?;
270+ Ok(())
271+}
272+
273+pub fn render_explain_tables(mounts: &[MountListRow], networks: &[NetworkListRow]) -> Result<()> {
274+ let (width, _) = crossterm::terminal::size()?;
275+ if width == 0 {
276+ return Ok(());
277+ }
278+
279+ let mounts_height = if mounts.is_empty() {
280+ 0
281+ } else {
282+ (mounts.len() as u16).saturating_add(3)
283+ };
284+ let networks_height = if networks.is_empty() {
285+ 0
286+ } else {
287+ (networks.len() as u16).saturating_add(3)
288+ };
289+ let gap = if mounts_height > 0 && networks_height > 0 {
290+ 1
291+ } else {
292+ 0
293+ };
294+ let total_height = mounts_height
295+ .saturating_add(gap)
296+ .saturating_add(networks_height);
297+ if total_height == 0 {
298+ return Ok(());
299+ }
300+
301+ let mut buffer = Buffer::empty(Rect::new(0, 0, width, total_height));
302+ let mut y = 0u16;
303+
304+ if mounts_height > 0 {
305+ let area = Rect::new(0, y, width, mounts_height);
306+ render_mounts_table_into(mounts, area, &mut buffer);
307+ y = y.saturating_add(mounts_height).saturating_add(gap);
308+ }
309+
310+ if networks_height > 0 {
311+ let area = Rect::new(0, y, width, networks_height);
312+ render_networks_table_into(networks, area, &mut buffer);
313+ }
314+
315+ let mut stdout = io::stdout();
316+ execute!(stdout, Clear(ClearType::All), MoveTo(0, 0), Show)?;
317+ write_buffer_with_style(&buffer, &mut stdout)?;
318+ stdout.flush()?;
319+ Ok(())
320+}
321+
322+fn render_mounts_table_into(rows: &[MountListRow], area: Rect, buffer: &mut Buffer) {
256 let header = Row::new(vec![
323 let header = Row::new(vec![
257 Cell::from("Host"),
324 Cell::from("Host"),
258 Cell::from("Guest"),
325 Cell::from("Guest"),
@@ -286,13 +353,41 @@ pub fn render_mounts_table(rows: &[MountListRow]) -> Result<()> {
286 .block(Block::default().title("Mounts").borders(Borders::ALL))
353 .block(Block::default().title("Mounts").borders(Borders::ALL))
287 .column_spacing(1);
354 .column_spacing(1);
288
355
289− table.render(area, &mut buffer);
356+ table.render(area, buffer);
357+}
290
358
291− let mut stdout = io::stdout();
359+fn render_networks_table_into(rows: &[NetworkListRow], area: Rect, buffer: &mut Buffer) {
292− execute!(stdout, Clear(ClearType::All), MoveTo(0, 0), Show)?;
360+ let header = Row::new(vec![
293− write_buffer_with_style(&buffer, &mut stdout)?;
361+ Cell::from("Type"),
294− stdout.flush()?;
362+ Cell::from("VM IP"),
295− Ok(())
363+ Cell::from("Host \u{2192} VM"),
364+ Cell::from("VM \u{2192} Host"),
365+ ])
366+ .style(Style::default().fg(Color::Cyan));
367+
368+ let table_rows = rows.iter().map(|row| {
369+ Row::new(vec![
370+ Cell::from(row.network_type.clone()),
371+ Cell::from(row.vm_ip.clone()),
372+ Cell::from(row.host_to_vm.clone()),
373+ Cell::from(row.vm_to_host.clone()),
374+ ])
375+ });
376+
377+ let table = Table::new(
378+ table_rows,
379+ [
380+ Constraint::Length(8),
381+ Constraint::Length(16),
382+ Constraint::Min(24),
383+ Constraint::Min(20),
384+ ],
385+ )
386+ .header(header)
387+ .block(Block::default().title("Network").borders(Borders::ALL))
388+ .column_spacing(1);
389+
390+ table.render(area, buffer);
296}
391}
297
392
298pub fn passthrough_vm_io(
393pub fn passthrough_vm_io(