feat: added explain command to display network

This commit is contained in:
robcholz
2026-02-07 16:37:31 -05:00
parent 94d941c21a
commit fa221419dd
3 changed files with 145 additions and 10 deletions

View File

@@ -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<Vec<tui::Moun
Ok(rows)
}
fn build_network_rows(cwd: &Path) -> Result<Vec<tui::NetworkListRow>> {
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: <pending>: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<Vec<tui::MountListRow>> {
let project_name = cwd
.file_name()

View File

@@ -162,6 +162,25 @@ pub(crate) fn load_or_create_instance_config(
Ok(config)
}
fn read_instance_config(
instance_dir: &Path,
) -> Result<Option<InstanceConfig>, Box<dyn std::error::Error>> {
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::<InstanceConfig>(&raw)?;
Ok(Some(config))
}
pub fn read_instance_vm_ip(
instance_dir: &Path,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
let mut config = load_or_create_instance_config(instance_dir)?;
let now = OffsetDateTime::now_utc().format(&Rfc3339)?;

View File

@@ -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(