feat: added explain command to display mounts

This commit is contained in:
robcholz
2026-02-07 16:31:32 -05:00
parent c98bc78f55
commit 94d941c21a
3 changed files with 171 additions and 5 deletions

View File

@@ -42,11 +42,11 @@
5. [x] wire up SessionManager.
6. [x] VM should be separated by a per-session VM daemon process (only accepts if to shut down vm and itself).
7. [x] setup vibebox commands
8. [ ] setup cli commands.
8. [x] setup cli commands.
1. [x] Organize all the params.
2. [x] Remove old cli.
3. [x] add an actual config file.
4. [ ] set up the cli.
4. [x] set up the cli.
9. [ ] fix ui overlap, and consistency issue.
10. [ ] intensive integration test.

View File

@@ -2,7 +2,7 @@ use std::{
env,
ffi::OsString,
io::{self, IsTerminal, Write},
path::PathBuf,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
@@ -32,6 +32,8 @@ enum Command {
List,
/// Delete the current project's .vibebox directory
Clean,
/// Explain mounts and mappings
Explain,
}
fn main() -> Result<()> {
@@ -42,7 +44,7 @@ fn main() -> Result<()> {
let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
if let Some(command) = cli.command {
return handle_command(command, &cwd);
return handle_command(command, &cwd, cli.config.as_deref());
}
let config_override = cli.config.clone();
@@ -111,7 +113,7 @@ fn main() -> Result<()> {
Ok(())
}
fn handle_command(command: Command, cwd: &PathBuf) -> Result<()> {
fn handle_command(command: Command, cwd: &PathBuf, config_override: Option<&Path>) -> Result<()> {
match command {
Command::List => {
let manager = SessionManager::new()?;
@@ -164,6 +166,16 @@ fn handle_command(command: Command, cwd: &PathBuf) -> Result<()> {
);
Ok(())
}
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.");
return Ok(());
}
tui::render_mounts_table(&rows)?;
Ok(())
}
}
}
@@ -231,6 +243,100 @@ fn format_last_active(value: Option<&str>) -> String {
format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
}
fn build_mount_rows(cwd: &Path, config: &config::Config) -> Result<Vec<tui::MountListRow>> {
let mut rows = Vec::new();
rows.extend(default_mounts(cwd)?);
for spec in &config.box_cfg.mounts {
rows.push(parse_mount_spec(cwd, spec, false)?);
}
Ok(rows)
}
fn default_mounts(cwd: &Path) -> Result<Vec<tui::MountListRow>> {
let project_name = cwd
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("project");
let project_guest = format!("/root/{project_name}");
let project_host = relative_to_home(&cwd.to_path_buf());
let mut rows = vec![tui::MountListRow {
host: project_host,
guest: project_guest,
mode: "read-write".to_string(),
default_mount: "yes".to_string(),
}];
let home = env::var("HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/"));
let cache_home = env::var("XDG_CACHE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| home.join(".cache"));
let cache_dir = cache_home.join(session_manager::GLOBAL_CACHE_DIR_NAME);
let guest_mise_cache = cache_dir.join(".guest-mise-cache");
rows.push(tui::MountListRow {
host: relative_to_home(&guest_mise_cache),
guest: "/root/.local/share/mise".to_string(),
mode: "read-write".to_string(),
default_mount: "yes".to_string(),
});
Ok(rows)
}
fn parse_mount_spec(cwd: &Path, spec: &str, default_mount: bool) -> Result<tui::MountListRow> {
let parts: Vec<&str> = spec.split(':').collect();
if parts.len() < 2 || parts.len() > 3 {
return Err(color_eyre::eyre::eyre!("invalid mount spec: {spec}"));
}
let host_part = parts[0];
let guest_part = parts[1];
let mode = if parts.len() == 3 {
match parts[2] {
"read-only" => "read-only",
"read-write" => "read-write",
other => {
return Err(color_eyre::eyre::eyre!(
"invalid mount mode '{}'; expected read-only or read-write",
other
));
}
}
} else {
"read-write"
};
let host_path = resolve_host_path(cwd, host_part);
let host_display = relative_to_home(&host_path);
let guest_display = if Path::new(guest_part).is_absolute() {
guest_part.to_string()
} else {
format!("/root/{guest_part}")
};
Ok(tui::MountListRow {
host: host_display,
guest: guest_display,
mode: mode.to_string(),
default_mount: if default_mount { "yes" } else { "no" }.to_string(),
})
}
fn resolve_host_path(cwd: &Path, host: &str) -> PathBuf {
if let Some(stripped) = host.strip_prefix("~/") {
if let Ok(home) = env::var("HOME") {
return PathBuf::from(home).join(stripped);
}
} else if host == "~" {
if let Ok(home) = env::var("HOME") {
return PathBuf::from(home);
}
}
let host_path = PathBuf::from(host);
if host_path.is_absolute() {
host_path
} else {
cwd.join(host_path)
}
}
fn init_tracing() {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err();

View File

@@ -107,6 +107,14 @@ pub struct SessionListRow {
pub id: String,
}
#[derive(Debug, Clone)]
pub struct MountListRow {
pub host: String,
pub guest: String,
pub mode: String,
pub default_mount: String,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct PageLayout {
header: Rect,
@@ -235,6 +243,58 @@ pub fn render_sessions_table(rows: &[SessionListRow]) -> Result<()> {
Ok(())
}
pub fn render_mounts_table(rows: &[MountListRow]) -> Result<()> {
let (width, _) = crossterm::terminal::size()?;
if width == 0 {
return Ok(());
}
let height = (rows.len() as u16).saturating_add(3);
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
let area = Rect::new(0, 0, width, height);
let header = Row::new(vec![
Cell::from("Host"),
Cell::from("Guest"),
Cell::from("Mode"),
Cell::from(""),
Cell::from("Default"),
])
.style(Style::default().fg(Color::Cyan));
let table_rows = rows.iter().map(|row| {
Row::new(vec![
Cell::from(row.host.clone()),
Cell::from(row.guest.clone()),
Cell::from(row.mode.clone()),
Cell::from(""),
Cell::from(row.default_mount.clone()),
])
});
let table = Table::new(
table_rows,
[
Constraint::Min(24),
Constraint::Min(24),
Constraint::Length(10),
Constraint::Length(1),
Constraint::Length(8),
],
)
.header(header)
.block(Block::default().title("Mounts").borders(Borders::ALL))
.column_spacing(1);
table.render(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 passthrough_vm_io(
app: Arc<Mutex<AppState>>,
output_monitor: Arc<vm::OutputMonitor>,