on main help sign in
feat: wired up vm and tui
commit52bcd88d7e293636464ac5b3bc5d1aadb2406912 authorrobcholz <84130577+robcholz@users.noreply.github.com> dateFri 6 Feb 2026, 17:04 (8 months ago) parente3ba0a0
7 files changed+240−294
docs/implementations.md+11−1 docs/tasks.md+6−3 src/bin/vibebox-cli.rs+49 src/bin/vibebox-tui.rs−247 src/lib.rs+5 src/tui.rs+21 src/main.rs → src/vm.rs+148−43
docs/implementations.md+11−1
@@ -88,4 +88,14 @@ They are also stored per project, in `vibebox.toml`
8888- use `:explain` to display:
8989 - mounts: host_path → guest_path, ro/rw
9090 - network: mode (allowlist/blocklist) and entries
91− - storage: paths to vibebox.toml and .vibebox/ (relative from the project_dir)
91+ - storage: paths to vibebox.toml and .vibebox/ (relative from the project_dir)
92+
93+## Connection
94+
95+### SSH
96+
97+- In Project cache, generate and store ssh pair
98+- In provisioning, install and enable openssh-server in VM
99+- Mount ssh pair to VM when starting up
100+- get ipv4 address of VM, store it to project cache
101+- and connect to VM via ssh with ip and ssh key
docs/tasks.md+6−3
@@ -29,10 +29,13 @@
2929
3030## TUI
3131
32−1. [ ] Fix the terminal component height issue.
33−2. [ ] Fix the input field that does not expand its height (currently, it just roll the text horizontally). The
32+1. [x] Fix the terminal component height issue.
33+2. [x] Fix the input field that does not expand its height (currently, it just roll the text horizontally). The
3434 inputfield it should not be scrollable.
3535
3636## Integration
3737
38−1. [ ] Integrate VM and SessionManager together.
38+1. [x] Wire up the vm and tui.
39+2. [ ] Use ssh to connect to vm.
40+3. [ ] wire up SessionManager.
41+4. [ ] VM should be separated by per-session VM daemon process (only accepts if to shutdown vm and itself).
src/bin/vibebox-cli.rsnew file+49
@@ -0,0 +1,49 @@
1+use std::{
2+ env,
3+ io::{self, Write},
4+ sync::{Arc, Mutex},
5+};
6+
7+use color_eyre::Result;
8+
9+use vibebox::tui::{AppState, VmInfo};
10+use vibebox::{tui, vm};
11+
12+fn main() -> Result<()> {
13+ color_eyre::install()?;
14+
15+ let args = vm::parse_cli().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
16+ if args.version() {
17+ vm::print_version();
18+ return Ok(());
19+ }
20+ if args.help() {
21+ vm::print_help();
22+ return Ok(());
23+ }
24+
25+ let vm_info = VmInfo {
26+ version: env!("CARGO_PKG_VERSION").to_string(),
27+ max_memory_mb: args.ram_mb(),
28+ cpu_cores: args.cpu_count(),
29+ };
30+ let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
31+ let app = Arc::new(Mutex::new(AppState::new(cwd, vm_info)));
32+
33+ {
34+ let mut locked = app.lock().expect("app state poisoned");
35+ tui::render_tui_once(&mut locked)?;
36+ }
37+ {
38+ let mut stdout = io::stdout().lock();
39+ writeln!(stdout)?;
40+ stdout.flush()?;
41+ }
42+
43+ vm::run_with_args(args, |output_monitor, vm_output_fd, vm_input_fd| {
44+ tui::passthrough_vm_io(app.clone(), output_monitor, vm_output_fd, vm_input_fd)
45+ })
46+ .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
47+
48+ Ok(())
49+}
src/bin/vibebox-tui.rsdeleted−247
@@ -1,247 +0,0 @@
1−use std::{
2− env,
3− ffi::OsString,
4− io::{self, Read, Write},
5− path::PathBuf,
6−};
7−
8−use color_eyre::Result;
9−use lexopt::prelude::*;
10−
11−#[path = "../tui.rs"]
12−mod tui;
13−
14−use tui::{AppState, VmInfo};
15−
16−#[derive(Debug, Clone, PartialEq, Eq)]
17−struct TuiConfig {
18− cwd: PathBuf,
19− vm_version: String,
20− max_memory_mb: u64,
21− cpu_cores: usize,
22−}
23−
24−#[derive(Debug, Clone, PartialEq, Eq)]
25−enum TuiCommand {
26− Run(TuiConfig),
27− Help,
28− Version,
29−}
30−
31−#[derive(Debug, thiserror::Error)]
32−enum CliError {
33− #[error("{0}")]
34− Message(String),
35− #[error(transparent)]
36− Lexopt(#[from] lexopt::Error),
37− #[error(transparent)]
38− Io(#[from] std::io::Error),
39−}
40−
41−fn main() -> Result<()> {
42− color_eyre::install()?;
43−
44− let command = parse_args(env::args_os())?;
45− match command {
46− TuiCommand::Help => {
47− print_help();
48− }
49− TuiCommand::Version => {
50− println!("vibebox-tui {}", env!("CARGO_PKG_VERSION"));
51− }
52− TuiCommand::Run(config) => {
53− let vm_info = VmInfo {
54− version: config.vm_version,
55− max_memory_mb: config.max_memory_mb,
56− cpu_cores: config.cpu_cores,
57− };
58− let mut app = AppState::new(config.cwd, vm_info);
59− tui::render_tui_once(&mut app)?;
60− {
61− let mut stdout = io::stdout().lock();
62− writeln!(stdout)?;
63− stdout.flush()?;
64− }
65− passthrough_stdio(&mut app)?;
66− }
67− }
68−
69− Ok(())
70−}
71−
72−fn passthrough_stdio(app: &mut AppState) -> io::Result<()> {
73− let mut stdin = io::stdin().lock();
74− let mut buf = [0u8; 8192];
75− let mut line_buf: Vec<u8> = Vec::new();
76− loop {
77− let n = stdin.read(&mut buf)?;
78− if n == 0 {
79− break;
80− }
81− for &b in &buf[..n] {
82− line_buf.push(b);
83− if b == b'\n' {
84− let line = String::from_utf8_lossy(&line_buf);
85− let trimmed = line.trim_end_matches(&['\r', '\n'][..]);
86− if trimmed == ":help" {
87− let _ = tui::render_commands_component(app);
88− }
89− line_buf.clear();
90− }
91− }
92− }
93− Ok(())
94−}
95−
96−fn print_help() {
97− println!(
98− "vibebox-tui\n\nUsage:\n vibebox-tui [options]\n\nOptions:\n --help, -h Show this help\n --version Show version\n --cwd <path> Working directory for the session header\n --vm-version <ver> VM version string for the header\n --max-memory <mb> Max memory in MB (default 2048)\n --cpu-cores <count> CPU core count (default 2)\n"
99− );
100−}
101−
102−fn parse_args<I>(args: I) -> Result<TuiCommand, CliError>
103−where
104− I: IntoIterator<Item = OsString>,
105−{
106− fn os_to_string(value: OsString, flag: &str) -> Result<String, CliError> {
107− value
108− .into_string()
109− .map_err(|_| CliError::Message(format!("{flag} expects valid UTF-8")))
110− }
111−
112− let mut parser = lexopt::Parser::from_iter(args);
113− let mut cwd: Option<PathBuf> = None;
114− let mut vm_version = env!("CARGO_PKG_VERSION").to_string();
115− let mut max_memory_mb: u64 = 2048;
116− let mut cpu_cores: usize = 2;
117−
118− while let Some(arg) = parser.next()? {
119− match arg {
120− Long("help") | Short('h') => return Ok(TuiCommand::Help),
121− Long("version") => return Ok(TuiCommand::Version),
122− Long("cwd") => {
123− let value = os_to_string(parser.value()?, "--cwd")?;
124− cwd = Some(PathBuf::from(value));
125− }
126− Long("vm-version") => {
127− vm_version = os_to_string(parser.value()?, "--vm-version")?;
128− }
129− Long("max-memory") => {
130− let value: u64 = os_to_string(parser.value()?, "--max-memory")?
131− .parse()
132− .map_err(|_| {
133− CliError::Message("--max-memory expects an integer".to_string())
134− })?;
135− if value == 0 {
136− return Err(CliError::Message("--max-memory must be >= 1".to_string()));
137− }
138− max_memory_mb = value;
139− }
140− Long("cpu-cores") => {
141− let value: usize = os_to_string(parser.value()?, "--cpu-cores")?
142− .parse()
143− .map_err(|_| CliError::Message("--cpu-cores expects an integer".to_string()))?;
144− if value == 0 {
145− return Err(CliError::Message("--cpu-cores must be >= 1".to_string()));
146− }
147− cpu_cores = value;
148− }
149− Value(value) => {
150− return Err(CliError::Message(format!(
151− "unexpected argument: {}",
152− value.to_string_lossy()
153− )));
154− }
155− _ => return Err(CliError::Message(arg.unexpected().to_string())),
156− }
157− }
158−
159− let cwd = match cwd {
160− Some(dir) => dir,
161− None => env::current_dir()?,
162− };
163−
164− Ok(TuiCommand::Run(TuiConfig {
165− cwd,
166− vm_version,
167− max_memory_mb,
168− cpu_cores,
169− }))
170−}
171−
172−#[cfg(test)]
173−mod tests {
174− use super::*;
175−
176− fn parse_from(args: &[&str]) -> Result<TuiCommand, CliError> {
177− let mut argv = vec![OsString::from("vibebox-tui")];
178− argv.extend(args.iter().map(OsString::from));
179− parse_args(argv)
180− }
181−
182− #[test]
183− fn parse_help_short_circuit() {
184− let command = parse_from(&["--help"]).unwrap();
185− assert!(matches!(command, TuiCommand::Help));
186− }
187−
188− #[test]
189− fn parse_version_short_circuit() {
190− let command = parse_from(&["--version"]).unwrap();
191− assert!(matches!(command, TuiCommand::Version));
192− }
193−
194− #[test]
195− fn parse_defaults() {
196− let command = parse_from(&[]).unwrap();
197− let TuiCommand::Run(config) = command else {
198− panic!("expected run command");
199− };
200−
201− assert_eq!(config.vm_version, env!("CARGO_PKG_VERSION"));
202− assert_eq!(config.max_memory_mb, 2048);
203− assert_eq!(config.cpu_cores, 2);
204− }
205−
206− #[test]
207− fn parse_overrides() {
208− let command = parse_from(&[
209− "--cwd",
210− "/tmp",
211− "--vm-version",
212− "13.1",
213− "--max-memory",
214− "4096",
215− "--cpu-cores",
216− "4",
217− ])
218− .unwrap();
219−
220− let TuiCommand::Run(config) = command else {
221− panic!("expected run command");
222− };
223−
224− assert_eq!(config.cwd, PathBuf::from("/tmp"));
225− assert_eq!(config.vm_version, "13.1");
226− assert_eq!(config.max_memory_mb, 4096);
227− assert_eq!(config.cpu_cores, 4);
228− }
229−
230− #[test]
231− fn parse_rejects_zero_cpu() {
232− let err = parse_from(&["--cpu-cores", "0"]).unwrap_err();
233− assert!(err.to_string().contains("cpu-cores"));
234− }
235−
236− #[test]
237− fn parse_rejects_zero_memory() {
238− let err = parse_from(&["--max-memory", "0"]).unwrap_err();
239− assert!(err.to_string().contains("max-memory"));
240− }
241−
242− #[test]
243− fn parse_rejects_unknown_argument() {
244− let err = parse_from(&["--unknown"]).unwrap_err();
245− assert!(!err.to_string().is_empty());
246− }
247−}
src/lib.rsnew file+5
@@ -0,0 +1,5 @@
1+pub mod session_manager;
2+pub mod tui;
3+pub mod vm;
4+
5+pub use session_manager::{SessionError, SessionManager, SessionRecord};
src/tui.rs+21
@@ -1,6 +1,8 @@
11use std::{
22 io::{self, Write},
3+ os::unix::io::OwnedFd,
34 path::PathBuf,
5+ sync::{Arc, Mutex},
46};
57
68use color_eyre::Result;
@@ -21,6 +23,8 @@ use ratatui::{
2123 widgets::{Block, Borders, List, ListItem, Paragraph, Widget},
2224};
2325
26+use crate::vm;
27+
2428const ASCII_BANNER: [&str; 7] = [
2529 "██╗ ██╗██╗██████╗ ███████╗██████╗ ██████╗ ██╗ ██╗",
2630 "██║ ██║██║██╔══██╗██╔════╝██╔══██╗██╔═══██╗╚██╗██╔╝",
@@ -170,6 +174,23 @@ pub fn render_commands_component(app: &mut AppState) -> Result<()> {
170174 Ok(())
171175}
172176
177+pub fn passthrough_vm_io(
178+ app: Arc<Mutex<AppState>>,
179+ output_monitor: Arc<vm::OutputMonitor>,
180+ vm_output_fd: OwnedFd,
181+ vm_input_fd: OwnedFd,
182+) -> vm::IoContext {
183+ vm::spawn_vm_io_with_line_handler(output_monitor, vm_output_fd, vm_input_fd, move |line| {
184+ if line == ":help" {
185+ if let Ok(mut locked) = app.lock() {
186+ let _ = render_commands_component(&mut locked);
187+ }
188+ return true;
189+ }
190+ false
191+ })
192+}
193+
173194fn render_static_buffer(app: &mut AppState, width: u16) -> Buffer {
174195 let layout = compute_page_layout(app, width);
175196 let content_height = layout.total_height.max(1);
src/main.rs → src/vm.rsrenamed+148−43
@@ -1,7 +1,3 @@
1−mod session_manager;
2−
3−pub use session_manager::{SessionError, SessionManager, SessionRecord};
4−
51use std::{
62 env,
73 ffi::OsString,
@@ -119,41 +115,26 @@ impl DirectoryShare {
119115 }
120116}
121117
122−fn main() -> Result<(), Box<dyn std::error::Error>> {
118+pub fn run_cli() -> Result<(), Box<dyn std::error::Error>> {
123119 let args = parse_cli()?;
124120
125− if args.version {
126− println!("Vibe");
127− println!("https://github.com/lynaghk/vibe/");
128− println!("Git SHA: {}", env!("GIT_SHA"));
129− std::process::exit(0);
121+ if args.version() {
122+ print_version();
123+ return Ok(());
130124 }
131125
132− if args.help {
133− println!(
134− "Vibe is a quick way to spin up a Linux virtual machine on Mac to sandbox LLM agents.
135−
136−vibe [OPTIONS] [disk-image.raw]
137−
138−Options
139−
140− --help Print this help message.
141− --version Print the version (commit SHA).
142− --no-default-mounts Disable all default mounts.
143− --mount host-path:guest-path[:read-only | :read-write] Mount `host-path` inside VM at `guest-path`.
144− Defaults to read-write.
145− Errors if host-path does not exist.
146− --cpus <count> Number of virtual CPUs (default {DEFAULT_CPU_COUNT}).
147− --ram <megabytes> RAM size in megabytes (default {DEFAULT_RAM_MB}).
148− --script <path/to/script.sh> Run script in VM.
149− --send <some-command> Type `some-command` followed by newline into the VM.
150− --expect <string> [timeout-seconds] Wait for `string` to appear in console output before executing next `--script` or `--send`.
151− If `string` does not appear within timeout (default 30 seconds), shutdown VM with error.
152−"
153− );
154− std::process::exit(0);
126+ if args.help() {
127+ print_help();
128+ return Ok(());
155129 }
156130
131+ run_with_args(args, spawn_vm_io)
132+}
133+
134+pub fn run_with_args<F>(args: CliArgs, io_handler: F) -> Result<(), Box<dyn std::error::Error>>
135+where
136+ F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
137+{
157138 ensure_signed();
158139
159140 let project_root = env::current_dir()?;
@@ -239,6 +220,11 @@ Options
239220 ),
240221 DirectoryShare::new(home.join(".codex"), "/root/.codex".into(), false),
241222 DirectoryShare::new(home.join(".claude"), "/root/.claude".into(), false),
223+ DirectoryShare::new(
224+ "/Users/zhangjie/Documents/Code/CompletePrograms/vibebox/.ssh".into(),
225+ "/root/.ssh".into(),
226+ true,
227+ ),
242228 ]
243229 .into_iter()
244230 .flatten()
@@ -258,16 +244,47 @@ Options
258244 // Any user-provided login actions must come after our system ones
259245 login_actions.extend(args.login_actions);
260246
261− run_vm(
247+ run_vm_with_io(
262248 &disk_path,
263249 &login_actions,
264250 &directory_shares[..],
265251 args.cpu_count,
266252 args.ram_bytes,
253+ io_handler,
267254 )
268255}
269256
270−struct CliArgs {
257+pub fn print_help() {
258+ println!(
259+ "Vibe is a quick way to spin up a Linux virtual machine on Mac to sandbox LLM agents.
260+
261+vibe [OPTIONS] [disk-image.raw]
262+
263+Options
264+
265+ --help Print this help message.
266+ --version Print the version (commit SHA).
267+ --no-default-mounts Disable all default mounts.
268+ --mount host-path:guest-path[:read-only | :read-write] Mount `host-path` inside VM at `guest-path`.
269+ Defaults to read-write.
270+ Errors if host-path does not exist.
271+ --cpus <count> Number of virtual CPUs (default {DEFAULT_CPU_COUNT}).
272+ --ram <megabytes> RAM size in megabytes (default {DEFAULT_RAM_MB}).
273+ --script <path/to/script.sh> Run script in VM.
274+ --send <some-command> Type `some-command` followed by newline into the VM.
275+ --expect <string> [timeout-seconds] Wait for `string` to appear in console output before executing next `--script` or `--send`.
276+ If `string` does not appear within timeout (default 30 seconds), shutdown VM with error.
277+"
278+ );
279+}
280+
281+pub fn print_version() {
282+ println!("Vibe");
283+ println!("https://github.com/lynaghk/vibe/");
284+ println!("Git SHA: {}", env!("GIT_SHA"));
285+}
286+
287+pub struct CliArgs {
271288 disk: Option<PathBuf>,
272289 version: bool,
273290 help: bool,
@@ -278,14 +295,43 @@ struct CliArgs {
278295 ram_bytes: u64,
279296}
280297
281−fn parse_cli() -> Result<CliArgs, Box<dyn std::error::Error>> {
298+impl CliArgs {
299+ pub fn version(&self) -> bool {
300+ self.version
301+ }
302+
303+ pub fn help(&self) -> bool {
304+ self.help
305+ }
306+
307+ pub fn cpu_count(&self) -> usize {
308+ self.cpu_count
309+ }
310+
311+ pub fn ram_bytes(&self) -> u64 {
312+ self.ram_bytes
313+ }
314+
315+ pub fn ram_mb(&self) -> u64 {
316+ self.ram_bytes / BYTES_PER_MB
317+ }
318+}
319+
320+pub fn parse_cli() -> Result<CliArgs, Box<dyn std::error::Error>> {
321+ parse_cli_from(env::args_os())
322+}
323+
324+pub fn parse_cli_from<I>(args: I) -> Result<CliArgs, Box<dyn std::error::Error>>
325+where
326+ I: IntoIterator<Item = OsString>,
327+{
282328 fn os_to_string(value: OsString, flag: &str) -> Result<String, Box<dyn std::error::Error>> {
283329 value
284330 .into_string()
285331 .map_err(|_| format!("{flag} expects valid UTF-8").into())
286332 }
287333
288− let mut parser = lexopt::Parser::from_env();
334+ let mut parser = lexopt::Parser::from_iter(args);
289335 let mut disk = None;
290336 let mut version = false;
291337 let mut help = false;
@@ -621,11 +667,15 @@ pub fn create_pipe() -> (OwnedFd, OwnedFd) {
621667 (read_stream.into(), write_stream.into())
622668}
623669
624−pub fn spawn_vm_io(
670+pub fn spawn_vm_io_with_line_handler<F>(
625671 output_monitor: Arc<OutputMonitor>,
626672 vm_output_fd: OwnedFd,
627673 vm_input_fd: OwnedFd,
628−) -> IoContext {
674+ mut on_line: F,
675+) -> IoContext
676+where
677+ F: FnMut(&str) -> bool + ::std::marker::Send + 'static,
678+{
629679 let (input_tx, input_rx): (Sender<VmInput>, Receiver<VmInput>) = mpsc::channel();
630680
631681 // raw_guard is set when we've put the user's terminal into raw mode because we've attached stdin/stdout to the VM.
@@ -679,15 +729,41 @@ pub fn spawn_vm_io(
679729
680730 move || {
681731 let mut buf = [0u8; 1024];
732+ let mut pending_command: Vec<u8> = Vec::new();
733+ let mut command_mode = false;
682734 loop {
683735 match poll_with_wakeup(libc::STDIN_FILENO, wakeup_read.as_raw_fd(), &mut buf) {
684736 PollResult::Shutdown | PollResult::Error => break,
685737 PollResult::Spurious => continue,
686738 PollResult::Ready(bytes) => {
739+ let mut send_buf: Vec<u8> = Vec::new();
740+ for &b in bytes {
741+ if pending_command.is_empty() && !command_mode && b == b':' {
742+ command_mode = true;
743+ }
744+
745+ if command_mode {
746+ pending_command.push(b);
747+ } else {
748+ send_buf.push(b);
749+ }
750+
751+ if b == b'\n' && command_mode {
752+ let line = String::from_utf8_lossy(&pending_command);
753+ let trimmed = line.trim_end_matches(&['\r', '\n'][..]);
754+ let consumed = on_line(trimmed);
755+ if !consumed {
756+ send_buf.extend_from_slice(&pending_command);
757+ }
758+ pending_command.clear();
759+ command_mode = false;
760+ }
761+ }
687762 if raw_guard.lock().unwrap().is_none() {
688763 continue;
689764 }
690− if input_tx.send(VmInput::Bytes(bytes.to_vec())).is_err() {
765+ if !send_buf.is_empty() && input_tx.send(VmInput::Bytes(send_buf)).is_err()
766+ {
691767 break;
692768 }
693769 }
@@ -753,6 +829,14 @@ pub fn spawn_vm_io(
753829 }
754830}
755831
832+pub fn spawn_vm_io(
833+ output_monitor: Arc<OutputMonitor>,
834+ vm_output_fd: OwnedFd,
835+ vm_input_fd: OwnedFd,
836+) -> IoContext {
837+ spawn_vm_io_with_line_handler(output_monitor, vm_output_fd, vm_input_fd, |_| false)
838+}
839+
756840impl IoContext {
757841 pub fn shutdown(self) {
758842 let _ = self.input_tx.send(VmInput::Shutdown);
@@ -954,13 +1038,17 @@ fn spawn_login_actions_thread(
9541038 })
9551039}
9561040
957−fn run_vm(
1041+fn run_vm_with_io<F>(
9581042 disk_path: &Path,
9591043 login_actions: &[LoginAction],
9601044 directory_shares: &[DirectoryShare],
9611045 cpu_count: usize,
9621046 ram_bytes: u64,
963−) -> Result<(), Box<dyn std::error::Error>> {
1047+ io_handler: F,
1048+) -> Result<(), Box<dyn std::error::Error>>
1049+where
1050+ F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
1051+{
9641052 let (vm_reads_from, we_write_to) = create_pipe();
9651053 let (we_read_from, vm_writes_to) = create_pipe();
9661054
@@ -1021,7 +1109,7 @@ fn run_vm(
10211109 println!("VM booting...");
10221110
10231111 let output_monitor = Arc::new(OutputMonitor::default());
1024− let io_ctx = spawn_vm_io(output_monitor.clone(), we_read_from, we_write_to);
1112+ let io_ctx = io_handler(output_monitor.clone(), we_read_from, we_write_to);
10251113
10261114 let mut all_login_actions = vec![
10271115 Expect {
@@ -1112,6 +1200,23 @@ fn run_vm(
11121200 exit_result
11131201}
11141202
1203+fn run_vm(
1204+ disk_path: &Path,
1205+ login_actions: &[LoginAction],
1206+ directory_shares: &[DirectoryShare],
1207+ cpu_count: usize,
1208+ ram_bytes: u64,
1209+) -> Result<(), Box<dyn std::error::Error>> {
1210+ run_vm_with_io(
1211+ disk_path,
1212+ login_actions,
1213+ directory_shares,
1214+ cpu_count,
1215+ ram_bytes,
1216+ spawn_vm_io,
1217+ )
1218+}
1219+
11151220fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>, Box<dyn std::error::Error>> {
11161221 let abs_path = if path.is_absolute() {
11171222 path.to_path_buf()