on main help sign in
feat: added explain command to display mounts
94d941crobcholz
docs/tasks.md+2−2
@@ -42,11 +42,11 @@
425. [x] wire up SessionManager.
425. [x] wire up SessionManager.
436. [x] VM should be separated by a per-session VM daemon process (only accepts if to shut down vm and itself).
436. [x] VM should be separated by a per-session VM daemon process (only accepts if to shut down vm and itself).
447. [x] setup vibebox commands
447. [x] setup vibebox commands
45−8. [ ] setup cli commands.
45+8. [x] setup cli commands.
46 1. [x] Organize all the params.
46 1. [x] Organize all the params.
47 2. [x] Remove old cli.
47 2. [x] Remove old cli.
48 3. [x] add an actual config file.
48 3. [x] add an actual config file.
49− 4. [ ] set up the cli.
49+ 4. [x] set up the cli.
509. [ ] fix ui overlap, and consistency issue.
509. [ ] fix ui overlap, and consistency issue.
5110. [ ] intensive integration test.
5110. [ ] intensive integration test.
52
52
src/bin/vibebox-cli.rs+109−3
@@ -2,7 +2,7 @@ use std::{
2 env,
2 env,
3 ffi::OsString,
3 ffi::OsString,
4 io::{self, IsTerminal, Write},
4 io::{self, IsTerminal, Write},
5− path::PathBuf,
5+ path::{Path, PathBuf},
6 sync::{Arc, Mutex},
6 sync::{Arc, Mutex},
7};
7};
8
8
@@ -32,6 +32,8 @@ enum Command {
32 List,
32 List,
33 /// Delete the current project's .vibebox directory
33 /// Delete the current project's .vibebox directory
34 Clean,
34 Clean,
35+ /// Explain mounts and mappings
36+ Explain,
35}
37}
36
38
37fn main() -> Result<()> {
39fn main() -> Result<()> {
@@ -42,7 +44,7 @@ fn main() -> Result<()> {
42 let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
44 let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
43 tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
45 tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
44 if let Some(command) = cli.command {
46 if let Some(command) = cli.command {
45− return handle_command(command, &cwd);
47+ return handle_command(command, &cwd, cli.config.as_deref());
46 }
48 }
47
49
48 let config_override = cli.config.clone();
50 let config_override = cli.config.clone();
@@ -111,7 +113,7 @@ fn main() -> Result<()> {
111 Ok(())
113 Ok(())
112}
114}
113
115
114−fn handle_command(command: Command, cwd: &PathBuf) -> Result<()> {
116+fn handle_command(command: Command, cwd: &PathBuf, config_override: Option<&Path>) -> Result<()> {
115 match command {
117 match command {
116 Command::List => {
118 Command::List => {
117 let manager = SessionManager::new()?;
119 let manager = SessionManager::new()?;
@@ -164,6 +166,16 @@ fn handle_command(command: Command, cwd: &PathBuf) -> Result<()> {
164 );
166 );
165 Ok(())
167 Ok(())
166 }
168 }
169+ Command::Explain => {
170+ let config = config::load_config_with_path(cwd, config_override);
171+ let rows = build_mount_rows(cwd, &config)?;
172+ if rows.is_empty() {
173+ println!("No mounts configured.");
174+ return Ok(());
175+ }
176+ tui::render_mounts_table(&rows)?;
177+ Ok(())
178+ }
167 }
179 }
168}
180}
169
181
@@ -231,6 +243,100 @@ fn format_last_active(value: Option<&str>) -> String {
231 format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
243 format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
232}
244}
233
245
246+fn build_mount_rows(cwd: &Path, config: &config::Config) -> Result<Vec<tui::MountListRow>> {
247+ let mut rows = Vec::new();
248+ rows.extend(default_mounts(cwd)?);
249+ for spec in &config.box_cfg.mounts {
250+ rows.push(parse_mount_spec(cwd, spec, false)?);
251+ }
252+ Ok(rows)
253+}
254+
255+fn default_mounts(cwd: &Path) -> Result<Vec<tui::MountListRow>> {
256+ let project_name = cwd
257+ .file_name()
258+ .and_then(|name| name.to_str())
259+ .unwrap_or("project");
260+ let project_guest = format!("/root/{project_name}");
261+ let project_host = relative_to_home(&cwd.to_path_buf());
262+ let mut rows = vec![tui::MountListRow {
263+ host: project_host,
264+ guest: project_guest,
265+ mode: "read-write".to_string(),
266+ default_mount: "yes".to_string(),
267+ }];
268+
269+ let home = env::var("HOME")
270+ .map(PathBuf::from)
271+ .unwrap_or_else(|_| PathBuf::from("/"));
272+ let cache_home = env::var("XDG_CACHE_HOME")
273+ .map(PathBuf::from)
274+ .unwrap_or_else(|_| home.join(".cache"));
275+ let cache_dir = cache_home.join(session_manager::GLOBAL_CACHE_DIR_NAME);
276+ let guest_mise_cache = cache_dir.join(".guest-mise-cache");
277+ rows.push(tui::MountListRow {
278+ host: relative_to_home(&guest_mise_cache),
279+ guest: "/root/.local/share/mise".to_string(),
280+ mode: "read-write".to_string(),
281+ default_mount: "yes".to_string(),
282+ });
283+ Ok(rows)
284+}
285+
286+fn parse_mount_spec(cwd: &Path, spec: &str, default_mount: bool) -> Result<tui::MountListRow> {
287+ let parts: Vec<&str> = spec.split(':').collect();
288+ if parts.len() < 2 || parts.len() > 3 {
289+ return Err(color_eyre::eyre::eyre!("invalid mount spec: {spec}"));
290+ }
291+ let host_part = parts[0];
292+ let guest_part = parts[1];
293+ let mode = if parts.len() == 3 {
294+ match parts[2] {
295+ "read-only" => "read-only",
296+ "read-write" => "read-write",
297+ other => {
298+ return Err(color_eyre::eyre::eyre!(
299+ "invalid mount mode '{}'; expected read-only or read-write",
300+ other
301+ ));
302+ }
303+ }
304+ } else {
305+ "read-write"
306+ };
307+ let host_path = resolve_host_path(cwd, host_part);
308+ let host_display = relative_to_home(&host_path);
309+ let guest_display = if Path::new(guest_part).is_absolute() {
310+ guest_part.to_string()
311+ } else {
312+ format!("/root/{guest_part}")
313+ };
314+ Ok(tui::MountListRow {
315+ host: host_display,
316+ guest: guest_display,
317+ mode: mode.to_string(),
318+ default_mount: if default_mount { "yes" } else { "no" }.to_string(),
319+ })
320+}
321+
322+fn resolve_host_path(cwd: &Path, host: &str) -> PathBuf {
323+ if let Some(stripped) = host.strip_prefix("~/") {
324+ if let Ok(home) = env::var("HOME") {
325+ return PathBuf::from(home).join(stripped);
326+ }
327+ } else if host == "~" {
328+ if let Ok(home) = env::var("HOME") {
329+ return PathBuf::from(home);
330+ }
331+ }
332+ let host_path = PathBuf::from(host);
333+ if host_path.is_absolute() {
334+ host_path
335+ } else {
336+ cwd.join(host_path)
337+ }
338+}
339+
234fn init_tracing() {
340fn init_tracing() {
235 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
341 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
236 let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err();
342 let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err();
src/tui.rs+60
@@ -107,6 +107,14 @@ pub struct SessionListRow {
107 pub id: String,
107 pub id: String,
108}
108}
109
109
110+#[derive(Debug, Clone)]
111+pub struct MountListRow {
112+ pub host: String,
113+ pub guest: String,
114+ pub mode: String,
115+ pub default_mount: String,
116+}
117+
110#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
118#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
111struct PageLayout {
119struct PageLayout {
112 header: Rect,
120 header: Rect,
@@ -235,6 +243,58 @@ pub fn render_sessions_table(rows: &[SessionListRow]) -> Result<()> {
235 Ok(())
243 Ok(())
236}
244}
237
245
246+pub fn render_mounts_table(rows: &[MountListRow]) -> Result<()> {
247+ let (width, _) = crossterm::terminal::size()?;
248+ if width == 0 {
249+ return Ok(());
250+ }
251+
252+ let height = (rows.len() as u16).saturating_add(3);
253+ let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
254+ let area = Rect::new(0, 0, width, height);
255+
256+ let header = Row::new(vec![
257+ Cell::from("Host"),
258+ Cell::from("Guest"),
259+ Cell::from("Mode"),
260+ Cell::from(""),
261+ Cell::from("Default"),
262+ ])
263+ .style(Style::default().fg(Color::Cyan));
264+
265+ let table_rows = rows.iter().map(|row| {
266+ Row::new(vec![
267+ Cell::from(row.host.clone()),
268+ Cell::from(row.guest.clone()),
269+ Cell::from(row.mode.clone()),
270+ Cell::from(""),
271+ Cell::from(row.default_mount.clone()),
272+ ])
273+ });
274+
275+ let table = Table::new(
276+ table_rows,
277+ [
278+ Constraint::Min(24),
279+ Constraint::Min(24),
280+ Constraint::Length(10),
281+ Constraint::Length(1),
282+ Constraint::Length(8),
283+ ],
284+ )
285+ .header(header)
286+ .block(Block::default().title("Mounts").borders(Borders::ALL))
287+ .column_spacing(1);
288+
289+ table.render(area, &mut buffer);
290+
291+ let mut stdout = io::stdout();
292+ execute!(stdout, Clear(ClearType::All), MoveTo(0, 0), Show)?;
293+ write_buffer_with_style(&buffer, &mut stdout)?;
294+ stdout.flush()?;
295+ Ok(())
296+}
297+
238pub fn passthrough_vm_io(
298pub fn passthrough_vm_io(
239 app: Arc<Mutex<AppState>>,
299 app: Arc<Mutex<AppState>>,
240 output_monitor: Arc<vm::OutputMonitor>,
300 output_monitor: Arc<vm::OutputMonitor>,